whereIn('id', $planModelIds) ->get(); if ($models->isEmpty()) { throw new Exception('未找到有效的排班模板'); } foreach ($models as $model) { if ($model->status != 1 || $model->is_del != 0) { throw new Exception("模板状态异常,请重新选择!异常模板Id: {$model->id}"); } } // 获取部门ID (用于查重,取第一个模板的部门ID,假设批量操作通常针对同一部门) // 如果业务允许跨部门混合勾选,这里可能需要调整查重逻辑(按部门分组查) $department_id = $models->first()->department_id; $start_date = new DateTime($dateRange[0]); $end_date = new DateTime($dateRange[1]); // ========================================== // 【新增】1. 整体校验:模板与资源时令配置匹配 // ========================================== $this->validateTemplates($models); // ========================================== // 【新增】2. 整体校验:资源时段冲突检测 // ========================================== $this->checkResourceTimeConflict($models, $start_date, $end_date); // ========================================== // 【核心】3. 重复性检测 (在事务外执行,提高性能) // ========================================== $this->checkDuplicateRecords( $department_id, $planModelIds, $start_date, $end_date, $models ); // 如果上面没抛异常,说明没有重复,继续往下执行 // 4. 获取节假日列表 $holiday_list = DB::table('s_holiday') ->whereBetween('date', $dateRange) ->where(['type' => 2]) ->pluck('date') ->toArray(); $success_count = 0; // 5. 开启事务 DB::beginTransaction(); try { $current_date = clone $start_date; while ($current_date <= $end_date) { $current_date_str = $current_date->format('Y-m-d'); // --- 逻辑判断:节假日过滤 --- // 如果是"仅节假日"模式 (date_type == 2),且当天不是节假日 -> 跳过 if ($dateType == 2 && !in_array($current_date_str, $holiday_list)) { $current_date->modify('+1 day'); continue; } // 如果"节假日不可用" (HolidayEnable == 0),且当天是节假日 -> 跳过 if ($holidayEnable == 0 && in_array($current_date_str, $holiday_list)) { $current_date->modify('+1 day'); continue; } // --- 获取星期 --- $weekday = (int)$current_date->format('w'); $weekname = $this->getWeekName($weekday); foreach ($models as $model) { // --- 逻辑判断:星期匹配 --- // 如果是"按星期"模式 (date_type == 1) 且模板也是按星期定义的,必须星期一致 if ($dateType == 1 && isset($model->date_type) && $model->date_type == 1) { if ($model->weekname !== $weekname) { continue; } } // 【新增】时令校验:跳过不在时令范围内的日期 $resource = $this->getCachedResource($model->resources_id); if (!$this->isDateInSeasonalRange($current_date, $model->type, $resource)) { continue; // 跳过该模板在此日期的生成 } // --- 构造插入数据 --- $data = [ 'roster_id' => $model->id, 'date' => $current_date_str, 'weekname' => $weekname, 'department_id' => $model->department_id, 'resources_id' => $model->resources_id ?? null, 'device_id' => $model->device_id ?? null, 'period_id' => $model->period_id ?? null, 'patient_type' => $model->patient_type ?? null, 'begin_time' => $model->begin_time, 'end_time' => $model->end_time, 'end_reservation_time' => $model->end_reservation_time ?? null, 'time_unit' => $model->time_unit ?? null, 'status' => 1, 'adduser' => $userId, 'is_del' => 0, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]; // --- 插入主表 --- $plan_id = DB::table('s_source_roster_detail')->insertGetId($data); if (!$plan_id) { throw new Exception("号源明细插入失败,日期:{$current_date_str}, 模板ID:{$model->id}"); } $success_count++; // --- 插入关联表:数量配置 --- $this->insertCountInfo($plan_id, $model->id); // --- 插入关联表:设备配置 --- // 注意:原逻辑如果 device_id 为空会报错,这里保持原逻辑 $this->insertDeviceInfo($plan_id, $model->device_id, $model->id); } $current_date->modify('+1 day'); } // 6. 提交事务 DB::commit(); return ['success' => true, 'count' => $success_count]; } catch (Exception $e) { // 7. 异常回滚 DB::rollBack(); // 记录日志 Log::error('Roster Generation Failed: ' . $e->getMessage(), [ 'dateRange' => $dateRange, 'user_id' => $userId ]); // 重新抛出,让 Controller 处理 throw $e; } } /** * 整体校验:模板与资源时令配置必须严格匹配 * - 模板 type=1/2 → 资源 time_mode=1 * - 模板 type=0 → 资源 time_mode=0 */ private function validateTemplates($models) { foreach ($models as $model) { // 获取资源信息 $resource = $this->getCachedResource($model->resources_id); if (!$resource) { throw new Exception("模板关联的资源不存在,模板ID: {$model->id}"); } // 严格一对一匹配 if ($model->type != 0 && $resource->time_mode != 1) { $errorMsg = "模板ID {$model->id} 配置了时令(type={$model->type}),但资源未开启时令,请检查资源配置!"; Log::error($errorMsg); throw new Exception($errorMsg); } if ($model->type == 0 && $resource->time_mode != 0) { $errorMsg = "模板ID {$model->id} 未配置时令(type=0),但资源开启了时令,请检查资源配置!"; Log::error($errorMsg); throw new Exception($errorMsg); } } } /** * 资源级别查重:同一资源同一天的时间段不能重叠 * 同一资源同一天可以有多个不同时段,但是时段不能交差 * 边界判断:不重叠(一个时间点结束,另一个可以开始) */ private function checkResourceTimeConflict($models, $startDate, $endDate) { $startStr = $startDate->format('Y-m-d'); $endStr = $endDate->format('Y-m-d'); foreach ($models as $model) { // 查询该资源在日期范围内已存在的排班 $existingPlans = DB::table('s_source_roster_detail') ->where('resources_id', $model->resources_id) ->where('date', '>=', $startStr) ->where('date', '<=', $endStr) ->where('is_del', 0) ->get(); foreach ($existingPlans as $existing) { // 检查时间是否重叠(同一天才检查) $existingDate = $existing->date; // 使用模板的星期作为参考日期 $templateDate = $this->getTemplateDate($model); if ($existingDate === $templateDate) { if ($this->isTimeOverlap( $model->begin_time, $model->end_time, $existing->begin_time, $existing->end_time )) { throw new Exception( "资源ID [{$model->resources_id}] 在 {$existingDate} 存在时段冲突!" . "新增: {$model->begin_time}-{$model->end_time} " . "与已存在的 {$existing->begin_time}-{$existing->end_time} 重叠" ); } } } } } /** * 判断两个时段是否重叠 * 不重叠返回 false * 边界判断:不重叠(一个时间点结束,另一个可以开始) * 时段1: [start1, end1) 时段2: [start2, end2) * 重叠条件:时段1的结束时间 > 时段2的开始时间 && 时段2的结束时间 > 时段1的开始时间 */ private function isTimeOverlap($start1, $end1, $start2, $end2) { return ($end1 > $start2) && ($end2 > $start1); } /** * 时令日期匹配:判断日期是否在时令范围内 * - 如果资源 time_mode != 1 或模板 type == 0,不需要时令校验,返回 true * - 否则检查日期是否在对应季节的周期范围内 */ private function isDateInSeasonalRange($currentDate, $modelType, $resource) { // 不需要时令校验 if ($resource->time_mode != 1 || $modelType == 0) { return true; } $currentMMDD = $currentDate->format('m-d'); // 解析 time_range(从缓存中获取) $time_range = json_decode($resource->time_range, true); if (!$time_range || !is_array($time_range)) { return false; } // 遍历所有季节周期 foreach ($time_range as $season) { // 转换类型进行匹配(字符串 "1"/"2" vs 整数 1/2) if ((string)($season['type'] ?? '') === (string)$modelType) { // 检查是否在任意周期范围内 if (isset($season['periods']) && is_array($season['periods'])) { foreach ($season['periods'] as $period) { $start = $period['start'] ?? ''; $end = $period['end'] ?? ''; if (!empty($start) && !empty($end) && $currentMMDD >= $start && $currentMMDD <= $end) { return true; // 匹配成功 } } } } } return false; // 不在时令范围内 } /** * 获取缓存的资源信息,避免重复查询 */ private function getCachedResource($resourceId) { if (!isset($this->resourceCache[$resourceId])) { $this->resourceCache[$resourceId] = DB::table('s_department_resources') ->where('id', $resourceId) ->first(); } return $this->resourceCache[$resourceId]; } /** * 获取模板的日期(用于资源查重时的日期比较) * 注意:这里仅用于比较,实际生成时按循环中的日期 */ private function getTemplateDate($model) { // 如果模板有固定日期,返回固定日期 if (isset($model->date)) { return $model->date; } // 否则返回 null(表示需要按循环日期比较) return null; } /** * 独立的重复检测方法 * 如果发现重复,直接抛出包含详细信息的异常 */ private function checkDuplicateRecords($department_id, $planModelIds, $startDate, $endDate, $models) { $startStr = $startDate->format('Y-m-d'); $endStr = $endDate->format('Y-m-d'); // 查询已存在的记录 $checkList = DB::table('s_source_roster_detail') ->where('department_id', $department_id) ->whereIn('roster_id', $planModelIds) ->where('date', '>=', $startStr) ->where('date', '<=', $endStr) ->where('is_del', 0) ->get(); if ($checkList->isNotEmpty()) { // 构造详细的错误提示信息 (完全还原你原代码的逻辑) $msg = '已有重复的计划明细,禁止创建!当前选中的'; $msglist = ''; $msgIds = ''; // 优化:将检查结果转为映射数组,避免双重循环 O(N*M) // key: roster_id, value: array of items $checkMap = []; foreach ($checkList as $item) { if (!isset($checkMap[$item->roster_id])) { $checkMap[$item->roster_id] = []; } $checkMap[$item->roster_id][] = $item; } foreach ($models as $model) { if (isset($checkMap[$model->id])) { foreach ($checkMap[$model->id] as $item) { $msglist .= $item->date . ' '; $msgIds .= $item->id . ' '; // 拼接模板信息 $msg .= "" . $model->weekname . $model->begin_time . '-' . $model->end_time . " "; } } } $fullErrorMessage = $msg . '已存在相同记录,
存在于:
' . $msglist . '
对应记录Id为:' . $msgIds . '
请先删除后再操作'; // 抛出异常,中断流程 throw new Exception($fullErrorMessage); } } private function getWeekName($weekday) { $map = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; return $map[$weekday] ?? ''; } private function insertCountInfo($detailId, $rosterId) { $model_count_info = DB::table('s_source_roster_count')->where(['roster_id' => $rosterId])->get(); if ($model_count_info->isEmpty()) { throw new Exception("模板数量信息异常,请重新选择!异常模板Id: {$rosterId}"); } foreach ($model_count_info as $info) { $success = DB::table('s_source_roster_detail_count')->insert([ 'roster_detail_id' => $detailId, 'appointment_type_id' => $info->appointment_type_id, 'count' => $info->count, 'locked_count' => $info->locked_count, 'max_total' => $info->max_total, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); if (!$success) { throw new Exception("渠道数量创建失败"); } } } private function insertDeviceInfo($detailId, $deviceIdStr, $modelId) { // 保持原逻辑:如果模板没配设备,视为异常 if (empty($deviceIdStr)) { throw new Exception("模板未关联设备,请重新选择!异常模板Id: {$modelId}"); } $device_ids = explode(",", $deviceIdStr); foreach ($device_ids as $dv_value) { $dv_value = trim($dv_value); if ($dv_value === '') continue; $success = DB::table('s_source_roster_detail_device')->insert([ 'roster_detail_id' => $detailId, 'device_id' => $dv_value, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); if (!$success) { throw new Exception("设备关联创建失败"); } } } }