155 lines
9.4 KiB
PHP
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 . ' 无效');
|
|
}
|
|
}
|
|
}
|