From 7644ab0e4fb0e221b0b3496936f251ea47d574df Mon Sep 17 00:00:00 2001 From: sa0ChunLuyu Date: Thu, 30 Jul 2026 19:59:35 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=84=E7=BA=A6=E5=8F=B7=E6=BA=90=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../API/Admin/YeWu/PlanListController.php | 16 ++ .../Services/Admin/YeWu/PlanListService.php | 235 +++++++++++------- .../app/Services/Xml/ShenQingDanService.php | 39 ++- Laravel/routes/api.php | 1 + YiJi-admin/src/api/api.js | 4 + .../src/components/Yewu/YuYue202506.vue | 48 ++-- 6 files changed, 228 insertions(+), 115 deletions(-) diff --git a/Laravel/app/Http/Controllers/API/Admin/YeWu/PlanListController.php b/Laravel/app/Http/Controllers/API/Admin/YeWu/PlanListController.php index 94478a2..69afb93 100644 --- a/Laravel/app/Http/Controllers/API/Admin/YeWu/PlanListController.php +++ b/Laravel/app/Http/Controllers/API/Admin/YeWu/PlanListController.php @@ -348,6 +348,22 @@ public function CancelYuYue(Request $request) } } + //批量取消预约 + public function BatchCancelYuYue(Request $request) + { + $ids = request('ids'); + $reg_num = request('reg_num'); + $userid = $request->get('userid'); + $do_userid = request('do_user') ?: $userid; + + if (empty($ids) || !is_array($ids)) { + return \Yz::echoError1('参数错误'); + } + + $service = new PlanListService(); + return $service->BatchCancelYuYue($ids, $reg_num, $do_userid); + } + //查询已预约明细 public function GetUsedList() { diff --git a/Laravel/app/Services/Admin/YeWu/PlanListService.php b/Laravel/app/Services/Admin/YeWu/PlanListService.php index 2cd235c..5b8108d 100644 --- a/Laravel/app/Services/Admin/YeWu/PlanListService.php +++ b/Laravel/app/Services/Admin/YeWu/PlanListService.php @@ -196,6 +196,17 @@ public function GetEnablePlan($regnum, $entrustids, $episodeid, $appointment_typ AND a.is_del = 0 AND b.is_del = 0 AND b.appointment_enabled = 1 + AND ( + b.execute_department_id IS NULL + OR b.execute_department_id = 0 + OR EXISTS ( + SELECT 1 FROM s_department + WHERE id = b.execute_department_id + AND is_del = 0 + AND appointment_enabled = 1 + AND department_status = 1 + ) + ) AND c.appointment_type_id = ?", $canshu); foreach ($plan as $p) { @@ -244,6 +255,17 @@ public function GetEnablePlan($regnum, $entrustids, $episodeid, $appointment_typ AND a.is_del = 0 AND b.is_del = 0 AND b.appointment_enabled = 1 + AND ( + b.execute_department_id IS NULL + OR b.execute_department_id = 0 + OR EXISTS ( + SELECT 1 FROM s_department + WHERE id = b.execute_department_id + AND is_del = 0 + AND appointment_enabled = 1 + AND department_status = 1 + ) + ) AND c.appointment_type_id IN ($appointment_types_placeholders)", $canshu); $mergedPlan = []; @@ -548,36 +570,27 @@ public function GetOptimalPlan($type, $regnum, $entrustids, $episodeid, $appoint } } - // 最优时间分配:按科室分组,逐个 item 找最早可用号源,可跨天 + // 最优时间分配:逐项目独立找最早可用号源,可跨天 private function doOptimalTimeAssign($startDate, $endDate, $regnum, $entrustids, $episodeid, $appointment_type, $items, $scope, $userDeptId, $occupied_plan_ids = [], $existingSharedAtPlan = []) { - // 按科室分组 - $deptGroups = $this->groupItemsByDept($items); - if (empty($deptGroups)) { - return \Yz::Return(true, '获取成功', []); - } - $result = []; - $currentDate = clone $startDate; - // 每组待分配的 items - $remainingGroups = []; - foreach ($deptGroups as $deptCode => $group) { - $remainingGroups[$deptCode] = [ - 'entrustids' => $group['entrustids'], - 'items' => $group['items'] - ]; - } + $usedCapacity = []; // 记录本次推荐中已分配占用的容量 + $assignedPlans = []; // 记录已分配的时间段,用于冲突检查 - while ($currentDate <= $endDate && !empty($remainingGroups)) { - $nowdate = $currentDate->format('Y-m-d'); - $newRemainingGroups = []; - $globalAssignedPlans = []; // 跨科室组的全局已分配时间段 + // 逐项目独立分配 + foreach ($items as $item) { + $entrustId = $item['entrust_id'] ?? null; + if (!$entrustId) { + $result[] = ['id' => $item['id'], 'plan_id' => 0, 'department_resources_name' => '', 'date' => '', 'begin_time' => '', 'end_time' => '']; + continue; + } - foreach ($remainingGroups as $deptCode => $group) { - $groupEntrustids = $group['entrustids']; - $groupItems = $group['items']; + $assigned = false; + $currentDate = clone $startDate; - $availablePlans = $this->getAvailablePlansForDate($regnum, $groupEntrustids, $episodeid, $appointment_type, $nowdate, $scope, $userDeptId); + while ($currentDate <= $endDate) { + $nowdate = $currentDate->format('Y-m-d'); + $availablePlans = $this->getAvailablePlansForDate($regnum, [$entrustId], $episodeid, $appointment_type, $nowdate, $scope, $userDeptId); // 过滤已占用的号源 if (!empty($occupied_plan_ids)) { @@ -588,72 +601,52 @@ private function doOptimalTimeAssign($startDate, $endDate, $regnum, $entrustids, } if (empty($availablePlans)) { - // 当天该科室无号源,整组留到下一轮 - $newRemainingGroups[$deptCode] = $group; + $currentDate->modify('+1 day'); continue; } - $usedCapacity = []; - $assignedPlans = []; - $remainingItems = []; - - foreach ($groupItems as $item) { - $assigned = false; - foreach ($availablePlans as $plan) { - $planId = $plan->id; - $useSeats = $item['use_seats'] ?? 1; - // 共享项目号源持有者:减去已有共享项目的 max - if (!empty($item['is_share_slot']) && $useSeats > 0) { - $existingMax = $existingSharedAtPlan[$planId] ?? 0; - $useSeats = max(0, $useSeats - $existingMax); - } - $remaining = ($plan->count ?? 0) - ($plan->used_count ?? 0); - $used = $usedCapacity[$planId] ?? 0; - - if (($remaining - $used) < $useSeats) continue; - - // 冲突检查:同一科室组内,不同排班时间不能重叠 - if ($this->isTimeConflict($assignedPlans, $plan)) continue; - // 冲突检查:跨科室组,时间段不能重叠 - if ($this->isTimeConflict($globalAssignedPlans, $plan)) continue; - - $result[] = [ - 'id' => $item['id'], - 'plan_id' => $planId, - 'department_resources_name' => $plan->department_resources_name, - 'date' => $plan->date, - 'begin_time' => $plan->begin_time, - 'end_time' => $plan->end_time - ]; - $usedCapacity[$planId] = ($usedCapacity[$planId] ?? 0) + $useSeats; - $assignedPlans[] = [ - 'resource_name' => $plan->department_resources_name, - 'begin_time' => $plan->begin_time, - 'end_time' => $plan->end_time - ]; - $globalAssignedPlans[] = [ - 'resource_name' => $plan->department_resources_name, - 'begin_time' => $plan->begin_time, - 'end_time' => $plan->end_time - ]; - $assigned = true; - break; + // 找到第一个能容纳的号源 + foreach ($availablePlans as $plan) { + $planId = $plan->id; + $useSeats = $item['use_seats'] ?? 1; + if (!empty($item['is_share_slot']) && $useSeats > 0) { + $existingMax = $existingSharedAtPlan[$planId] ?? 0; + $useSeats = max(0, $useSeats - $existingMax); } - if (!$assigned) { - $remainingItems[] = $item; - } - } - if (!empty($remainingItems)) { - $newRemainingGroups[$deptCode] = [ - 'entrustids' => $groupEntrustids, - 'items' => $remainingItems + // 容量检查:减去数据库已用 + 本次已分配 + $remaining = ($plan->count ?? 0) - ($plan->used_count ?? 0); + $used = $usedCapacity[$planId] ?? 0; + if (($remaining - $used) < $useSeats) continue; + + // 冲突检查:同一患者的不同项目时间段不能重叠 + if ($this->isTimeConflict($assignedPlans, $plan)) continue; + + $result[] = [ + 'id' => $item['id'], + 'plan_id' => $planId, + 'department_resources_name' => $plan->department_resources_name, + 'date' => $plan->date, + 'begin_time' => $plan->begin_time, + 'end_time' => $plan->end_time ]; + $usedCapacity[$planId] = ($usedCapacity[$planId] ?? 0) + $useSeats; + $assignedPlans[] = [ + 'resource_name' => $plan->department_resources_name, + 'begin_time' => $plan->begin_time, + 'end_time' => $plan->end_time + ]; + $assigned = true; + break; } + + if ($assigned) break; + $currentDate->modify('+1 day'); } - $remainingGroups = $newRemainingGroups; - $currentDate->modify('+1 day'); + if (!$assigned) { + $result[] = ['id' => $item['id'], 'plan_id' => 0, 'department_resources_name' => '', 'date' => '', 'begin_time' => '', 'end_time' => '']; + } } return \Yz::Return(true, '获取成功', $result); @@ -1706,6 +1699,79 @@ public function CancelYuYue($MainListId, $reg_num,$do_user=null) } } + //批量取消预约:统一事务,避免并发号源校正冲突 + public function BatchCancelYuYue($ids, $reg_num, $do_user = null) + { + date_default_timezone_set('PRC'); + $nowdatetime = date('Y-m-d H:i:s'); + + DB::beginTransaction(); + try { + $rosterIds = []; + + foreach ($ids as $MainListId) { + $mainInfo = DB::table('s_list') + ->where(['id' => $MainListId, 'reg_num' => $reg_num]) + ->lockForUpdate() + ->first(); + + if (!$mainInfo) { + throw new \Exception("医嘱不存在: {$MainListId}"); + } + + if ($mainInfo->list_status != 1) { + throw new \Exception("{$mainInfo->entrust} 无法取消,当前状态:" . $mainInfo->list_status); + } + + if ($mainInfo->roster_id) { + $rosterIds[] = $mainInfo->roster_id; + } + + $u_data = [ + 'list_status' => 0, + 'reservation_date' => null, + 'reservation_time' => null, + 'reservation_sources' => null, + 'services_group' => null, + 'roster_id' => null, + 'xuhao' => null, + 'department_id' => null, + 'appointment_type_id' => null, + 'appointment_use_plan_detail' => null, + 'canel_time' => $nowdatetime, + 'is_emergency' => 0, + ]; + + DB::table('s_list')->where(['id' => $MainListId])->update($u_data); + + DB::table('s_list_log')->insert([ + 'list_id' => $mainInfo->id, + 'reg_num' => $mainInfo->reg_num, + 'old_status' => $mainInfo->list_status, + 'new_status' => 0, + 'create_user' => $do_user, + 'note' => '取消预约', + 'data' => json_encode($u_data, JSON_UNESCAPED_UNICODE), + 'created_at' => $nowdatetime, + ]); + } + + // 每个 roster 只校正一次 + $rosterIds = array_unique($rosterIds); + foreach ($rosterIds as $rid) { + $this->correctUsedCount($rid); + } + + DB::commit(); + return \Yz::Return(true, '取消成功', []); + + } catch (\Exception $e) { + DB::rollBack(); + Log::error('批量取消预约失败', ['ids' => $ids, 'error' => $e->getMessage()]); + return \Yz::echoError1($e->getMessage()); + } + } + public function releaseSourceFromDetail($mainListId, $mainInfo = null) { // 由 correctUsedCount 校正替代,不再在此处释放号源 @@ -1737,7 +1803,7 @@ private function releaseSourceFallback($mainInfo) /** * 校正排班号源:根据项目配置(is_share_slot / use_seats)重新计算 used_count */ - private function correctUsedCount($rosterId) + public function correctUsedCount($rosterId) { if (!$rosterId) return; @@ -1748,9 +1814,10 @@ private function correctUsedCount($rosterId) // 获取该排班上所有活跃项目及其配置 $activeItems = DB::table('s_list') - ->join('s_check_item', 's_list.entrust', '=', 's_check_item.item_name') + ->join('s_check_item', 's_list.entrust_code', '=', 's_check_item.item_code') ->where('s_list.roster_id', $rosterId) - ->where('s_list.list_status', 1) + ->whereIn('s_list.list_status', [1, 2, 3]) + ->where('s_list.is_nullify', 0) ->where('s_check_item.status', 1) ->where('s_check_item.is_del', 0) ->select('s_list.*', 's_check_item.use_seats', 's_check_item.is_share_slot') diff --git a/Laravel/app/Services/Xml/ShenQingDanService.php b/Laravel/app/Services/Xml/ShenQingDanService.php index f48a634..1495f4a 100644 --- a/Laravel/app/Services/Xml/ShenQingDanService.php +++ b/Laravel/app/Services/Xml/ShenQingDanService.php @@ -143,10 +143,41 @@ public function UpdateStatus($result, $jsonData) if(!!$mainInfo){ if(in_array($order['OEORIStatusCode'],$zuofei)){ //如果是作废 $beizhu='作废'; - $data=[ - 'is_nullify'=>1, - 'updated_at'=>date('Y-m-d H:i:s') - ]; + $nowdatetime = date('Y-m-d H:i:s'); + + if ($mainInfo->list_status != 0 && $mainInfo->roster_id) { + // 清空预约相关字段 + $clearData = [ + 'list_status' => 0, + 'reservation_date' => null, + 'reservation_time' => null, + 'reservation_sources' => null, + 'services_group' => null, + 'roster_id' => null, + 'xuhao' => null, + 'department_id' => null, + 'appointment_type_id' => null, + 'appointment_use_plan_detail' => null, + 'canel_time' => $nowdatetime, + 'is_emergency' => 0, + 'is_nullify' => 1, + 'updated_at' => $nowdatetime, + ]; + + $update = DB::table('s_list')->where('id', $mainInfo->id)->update($clearData); + + // 校正号源 + if ($update) { + $planService = new PlanListService(); + $planService->correctUsedCount($mainInfo->roster_id); + } + } else { + $data = [ + 'is_nullify' => 1, + 'updated_at' => $nowdatetime, + ]; + $update = DB::table('s_list')->where('id', $mainInfo->id)->update($data); + } } if(in_array($order['OEORIStatusCode'],$jiaofei)){ //如果是完成 $beizhu='缴费完成'; diff --git a/Laravel/routes/api.php b/Laravel/routes/api.php index 51165d8..3a4952d 100644 --- a/Laravel/routes/api.php +++ b/Laravel/routes/api.php @@ -181,6 +181,7 @@ Route::post('admin/GetPlanUsedList','App\Http\Controllers\API\Admin\YeWu\PlanListController@GetUsedList');//计划占用详情列表 Route::get('admin/NoPayCancel','App\Http\Controllers\API\Admin\YeWu\WorkMainController@NoPayCancel');//检查是否有超时未支付的门诊预约记录,如果有则给其取消,并恢复名额 Route::post('admin/DoctorCancelYuYue','App\Http\Controllers\API\Admin\YeWu\WorkMainController@DoctorCancelYuYue');//医生取消预约 + Route::post('admin/BatchCancelYuYue','App\Http\Controllers\API\Admin\YeWu\PlanListController@BatchCancelYuYue');//批量取消预约 Route::post('admin/NearestEnablePlanDate','App\Http\Controllers\API\Admin\YeWu\PlanListController@NearestEnablePlanDate');//获取最近可用日期 Route::post('testSendMsg','App\Http\Controllers\TestSendMsgController@SendMsg');//测试发送短信 Route::post('admin/GetPersonYuYueList','App\Http\Controllers\API\Admin\YeWu\WorkMainController@GetPersonYuYueList');//医生获取某人全部预约记录 diff --git a/YiJi-admin/src/api/api.js b/YiJi-admin/src/api/api.js index fa1128a..c7ff5bf 100644 --- a/YiJi-admin/src/api/api.js +++ b/YiJi-admin/src/api/api.js @@ -582,6 +582,10 @@ export const CheckIsDaiJian = (data = {}) => { export const DoctorCancelYuYue = (data = {}) => { return axios({ url: import.meta.env.VITE_APP_API + 'admin/DoctorCancelYuYue', data: data }) } +//批量取消预约 +export const BatchCancelYuYue = (data = {}) => { + return axios({ url: import.meta.env.VITE_APP_API + 'admin/BatchCancelYuYue', data: data }) +} //预约渠道统计 export const countAppointmentType = (data = {}) => { return axios({ url: import.meta.env.VITE_APP_API + 'v1/admin/countAppointmentType', data: data }) diff --git a/YiJi-admin/src/components/Yewu/YuYue202506.vue b/YiJi-admin/src/components/Yewu/YuYue202506.vue index 4a021d1..483c446 100644 --- a/YiJi-admin/src/components/Yewu/YuYue202506.vue +++ b/YiJi-admin/src/components/Yewu/YuYue202506.vue @@ -224,7 +224,8 @@ CheckIsDaiJian, PlanYuYue, CheckEntrstItemGroup2, - DoctorCancelYuYue,GetConfigInfo2,GetOptimalPlan + DoctorCancelYuYue,GetConfigInfo2,GetOptimalPlan, + BatchCancelYuYue } from '@/api/api.js' import { ElMessage, @@ -651,8 +652,10 @@ cachedPlans.value = plans zhanWeiCount.value = zwCount if (plans.length > 0) { - - if (activeZhenShi.value == '') activeZhenShi.value = plans[0].department_resources_name + const deptExists = plans.some(v => v.department_resources_name === activeZhenShi.value) + if (!deptExists) { + activeZhenShi.value = plans[0].department_resources_name + } const timeSlotsSet = new Set(); const zhenshiSet = new Set(); plans.forEach(v => { @@ -1708,32 +1711,19 @@ })) loading.value = true - const promises = selectedRows.value.map((v) => { - return DoctorCancelYuYue({ - MainListId: v.id, - reg_num: v.reg_num, - do_user:props.do_user, - }).then(res => { - if (res.status) { - ElMessage.success('取消成功') - } else { - ElMessage.error(res.msg) - } - }).catch(err => { - ElMessage.error('取消失败') - console.error(err) - }) + const res = await BatchCancelYuYue({ + ids: selectedRows.value.map(v => v.id), + reg_num: selectedRows.value[0].reg_num, + do_user: props.do_user, }) - // 等待所有请求完成 - await Promise.all(promises) - - loading.value = false - GetMainInfo(() => { - // 回填未勾选项目的临时匹配号源 - entrustTableDate.value.forEach(v => { - if (tempPlanCache[v.id]) { - v.temp_plan_id = tempPlanCache[v.id].temp_plan_id + if (res.status) { + ElMessage.success('取消成功') + loading.value = false + GetMainInfo(() => { + entrustTableDate.value.forEach(v => { + if (tempPlanCache[v.id]) { + v.temp_plan_id = tempPlanCache[v.id].temp_plan_id if (!v.department_resources) v.department_resources = {} v.department_resources.department_resources_name = tempPlanCache[v.id].department_resources_name v.reservation_date = tempPlanCache[v.id].reservation_date @@ -1782,6 +1772,10 @@ }) } }, true) + } else { + ElMessage.error(res.msg || '取消失败') + loading.value = false + } })