448 lines
16 KiB
PHP
448 lines
16 KiB
PHP
<?php
|
|
|
|
namespace app\adminapi\logic\doctor;
|
|
|
|
use app\common\logic\BaseLogic;
|
|
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\auth\Admin;
|
|
use app\api\logic\ChatNotifyLogic;
|
|
use app\adminapi\logic\tcm\PrescriptionLogic;
|
|
use think\facade\Db;
|
|
use think\facade\Log;
|
|
|
|
/**
|
|
* 医生预约逻辑
|
|
* Class AppointmentLogic
|
|
* @package app\adminapi\logic\doctor
|
|
*/
|
|
class AppointmentLogic extends BaseLogic
|
|
{
|
|
/**
|
|
* @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)
|
|
{
|
|
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();
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
$appointment->status = 2; // 已取消
|
|
$appointment->update_time = time();
|
|
$appointment->save();
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|