diff --git a/Laravel/app/Http/Controllers/API/Admin/YeWu/PlanListController.php b/Laravel/app/Http/Controllers/API/Admin/YeWu/PlanListController.php index 520b19e..61cc0ed 100644 --- a/Laravel/app/Http/Controllers/API/Admin/YeWu/PlanListController.php +++ b/Laravel/app/Http/Controllers/API/Admin/YeWu/PlanListController.php @@ -303,6 +303,20 @@ public function YuYue() $service = new PlanListService(); return $service->YuYue($planid, $appointment_type, $mainlistid, $do_type,$is_emergency,$do_userid); } + + public function PreCheckYuYue() + { + $do_userid = request('do_user') ?: request()->get('userid'); + $is_emergency = request('is_emergency'); + $appointment_type = request('appointment_type'); + $do_type = request('dotype'); + $groups = request('groups'); + if (!isset($do_type)) return \Yz::echoError1('参数:操作类型 不能为空'); + if (empty($groups) || !is_array($groups)) return \Yz::echoError1('参数:groups 不能为空'); + + $service = new PlanListService(); + return $service->PreCheckYuYue($groups, $appointment_type, $do_type, $is_emergency, $do_userid); + } //自动预约 public function AutoYuYue() { diff --git a/Laravel/app/Services/Admin/YeWu/PlanListService.php b/Laravel/app/Services/Admin/YeWu/PlanListService.php index d2b6fa0..7d6db00 100644 --- a/Laravel/app/Services/Admin/YeWu/PlanListService.php +++ b/Laravel/app/Services/Admin/YeWu/PlanListService.php @@ -1074,231 +1074,28 @@ private function groupItemsByDept($items) //开始预约占用名额 public function YuYue($planid, $appointment_type, $mainlistids, $do_type,$is_emergency=0,$do_user=null) { - date_default_timezone_set('PRC'); - $nowdatetime = date("Y-m-d H:i:s"); - - if ($is_emergency == 1 && !empty($do_user)) {//如果是加急,校验用户是否有加急权限 - $do_user_info=DB::table('users')->where('id',$do_user)->first(); - if(!$do_user_info){ - return \Yz::echoError1('无效的用户ID,无法进行加急预约'); - }else{ - $special_privileges=[]; - $decoded=json_decode($do_user_info->special_privileges,true); - if(is_array($decoded)){ - $special_privileges=$decoded; - } - if(!in_array('emergency', $special_privileges)){ - return \Yz::echoError1('无权进行加急预约'); - } - } - }else{ - $is_emergency=0; - } - - $planInfo = DB::table('s_source_roster_detail')->where(['id' => $planid, 'status' => 1, 'is_del' => 0])->first(); - if (!$planInfo) return \Yz::echoError1('当前时段不可用'); - if ($nowdatetime > $planInfo->date . ' ' . $planInfo->end_reservation_time) return \Yz::echoError1('已经超过预约截止时间'); - - // 查询当前排班资源绑定的设备ID列表,用于校验检查项目与资源的匹配性 - $resourceDeviceIds = DB::table('s_department_resources_device') - ->where('rsourece_id', $planInfo->resources_id) - ->pluck('device_id') - ->toArray(); - - $zhanweiCount=0;//各个检查项目需要占用检查名额的总和 - $itemSeatsQueue = []; // 队列:按顺序存储每个项目需要的座位数 [项目ID => 座位数] - $itemSeatsData = []; // 原始数据:临时收集每个项目的use_seats和is_share_slot - $huchiList=[];//互斥item_id对应关系 - $oldMainInfos = [];//临时存储原来的主表信息,用于改约 - //遍历多个s_list表id,前端多选,一次预约多个检查项目 - foreach ($mainlistids as $key_m => $mainlistid) { - $mainInfo = DB::table('s_list as a') - ->select('a.*', DB::raw('COALESCE(a.reservation_begin_time, b.period_begin_time) as period_begin_time'), DB::raw('COALESCE(a.reservation_end_time, b.period_end_time) as period_end_time')) - ->leftJoin('s_period as b', 'a.reservation_time', '=', 'b.id') - ->where(['a.id' => $mainlistid])->first(); - $oldMainInfos[] = $mainInfo; - if (!$mainInfo) return \Yz::echoError1('医嘱不存在'); - //判断状态 - $msg_t = ""; - if ($mainInfo->list_status == 0) { - $msg_t = "当前状态为待预约"; - } - if ($mainInfo->list_status == 1) { - $msg_t = "已经在" . $mainInfo->reservation_date . '的' . $mainInfo->period_begin_time . '-' . $mainInfo->period_end_time . '预约成功,不能再次预约'; - } - if ($mainInfo->list_status == 2) { - $msg_t = "当前状态为已登记"; - } - if ($mainInfo->list_status == 3) { - $msg_t = "当前状态为已完成"; - } - if ($do_type == 1 && $mainInfo->list_status <> 0) return \Yz::echoError1($mainInfo->entrust . ' ' . $msg_t . ',禁止预约'); - if ($do_type == 2 && $mainInfo->list_status <> 1) return \Yz::echoError1($mainInfo->entrust . ' ' . $msg_t . ',不允许改约操作'); - if ($do_type == 2 && $mainInfo->roster_id == $planid) return \Yz::echoError1($mainInfo->entrust . ' ' . ',改约时间与之前相同,无需改约'); - - - //判断病人类型 - $plan_patient_type=explode(",", $planInfo->patient_type); - if(!in_array($mainInfo->patient_type, $plan_patient_type)) return \Yz::echoError1('当前计划不支持此病人类型'); - - //判断互斥(暂时根据reg_num判断身份) - //查询想要预约的项目 其自身code - $deptId = $planInfo->department_id ?? 0; - $item = DB::table('s_check_item') - ->leftJoin('s_dept_check_item', function($j) use ($deptId) { - $j->on('s_dept_check_item.check_item_id', '=', 's_check_item.id') - ->where('s_dept_check_item.department_id', '=', $deptId); - }) - ->where(['s_check_item.item_code' => $mainInfo->entrust_code, 's_check_item.status' => 1, 's_check_item.is_del' => 0]) - ->select('s_check_item.*', - DB::raw('COALESCE(s_dept_check_item.use_seats, s_check_item.use_seats, 1) as use_seats'), - DB::raw('COALESCE(s_dept_check_item.is_share_slot, s_check_item.is_share_slot, 0) as is_share_slot'), - DB::raw('COALESCE(s_dept_check_item.check_begin_time, s_check_item.check_begin_time, 0) as check_begin_time'), - DB::raw('COALESCE(s_dept_check_item.limosis, s_check_item.limosis) as limosis')) - ->first(); - if (!$item) return \Yz::echoError1('此检查项目不可用'); - // 校验检查项目与排班资源的匹配性(通过设备/服务组交集判断) - $itemDeviceIds = DB::table('s_check_item_device') - ->where('item_id', $item->id) - ->pluck('device_id') - ->toArray(); - if (empty(array_intersect($itemDeviceIds, $resourceDeviceIds))) { - return \Yz::echoError1($item->item_name . ' 不支持在当前检查资源上预约'); - } - $itemSeatsData[$mainlistid] = [ - 'use_seats' => $item->use_seats, - 'is_share_slot' => $item->is_share_slot ?? 0, - ]; - //医嘱开具后,预约时间需在设定的等待期之后,单位分钟 - $entrust_time = $mainInfo->entrust_date . ' ' . $mainInfo->entrust_time; //医嘱时间 - $date = new DateTime($entrust_time); - $date->modify("+" . $item->check_begin_time . " minutes"); - $enableCheckTime = $date;//到此时间后可进行预约 - $plan_dateTime = $planInfo->date . ' ' . $planInfo->end_reservation_time; - $plan_dateTime = new DateTime($plan_dateTime); - if ($plan_dateTime < $enableCheckTime and $item->check_begin_time>0) return \Yz::echoError1($item->item_name . " 已设置只能预约医嘱开具后" . $item->check_begin_time . "分钟后的时间,请预约" . $enableCheckTime->format("Y-m-d H:i:s") . "之后的时间"); - - //检测是否空腹 - $configs = DB::table('configs')->where('label', '开启空腹')->first(); - if ($item->limosis == 1 and $planInfo->end_reservation_time > '12:00:00' and $configs->value == 1) return \Yz::echoError1($item->item_name . ' 项目必须空腹,只能预约上午,请重新选择时间'); - //查询当前检查项目是否存在互斥 - $cha_hc = DB::table('s_huchi')->where('is_del', 0) - ->where(function ($q) use ($item) { - $q->where(['code1' => $item->item_code])->orWhere('code2', $item->item_code); - })->get(); - foreach ($cha_hc as $hc) { - $huchiList[]=[$hc->code1,$hc->code2]; - } - if (count($cha_hc) > 0) { - - //查询用户预约中的医嘱 - $status_1 = DB::table('s_list') - ->select('s_check_item.item_code', 's_list.entrust', 's_list.reservation_date', 's_list.reservation_time') - ->leftJoin('s_check_item', 's_list.entrust_code', '=', 's_check_item.item_code') - ->where(['s_list.reg_num' => $mainInfo->reg_num, 's_list.list_status' => 1, 's_list.is_del' => 0, 's_list.is_nullify' => 0]) - //排除自身,自己不能互斥自己 - ->whereNotIn('s_list.entrust_code', [$mainInfo->entrust_code]) - ->get(); - if (count($status_1) > 0) { - foreach ($status_1 as $key => $value) { - foreach ($cha_hc as $k => $v) { - if ($v->code1 == $value->item_code or $v->code2 == $value->item_code) { - if ($v->time == 0) { - //如果是永久互斥,直接拒绝 - return \Yz::Return(false, '当前预约项目与' . $value->entrust . '互斥,暂不可预约', ['name' => $value->entrust]); - } - if ($v->time > 0) { - //如果设置互斥时间,则判断预约时间是否超过 正在预约的最后时间段+互斥时间 - $period = DB::table('s_period')->where(['id' => $value->reservation_time])->first(); - $endTime = $period->period_end_time; - $periodEndDateTime = $value->reservation_date . ' ' . $endTime; - $date = new DateTime($periodEndDateTime); - // 添加互斥时间/小时 - $date->add(new \DateInterval('PT' . $v->time . 'H')); // PTXH 表示X小时的时间间隔 - $HuChi_EndDateTime = $date->format('Y-m-d H:i:s');//已经预约的项目结束互斥时间 - $YuYueDateTime = substr($planInfo->date, 0, 10) . ' ' . $planInfo->begin_time; - if ($HuChi_EndDateTime > $YuYueDateTime) { - return \Yz::Return(false, '当前预约项目与' . $value->entrust . '互斥,暂不可预约,请预约' . $HuChi_EndDateTime . '后的日期', ['name' => $value->entrust]); - } - - } - - } - } - } - } - } - - //校验检查项目时段规则 - $itemRules = DB::table('s_check_item_rule') - ->where(['target_type' => 'ITEM', 'target_id' => $item->id, 'rule_type' => 'TIME_RESTRICTION', 'status' => 1, 'deleted' => 0]) - ->get(); - if ($itemRules->isNotEmpty()) { - $planWeekName = $planInfo->weekname; - $planBeginTime = $planInfo->begin_time; - $planResourcesId = $planInfo->resources_id; - - // 筛选出适用于当前排班资源的规则 - $applicableRules = []; - foreach ($itemRules as $rule) { - $rv = json_decode($rule->rule_value, true); - $resourcesId = $rv['resources_id'] ?? null; - if (empty($resourcesId)) { - // resources_id 为空,规则跳过 - continue; - } - if ($resourcesId != $planResourcesId) { - // 指定了其他排班资源,跳过 - continue; - } - $applicableRules[] = $rv; - } - - // 无适用规则 → 不受限 → 通过 - if (!empty($applicableRules)) { - $rulePass = false; - foreach ($applicableRules as $rv) { - $days = $rv['days'] ?? []; - $startTime = $rv['start_time'] ?? null; - $endTime = $rv['end_time'] ?? null; - - if (!in_array($planWeekName, $days)) continue; - - if ($startTime && $endTime && $planBeginTime) { - if ($planBeginTime >= $startTime && $planBeginTime < $endTime) { - $rulePass = true; - break; - } - } else { - $rulePass = true; - break; - } - } - if (!$rulePass) { - return \Yz::echoError1($item->item_name . ' 当前时段不在可预约时间范围内'); - } - } - } - - } - - // 共享号源预计算(不依赖排班数据的部分) - $shareSlotIds = []; - $shareSlotMax = 0; + // 使用公共方法进行前置条件校验(含共享号源预计算、批次内互斥校验) + $validation = $this->validateYuYueConditions($planid, $mainlistids, $do_type, $is_emergency, $do_user); + if (!$validation['pass']) { + return \Yz::echoError1($validation['fail_items'][0]['msg']); + } + + $planInfo = $validation['planInfo']; + $is_emergency = $validation['is_emergency']; + $resourceDeviceIds = $validation['resourceDeviceIds']; + $oldMainInfos = $validation['oldMainInfos']; + $itemSeatsData = $validation['itemSeatsData']; + $huchiList = $validation['huchiList']; + $shareSlotIds = $validation['shareSlotIds']; + $shareSlotMax = $validation['shareSlotMax']; + $maxShareSlotId = $validation['maxShareSlotId']; + + // 构建 itemSeatsQueue(兼容原有流式分配逻辑) + $itemSeatsQueue = []; foreach ($itemSeatsData as $id => $data) { - if (!empty($data['is_share_slot'])) { - $shareSlotIds[] = $id; - $shareSlotMax = max($shareSlotMax, $data['use_seats']); - } - } - $maxShareSlotId = null; - $maxShareSeats = 0; - foreach ($itemSeatsData as $id => $data) { - if (!empty($data['is_share_slot']) && $data['use_seats'] > $maxShareSeats) { - $maxShareSeats = $data['use_seats']; - $maxShareSlotId = $id; - } + $itemSeatsQueue[$id] = $data['use_seats']; } + // 以下变量在事务内计算 $existingSharedMax = 0; $additionalNeeded = 0; @@ -1359,6 +1156,12 @@ public function YuYue($planid, $appointment_type, $mainlistids, $do_type,$is_eme $maxShareSlotId = $id; } } + + // 重建 itemSeatsQueue + $itemSeatsQueue = []; + foreach ($itemSeatsData as $id => $data) { + $itemSeatsQueue[$id] = $data['use_seats']; + } } DB::beginTransaction(); @@ -1588,29 +1391,6 @@ public function YuYue($planid, $appointment_type, $mainlistids, $do_type,$is_eme } - //判断某人这些待预约项目里,是否存在互斥 - $userGroup=[]; - foreach ($oldMainInfos as $key1 => $value1) { - $reg_num = $value1->reg_num; - if (!isset($userGroup[$reg_num])) { - $userGroup[$reg_num] = []; - } - $userGroup[$reg_num][]=[ - 'reg_num' => $value1->reg_num, - 'user_name' => $value1->user_name, - 'entrust_code' => $value1->entrust_code, - 'entrust' => $value1->entrust, - ]; - } - // $huchiList - - foreach ($userGroup as $personEntry) { - $ids = array_column($personEntry, 'entrust_code'); - if ($this->isMutuallyExclusive($ids, $huchiList)) { - return \Yz::echoError1($personEntry[0]['user_name'].'勾选的项目存在互斥,不可同时预约'); - } - } - // === 号源总量校验 + 精确扣减(替换原乐观锁扣减)=== // 加急预约允许超量,跳过总量校验 @@ -2950,4 +2730,575 @@ function generateQueueNumber($planId) return $minAvailableNumber ?? $maxQueueNumber + 1; } + + /** + * 预约前置条件校验(事务之前) + * 从 YuYue 方法中提取,供 YuYue 和 PreCheckYuYue 共用 + * 返回结构化结果,不提前返回错误响应 + */ + private function validateYuYueConditions($planid, $mainlistids, $do_type, $is_emergency = 0, $do_user = null) + { + date_default_timezone_set('PRC'); + $nowdatetime = date("Y-m-d H:i:s"); + + $result = [ + 'pass' => true, + 'fail_items' => [], + 'planInfo' => null, + 'resourceDeviceIds' => [], + 'oldMainInfos' => [], + 'itemSeatsData' => [], + 'huchiList' => [], + 'shareSlotIds' => [], + 'shareSlotMax' => 0, + 'maxShareSlotId' => null, + 'is_emergency' => 0, + ]; + + // 加急校验 + if ($is_emergency == 1 && !empty($do_user)) { + $do_user_info = DB::table('users')->where('id', $do_user)->first(); + if (!$do_user_info) { + $result['pass'] = false; + $result['fail_items'][] = ['mainlistid' => 0, 'item_name' => '', 'msg' => '无效的用户ID,无法进行加急预约']; + return $result; + } else { + $special_privileges = []; + $decoded = json_decode($do_user_info->special_privileges, true); + if (is_array($decoded)) { + $special_privileges = $decoded; + } + if (!in_array('emergency', $special_privileges)) { + $result['pass'] = false; + $result['fail_items'][] = ['mainlistid' => 0, 'item_name' => '', 'msg' => '无权进行加急预约']; + return $result; + } + } + } else { + $is_emergency = 0; + } + $result['is_emergency'] = $is_emergency; + + // 排班明细有效性 + $planInfo = DB::table('s_source_roster_detail')->where(['id' => $planid, 'status' => 1, 'is_del' => 0])->first(); + if (!$planInfo) { + $result['pass'] = false; + $result['fail_items'][] = ['mainlistid' => 0, 'item_name' => '', 'msg' => '当前时段不可用']; + return $result; + } + if ($nowdatetime > $planInfo->date . ' ' . $planInfo->end_reservation_time) { + $result['pass'] = false; + $result['fail_items'][] = ['mainlistid' => 0, 'item_name' => '', 'msg' => '已经超过预约截止时间']; + return $result; + } + $result['planInfo'] = $planInfo; + + // 查询排班资源绑定的设备ID列表 + $resourceDeviceIds = DB::table('s_department_resources_device') + ->where('rsourece_id', $planInfo->resources_id) + ->pluck('device_id') + ->toArray(); + $result['resourceDeviceIds'] = $resourceDeviceIds; + + $itemSeatsData = []; + $huchiList = []; + $oldMainInfos = []; + + // 遍历多个mainlistid + foreach ($mainlistids as $key_m => $mainlistid) { + $mainInfo = DB::table('s_list as a') + ->select('a.*', DB::raw('COALESCE(a.reservation_begin_time, b.period_begin_time) as period_begin_time'), DB::raw('COALESCE(a.reservation_end_time, b.period_end_time) as period_end_time')) + ->leftJoin('s_period as b', 'a.reservation_time', '=', 'b.id') + ->where(['a.id' => $mainlistid])->first(); + $oldMainInfos[] = $mainInfo; + + if (!$mainInfo) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => '', 'msg' => '医嘱不存在']; + $result['pass'] = false; + continue; + } + + // 状态校验 + $msg_t = ""; + if ($mainInfo->list_status == 0) $msg_t = "当前状态为待预约"; + if ($mainInfo->list_status == 1) $msg_t = "已经在" . $mainInfo->reservation_date . '的' . $mainInfo->period_begin_time . '-' . $mainInfo->period_end_time . '预约成功,不能再次预约'; + if ($mainInfo->list_status == 2) $msg_t = "当前状态为已登记"; + if ($mainInfo->list_status == 3) $msg_t = "当前状态为已完成"; + + if ($do_type == 1 && $mainInfo->list_status <> 0) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $mainInfo->entrust, 'msg' => $mainInfo->entrust . ' ' . $msg_t . ',禁止预约']; + $result['pass'] = false; + continue; + } + if ($do_type == 2 && $mainInfo->list_status <> 1) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $mainInfo->entrust, 'msg' => $mainInfo->entrust . ' ' . $msg_t . ',不允许改约操作']; + $result['pass'] = false; + continue; + } + if ($do_type == 2 && $mainInfo->roster_id == $planid) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $mainInfo->entrust, 'msg' => $mainInfo->entrust . ' 改约时间与之前相同,无需改约']; + $result['pass'] = false; + continue; + } + + // 病人类型匹配 + $plan_patient_type = explode(",", $planInfo->patient_type); + if (!in_array($mainInfo->patient_type, $plan_patient_type)) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $mainInfo->entrust, 'msg' => '当前计划不支持此病人类型']; + $result['pass'] = false; + continue; + } + + // 检查项目可用性 + $deptId = $planInfo->department_id ?? 0; + $item = DB::table('s_check_item') + ->leftJoin('s_dept_check_item', function($j) use ($deptId) { + $j->on('s_dept_check_item.check_item_id', '=', 's_check_item.id') + ->where('s_dept_check_item.department_id', '=', $deptId); + }) + ->where(['s_check_item.item_code' => $mainInfo->entrust_code, 's_check_item.status' => 1, 's_check_item.is_del' => 0]) + ->select('s_check_item.*', + DB::raw('COALESCE(s_dept_check_item.use_seats, s_check_item.use_seats, 1) as use_seats'), + DB::raw('COALESCE(s_dept_check_item.is_share_slot, s_check_item.is_share_slot, 0) as is_share_slot'), + DB::raw('COALESCE(s_dept_check_item.check_begin_time, s_check_item.check_begin_time, 0) as check_begin_time'), + DB::raw('COALESCE(s_dept_check_item.limosis, s_check_item.limosis) as limosis')) + ->first(); + if (!$item) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $mainInfo->entrust, 'msg' => '此检查项目不可用']; + $result['pass'] = false; + continue; + } + + // 设备匹配 + $itemDeviceIds = DB::table('s_check_item_device') + ->where('item_id', $item->id) + ->pluck('device_id') + ->toArray(); + if (empty(array_intersect($itemDeviceIds, $resourceDeviceIds))) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $item->item_name, 'msg' => $item->item_name . ' 不支持在当前检查资源上预约']; + $result['pass'] = false; + continue; + } + + $itemSeatsData[$mainlistid] = [ + 'use_seats' => $item->use_seats, + 'is_share_slot' => $item->is_share_slot ?? 0, + ]; + + // 预约等待期 + $entrust_time = $mainInfo->entrust_date . ' ' . $mainInfo->entrust_time; + $date = new DateTime($entrust_time); + $date->modify("+" . $item->check_begin_time . " minutes"); + $enableCheckTime = $date; + $plan_dateTime = $planInfo->date . ' ' . $planInfo->end_reservation_time; + $plan_dateTime = new DateTime($plan_dateTime); + if ($plan_dateTime < $enableCheckTime and $item->check_begin_time > 0) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $item->item_name, 'msg' => $item->item_name . " 已设置只能预约医嘱开具后" . $item->check_begin_time . "分钟后的时间,请预约" . $enableCheckTime->format("Y-m-d H:i:s") . "之后的时间"]; + $result['pass'] = false; + continue; + } + + // 空腹校验 + $configs = DB::table('configs')->where('label', '开启空腹')->first(); + if ($configs && $item->limosis == 1 and $planInfo->end_reservation_time > '12:00:00' and $configs->value == 1) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $item->item_name, 'msg' => $item->item_name . ' 项目必须空腹,只能预约上午,请重新选择时间']; + $result['pass'] = false; + continue; + } + + // 互斥校验 + $cha_hc = DB::table('s_huchi')->where('is_del', 0) + ->where(function ($q) use ($item) { + $q->where(['code1' => $item->item_code])->orWhere('code2', $item->item_code); + })->get(); + foreach ($cha_hc as $hc) { + $huchiList[] = [$hc->code1, $hc->code2]; + } + if (count($cha_hc) > 0) { + $status_1 = DB::table('s_list') + ->select('s_check_item.item_code', 's_list.entrust', 's_list.reservation_date', 's_list.reservation_time') + ->leftJoin('s_check_item', 's_list.entrust_code', '=', 's_check_item.item_code') + ->where(['s_list.reg_num' => $mainInfo->reg_num, 's_list.list_status' => 1, 's_list.is_del' => 0, 's_list.is_nullify' => 0]) + ->whereNotIn('s_list.entrust_code', [$mainInfo->entrust_code]) + ->get(); + if (count($status_1) > 0) { + $huchiFail = false; + foreach ($status_1 as $key => $value) { + foreach ($cha_hc as $k => $v) { + if ($v->code1 == $value->item_code or $v->code2 == $value->item_code) { + if ($v->time == 0) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $item->item_name, 'msg' => '当前预约项目与' . $value->entrust . '互斥,暂不可预约']; + $result['pass'] = false; + $huchiFail = true; + break 2; + } + if ($v->time > 0) { + $period = DB::table('s_period')->where(['id' => $value->reservation_time])->first(); + $endTime = $period->period_end_time; + $periodEndDateTime = $value->reservation_date . ' ' . $endTime; + $date = new DateTime($periodEndDateTime); + $date->add(new \DateInterval('PT' . $v->time . 'H')); + $HuChi_EndDateTime = $date->format('Y-m-d H:i:s'); + $YuYueDateTime = substr($planInfo->date, 0, 10) . ' ' . $planInfo->begin_time; + if ($HuChi_EndDateTime > $YuYueDateTime) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $item->item_name, 'msg' => '当前预约项目与' . $value->entrust . '互斥,暂不可预约,请预约' . $HuChi_EndDateTime . '后的日期']; + $result['pass'] = false; + $huchiFail = true; + break 2; + } + } + } + } + } + if ($huchiFail) continue; + } + } + + // 时段规则校验 + $itemRules = DB::table('s_check_item_rule') + ->where(['target_type' => 'ITEM', 'target_id' => $item->id, 'rule_type' => 'TIME_RESTRICTION', 'status' => 1, 'deleted' => 0]) + ->get(); + if ($itemRules->isNotEmpty()) { + $planWeekName = $planInfo->weekname; + $planBeginTime = $planInfo->begin_time; + $planResourcesId = $planInfo->resources_id; + + $applicableRules = []; + foreach ($itemRules as $rule) { + $rv = json_decode($rule->rule_value, true); + $resourcesId = $rv['resources_id'] ?? null; + if (empty($resourcesId)) continue; + if ($resourcesId != $planResourcesId) continue; + $applicableRules[] = $rv; + } + + if (!empty($applicableRules)) { + $rulePass = false; + foreach ($applicableRules as $rv) { + $days = $rv['days'] ?? []; + $startTime = $rv['start_time'] ?? null; + $endTime = $rv['end_time'] ?? null; + if (!in_array($planWeekName, $days)) continue; + if ($startTime && $endTime && $planBeginTime) { + if ($planBeginTime >= $startTime && $planBeginTime < $endTime) { + $rulePass = true; + break; + } + } else { + $rulePass = true; + break; + } + } + if (!$rulePass) { + $result['fail_items'][] = ['mainlistid' => $mainlistid, 'item_name' => $item->item_name, 'msg' => $item->item_name . ' 当前时段不在可预约时间范围内']; + $result['pass'] = false; + continue; + } + } + } + } + + $result['oldMainInfos'] = $oldMainInfos; + $result['itemSeatsData'] = $itemSeatsData; + $result['huchiList'] = $huchiList; + + // 共享号源预计算 + $shareSlotIds = []; + $shareSlotMax = 0; + foreach ($itemSeatsData as $id => $data) { + if (!empty($data['is_share_slot'])) { + $shareSlotIds[] = $id; + $shareSlotMax = max($shareSlotMax, $data['use_seats']); + } + } + $maxShareSlotId = null; + $maxShareSeats = 0; + foreach ($itemSeatsData as $id => $data) { + if (!empty($data['is_share_slot']) && $data['use_seats'] > $maxShareSeats) { + $maxShareSeats = $data['use_seats']; + $maxShareSlotId = $id; + } + } + $result['shareSlotIds'] = $shareSlotIds; + $result['shareSlotMax'] = $shareSlotMax; + $result['maxShareSlotId'] = $maxShareSlotId; + + // 批次内互斥校验 + if ($result['pass'] && count($oldMainInfos) > 0) { + $userGroup = []; + foreach ($oldMainInfos as $key1 => $value1) { + $reg_num = $value1->reg_num; + if (!isset($userGroup[$reg_num])) { + $userGroup[$reg_num] = []; + } + $userGroup[$reg_num][] = [ + 'reg_num' => $value1->reg_num, + 'user_name' => $value1->user_name, + 'entrust_code' => $value1->entrust_code, + 'entrust' => $value1->entrust, + ]; + } + foreach ($userGroup as $personEntry) { + $ids = array_column($personEntry, 'entrust_code'); + if ($this->isMutuallyExclusive($ids, $huchiList)) { + $result['fail_items'][] = ['mainlistid' => 0, 'item_name' => $personEntry[0]['user_name'], 'msg' => $personEntry[0]['user_name'] . '勾选的项目存在互斥,不可同时预约']; + $result['pass'] = false; + } + } + } + + return $result; + } + + /** + * 预约前预检接口:条件校验 + 余量判断(只读),不扣减、不回写 HIS + * 入参:{groups, appointment_type, do_type, is_emergency, do_user} + * 返回:预检结果列表 + */ + public function PreCheckYuYue($groups, $appointment_type, $do_type, $is_emergency = 0, $do_user = null) + { + date_default_timezone_set('PRC'); + $nowdatetime = date("Y-m-d H:i:s"); + + $allList = []; + $hasMismatch = false; + $hasFail = false; + + foreach ($groups as $group) { + $planid = $group['planid']; + $mainlistids = $group['mainlistid']; + if (!is_array($mainlistids)) $mainlistids = [$mainlistids]; + + // 复用 validateYuYueConditions 进行条件校验 + $validation = $this->validateYuYueConditions($planid, $mainlistids, $do_type, $is_emergency, $do_user); + + $planInfo = $validation['planInfo']; + $isEmergency = $validation['is_emergency']; + + // 查询排班资源信息,用于组装返回数据 + $resource = null; + $appointmentDeptCode = ''; + $appointmentDeptName = ''; + if ($planInfo) { + $resource = DB::table('s_department_resources')->where('id', $planInfo->resources_id)->first(); + if ($resource) { + $deptId = $resource->execute_department_id ?: $resource->department_id; + if ($deptId) { + $dept = DB::table('s_department')->where('id', $deptId)->first(); + if ($dept) { + $appointmentDeptCode = $dept->department_number; + $appointmentDeptName = $dept->department_name; + } + } + } + } + + // 若条件校验失败,将失败项记录,从 fail_items 构建 + if (!$validation['pass']) { + // 区分组级失败还是项目级失败 + $groupLevelFail = []; + $itemLevelFail = []; + foreach ($validation['fail_items'] as $fi) { + if ($fi['mainlistid'] == 0) { + $groupLevelFail[] = $fi; + } else { + $itemLevelFail[$fi['mainlistid']] = $fi; + } + } + + // 组级失败:整组所有项目标红 + if (!empty($groupLevelFail)) { + foreach ($mainlistids as $mid) { + $mainInfo = DB::table('s_list')->where('id', $mid)->first(); + $item_name = $mainInfo ? $mainInfo->entrust : ''; + $entrustDeptCode = $mainInfo ? $mainInfo->RISRAcceptDeptCode : ''; + $entrustDeptName = ''; + if ($entrustDeptCode) { + $deptInfo = DB::table('s_department')->where('department_number', $entrustDeptCode)->first(); + if ($deptInfo) $entrustDeptName = $deptInfo->department_name; + } + $mismatch = ($entrustDeptCode !== '' && $appointmentDeptCode !== '' && $entrustDeptCode !== $appointmentDeptCode); + if ($mismatch) $hasMismatch = true; + $hasFail = true; + $allList[] = [ + 'item_name' => $item_name, + 'entrust_dept_code' => $entrustDeptCode, + 'entrust_dept_name' => $entrustDeptName, + 'appointment_dept_code' => $appointmentDeptCode, + 'appointment_dept_name' => $appointmentDeptName, + 'dept_mismatch' => $mismatch, + 'appointment_date' => $planInfo ? $planInfo->date : '', + 'begin_time' => $planInfo ? $planInfo->begin_time : '', + 'end_time' => $planInfo ? $planInfo->end_time : '', + 'check_status' => 'fail', + 'check_msg' => $groupLevelFail[0]['msg'], + ]; + } + } else { + // 项目级失败:失败项标红,通过项标绿 + foreach ($mainlistids as $mid) { + $mainInfo = DB::table('s_list')->where('id', $mid)->first(); + $item_name = $mainInfo ? $mainInfo->entrust : ''; + $entrustDeptCode = $mainInfo ? $mainInfo->RISRAcceptDeptCode : ''; + $entrustDeptName = ''; + if ($entrustDeptCode) { + $deptInfo = DB::table('s_department')->where('department_number', $entrustDeptCode)->first(); + if ($deptInfo) $entrustDeptName = $deptInfo->department_name; + } + $mismatch = ($entrustDeptCode !== '' && $appointmentDeptCode !== '' && $entrustDeptCode !== $appointmentDeptCode); + if ($mismatch) $hasMismatch = true; + + $failMsg = ''; + $checkStatus = 'pass'; + if (isset($itemLevelFail[$mid])) { + $failMsg = $itemLevelFail[$mid]['msg']; + $checkStatus = 'fail'; + $hasFail = true; + } + $allList[] = [ + 'item_name' => $item_name, + 'entrust_dept_code' => $entrustDeptCode, + 'entrust_dept_name' => $entrustDeptName, + 'appointment_dept_code' => $appointmentDeptCode, + 'appointment_dept_name' => $appointmentDeptName, + 'dept_mismatch' => $mismatch, + 'appointment_date' => $planInfo ? $planInfo->date : '', + 'begin_time' => $planInfo ? $planInfo->begin_time : '', + 'end_time' => $planInfo ? $planInfo->end_time : '', + 'check_status' => $checkStatus, + 'check_msg' => $failMsg, + ]; + } + } + continue; + } + + // 条件校验通过,进行只读余量判断 + $itemSeatsData = $validation['itemSeatsData']; + $shareSlotIds = $validation['shareSlotIds']; + $shareSlotMax = $validation['shareSlotMax']; + $maxShareSlotId = $validation['maxShareSlotId']; + $oldMainInfos = $validation['oldMainInfos']; + + // 渠道合并查询 + $appointment_types = [$appointment_type]; + $appointment_type_link = DB::table('s_appointment_type_ratio')->where(['department_id' => $planInfo->department_id, 'appointment_type_id' => $appointment_type])->first(); + if (!empty($appointment_type_link->link)) { + $appointment_types = json_decode($appointment_type_link->link, true); + $appointment_types = array_merge($appointment_types, [$appointment_type]); + sort($appointment_types); + } + + // 只读查询号源余量 + $roster_detail_counts = DB::table('s_source_roster_detail_count') + ->where('roster_detail_id', $planid) + ->whereIn('appointment_type_id', $appointment_types) + ->orderBy('appointment_type_id', 'asc') + ->get(); + + // 共享号源计算(只读) + $existingSharedMax = 0; + $additionalNeeded = 0; + $zhanweiCount = 0; + $regNum = $oldMainInfos[0]->reg_num ?? null; + if (!empty($shareSlotIds) && $regNum) { + $existingSharedItems = DB::table('s_list') + ->join('s_check_item', 's_list.entrust_code', '=', 's_check_item.item_code') + ->leftJoin('s_dept_check_item', function($j) { + $j->on('s_dept_check_item.check_item_id', '=', 's_check_item.id') + ->on('s_dept_check_item.department_id', '=', 's_list.department_id'); + }) + ->where('s_list.reg_num', $regNum) + ->where('s_list.roster_id', $planid) + ->where('s_list.list_status', 1) + ->where(DB::raw('COALESCE(s_dept_check_item.is_share_slot, s_check_item.is_share_slot, 0)'), 1) + ->select(DB::raw('COALESCE(s_dept_check_item.use_seats, s_check_item.use_seats, 1) as use_seats')) + ->get(); + if ($existingSharedItems->isNotEmpty()) { + $existingSharedMax = $existingSharedItems->max('use_seats'); + } + $overallMax = max($existingSharedMax, $shareSlotMax); + $additionalNeeded = max(0, $overallMax - $existingSharedMax); + } + foreach ($itemSeatsData as $id => $data) { + if (!empty($data['is_share_slot'])) { + if ($id === $maxShareSlotId) { + $zhanweiCount += $additionalNeeded; + } + } else { + $zhanweiCount += $data['use_seats']; + } + } + + // 余量判断:非紧急模式下,总余量是否满足占位需求 + $quotaFail = false; + $quotaMsg = ''; + if ($isEmergency !== 1) { + $totalAvailable = 0; + foreach ($roster_detail_counts as $rdc) { + $physicalAvailable = $rdc->count - $rdc->used_count - ($rdc->locked_count ?? 0); + $totalAvailable += max(0, $physicalAvailable); + } + if ($totalAvailable < $zhanweiCount) { + $quotaFail = true; + $quotaMsg = '当前预约时间名额不足,剩余' . $totalAvailable . ',需要' . $zhanweiCount; + } + } + + // 组装返回数据 + foreach ($mainlistids as $mid) { + $mainInfo = null; + foreach ($oldMainInfos as $omi) { + if ($omi && $omi->id == $mid) { + $mainInfo = $omi; + break; + } + } + $item_name = $mainInfo ? $mainInfo->entrust : ''; + $entrustDeptCode = $mainInfo ? $mainInfo->RISRAcceptDeptCode : ''; + $entrustDeptName = ''; + if ($entrustDeptCode) { + $deptInfo = DB::table('s_department')->where('department_number', $entrustDeptCode)->first(); + if ($deptInfo) $entrustDeptName = $deptInfo->department_name; + } + + // 查询检查项目名称 + if ($mainInfo) { + $checkItem = DB::table('s_check_item')->where(['item_code' => $mainInfo->entrust_code, 'status' => 1, 'is_del' => 0])->first(); + if ($checkItem) $item_name = $checkItem->item_name; + } + + $mismatch = ($entrustDeptCode !== '' && $appointmentDeptCode !== '' && $entrustDeptCode !== $appointmentDeptCode); + if ($mismatch) $hasMismatch = true; + + $checkStatus = 'pass'; + $checkMsg = ''; + if ($quotaFail) { + $checkStatus = 'fail'; + $checkMsg = $quotaMsg; + $hasFail = true; + } + + $allList[] = [ + 'item_name' => $item_name, + 'entrust_dept_code' => $entrustDeptCode, + 'entrust_dept_name' => $entrustDeptName, + 'appointment_dept_code' => $appointmentDeptCode, + 'appointment_dept_name' => $appointmentDeptName, + 'dept_mismatch' => $mismatch, + 'appointment_date' => $planInfo ? $planInfo->date : '', + 'begin_time' => $planInfo ? $planInfo->begin_time : '', + 'end_time' => $planInfo ? $planInfo->end_time : '', + 'check_status' => $checkStatus, + 'check_msg' => $checkMsg, + ]; + } + } + + return \Yz::Return(true, '检查完成', [ + 'has_mismatch' => $hasMismatch, + 'has_fail' => $hasFail, + 'list' => $allList, + ]); + } } diff --git a/Laravel/routes/api.php b/Laravel/routes/api.php index 4c5cb98..9b64ffe 100644 --- a/Laravel/routes/api.php +++ b/Laravel/routes/api.php @@ -181,6 +181,7 @@ Route::post('admin/GetEnablePlan','App\Http\Controllers\API\Admin\YeWu\PlanListController@GetEnablePlan');//获取可用的计划,用于计划占用 Route::post('admin/GetOptimalPlan','App\Http\Controllers\API\Admin\YeWu\PlanListController@GetOptimalPlan');//最优时间/单天全检 分配 Route::post('admin/PlanYuYue','App\Http\Controllers\API\Admin\YeWu\PlanListController@YuYue');//开始预约 + Route::post('admin/PreCheckYuYue','App\Http\Controllers\API\Admin\YeWu\PlanListController@PreCheckYuYue');//预约前预检 Route::post('admin/CheckEntrstItemGroup','App\Http\Controllers\API\Admin\YeWu\WorkMainController@CheckEntrstItemGroup');//批量检查医嘱检查项目是否可以同时预约 Route::post('admin/CheckIsDaiJian','App\Http\Controllers\API\Admin\YeWu\WorkMainController@CheckIsDaiJian');//检查当前时段是否有存在已经预约的待检查项目 Route::post('admin/GetPlanUsedList','App\Http\Controllers\API\Admin\YeWu\PlanListController@GetUsedList');//计划占用详情列表 diff --git a/YiJi-admin/src/api/api.js b/YiJi-admin/src/api/api.js index 42b2c1c..a91014a 100644 --- a/YiJi-admin/src/api/api.js +++ b/YiJi-admin/src/api/api.js @@ -578,6 +578,10 @@ export const GetEnablePlan = (data = {}) => { export const GetOptimalPlan = (data = {}) => { return axios({ url: import.meta.env.VITE_APP_API + 'admin/GetOptimalPlan', data: data }) } +//预约前预检 +export const PreCheckYuYue = (data = {}) => { + return axios({ url: import.meta.env.VITE_APP_API + 'admin/PreCheckYuYue', data: data }) +} //获取某日可用号源 export const PlanYuYue = (data = {}) => { return axios({ url: import.meta.env.VITE_APP_API + 'admin/PlanYuYue', data: data }) diff --git a/YiJi-admin/src/components/Yewu/YuYue202506.vue b/YiJi-admin/src/components/Yewu/YuYue202506.vue index d9a1b04..3a7d6f8 100644 --- a/YiJi-admin/src/components/Yewu/YuYue202506.vue +++ b/YiJi-admin/src/components/Yewu/YuYue202506.vue @@ -224,6 +224,39 @@ 确定 + + + 医嘱选择的执行科室与预约选择的执行科室不一致,请确认。 + + + + + + + {{ scope.row.entrust_dept_name }} + → {{ scope.row.appointment_dept_name }} + + + + + + {{ scope.row.appointment_date }} {{ scope.row.begin_time?.substring(0, 5) }}-{{ scope.row.end_time?.substring(0, 5) }} + {{ scope.row.check_msg }} + + + + + 通过 + 失败 + + + + + 取消 + 确认 + 确认 + + @@ -238,6 +271,7 @@ GetEnablePlan, CheckIsDaiJian, PlanYuYue, + PreCheckYuYue, CheckEntrstItemGroup2, DoctorCancelYuYue,GetConfigInfo2,GetOptimalPlan, BatchCancelYuYue @@ -323,6 +357,20 @@ let TanChuangMsgDialogVisible=ref(false) let resultDialogHtml = ref('') let recommendResultVisible = ref(false) let recommendResultHtml = ref('') + let preCheckDialogVisible = ref(false) + let preCheckList = ref([]) + let preCheckHasMismatch = ref(false) + let preCheckHasFail = ref(false) + let preCheckDotype = ref(null) + + const preCheckConfirm = () => { + preCheckDialogVisible.value = false + startYuYue(preCheckDotype.value) + } + const preCheckRowClassName = ({ row }) => { + if (row.check_status === 'fail') return 'precheck-fail-row' + return '' + } const getWeekday = (date1) => { let days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; const date = new Date(date1); // 注意:格式可能因浏览器而异 @@ -1451,7 +1499,7 @@ let TanChuangMsgDialogVisible=ref(false) } }) } - const YuYueButtonClick = (type) => { + const YuYueButtonClick = async (type) => { if (selectedEntrustId.length == 0) { ElMessage.error("请选择检查项目") return false; @@ -1461,25 +1509,35 @@ let TanChuangMsgDialogVisible=ref(false) return false; } - let msg='确定预约此时间吗?' - if(type==2){ - msg='确定改约至此时间吗?' - } - ElMessageBox.confirm( - msg, - '提示', { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning', - } - ) - .then(() => { - startYuYue(type) - }) - .catch(() => { + // 构建 groups 参数 + const groupMap = {} + selectedMianListId.value.forEach(id => { + const row = entrustTableDate.value.find(v => v.id === id) + const planId = row?.temp_plan_id || row?.roster_id + if (!planId) return + if (!groupMap[planId]) groupMap[planId] = [] + groupMap[planId].push(id) + }) + const groups = Object.entries(groupMap).map(([planid, mainlistid]) => ({ planid: Number(planid), mainlistid })) - }) + // 调用预检接口 + const res = await PreCheckYuYue({ + groups, + appointment_type: props.appointment_type, + dotype: type, + do_user: props.do_user, + is_emergency: type == 111 ? 1 : null + }) + if (!res.status) { + ElMessage.error(res.msg) + return + } + preCheckList.value = res.data.list || [] + preCheckHasMismatch.value = res.data.has_mismatch || false + preCheckHasFail.value = res.data.has_fail || false + preCheckDotype.value = type + preCheckDialogVisible.value = true } const startYuYue = async (type) => { planLoading.value = true @@ -2130,6 +2188,10 @@ let TanChuangMsgDialogVisible=ref(false) background-color: #9cd0de; } +/** 预检弹窗失败行标红 */ +.precheck-fail-row { + background-color: #fef0f0 !important; +}