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.

415 lines
16 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}");
}
}
$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. 获取节假日列表
$holiday_list = DB::table('s_holiday')
->whereBetween('date', $dateRange)
->where(['type' => 2])
->pluck('date')
->toArray();
$generatedDates = [];
$skippedDates = [];
$success_count = 0;
// 4. 开启事务
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)) {
foreach ($models as $model) {
$skippedDates[] = [
'date' => $current_date_str,
'template_id' => $model->id,
'reason' => '非节假日,跳过',
];
}
$current_date->modify('+1 day');
continue;
}
// 如果"节假日不可用" (HolidayEnable == 0),且当天是节假日 -> 跳过
if ($holidayEnable == 0 && in_array($current_date_str, $holiday_list)) {
foreach ($models as $model) {
$skippedDates[] = [
'date' => $current_date_str,
'template_id' => $model->id,
'reason' => '节假日不可用,跳过',
];
}
$current_date->modify('+1 day');
continue;
}
// --- 资源级存在性检查:当天该资源下是否有任何号源 ---
$resources_id = $models->first()->resources_id;
$resourceExists = DB::table('s_source_roster_detail')
->where('resources_id', $resources_id)
->where('date', $current_date_str)
->where('is_del', 0)
->exists();
if ($resourceExists) {
foreach ($models as $model) {
$skippedDates[] = [
'date' => $current_date_str,
'template_id' => $model->id,
'reason' => '已存在',
];
}
$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)) {
$skippedDates[] = [
'date' => $current_date_str,
'template_id' => $model->id,
'reason' => '不在时令范围',
];
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}");
}
$generatedDates[] = [
'date' => $current_date_str,
'template_id' => $model->id,
'detail_id' => $plan_id,
];
$success_count++;
// --- 插入关联表:数量配置 ---
$this->insertCountInfo($plan_id, $model->id);
// --- 插入关联表:设备配置 ---
$this->insertDeviceInfo($plan_id, $model->device_id, $model->id);
}
$current_date->modify('+1 day');
}
// 5. 提交事务
DB::commit();
return [
'success' => true,
'count' => $success_count,
'generated_dates' => $generatedDates,
'skipped_dates' => $skippedDates,
];
} catch (Exception $e) {
// 6. 异常回滚
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 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("设备关联创建失败");
}
}
}
}