This commit is contained in:
Your Name
2026-04-30 14:04:12 +08:00
parent eb782305e6
commit d3003ce0ac
19 changed files with 1560 additions and 313 deletions
@@ -7,6 +7,7 @@ use app\common\model\doctor\Appointment;
use app\common\model\doctor\Roster;
use app\common\service\doctor\RosterSegmentService;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\DiagnosisGuahaoLog;
use app\common\model\auth\Admin;
use app\api\logic\ChatNotifyLogic;
use app\adminapi\logic\tcm\PrescriptionLogic;
@@ -183,7 +184,7 @@ class AppointmentLogic extends BaseLogic
* @param array $params
* @return array|bool
*/
public static function create(array $params)
public static function create(array $params, int $operatorAdminId = 0, array $operatorAdminInfo = [])
{
try {
Db::startTrans();
@@ -244,6 +245,25 @@ class AppointmentLogic extends BaseLogic
$appointment = Appointment::create($data);
Db::commit();
$doctorName = (string) (Admin::where('id', (int) $params['doctor_id'])->value('name') ?? '');
$hm = substr((string) $appointmentTime, 0, 5);
$summary = sprintf(
'挂号 #%d:医生「%s」,%s %s',
(int) $appointment->id,
$doctorName !== '' ? $doctorName : ('ID:' . (int) $params['doctor_id']),
(string) $params['appointment_date'],
$hm
);
self::appendDiagnosisGuahaoLog(
(int) $params['patient_id'],
(int) $appointment->id,
$operatorAdminId,
$operatorAdminInfo,
'create',
$summary
);
return ['id' => $appointment->id];
} catch (\Exception $e) {
Db::rollback();
@@ -257,7 +277,7 @@ class AppointmentLogic extends BaseLogic
* @param array $params
* @return bool
*/
public static function cancel(array $params)
public static function cancel(array $params, int $operatorAdminId = 0, array $operatorAdminInfo = [])
{
try {
$appointment = Appointment::findOrEmpty($params['id']);
@@ -279,10 +299,34 @@ class AppointmentLogic extends BaseLogic
return false;
}
$diagId = (int) $appointment->patient_id;
$aptId = (int) $appointment->id;
$doctorId = (int) $appointment->doctor_id;
$aptDate = (string) ($appointment->appointment_date ?? '');
$aptTimeRaw = (string) ($appointment->appointment_time ?? '');
$hm = strlen($aptTimeRaw) >= 5 ? substr($aptTimeRaw, 0, 5) : $aptTimeRaw;
$appointment->status = 2; // 已取消
$appointment->update_time = time();
$appointment->save();
$doctorName = (string) (Admin::where('id', $doctorId)->value('name') ?? '');
$summary = sprintf(
'取消挂号 #%d:医生「%s」,%s %s',
$aptId,
$doctorName !== '' ? $doctorName : ('ID:' . $doctorId),
$aptDate,
$hm
);
self::appendDiagnosisGuahaoLog(
$diagId,
$aptId,
$operatorAdminId,
$operatorAdminInfo,
'cancel',
$summary
);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
@@ -444,4 +488,37 @@ class AppointmentLogic extends BaseLogic
return false;
}
}
/**
* 诊单列表维度:记录谁在后台发起挂号 / 取消挂号(写入失败不影响主流程)
*/
private static function appendDiagnosisGuahaoLog(
int $diagnosisId,
int $appointmentId,
int $adminId,
array $adminInfo,
string $action,
string $summary
): void {
if ($diagnosisId <= 0) {
return;
}
$adminName = (string) ($adminInfo['name'] ?? '');
if ($adminName === '' && $adminId > 0) {
$adminName = (string) (Admin::where('id', $adminId)->value('name') ?? '');
}
try {
$log = new DiagnosisGuahaoLog();
$log->diagnosis_id = $diagnosisId;
$log->appointment_id = $appointmentId;
$log->admin_id = $adminId;
$log->admin_name = mb_substr($adminName, 0, 64);
$log->action = mb_substr($action, 0, 32);
$log->summary = mb_substr($summary, 0, 500);
$log->create_time = time();
$log->save();
} catch (\Throwable $e) {
Log::warning('诊单挂号日志写入失败: ' . $e->getMessage());
}
}
}
@@ -14,7 +14,7 @@ use think\facade\Db;
* · 与 qywx.customer/todayArrival、客户列表同源事件表;选定渠道时客户须带对应企微标签(contact_tag);**二中心展示部门在选定渠道下计 0**
* - 接诊诊单 = 已完成挂号 status=3,医助 role_id=2;按 TIMESTAMP(appointment_date, appointment_time) 落入统计区间(挂号条数,非订单)。
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag/creator 两套口径均按订单计,非挂号条数。
* - 合计业绩 = 区间内 zyt_tcm_prescription_order.amount 求和(排除已取消(4) 与软删),按 creator_id 归属到「中心」展示部门;
* - 合计业绩 = 区间内 zyt_tcm_prescription_order.amount 求和(排除已取消(4) 与软删),按 dg.assistant_id 归属到「中心」展示部门;
* 无法归属到任一展示部门的金额单列「未归属中心」行;合计行 = 与列表同条件的全表 SUM(不限部门)
* · 时间窗与业绩条件与列表 stats_order_amount_performance 一致;列表若另有患者/单号等筛选或账号数据域收窄,金额可能仍不同
* · 选定渠道时「{渠道}业绩」列仍按比例展示;合计列全量与渠道列含未归属补差;**名称含「二中心」的展示部门不参与该渠道进线/业绩/成交单/ROI 分子**
@@ -348,7 +348,7 @@ class YejiStatsLogic
/**
* 医助排行榜:按展示部门分表,每表内按诊金降序排名。
* 诊金口径与 overview 部门表一致(无渠道=合计业绩/creator;有渠道={渠道}业绩/dict 诊单医助或 tag/creator;二中心在选渠道下注 0
* 诊金口径:无渠道=诊单 dg.assistant_id 业绩(与列表 assistant_id 筛选一致);有渠道={渠道}业绩/dict 诊单医助;二中心在选渠道下注 0。
* 接诊率 = 排行用诊金 ÷ 进线条数(元/进线,1 位小数;进线为 0 时为 0)。
*
* @param array{start_date?:string,end_date?:string,dept_ids?:int[]|string,channel_code?:string,tag_id?:string} $params
@@ -431,7 +431,7 @@ class YejiStatsLogic
$rangeNote = $channelFilterActive
? ('排行诊金 = 选定渠道「' . $channelName . '」业绩口径(与部门表该渠道业绩列一致)。接诊率 = 诊金 ÷ 进线(元/进线)。'
. '「二中心」医助在该渠道下诊金与进线计 0。')
: '排行诊金 = 合计业绩口径(creator,与部门表「合计业绩」一致)。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。';
: '排行诊金 = 处方订单列表 assistant_id 口径(诊单医助业绩,与列表按医助筛选一致)。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。';
$leaderboards = [];
foreach ($primaryDeptIds as $deptId) {
@@ -1084,7 +1084,7 @@ class YejiStatsLogic
}
/**
* 业绩按 creator_id 聚合到 admin(与 aggregatePerformance 同源 WHERE)。
* 业绩按 dg.assistant_id 聚合到 admin(与处方订单列表 assistant_id 筛选口径一致)。
*
* @return array<int, float>
*/
@@ -1100,19 +1100,23 @@ class YejiStatsLogic
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
return [];
}
$diagTable = self::tableWithPrefix('tcm_diagnosis');
$query = Db::name('tcm_prescription_order')
->alias('o')
->join("{$diagTable} dg", 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
->whereNull('o.delete_time')
->where('o.create_time', 'between', [$startTs, $endTs]);
->where('o.create_time', 'between', [$startTs, $endTs])
->where('o.diagnosis_id', '>', 0)
->where('dg.assistant_id', '>', 0);
self::applyPrescriptionOrderNotCancelledForPerformance($query);
$query->field(['o.creator_id', 'SUM(o.amount) AS amt'])
->group('o.creator_id');
$query->field(['dg.assistant_id', 'SUM(o.amount) AS amt'])
->group('dg.assistant_id');
if ($tagDiagIds !== null) {
$query->whereIn('o.diagnosis_id', $tagDiagIds);
}
if ($tagAssistantIds !== null) {
$query->whereIn('o.creator_id', array_keys($tagAssistantIds));
$query->whereIn('dg.assistant_id', array_keys($tagAssistantIds));
}
$rows = $query->select()->toArray();
@@ -1121,11 +1125,11 @@ class YejiStatsLogic
}
$byAdmin = [];
foreach ($rows as $r) {
$cid = (int) ($r['creator_id'] ?? 0);
if ($cid <= 0) {
$aid = (int) ($r['assistant_id'] ?? 0);
if ($aid <= 0) {
continue;
}
$byAdmin[$cid] = ($byAdmin[$cid] ?? 0) + (float) $r['amt'];
$byAdmin[$aid] = ($byAdmin[$aid] ?? 0) + (float) $r['amt'];
}
return $byAdmin;
@@ -16,6 +16,7 @@ namespace app\adminapi\logic\tcm;
use app\common\logic\BaseLogic;
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\DiagnosisViewRecord;
@@ -3296,4 +3297,36 @@ class DiagnosisLogic extends BaseLogic
],
];
}
/**
* @notes 诊单挂号 / 取消挂号 操作日志(后台)
*
* @return array<int, array<string, mixed>>
*/
public static function guahaoLogList(int $diagnosisId): array
{
self::$error = '';
$exists = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->find();
if (!$exists) {
self::setError('诊单不存在');
return [];
}
$logs = DiagnosisGuahaoLog::where('diagnosis_id', $diagnosisId)
->order('id', 'desc')
->limit(200)
->select()
->toArray();
foreach ($logs as &$r) {
$t = (int) ($r['create_time'] ?? 0);
$r['create_time_text'] = $t > 0 ? date('Y-m-d H:i:s', $t) : '';
$act = (string) ($r['action'] ?? '');
$r['action_desc'] = $act === 'cancel' ? '取消挂号' : ($act === 'create' ? '挂号' : $act);
}
unset($r);
return $logs;
}
}
@@ -382,10 +382,13 @@ class PrescriptionLogic
'dietary_taboo' => is_array($params['dietary_taboo'] ?? null) ? implode(',', $params['dietary_taboo']) : ($params['dietary_taboo'] ?? $prescription->dietary_taboo),
'usage_notes' => $params['usage_notes'] ?? $prescription->usage_notes,
'doctor_name' => $params['doctor_name'] ?? $prescription->doctor_name,
'doctor_signature' => $params['doctor_signature'] ?? $prescription->doctor_signature,
'is_shared' => (int)($params['is_shared'] ?? $prescription->is_shared),
'visible_role_ids' => array_key_exists('visible_role_ids', $params)
? self::normalizeVisibleRoleIds($params['visible_role_ids'])
: (string) ($prescription->visible_role_ids ?? ''),
// 后台编辑保存后视为手工处方(原系统代开标记清除)
'is_system_auto' => 0,
// 开方医生修改后重新进入待审核
'audit_status' => 0,
'audit_time' => null,
@@ -13,6 +13,7 @@ use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderLog;
use app\common\model\tcm\PrescriptionOrderPayOrder;
use app\common\model\doctor\Appointment;
use app\common\model\auth\Admin;
use app\common\service\ExpressTrackService;
use app\common\service\gancao\GancaoScmRecipelService;
@@ -706,9 +707,176 @@ class PrescriptionOrderLogic
}
$arr['diagnosis_creator_dept_path'] = $deptPath;
// 挂号预约摘要:优先处方 appointment_id;否则按诊单 id 匹配预约表 patient_id(诊单)
$arr['linked_appointment'] = null;
$rxApptId = 0;
if (isset($arr['prescription']) && is_array($arr['prescription'])) {
$rxApptId = (int) ($arr['prescription']['appointment_id'] ?? 0);
}
$resolvedApptId = $rxApptId;
if ($resolvedApptId <= 0 && $diagIdForMeta > 0) {
$apFallback = Appointment::where('patient_id', $diagIdForMeta)->order('id', 'desc')->find();
if ($apFallback !== null) {
$resolvedApptId = (int) $apFallback->id;
}
}
if ($resolvedApptId > 0) {
$brief = self::buildAppointmentBrief($resolvedApptId);
if ($brief !== null) {
$brief['resolved_from'] = $rxApptId === $resolvedApptId ? 'prescription' : 'diagnosis';
$arr['linked_appointment'] = $brief;
}
}
return $arr;
}
/**
* 业务订单详情场景:仅更新关联处方的患者姓名与手机号(不重置消费者处方审核、不改变其它处方字段)
*/
public static function patchPrescriptionPatient(
int $prescriptionOrderId,
string $patientName,
string $phone,
int $adminId,
array $adminInfo
): bool {
self::$error = '';
$order = PrescriptionOrder::where('id', $prescriptionOrderId)->whereNull('delete_time')->find();
if (!$order) {
self::setError('订单不存在');
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::setError('无权限操作');
return false;
}
$rxId = (int) ($order->prescription_id ?? 0);
if ($rxId <= 0) {
self::setError('该订单未关联处方');
return false;
}
$rx = Prescription::where('id', $rxId)->whereNull('delete_time')->find();
if (!$rx) {
self::setError('处方不存在');
return false;
}
if (!PrescriptionLogic::canViewPrescription($rx, $adminId, $adminInfo)) {
self::setError('无权限修改此处方');
return false;
}
$patientName = trim($patientName);
$phone = trim($phone);
if ($patientName === '') {
self::setError('患者姓名不能为空');
return false;
}
if (mb_strlen($patientName) > 50) {
self::setError('患者姓名过长');
return false;
}
if ($phone === '') {
self::setError('手机号不能为空');
return false;
}
if (strlen($phone) > 20) {
self::setError('手机号过长');
return false;
}
$oldName = trim((string) ($rx->patient_name ?? ''));
$oldPhone = trim((string) ($rx->phone ?? ''));
try {
$rx->save([
'patient_name' => $patientName,
'phone' => $phone,
]);
$nameFrom = $oldName === '' ? '—' : $oldName;
$phoneFrom = $oldPhone === '' ? '—' : $oldPhone;
$summary = sprintf(
'关联处方 #%d:患者姓名「%s」→「%s」,手机「%s」→「%s」',
$rxId,
$nameFrom,
$patientName,
$phoneFrom,
$phone
);
self::writeLog($prescriptionOrderId, $adminId, $adminInfo, 'patch_rx_patient', $summary);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* 挂号预约简要信息(业务订单详情展示)
*
* @return array<string, mixed>|null
*/
private static function buildAppointmentBrief(int $appointmentId): ?array
{
if ($appointmentId <= 0) {
return null;
}
$ap = Appointment::find($appointmentId);
if ($ap === null) {
return null;
}
$row = $ap->toArray();
$doctorName = '';
$did = (int) ($row['doctor_id'] ?? 0);
if ($did > 0) {
$doctorName = (string) (Admin::where('id', $did)->value('name') ?? '');
}
$statusMap = [
1 => '已预约',
2 => '已取消',
3 => '已完成',
4 => '已过号',
];
$typeMap = [
'video' => '视频问诊',
'text' => '图文问诊',
'phone' => '电话问诊',
];
$periodRaw = (string) ($row['period'] ?? '');
$period = $periodRaw === 'afternoon' ? '下午' : ($periodRaw === 'morning' ? '上午' : $periodRaw);
$timePart = (string) ($row['appointment_time'] ?? '');
if (strlen($timePart) > 5) {
$timePart = substr($timePart, 0, 5);
}
$status = (int) ($row['status'] ?? 0);
$atype = (string) ($row['appointment_type'] ?? '');
return [
'id' => $appointmentId,
'appointment_date' => (string) ($row['appointment_date'] ?? ''),
'period' => $period,
'appointment_time' => $timePart,
'appointment_type' => $atype,
'appointment_type_desc' => $typeMap[$atype] ?? '未知',
'status' => $status,
'status_desc' => $statusMap[$status] ?? '未知',
'doctor_name' => $doctorName,
'channel_source' => (string) ($row['channel_source'] ?? ''),
'remark' => (string) ($row['remark'] ?? ''),
];
}
/**
* 物流轨迹(顺丰/京东等,优先快递100;未配置密钥时返回官网链接)
*