Merge branch 'master' into cs
This commit is contained in:
@@ -18,6 +18,7 @@ use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\lists\tcm\DiagnosisLists;
|
||||
use app\adminapi\logic\order\OrderActionLogLogic;
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
use app\adminapi\logic\tcm\TrackingNoteLogic;
|
||||
use app\adminapi\validate\tcm\DiagnosisValidate;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\WechatChatRecord;
|
||||
@@ -113,12 +114,86 @@ 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 跟踪信息(血糖血压 / 饮食 / 运动)—— 按日期区间 lazy load
|
||||
*
|
||||
* 路由:GET /tcm.diagnosis/trackingWindow?id=:diagnosisId&start_date=&end_date=
|
||||
* 权限:tcm.diagnosis/readonlyDetail(与只读病例页同档;接诊台亦复用)
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function trackingWindow()
|
||||
{
|
||||
$params = (new DiagnosisValidate())->goCheck('trackingWindow');
|
||||
$result = DiagnosisLogic::fetchTrackingWindow(
|
||||
(int) $params['id'],
|
||||
(string) ($params['start_date'] ?? ''),
|
||||
(string) ($params['end_date'] ?? '')
|
||||
);
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 新增跟踪备注(按天合并追加,仅文字)
|
||||
*
|
||||
* 路由:POST /tcm.diagnosis/addTrackingNote
|
||||
* 权限:tcm.diagnosis/addTrackingNote
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function addTrackingNote()
|
||||
{
|
||||
$params = (new DiagnosisValidate())->post()->goCheck('addTrackingNote');
|
||||
$ok = TrackingNoteLogic::addOrAppend([
|
||||
'diagnosis_id' => (int) $params['diagnosis_id'],
|
||||
'admin_id' => (int) $this->adminId,
|
||||
'content' => (string) $params['tracking_content'],
|
||||
]);
|
||||
if ($ok === false) {
|
||||
return $this->fail(TrackingNoteLogic::getError() ?: '保存失败');
|
||||
}
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 拉取跟踪备注列表(note_date DESC)
|
||||
*
|
||||
* 路由:GET /tcm.diagnosis/trackingNotes?diagnosis_id=:diagnosisId
|
||||
* 权限:tcm.diagnosis/trackingNotes
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function trackingNotes()
|
||||
{
|
||||
$params = (new DiagnosisValidate())->goCheck('trackingNotes');
|
||||
return $this->data(TrackingNoteLogic::getByDiagnosis((int) $params['diagnosis_id']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 诊单挂号 / 取消挂号 操作日志(谁在何时操作)
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||
// | 开源版本可自由商用,可去除界面版权logo
|
||||
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||
// | 访问官网:https://www.likeadmin.cn
|
||||
// | likeadmin团队 版权所有 拥有最终解释权
|
||||
// +----------------------------------------------------------------------
|
||||
// | author: likeadminTeam
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\controller\tcm;
|
||||
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\lists\tcm\DiagnosisTodoLists;
|
||||
use app\adminapi\logic\tcm\DiagnosisTodoLogic;
|
||||
use app\adminapi\validate\tcm\DiagnosisTodoValidate;
|
||||
|
||||
/**
|
||||
* 诊单待办事项控制器
|
||||
*
|
||||
* 职责:仅 lists / add / cancel / detail;编辑能力故意不开放(要改 = 取消重建)
|
||||
*
|
||||
* @package app\adminapi\controller\tcm
|
||||
*/
|
||||
class DiagnosisTodoController extends BaseAdminController
|
||||
{
|
||||
/**
|
||||
* @notes 待办列表(按 diagnosis_id)
|
||||
*/
|
||||
public function lists()
|
||||
{
|
||||
(new DiagnosisTodoValidate())->goCheck('list');
|
||||
|
||||
return $this->dataLists(new DiagnosisTodoLists());
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 新增待办
|
||||
*/
|
||||
public function add()
|
||||
{
|
||||
$params = (new DiagnosisTodoValidate())->post()->goCheck('add');
|
||||
$result = DiagnosisTodoLogic::add($params, $this->adminId, $this->adminInfo);
|
||||
if (!$result) {
|
||||
return $this->fail(DiagnosisTodoLogic::getError() ?: '创建失败');
|
||||
}
|
||||
|
||||
return $this->success('创建成功', [], 1, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 取消待办(仅 status=0 可取消,仅创建人/超管)
|
||||
*/
|
||||
public function cancel()
|
||||
{
|
||||
$params = (new DiagnosisTodoValidate())->post()->goCheck('cancel');
|
||||
$result = DiagnosisTodoLogic::cancel((int) $params['id'], $this->adminId);
|
||||
if (!$result) {
|
||||
return $this->fail(DiagnosisTodoLogic::getError() ?: '取消失败');
|
||||
}
|
||||
|
||||
return $this->success('已取消', [], 1, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 待办详情
|
||||
*/
|
||||
public function detail()
|
||||
{
|
||||
$params = (new DiagnosisTodoValidate())->goCheck('detail');
|
||||
$result = DiagnosisTodoLogic::detail((int) $params['id'], $this->adminId);
|
||||
|
||||
return $this->data($result);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||
// | 开源版本可自由商用,可去除界面版权logo
|
||||
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||
// | 访问官网:https://www.likeadmin.cn
|
||||
// | likeadmin团队 版权所有 拥有最终解释权
|
||||
// +----------------------------------------------------------------------
|
||||
// | author: likeadminTeam
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\lists\tcm;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\adminapi\logic\tcm\DiagnosisTodoLogic;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\model\tcm\DiagnosisTodo;
|
||||
|
||||
/**
|
||||
* 诊单待办事项列表
|
||||
*
|
||||
* 默认按 remind_time desc 排序;按 diagnosis_id 过滤。
|
||||
*
|
||||
* @package app\adminapi\lists\tcm
|
||||
*/
|
||||
class DiagnosisTodoLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
{
|
||||
public function setSearch(): array
|
||||
{
|
||||
return [
|
||||
'=' => ['diagnosis_id', 'status', 'creator_id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function lists(): array
|
||||
{
|
||||
$lists = DiagnosisTodo::where($this->searchWhere)
|
||||
->field([
|
||||
'id', 'diagnosis_id', 'patient_id', 'content', 'remind_time',
|
||||
'status', 'creator_id', 'creator_name', 'notified_at', 'error',
|
||||
'cancelled_at', 'cancelled_by', 'create_time', 'update_time',
|
||||
])
|
||||
->append([
|
||||
'status_text',
|
||||
'remind_time_text',
|
||||
'notified_at_text',
|
||||
'cancelled_at_text',
|
||||
])
|
||||
->order('remind_time', 'desc')
|
||||
->order('id', 'desc')
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 业务态:是否能被「我」取消(前端按钮显隐用)
|
||||
$adminId = (int) $this->adminId;
|
||||
foreach ($lists as &$item) {
|
||||
$item['can_cancel'] = DiagnosisTodoLogic::canCancel($item, $adminId);
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return DiagnosisTodo::where($this->searchWhere)->count();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use app\api\logic\ChatNotifyLogic;
|
||||
use app\adminapi\logic\tcm\PrescriptionLogic;
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
use app\adminapi\logic\tcm\BloodRecordLogic;
|
||||
use app\adminapi\logic\tcm\TrackingNoteLogic;
|
||||
use app\adminapi\logic\doctor\DoctorNoteLogic;
|
||||
use app\common\model\tcm\Prescription;
|
||||
use app\common\service\wechat\WechatWorkAppMessageService;
|
||||
@@ -516,10 +517,8 @@ class AppointmentLogic extends BaseLogic
|
||||
? DiagnosisLogic::detail(['id' => $diagnosisId])
|
||||
: [];
|
||||
|
||||
// 3) 血糖血压记录
|
||||
$bloodRecords = $diagnosisId > 0
|
||||
? BloodRecordLogic::getRecordsByPatient(['diagnosis_id' => $diagnosisId])
|
||||
: [];
|
||||
// 3) 跟踪信息(血糖血压 / 饮食 / 运动)改为前端按窗口 lazy load,
|
||||
// 详见 DiagnosisController::trackingWindow。此处不再一次性返回。
|
||||
|
||||
// 4) 补全医助姓名(detail() 未关联 admin 助理表)
|
||||
$assistantName = '';
|
||||
@@ -551,23 +550,28 @@ class AppointmentLogic extends BaseLogic
|
||||
? DoctorNoteLogic::getByDiagnosis($diagnosisId)
|
||||
: [];
|
||||
|
||||
// 7.1) 跟踪备注
|
||||
$trackingNotes = $diagnosisId > 0
|
||||
? TrackingNoteLogic::getByDiagnosis($diagnosisId)
|
||||
: [];
|
||||
|
||||
return [
|
||||
'appointment' => $appointment,
|
||||
'diagnosis' => $diagnosis,
|
||||
'blood_records' => $bloodRecords,
|
||||
'doctor_notes' => $doctorNotes,
|
||||
'appointment' => $appointment,
|
||||
'diagnosis' => $diagnosis,
|
||||
'doctor_notes' => $doctorNotes,
|
||||
'tracking_notes' => $trackingNotes,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典翻译:把诊单中以 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,18 @@ 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\adminapi\logic\tcm\TrackingNoteLogic;
|
||||
use app\common\service\FileService;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use think\facade\Db;
|
||||
/**
|
||||
* 中医辨房病因诊单逻辑
|
||||
@@ -3324,4 +3333,270 @@ 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) 跟踪信息(血糖血压 / 饮食 / 运动)改为前端按窗口 lazy load,
|
||||
// 详见 DiagnosisController::trackingWindow。此处不再一次性返回。
|
||||
|
||||
// 5) 医生备注
|
||||
$doctorNotes = DoctorNoteLogic::getByDiagnosis($diagnosisId);
|
||||
|
||||
// 5.1) 跟踪备注(只读展示,按 note_date DESC)
|
||||
$trackingNotes = TrackingNoteLogic::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,
|
||||
'doctor_notes' => $doctorNotes,
|
||||
'tracking_notes' => $trackingNotes,
|
||||
'unserved_days' => $unservedDays,
|
||||
'last_blood_record_at' => $lastBloodRecordAt,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 取指定日期区间内的三类跟踪记录(血糖血压 / 饮食 / 运动),供 readonlyDetail 与
|
||||
* 医生接诊台 reception 通过独立接口 lazy load。
|
||||
*
|
||||
* 区间语义:闭区间 [startDate, endDate](Y-m-d),均不传则不限。
|
||||
*
|
||||
* @return array{
|
||||
* blood_records: array<int,array<string,mixed>>,
|
||||
* diet_records: array<int,array<string,mixed>>,
|
||||
* exercise_records: array<int,array<string,mixed>>,
|
||||
* start_date: string,
|
||||
* end_date: string,
|
||||
* }
|
||||
*/
|
||||
public static function fetchTrackingWindow(int $diagnosisId, string $startDate = '', string $endDate = ''): array
|
||||
{
|
||||
$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),
|
||||
'diet_records' => self::fetchDietRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
'exercise_records' => self::fetchExerciseRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼一份「最近一条挂号」给只读页 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, int $untilTs = 0): array
|
||||
{
|
||||
$query = BloodRecord::where('diagnosis_id', $diagnosisId);
|
||||
if ($sinceTs > 0) {
|
||||
$query->where('record_date', '>=', $sinceTs);
|
||||
}
|
||||
if ($untilTs > 0) {
|
||||
$query->where('record_date', '<=', $untilTs);
|
||||
}
|
||||
$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, int $untilTs = 0): array
|
||||
{
|
||||
$query = DietRecord::where('diagnosis_id', $diagnosisId);
|
||||
if ($sinceTs > 0) {
|
||||
$query->where('record_date', '>=', $sinceTs);
|
||||
}
|
||||
if ($untilTs > 0) {
|
||||
$query->where('record_date', '<=', $untilTs);
|
||||
}
|
||||
$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, int $untilTs = 0): array
|
||||
{
|
||||
$query = ExerciseRecord::where('diagnosis_id', $diagnosisId);
|
||||
if ($sinceTs > 0) {
|
||||
$query->where('record_date', '>=', $sinceTs);
|
||||
}
|
||||
if ($untilTs > 0) {
|
||||
$query->where('record_date', '<=', $untilTs);
|
||||
}
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||
// | 开源版本可自由商用,可去除界面版权logo
|
||||
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||
// | 访问官网:https://www.likeadmin.cn
|
||||
// | likeadmin团队 版权所有 拥有最终解释权
|
||||
// +----------------------------------------------------------------------
|
||||
// | author: likeadminTeam
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\DiagnosisTodo;
|
||||
|
||||
/**
|
||||
* 诊单待办事项逻辑
|
||||
*
|
||||
* 状态机:
|
||||
* add() → status=0 (待执行)
|
||||
* cron → status=1 (已发送) / status=3 (失败)
|
||||
* cancel() → status=2 (已取消,仅 status=0 可取消)
|
||||
*
|
||||
* 鉴权:
|
||||
* add:诊单需存在;数据权限交由路由层「tcm.diagnosisTodo/add」节点 + 前端 tab disabled 控制。
|
||||
* cancel:仅创建人本人或超级管理员 (role_id=1) 可取消,且仅 status=0 时。
|
||||
*
|
||||
* @package app\adminapi\logic\tcm
|
||||
*/
|
||||
class DiagnosisTodoLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* @notes 新增待办
|
||||
*
|
||||
* @param array<string,mixed> $params 已通过 sceneAdd 验证
|
||||
* @param int $adminId 当前 admin id
|
||||
* @param array<string,mixed> $adminInfo request->adminInfo
|
||||
*/
|
||||
public static function add(array $params, int $adminId, array $adminInfo): bool
|
||||
{
|
||||
try {
|
||||
$diagnosisId = (int) ($params['diagnosis_id'] ?? 0);
|
||||
$diagnosis = Diagnosis::findOrEmpty($diagnosisId);
|
||||
if ($diagnosis->isEmpty()) {
|
||||
self::setError('诊单不存在');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$remindTime = (int) ($params['remind_time'] ?? 0);
|
||||
if ($remindTime <= time()) {
|
||||
self::setError('提醒时间必须晚于当前时间');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$creatorName = trim((string) ($adminInfo['name'] ?? ''));
|
||||
if ($creatorName === '' && $adminId > 0) {
|
||||
$creatorName = (string) Admin::where('id', $adminId)->value('name');
|
||||
}
|
||||
|
||||
DiagnosisTodo::create([
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'patient_id' => (int) ($diagnosis->getAttr('patient_id') ?? 0),
|
||||
'content' => trim((string) ($params['content'] ?? '')),
|
||||
'remind_time' => $remindTime,
|
||||
'status' => DiagnosisTodo::STATUS_PENDING,
|
||||
'creator_id' => $adminId,
|
||||
'creator_name' => $creatorName,
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 取消待办(人工)
|
||||
*/
|
||||
public static function cancel(int $id, int $adminId): bool
|
||||
{
|
||||
try {
|
||||
$todo = DiagnosisTodo::findOrEmpty($id);
|
||||
if ($todo->isEmpty()) {
|
||||
self::setError('待办不存在');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$status = (int) $todo->getAttr('status');
|
||||
if ($status !== DiagnosisTodo::STATUS_PENDING) {
|
||||
self::setError('该待办已不是「待执行」状态,无法取消');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$creatorId = (int) $todo->getAttr('creator_id');
|
||||
$isSuper = self::isSuperAdmin($adminId);
|
||||
if ($creatorId !== $adminId && !$isSuper) {
|
||||
self::setError('仅创建人或超级管理员可取消');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$todo->save([
|
||||
'status' => DiagnosisTodo::STATUS_CANCELLED,
|
||||
'cancelled_at' => time(),
|
||||
'cancelled_by' => $adminId,
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 详情
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function detail(int $id, int $adminId = 0): array
|
||||
{
|
||||
$todo = DiagnosisTodo::findOrEmpty($id);
|
||||
if ($todo->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
$arr = $todo->append([
|
||||
'status_text',
|
||||
'remind_time_text',
|
||||
'notified_at_text',
|
||||
'cancelled_at_text',
|
||||
])->toArray();
|
||||
|
||||
// 业务态:是否能被「我」取消(前端按钮显隐用)
|
||||
$arr['can_cancel'] = self::canCancel($todo->toArray(), $adminId);
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否超管:role_id=1
|
||||
*/
|
||||
public static function isSuperAdmin(int $adminId): bool
|
||||
{
|
||||
if ($adminId <= 0) {
|
||||
return false;
|
||||
}
|
||||
$roleIds = AdminRole::where('admin_id', $adminId)->column('role_id');
|
||||
|
||||
return in_array(1, array_map('intval', $roleIds), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务判定:当前 admin 能否取消该待办
|
||||
*
|
||||
* @param array<string,mixed> $todoRow
|
||||
*/
|
||||
public static function canCancel(array $todoRow, int $adminId): bool
|
||||
{
|
||||
if ((int) ($todoRow['status'] ?? -1) !== DiagnosisTodo::STATUS_PENDING) {
|
||||
return false;
|
||||
}
|
||||
if ($adminId <= 0) {
|
||||
return false;
|
||||
}
|
||||
if ((int) ($todoRow['creator_id'] ?? 0) === $adminId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return self::isSuperAdmin($adminId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\tcm\TrackingNote;
|
||||
|
||||
/**
|
||||
* 诊单跟踪备注 Logic
|
||||
*
|
||||
* - 按 diagnosis_id + 当天 find-or-create
|
||||
* - 多次追加 content(换行分隔,带 [HH:MM] 前缀)
|
||||
* - 不涉及附件(与医生备注 DoctorNoteLogic 不同)
|
||||
*/
|
||||
class TrackingNoteLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 追加跟踪备注(按天合并)
|
||||
*
|
||||
* @param array $params diagnosis_id (int)、admin_id (int)、content (string)
|
||||
* @return bool
|
||||
*/
|
||||
public static function addOrAppend(array $params): bool
|
||||
{
|
||||
try {
|
||||
$diagnosisId = (int) ($params['diagnosis_id'] ?? 0);
|
||||
$adminId = (int) ($params['admin_id'] ?? 0);
|
||||
$newContent = trim((string) ($params['content'] ?? ''));
|
||||
|
||||
if ($diagnosisId <= 0) {
|
||||
self::setError('诊单ID缺失');
|
||||
return false;
|
||||
}
|
||||
if ($newContent === '') {
|
||||
self::setError('备注内容不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$time = date('H:i');
|
||||
$line = "[{$time}] {$newContent}";
|
||||
|
||||
$existing = TrackingNote::where('diagnosis_id', $diagnosisId)
|
||||
->where('note_date', $today)
|
||||
->whereNull('delete_time')
|
||||
->find();
|
||||
|
||||
if ($existing) {
|
||||
$prev = trim((string) ($existing->content ?? ''));
|
||||
$existing->content = $prev !== '' ? ($prev . "\n" . $line) : $line;
|
||||
$existing->save();
|
||||
} else {
|
||||
TrackingNote::create([
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'admin_id' => $adminId,
|
||||
'note_date' => $today,
|
||||
'content' => $line,
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 diagnosis_id 获取跟踪备注列表(note_date DESC)
|
||||
*
|
||||
* @param int $diagnosisId
|
||||
* @param int $limit 最近 N 天
|
||||
* @return array
|
||||
*/
|
||||
public static function getByDiagnosis(int $diagnosisId, int $limit = 60): array
|
||||
{
|
||||
try {
|
||||
if ($diagnosisId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$records = TrackingNote::where('diagnosis_id', $diagnosisId)
|
||||
->whereNull('delete_time')
|
||||
->order('note_date', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return $records;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||
// | 开源版本可自由商用,可去除界面版权logo
|
||||
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||
// | 访问官网:https://www.likeadmin.cn
|
||||
// | likeadmin团队 版权所有 拥有最终解释权
|
||||
// +----------------------------------------------------------------------
|
||||
// | author: likeadminTeam
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\validate\tcm;
|
||||
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\DiagnosisTodo;
|
||||
use app\common\validate\BaseValidate;
|
||||
|
||||
/**
|
||||
* 诊单待办事项验证
|
||||
* Class DiagnosisTodoValidate
|
||||
* @package app\adminapi\validate\tcm
|
||||
*/
|
||||
class DiagnosisTodoValidate extends BaseValidate
|
||||
{
|
||||
protected $rule = [
|
||||
'id' => 'require|number',
|
||||
'diagnosis_id' => 'require|number|gt:0|checkDiagnosis',
|
||||
'content' => 'require|length:1,500',
|
||||
'remind_time' => 'require|number|checkRemindFuture',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'id.require' => '参数缺失',
|
||||
'diagnosis_id.require' => '诊单 id 不能为空',
|
||||
'diagnosis_id.gt' => '诊单 id 非法',
|
||||
'content.require' => '请输入提醒内容',
|
||||
'content.length' => '提醒内容长度需在 1-500 字',
|
||||
'remind_time.require' => '请选择提醒时间',
|
||||
'remind_time.number' => '提醒时间格式错误',
|
||||
];
|
||||
|
||||
public function sceneAdd()
|
||||
{
|
||||
return $this->only(['diagnosis_id', 'content', 'remind_time']);
|
||||
}
|
||||
|
||||
public function sceneCancel()
|
||||
{
|
||||
return $this->only(['id']);
|
||||
}
|
||||
|
||||
public function sceneDetail()
|
||||
{
|
||||
return $this->only(['id']);
|
||||
}
|
||||
|
||||
public function sceneList()
|
||||
{
|
||||
return $this->only(['diagnosis_id'])
|
||||
->append('diagnosis_id', 'require|number|gt:0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 诊单存在校验
|
||||
*/
|
||||
protected function checkDiagnosis($value): bool|string
|
||||
{
|
||||
$diagnosis = Diagnosis::findOrEmpty((int) $value);
|
||||
if ($diagnosis->isEmpty()) {
|
||||
return '诊单不存在';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提醒时间必须在当前时间之后(至少 30 秒),避免立刻就过期
|
||||
*/
|
||||
protected function checkRemindFuture($value): bool|string
|
||||
{
|
||||
$ts = (int) $value;
|
||||
if ($ts <= time() + 30) {
|
||||
return '提醒时间必须晚于当前时间';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,10 @@ class DiagnosisValidate extends BaseValidate
|
||||
'tongue_images' => 'array',
|
||||
'report_files' => 'array',
|
||||
'diabetes_discovery_year' => 'max:50',
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date|checkDateRange',
|
||||
'diagnosis_id' => 'require|integer|checkDiagnosisId',
|
||||
'tracking_content' => 'require|length:1,1000',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
@@ -55,6 +59,11 @@ class DiagnosisValidate extends BaseValidate
|
||||
'status.in' => '状态参数错误',
|
||||
'current_medications.max' => '在用药物最多2000个字符',
|
||||
'diabetes_discovery_year.max' => '发现糖尿病患病史最多50个字符',
|
||||
'start_date.date' => '开始日期格式不正确',
|
||||
'end_date.date' => '结束日期格式不正确',
|
||||
'diagnosis_id.require' => '诊单ID不能为空',
|
||||
'tracking_content.require' => '跟踪备注内容不能为空',
|
||||
'tracking_content.length' => '跟踪备注最多1000个字符',
|
||||
];
|
||||
|
||||
public function sceneAdd()
|
||||
@@ -71,7 +80,29 @@ class DiagnosisValidate extends BaseValidate
|
||||
{
|
||||
return $this->only(['id']);
|
||||
}
|
||||
|
||||
|
||||
public function sceneReadonlyDetail()
|
||||
{
|
||||
return $this->only(['id']);
|
||||
}
|
||||
|
||||
public function sceneTrackingWindow()
|
||||
{
|
||||
return $this->only(['id', 'start_date', 'end_date']);
|
||||
}
|
||||
|
||||
/** 新增跟踪备注:诊单ID + 备注内容 */
|
||||
public function sceneAddTrackingNote()
|
||||
{
|
||||
return $this->only(['diagnosis_id', 'tracking_content']);
|
||||
}
|
||||
|
||||
/** 拉取跟踪备注列表:诊单ID */
|
||||
public function sceneTrackingNotes()
|
||||
{
|
||||
return $this->only(['diagnosis_id']);
|
||||
}
|
||||
|
||||
public function sceneGenerateQrcode()
|
||||
{
|
||||
return $this->only(['diagnosis_id', 'doctor_id', 'patient_id', 'share_user_id', 'mini_program_path'])
|
||||
@@ -107,6 +138,26 @@ class DiagnosisValidate extends BaseValidate
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 校验 diagnosis_id 字段(addTrackingNote 等场景使用,避开 id 字段) */
|
||||
protected function checkDiagnosisId($value)
|
||||
{
|
||||
$diagnosis = Diagnosis::findOrEmpty($value);
|
||||
if ($diagnosis->isEmpty()) {
|
||||
return '诊单不存在';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** end_date 不能早于 start_date */
|
||||
protected function checkDateRange($value, $rule, $data = [])
|
||||
{
|
||||
$start = $data['start_date'] ?? '';
|
||||
if ($start && $value && strtotime($value) < strtotime($start)) {
|
||||
return '结束日期不能早于开始日期';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 视频场景需 doctor_id,确认诊单需 diagnosis_id */
|
||||
protected function checkQrcodeIds($value, $rule, $data = [])
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user