package com.designhub.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.designhub.entity.*; import com.designhub.mapper.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; /** * 任务排队解锁服务 * 实现任务排队机制,确保先发布的任务优先被处理 */ @Service @Slf4j public class TaskUnlockService { @Autowired private TaskUnlockStatusMapper unlockStatusMapper; @Autowired private TaskItemMapper taskItemMapper; @Autowired private TaskMapper taskMapper; @Autowired private DepartmentMapper departmentMapper; @Autowired private SystemConfigService systemConfigService; @Autowired private DemandTypeMapper demandTypeMapper; @Autowired private DepartmentService departmentService; @Autowired private TaskStatusNotificationService taskStatusNotificationService; private static final int DEFAULT_DAILY_UNLOCK_MIN = 5; private static final int DEFAULT_BATCH_SIZE = 5; /** * 获取 TaskUnlockStatusMapper(供其他 Service 使用) */ public TaskUnlockStatusMapper getUnlockStatusMapper() { return unlockStatusMapper; } // ==================== 配置相关 ==================== /** * 检查排队解锁功能是否启用 */ public boolean isUnlockEnabled() { String enabled = systemConfigService.getConfigValue("task_unlock_enabled", "false"); return "true".equalsIgnoreCase(enabled); } /** * 获取每日最少解锁数 */ public int getDailyUnlockMin() { String value = systemConfigService.getConfigValue("task_unlock_daily_min", String.valueOf(DEFAULT_DAILY_UNLOCK_MIN)); try { return Integer.parseInt(value); } catch (NumberFormatException e) { return DEFAULT_DAILY_UNLOCK_MIN; } } /** * 获取解锁批次大小 */ public int getBatchSize() { String value = systemConfigService.getConfigValue("task_unlock_batch_size", String.valueOf(DEFAULT_BATCH_SIZE)); try { return Integer.parseInt(value); } catch (NumberFormatException e) { return DEFAULT_BATCH_SIZE; } } // ==================== 定时任务 ==================== /** * 每日解锁任务(每天凌晨1点执行) */ @Scheduled(cron = "0 0 1 * * ?") public void dailyUnlockTask() { if (!isUnlockEnabled()) { log.info("任务排队解锁功能已关闭,跳过每日解锁任务"); return; } log.info("========== 开始执行每日任务解锁 =========="); // 获取所有接单部门 List receivingDepts = departmentMapper.selectList( new LambdaQueryWrapper() .eq(Department::getDeptType, "receiving") .eq(Department::getStatus, 1) ); int totalUnlocked = 0; int dailyMin = getDailyUnlockMin(); for (Department dept : receivingDepts) { try { int count = unlockTasksForDept(dept.getId(), dailyMin); totalUnlocked += count; if (count > 0) { log.info("部门 {} ({}) 解锁了 {} 个任务", dept.getDeptName(), dept.getId(), count); } } catch (Exception e) { log.error("部门 {} 解锁任务失败: {}", dept.getId(), e.getMessage(), e); } } log.info("========== 每日任务解锁完成,共解锁 {} 个任务 ==========", totalUnlocked); } // ==================== 核心解锁逻辑 ==================== /** * 为指定部门解锁任务 * @param deptId 部门ID * @param minCount 最少解锁数量 * @return 实际解锁数量 */ @Transactional(rollbackFor = Exception.class) public int unlockTasksForDept(Long deptId, int minCount) { // 1. 检查当前已解锁但未接单的任务数 int currentUnlockedPending = unlockStatusMapper.countUnlockedPendingTasks(deptId); // 如果已有足够的可接任务,不需要解锁更多 if (currentUnlockedPending >= minCount) { log.debug("部门 {} 已有 {} 个可接任务,无需解锁更多", deptId, currentUnlockedPending); return 0; } // 2. 计算需要解锁的数量 int needToUnlock = minCount - currentUnlockedPending; // 3. 获取待解锁的任务ID列表(按发布日期排序) List tasksToUnlock = unlockStatusMapper.selectTasksToUnlock(deptId, needToUnlock); if (tasksToUnlock.isEmpty()) { log.debug("部门 {} 没有待解锁的任务", deptId); return 0; } // 4. 批量解锁 int unlocked = unlockStatusMapper.batchUnlock(deptId, tasksToUnlock); log.info("部门 {} 成功解锁 {} 个任务", deptId, unlocked); // 5. 发送解锁通知给需求人 try { notifyUnlockedTasks(tasksToUnlock); } catch (Exception e) { log.error("发送解锁通知失败: {}", e.getMessage()); // 不影响解锁流程 } return unlocked; } // ==================== 应急解锁(管理员/机器人调用) ==================== /** * 应急解锁:将指定任务(主任务编号/子任务编号)从排队中直接解锁到大厅 *

* - 如果传入的是子任务编号(itemNo),仅解锁该子任务(仅限 pending) * - 如果传入的是主任务编号(taskNo),解锁该主任务下所有 pending 子任务(仅解锁排队中的) *

*/ @Transactional(rollbackFor = Exception.class) public Map emergencyUnlockToHall(String taskNoOrItemNo) { String key = taskNoOrItemNo == null ? "" : taskNoOrItemNo.trim(); if (key.isEmpty()) { throw new IllegalArgumentException("任务编号不能为空"); } TaskItem item = taskItemMapper.selectOne( new LambdaQueryWrapper() .eq(TaskItem::getItemNo, key) .last("LIMIT 1") ); if (item != null) { return emergencyUnlockTaskItemToHall(item); } Task task = taskMapper.selectOne( new LambdaQueryWrapper() .eq(Task::getTaskNo, key) .last("LIMIT 1") ); if (task != null) { return emergencyUnlockTaskToHall(task); } throw new IllegalArgumentException("任务不存在,请检查编号是否正确"); } /** * 应急解锁:按主任务编号解锁 */ @Transactional(rollbackFor = Exception.class) public Map emergencyUnlockToHallByTaskNo(String taskNo) { String key = taskNo == null ? "" : taskNo.trim(); if (key.isEmpty()) { throw new IllegalArgumentException("任务编号不能为空"); } Task task = taskMapper.selectOne( new LambdaQueryWrapper() .eq(Task::getTaskNo, key) .last("LIMIT 1") ); if (task == null) { throw new IllegalArgumentException("任务不存在,请检查任务编号是否正确"); } return emergencyUnlockTaskToHall(task); } private Map emergencyUnlockTaskItemToHall(TaskItem item) { if (item.getTaskId() == null) { throw new IllegalArgumentException("子任务缺少主任务ID,无法解锁"); } Task task = taskMapper.selectById(item.getTaskId()); if (task == null) { throw new IllegalArgumentException("主任务不存在,无法解锁"); } if ("assigned".equals(task.getReleaseType())) { Map result = new HashMap<>(); result.put("type", "item"); result.put("taskNo", task.getTaskNo()); result.put("itemNo", item.getItemNo()); result.put("taskItemId", item.getId()); result.put("unlockedItemCount", 0); result.put("message", "指定任务不受排队限制,无需解锁"); return result; } if (!"pending".equals(item.getStatus())) { throw new IllegalArgumentException("子任务状态不是待接单,无法解锁到大厅"); } List queuedRows = unlockStatusMapper.selectList( new LambdaQueryWrapper() .eq(TaskUnlockStatus::getTaskItemId, item.getId()) .eq(TaskUnlockStatus::getIsUnlocked, 0) ); if (queuedRows == null || queuedRows.isEmpty()) { Map result = new HashMap<>(); result.put("type", "item"); result.put("taskNo", task.getTaskNo()); result.put("itemNo", item.getItemNo()); result.put("taskItemId", item.getId()); result.put("unlockedItemCount", 0); result.put("message", "该子任务不在排队中或已解锁"); return result; } LocalDateTime now = LocalDateTime.now(); int affectedRows = unlockStatusMapper.update( null, new LambdaUpdateWrapper() .eq(TaskUnlockStatus::getTaskItemId, item.getId()) .eq(TaskUnlockStatus::getIsUnlocked, 0) .set(TaskUnlockStatus::getIsUnlocked, 1) .set(TaskUnlockStatus::getUnlockTime, now) .set(TaskUnlockStatus::getUpdateTime, now) ); try { notifyUnlockedTasks(Collections.singletonList(item.getId())); } catch (Exception e) { log.error("发送应急解锁通知失败: itemId={}, error={}", item.getId(), e.getMessage()); } Map result = new HashMap<>(); result.put("type", "item"); result.put("taskNo", task.getTaskNo()); result.put("itemNo", item.getItemNo()); result.put("taskItemId", item.getId()); result.put("deptId", queuedRows.get(0).getDeptId()); result.put("unlockedItemCount", affectedRows > 0 ? 1 : 0); result.put("affectedRows", affectedRows); result.put("message", affectedRows > 0 ? "已解锁到大厅" : "解锁失败或已被解锁"); return result; } private Map emergencyUnlockTaskToHall(Task task) { if ("assigned".equals(task.getReleaseType())) { Map result = new HashMap<>(); result.put("type", "task"); result.put("taskNo", task.getTaskNo()); result.put("taskId", task.getId()); result.put("unlockedItemCount", 0); result.put("message", "指定任务不受排队限制,无需解锁"); return result; } List pendingItems = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getTaskId, task.getId()) .eq(TaskItem::getStatus, "pending") ); Map result = new HashMap<>(); result.put("type", "task"); result.put("taskNo", task.getTaskNo()); result.put("taskId", task.getId()); result.put("pendingItemCount", pendingItems == null ? 0 : pendingItems.size()); if (pendingItems == null || pendingItems.isEmpty()) { result.put("unlockedItemCount", 0); result.put("message", "主任务下没有待接单子任务,无需解锁"); return result; } List itemIds = pendingItems.stream() .map(TaskItem::getId) .filter(Objects::nonNull) .toList(); if (itemIds.isEmpty()) { result.put("unlockedItemCount", 0); result.put("message", "子任务ID为空,无法解锁"); return result; } List queuedRows = unlockStatusMapper.selectList( new LambdaQueryWrapper() .in(TaskUnlockStatus::getTaskItemId, itemIds) .eq(TaskUnlockStatus::getIsUnlocked, 0) ); if (queuedRows == null || queuedRows.isEmpty()) { result.put("unlockedItemCount", 0); result.put("message", "该主任务下没有排队中的待接单子任务"); return result; } List queuedItemIds = queuedRows.stream() .map(TaskUnlockStatus::getTaskItemId) .filter(Objects::nonNull) .distinct() .toList(); LocalDateTime now = LocalDateTime.now(); int affectedRows = unlockStatusMapper.update( null, new LambdaUpdateWrapper() .in(TaskUnlockStatus::getTaskItemId, queuedItemIds) .eq(TaskUnlockStatus::getIsUnlocked, 0) .set(TaskUnlockStatus::getIsUnlocked, 1) .set(TaskUnlockStatus::getUnlockTime, now) .set(TaskUnlockStatus::getUpdateTime, now) ); try { notifyUnlockedTasks(queuedItemIds); } catch (Exception e) { log.error("发送应急解锁通知失败: taskNo={}, error={}", task.getTaskNo(), e.getMessage()); } result.put("unlockedItemCount", queuedItemIds.size()); result.put("affectedRows", affectedRows); result.put("message", queuedItemIds.isEmpty() ? "无可解锁子任务" : "已解锁到大厅"); return result; } /** * 任务被抢单后,检查并补充解锁 */ @Transactional(rollbackFor = Exception.class) public void onTaskGrabbed(Long deptId) { if (!isUnlockEnabled()) { return; } // 检查剩余可接任务数 int remainingUnlocked = unlockStatusMapper.countUnlockedPendingTasks(deptId); int batchSize = getBatchSize(); // 如果少于批次大小,补充解锁 if (remainingUnlocked < batchSize) { int unlocked = unlockTasksForDept(deptId, batchSize); if (unlocked > 0) { log.info("任务被接单后,为部门 {} 补充解锁 {} 个任务", deptId, unlocked); } } } // ==================== 任务入队逻辑 ==================== /** * 任务审核通过后,加入排队池 * @param task 主任务 * @param items 子任务列表 */ @Transactional(rollbackFor = Exception.class) public void onTaskApproved(Task task, List items) { if (!isUnlockEnabled()) { log.debug("排队解锁功能未启用,跳过入队处理"); return; } // 指定任务不进入排队池 if ("assigned".equals(task.getReleaseType())) { log.info("指定任务 {} 不进入排队池,直接可见", task.getTaskNo()); return; } LocalDate publishDate = task.getCreateTime() != null ? task.getCreateTime().toLocalDate() : LocalDate.now(); int addedRows = 0; Set affectedDeptIds = new LinkedHashSet<>(); for (TaskItem item : items) { if (item.getId() == null) { continue; } List deptIds = resolveTargetDeptIds(item, task); if (deptIds.isEmpty()) { log.warn("子任务 {} 无法确定目标部门,跳过入队", item.getItemNo()); continue; } for (Long deptId : deptIds) { if (deptId == null) { continue; } int inserted = unlockStatusMapper.insertIfNotExists(item.getId(), deptId, publishDate, 0); addedRows += inserted; if (inserted > 0) { affectedDeptIds.add(deptId); } } } if (addedRows > 0) { log.info("任务 {} 已写入 {} 条排队记录", task.getTaskNo(), addedRows); int minUnlockedCount = getDailyUnlockMin(); for (Long deptId : affectedDeptIds) { try { int unlocked = unlockTasksForDept(deptId, minUnlockedCount); if (unlocked > 0) { log.info("任务 {} 入队后,为部门 {} 立即补充解锁 {} 个任务", task.getTaskNo(), deptId, unlocked); } } catch (Exception e) { log.error("任务 {} 入队后补充部门 {} 可接任务失败: {}", task.getTaskNo(), deptId, e.getMessage(), e); } } } } /** * 流水线解锁时,自动标记为已解锁(不走排队) */ @Transactional(rollbackFor = Exception.class) public void onPipelineUnlock(TaskItem item, Long deptId) { if (!isUnlockEnabled()) { return; } // 检查是否已有记录 TaskUnlockStatus existing = unlockStatusMapper.selectOne( new LambdaQueryWrapper() .eq(TaskUnlockStatus::getTaskItemId, item.getId()) .eq(TaskUnlockStatus::getDeptId, deptId) ); if (existing != null) { // 更新为已解锁 existing.setIsUnlocked(1); existing.setUnlockTime(LocalDateTime.now()); unlockStatusMapper.updateById(existing); } else { // 创建已解锁记录 TaskUnlockStatus unlockStatus = new TaskUnlockStatus(); unlockStatus.setTaskItemId(item.getId()); unlockStatus.setDeptId(deptId); unlockStatus.setPublishDate(LocalDate.now()); unlockStatus.setIsUnlocked(1); // 直接解锁 unlockStatus.setUnlockTime(LocalDateTime.now()); unlockStatus.setCreateTime(LocalDateTime.now()); unlockStatusMapper.insert(unlockStatus); } log.info("流水线子任务 {} 已自动标记为已解锁", item.getItemNo()); } // ==================== 查询方法 ==================== /** * 获取已解锁的任务ID列表 */ public List getUnlockedTaskItemIds(Long deptId) { return unlockStatusMapper.selectUnlockedTaskItemIds(deptId); } /** * 获取排队中的任务ID列表 */ public List getQueuedTaskItemIds(Long deptId) { return unlockStatusMapper.selectQueuedTaskItemIds(deptId); } /** * 填充任务的排队状态信息 */ public void fillUnlockStatus(TaskItem item, Long deptId, String releaseType) { // 指定任务不显示排队状态 if ("assigned".equals(releaseType)) { item.setUnlockStatus(null); item.setCanGrab(true); return; } // 非 pending 状态不需要排队信息 if (!"pending".equals(item.getStatus())) { item.setUnlockStatus(null); item.setCanGrab(false); return; } // 功能未启用时,所有任务都可抢 if (!isUnlockEnabled()) { item.setUnlockStatus("unlocked"); item.setCanGrab(true); return; } // 查询解锁状态 Integer isUnlocked = unlockStatusMapper.isUnlocked(item.getId(), deptId); if (isUnlocked == null) { // 没有记录:补全排队记录,避免“新任务绕过排队” ensureUnlockStatusExists(taskItemId(item), deptId); tryTopUpDeptQueue(deptId); isUnlocked = unlockStatusMapper.isUnlocked(item.getId(), deptId); // 兜底:依然查不到则按排队中处理 if (isUnlocked == null) { item.setUnlockStatus("queuing"); item.setCanGrab(false); return; } } if (isUnlocked == 1) { // 已解锁 = 抢单中 item.setUnlockStatus("unlocked"); item.setCanGrab(true); } else { // 未解锁 = 排队中 item.setUnlockStatus("queuing"); item.setCanGrab(false); // 计算排队位置 int position = unlockStatusMapper.calculateQueuePosition(item.getId(), deptId); item.setQueuePosition(position); // 计算总排队数 int totalQueue = unlockStatusMapper.countQueuedTasks(deptId); item.setTotalQueueSize(totalQueue); // 预估解锁日期(每天解锁 dailyMin 个) int dailyMin = getDailyUnlockMin(); int daysToUnlock = (int) Math.ceil((double) position / dailyMin); LocalDate unlockDate = LocalDate.now().plusDays(daysToUnlock); item.setEstimatedUnlockDate(unlockDate.format(DateTimeFormatter.ofPattern("MM月dd日"))); // 计算进度百分比 int progress = totalQueue > 0 ? (int) Math.round((totalQueue - position + 1.0) / totalQueue * 100) : 0; item.setQueueProgress(Math.min(progress, 99)); // 最高99%,100%表示已解锁 } } /** * 批量填充任务的排队状态 */ public void batchFillUnlockStatus(List items, Long deptId) { if (items == null || items.isEmpty()) { return; } for (TaskItem item : items) { String releaseType = item.getReleaseType(); fillUnlockStatus(item, deptId, releaseType); } } /** * 检查任务是否可以抢单 */ public boolean canGrab(Long taskItemId, Long deptId, String releaseType) { // 指定任务始终可抢(由其他逻辑控制权限) if ("assigned".equals(releaseType)) { return true; } // 功能未启用时,都可抢 if (!isUnlockEnabled()) { return true; } // 检查解锁状态 Integer isUnlocked = unlockStatusMapper.isUnlocked(taskItemId, deptId); if (isUnlocked == null) { // 没有记录:补全排队记录,并尝试按部门阈值补位 ensureUnlockStatusExists(taskItemId, deptId); tryTopUpDeptQueue(deptId); isUnlocked = unlockStatusMapper.isUnlocked(taskItemId, deptId); return isUnlocked != null && isUnlocked == 1; } return isUnlocked == 1; } // ==================== 辅助方法 ==================== /** * 补全排队记录(仅补全当前部门维度) *

用于修复历史数据缺失导致的绕过排队与不可解锁问题

*/ private void ensureUnlockStatusExists(Long taskItemId, Long deptId) { if (taskItemId == null || deptId == null) { return; } LocalDate publishDate = LocalDate.now(); try { TaskItem item = taskItemMapper.selectById(taskItemId); if (item != null && item.getCreateTime() != null) { publishDate = item.getCreateTime().toLocalDate(); } if (item != null && item.getTaskId() != null) { Task task = taskMapper.selectById(item.getTaskId()); if (task != null && task.getCreateTime() != null) { publishDate = task.getCreateTime().toLocalDate(); } } } catch (Exception e) { log.warn("补全排队记录时读取任务信息失败: taskItemId={}, deptId={}, error={}", taskItemId, deptId, e.getMessage()); } try { unlockStatusMapper.insertIfNotExists(taskItemId, deptId, publishDate, 0); } catch (Exception e) { log.warn("补全排队记录失败: taskItemId={}, deptId={}, error={}", taskItemId, deptId, e.getMessage()); } } private void tryTopUpDeptQueue(Long deptId) { if (deptId == null || !isUnlockEnabled()) { return; } try { unlockTasksForDept(deptId, getDailyUnlockMin()); } catch (Exception e) { log.warn("补齐部门可接任务失败: deptId={}, error={}", deptId, e.getMessage()); } } private Long taskItemId(TaskItem item) { return item == null ? null : item.getId(); } /** * 获取任务的目标接单部门ID列表(支持多部门 & 三级分类继承) */ private List resolveTargetDeptIds(TaskItem item, Task task) { if (item == null) { return Collections.emptyList(); } // 优先使用子任务的接单部门(明确指定) if (item.getReceiverDeptId() != null) { return Collections.singletonList(item.getReceiverDeptId()); } // 优先使用子任务类型配置(流水线任务主任务 demandTypeId 常为空) Long taskTypeId = item.getTaskTypeId(); if (taskTypeId == null && task != null) { taskTypeId = task.getDemandTypeId() != null ? task.getDemandTypeId() : task.getTaskTypeId(); } String allowedDeptIds = null; if (taskTypeId != null) { DemandType demandType = demandTypeMapper.selectById(taskTypeId); if (demandType != null) { allowedDeptIds = demandType.getAllowedDeptIds(); if ((allowedDeptIds == null || allowedDeptIds.trim().isEmpty()) && demandType.getParentId() != null) { DemandType parentType = demandTypeMapper.selectById(demandType.getParentId()); if (parentType != null) { allowedDeptIds = parentType.getAllowedDeptIds(); } } } } List parsedDeptIds = parseDeptIds(allowedDeptIds); if (!parsedDeptIds.isEmpty()) { return parsedDeptIds; } // 未配置则视为所有接单部门可接:写入所有接单部门队列 List receivingDepts = departmentMapper.selectList( new LambdaQueryWrapper() .eq(Department::getDeptType, "receiving") .eq(Department::getStatus, 1) ); if (receivingDepts == null || receivingDepts.isEmpty()) { return Collections.emptyList(); } List deptIds = new ArrayList<>(); for (Department dept : receivingDepts) { if (dept != null && dept.getId() != null) { deptIds.add(dept.getId()); } } return deptIds; } private List parseDeptIds(String allowedDeptIds) { if (allowedDeptIds == null || allowedDeptIds.trim().isEmpty()) { return Collections.emptyList(); } return departmentService.expandDeptIdsWithChildren(allowedDeptIds); } /** * 删除任务的解锁状态(任务取消/删除时调用) */ @Transactional(rollbackFor = Exception.class) public void deleteUnlockStatus(Long taskItemId) { unlockStatusMapper.deleteByTaskItemId(taskItemId); } /** * 通知任务解锁 */ private void notifyUnlockedTasks(List taskItemIds) { if (taskItemIds == null || taskItemIds.isEmpty()) { return; } // 查询子任务和主任务信息 List items = taskItemMapper.selectBatchIds(taskItemIds); if (items.isEmpty()) { return; } // 获取主任务信息 Set taskIds = new HashSet<>(); for (TaskItem item : items) { if (item.getTaskId() != null) { taskIds.add(item.getTaskId()); } } Map taskMap = new HashMap<>(); if (!taskIds.isEmpty()) { List tasks = taskMapper.selectBatchIds(taskIds); for (Task task : tasks) { taskMap.put(task.getId(), task); } } // 批量通知 taskStatusNotificationService.batchNotifyTasksUnlocked(items, taskMap); } // ==================== 统计方法 ==================== /** * 获取解锁状态统计 */ public Map getUnlockStats() { Map stats = new HashMap<>(); // 按部门统计 List depts = departmentMapper.selectList( new LambdaQueryWrapper() .eq(Department::getDeptType, "receiving") .eq(Department::getStatus, 1) ); List> deptStats = new ArrayList<>(); long totalQueued = 0; long totalUnlocked = 0; for (Department dept : depts) { Map deptStat = new HashMap<>(); int queuedCount = unlockStatusMapper.countQueuedPendingTasks(dept.getId()); int unlockedPendingCount = unlockStatusMapper.countUnlockedPendingTasks(dept.getId()); deptStat.put("deptId", dept.getId()); deptStat.put("deptName", dept.getDeptName()); deptStat.put("queuedCount", queuedCount); deptStat.put("unlockedPendingCount", unlockedPendingCount); deptStats.add(deptStat); totalQueued += queuedCount; totalUnlocked += unlockedPendingCount; } stats.put("totalQueued", totalQueued); stats.put("totalUnlocked", totalUnlocked); stats.put("deptStats", deptStats); return stats; } }