'require|checkDiagnosis', 'patient_name' => 'require|length:1,50', 'id_card' => 'length:15,18', 'phone' => 'require|mobile', 'gender' => 'require|in:0,1', 'age' => 'require|number|between:0,150', 'diagnosis_type' => 'require', 'local_hospital_name' => 'require|max:255', 'status' => 'in:0,1', 'show_card' => 'in:0,1', 'create_source' => 'max:32', 'current_medications' => 'max:2000', 'tongue_images' => 'array', 'report_files' => 'array', 'diabetes_discovery_year' => 'max:50', 'start_date' => 'date', 'end_date' => 'date|checkDateRange', 'diagnosis_id' => 'require|integer|checkDiagnosisId', 'tracking_content' => 'require|length:1,1000', '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', ]; protected $message = [ 'id.require' => '参数缺失', 'patient_name.require' => '请输入患者姓名', 'patient_name.length' => '患者姓名长度须在1-50位字符', 'id_card.length' => '身份证号长度不正确', 'id_card.require' => '请输入身份证号', 'phone.require' => '请输入手机号', 'phone.mobile' => '手机号格式不正确', 'gender.require' => '请选择性别', 'gender.in' => '性别参数错误', 'age.require' => '请输入年龄', 'age.number' => '年龄必须为数字', 'age.between' => '年龄范围0-150', 'diagnosis_type.require' => '请选择诊断类型', 'local_hospital_name.require' => '请输入当地就诊医院名称', 'local_hospital_name.max' => '当地就诊医院名称最多255个字符', 'status.in' => '状态参数错误', 'create_source.max' => '渠道来源长度不能超过32个字符', 'current_medications.max' => '在用药物最多2000个字符', 'diabetes_discovery_year.max' => '发现糖尿病患病史最多50个字符', 'start_date.date' => '开始日期格式不正确', 'end_date.date' => '结束日期格式不正确', 'diagnosis_id.require' => '诊单ID不能为空', 'tracking_content.require' => '跟踪备注内容不能为空', 'tracking_content.length' => '跟踪备注最多1000个字符', 'report_id.require' => '报告ID不能为空', 'report_id.number' => '报告ID必须为数字', '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', ]; public function sceneAdd() { // 全局规则里混有跟踪备注、AI 助手字段;新增诊单只去掉这些无关校验。 // 不要改成 only([...]):add 还会写入现病史等多选字段,only 会把它们从请求里丢掉。 return $this->remove('id', true) ->remove('diagnosis_id', true) ->remove('tracking_content', true) ->remove('task', true) ->remove('prompt', true) ->remove('model', true) ->remove('report_id', true) ->remove('content', true); } public function sceneEdit() { return $this->only(['id', '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']); } public function sceneId() { return $this->only(['id']); } public function sceneReadonlyDetail() { return $this->only(['id']); } public function sceneTrackingWindow() { return $this->only(['id', 'start_date', 'end_date']); } /** 新增跟踪备注:诊单ID + 备注内容 */ public function sceneAddTrackingNote() { return $this->only(['diagnosis_id', 'tracking_content']); } /** 拉取跟踪备注列表:诊单ID */ public function sceneTrackingNotes() { return $this->only(['diagnosis_id']); } public function sceneGenerateQrcode() { return $this->only(['diagnosis_id', 'doctor_id', 'patient_id', 'share_user_id', 'mini_program_path']) // The global diagnosis_id rule is required for diagnosis APIs, but // video QR codes identify the doctor instead. Keep diagnosis_id // optional here while still validating it when it is supplied. ->remove('diagnosis_id', 'require') ->append('patient_id', 'require|integer|checkQrcodeIds') ->append('share_user_id', 'require|integer') ->append('doctor_id', 'integer'); } public function sceneGenerateOrderQrcode() { return $this->only([]); } public function sceneFillIdCard() { return $this->only(['id', 'id_card']) ->append('id', 'require') ->append('id_card', 'require|length:15,18'); } public function sceneGuahaoLogList() { return $this->only(['id']); } /** 业务订单 tab:设置复诊接诊率统计起始偏移 */ public function sceneSetRevisitSlotStartOffset() { return $this->only(['id', 'revisit_slot_start_offset']) ->append('revisit_slot_start_offset', 'require|integer|between:0,20'); } public function sceneAiReports() { return $this->only(['id']); } public function sceneAiAssistant() { 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 sceneGenerateAiReports() { return $this->only(['id']); } public function sceneEditAiReport() { return $this->only(['id', 'report_id', 'content']) ->append('report_id', 'require|number|gt:0') ->append('content', 'require|max:12000'); } protected function checkDiagnosis($value) { $diagnosis = Diagnosis::findOrEmpty($value); if ($diagnosis->isEmpty()) { return '诊单不存在'; } return true; } /** 校验 diagnosis_id 字段(addTrackingNote 等场景使用,避开 id 字段) */ protected function checkDiagnosisId($value) { $diagnosis = Diagnosis::findOrEmpty($value); if ($diagnosis->isEmpty()) { return '诊单不存在'; } return true; } /** end_date 不能早于 start_date */ protected function checkDateRange($value, $rule, $data = []) { $start = $data['start_date'] ?? ''; if ($start && $value && strtotime($value) < strtotime($start)) { return '结束日期不能早于开始日期'; } return true; } /** 视频场景需 doctor_id,确认诊单需 diagnosis_id */ protected function checkQrcodeIds($value, $rule, $data = []) { $page = $data['mini_program_path'] ?? ''; $hasDoctor = !empty($data['doctor_id']); $hasDiagnosis = !empty($data['diagnosis_id']); if ($page === 'pages/login/login') { return $hasDoctor ? true : '视频二维码需传挂号医生ID'; } return $hasDiagnosis ? true : '诊单ID不能为空'; } /** * AI 智能分析仅接受诊单 ID 和可选的固定模型键。客户端不得提供 * provider、凭据、上游地址、提示词等服务端配置或控制字段。 * * @param mixed $value * @param mixed $rule * @param array $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 $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 $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; } }