Files
zyt/server/app/adminapi/validate/tcm/AssistantServiceValidate.php
T
2026-11-12 09:07:07 +08:00

155 lines
9.4 KiB
PHP

<?php
declare(strict_types=1);
namespace app\adminapi\validate\tcm;
use DomainException;
/** 保留 JSON 的类型,拒绝数组文本、浮点 ID、未知参数和未确认转写。 */
final class AssistantServiceValidate
{
public static function parameters(string $action, array $input): array
{
$fields = [
'capabilities' => [], '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 . ' 无效');
}
}
}