|
|
<?php
|
|
|
|
|
|
namespace App\Services\Admin\YeWu;
|
|
|
|
|
|
use DateTime;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
|
use Exception;
|
|
|
|
|
|
class RosterService
|
|
|
{
|
|
|
// 资源缓存,避免重复查询
|
|
|
private $resourceCache = [];
|
|
|
|
|
|
public function generatePlans($dateRange, $planModelIds, $userId, $dateType = 1, $holidayEnable = 1)
|
|
|
{
|
|
|
// 1. 基础参数校验
|
|
|
if (!is_array($dateRange) || count($dateRange) != 2 || empty($planModelIds)) {
|
|
|
throw new Exception('日期范围或模板ID参数错误');
|
|
|
}
|
|
|
|
|
|
// 2. 获取模板信息并校验状态
|
|
|
$models = DB::table('s_source_roster')
|
|
|
->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 . '已存在相同记录,</br>存在于:</br>' . $msglist . '</br>对应记录Id为:' . $msgIds . '</br>请先删除后再操作';
|
|
|
|
|
|
// 抛出异常,中断流程
|
|
|
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,
|
|
|
'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("设备关联创建失败");
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|