This commit is contained in:
2026-05-05 15:02:04 +08:00
parent 0da27ed745
commit 19bfeef98f
19 changed files with 1771 additions and 272 deletions
@@ -113,12 +113,31 @@ class DiagnosisController extends BaseAdminController
*/
public function detail()
{
$params = (new DiagnosisValidate())->goCheck('id');
$result = DiagnosisLogic::detail($params);
return $this->data($result);
}
/**
* @notes 二诊只读病例详情(患者信息 + 病例 + 跟踪记录)
*
* 路由:GET /tcm.diagnosis/readonlyDetail?id=:diagnosisId
* 权限:tcm.diagnosis/readonlyDetail
*
* @return \think\response\Json
*/
public function readonlyDetail()
{
$params = (new DiagnosisValidate())->goCheck('readonlyDetail');
$result = DiagnosisLogic::readonlyDetail($params, $this->adminId, $this->adminInfo);
if (empty($result)) {
return $this->fail(DiagnosisLogic::getError() ?: '诊单不存在或无权访问');
}
return $this->data($result);
}
/**
* @notes 诊单挂号 / 取消挂号 操作日志(谁在何时操作)
*/
@@ -17,6 +17,7 @@ namespace app\adminapi\lists\tcm;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\dept\DeptLogic;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\BloodRecord;
use app\common\model\DiagnosisViewRecord;
use app\common\model\doctor\Appointment;
use app\common\model\tcm\Prescription;
@@ -381,6 +382,25 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
foreach ($lists as &$item) {
$item['video_call_hint'] = $this->buildVideoCallHint($latestCallByDiag[$item['id']] ?? null);
}
unset($item);
// 未服务天数:以血糖记录最近一条 record_date 为锚点(口径见 prd T3
// 单次 GROUP BY 聚合,不会成 N+1;走 idx_diagnosis_id 索引。
$diagIdsForUnserved = array_values(array_filter(array_unique(array_column($lists, 'id'))));
$maxBloodByDiag = [];
if (!empty($diagIdsForUnserved)) {
$maxBloodByDiag = BloodRecord::whereIn('diagnosis_id', $diagIdsForUnserved)
->whereNull('delete_time')
->group('diagnosis_id')
->column('MAX(record_date)', 'diagnosis_id');
}
$nowTs = time();
foreach ($lists as &$item) {
$last = (int) ($maxBloodByDiag[(int) $item['id']] ?? 0);
$item['last_blood_record_at'] = $last > 0 ? date('Y-m-d', $last) : null;
$item['unserved_days'] = $last > 0 ? max(0, (int) floor(($nowTs - $last) / 86400)) : null;
}
unset($item);
return $lists;
}
@@ -561,13 +561,13 @@ class AppointmentLogic extends BaseLogic
/**
* 字典翻译:把诊单中以 code 存储的字段(字典/枚举)补一份中文 label 字段,
* 便于前端(接诊台等只读场景)直接渲染,无需加载字典。
* 便于前端(接诊台 / 只读病例页等只读场景)直接渲染,无需加载字典。
* 原始 code 字段保留不动,edit 场景仍可使用。
*
* @param array $diagnosis
* @return array
*/
private static function enrichDiagnosisLabels(array $diagnosis): array
public static function enrichDiagnosisLabels(array $diagnosis): array
{
if (empty($diagnosis)) {
return $diagnosis;
@@ -19,9 +19,17 @@ use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\DiagnosisGuahaoLog;
use app\common\model\tcm\DiagnosisAssignLog;
use app\common\model\tcm\ImChatMessage;
use app\common\model\tcm\BloodRecord;
use app\common\model\tcm\DietRecord;
use app\common\model\tcm\ExerciseRecord;
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\doctor\DoctorNoteLogic;
use app\adminapi\logic\doctor\AppointmentLogic;
use app\common\service\FileService;
use app\common\service\DataScope\DataScopeService;
use think\facade\Db;
/**
* 中医辨房病因诊单逻辑
@@ -3324,4 +3332,237 @@ class DiagnosisLogic extends BaseLogic
return $logs;
}
/**
* @notes 二诊只读病例详情
*
* 用途:医助从诊单列表「查看 / 双击」进入只读页(T1+T2),返回与接诊台同结构的
* 三块内容(appointment + diagnosis + 跟踪记录)。
*
* 数据权限(与列表 lists 接口一致):
* - 医助 (role_id=2):仅能访问 assistant_id == self 的诊单
* - DataScope 启用:assistant_id 须落在 visibleAdminIds
* - 越权 → setError + 返回 []
*
* @param array $params 至少含 id
* @param int $adminId
* @param array<string,mixed> $adminInfo request->adminInfo
* @return array
*/
public static function readonlyDetail(array $params, int $adminId, array $adminInfo): array
{
$diagnosisId = (int) ($params['id'] ?? 0);
if ($diagnosisId <= 0) {
self::setError('诊单 id 不能为空');
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 [];
}
// 2) 诊单详情(含图片聚合等)+ 字典翻译
$diagnosis = self::detail(['id' => $diagnosisId]);
if (empty($diagnosis)) {
self::setError('诊单数据为空');
return [];
}
$diagnosis = AppointmentLogic::enrichDiagnosisLabels($diagnosis);
// 3) 最近一条挂号(PatientInfoCard 显示用,无挂号则给默认空对象)
$appointment = self::buildLatestAppointmentForReadonly((int) ($diagnosis['id'] ?? $diagnosisId), $diagnosis);
// 4) 最近 30 天的跟踪记录(一次性 join 出三类,前端透视为矩阵)
$windowDays = 30;
$sinceTs = strtotime('-' . $windowDays . ' days');
$sinceTs = $sinceTs > 0 ? $sinceTs : 0;
$bloodRecords = self::fetchBloodRecordsForReadonly($diagnosisId, $sinceTs);
$dietRecords = self::fetchDietRecordsForReadonly($diagnosisId, $sinceTs);
$exerciseRecords = self::fetchExerciseRecordsForReadonly($diagnosisId, $sinceTs);
// 5) 医生备注
$doctorNotes = DoctorNoteLogic::getByDiagnosis($diagnosisId);
// 6) 未服务天数(与 T3 列同口径)
$maxBloodTs = BloodRecord::where('diagnosis_id', $diagnosisId)
->max('record_date');
$maxBloodTs = (int) $maxBloodTs;
$unservedDays = $maxBloodTs > 0 ? max(0, (int) floor((time() - $maxBloodTs) / 86400)) : null;
$lastBloodRecordAt = $maxBloodTs > 0 ? date('Y-m-d', $maxBloodTs) : null;
return [
'appointment' => $appointment,
'diagnosis' => $diagnosis,
'blood_records' => $bloodRecords,
'diet_records' => $dietRecords,
'exercise_records' => $exerciseRecords,
'doctor_notes' => $doctorNotes,
'unserved_days' => $unservedDays,
'last_blood_record_at' => $lastBloodRecordAt,
'window_days' => $windowDays,
];
}
/**
* 拼一份「最近一条挂号」给只读页 PatientInfoCard 用;没有挂号则给默认空骨架。
*
* @param int $diagnosisId
* @param array<string,mixed> $diagnosis 已字典翻译过的诊单数据
* @return array<string,mixed>
*/
private static function buildLatestAppointmentForReadonly(int $diagnosisId, array $diagnosis): array
{
$apt = Appointment::where('patient_id', $diagnosisId)
->order('id', 'desc')
->find();
$patientName = (string) ($diagnosis['patient_name'] ?? '');
$patientPhone = (string) ($diagnosis['phone'] ?? '');
if (!$apt) {
return [
'id' => 0,
'patient_id' => $diagnosisId,
'patient_name' => $patientName,
'patient_phone' => $patientPhone,
'doctor_id' => 0,
'doctor_name' => '',
'assistant_id' => (int) ($diagnosis['assistant_id'] ?? 0),
'assistant_name' => self::resolveAssistantName((int) ($diagnosis['assistant_id'] ?? 0)),
'appointment_date' => '',
'appointment_time' => '',
'period' => '',
'status' => 0,
'status_desc' => '无挂号',
'has_prescription' => 0,
'remark' => '',
];
}
$aptArr = $apt->toArray();
$aptArr['patient_name'] = $patientName;
$aptArr['patient_phone'] = $patientPhone;
// 医生姓名
$aptArr['doctor_name'] = '';
$docId = (int) ($aptArr['doctor_id'] ?? 0);
if ($docId > 0) {
$aptArr['doctor_name'] = (string) Admin::where('id', $docId)->value('name');
}
$aptArr['assistant_id'] = (int) ($diagnosis['assistant_id'] ?? 0);
$aptArr['assistant_name'] = self::resolveAssistantName((int) ($diagnosis['assistant_id'] ?? 0));
// status_desc 与现有列表口径保持简单映射
$statusMap = [
0 => '未挂号',
1 => '已预约',
2 => '已取消',
3 => '已完成',
4 => '已过号',
];
$aptArr['status_desc'] = $statusMap[(int) ($aptArr['status'] ?? 0)] ?? '';
$aptArr['has_prescription'] = (int) ($diagnosis['has_prescription'] ?? 0);
return $aptArr;
}
private static function resolveAssistantName(int $assistantId): string
{
if ($assistantId <= 0) {
return '';
}
return (string) Admin::where('id', $assistantId)->value('name');
}
/**
* @return array<int,array<string,mixed>>
*/
private static function fetchBloodRecordsForReadonly(int $diagnosisId, int $sinceTs): array
{
$query = BloodRecord::where('diagnosis_id', $diagnosisId);
if ($sinceTs > 0) {
$query->where('record_date', '>=', $sinceTs);
}
$rows = $query->order('record_date', 'desc')
->order('record_time', 'desc')
->select()
->toArray();
// 给前端一个 Y-m-d 字符串方便按日期透视;保留原 record_date 整数戳作日期窗口比较
foreach ($rows as &$row) {
$ts = (int) ($row['record_date'] ?? 0);
$row['record_date_ts'] = $ts;
$row['record_date'] = $ts > 0 ? date('Y-m-d', $ts) : '';
}
return $rows;
}
/**
* @return array<int,array<string,mixed>>
*/
private static function fetchDietRecordsForReadonly(int $diagnosisId, int $sinceTs): array
{
$query = DietRecord::where('diagnosis_id', $diagnosisId);
if ($sinceTs > 0) {
$query->where('record_date', '>=', $sinceTs);
}
$rows = $query->order('record_date', 'desc')->select()->toArray();
foreach ($rows as &$row) {
$ts = (int) ($row['record_date'] ?? 0);
$row['record_date_ts'] = $ts;
$row['record_date'] = $ts > 0 ? date('Y-m-d', $ts) : '';
}
return $rows;
}
/**
* @return array<int,array<string,mixed>>
*/
private static function fetchExerciseRecordsForReadonly(int $diagnosisId, int $sinceTs): array
{
$query = ExerciseRecord::where('diagnosis_id', $diagnosisId);
if ($sinceTs > 0) {
$query->where('record_date', '>=', $sinceTs);
}
$rows = $query->order('record_date', 'desc')
->append(['intensity_text'])
->select()
->toArray();
foreach ($rows as &$row) {
$ts = (int) ($row['record_date'] ?? 0);
$row['record_date_ts'] = $ts;
$row['record_date'] = $ts > 0 ? date('Y-m-d', $ts) : '';
}
return $rows;
}
}
@@ -71,6 +71,11 @@ class DiagnosisValidate extends BaseValidate
{
return $this->only(['id']);
}
public function sceneReadonlyDetail()
{
return $this->only(['id']);
}
public function sceneGenerateQrcode()
{