198 lines
7.0 KiB
PHP
198 lines
7.0 KiB
PHP
<?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 内容导致的医疗差错,由操作医师及医疗机构承担责任。";
|
||
}
|
||
}
|