From 2331aec8c0ef6e58d23f9b9d33a40c98392a491f Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 12 Mar 2026 14:34:43 +0800 Subject: [PATCH] fix: prevent queue bypass when unlock_status missing --- .../mapper/TaskUnlockStatusMapper.java | 91 +++++++-- .../designhub/service/TaskUnlockService.java | 187 +++++++++++++----- 2 files changed, 211 insertions(+), 67 deletions(-) diff --git a/backend/src/main/java/com/designhub/mapper/TaskUnlockStatusMapper.java b/backend/src/main/java/com/designhub/mapper/TaskUnlockStatusMapper.java index c677f92..2cbadcc 100644 --- a/backend/src/main/java/com/designhub/mapper/TaskUnlockStatusMapper.java +++ b/backend/src/main/java/com/designhub/mapper/TaskUnlockStatusMapper.java @@ -16,21 +16,38 @@ public interface TaskUnlockStatusMapper extends BaseMapper { /** * 获取指定部门已解锁的任务ID列表 */ - @Select("SELECT task_item_id FROM task_unlock_status WHERE dept_id = #{deptId} AND is_unlocked = 1") + @Select("SELECT tus.task_item_id FROM task_unlock_status tus " + + "INNER JOIN task_item ti ON tus.task_item_id = ti.id " + + "INNER JOIN task t ON ti.task_id = t.id " + + "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 1 " + + " AND ti.status = 'pending' AND ti.is_locked = 0 " + + " AND t.deleted = 0 AND t.approval_status = 'approved' " + + " AND (t.release_type IS NULL OR t.release_type != 'assigned')") List selectUnlockedTaskItemIds(@Param("deptId") Long deptId); /** * 获取指定部门未解锁的任务ID列表(按发布日期排序) */ - @Select("SELECT task_item_id FROM task_unlock_status " + - "WHERE dept_id = #{deptId} AND is_unlocked = 0 " + - "ORDER BY publish_date ASC, id ASC") + @Select("SELECT tus.task_item_id FROM task_unlock_status tus " + + "INNER JOIN task_item ti ON tus.task_item_id = ti.id " + + "INNER JOIN task t ON ti.task_id = t.id " + + "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 0 " + + " AND ti.status = 'pending' AND ti.is_locked = 0 " + + " AND t.deleted = 0 AND t.approval_status = 'approved' " + + " AND (t.release_type IS NULL OR t.release_type != 'assigned') " + + "ORDER BY tus.publish_date ASC, tus.id ASC") List selectQueuedTaskItemIds(@Param("deptId") Long deptId); /** * 获取指定部门未解锁任务总数 */ - @Select("SELECT COUNT(*) FROM task_unlock_status WHERE dept_id = #{deptId} AND is_unlocked = 0") + @Select("SELECT COUNT(*) FROM task_unlock_status tus " + + "INNER JOIN task_item ti ON tus.task_item_id = ti.id " + + "INNER JOIN task t ON ti.task_id = t.id " + + "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 0 " + + " AND ti.status = 'pending' AND ti.is_locked = 0 " + + " AND t.deleted = 0 AND t.approval_status = 'approved' " + + " AND (t.release_type IS NULL OR t.release_type != 'assigned')") int countQueuedTasks(@Param("deptId") Long deptId); /** @@ -38,7 +55,11 @@ public interface TaskUnlockStatusMapper extends BaseMapper { */ @Select("SELECT COUNT(*) FROM task_unlock_status tus " + "INNER JOIN task_item ti ON tus.task_item_id = ti.id " + - "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 1 AND ti.status = 'pending'") + "INNER JOIN task t ON ti.task_id = t.id " + + "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 1 " + + " AND ti.status = 'pending' AND ti.is_locked = 0 " + + " AND t.deleted = 0 AND t.approval_status = 'approved' " + + " AND (t.release_type IS NULL OR t.release_type != 'assigned')") int countUnlockedPendingTasks(@Param("deptId") Long deptId); /** @@ -46,7 +67,11 @@ public interface TaskUnlockStatusMapper extends BaseMapper { */ @Select("SELECT COUNT(*) FROM task_unlock_status tus " + "INNER JOIN task_item ti ON tus.task_item_id = ti.id " + - "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 0 AND ti.status = 'pending'") + "INNER JOIN task t ON ti.task_id = t.id " + + "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 0 " + + " AND ti.status = 'pending' AND ti.is_locked = 0 " + + " AND t.deleted = 0 AND t.approval_status = 'approved' " + + " AND (t.release_type IS NULL OR t.release_type != 'assigned')") int countQueuedPendingTasks(@Param("deptId") Long deptId); /** @@ -65,9 +90,14 @@ public interface TaskUnlockStatusMapper extends BaseMapper { /** * 获取指定部门按发布日期分组的未解锁任务统计 */ - @Select("SELECT publish_date, COUNT(*) as count FROM task_unlock_status " + - "WHERE dept_id = #{deptId} AND is_unlocked = 0 " + - "GROUP BY publish_date ORDER BY publish_date ASC") + @Select("SELECT tus.publish_date, COUNT(*) as count FROM task_unlock_status tus " + + "INNER JOIN task_item ti ON tus.task_item_id = ti.id " + + "INNER JOIN task t ON ti.task_id = t.id " + + "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 0 " + + " AND ti.status = 'pending' AND ti.is_locked = 0 " + + " AND t.deleted = 0 AND t.approval_status = 'approved' " + + " AND (t.release_type IS NULL OR t.release_type != 'assigned') " + + "GROUP BY tus.publish_date ORDER BY tus.publish_date ASC") @Results({ @Result(column = "publish_date", property = "publishDate"), @Result(column = "count", property = "count") @@ -77,20 +107,30 @@ public interface TaskUnlockStatusMapper extends BaseMapper { /** * 获取需要解锁的任务ID列表(按日期顺序,限制数量) */ - @Select("SELECT task_item_id FROM task_unlock_status " + - "WHERE dept_id = #{deptId} AND is_unlocked = 0 " + - "ORDER BY publish_date ASC, id ASC " + + @Select("SELECT tus.task_item_id FROM task_unlock_status tus " + + "INNER JOIN task_item ti ON tus.task_item_id = ti.id " + + "INNER JOIN task t ON ti.task_id = t.id " + + "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 0 " + + " AND ti.status = 'pending' AND ti.is_locked = 0 " + + " AND t.deleted = 0 AND t.approval_status = 'approved' " + + " AND (t.release_type IS NULL OR t.release_type != 'assigned') " + + "ORDER BY tus.publish_date ASC, tus.id ASC " + "LIMIT #{limit}") List selectTasksToUnlock(@Param("deptId") Long deptId, @Param("limit") int limit); /** * 计算任务在队列中的位置 */ - @Select("SELECT COUNT(*) + 1 FROM task_unlock_status " + - "WHERE dept_id = #{deptId} AND is_unlocked = 0 " + - "AND (publish_date < (SELECT publish_date FROM task_unlock_status WHERE task_item_id = #{taskItemId} AND dept_id = #{deptId}) " + - " OR (publish_date = (SELECT publish_date FROM task_unlock_status WHERE task_item_id = #{taskItemId} AND dept_id = #{deptId}) " + - " AND id < (SELECT id FROM task_unlock_status WHERE task_item_id = #{taskItemId} AND dept_id = #{deptId})))") + @Select("SELECT COUNT(*) + 1 FROM task_unlock_status tus " + + "INNER JOIN task_item ti ON tus.task_item_id = ti.id " + + "INNER JOIN task t ON ti.task_id = t.id " + + "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 0 " + + " AND ti.status = 'pending' AND ti.is_locked = 0 " + + " AND t.deleted = 0 AND t.approval_status = 'approved' " + + " AND (t.release_type IS NULL OR t.release_type != 'assigned') " + + " AND (tus.publish_date < (SELECT publish_date FROM task_unlock_status WHERE task_item_id = #{taskItemId} AND dept_id = #{deptId} LIMIT 1) " + + " OR (tus.publish_date = (SELECT publish_date FROM task_unlock_status WHERE task_item_id = #{taskItemId} AND dept_id = #{deptId} LIMIT 1) " + + " AND tus.id < (SELECT id FROM task_unlock_status WHERE task_item_id = #{taskItemId} AND dept_id = #{deptId} LIMIT 1)))") int calculateQueuePosition(@Param("taskItemId") Long taskItemId, @Param("deptId") Long deptId); /** @@ -99,6 +139,21 @@ public interface TaskUnlockStatusMapper extends BaseMapper { @Select("SELECT is_unlocked FROM task_unlock_status WHERE task_item_id = #{taskItemId} AND dept_id = #{deptId}") Integer isUnlocked(@Param("taskItemId") Long taskItemId, @Param("deptId") Long deptId); + /** + * 写入排队记录(若不存在则插入) + *

用于修复历史数据缺失导致的“新任务绕过排队”问题

+ */ + @Insert("INSERT INTO task_unlock_status (task_item_id, dept_id, publish_date, is_unlocked, unlock_time, create_time, update_time) " + + "SELECT #{taskItemId}, #{deptId}, #{publishDate}, #{isUnlocked}, " + + " CASE WHEN #{isUnlocked} = 1 THEN NOW() ELSE NULL END, " + + " NOW(), NOW() " + + "FROM DUAL " + + "WHERE NOT EXISTS (SELECT 1 FROM task_unlock_status WHERE task_item_id = #{taskItemId} AND dept_id = #{deptId})") + int insertIfNotExists(@Param("taskItemId") Long taskItemId, + @Param("deptId") Long deptId, + @Param("publishDate") LocalDate publishDate, + @Param("isUnlocked") Integer isUnlocked); + /** * 删除任务的解锁状态(任务取消/删除时调用) */ diff --git a/backend/src/main/java/com/designhub/service/TaskUnlockService.java b/backend/src/main/java/com/designhub/service/TaskUnlockService.java index e415b22..d923208 100644 --- a/backend/src/main/java/com/designhub/service/TaskUnlockService.java +++ b/backend/src/main/java/com/designhub/service/TaskUnlockService.java @@ -423,42 +423,30 @@ public class TaskUnlockService { return; } - LocalDate publishDate = task.getCreateTime().toLocalDate(); - int addedCount = 0; + LocalDate publishDate = task.getCreateTime() != null ? task.getCreateTime().toLocalDate() : LocalDate.now(); + int addedRows = 0; for (TaskItem item : items) { - // 获取目标接单部门 - Long deptId = getTargetDeptId(item, task); - if (deptId == null) { + if (item.getId() == null) { + continue; + } + + List deptIds = resolveTargetDeptIds(item, task); + if (deptIds.isEmpty()) { log.warn("子任务 {} 无法确定目标部门,跳过入队", item.getItemNo()); continue; } - // 检查是否已存在 - TaskUnlockStatus existing = unlockStatusMapper.selectOne( - new LambdaQueryWrapper() - .eq(TaskUnlockStatus::getTaskItemId, item.getId()) - .eq(TaskUnlockStatus::getDeptId, deptId) - ); - - if (existing != null) { - continue; // 已存在,跳过 + for (Long deptId : deptIds) { + if (deptId == null) { + continue; + } + addedRows += unlockStatusMapper.insertIfNotExists(item.getId(), deptId, publishDate, 0); } - - // 创建排队记录 - TaskUnlockStatus unlockStatus = new TaskUnlockStatus(); - unlockStatus.setTaskItemId(item.getId()); - unlockStatus.setDeptId(deptId); - unlockStatus.setPublishDate(publishDate); - unlockStatus.setIsUnlocked(0); // 未解锁 - unlockStatus.setCreateTime(LocalDateTime.now()); - - unlockStatusMapper.insert(unlockStatus); - addedCount++; } - if (addedCount > 0) { - log.info("任务 {} 的 {} 个子任务已加入排队池", task.getTaskNo(), addedCount); + if (addedRows > 0) { + log.info("任务 {} 已写入 {} 条排队记录", task.getTaskNo(), addedRows); } } @@ -544,10 +532,17 @@ public class TaskUnlockService { Integer isUnlocked = unlockStatusMapper.isUnlocked(item.getId(), deptId); if (isUnlocked == null) { - // 没有记录,视为已解锁(兼容旧数据) - item.setUnlockStatus("unlocked"); - item.setCanGrab(true); - return; + // 没有记录:补全排队记录,避免“新任务绕过排队” + LocalDate publishDate = item.getCreateTime() != null ? item.getCreateTime().toLocalDate() : LocalDate.now(); + unlockStatusMapper.insertIfNotExists(item.getId(), deptId, publishDate, 0); + isUnlocked = unlockStatusMapper.isUnlocked(item.getId(), deptId); + + // 兜底:依然查不到则按排队中处理 + if (isUnlocked == null) { + item.setUnlockStatus("queuing"); + item.setCanGrab(false); + return; + } } if (isUnlocked == 1) { @@ -611,37 +606,131 @@ public class TaskUnlockService { // 检查解锁状态 Integer isUnlocked = unlockStatusMapper.isUnlocked(taskItemId, deptId); - // 没有记录或已解锁 = 可抢 - return isUnlocked == null || isUnlocked == 1; + if (isUnlocked == null) { + // 没有记录:补全排队记录,默认排队中(不可抢) + ensureUnlockStatusExists(taskItemId, deptId); + return false; + } + + return isUnlocked == 1; } // ==================== 辅助方法 ==================== /** - * 获取任务的目标接单部门ID + * 补全排队记录(仅补全当前部门维度) + *

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

*/ - private Long getTargetDeptId(TaskItem item, Task task) { - // 优先使用子任务的接单部门 - if (item.getReceiverDeptId() != null) { - return item.getReceiverDeptId(); + private void ensureUnlockStatusExists(Long taskItemId, Long deptId) { + if (taskItemId == null || deptId == null) { + return; } - // 从任务类型获取 - if (task.getDemandTypeId() != null) { - DemandType demandType = demandTypeMapper.selectById(task.getDemandTypeId()); - if (demandType != null && demandType.getAllowedDeptIds() != null) { - String[] deptIds = demandType.getAllowedDeptIds().split(","); - if (deptIds.length > 0) { - try { - return Long.parseLong(deptIds[0].trim()); - } catch (NumberFormatException e) { - // 忽略 + 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()); + } + } + + /** + * 获取任务的目标接单部门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(); } } } } - return null; + 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(); + } + String[] parts = allowedDeptIds.split(","); + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (String part : parts) { + if (part == null) { + continue; + } + String s = part.trim(); + if (s.isEmpty()) { + continue; + } + try { + Long id = Long.parseLong(s); + if (seen.add(id)) { + result.add(id); + } + } catch (NumberFormatException e) { + // 忽略 + } + } + return result; } /**