This commit is contained in:
Your Name
2026-08-17 11:57:33 +08:00
parent c1eee2d28a
commit 94b4821d4c
103 changed files with 354 additions and 47247 deletions
@@ -47,10 +47,10 @@ class DiagnosisValidate extends BaseValidate
'revisit_slot_start_offset' => 'integer|between:0,20',
'report_id' => 'number|gt:0',
'content' => 'max:12000',
'task' => 'require|in:summary,tcm_pattern,prescription_review,medication_review,exam_review,complication_risk,guideline_review,custom',
'prompt' => 'max:500',
'model' => 'in:qwen,openai',
'patient_id' => 'integer|gt:0',
'task' => 'require|in:summary,tcm_pattern,prescription_review,medication_review,exam_review,complication_risk,guideline_review,custom',
'prompt' => 'max:500',
'model' => 'in:qwen,openai',
'patient_id' => 'integer|gt:0',
];
protected $message = [
@@ -83,21 +83,27 @@ class DiagnosisValidate extends BaseValidate
'report_id.gt' => '报告ID必须大于0',
'content.require' => '报告内容不能为空',
'content.max' => '报告内容最多12000个字符',
'task.require' => '请选择 AI 助手任务',
'task.in' => 'AI 助手任务不受支持',
'prompt.max' => '问题最多500个字符',
'model.in' => 'AI模型仅支持qwen或openai',
'patient_id.require' => '患者ID不能为空',
'patient_id.integer' => '患者ID必须为整数',
'patient_id.gt' => '患者ID必须大于0',
'task.require' => '请选择 AI 助手任务',
'task.in' => 'AI 助手任务不受支持',
'prompt.max' => '问题最多500个字符',
'model.in' => 'AI模型仅支持qwen或openai',
'patient_id.require' => '患者ID不能为空',
'patient_id.integer' => '患者ID必须为整数',
'patient_id.gt' => '患者ID必须大于0',
];
public function sceneAdd()
{
// 全局规则中含 tracking 相关字段,新增诊单不应校验诊单 ID
return $this->remove('id', true)
->remove('diagnosis_id', true)
->remove('tracking_content', true);
// 与 sceneEdit 一样用 only() 白名单,避免全局规则里的 AI 助手 / 跟踪备注字段泄漏到新增诊单
return $this->only([
'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type',
'syndrome_type', 'marital_status', 'height', 'weight', 'region', 'systolic_pressure',
'diastolic_pressure', 'fasting_blood_sugar', 'diabetes_discovery_year',
'local_hospital_diagnosis', 'local_hospital_name', 'past_history', 'symptoms',
'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice',
'remark', 'current_medications', 'status', 'show_card', 'create_source',
'tongue_images', 'report_files',
]);
}
public function sceneEdit()
@@ -178,25 +184,25 @@ class DiagnosisValidate extends BaseValidate
return $this->only(['id', 'task', 'prompt']);
}
public function sceneAiAnalysis()
{
return $this->only(['id', 'model'])
->remove('id', 'checkDiagnosis')
->append('id', 'integer|gt:0|checkAiAnalysisPayload');
}
public function scenePatientAiReports()
{
return $this->only(['patient_id'])
->append('patient_id', 'require|integer|gt:0|checkPatientAiReportsPayload');
}
public function sceneGeneratePatientAiReport()
{
return $this->only(['patient_id', 'model'])
->append('patient_id', 'require|integer|gt:0|checkGeneratePatientAiReportPayload')
->append('model', 'require|in:qwen,openai');
}
public function sceneAiAnalysis()
{
return $this->only(['id', 'model'])
->remove('id', 'checkDiagnosis')
->append('id', 'integer|gt:0|checkAiAnalysisPayload');
}
public function scenePatientAiReports()
{
return $this->only(['patient_id'])
->append('patient_id', 'require|integer|gt:0|checkPatientAiReportsPayload');
}
public function sceneGeneratePatientAiReport()
{
return $this->only(['patient_id', 'model'])
->append('patient_id', 'require|integer|gt:0|checkGeneratePatientAiReportPayload')
->append('model', 'require|in:qwen,openai');
}
public function sceneGenerateAiReports()
{
@@ -252,62 +258,62 @@ class DiagnosisValidate extends BaseValidate
}
/**
* AI 智能分析仅接受诊单 ID 和可选的固定模型键。客户端不得提供
* provider、凭据、上游地址、提示词等服务端配置或控制字段。
* AI 智能分析仅接受诊单 ID 和可选的固定模型键。客户端不得提供
* provider、凭据、上游地址、提示词等服务端配置或控制字段。
*
* @param mixed $value
* @param mixed $rule
* @param array<string,mixed> $data
* @return bool|string
*/
protected function checkAiAnalysisPayload($value, $rule, array $data = [])
{
if (!array_key_exists('id', $data)) {
return 'AI智能分析请求缺少id参数';
}
$extraFields = array_diff(array_keys($data), ['id', 'model']);
if ($extraFields !== []) {
return 'AI智能分析请求仅允许id及可选model参数';
}
if (array_key_exists('model', $data)) {
$model = $data['model'];
if (!is_string($model) || !in_array($model, ['qwen', 'openai'], true)) {
return 'AI模型仅支持qwen或openai';
}
}
return true;
}
/** @param array<string,mixed> $data */
protected function checkPatientAiReportsPayload($value, $rule, array $data = [])
{
if (!array_key_exists('patient_id', $data)) {
return '请求缺少patient_id参数';
}
$extraFields = array_diff(array_keys($data), ['patient_id']);
return $extraFields === [] ? true : '请求仅允许patient_id参数';
}
/**
* 患者报告生成端点只接受稳定患者ID与服务端模型白名单键。
* provider、凭据、BASE_URL、提示词和任意来源正文均不得由客户端注入。
*
* @param array<string,mixed> $data
*/
protected function checkGeneratePatientAiReportPayload($value, $rule, array $data = [])
{
if (!array_key_exists('patient_id', $data) || !array_key_exists('model', $data)) {
return '请求缺少patient_id或model参数';
}
$extraFields = array_diff(array_keys($data), ['patient_id', 'model']);
if ($extraFields !== []) {
return '请求仅允许patient_id和model参数';
}
if (!is_string($data['model']) || !in_array($data['model'], ['qwen', 'openai'], true)) {
return 'AI模型仅支持qwen或openai';
}
return true;
}
}
protected function checkAiAnalysisPayload($value, $rule, array $data = [])
{
if (!array_key_exists('id', $data)) {
return 'AI智能分析请求缺少id参数';
}
$extraFields = array_diff(array_keys($data), ['id', 'model']);
if ($extraFields !== []) {
return 'AI智能分析请求仅允许id及可选model参数';
}
if (array_key_exists('model', $data)) {
$model = $data['model'];
if (!is_string($model) || !in_array($model, ['qwen', 'openai'], true)) {
return 'AI模型仅支持qwen或openai';
}
}
return true;
}
/** @param array<string,mixed> $data */
protected function checkPatientAiReportsPayload($value, $rule, array $data = [])
{
if (!array_key_exists('patient_id', $data)) {
return '请求缺少patient_id参数';
}
$extraFields = array_diff(array_keys($data), ['patient_id']);
return $extraFields === [] ? true : '请求仅允许patient_id参数';
}
/**
* 患者报告生成端点只接受稳定患者ID与服务端模型白名单键。
* provider、凭据、BASE_URL、提示词和任意来源正文均不得由客户端注入。
*
* @param array<string,mixed> $data
*/
protected function checkGeneratePatientAiReportPayload($value, $rule, array $data = [])
{
if (!array_key_exists('patient_id', $data) || !array_key_exists('model', $data)) {
return '请求缺少patient_id或model参数';
}
$extraFields = array_diff(array_keys($data), ['patient_id', 'model']);
if ($extraFields !== []) {
return '请求仅允许patient_id和model参数';
}
if (!is_string($data['model']) || !in_array($data['model'], ['qwen', 'openai'], true)) {
return 'AI模型仅支持qwen或openai';
}
return true;
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\validate\tcm\DiagnosisValidate;
function diagnosisSceneExpect(bool $condition, string $message): void
{
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
}
function sceneOnlyFields(DiagnosisValidate $validator): array
{
$property = (new ReflectionClass($validator))->getProperty('only');
$property->setAccessible(true);
$fields = $property->getValue($validator);
return is_array($fields) ? $fields : [];
}
$addPayload = [
'patient_name' => '张三',
'phone' => '13812345678',
'gender' => 1,
'age' => 42,
'diagnosis_type' => '复诊',
'local_hospital_name' => '某医院',
'status' => 1,
'create_source' => 'admin',
];
$addOnly = sceneOnlyFields((new DiagnosisValidate())->sceneAdd());
foreach (['task', 'prompt', 'model', 'report_id', 'content', 'diagnosis_id', 'tracking_content', 'id'] as $leaked) {
diagnosisSceneExpect(
!in_array($leaked, $addOnly, true),
"新增诊单场景不得校验 {$leaked}"
);
}
foreach (['patient_name', 'phone', 'gender', 'age', 'diagnosis_type', 'local_hospital_name'] as $required) {
diagnosisSceneExpect(
in_array($required, $addOnly, true),
"新增诊单场景仍应校验 {$required}"
);
}
$addScene = (new DiagnosisValidate())->scene('add');
$addOk = $addScene->check($addPayload) === true;
$addError = $addOk ? '' : json_encode($addScene->getError(), JSON_UNESCAPED_UNICODE);
diagnosisSceneExpect($addOk, '新增诊单不带 task 应通过校验' . ($addError !== '' ? ": {$addError}" : ''));
$editScene = (new DiagnosisValidate())->sceneEdit();
diagnosisSceneExpect(
!in_array('task', sceneOnlyFields($editScene), true),
'编辑诊单场景不得校验 AI 助手任务'
);
$assistantScene = (new DiagnosisValidate())->sceneAiAssistant();
diagnosisSceneExpect(
in_array('task', sceneOnlyFields($assistantScene), true),
'AI 助手场景仍应校验 task'
);
echo "DiagnosisValidateSceneTest passed\n";