$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; } $data = [ 'patient_id' => $params['patient_id'], 'assistant_id'=>$params['assistant_id'], 'doctor_id' => $params['doctor_id'], 'roster_id' => 0, // 不再关联排班表 'appointment_date' => $params['appointment_date'], 'period' => $params['period'] ?? 'all', 'appointment_time' => $appointmentTime, 'appointment_type' => $params['appointment_type'] ?? 'video', 'remark' => $params['remark'] ?? '', 'channels' => $params['channel_source'] ?? '', 'status' => 1, 'create_time' => time(), 'update_time' => time(), ]; $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(); 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; } } /** * 诊单列表维度:记录谁在后台发起挂号 / 取消挂号(写入失败不影响主流程) */ 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()); } } }