main
岩仔88 1 week ago
parent ff7c1eb579
commit ff6e459884

@ -10,6 +10,9 @@ use Exception;
class RosterService class RosterService
{ {
// 资源缓存,避免重复查询
private $resourceCache = [];
public function generatePlans($dateRange, $planModelIds, $userId, $dateType = 1, $holidayEnable = 1) public function generatePlans($dateRange, $planModelIds, $userId, $dateType = 1, $holidayEnable = 1)
{ {
// 1. 基础参数校验 // 1. 基础参数校验
@ -39,6 +42,16 @@ class RosterService
$start_date = new DateTime($dateRange[0]); $start_date = new DateTime($dateRange[0]);
$end_date = new DateTime($dateRange[1]); $end_date = new DateTime($dateRange[1]);
// ==========================================
// 【新增】1. 整体校验:模板与资源时令配置匹配
// ==========================================
$this->validateTemplates($models);
// ==========================================
// 【新增】2. 整体校验:资源时段冲突检测
// ==========================================
$this->checkResourceTimeConflict($models, $start_date, $end_date);
// ========================================== // ==========================================
// 【核心】3. 重复性检测 (在事务外执行,提高性能) // 【核心】3. 重复性检测 (在事务外执行,提高性能)
// ========================================== // ==========================================
@ -70,13 +83,13 @@ class RosterService
// --- 逻辑判断:节假日过滤 --- // --- 逻辑判断:节假日过滤 ---
// 如果是“仅节假日”模式 (date_type == 2),且当天不是节假日 -> 跳过 // 如果是"仅节假日"模式 (date_type == 2),且当天不是节假日 -> 跳过
if ($dateType == 2 && !in_array($current_date_str, $holiday_list)) { if ($dateType == 2 && !in_array($current_date_str, $holiday_list)) {
$current_date->modify('+1 day'); $current_date->modify('+1 day');
continue; continue;
} }
// 如果“节假日不可用” (HolidayEnable == 0),且当天是节假日 -> 跳过 // 如果"节假日不可用" (HolidayEnable == 0),且当天是节假日 -> 跳过
if ($holidayEnable == 0 && in_array($current_date_str, $holiday_list)) { if ($holidayEnable == 0 && in_array($current_date_str, $holiday_list)) {
$current_date->modify('+1 day'); $current_date->modify('+1 day');
continue; continue;
@ -88,13 +101,19 @@ class RosterService
foreach ($models as $model) { foreach ($models as $model) {
// --- 逻辑判断:星期匹配 --- // --- 逻辑判断:星期匹配 ---
// 如果是“按星期”模式 (date_type == 1) 且模板也是按星期定义的,必须星期一致 // 如果是"按星期"模式 (date_type == 1) 且模板也是按星期定义的,必须星期一致
if ($dateType == 1 && isset($model->date_type) && $model->date_type == 1) { if ($dateType == 1 && isset($model->date_type) && $model->date_type == 1) {
if ($model->weekname !== $weekname) { if ($model->weekname !== $weekname) {
continue; continue;
} }
} }
// 【新增】时令校验:跳过不在时令范围内的日期
$resource = $this->getCachedResource($model->resources_id);
if (!$this->isDateInSeasonalRange($current_date, $model->type, $resource)) {
continue; // 跳过该模板在此日期的生成
}
// --- 构造插入数据 --- // --- 构造插入数据 ---
$data = [ $data = [
'roster_id' => $model->id, '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 . ' '; $msglist .= $item->date . ' ';
$msgIds .= $item->id . ' '; $msgIds .= $item->id . ' ';
// 拼接模板信息 // 拼接模板信息
$msg .= " " . $model->weekname . $model->begin_time . '-' . $model->end_time . " "; $msg .= "" . $model->weekname . $model->begin_time . '-' . $model->end_time . " ";
} }
} }
} }

@ -99,12 +99,12 @@
// //
const timeRangeList = ref([ const timeRangeList = ref([
{ {
type: 'summer', type: '1',
type_name: '夏季排班', type_name: '夏季排班',
periods: [['', '']] periods: [['', '']]
}, },
{ {
type: 'winter', type: '2',
type_name: '冬季排班', type_name: '冬季排班',
periods: [['', '']] periods: [['', '']]
} }
@ -146,12 +146,12 @@
// timeRangeList // timeRangeList
timeRangeList.value = [ timeRangeList.value = [
{ {
type: 'summer', type: '1',
type_name: '夏季排班', type_name: '夏季排班',
periods: [['', '']] periods: [['', '']]
}, },
{ {
type: 'winter', type: '2',
type_name: '冬季排班', type_name: '冬季排班',
periods: [['', '']] periods: [['', '']]
} }

@ -43,7 +43,7 @@
<el-tag v-if="scope.row.time_mode==1" class="ml-2" type="warning"></el-tag> <el-tag v-if="scope.row.time_mode==1" class="ml-2" type="warning"></el-tag>
<div v-if="scope.row.time_mode==1 && scope.row.time_range" style="font-size: 12px; margin-left: 8px;"> <div v-if="scope.row.time_mode==1 && scope.row.time_range" style="font-size: 12px; margin-left: 8px;">
<div v-for="season in scope.row.time_range" :key="season.type" style="margin-bottom: 2px;"> <div v-for="season in scope.row.time_range" :key="season.type" style="margin-bottom: 2px;">
<span style="color: #E6A23C;" v-if="season.type === 'summer'"></span> <span style="color: #E6A23C;" v-if="season.type === '1'"></span>
<span style="color: #409EFF;" v-else></span> <span style="color: #409EFF;" v-else></span>
<span v-for="(period, index) in season.periods" :key="index"> <span v-for="(period, index) in season.periods" :key="index">
{{ period.start }}~{{ period.end }}<span v-if="index < season.periods.length - 1"></span> {{ period.start }}~{{ period.end }}<span v-if="index < season.periods.length - 1"></span>

Loading…
Cancel
Save