Files
nl-admin-api/app/Service/common/ai/AiDisclaimerService.php
2026-08-10 15:51:00 +08:00

198 lines
7.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Service\common\ai;
use App\BaseApp\BaseService;
use App\Models\new\ai\AiDoctorConsentModel;
use App\Models\new\ai\AiFeatureCopyModel;
use App\Models\new\vip\VipFeatureModel;
/**
* AI 醒目提示文案 + 首次知情同意
* 文案按 VIP 功能码隔离;医生签署后仅该功能无需再次确认
*/
class AiDisclaimerService extends BaseService
{
public const FEATURE_MR = 'ai_medical_record';
public const FEATURE_RX = 'ai_prescription';
/**
* 系统配置:启用中的文案列表(含 VIP 功能名)
*/
public function listCopiesForConfig(): array
{
$rows = AiFeatureCopyModel::where('deleted_at', 0)
->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<array{id?:int,feature_code?:string,record_label?:string,disclaimer_text?:string,consent_text?:string}> $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 内容导致的医疗差错,由操作医师及医疗机构承担责任。";
}
}