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.

1546 lines
64 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\Http\Controllers\API\Admin;
use App\Http\Controllers\Controller;
use App\Services\UserService;
use App\Services\PlanService;
use Illuminate\Http\Request;
use App\Lib\Rs;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
class PlanController extends Controller
{
private $userService;
private $planService;
public function __construct(UserService $userService, PlanService $planService)
{
$this->userService = $userService;
$this->planService = $planService;
}
/**
* 获取号源列表
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function list(Request $request)
{
$payload = $request->attributes->get('payload');
if (!$payload) {
return Rs::error('用户信息无效', 401);
}
$user = $this->userService->getUserInfoFromPayload($payload);
if (!$user) {
return Rs::error('用户不存在', 401);
}
$isAdmin = $this->userService->isAdmin($payload);
$userDeptId = $user->dept_id ?? null;
$resourceId = $request->input('resource_id', null);
$startDate = $request->input('start_date', null);
$endDate = $request->input('end_date', null);
// 必须传入资源ID
if (!$resourceId) {
return Rs::error('请选择资源');
}
// 必须传入日期范围
if (!$startDate || !$endDate) {
return Rs::error('请选择日期范围');
}
$query = DB::table('plan_batch')
->leftJoin('department_resource', 'plan_batch.resource_id', '=', 'department_resource.id')
->where('plan_batch.deleted', 0)
->where('plan_batch.resource_id', $resourceId)
->where('plan_batch.plan_date', '>=', $startDate)
->where('plan_batch.plan_date', '<=', $endDate)
->select(
'plan_batch.id',
'plan_batch.id as batch_id',
'plan_batch.resource_id',
'department_resource.name as resource_name',
'department_resource.slot_mode',
'plan_batch.plan_date',
'plan_batch.weekname',
'plan_batch.patient_type_mask',
'plan_batch.start_time',
'plan_batch.end_time',
'plan_batch.cutoff_time',
'plan_batch.total_quota',
'plan_batch.used_quota',
'plan_batch.slot_mode',
'plan_batch.status',
'plan_batch.enable_channel_quota'
);
// 权限控制:非管理员只能查看自己科室的号源
if (!$isAdmin) {
if (empty($userDeptId)) {
return Rs::error('请先绑定科室');
}
$query->where('plan_batch.department_id', $userDeptId);
}
$planBatches = $query->orderBy('plan_batch.plan_date', 'asc')
->orderBy('plan_batch.start_time', 'asc')
->get();
if ($planBatches->isEmpty()) {
return Rs::success(['list' => $planBatches]);
}
$allBatchIds = $planBatches->pluck('batch_id')->toArray();
// 批量预加载 locked_quota从 plan 表按 batch_id 聚合)
$lockedQuotaMap = DB::table('plan')
->whereIn('batch_id', $allBatchIds)
->where('deleted', 0)
->selectRaw('batch_id, SUM(locked_quota) as locked_quota')
->groupBy('batch_id')
->pluck('locked_quota', 'batch_id');
foreach ($planBatches as $batch) {
$batch->locked_quota = $lockedQuotaMap[$batch->batch_id] ?? 0;
}
// 批量预加载渠道配额
$channelQuotaMap = DB::table('plan_batch_channel_quota as pbcq')
->leftJoin('channel', 'pbcq.channel_id', '=', 'channel.id')
->whereIn('pbcq.batch_id', $allBatchIds)
->where(['pbcq.deleted' => 0, 'pbcq.status' => 1])
->select(
'pbcq.id',
'pbcq.batch_id',
'pbcq.channel_id',
'channel.name as channel_name',
'channel.short_name',
'pbcq.total_quota',
'pbcq.used_quota',
'pbcq.locked_quota',
'pbcq.allow_overuse_public_pool'
)
->get()
->groupBy('batch_id');
// 批量预加载 plan 记录
$planRecordsMap = DB::table('plan')
->whereIn('batch_id', $allBatchIds)
->where('deleted', 0)
->select('id', 'batch_id', 'start_time', 'end_time', 'total_quota', 'used_quota', 'locked_quota', 'status')
->orderBy('start_time', 'asc')
->get()
->groupBy('batch_id');
foreach ($planBatches as $batch) {
$batch->channel_quotas = $channelQuotaMap[$batch->batch_id] ?? collect([]);
$batchPlans = $planRecordsMap[$batch->batch_id] ?? collect([]);
if ($batch->slot_mode == 2) {
$batch->time_points = $batchPlans->map(function ($p) {
return (object)[
'id' => $p->id,
'start_time' => $p->start_time,
'end_time' => $p->end_time,
'total_quota' => $p->total_quota,
'used_quota' => $p->used_quota,
'locked_quota' => $p->locked_quota,
'status' => $p->status,
];
});
} else {
$batch->time_points = [];
$firstPlan = $batchPlans->first();
$batch->plan_id = $firstPlan ? $firstPlan->id : null;
}
}
$data = [
'list' => $planBatches
];
return Rs::success($data);
}
/**
* 删除号源批次
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function delete(Request $request)
{
$payload = $request->attributes->get('payload');
if (!$payload) {
return Rs::error('用户信息无效', 401);
}
$user = $this->userService->getUserInfoFromPayload($payload);
if (!$user) {
return Rs::error('用户不存在', 401);
}
$isAdmin = $this->userService->isAdmin($payload);
$userDeptId = $user->dept_id ?? null;
$data = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer'
]);
try {
DB::beginTransaction();
foreach ($data['ids'] as $id) {
// 检查是否存在
$batch = DB::table('plan_batch')
->where('id', $id)
->where('deleted', 0)
->first();
if (!$batch) {
DB::rollBack();
return Rs::error('号源批次不存在:' . $id);
}
// 权限控制:非管理员只能删除自己科室的号源
if (!$isAdmin) {
if (empty($userDeptId)) {
DB::rollBack();
return Rs::error('请先绑定科室');
}
if ($batch->department_id != $userDeptId) {
DB::rollBack();
return Rs::error('无权限删除该号源批次');
}
}
//如果被占用则不允许删除
if($batch->used_quota>0){
DB::rollBack();
return Rs::error($batch->plan_date.' '.$batch->start_time.'已有预约记录,不能删除');
}
// 删除plan记录
DB::table('plan')
->where('batch_id', $batch->id)
->update([
'deleted' => 1,
'updated_at' => now()
]);
// 删除plan_batch_channel_quota记录
DB::table('plan_batch_channel_quota')
->where('batch_id', $batch->id)
->update([
'deleted' => 1,
'updated_at' => now()
]);
// 软删除plan_batch记录
DB::table('plan_batch')
->where('id', $id)
->update([
'deleted' => 1,
'updated_at' => now()
]);
}
DB::commit();
return Rs::success(null, '成功删除 ' . count($data['ids']) . ' 个号源批次');
} catch (\Exception $e) {
DB::rollBack();
return Rs::error('删除号源批次失败:' . $e->getMessage());
}
}
/**
* 更新号源批次
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function update(Request $request)
{
$payload = $request->attributes->get('payload');
if (!$payload) {
return Rs::error('用户信息无效', 401);
}
$user = $this->userService->getUserInfoFromPayload($payload);
if (!$user) {
return Rs::error('用户不存在', 401);
}
$isAdmin = $this->userService->isAdmin($payload);
$userDeptId = $user->dept_id ?? null;
$data = $request->validate([
'id' => 'required|integer',
'start_time' => 'nullable|date_format:H:i:s',
'end_time' => 'nullable|date_format:H:i:s',
'status' => 'nullable|integer|in:0,1',
'total_quota' => 'nullable|integer|min:0',
'enable_channel_quota' => 'nullable|integer|in:0,1',
'channel_quotas' => 'nullable|array',
'channel_quotas.*.channel_id' => 'required|integer',
'channel_quotas.*.total_quota' => 'nullable|integer|min:0',
'channel_quotas.*.allow_overuse_public_pool' => 'nullable|integer|in:0,1',
'time_points' => 'nullable|array',
'time_points.*.id' => 'nullable|integer',
'time_points.*.time_range' => 'nullable|array',
'time_points.*.time_range.*' => 'nullable|date_format:H:i:s',
'time_points.*.status' => 'nullable|integer|in:0,1'
]);
try {
DB::beginTransaction();
// 检查是否存在
$batch = DB::table('plan_batch')
->where('id', $data['id'])
->where('deleted', 0)
->first();
if (!$batch) {
DB::rollBack();
return Rs::error('号源批次不存在');
}
// 权限控制:非管理员只能修改自己科室的号源
if (!$isAdmin) {
if (empty($userDeptId)) {
DB::rollBack();
return Rs::error('请先绑定科室');
}
if ($batch->department_id != $userDeptId) {
DB::rollBack();
return Rs::error('无权限修改该号源批次');
}
}
// 如果是时间点模式,验证时间段时长能否被总数整除
if ($batch->slot_mode == 2) {
$startTime = strtotime('2000-01-01 ' . (isset($data['start_time']) ? $data['start_time'] : $batch->start_time));
$endTime = strtotime('2000-01-01 ' . (isset($data['end_time']) ? $data['end_time'] : $batch->end_time));
$totalMinutes = ($endTime - $startTime) / 60;
$totalQuota = isset($data['total_quota']) ? $data['total_quota'] : $batch->total_quota;
if ($totalMinutes % $totalQuota !== 0) {
DB::rollBack();
return Rs::error('时间点模式下,时间段总时长必须能被号源数量整除。当前时长 ' . $totalMinutes . ' 分钟,号源数量 ' . $totalQuota . ',无法整除');
}
}
// 验证时间逻辑
if (!empty($data['start_time']) && !empty($data['end_time'])) {
$startTime = strtotime('2000-01-01 ' . $data['start_time']);
$endTime = strtotime('2000-01-01 ' . $data['end_time']);
if ($endTime <= $startTime) {
DB::rollBack();
return Rs::error('结束时间必须晚于开始时间');
}
}
// 验证总数与渠道配额之和是否相等
if (!empty($data['channel_quotas'])) {
$channelTotal = 0;
foreach ($data['channel_quotas'] as $quota) {
$channelTotal += $quota['total_quota'];
}
$planTotal = isset($data['total_quota']) ? $data['total_quota'] : $batch->total_quota;
if ($channelTotal != $planTotal) {
DB::rollBack();
return Rs::error('号源总数(' . $planTotal . ')与渠道配额之和(' . $channelTotal . ')不相等,请检查后重试');
}
}
// 校验:总配额不能小于已预约数
if (isset($data['total_quota']) && $data['total_quota'] < $batch->used_quota) {
DB::rollBack();
return Rs::error('总配额不能小于已预约数(当前已预约 ' . $batch->used_quota . '');
}
// 校验:启用渠道配额时,修改总数必须同时提交渠道配额
if (isset($data['total_quota']) && ($batch->enable_channel_quota ?? 0) == 1 && empty($data['channel_quotas'])) {
DB::rollBack();
return Rs::error('当前号源已启用渠道配额,修改总数时必须同时提交渠道配额分配');
}
// 记录乐观锁版本号
$batchVersion = $batch->version ?? 0;
// 更新plan_batch
$updateData = [];
if (isset($data['status'])) {
$updateData['status'] = $data['status'];
}
if (isset($data['total_quota'])) {
$updateData['total_quota'] = $data['total_quota'];
}
if (isset($data['enable_channel_quota'])) {
$updateData['enable_channel_quota'] = $data['enable_channel_quota'];
}
if (!empty($data['start_time'])) {
$updateData['start_time'] = $data['start_time'];
}
if (!empty($data['end_time'])) {
$updateData['end_time'] = $data['end_time'];
$updateData['cutoff_time'] = $data['end_time'];
}
if (!empty($updateData)) {
$updateData['updated_at'] = now();
$updateData['version'] = $batchVersion + 1;
$batchAffected = DB::table('plan_batch')
->where('id', $data['id'])
->where('version', $batchVersion)
->update($updateData);
if ($batchAffected === 0) {
DB::rollBack();
return Rs::error('数据已被其他操作修改,请刷新后重试');
}
}
// 时间段模式:同步 plan.total_quota
if ($batch->slot_mode == 1 && isset($data['total_quota'])) {
$planRecord = DB::table('plan')
->where('batch_id', $data['id'])
->where('deleted', 0)
->first();
if ($planRecord) {
$planAffected = DB::table('plan')
->where('id', $planRecord->id)
->where('version', $planRecord->version ?? 0)
->update([
'total_quota' => $data['total_quota'],
'version' => ($planRecord->version ?? 0) + 1,
'updated_at' => now()
]);
if ($planAffected === 0) {
DB::rollBack();
return Rs::error('号源数据已被其他操作修改,请刷新后重试');
}
}
}
// 时间点模式:如果调整了号源总量,需要重新生成时间点节点
if ($batch->slot_mode == 2 && isset($data['total_quota'])) {
$newTotalQuota = $data['total_quota'];
$oldTotalQuota = $batch->total_quota;
// 只有当号源总量发生变化时才处理
if ($newTotalQuota != $oldTotalQuota) {
// 获取时间范围(使用更新后的值,如果没有传则使用原值)
$startTime = $batch->start_time;
$endTime = $batch->end_time;
// 计算时间间隔(秒)
// 需要在同一天的基础上计算时间差
$baseDate = '2024-01-01';
$startTimeObj = \Carbon\Carbon::parse($baseDate . ' ' . $startTime);
$endTimeObj = \Carbon\Carbon::parse($baseDate . ' ' . $endTime);
$totalSeconds = $startTimeObj->diffInSeconds($endTimeObj);
$intervalSeconds = $totalSeconds / $newTotalQuota;
// 生成新的时间点列表
$newTimePoints = [];
for ($i = 0; $i < $newTotalQuota; $i++) {
$pointStartTime = $startTimeObj->copy()->addSeconds($i * $intervalSeconds);
$pointEndTime = $startTimeObj->copy()->addSeconds(($i + 1) * $intervalSeconds);
$newTimePoints[] = [
'start_time' => $pointStartTime->format('H:i:s'),
'end_time' => $pointEndTime->format('H:i:s')
];
}
Log::info($newTimePoints);
// 获取原有的 plan 节点(只查询未删除的)
$existingPlans = DB::table('plan')
->where('batch_id', $batch->id)
->where('deleted', 0)
->select('id', 'start_time', 'end_time', 'used_quota', 'status')
->get();
// 建立 start_time 到现有节点的映射(方便快速查找)
$existingPlansByStartTime = [];
foreach ($existingPlans as $plan) {
$existingPlansByStartTime[$plan->start_time] = $plan;
}
// 遍历新的时间点,找出需要保留和新增的
$keepStartTimes = []; // 记录需要保留的时间点
foreach ($newTimePoints as $newPoint) {
$startTimeStr = $newPoint['start_time'];
$keepStartTimes[] = $startTimeStr;
if (!isset($existingPlansByStartTime[$startTimeStr])) {
// 时间点不存在,新增
DB::table('plan')->insert([
'batch_id' => $batch->id,
'start_time' => $newPoint['start_time'],
'end_time' => $newPoint['end_time'],
'total_quota' => 1,
'used_quota' => 0,
'status' => 1,
'deleted' => 0,
'created_at' => now(),
'updated_at' => now()
]);
}
// 时间点已存在则什么都不做,保留原样
}
// 处理不在新节点列表中的原有节点
foreach ($existingPlans as $plan) {
if (!in_array($plan->start_time, $keepStartTimes)) {
// 不在新节点列表中
if ($plan->used_quota > 0) {
// 已占用,设置为不可用
DB::table('plan')
->where('id', $plan->id)
->update([
'status' => 0,
'updated_at' => now()
]);
} else {
// 未占用,软删除
DB::table('plan')
->where('id', $plan->id)
->update([
'deleted' => 1,
'updated_at' => now()
]);
}
}
}
}
}
// 更新渠道配额
if (!empty($data['channel_quotas'])) {
// 获取前端提交的 channel_id 列表
$submittedChannelIds = array_map(function($quota) {
return $quota['channel_id'];
}, $data['channel_quotas']);
// 停用数据库中存在但前端未提交的渠道配额(即关闭的渠道)
DB::table('plan_batch_channel_quota')
->where('batch_id', $batch->id)
->where('status', 1) // 只停用正常的
->whereNotIn('channel_id', $submittedChannelIds)
->update(['status' => 0, 'updated_at' => now()]);
// 更新或插入前端提交的渠道配额
foreach ($data['channel_quotas'] as $quota) {
$updateQuotaData = [
'total_quota' => $quota['total_quota'],
'status' => 1, // 恢复或设置为正常
'updated_at' => now()
];
// 如果提供了 allow_overuse_public_pool则更新该字段
if (isset($quota['allow_overuse_public_pool'])) {
$updateQuotaData['allow_overuse_public_pool'] = $quota['allow_overuse_public_pool'];
}
// 检查是否已存在该渠道配额(包括已停用的)
$existingQuota = DB::table('plan_batch_channel_quota')
->where('batch_id', $batch->id)
->where('channel_id', $quota['channel_id'])
->first();
if ($existingQuota) {
// 存在则更新(恢复停用的,或更新正常的,保留 used_quota
DB::table('plan_batch_channel_quota')
->where('id', $existingQuota->id)
->update($updateQuotaData);
} else {
// 不存在则插入(新增渠道)
DB::table('plan_batch_channel_quota')->insert([
'batch_id' => $batch->id,
'channel_id' => $quota['channel_id'],
'total_quota' => $quota['total_quota'],
'used_quota' => 0,
'allow_overuse_public_pool' => $quota['allow_overuse_public_pool'] ?? 0,
'status' => 1,
'created_at' => now(),
'updated_at' => now()
]);
}
}
}
// 更新时间点
// 如果时间点模式且调整了号源总量,已经由上面的逻辑重新生成了时间点,这里跳过处理
$skipTimePoints = ($batch->slot_mode == 2 && isset($data['total_quota']) && $data['total_quota'] != $batch->total_quota);
if (!$skipTimePoints && !empty($data['time_points'])) {
foreach ($data['time_points'] as $point) {
$updatePointData = [];
if (!empty($point['time_range']) && is_array($point['time_range'])) {
$updatePointData['start_time'] = $point['time_range'][0];
$updatePointData['end_time'] = $point['time_range'][1];
}
if (isset($point['status'])) {
$updatePointData['status'] = $point['status'];
}
if (!empty($updatePointData)) {
$updatePointData['updated_at'] = now();
DB::table('plan')
->where('id', $point['id'])
->update($updatePointData);
}
}
}
DB::commit();
return Rs::success(null, '更新成功');
} catch (\Exception $e) {
DB::rollBack();
return Rs::error('更新号源批次失败:' . $e->getMessage());
}
}
/**
* 获取号源预约记录
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function appointments(Request $request)
{
$payload = $request->attributes->get('payload');
if (!$payload) {
return Rs::error('用户信息无效', 401);
}
$user = $this->userService->getUserInfoFromPayload($payload);
if (!$user) {
return Rs::error('用户不存在', 401);
}
$isAdmin = $this->userService->isAdmin($payload);
$userDeptId = $user->dept_id ?? null;
$planBatchId = $request->input('plan_batch_id', null);
// 必须传入号源批次ID
if (!$planBatchId) {
return Rs::error('请提供号源批次ID');
}
// 获取plan批次信息
$planBatch = DB::table('plan_batch')
->where('id', $planBatchId)
->where('deleted', 0)
->first();
if (!$planBatch) {
return Rs::error('号源批次不存在');
}
// 权限控制:非管理员只能查看自己科室的号源
if (!$isAdmin) {
if (empty($userDeptId)) {
return Rs::error('请先绑定科室');
}
if ($planBatch->department_id != $userDeptId) {
return Rs::error('无权限查看该号源的预约记录');
}
}
// 获取所有plan的ID时间段和时间点模式都需要查询plan表
$planIds = [];
$plans = DB::table('plan')
->where('batch_id', $planBatchId)
->where('deleted', 0)
->select('id')
->get();
foreach ($plans as $plan) {
$planIds[] = $plan->id;
}
// 查询exam_appointment表
$query = DB::table('exam_appointment')
->whereIn('time_slot_id', $planIds)
->whereIn('exam_appointment.status', [1,2,3])
->leftJoin('channel', 'exam_appointment.channel_id', '=', 'channel.id')
->leftJoin('exam_application', 'exam_appointment.exam_application_id', '=', 'exam_application.id')
->select(
'exam_appointment.id',
'exam_appointment.time_slot_id',
'exam_appointment.patient_id',
'exam_appointment.patient_name',
'exam_appointment.appointment_time',
'exam_appointment.status',
'exam_appointment.channel_id',
'channel.name as channel_name',
'exam_application.examination_item_name',
'exam_appointment.created_at'
)
->orderBy('exam_appointment.created_at', 'desc');
$appointments = $query->get();
// 批量预加载 plan → batch → resource 映射
$uniqueTimeSlotIds = $appointments->pluck('time_slot_id')->unique()->toArray();
$planMap = DB::table('plan')
->whereIn('id', $uniqueTimeSlotIds)
->where('deleted', 0)
->pluck('batch_id', 'id');
$uniqueBatchIds = $planMap->unique()->values()->toArray();
$batchMap = DB::table('plan_batch')
->whereIn('id', $uniqueBatchIds)
->pluck('resource_id', 'id');
$uniqueResourceIds = $batchMap->unique()->values()->toArray();
$resourceMap = DB::table('department_resource')
->whereIn('id', $uniqueResourceIds)
->pluck('name', 'id');
foreach ($appointments as $appointment) {
$batchId = $planMap[$appointment->time_slot_id] ?? null;
if ($batchId) {
$resourceId = $batchMap[$batchId] ?? null;
$appointment->resource_name = $resourceId ? ($resourceMap[$resourceId] ?? '') : '';
} else {
$appointment->resource_name = '';
}
}
$data = [
'list' => $appointments
];
return Rs::success($data);
}
/**
* 获取可用号源(用于预约)
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function GetEnablePlan(Request $request)
{
$data = $request->validate([
'exam_application_ids' => 'required|array',
'exam_application_ids.*' => 'integer',
'channel_id' => 'required|integer|min:1',
'start_date' => 'nullable|date|after_or_equal:today'
]);
$examApplicationIds = $data['exam_application_ids'];
$channelId = $data['channel_id'];
$startDate = $data['start_date'] ?? date('Y-m-d');
try {
// 使用 PlanService 获取公共数据
$planData = $this->planService->queryPlanBatchesData($examApplicationIds, $channelId, $startDate, 15);
$planBatches = $planData['planBatches'];
$quotaByBatch = $planData['quotaByBatch'];
$totalUseSeats = $planData['totalUseSeats'];
// 8. 获取当前时间用于判断截止时间
$now = now();
$currentDate = $now->format('Y-m-d');
// 8.1 批量预加载所有 plan 记录,避免循环内 N+1 查询
$allBatchIds = $planBatches->pluck('batch_id')->toArray();
$allPlanRecordsMap = DB::table('plan')
->whereIn('batch_id', $allBatchIds)
->where('deleted', 0)
->select('id', 'batch_id', 'start_time', 'end_time', 'total_quota', 'used_quota', 'locked_quota', 'status')
->orderBy('start_time')
->get()
->groupBy('batch_id');
// 9. 构建响应数据
$resourcesData = [];
$batchesByResource = $planBatches->groupBy('resource_id');
foreach ($batchesByResource as $resourceId => $batches) {
$resource = $batches->first();
$slotMode = $resource->slot_mode;
$datesData = [];
$batchesByDate = $batches->groupBy('plan_date');
foreach ($batchesByDate->keys() as $date) {
if (!isset($batchesByDate[$date])) {
$datesData[$date] = ['date' => $date, 'periods' => []];
continue;
}
$periods = [];
foreach ($batchesByDate[$date] as $batch) {
// 判断是否超过截止时间
$isPastCutoff = ($batch->plan_date === $currentDate) &&
$now->gt(\Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $batch->plan_date . ' ' . $batch->cutoff_time));
if ($slotMode == 1) {
// ========== 时间段模式 ==========
$slots = [];
$quotas = $quotaByBatch[$batch->batch_id] ?? ['public' => null, 'channel' => null];
$publicQuota = $quotas['public'];
$channelQuota = $quotas['channel'];
if (($batch->enable_channel_quota ?? 0) == 0) {
if (!$publicQuota) continue;
$effectiveQuota = $publicQuota;
$allowOveruse = 0;
} else {
if (!$channelQuota) continue;
$effectiveQuota = $channelQuota;
$allowOveruse = $channelQuota->allow_overuse_public_pool ?? 0;
}
$totalQuota = $effectiveQuota->total_quota;
$usedQuota = $effectiveQuota->used_quota;
$lockedQuota = $effectiveQuota->locked_quota ?? 0;
if ($allowOveruse && $publicQuota && ($batch->enable_channel_quota ?? 0) != 0) {
$totalQuota += $publicQuota->total_quota;
$usedQuota += $publicQuota->used_quota;
$lockedQuota += $publicQuota->locked_quota ?? 0;
}
$remainCount = max(0, $totalQuota - $usedQuota - $lockedQuota);
// 判断剩余号源是否满足所有项目的占位数,且未超过截止时间
$enable = $remainCount >= $totalUseSeats && !$isPastCutoff;
// 从预加载的 plan 记录中获取时间段模式只有1条
$batchPlans = $allPlanRecordsMap[$batch->batch_id] ?? collect([]);
$planRecord = $batchPlans->first();
$slots[] = [
'id' => $planRecord ? $planRecord->id : $batch->batch_id,
'time' => substr($batch->start_time, 0, 5),
'used_count' => $totalQuota - $remainCount,
'total_count' => $totalQuota,
'enable' => $enable
];
$periods[] = [
'period_id' => $batch->batch_id,
'period_name' => '',
'time_range' => substr($batch->start_time, 0, 5) . '-' . substr($batch->end_time, 0, 5),
'slots' => $slots
];
} else {
// ========== 时间点模式(关键修正)==========
$quotas = $quotaByBatch[$batch->batch_id] ?? ['public' => null, 'channel' => null];
$publicQuota = $quotas['public'];
$channelQuota = $quotas['channel'];
$enableChannelQuota = ($batch->enable_channel_quota ?? 0) == 1;
// 如果开启了渠道配额但该渠道没有配额无记录或status=0或deleted=1则跳过该批次
if ($enableChannelQuota && !$channelQuota) {
continue;
}
// 计算整体配额状态
$overallTotal = 0;
$overallUsed = 0;
$hasQuota = false;
if (!$enableChannelQuota) {
if ($publicQuota) {
$overallTotal = $publicQuota->total_quota;
$overallUsed = $publicQuota->used_quota;
$hasQuota = true;
}
} else {
if ($channelQuota) {
$overallTotal = $channelQuota->total_quota;
$overallUsed = $channelQuota->used_quota;
$allowOveruse = $channelQuota->allow_overuse_public_pool ?? 0;
if ($allowOveruse && $publicQuota) {
$overallTotal += $publicQuota->total_quota;
$overallUsed += $publicQuota->used_quota;
}
$hasQuota = true;
}
}
// 从预加载的 plan 记录中获取时间点
$batchPlanRecords = $allPlanRecordsMap[$batch->batch_id] ?? collect([]);
$timePoints = $batchPlanRecords->filter(function ($p) {
return $p->status == 1;
});
if ($timePoints->isEmpty()) {
continue;
}
$slots = [];
foreach ($timePoints as $tp) {
// 判断时间点是否过期(超过开始时间就不能用了)
$isTimeExpired = false;
if ($batch->plan_date === $currentDate) {
// 当日:比较时间点的开始时间与当前时间
$tpStartTime = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $batch->plan_date . ' ' . $tp->start_time);
$isTimeExpired = $now->gt($tpStartTime);
} else if ($batch->plan_date < $currentDate) {
// 过去的日期:直接算过期
$isTimeExpired = true;
}
if (!$hasQuota || ($overallTotal - $overallUsed) <= 0) {
// 无配额 或 整体已满 → 强制显示为占满
$slots[] = [
'id' => $tp->id,
'time' => substr($tp->start_time, 0, 5),
'used_count' => $tp->used_quota,
'total_count' => $tp->total_quota,
'enable' => false
];
} else {
// 有剩余配额 → 显示真实数据,同时判断是否超过截止时间或时间点过期
$availableQuota = $tp->total_quota - $tp->used_quota - ($tp->locked_quota ?? 0);
$enable = $availableQuota > 0 && !$isPastCutoff && !$isTimeExpired;
$slots[] = [
'id' => $tp->id,
'time' => substr($tp->start_time, 0, 5),
'used_count' => $tp->used_quota,
'total_count' => $tp->total_quota,
'locked_count' => $tp->locked_quota ?? 0,
'enable' => $enable
];
}
}
$periods[] = [
'period_id' => $batch->batch_id,
'period_name' => '',
'time_range' => substr($batch->start_time, 0, 5) . '-' . substr($batch->end_time, 0, 5),
'slots' => $slots
];
}
}
$datesData[$date] = ['date' => $date, 'periods' => $periods];
}
$resourcesData[] = [
'room_id' => $resourceId,
'room_name' => $resource->resource_name,
'slot_mode' => $slotMode,
'slot_data' => array_values($datesData)
];
}
return Rs::success(['resources' => $resourcesData]);
} catch (\Exception $e) {
return Rs::error('获取可用号源失败:' . $e->getMessage());
}
}
/**
* 获取号源日历(按月查询)
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function GetPlanCalendar(Request $request)
{
$data = $request->validate([
'exam_application_ids' => 'required|array',
'exam_application_ids.*' => 'integer',
'channel_id' => 'required|integer|min:1',
'year' => 'required|integer|min:2020|max:2099',
'month' => 'required|integer|min:1|max:12'
]);
$examApplicationIds = $data['exam_application_ids'];
$channelId = $data['channel_id'];
$year = $data['year'];
$month = $data['month'];
try {
// 1. 生成日历显示的日期范围(覆盖整个日历网格,包括上月末尾和下月开头的日期)
$firstDayOfMonth = strtotime(sprintf('%04d-%02d-01', $year, $month));
$firstDayOfWeek = date('w', $firstDayOfMonth);
$startDate = date('Y-m-d', strtotime("-{$firstDayOfWeek} days", $firstDayOfMonth));
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
$lastDayOfWeek = date('w', strtotime(sprintf('%04d-%02d-%d', $year, $month, $daysInMonth)));
$totalDays = $firstDayOfWeek + $daysInMonth + (6 - $lastDayOfWeek);
$totalRows = ceil($totalDays / 7);
$today = date('Y-m-d');
$calendarDates = [];
$currentDate = $startDate;
for ($i = 0; $i < $totalRows * 7; $i++) {
if ($currentDate >= $today) {
$calendarDates[] = $currentDate;
}
$currentDate = date('Y-m-d', strtotime('+1 day', strtotime($currentDate)));
}
if (empty($calendarDates)) {
return Rs::success(['resources' => []]);
}
// 2. 使用 PlanService 获取公共数据
$planData = $this->planService->queryPlanBatchesData(
$examApplicationIds, $channelId, '', 0, $calendarDates
);
$planBatches = $planData['planBatches'];
$quotaByBatch = $planData['quotaByBatch'];
if ($planBatches->isEmpty()) {
return Rs::success(['resources' => []]);
}
// 3. 按资源分组,聚合为日历格式
$resourcesData = [];
$batchesByResource = $planBatches->groupBy('resource_id');
foreach ($batchesByResource as $resourceId => $batches) {
$resource = $batches->first();
$batchesByDate = $batches->groupBy('plan_date');
$datesData = [];
foreach ($calendarDates as $date) {
if (!isset($batchesByDate[$date])) {
$datesData[$date] = [
'date' => $date,
'total_count' => 0,
'available_count' => 0
];
continue;
}
$totalCount = 0;
$availableCount = 0;
foreach ($batchesByDate[$date] as $batch) {
$remaining = $this->planService->calculateRemainingQuota(
$batch,
$channelId,
$quotaByBatch[$batch->batch_id]['channel'] ?? null,
$quotaByBatch[$batch->batch_id]['public'] ?? null,
1
);
$quotas = $quotaByBatch[$batch->batch_id] ?? ['public' => null, 'channel' => null];
$publicQuota = $quotas['public'];
$channelQuota = $quotas['channel'];
$enableChannelQuota = ($batch->enable_channel_quota ?? 0) == 1;
if (!$enableChannelQuota) {
if (!$publicQuota) continue;
$totalQuota = $publicQuota->total_quota;
$usedQuota = $publicQuota->used_quota;
$lockedQuota = $publicQuota->locked_quota ?? 0;
} else {
if (!$channelQuota) continue;
$totalQuota = $channelQuota->total_quota;
$usedQuota = $channelQuota->used_quota;
$lockedQuota = $channelQuota->locked_quota ?? 0;
$allowOveruse = $channelQuota->allow_overuse_public_pool ?? 0;
if ($allowOveruse && $publicQuota) {
$totalQuota += $publicQuota->total_quota;
$usedQuota += $publicQuota->used_quota;
$lockedQuota += $publicQuota->locked_quota ?? 0;
}
}
$totalCount += $totalQuota;
$availableCount += max(0, $totalQuota - $usedQuota - $lockedQuota);
}
$datesData[$date] = [
'date' => $date,
'total_count' => $totalCount,
'available_count' => $availableCount
];
}
$resourcesData[] = [
'room_id' => $resourceId,
'room_name' => $resource->resource_name,
'slot_data' => array_values($datesData)
];
}
return Rs::success([
'resources' => $resourcesData
]);
} catch (\Exception $e) {
return Rs::error('获取号源日历失败:' . $e->getMessage());
}
}
/**
* 创建占位
*/
public function createLock(Request $request)
{
$data = $request->validate([
'plan_ids' => 'nullable|array',
'plan_ids.*' => 'nullable|integer',
'plan_id' => 'nullable|integer',
'channel_locks' => 'nullable|array',
'channel_locks.*.channel_id' => 'required|integer',
'channel_locks.*.locked_count' => 'required|integer|min:1',
'reason' => 'required|string|max:500'
]);
$payload = $request->attributes->get('payload');
if (!$payload) {
return Rs::error('用户信息无效', 401);
}
$user = $this->userService->getUserInfoFromPayload($payload);
if (!$user) {
return Rs::error('用户不存在', 401);
}
$operatorId = $user->user_id ?? null;
$operatorName = $user->name ?? '';
try {
DB::beginTransaction();
$planIds = [];
// 时间段模式单个plan_id + 渠道占位
if (isset($data['plan_id'])) {
$planId = $data['plan_id'];
// 查询plan和batch信息
$plan = DB::table('plan')->where('id', $planId)->where('deleted', 0)->first();
if (!$plan) {
throw new \Exception('号源不存在');
}
$batch = DB::table('plan_batch')->where('id', $plan->batch_id)->first();
if (!$batch) {
throw new \Exception('批次不存在');
}
// 查询resource获取slot_mode
$resource = DB::table('department_resource')->where('id', $batch->resource_id)->first();
$slotMode = $resource->slot_mode ?? 1;
if (($batch->enable_channel_quota ?? 0) == 0) {
// 未启用渠道配额:直接在 plan 表占位
$lockedCount = 1;
if (isset($data['channel_locks']) && !empty($data['channel_locks'])) {
$lockedCount = $data['channel_locks'][0]['locked_count'] ?? 1;
}
$availableQuota = $plan->total_quota - $plan->used_quota - ($plan->locked_quota ?? 0);
if ($availableQuota < $lockedCount) {
throw new \Exception('可用配额不足,剩余 ' . $availableQuota);
}
DB::table('plan')
->where('id', $planId)
->increment('locked_quota', $lockedCount);
// 同步更新公共池 channel_quota 的 locked_quota保持前端展示一致
DB::table('plan_batch_channel_quota')
->where('batch_id', $batch->id)
->where('channel_id', -1)
->where('deleted', 0)
->increment('locked_quota', $lockedCount);
DB::table('plan_locked_log')->insert([
'plan_id' => $planId,
'batch_id' => $batch->id,
'channel_id' => null,
'locked_quota' => $lockedCount,
'reason' => $data['reason'],
'operator_id' => $operatorId,
'operator_name' => $operatorName,
'action' => 1,
'slot_mode' => $slotMode,
'created_at' => now()
]);
} else {
// 启用了渠道配额:按渠道占位
if (!isset($data['channel_locks']) || empty($data['channel_locks'])) {
throw new \Exception('请提供渠道占位信息');
}
$channelLocks = $data['channel_locks'];
$totalLockedInChannels = 0;
foreach ($channelLocks as $channelLock) {
$channelId = $channelLock['channel_id'];
$lockedCount = $channelLock['locked_count'];
$channelQuota = DB::table('plan_batch_channel_quota')
->where('batch_id', $batch->id)
->where('channel_id', $channelId)
->where('deleted', 0)
->first();
if (!$channelQuota) {
throw new \Exception("渠道 {$channelId} 配额不存在");
}
$availableQuota = $channelQuota->total_quota - $channelQuota->used_quota - ($channelQuota->locked_quota ?? 0);
if ($availableQuota < $lockedCount) {
throw new \Exception("渠道 {$channelId} 可用配额不足,剩余 {$availableQuota}");
}
DB::table('plan_batch_channel_quota')
->where('id', $channelQuota->id)
->increment('locked_quota', $lockedCount);
$totalLockedInChannels += $lockedCount;
DB::table('plan_locked_log')->insert([
'plan_id' => $planId,
'batch_id' => $batch->id,
'channel_id' => $channelId,
'locked_quota' => $lockedCount,
'reason' => $data['reason'],
'operator_id' => $operatorId,
'operator_name' => $operatorName,
'action' => 1,
'slot_mode' => $slotMode,
'created_at' => now()
]);
}
// 同步更新 plan.locked_quota保持列表展示一致
if ($totalLockedInChannels > 0) {
DB::table('plan')
->where('id', $planId)
->increment('locked_quota', $totalLockedInChannels);
}
}
$planIds[] = $planId;
}
// 时间点模式多个plan_ids不按渠道
elseif (isset($data['plan_ids']) && !empty($data['plan_ids'])) {
$planIdList = $data['plan_ids'];
foreach ($planIdList as $planId) {
// 查询plan和batch信息
$plan = DB::table('plan')->where('id', $planId)->where('deleted', 0)->first();
if (!$plan) {
throw new \Exception("号源 {$planId} 不存在");
}
$batch = DB::table('plan_batch')->where('id', $plan->batch_id)->first();
if (!$batch) {
throw new \Exception("批次 {$plan->batch_id} 不存在");
}
// 查询resource获取slot_mode
$resource = DB::table('department_resource')->where('id', $batch->resource_id)->first();
$slotMode = $resource->slot_mode ?? 1;
// 检查可用配额
$availableQuota = $plan->total_quota - $plan->used_quota - ($plan->locked_quota ?? 0);
if ($availableQuota < 1) {
throw new \Exception("时间点 {$plan->start_time} 可用配额不足");
}
// 增加占位数量时间点模式始终为1
DB::table('plan')
->where('id', $planId)
->increment('locked_quota', 1);
// 同步更新公共池 channel_quota 的 locked_quota保持列表展示一致
DB::table('plan_batch_channel_quota')
->where('batch_id', $batch->id)
->where('channel_id', -1)
->where('deleted', 0)
->increment('locked_quota', 1);
// 记录日志时间点模式不记录channel_id
$timePointInfo = $plan->start_time ? substr($plan->start_time, 0, 5) : '';
$logReason = $timePointInfo ? "{$timePointInfo} {$data['reason']}" : $data['reason'];
DB::table('plan_locked_log')->insert([
'plan_id' => $planId,
'batch_id' => $batch->id,
'channel_id' => null,
'locked_quota' => 1,
'reason' => $logReason,
'operator_id' => $operatorId,
'operator_name' => $operatorName,
'action' => 1,
'slot_mode' => $slotMode,
'created_at' => now()
]);
$planIds[] = $planId;
}
} else {
throw new \Exception('请提供plan_id或plan_ids');
}
DB::commit();
return Rs::success(null, '占位成功');
} catch (\Exception $e) {
DB::rollBack();
return Rs::error('占位失败:' . $e->getMessage());
}
}
/**
* 释放占位
*/
public function releaseLock(Request $request)
{
$data = $request->validate([
'slot_mode' => 'required|integer|in:1,2',
'plan_ids' => 'nullable|array',
'plan_ids.*' => 'integer',
'channel_id' => 'nullable|integer',
'release_count' => 'nullable|integer|min:1'
]);
$payload = $request->attributes->get('payload');
if (!$payload) {
return Rs::error('用户信息无效', 401);
}
$user = $this->userService->getUserInfoFromPayload($payload);
if (!$user) {
return Rs::error('用户不存在', 401);
}
$operatorId = $user->user_id ?? null;
$operatorName = $user->name ?? '';
try {
DB::beginTransaction();
$slotMode = $data['slot_mode'];
// 时间点模式slot_mode=2使用plan_ids释放
if ($slotMode == 2) {
if (!isset($data['plan_ids']) || empty($data['plan_ids'])) {
throw new \Exception('时间点模式请提供plan_ids');
}
$planIdList = $data['plan_ids'];
foreach ($planIdList as $planId) {
// 查询plan信息
$plan = DB::table('plan')->where('id', $planId)->where('deleted', 0)->first();
if (!$plan) {
throw new \Exception("号源 {$planId} 不存在");
}
$lockedQuota = $plan->locked_quota ?? 0;
if ($lockedQuota <= 0) {
continue; // 该时间点无占位,跳过
}
// 获取batch和resource信息
$batch = DB::table('plan_batch')->where('id', $plan->batch_id)->first();
if (!$batch) {
throw new \Exception("批次 {$plan->batch_id} 不存在");
}
$resource = DB::table('department_resource')->where('id', $batch->resource_id)->first();
// 减少占位数量(时间点模式始终全部释放)
DB::table('plan')
->where('id', $planId)
->update(['locked_quota' => 0]);
// 同步释放公共池 channel_quota 的 locked_quota保持配额展示一致
DB::table('plan_batch_channel_quota')
->where('batch_id', $batch->id)
->where('channel_id', -1)
->where('deleted', 0)
->decrement('locked_quota', $lockedQuota);
// 记录日志时间点模式不记录channel_id
$timePointInfo = $plan->start_time ? substr($plan->start_time, 0, 5) : '';
$logReason = $timePointInfo ? "{$timePointInfo} 释放占位" : '释放占位';
DB::table('plan_locked_log')->insert([
'plan_id' => $planId,
'batch_id' => $batch->id,
'channel_id' => null,
'locked_quota' => $lockedQuota,
'reason' => $logReason,
'operator_id' => $operatorId,
'operator_name' => $operatorName,
'action' => 2,
'slot_mode' => $slotMode,
'created_at' => now()
]);
}
}
// 时间段模式slot_mode=1
elseif ($slotMode == 1) {
if (!isset($data['plan_ids']) || empty($data['plan_ids'])) {
throw new \Exception('时间段模式请提供plan_ids');
}
$planId = $data['plan_ids'][0];
$plan = DB::table('plan')->where('id', $planId)->where('deleted', 0)->first();
if (!$plan) {
throw new \Exception('号源不存在');
}
$batch = DB::table('plan_batch')->where('id', $plan->batch_id)->first();
if (!$batch) {
throw new \Exception('批次不存在');
}
if (($batch->enable_channel_quota ?? 0) == 0) {
// 未启用渠道配额:直接在 plan 表释放
$releaseCount = $data['release_count'] ?? 0;
$lockedQuota = $plan->locked_quota ?? 0;
if (!$releaseCount) {
$releaseCount = $lockedQuota;
} elseif ($releaseCount > $lockedQuota) {
throw new \Exception('释放数量不能超过当前占位数量');
}
if ($releaseCount <= 0) {
throw new \Exception('当前无占位可释放');
}
DB::table('plan')
->where('id', $planId)
->decrement('locked_quota', $releaseCount);
// 同步释放公共池 channel_quota 的 locked_quota
DB::table('plan_batch_channel_quota')
->where('batch_id', $batch->id)
->where('channel_id', -1)
->where('deleted', 0)
->decrement('locked_quota', $releaseCount);
DB::table('plan_locked_log')->insert([
'plan_id' => $planId,
'batch_id' => $batch->id,
'channel_id' => null,
'locked_quota' => $releaseCount,
'reason' => '释放占位',
'operator_id' => $operatorId,
'operator_name' => $operatorName,
'action' => 2,
'slot_mode' => $slotMode,
'created_at' => now()
]);
} else {
// 启用了渠道配额:按渠道释放
if (!isset($data['channel_id'])) {
throw new \Exception('时间段模式请提供channel_id');
}
$channelId = $data['channel_id'];
$releaseCount = $data['release_count'] ?? 0;
$channelQuota = DB::table('plan_batch_channel_quota')
->where('batch_id', $batch->id)
->where('channel_id', $channelId)
->where('deleted', 0)
->first();
if (!$channelQuota) {
throw new \Exception("渠道 {$channelId} 配额不存在");
}
if (!$releaseCount) {
$releaseCount = $channelQuota->locked_quota ?? 0;
} else {
if ($releaseCount > ($channelQuota->locked_quota ?? 0)) {
throw new \Exception('释放数量不能超过当前占位数量');
}
}
if ($releaseCount <= 0) {
throw new \Exception('该渠道当前无占位可释放');
}
DB::table('plan_batch_channel_quota')
->where('id', $channelQuota->id)
->decrement('locked_quota', $releaseCount);
// 同步释放 plan.locked_quota保持列表展示一致
DB::table('plan')
->where('id', $planId)
->decrement('locked_quota', $releaseCount);
DB::table('plan_locked_log')->insert([
'plan_id' => $planId,
'batch_id' => $batch->id,
'channel_id' => $channelId,
'locked_quota' => $releaseCount,
'reason' => '释放占位',
'operator_id' => $operatorId,
'operator_name' => $operatorName,
'action' => 2,
'slot_mode' => $slotMode,
'created_at' => now()
]);
}
}
DB::commit();
return Rs::success(null, '释放成功');
} catch (\Exception $e) {
DB::rollBack();
return Rs::error('释放失败:' . $e->getMessage());
}
}
/**
* 查询占位记录
*/
public function getLockHistory(Request $request)
{
$data = $request->validate([
'plan_id' => 'nullable|integer',
'batch_id' => 'nullable|integer'
]);
try {
$query = DB::table('plan_locked_log as log')
->orderBy('log.created_at', 'desc');
if (isset($data['plan_id'])) {
$query->where('log.plan_id', $data['plan_id']);
}
if (isset($data['batch_id'])) {
$query->where('log.batch_id', $data['batch_id']);
}
$logs = $query->limit(100)->get();
return Rs::success(['list' => $logs]);
} catch (\Exception $e) {
return Rs::error('查询失败:' . $e->getMessage());
}
}
}