From ff6e4598845516f354ae1983694f07dec16cea65 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B2=A9=E4=BB=9488?= <>
Date: Thu, 12 Mar 2026 17:30:59 +0800
Subject: [PATCH] =?UTF-8?q?=E6=97=B6=E4=BB=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/Services/Admin/YeWu/RosterService.php | 181 +++++++++++++++++-
.../Yewu/DepartmentResourcesSave.vue | 8 +-
.../src/views/YeWu/DepartmentResources.vue | 2 +-
3 files changed, 182 insertions(+), 9 deletions(-)
diff --git a/Laravel/app/Services/Admin/YeWu/RosterService.php b/Laravel/app/Services/Admin/YeWu/RosterService.php
index 8315df8..2dd0021 100644
--- a/Laravel/app/Services/Admin/YeWu/RosterService.php
+++ b/Laravel/app/Services/Admin/YeWu/RosterService.php
@@ -10,6 +10,9 @@ use Exception;
class RosterService
{
+ // 资源缓存,避免重复查询
+ private $resourceCache = [];
+
public function generatePlans($dateRange, $planModelIds, $userId, $dateType = 1, $holidayEnable = 1)
{
// 1. 基础参数校验
@@ -39,6 +42,16 @@ class RosterService
$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. 重复性检测 (在事务外执行,提高性能)
// ==========================================
@@ -70,13 +83,13 @@ class RosterService
// --- 逻辑判断:节假日过滤 ---
- // 如果是“仅节假日”模式 (date_type == 2),且当天不是节假日 -> 跳过
+ // 如果是"仅节假日"模式 (date_type == 2),且当天不是节假日 -> 跳过
if ($dateType == 2 && !in_array($current_date_str, $holiday_list)) {
$current_date->modify('+1 day');
continue;
}
- // 如果“节假日不可用” (HolidayEnable == 0),且当天是节假日 -> 跳过
+ // 如果"节假日不可用" (HolidayEnable == 0),且当天是节假日 -> 跳过
if ($holidayEnable == 0 && in_array($current_date_str, $holiday_list)) {
$current_date->modify('+1 day');
continue;
@@ -88,13 +101,19 @@ class RosterService
foreach ($models as $model) {
// --- 逻辑判断:星期匹配 ---
- // 如果是“按星期”模式 (date_type == 1) 且模板也是按星期定义的,必须星期一致
+ // 如果是"按星期"模式 (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,
@@ -153,6 +172,160 @@ class RosterService
}
}
+ /**
+ * 整体校验:模板与资源时令配置必须严格匹配
+ * - 模板 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;
+ }
+
/**
* 独立的重复检测方法
* 如果发现重复,直接抛出包含详细信息的异常
@@ -193,7 +366,7 @@ class RosterService
$msglist .= $item->date . ' ';
$msgIds .= $item->id . ' ';
// 拼接模板信息
- $msg .= " " . $model->weekname . $model->begin_time . '-' . $model->end_time . " ";
+ $msg .= "" . $model->weekname . $model->begin_time . '-' . $model->end_time . " ";
}
}
}
diff --git a/YiJi-admin/src/components/Yewu/DepartmentResourcesSave.vue b/YiJi-admin/src/components/Yewu/DepartmentResourcesSave.vue
index 142d77b..e70d264 100644
--- a/YiJi-admin/src/components/Yewu/DepartmentResourcesSave.vue
+++ b/YiJi-admin/src/components/Yewu/DepartmentResourcesSave.vue
@@ -99,12 +99,12 @@
// 时令数据
const timeRangeList = ref([
{
- type: 'summer',
+ type: '1',
type_name: '夏季排班',
periods: [['', '']]
},
{
- type: 'winter',
+ type: '2',
type_name: '冬季排班',
periods: [['', '']]
}
@@ -146,12 +146,12 @@
// 如果开启时令且 timeRangeList 为空,才初始化默认值
timeRangeList.value = [
{
- type: 'summer',
+ type: '1',
type_name: '夏季排班',
periods: [['', '']]
},
{
- type: 'winter',
+ type: '2',
type_name: '冬季排班',
periods: [['', '']]
}
diff --git a/YiJi-admin/src/views/YeWu/DepartmentResources.vue b/YiJi-admin/src/views/YeWu/DepartmentResources.vue
index fb94bfc..60cc10f 100644
--- a/YiJi-admin/src/views/YeWu/DepartmentResources.vue
+++ b/YiJi-admin/src/views/YeWu/DepartmentResources.vue
@@ -43,7 +43,7 @@