预约号源调整

main
鹿和sa0ChunLuyu 6 days ago
parent 3544e0aecb
commit 7644ab0e4f

@ -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()
{

@ -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 = [];
$usedCapacity = []; // 记录本次推荐中已分配占用的容量
$assignedPlans = []; // 记录已分配的时间段,用于冲突检查
// 逐项目独立分配
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;
}
$result = [];
$assigned = false;
$currentDate = clone $startDate;
// 每组待分配的 items
$remainingGroups = [];
foreach ($deptGroups as $deptCode => $group) {
$remainingGroups[$deptCode] = [
'entrustids' => $group['entrustids'],
'items' => $group['items']
];
}
while ($currentDate <= $endDate && !empty($remainingGroups)) {
while ($currentDate <= $endDate) {
$nowdate = $currentDate->format('Y-m-d');
$newRemainingGroups = [];
$globalAssignedPlans = []; // 跨科室组的全局已分配时间段
foreach ($remainingGroups as $deptCode => $group) {
$groupEntrustids = $group['entrustids'];
$groupItems = $group['items'];
$availablePlans = $this->getAvailablePlansForDate($regnum, $groupEntrustids, $episodeid, $appointment_type, $nowdate, $scope, $userDeptId);
$availablePlans = $this->getAvailablePlansForDate($regnum, [$entrustId], $episodeid, $appointment_type, $nowdate, $scope, $userDeptId);
// 过滤已占用的号源
if (!empty($occupied_plan_ids)) {
@ -588,34 +601,26 @@ 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'],
@ -631,29 +636,17 @@ private function doOptimalTimeAssign($startDate, $endDate, $regnum, $entrustids,
'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;
}
if (!$assigned) {
$remainingItems[] = $item;
}
}
if (!empty($remainingItems)) {
$newRemainingGroups[$deptCode] = [
'entrustids' => $groupEntrustids,
'items' => $remainingItems
];
}
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')

@ -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='缴费完成';

@ -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');//医生获取某人全部预约记录

@ -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 })

@ -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,29 +1711,16 @@
}))
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)
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
@ -1782,6 +1772,10 @@
})
}
}, true)
} else {
ElMessage.error(res.msg || '取消失败')
loading.value = false
}
})

Loading…
Cancel
Save