geng
This commit is contained in:
@@ -4,13 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\common\cache\AdminAuthCache;
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\DiagnosisAiReport;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use app\common\service\DifyChatService;
|
||||
use app\common\cache\AdminAuthCache;
|
||||
use app\adminapi\logic\firstvisit\MyPatientLogic;
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\DiagnosisAiReport;
|
||||
use app\common\service\DifyChatService;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
@@ -201,6 +200,44 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
string $prompt,
|
||||
int $adminId,
|
||||
array $adminInfo
|
||||
): ?array {
|
||||
$prepared = self::prepareAssistant($diagnosisId, $task, $prompt, $adminId, $adminInfo);
|
||||
if ($prepared === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = DifyChatService::chat(
|
||||
$prepared['profile'],
|
||||
$prepared['inputs'],
|
||||
$prepared['query'],
|
||||
$prepared['user']
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
self::logAssistantFailure($diagnosisId, $prepared['profile'], $adminId, $e);
|
||||
self::setError('AI 助手暂时不可用,请稍后重试');
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::formatAssistantResult($prepared, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 SSE headers 发出前完成参数、权限、DataScope、病例和模型选择预检。
|
||||
* 返回值只供同一请求内的流执行使用,绝不能直接序列化给客户端。
|
||||
*
|
||||
* @param array<string,mixed> $adminInfo
|
||||
* @return array{
|
||||
* diagnosis_id:int,profile:string,model_name:string,model_label:string,task:string,
|
||||
* inputs:array<string,mixed>,query:string,user:string,admin_id:int
|
||||
* }|null
|
||||
*/
|
||||
public static function prepareAssistant(
|
||||
int $diagnosisId,
|
||||
string $task,
|
||||
string $prompt,
|
||||
int $adminId,
|
||||
array $adminInfo
|
||||
): ?array {
|
||||
$diagnosis = self::loadAuthorizedDiagnosis(
|
||||
$diagnosisId,
|
||||
@@ -239,28 +276,63 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'profile' => $profile,
|
||||
'model_name' => $model,
|
||||
'model_label' => $modelLabel,
|
||||
'task' => $task,
|
||||
'inputs' => self::buildUpstreamInputs(
|
||||
$context,
|
||||
'病例问诊助手',
|
||||
self::ASSISTANT_PROMPT_VERSION
|
||||
),
|
||||
'query' => self::buildAssistantPrompt($context, $task, $prompt),
|
||||
'user' => 'admin-diagnosis-assistant-' . $adminId,
|
||||
'admin_id' => $adminId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $prepared prepareAssistant() 的内部返回值
|
||||
* @param callable(string):mixed $onDelta
|
||||
* @param callable():bool|null $shouldAbort
|
||||
* @return array{answer:string,model_key:string,model_label:string,model_name:string,task:string}|null
|
||||
*/
|
||||
public static function streamPreparedAssistant(
|
||||
array $prepared,
|
||||
callable $onDelta,
|
||||
?callable $shouldAbort = null
|
||||
): ?array {
|
||||
$diagnosisId = (int) ($prepared['diagnosis_id'] ?? 0);
|
||||
$profile = (string) ($prepared['profile'] ?? '');
|
||||
$adminId = (int) ($prepared['admin_id'] ?? 0);
|
||||
|
||||
try {
|
||||
$result = DifyChatService::chat(
|
||||
$result = DifyChatService::streamChat(
|
||||
$profile,
|
||||
self::buildUpstreamInputs(
|
||||
$context,
|
||||
'病例问诊助手',
|
||||
self::ASSISTANT_PROMPT_VERSION
|
||||
),
|
||||
self::buildAssistantPrompt($context, $task, $prompt),
|
||||
'admin-diagnosis-assistant-' . $adminId
|
||||
is_array($prepared['inputs'] ?? null) ? $prepared['inputs'] : [],
|
||||
(string) ($prepared['query'] ?? ''),
|
||||
(string) ($prepared['user'] ?? ''),
|
||||
$onDelta,
|
||||
$shouldAbort
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('diagnosis ai assistant upstream call failed', [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'profile' => $profile,
|
||||
'admin_id' => $adminId,
|
||||
'exception_class' => get_class($e),
|
||||
]);
|
||||
self::logAssistantFailure($diagnosisId, $profile, $adminId, $e);
|
||||
self::setError('AI 助手暂时不可用,请稍后重试');
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::formatAssistantResult($prepared, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $prepared
|
||||
* @param array<string,mixed> $result
|
||||
* @return array{answer:string,model_key:string,model_label:string,model_name:string,task:string}|null
|
||||
*/
|
||||
private static function formatAssistantResult(array $prepared, array $result): ?array
|
||||
{
|
||||
if (empty($result['ok'])) {
|
||||
self::setError((string) ($result['error'] ?? 'AI 助手暂时不可用,请稍后重试'));
|
||||
return null;
|
||||
@@ -273,13 +345,27 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
|
||||
return [
|
||||
'answer' => $content,
|
||||
'model_key' => $profile,
|
||||
'model_label' => $modelLabel,
|
||||
'model_name' => $model,
|
||||
'task' => $task,
|
||||
'model_key' => (string) ($prepared['profile'] ?? ''),
|
||||
'model_label' => (string) ($prepared['model_label'] ?? ''),
|
||||
'model_name' => (string) ($prepared['model_name'] ?? ''),
|
||||
'task' => (string) ($prepared['task'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private static function logAssistantFailure(
|
||||
int $diagnosisId,
|
||||
string $profile,
|
||||
int $adminId,
|
||||
\Throwable $exception
|
||||
): void {
|
||||
Log::warning('diagnosis ai assistant upstream call failed', [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'profile' => $profile,
|
||||
'admin_id' => $adminId,
|
||||
'exception_class' => get_class($exception),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接诊台结构化 AI 智能分析。每次只调用客户端白名单键对应的服务端模型,
|
||||
* 上游失败或响应不符合契约时直接失败,不构造本地伪分析。
|
||||
@@ -604,28 +690,10 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
return null;
|
||||
}
|
||||
|
||||
$accessQuery = Diagnosis::where('id', $id)->whereNull('delete_time');
|
||||
$isRoot = !empty($adminInfo['root']) && (int) $adminInfo['root'] === 1;
|
||||
if (!$isRoot) {
|
||||
$roleIds = array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id'));
|
||||
if (in_array(2, $roleIds, true)) {
|
||||
$accessQuery->where('assistant_id', $adminId);
|
||||
}
|
||||
if (DataScopeService::isEnabled()) {
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
if ($visibleIds === []) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
return null;
|
||||
}
|
||||
if (is_array($visibleIds)) {
|
||||
$accessQuery->whereIn('assistant_id', $visibleIds);
|
||||
}
|
||||
}
|
||||
if (!MyPatientLogic::canAccessDiagnosis($id, $adminId, $adminInfo)) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
return null;
|
||||
}
|
||||
if (!$accessQuery->find()) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
return null;
|
||||
}
|
||||
|
||||
$diagnosis = DiagnosisLogic::detail(['id' => $id], $adminInfo);
|
||||
if ($diagnosis === [] || empty($diagnosis['id'])) {
|
||||
|
||||
@@ -28,10 +28,11 @@ use app\common\model\DiagnosisViewRecord;
|
||||
use app\common\model\doctor\Appointment;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\doctor\DoctorNoteLogic;
|
||||
use app\adminapi\logic\doctor\AppointmentLogic;
|
||||
use app\adminapi\logic\tcm\TrackingNoteLogic;
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\doctor\DoctorNoteLogic;
|
||||
use app\adminapi\logic\doctor\AppointmentLogic;
|
||||
use app\adminapi\logic\firstvisit\MyPatientLogic;
|
||||
use app\adminapi\logic\tcm\TrackingNoteLogic;
|
||||
use app\common\service\ConfigService;
|
||||
use app\common\service\FileService;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
@@ -4182,32 +4183,10 @@ class DiagnosisLogic extends BaseLogic
|
||||
return [];
|
||||
}
|
||||
|
||||
// 1) 数据权限闸 — 不通过则返回「不存在或无权访问」
|
||||
$accessQuery = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time');
|
||||
|
||||
$roleIds = array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id'));
|
||||
if (in_array(2, $roleIds, true)) {
|
||||
// 医助仅看自己被指派的
|
||||
$accessQuery->where('assistant_id', $adminId);
|
||||
}
|
||||
|
||||
if (DataScopeService::isEnabled()) {
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
if ($visibleIds === []) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
|
||||
return [];
|
||||
}
|
||||
if (is_array($visibleIds)) {
|
||||
$accessQuery->whereIn('assistant_id', $visibleIds);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$accessQuery->find()) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
|
||||
return [];
|
||||
}
|
||||
// 1) 数据权限闸 — 不通过则返回「不存在或无权访问」
|
||||
if (!self::canViewReadonlyDiagnosis($diagnosisId, $adminId, $adminInfo)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2) 诊单详情(含图片聚合等)+ 字典翻译
|
||||
$diagnosis = self::detail(['id' => $diagnosisId]);
|
||||
@@ -4238,24 +4217,43 @@ class DiagnosisLogic extends BaseLogic
|
||||
$unservedDays = $maxRecordTs > 0 ? max(0, (int) floor((time() - $maxRecordTs) / 86400)) : null;
|
||||
$lastBloodRecordAt = $maxRecordTs > 0 ? date('Y-m-d', $maxRecordTs) : null;
|
||||
|
||||
return [
|
||||
return [
|
||||
'appointment' => $appointment,
|
||||
'diagnosis' => $diagnosis,
|
||||
'doctor_notes' => $doctorNotes,
|
||||
'tracking_notes' => $trackingNotes,
|
||||
'unserved_days' => $unservedDays,
|
||||
'last_blood_record_at' => $lastBloodRecordAt,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 readonlyDetail 共用的诊单行级可见性入口。
|
||||
*
|
||||
* 复用“我的患者”统一行权策略:医生按有效接诊关系,医助按归属关系,
|
||||
* 团队管理角色才使用 DataScope。不存在与越权使用同一错误避免枚举。
|
||||
*/
|
||||
public static function canViewReadonlyDiagnosis(int $diagnosisId, int $adminId, array $adminInfo): bool
|
||||
{
|
||||
self::$error = '';
|
||||
if (!MyPatientLogic::canAccessDiagnosis($diagnosisId, $adminId, $adminInfo)) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取指定日期区间内的三类跟踪记录(血糖血压 / 饮食 / 运动),供 readonlyDetail 与
|
||||
* 医生接诊台 reception 通过独立接口 lazy load。
|
||||
*
|
||||
* 区间语义:闭区间 [startDate, endDate](Y-m-d),均不传则不限。
|
||||
*
|
||||
* @return array{
|
||||
* blood_records: array<int,array<string,mixed>>,
|
||||
* diagnosis_id: int,
|
||||
* blood_records: array<int,array<string,mixed>>,
|
||||
* diet_records: array<int,array<string,mixed>>,
|
||||
* exercise_records: array<int,array<string,mixed>>,
|
||||
* start_date: string,
|
||||
@@ -4267,10 +4265,11 @@ class DiagnosisLogic extends BaseLogic
|
||||
$sinceTs = $startDate !== '' ? (int) strtotime($startDate . ' 00:00:00') : 0;
|
||||
$untilTs = $endDate !== '' ? (int) strtotime($endDate . ' 23:59:59') : 0;
|
||||
$sinceTs = $sinceTs > 0 ? $sinceTs : 0;
|
||||
$untilTs = $untilTs > 0 ? $untilTs : 0;
|
||||
|
||||
return [
|
||||
'blood_records' => self::fetchBloodRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
$untilTs = $untilTs > 0 ? $untilTs : 0;
|
||||
|
||||
return [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'blood_records' => self::fetchBloodRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
'diet_records' => self::fetchDietRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
'exercise_records' => self::fetchExerciseRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
'start_date' => $startDate,
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\common\model\auth\Admin;
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\adminapi\logic\firstvisit\MyPatientLogic;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\doctor\Appointment;
|
||||
use app\common\model\doctor\Medicine as DoctorMedicine;
|
||||
use app\common\model\tcm\Prescription;
|
||||
@@ -934,14 +935,35 @@ class PrescriptionLogic
|
||||
/**
|
||||
* 根据诊单ID获取处方列表
|
||||
*/
|
||||
public static function listByDiagnosis(int $diagnosisId): array
|
||||
{
|
||||
return Prescription::where('diagnosis_id', $diagnosisId)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
public static function listByDiagnosis(int $diagnosisId, int $viewerAdminId, array $viewerAdminInfo): array
|
||||
{
|
||||
self::$error = '';
|
||||
if (!MyPatientLogic::canAccessDiagnosis($diagnosisId, $viewerAdminId, $viewerAdminInfo)) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = Prescription::where('diagnosis_id', $diagnosisId)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return self::filterViewablePrescriptions($rows, $viewerAdminId, $viewerAdminInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string,mixed>> $rows
|
||||
* @return array<int, array<string,mixed>>
|
||||
*/
|
||||
private static function filterViewablePrescriptions(array $rows, int $viewerAdminId, array $viewerAdminInfo): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$rows,
|
||||
static fn (array $row): bool => self::canViewPrescription($row, $viewerAdminId, $viewerAdminInfo)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据预约ID获取处方(带权限检查)
|
||||
|
||||
Reference in New Issue
Block a user