$enabled, 'audio_verified' => $verified, 'preview_only' => $preview, 'preview_ready' => $preview && $ready, 'test_scope_allowed' => $scope, 'can_upload' => $enabled && $ready, 'can_review' => $enabled && $ready, 'can_apply' => $enabled && $verified && !$preview, 'can_daily' => $daily, 'limits' => Upload::limits(), 'models' => $enabled ? ProviderConfig::readyModels() : [], // No migration/dictionary dependency is introduced when the feature is OFF. 'fields' => $enabled ? self::catalogForActor($diagnosis, $actor, $info) : [], ]; } public static function requireEnabled(bool $verified = false): void { if (!Store::enabled()) { throw new DomainException('回访录音功能尚未启用'); } if ($verified && !Store::ready()) { throw new DomainException('当前服务与模型的音频能力尚未验证,暂不接受真实录音或写入'); } } public static function create(array $p, int $actor, array $info): array { self::requireEnabled(true); Access::diagnosis($p['diagnosis_id'], $actor, $info); $upload = Upload::owned($p['upload_id'], $actor, $info); if ((int) $upload['diagnosis_id'] !== $p['diagnosis_id'] || $upload['status'] !== 'complete') { throw new DomainException('录音与当前诊单不匹配或尚未上传完成'); } Upload::path($upload); $date = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $p['recorded_at'], new \DateTimeZone('Asia/Shanghai')); if (!$date || $date->format('Y-m-d H:i:s') !== $p['recorded_at'] || $date->getTimestamp() > time() + 300) { throw new DomainException('请填写正确的实际通话时间(北京时间),不能晚于当前时间'); } if (!in_array($p['model_key'], ['qwen', 'openai'], true) || !Store::ready($p['model_key'])) { throw new DomainException('所选模型音频能力尚未验证'); } return Store::create($upload, $p['recorded_at'], $p['model_key'], $actor, $info); } public static function lists(int $diagnosisId, int $actor, array $info): array { Access::diagnosis($diagnosisId, $actor, $info); return ['items' => Store::enabled() ? Store::lists($diagnosisId) : []]; } public static function detail(int $id, int $actor, array $info): array { Access::task($id, $actor, $info); return self::protectDetail(Store::detail($id), $actor, $info); } public static function saveDraft(int $id, int $version, array $items, int $actor, array $info, ?string $channelRoles = null): array { self::requireEnabled(); $task = Access::task($id, $actor, $info); self::checkDailyItems($task, $items, $actor, $info); return self::protectDetail(Store::saveDraft($id, $version, $items, $actor, $channelRoles), $actor, $info); } public static function apply(int $id, int $version, array $items, int $actor, array $info): array { Gate::assertMayApply(); self::requireEnabled(true); $task = Access::task($id, $actor, $info); self::checkDailyItems($task, $items, $actor, $info); return Apply::apply($id, $version, $items, $actor, $info); } public static function retry(int $id, int $actor, array $info): array { self::requireEnabled(true); Access::task($id, $actor, $info); return Store::retry($id); } private static function checkDailyItems(array $task, array $items, int $actor, array $info): void { $diagnosis = Access::diagnosis((int) $task['diagnosis_id'], $actor, $info); $plain = Access::allowed($actor, Access::actor($actor), 'tcm.diagnosis/phonePlain'); foreach ($items as $item) { if (!is_array($item)) { throw new DomainException('审阅内容无效'); } if (!empty($item['selected']) && ($item['kind'] ?? '') === 'diagnosis' && !$plain) { foreach (['phone', 'id_card'] as $key) { if ($key === 'id_card' && trim((string) ($diagnosis[$key] ?? '')) === '') { continue; } if (array_key_exists($key, (array) ($item['values'] ?? [])) && (string) $item['values'][$key] !== (string) ($diagnosis[$key] ?? '')) { throw new DomainException('无权通过录音修改已有手机号或身份证'); } } } if (!empty($item['selected']) && ($item['kind'] ?? '') !== 'diagnosis') { Access::diagnosis((int) $task['diagnosis_id'], $actor, $info, true); // Continue to validate identity suggestions later in this same batch. } } } private static function catalogForActor(array $diagnosis, int $actor, array $info): array { $info = Access::actor($actor); $catalog = Fields::catalog(); $basic = DiagnosisLogic::canEditPatientBasicInfo((int) $diagnosis['id'], $info); $plain = Access::allowed($actor, $info, 'tcm.diagnosis/phonePlain'); foreach ($catalog['diagnosis'] as &$field) { $key = $field['key']; if ((!$basic && in_array($key, ['patient_name', 'id_card', 'phone', 'gender', 'age'], true)) || (!$plain && ($key === 'phone' || ($key === 'id_card' && !empty($diagnosis['id_card']))))) { $field['readonly'] = true; } } unset($field); return $catalog; } private static function protectDetail(array $detail, int $actor, array $info): array { $diagnosis = Access::diagnosis((int) $detail['diagnosis_id'], $actor, $info); $detail['fields'] = self::catalogForActor($diagnosis, $actor, $info); if (!Access::allowed($actor, Access::actor($actor), 'tcm.diagnosis/phonePlain')) { foreach ($detail['items'] as &$item) { foreach (['phone', 'id_card'] as $key) { $value = (string) ($item['current_values'][$key] ?? ''); if ($value !== '') { $item['current_values'][$key] = mb_substr($value, 0, 3) . '****' . mb_substr($value, -4); } } } unset($item); } return $detail; } }