orderByDesc('sort') ->orderBy('id') ->get() ->toArray(); $nameMap = VipFeatureModel::where('deleted_at', 0) ->whereIn('code', array_column($rows, 'feature_code')) ->get(['code', 'name']) ->keyBy('code') ->toArray(); foreach ($rows as &$row) { $code = (string) ($row['feature_code'] ?? ''); $row['feature_name'] = $nameMap[$code]['name'] ?? $code; $row['disclaimer_text'] = (string) ($row['disclaimer_text'] ?? ''); $row['consent_text'] = (string) ($row['consent_text'] ?? ''); } unset($row); return $rows; } /** * 批量保存文案(系统配置 AI 模型 Tab) * * @param list $items */ public function saveCopies(array $items): bool { if (empty($items)) { $this->utils->errorThrow('无保存内容'); } $now = time(); foreach ($items as $item) { if (!is_array($item)) { continue; } $id = (int) ($item['id'] ?? 0); $code = trim((string) ($item['feature_code'] ?? '')); $row = null; if ($id > 0) { $row = AiFeatureCopyModel::where('id', $id)->where('deleted_at', 0)->first(); } if (!$row && $code !== '') { $row = AiFeatureCopyModel::where('feature_code', $code)->where('deleted_at', 0)->first(); } if (!$row) { continue; } if (array_key_exists('record_label', $item)) { $row->record_label = trim((string) $item['record_label']); } if (array_key_exists('disclaimer_text', $item)) { $row->disclaimer_text = (string) $item['disclaimer_text']; } if (array_key_exists('consent_text', $item)) { $row->consent_text = (string) $item['consent_text']; } $row->updated_at = $now; $row->save(); } return true; } /** * 按功能码取文案;不存在则回落内置默认 */ public function getCopyByFeature(string $featureCode): array { $featureCode = trim($featureCode); if ($featureCode === '') { $this->utils->errorThrow('功能码不能为空'); } $row = AiFeatureCopyModel::where('feature_code', $featureCode) ->where('deleted_at', 0) ->where('status', 1) ->first(); if ($row) { return [ 'id' => (int) $row->id, 'feature_code' => (string) $row->feature_code, 'record_label' => (string) ($row->record_label ?: $this->defaultRecordLabel($featureCode)), 'disclaimer_text' => (string) ($row->disclaimer_text ?: $this->defaultDisclaimer($featureCode)), 'consent_text' => (string) ($row->consent_text ?: $this->defaultConsent()), ]; } return [ 'id' => 0, 'feature_code' => $featureCode, 'record_label' => $this->defaultRecordLabel($featureCode), 'disclaimer_text' => $this->defaultDisclaimer($featureCode), 'consent_text' => $this->defaultConsent(), ]; } /** * 门闸:文案 + 是否已签署(按医生+功能码) */ public function getGate(string $featureCode, int $doctorId): array { if ($doctorId <= 0) { $this->utils->errorThrow('无法识别医生身份'); } $copy = $this->getCopyByFeature($featureCode); $consented = AiDoctorConsentModel::where('doctor_id', $doctorId) ->where('feature_code', $featureCode) ->where('deleted_at', 0) ->exists(); return array_merge($copy, [ 'consented' => $consented, ]); } /** * 医生签署知情同意(同一功能幂等) */ public function agree(string $featureCode, int $doctorId, int $storeId = 0): array { if ($doctorId <= 0) { $this->utils->errorThrow('无法识别医生身份'); } $featureCode = trim($featureCode); if ($featureCode === '') { $this->utils->errorThrow('功能码不能为空'); } $copy = $this->getCopyByFeature($featureCode); $exists = AiDoctorConsentModel::where('doctor_id', $doctorId) ->where('feature_code', $featureCode) ->where('deleted_at', 0) ->first(); if ($exists) { return [ 'consented' => true, 'feature_code' => $featureCode, 'already' => true, ]; } $now = time(); AiDoctorConsentModel::query()->insert([ 'doctor_id' => $doctorId, 'feature_code' => $featureCode, 'copy_id' => (int) ($copy['id'] ?? 0), 'store_id' => $storeId, 'created_at' => $now, 'updated_at' => 0, 'deleted_at' => 0, ]); return [ 'consented' => true, 'feature_code' => $featureCode, 'already' => false, ]; } private function defaultRecordLabel(string $featureCode): string { return $featureCode === self::FEATURE_RX ? '处方' : '病历'; } private function defaultDisclaimer(string $featureCode): string { $label = $this->defaultRecordLabel($featureCode); return '本' . $label . '由 AI 根据问诊/语音转写内容辅助生成,仅供起草参考。' . "\nAI 可能存在漏记、误读或“幻觉”,不能替代医师专业判断。" . "\n诊断、处置、用药、知情告知等关键内容须由你逐条核对、修改后确认。"; } private function defaultConsent(): string { return "我已知悉:\n" . "1. 本平台 AI 仅用于病历起草、信息归纳、术语规范化;\n" . "2. 不得用于自动诊断、自动开嘱、自动出报告;\n" . "3. 所有 AI 输出须由具备资质的医师审核、修改、电子签名;\n" . "4. 因未审核直接使用 AI 内容导致的医疗差错,由操作医师及医疗机构承担责任。"; } }