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.

422 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. 获取节假日列表
$holiday_list = DB::table('s_holiday')
->whereBetween('date', $dateRange)
->where(['type' => 2])
->pluck('date')
->toArray();
// ==========================================
// 3. 整体校验:资源时段冲突检测(先检查再生成)
// ==========================================
$conflicts = $this->checkResourceTimeConflict(
$models, $start_date, $end_date, $dateType, $holidayEnable, $holiday_list
);
if (!empty($conflicts)) {
return [
'success' => false,
'conflicts' => $conflicts,
];
}
$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;
}
// --- 获取星期 ---
$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, $dateType, $holidayEnable, $holidayList)
{
$conflicts = [];
$currentDate = clone $startDate;
while ($currentDate <= $endDate) {
$dateStr = $currentDate->format('Y-m-d');
// --- 节假日过滤(与主循环一致) ---
if ($dateType == 2 && !in_array($dateStr, $holidayList)) {
$currentDate->modify('+1 day');
continue;
}
if ($holidayEnable == 0 && in_array($dateStr, $holidayList)) {
$currentDate->modify('+1 day');
continue;
}
$weekday = (int)$currentDate->format('w');
$weekname = $this->getWeekName($weekday);
foreach ($models as $model) {
// --- 星期匹配(与主循环一致) ---
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($currentDate, $model->type, $resource)) {
continue;
}
// --- 查询该资源在该日期下已有的号源,检查时间段是否重叠 ---
$existingPlans = DB::table('s_source_roster_detail')
->where('resources_id', $model->resources_id)
->where('date', $dateStr)
->where('is_del', 0)
->get();
foreach ($existingPlans as $existing) {
if ($this->isTimeOverlap(
$model->begin_time,
$model->end_time,
$existing->begin_time,
$existing->end_time
)) {
$conflicts[] = [
'date' => $dateStr,
'weekname' => $weekname,
'template_id' => $model->id,
'new_time' => $model->begin_time . '-' . $model->end_time,
'existing_time' => $existing->begin_time . '-' . $existing->end_time,
'existing_detail_id' => $existing->id,
'resources_id' => $model->resources_id,
];
}
}
}
$currentDate->modify('+1 day');
}
return $conflicts;
}
/**
* 判断两个时段是否重叠
* 不重叠返回 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 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 ?? 0,
'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("设备关联创建失败");
}
}
}
}