You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

433 lines
17 KiB
PHP

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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,
'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("设备关联创建失败");
}
}
}
}