|
|
|
|
@ -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 . " ";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|