$cols Db::name('doctor_appointment')->getTableFields() */ private static function assertAppointmentChannelWritable(array $cols, string $chSrc): ?string { $hasChannelSource = in_array('channel_source', $cols, true); $hasChannels = in_array('channels', $cols, true); if (!$hasChannelSource && !$hasChannels) { return '挂号表缺少渠道字段(channel_source 或 channels),无法保存渠道来源'; } if (!$hasChannelSource && $hasChannels && $chSrc !== '' && !is_numeric($chSrc)) { return '当前挂号表仅有数值型渠道字段 channels,无法写入该渠道;请在数据库增加 channel_source 列'; } return null; } /** * @param array $row * @param array $cols */ private static function mergeAppointmentChannelIntoRow(array &$row, array $cols, string $chSrc, string $chDetail): void { $hasChannelSource = in_array('channel_source', $cols, true); $hasChannelDetail = in_array('channel_source_detail', $cols, true); $hasChannels = in_array('channels', $cols, true); if ($hasChannelSource) { $row['channel_source'] = $chSrc; } if ($hasChannelDetail) { $row['channel_source_detail'] = $chDetail; } if ($hasChannels && is_numeric($chSrc)) { $row['channels'] = (int) $chSrc; } } /** * @param array $row * @param array $cols * * @return array */ private static function filterAppointmentRowByExistingColumns(array $row, array $cols): array { $out = []; foreach ($row as $k => $v) { if (in_array((string) $k, $cols, true)) { $out[$k] = $v; } } return $out; } /** * @notes 获取可用时间段 * @param array $params * @return array */ public static function getAvailableSlots(array $params) { try { $doctorId = $params['doctor_id']; $date = $params['appointment_date']; $period = $params['period'] ?? 'all'; // 记录请求参数 \think\facade\Log::info('获取可用时段 - 请求参数', [ 'doctor_id' => $doctorId, 'date' => $date, 'period' => $period ]); // 1. 检查医生在该日期是否有出诊排班(只查询status=1的记录) $rosters = Roster::where([ 'doctor_id' => $doctorId, 'date' => $date, 'status' => 1 // 只查询出诊状态 ])->select()->toArray(); // 记录查询到的排班 \think\facade\Log::info('查询到的排班数据', [ 'doctor_id' => $doctorId, 'date' => $date, 'count' => count($rosters), 'rosters' => $rosters ]); // 额外记录:查询该日期所有排班(用于调试) $allRosters = Roster::where([ 'doctor_id' => $doctorId, 'date' => $date ])->select()->toArray(); \think\facade\Log::info('该日期所有排班数据(包括非出诊)', [ 'count' => count($allRosters), 'all_rosters' => $allRosters ]); // 如果没有出诊排班,返回空数组 if (empty($rosters)) { \think\facade\Log::warning('没有找到出诊排班', [ 'doctor_id' => $doctorId, 'date' => $date ]); return ['slots' => []]; } // 2. 按每段排班的起止时间与号源间隔生成时间段 $slots = []; foreach ($rosters as $roster) { if ((int) $roster['status'] !== 1) { \think\facade\Log::warning('跳过非出诊排班', [ 'roster_id' => $roster['id'], 'status' => $roster['status'], ]); continue; } $window = RosterSegmentService::resolveWindow($roster); if ($window === null) { \think\facade\Log::warning('无法解析排班起止时间,跳过', [ 'roster_id' => $roster['id'], 'period' => $roster['period'] ?? null, ]); continue; } [$startHm, $endHm] = $window; $slotMinutes = RosterSegmentService::normalizeSlotMinutes($roster['slot_minutes'] ?? 15); $times = RosterSegmentService::generateSlotTimes($startHm, $endHm, $slotMinutes); $times = RosterSegmentService::applyQuotaCap($times, (int) ($roster['quota'] ?? 0)); \think\facade\Log::info('处理排班时段', [ 'roster_id' => $roster['id'], 'window' => $window, 'slot_minutes' => $slotMinutes, 'slot_count' => count($times), ]); foreach ($times as $time) { $slots[] = [ 'time' => $time, 'available' => true, 'quota' => 1, 'period' => $roster['period'] ?? 'segment', ]; } } \think\facade\Log::info('生成的时间段数量(去重前)', [ 'count' => count($slots), 'sample_slots' => array_slice($slots, 0, 3) ]); // 如果没有生成任何时间段,返回空数组 if (empty($slots)) { \think\facade\Log::warning('没有生成任何时间段'); return ['slots' => []]; } // 3. 去重:按时间去重(因为可能有重复的排班记录) $uniqueSlots = []; $timeMap = []; foreach ($slots as $slot) { $time = $slot['time']; if (!isset($timeMap[$time])) { $timeMap[$time] = true; $uniqueSlots[] = $slot; } } $slots = $uniqueSlots; \think\facade\Log::info('生成的时间段数量(去重后)', [ 'count' => count($slots), 'first_3_slots' => array_slice($slots, 0, 3), 'last_3_slots' => array_slice($slots, -3), ]); // 4. 查询已预约的时间段 $appointmentTimes = Appointment::where([ 'doctor_id' => $doctorId, 'appointment_date' => $date, 'status' => 1 // 只查询有效预约 ])->column('appointment_time'); // 将时间格式统一为 HH:MM(去掉秒) $appointments = array_map(function($time) { // 如果是 HH:MM:SS 格式,截取前5位 return substr($time, 0, 5); }, $appointmentTimes); // 4. 标记已占用的时间段 foreach ($slots as &$slot) { if (in_array($slot['time'], $appointments)) { $slot['available'] = false; $slot['quota'] = 0; } } // 5. 按时间排序 usort($slots, function($a, $b) { return strcmp($a['time'], $b['time']); }); return ['slots' => $slots]; } catch (\Exception $e) { self::setError($e->getMessage()); return ['slots' => []]; } } /** * @notes 创建预约 * @param array $params * @return array|bool */ public static function create(array $params, int $operatorAdminId = 0, array $operatorAdminInfo = []) { try { Db::startTrans(); // 同一诊单患者在「所选预约日」仅允许一条「已预约」或「已过号」记录(与 appointment_date 一致,不能误用服务器当天拦其它日期) $apptDate = trim((string) ($params['appointment_date'] ?? '')); if ($apptDate === '') { self::setError('预约日期不能为空'); Db::rollback(); return false; } $patientSameDay = Appointment::where('patient_id', (int) $params['patient_id']) ->where('appointment_date', $apptDate) ->whereIn('status', [1, 4]) ->find(); if ($patientSameDay) { self::setError('该患者在所选日期已有挂号(已预约或已过号),无法重复预约'); Db::rollback(); return false; } // 1. 检查时间段是否已被预约(每个时间段只能预约1次) // 统一时间格式为 HH:MM:SS $appointmentTime = strlen($params['appointment_time']) == 5 ? $params['appointment_time'] . ':00' : $params['appointment_time']; $exists = Appointment::where([ 'doctor_id' => $params['doctor_id'], 'appointment_date' => $params['appointment_date'], 'status' => 1 ])->where('appointment_time', $appointmentTime) ->find(); if ($exists) { self::setError('该时间段已被预约'); Db::rollback(); return false; } $chSrc = trim((string) ($params['channel_source'] ?? '')); $chDetail = trim((string) ($params['channel_source_detail'] ?? '')); $channelDictName = trim((string) (DictData::where('type_value', 'channels') ->where('value', $chSrc) ->value('name') ?? '')); if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) { if ($chDetail === '') { self::setError('请填写自媒体渠道补充信息'); Db::rollback(); return false; } } $cols = Db::name('doctor_appointment')->getTableFields(); $cols = is_array($cols) ? $cols : []; $channelErr = self::assertAppointmentChannelWritable($cols, $chSrc); if ($channelErr !== null) { self::setError($channelErr); Db::rollback(); return false; } $insert = [ 'patient_id' => (int) $params['patient_id'], 'doctor_id' => (int) $params['doctor_id'], 'appointment_date' => $params['appointment_date'], 'appointment_time' => $appointmentTime, 'appointment_type' => $params['appointment_type'] ?? 'video', 'remark' => $params['remark'] ?? '', 'status' => 1, 'create_time' => time(), 'update_time' => time(), ]; if (in_array('roster_id', $cols, true)) { $insert['roster_id'] = 0; } if (in_array('assistant_id', $cols, true)) { $insert['assistant_id'] = (int) ($params['assistant_id'] ?? 0); } $periodVal = $params['period'] ?? 'all'; if (in_array('period', $cols, true)) { $insert['period'] = $periodVal; } elseif (in_array('type', $cols, true)) { $insert['type'] = $periodVal; } self::mergeAppointmentChannelIntoRow($insert, $cols, $chSrc, $chDetail); $insert = self::filterAppointmentRowByExistingColumns($insert, $cols); $newId = (int) Db::name('doctor_appointment')->insertGetId($insert); if ($newId <= 0) { self::setError('预约创建失败'); Db::rollback(); return false; } 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', $newId, $doctorName !== '' ? $doctorName : ('ID:' . (int) $params['doctor_id']), (string) $params['appointment_date'], $hm ); self::appendDiagnosisGuahaoLog( (int) $params['patient_id'], $newId, $operatorAdminId, $operatorAdminInfo, 'create', $summary ); return ['id' => $newId]; } catch (\Exception $e) { Db::rollback(); self::setError($e->getMessage()); return false; } } /** * @notes 取消预约 * @param array $params * @return bool */ public static function cancel(array $params, int $operatorAdminId = 0, array $operatorAdminInfo = []) { try { $appointment = Appointment::findOrEmpty($params['id']); if ($appointment->isEmpty()) { self::setError('预约记录不存在'); return false; } $st = (int) $appointment->status; if ($st === 2) { return true; } if ($st === 3) { self::setError('预约已完成,无法取消'); return false; } if ($st !== 1 && $st !== 4) { self::setError('当前状态不可取消'); 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()); return false; } } /** * @notes 预约详情 * @param array $params * @return array */ public static function detail(array $params) { $appointment = Appointment::alias('a') ->leftJoin('zyt_tcm_diagnosis u', 'a.patient_id = u.id') ->leftJoin('zyt_admin ad', 'a.doctor_id = ad.id') ->field('a.*, u.patient_name as patient_name, u.phone as patient_phone, ad.name as doctor_name') ->where('a.id', $params['id']) ->findOrEmpty() ->toArray(); if (empty($appointment)) { return []; } // 添加状态和类型描述 $statusMap = [ 1 => '已预约', 2 => '已取消', 3 => '已完成', ]; $appointment['status_desc'] = $statusMap[$appointment['status']] ?? '未知'; $typeMap = [ 'video' => '视频问诊', 'text' => '图文问诊', 'phone' => '电话问诊', ]; $appointment['appointment_type_desc'] = $typeMap[$appointment['appointment_type']] ?? '未知'; // 格式化时间戳为日期时间 if (isset($appointment['create_time']) && is_numeric($appointment['create_time'])) { $appointment['create_time'] = date('Y-m-d H:i:s', $appointment['create_time']); } if (isset($appointment['update_time']) && is_numeric($appointment['update_time'])) { $appointment['update_time'] = date('Y-m-d H:i:s', $appointment['update_time']); } return $appointment; } /** * @notes 获取医生某天的可用号源总数 * @param int $doctorId * @param string $date * @return int */ public static function getDoctorAvailability($doctorId, $date) { try { $rosters = Roster::where([ 'doctor_id' => $doctorId, 'date' => $date, 'status' => 1, ])->select()->toArray(); if (empty($rosters)) { return 0; } $bookedHm = Appointment::where([ 'doctor_id' => $doctorId, 'appointment_date' => $date, 'status' => 1, ])->column('appointment_time'); $bookedHm = array_map(static function ($t) { return substr((string) $t, 0, 5); }, $bookedHm); $bookedSet = array_fill_keys($bookedHm, true); $totalAvailable = 0; foreach ($rosters as $roster) { $window = RosterSegmentService::resolveWindow($roster); if ($window === null) { continue; } [$startHm, $endHm] = $window; $slotMinutes = RosterSegmentService::normalizeSlotMinutes($roster['slot_minutes'] ?? 15); $times = RosterSegmentService::generateSlotTimes($startHm, $endHm, $slotMinutes); $times = RosterSegmentService::applyQuotaCap($times, (int) ($roster['quota'] ?? 0)); foreach ($times as $t) { if (empty($bookedSet[$t])) { ++$totalAvailable; } } } return $totalAvailable; } catch (\Exception $e) { return 0; } } /** * @notes 完成预约 * @param array $params * @return bool */ public static function complete(array $params) { try { $appointment = Appointment::findOrEmpty($params['id']); if ($appointment->isEmpty()) { self::setError('预约记录不存在'); return false; } if ($appointment->status == 3) { self::setError('该预约已处理,无法完成'); return false; } $appointment->status = 3; // 已完成 $appointment->update_time = time(); $appointment->save(); // 面诊结束:通知对应医助(从诊单 assistant_id 获取) try { $diagnosis = Diagnosis::where('id', $appointment->patient_id)->find(); if ($diagnosis && !empty($diagnosis->assistant_id)) { $assistantId = (int) $diagnosis->assistant_id; $patientName = $diagnosis->patient_name ?? '患者'; $doctor = Admin::where('id', $appointment->doctor_id)->find(); $doctorName = $doctor ? $doctor->name : '医生'; ChatNotifyLogic::addConsultationCompleteNotify( $assistantId, $patientName, $doctorName, (int) $appointment->patient_id ); } } catch (\Throwable $e) { \think\facade\Log::warning('面诊结束通知医助失败: ' . $e->getMessage()); } // 完成就诊后系统代开处方(无药材、标记 is_system_auto;失败不影响「完成」成功) try { PrescriptionLogic::createSystemAutoFromCompletedAppointment((int) $appointment->id); } catch (\Throwable $e) { Log::warning('完成挂号后系统代开处方失败: ' . $e->getMessage()); } return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * @notes 接诊台聚合:单次返回挂号信息 + 诊单病例 + 血糖血压记录 + 助理姓名 * 数据复用:detail() / DiagnosisLogic::detail() / BloodRecordLogic::getRecordsByPatient() * 备注:appointment.patient_id 实际对应 tcm_diagnosis.id * @param array $params * @return array */ public static function reception(array $params): array { // 1) 挂号详情(已包含 patient_name / patient_phone / doctor_name / status_desc 等) $appointment = self::detail($params); if (empty($appointment)) { return []; } // 2) 诊单(病例)详情:appointment.patient_id == tcm_diagnosis.id $diagnosisId = (int) ($appointment['patient_id'] ?? 0); $diagnosis = $diagnosisId > 0 ? DiagnosisLogic::detail(['id' => $diagnosisId]) : []; // 3) 跟踪信息(血糖血压 / 饮食 / 运动)改为前端按窗口 lazy load, // 详见 DiagnosisController::trackingWindow。此处不再一次性返回。 // 4) 补全医助姓名(detail() 未关联 admin 助理表) $assistantName = ''; if (!empty($diagnosis['assistant_id'])) { $assistant = Admin::where('id', (int) $diagnosis['assistant_id'])->find(); if ($assistant) { $assistantName = $assistant->name ?? ''; } } $appointment['assistant_name'] = $assistantName; $appointment['assistant_id'] = $diagnosis['assistant_id'] ?? 0; // 5) 是否已开方(与列表口径一致:未删除 & 未作废) $hasPrescription = 0; if ($diagnosisId > 0) { $rxExists = Prescription::where('diagnosis_id', $diagnosisId) ->whereNull('delete_time') ->where('void_status', 0) ->find(); $hasPrescription = $rxExists ? 1 : 0; } $appointment['has_prescription'] = $hasPrescription; // 6) 对诊单字段进行字典/枚举翻译(产出 *_text 附加字段,不覆盖原 value) $diagnosis = self::enrichDiagnosisLabels($diagnosis); // 7) 医生备注 $doctorNotes = $diagnosisId > 0 ? DoctorNoteLogic::getByDiagnosis($diagnosisId) : []; // 7.1) 跟踪备注 $trackingNotes = $diagnosisId > 0 ? TrackingNoteLogic::getByDiagnosis($diagnosisId) : []; return [ 'appointment' => $appointment, 'diagnosis' => $diagnosis, 'doctor_notes' => $doctorNotes, 'tracking_notes' => $trackingNotes, ]; } /** * 字典翻译:把诊单中以 code 存储的字段(字典/枚举)补一份中文 label 字段, * 便于前端(接诊台 / 只读病例页等只读场景)直接渲染,无需加载字典。 * 原始 code 字段保留不动,edit 场景仍可使用。 * * @param array $diagnosis * @return array */ public static function enrichDiagnosisLabels(array $diagnosis): array { if (empty($diagnosis)) { return $diagnosis; } // 单值字段:value 为字典 value,翻译成字典 name $singleDictFields = [ 'diagnosis_type' => 'diagnosis_type', 'syndrome_type' => 'syndrome_type', 'diabetes_type' => 'diabetes_type', 'water_intake' => 'water_intake', 'weight_change' => 'weight_change', 'fatty_liver_degree' => 'fatty_liver_degree', ]; // 多值字段:value 为数组或逗号分隔字符串 $multiDictFields = [ 'past_history' => 'past_history', 'appetite' => 'appetite', 'diet_condition' => 'diet_condition', 'body_feeling' => 'body_feeling', 'sleep_condition' => 'sleep_condition', 'eye_condition' => 'eye_condition', 'head_feeling' => 'head_feeling', 'sweat_condition' => 'sweat_condition', 'skin_condition' => 'skin_condition', 'urine_condition' => 'urine_condition', 'stool_condition' => 'stool_condition', 'kidney_condition' => 'kidney_condition', ]; // 一次性查出用到的所有字典 $allTypes = array_values(array_unique(array_merge(array_values($singleDictFields), array_values($multiDictFields)))); $dictMap = self::buildDictMap($allTypes); foreach ($singleDictFields as $field => $type) { if (!array_key_exists($field, $diagnosis)) { continue; } $diagnosis[$field . '_text'] = self::dictLabel($dictMap, $type, $diagnosis[$field]); } foreach ($multiDictFields as $field => $type) { if (!array_key_exists($field, $diagnosis)) { continue; } $v = $diagnosis[$field]; $arr = is_array($v) ? $v : (is_string($v) && $v !== '' ? preg_split('/[,,、]/', $v) : []); $labels = []; foreach ($arr as $item) { $item = is_string($item) ? trim($item) : $item; if ($item === '' || $item === null) { continue; } $labels[] = self::dictLabel($dictMap, $type, $item); } $diagnosis[$field . '_text'] = implode('、', array_filter($labels)); } // 枚举字段 $diagnosis['gender_text'] = isset($diagnosis['gender']) ? self::genderText((int) $diagnosis['gender']) : ''; $diagnosis['marital_status_text'] = array_key_exists('marital_status', $diagnosis) ? self::maritalText($diagnosis['marital_status']) : ''; foreach (['trauma_history', 'surgery_history', 'allergy_history', 'family_history', 'pregnancy_history'] as $f) { if (array_key_exists($f, $diagnosis)) { $diagnosis[$f . '_text'] = self::yesNoText($diagnosis[$f]); } } return $diagnosis; } /** * 构建 type_value => [value => name] 的两级查询映射 * * @param array $types * @return array */ private static function buildDictMap(array $types): array { if (empty($types)) { return []; } $rows = DictData::whereIn('type_value', $types) ->field(['type_value', 'value', 'name']) ->select() ->toArray(); $map = []; foreach ($rows as $r) { $tv = (string) ($r['type_value'] ?? ''); $v = (string) ($r['value'] ?? ''); $n = (string) ($r['name'] ?? ''); if ($tv === '' || $v === '') { continue; } $map[$tv][$v] = $n; } return $map; } /** * 找不到字典项时回显原值(避免空白) */ private static function dictLabel(array $dictMap, string $type, $value): string { if ($value === null || $value === '') { return ''; } $key = (string) $value; return $dictMap[$type][$key] ?? $key; } private static function genderText(int $gender): string { if ($gender === 1) return '男'; if ($gender === 0) return '女'; return ''; } private static function maritalText($v): string { if ($v === null || $v === '') return ''; $n = (int) $v; if ($n === 0) return '未婚'; if ($n === 1) return '已婚'; if ($n === 2) return '离异'; return ''; } private static function yesNoText($v): string { if ($v === null || $v === '') return ''; return ((int) $v) === 1 ? '有' : '无'; } /** * @notes 通知接诊医助(按诊单 assistant_id 查企微 userid 后发应用消息) * 文案固定:马上面诊到你对应的患者了,请联系患者进入诊室等待。 * @param int $appointmentId * @return array{ok: bool, message?: string} */ public static function notifyAssistant(int $appointmentId): array { try { $appointment = Appointment::findOrEmpty($appointmentId); if ($appointment->isEmpty()) { return ['ok' => false, 'message' => '挂号记录不存在']; } $diagnosis = Diagnosis::where('id', (int) $appointment->patient_id)->find(); if (!$diagnosis) { return ['ok' => false, 'message' => '未找到对应的诊单']; } $assistantId = (int) ($diagnosis->assistant_id ?? 0); if ($assistantId <= 0) { return ['ok' => false, 'message' => '该患者尚未指派医助']; } $assistant = Admin::whereNull('delete_time')->find($assistantId); if (!$assistant) { return ['ok' => false, 'message' => '医助账号不存在']; } $wxId = trim((string) ($assistant->work_wechat_userid ?? '')); if ($wxId === '') { Log::warning("接诊台通知医助跳过:医助 admin_id={$assistantId} 未绑定 work_wechat_userid"); return [ 'ok' => false, 'message' => '该医助未绑定企业微信账号,无法推送消息(请其在后台「扫码绑定企业微信」后再试)', ]; } $patientName = (string) ($diagnosis->patient_name ?? '患者'); $text = "【接诊通知】马上面诊到你对应的患者了,请联系患者进入诊室等待。\n患者:{$patientName}\n时间:" . (string) $appointment->appointment_date . ' ' . (string) $appointment->appointment_time; $result = WechatWorkAppMessageService::sendTextToUser($wxId, $text); if (empty($result['ok'])) { Log::warning('接诊台通知医助失败:' . ($result['message'] ?? '')); } return $result; } catch (\Throwable $e) { Log::error('接诊台通知医助异常:' . $e->getMessage()); return ['ok' => false, 'message' => '企业微信通知异常:' . $e->getMessage()]; } } /** * 与 AppointmentLists 一致的可见性(不含 progress_board / diag_scope_relax) */ private static function appointmentRowManageableByAdmin( Appointment $appointment, ?Diagnosis $diag, int $adminId, array $adminInfo ): bool { $roleIds = array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id')); if (in_array(1, $roleIds, true) && (int) $appointment->doctor_id !== $adminId) { return false; } if (in_array(2, $roleIds, true)) { $asst = $diag ? (int) $diag->assistant_id : 0; if ($asst !== $adminId) { return false; } } if (!DataScopeService::isEnabled()) { return true; } $ids = DataScopeService::getVisibleAdminIds($adminId, $adminInfo); if ($ids === []) { return false; } if ($ids === null) { return true; } $docId = (int) $appointment->doctor_id; $asstId = $diag ? (int) $diag->assistant_id : 0; return in_array($docId, $ids, true) || ($asstId > 0 && in_array($asstId, $ids, true)); } /** * 后台编辑挂号(预约日期/时段/类型/状态/备注/医助) * * @param array $params */ public static function adminEdit(array $params, int $adminId, array $adminInfo): bool { try { $id = (int) ($params['id'] ?? 0); if ($id <= 0) { self::setError('参数错误'); return false; } $appointment = Appointment::findOrEmpty($id); if ($appointment->isEmpty()) { self::setError('预约记录不存在'); return false; } $diag = Diagnosis::where('id', (int) $appointment->patient_id)->whereNull('delete_time')->find(); if (!self::appointmentRowManageableByAdmin($appointment, $diag ?: null, $adminId, $adminInfo)) { self::setError('无权限编辑'); return false; } $period = trim((string) ($params['period'] ?? '')); if (!in_array($period, ['morning', 'afternoon', 'all'], true)) { self::setError('时段无效'); return false; } $appointmentDate = trim((string) ($params['appointment_date'] ?? '')); if ($appointmentDate === '') { self::setError('预约日期不能为空'); return false; } $rawTime = trim((string) ($params['appointment_time'] ?? '')); if ($rawTime === '') { self::setError('预约时间不能为空'); return false; } $appointmentTime = strlen($rawTime) === 5 ? $rawTime . ':00' : $rawTime; $status = (int) ($params['status'] ?? 0); if ($status < 1 || $status > 4) { self::setError('状态无效'); return false; } $appointmentType = trim((string) ($params['appointment_type'] ?? '')); if (!in_array($appointmentType, ['video', 'text', 'phone'], true)) { self::setError('预约类型无效'); return false; } $remark = isset($params['remark']) ? trim((string) $params['remark']) : ''; if (mb_strlen($remark) > 500) { self::setError('备注过长'); return false; } $chSrc = trim((string) ($params['channel_source'] ?? '')); if ($chSrc === '') { self::setError('请选择渠道来源'); return false; } $chDetail = trim((string) ($params['channel_source_detail'] ?? '')); $channelDictName = trim((string) (DictData::where('type_value', 'channels') ->where('value', $chSrc) ->value('name') ?? '')); if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) { if ($chDetail === '') { self::setError('请填写自媒体渠道补充信息'); return false; } } if (mb_strlen($chDetail) > 128) { self::setError('渠道补充说明过长'); return false; } $postAssistant = array_key_exists('assistant_id', $params); $assistantId = null; if ($postAssistant) { $assistantRaw = $params['assistant_id']; if ($assistantRaw !== null && $assistantRaw !== '') { $aid = (int) $assistantRaw; $assistantId = $aid > 0 ? $aid : null; } } $patientId = (int) $appointment->patient_id; $doctorId = (int) $appointment->doctor_id; if (in_array($status, [1, 4], true)) { $patientDup = Appointment::where('patient_id', $patientId) ->where('appointment_date', $appointmentDate) ->whereIn('status', [1, 4]) ->where('id', '<>', $id) ->find(); if ($patientDup) { self::setError('该诊单在所选日期已有其他有效挂号(已预约/已过号)'); return false; } } if ($status === 1) { $slotDup = Appointment::where('doctor_id', $doctorId) ->where('appointment_date', $appointmentDate) ->where('appointment_time', $appointmentTime) ->where('status', 1) ->where('id', '<>', $id) ->find(); if ($slotDup) { self::setError('该医生在所选时段已被占用'); return false; } } $beforeDate = (string) ($appointment->appointment_date ?? ''); $beforeTimeRaw = (string) ($appointment->appointment_time ?? ''); $beforeHm = strlen($beforeTimeRaw) >= 5 ? substr($beforeTimeRaw, 0, 5) : $beforeTimeRaw; $snap = $appointment->toArray(); $beforePeriod = (string) ($snap['period'] ?? $snap['type'] ?? ''); $beforeStatus = (int) $appointment->status; $tblFields = Db::name('doctor_appointment')->getTableFields(); $cols = is_array($tblFields) ? $tblFields : []; $channelErr = self::assertAppointmentChannelWritable($cols, $chSrc); if ($channelErr !== null) { self::setError($channelErr); return false; } Db::startTrans(); $saveData = [ 'appointment_date' => $appointmentDate, 'appointment_time' => $appointmentTime, 'appointment_type' => $appointmentType, 'status' => $status, 'remark' => $remark, 'update_time' => time(), ]; if (in_array('assistant_id', $cols, true) && $postAssistant) { $saveData['assistant_id'] = $assistantId; } if (in_array('period', $cols, true)) { $saveData['period'] = $period; } elseif (in_array('type', $cols, true)) { $saveData['type'] = $period; } self::mergeAppointmentChannelIntoRow($saveData, $cols, $chSrc, $chDetail); Db::name('doctor_appointment')->where('id', $id)->update($saveData); Db::commit(); $hm = substr($appointmentTime, 0, 5); $summary = sprintf( '后台修改挂号 #%d:日期 %s %s→%s %s,时段 %s→%s,状态 %d→%d', $id, $beforeDate, $beforeHm, $appointmentDate, $hm, $beforePeriod !== '' ? $beforePeriod : '—', $period, $beforeStatus, $status ); self::appendDiagnosisGuahaoLog($patientId, $id, $adminId, $adminInfo, 'admin_edit', $summary); return true; } catch (\Throwable $e) { Db::rollback(); self::setError($e->getMessage()); return false; } } /** * 后台批量修改挂号渠道(仅渠道号与可选补充) * * @param array $params * @return array{updated:int}|false */ public static function adminBatchEditChannel(array $params, int $adminId, array $adminInfo) { try { $idsRaw = $params['ids'] ?? []; if (!\is_array($idsRaw) || $idsRaw === []) { self::setError('请选择要修改的挂号记录'); return false; } $ids = []; foreach ($idsRaw as $raw) { $id = (int) $raw; if ($id > 0) { $ids[$id] = $id; } } $ids = array_values($ids); $maxBatch = 300; if (\count($ids) === 0) { self::setError('预约ID无效'); return false; } if (\count($ids) > $maxBatch) { self::setError('单次最多修改 ' . $maxBatch . ' 条挂号'); return false; } $chSrc = trim((string) ($params['channel_source'] ?? '')); if ($chSrc === '') { self::setError('请选择渠道来源'); return false; } $chDetail = isset($params['channel_source_detail']) ? trim((string) $params['channel_source_detail']) : ''; $channelDictName = trim((string) (DictData::where('type_value', 'channels') ->where('value', $chSrc) ->value('name') ?? '')); if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) { if ($chDetail === '') { self::setError('请填写自媒体渠道补充信息'); return false; } } if (mb_strlen($chDetail) > 128) { self::setError('渠道补充说明过长'); return false; } $tblFields = Db::name('doctor_appointment')->getTableFields(); $cols = \is_array($tblFields) ? $tblFields : []; $channelErr = self::assertAppointmentChannelWritable($cols, $chSrc); if ($channelErr !== null) { self::setError($channelErr); return false; } Db::startTrans(); $updated = 0; foreach ($ids as $id) { $appointment = Appointment::findOrEmpty((int) $id); if ($appointment->isEmpty()) { Db::rollback(); self::setError('预约记录不存在(ID ' . $id . ')'); return false; } $diag = Diagnosis::where('id', (int) $appointment->patient_id)->whereNull('delete_time')->find(); if (!self::appointmentRowManageableByAdmin($appointment, $diag ?: null, $adminId, $adminInfo)) { Db::rollback(); self::setError('无权限修改挂号 #' . $id); return false; } $saveData = [ 'update_time' => time(), ]; self::mergeAppointmentChannelIntoRow($saveData, $cols, $chSrc, $chDetail); $saveData = self::filterAppointmentRowByExistingColumns($saveData, $cols); Db::name('doctor_appointment')->where('id', $id)->update($saveData); $patientId = (int) $appointment->patient_id; self::appendDiagnosisGuahaoLog( $patientId, (int) $id, $adminId, $adminInfo, 'admin_batch_channel', sprintf('批量修改挂号 #%d:渠道设为 %s', $id, $chSrc !== '' ? $chSrc : '—') ); ++$updated; } Db::commit(); return [ 'updated' => $updated, 'msg' => sprintf('已更新 %d 条挂号渠道', $updated), ]; } catch (\Throwable $e) { Db::rollback(); self::setError($e->getMessage()); 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()); } } }