[], 'patients' => ['keyword', 'page_no', 'page_size'], 'staff' => ['keyword', 'page_no', 'page_size'], 'teamOverview' => ['keyword', 'filter', 'page_no', 'page_size'], 'staffOverview' => ['assistant_id', 'days'], 'staffCases' => ['assistant_id', 'days', 'keyword', 'page_no', 'page_size'], 'staffIssues' => ['assistant_id', 'days', 'page_no', 'page_size'], 'staffDailyContacts' => ['assistant_id', 'status', 'page_no', 'page_size'], 'alerts' => ['keyword', 'status', 'severity', 'role', 'page_no', 'page_size'], 'wecomDirectory' => [], 'wecomChats' => ['assistant_id', 'account'], 'wecomMessages' => ['assistant_id', 'account', 'conversation_id', 'cursor'], 'reviewAlert' => ['id', 'status', 'note'], 'analyzeStaff' => ['assistant_id', 'days', 'scenario', 'diagnosis_ids'], 'context' => ['diagnosis_id', 'days', 'assistant_id'], 'record' => ['diagnosis_id', 'content', 'occurred_at', 'record_type', 'speaker_confirmed'], 'analyze' => ['diagnosis_id', 'days', 'scenario', 'assistant_id'], 'draftReply' => ['diagnosis_id', 'days', 'scenario', 'question', 'assistant_id'], 'job' => ['id'], 'jobs' => ['diagnosis_id', 'status', 'assistant_id', 'page_no', 'page_size'], 'reviewQuality' => ['job_id', 'item_key', 'decision', 'note', 'version'], 'adoptAction' => ['job_id', 'action_key', 'version', 'due_at', 'content', 'contact_required'], 'actions' => ['diagnosis_id', 'assistant_id', 'state', 'page_no', 'page_size'], 'completeAction' => ['id', 'state', 'note'], 'knowledge' => ['keyword', 'status', 'page_no', 'page_size'], 'saveKnowledge' => ['id', 'version', 'question', 'answer', 'category', 'source_note'], 'publishKnowledge' => ['id', 'version', 'status'], ]; if (!isset($fields[$action]) || array_diff(array_keys($input), $fields[$action])) { throw new DomainException('请求包含不支持的字段'); } $p = $input; if (in_array('assistant_id', $fields[$action], true)) { $required = in_array($action, ['staffOverview', 'staffCases', 'staffIssues', 'staffDailyContacts', 'analyzeStaff'], true); $p['assistant_id'] = self::integer($p['assistant_id'] ?? ($required ? null : 0), '医助', $required ? 1 : 0, PHP_INT_MAX); } if ($action === 'analyzeStaff') { if (!is_array($p['diagnosis_ids'] ?? null) || array_keys($p['diagnosis_ids']) !== range(0, count($p['diagnosis_ids']) - 1) || count($p['diagnosis_ids']) < 1 || count($p['diagnosis_ids']) > 10) { throw new DomainException('请勾选 1 至 10 条沟通记录'); } $p['diagnosis_ids'] = array_map(static fn ($id): int => self::integer($id, '诊单', 1, PHP_INT_MAX), $p['diagnosis_ids']); if (count(array_unique($p['diagnosis_ids'])) !== count($p['diagnosis_ids'])) { throw new DomainException('不能重复勾选同一诊单'); } } foreach (['id', 'job_id', 'version', 'diagnosis_id', 'occurred_at', 'due_at'] as $key) { if (in_array($key, $fields[$action], true)) { $optional = ($key === 'diagnosis_id' && in_array($action, ['jobs', 'actions'], true)) || ($action === 'saveKnowledge' && in_array($key, ['id', 'version'], true)); $p[$key] = self::integer($p[$key] ?? ($optional ? 0 : null), $key, $optional ? 0 : 1, PHP_INT_MAX); } } if ($action === 'saveKnowledge' && $p['id'] > 0 && $p['version'] <= 0) { throw new DomainException('更新知识必须提供当前版本'); } if (in_array('page_no', $fields[$action], true)) { $p['page_no'] = self::integer(array_key_exists('page_no', $p) ? $p['page_no'] : 1, '页码', 1, 100000); $p['page_size'] = self::integer(array_key_exists('page_size', $p) ? $p['page_size'] : 20, '每页数量', 1, 50); } if (in_array('days', $fields[$action], true)) { $p['days'] = self::integer($p['days'] ?? 30, '时间范围', 7, 90); self::oneOf($p['days'], [7, 30, 90], '时间范围'); } $limits = ['keyword' => 100, 'question' => $action === 'saveKnowledge' ? 300 : (int) config('assistant_service.max_question_chars', 1000), 'answer' => 10000, 'category' => 64, 'source_note' => 1000, 'note' => $action === 'reviewAlert' ? 200 : 1000, 'content' => $action === 'record' ? (int) config('assistant_service.max_manual_chars', 10000) : 500, 'item_key' => 64, 'action_key' => 64, 'account' => 64, 'conversation_id' => 80, 'cursor' => 200]; foreach ($limits as $key => $limit) { if (in_array($key, $fields[$action], true)) { $required = ($key === 'content' && $action === 'record') || ($key === 'conversation_id' && $action === 'wecomMessages') || in_array($key, ['question', 'answer', 'item_key', 'action_key'], true); $p[$key] = self::text($p[$key] ?? '', $key, $limit, $required); } } $enums = [ 'filter' => ['all', 'issues', 'overdue', 'attention', 'today_pending', 'today_done'], 'scenario' => ['initial', 'followup', 'revisit'], 'record_type' => ['service_note', 'verified_transcript'], 'decision' => ['confirmed', 'dismissed', 'not_applicable'], 'severity' => ['all', 'high', 'medium', 'low'], 'role' => ['all', 'assistant', 'doctor'], 'state' => $action === 'actions' ? ['', 'pending', 'in_progress', 'completed', 'cancelled'] : ['in_progress', 'completed', 'cancelled'], 'status' => $action === 'jobs' ? ['', 'queued', 'running', 'ready', 'failed', 'needs_reconciliation', 'stale'] : ($action === 'staffDailyContacts' ? ['all', 'pending', 'reported', 'contacted', 'exempt'] : ($action === 'alerts' ? ['all', 'open', 'confirmed', 'false_positive', 'followed'] : ($action === 'reviewAlert' ? ['confirmed', 'false_positive', 'followed'] : ($action === 'knowledge' ? ['', 'draft', 'published', 'disabled'] : ['published', 'disabled'])))), ]; foreach ($enums as $key => $values) { if (in_array($key, $fields[$action], true)) { $p[$key] = array_key_exists($key, $p) ? $p[$key] : ($key === 'filter' ? 'all' : ($action === 'staffDailyContacts' && $key === 'status' ? 'pending' : ($action === 'alerts' && $key === 'status' ? 'open' : ($action === 'alerts' && in_array($key, ['severity', 'role'], true) ? 'all' : '')))); self::oneOf($p[$key], $values, $key); } } if ($action === 'reviewAlert' && $p['status'] === 'false_positive' && $p['note'] === '') { throw new DomainException('误报需要填写原因'); } if ($action === 'adoptAction') { if (array_key_exists('contact_required', $p) && !is_bool($p['contact_required'])) { throw new DomainException('需要联系患者标记必须是布尔值'); } $p['contact_required'] = $p['contact_required'] ?? false; } if ($action === 'record') { if (!is_bool($p['speaker_confirmed'] ?? false)) { throw new DomainException('说话人确认必须是布尔值'); } $p['speaker_confirmed'] = $p['speaker_confirmed'] ?? false; if ($p['record_type'] === 'verified_transcript' && !$p['speaker_confirmed']) { throw new DomainException('请先明确确认转写内容及说话人'); } if ($p['occurred_at'] > time() + 300) { throw new DomainException('服务记录时间不能在未来'); } } if ($action === 'completeAction' && in_array($p['state'], ['completed', 'cancelled'], true) && $p['note'] === '') { throw new DomainException('完成或取消时必须填写执行结果或原因'); } return $p; } private static function integer($value, string $label, int $min, int $max): int { if (!(is_int($value) || is_string($value)) || !preg_match('/^(0|[1-9][0-9]{0,17})$/D', (string) $value) || (int) $value < $min || (int) $value > $max) { throw new DomainException($label . ' 无效'); } return (int) $value; } private static function text($value, string $label, int $max, bool $required): string { if (!is_string($value) || mb_strlen($value) > max(1, $max) || ($required && trim($value) === '')) { throw new DomainException($label . ' 为空或超过长度限制'); } return trim($value); } private static function oneOf($value, array $allowed, string $label): void { if (!in_array($value, $allowed, true)) { throw new DomainException($label . ' 无效'); } } }