833 lines
30 KiB
Java
833 lines
30 KiB
Java
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<Department> receivingDepts = departmentMapper.selectList(
|
||
new LambdaQueryWrapper<Department>()
|
||
.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<Long> 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;
|
||
}
|
||
|
||
// ==================== 应急解锁(管理员/机器人调用) ====================
|
||
|
||
/**
|
||
* 应急解锁:将指定任务(主任务编号/子任务编号)从排队中直接解锁到大厅
|
||
* <p>
|
||
* - 如果传入的是子任务编号(itemNo),仅解锁该子任务(仅限 pending)
|
||
* - 如果传入的是主任务编号(taskNo),解锁该主任务下所有 pending 子任务(仅解锁排队中的)
|
||
* </p>
|
||
*/
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Map<String, Object> emergencyUnlockToHall(String taskNoOrItemNo) {
|
||
String key = taskNoOrItemNo == null ? "" : taskNoOrItemNo.trim();
|
||
if (key.isEmpty()) {
|
||
throw new IllegalArgumentException("任务编号不能为空");
|
||
}
|
||
|
||
TaskItem item = taskItemMapper.selectOne(
|
||
new LambdaQueryWrapper<TaskItem>()
|
||
.eq(TaskItem::getItemNo, key)
|
||
.last("LIMIT 1")
|
||
);
|
||
if (item != null) {
|
||
return emergencyUnlockTaskItemToHall(item);
|
||
}
|
||
|
||
Task task = taskMapper.selectOne(
|
||
new LambdaQueryWrapper<Task>()
|
||
.eq(Task::getTaskNo, key)
|
||
.last("LIMIT 1")
|
||
);
|
||
if (task != null) {
|
||
return emergencyUnlockTaskToHall(task);
|
||
}
|
||
|
||
throw new IllegalArgumentException("任务不存在,请检查编号是否正确");
|
||
}
|
||
|
||
/**
|
||
* 应急解锁:按主任务编号解锁
|
||
*/
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Map<String, Object> emergencyUnlockToHallByTaskNo(String taskNo) {
|
||
String key = taskNo == null ? "" : taskNo.trim();
|
||
if (key.isEmpty()) {
|
||
throw new IllegalArgumentException("任务编号不能为空");
|
||
}
|
||
Task task = taskMapper.selectOne(
|
||
new LambdaQueryWrapper<Task>()
|
||
.eq(Task::getTaskNo, key)
|
||
.last("LIMIT 1")
|
||
);
|
||
if (task == null) {
|
||
throw new IllegalArgumentException("任务不存在,请检查任务编号是否正确");
|
||
}
|
||
return emergencyUnlockTaskToHall(task);
|
||
}
|
||
|
||
private Map<String, Object> 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<String, Object> 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<TaskUnlockStatus> queuedRows = unlockStatusMapper.selectList(
|
||
new LambdaQueryWrapper<TaskUnlockStatus>()
|
||
.eq(TaskUnlockStatus::getTaskItemId, item.getId())
|
||
.eq(TaskUnlockStatus::getIsUnlocked, 0)
|
||
);
|
||
if (queuedRows == null || queuedRows.isEmpty()) {
|
||
Map<String, Object> 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<TaskUnlockStatus>()
|
||
.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<String, Object> 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<String, Object> emergencyUnlockTaskToHall(Task task) {
|
||
if ("assigned".equals(task.getReleaseType())) {
|
||
Map<String, Object> 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<TaskItem> pendingItems = taskItemMapper.selectList(
|
||
new LambdaQueryWrapper<TaskItem>()
|
||
.eq(TaskItem::getTaskId, task.getId())
|
||
.eq(TaskItem::getStatus, "pending")
|
||
);
|
||
|
||
Map<String, Object> 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<Long> itemIds = pendingItems.stream()
|
||
.map(TaskItem::getId)
|
||
.filter(Objects::nonNull)
|
||
.toList();
|
||
|
||
if (itemIds.isEmpty()) {
|
||
result.put("unlockedItemCount", 0);
|
||
result.put("message", "子任务ID为空,无法解锁");
|
||
return result;
|
||
}
|
||
|
||
List<TaskUnlockStatus> queuedRows = unlockStatusMapper.selectList(
|
||
new LambdaQueryWrapper<TaskUnlockStatus>()
|
||
.in(TaskUnlockStatus::getTaskItemId, itemIds)
|
||
.eq(TaskUnlockStatus::getIsUnlocked, 0)
|
||
);
|
||
|
||
if (queuedRows == null || queuedRows.isEmpty()) {
|
||
result.put("unlockedItemCount", 0);
|
||
result.put("message", "该主任务下没有排队中的待接单子任务");
|
||
return result;
|
||
}
|
||
|
||
List<Long> queuedItemIds = queuedRows.stream()
|
||
.map(TaskUnlockStatus::getTaskItemId)
|
||
.filter(Objects::nonNull)
|
||
.distinct()
|
||
.toList();
|
||
|
||
LocalDateTime now = LocalDateTime.now();
|
||
int affectedRows = unlockStatusMapper.update(
|
||
null,
|
||
new LambdaUpdateWrapper<TaskUnlockStatus>()
|
||
.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<TaskItem> 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<Long> affectedDeptIds = new LinkedHashSet<>();
|
||
|
||
for (TaskItem item : items) {
|
||
if (item.getId() == null) {
|
||
continue;
|
||
}
|
||
|
||
List<Long> 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<TaskUnlockStatus>()
|
||
.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<Long> getUnlockedTaskItemIds(Long deptId) {
|
||
return unlockStatusMapper.selectUnlockedTaskItemIds(deptId);
|
||
}
|
||
|
||
/**
|
||
* 获取排队中的任务ID列表
|
||
*/
|
||
public List<Long> 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<TaskItem> 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;
|
||
}
|
||
|
||
// ==================== 辅助方法 ====================
|
||
|
||
/**
|
||
* 补全排队记录(仅补全当前部门维度)
|
||
* <p>用于修复历史数据缺失导致的绕过排队与不可解锁问题</p>
|
||
*/
|
||
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<Long> 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<Long> parsedDeptIds = parseDeptIds(allowedDeptIds);
|
||
if (!parsedDeptIds.isEmpty()) {
|
||
return parsedDeptIds;
|
||
}
|
||
|
||
// 未配置则视为所有接单部门可接:写入所有接单部门队列
|
||
List<Department> receivingDepts = departmentMapper.selectList(
|
||
new LambdaQueryWrapper<Department>()
|
||
.eq(Department::getDeptType, "receiving")
|
||
.eq(Department::getStatus, 1)
|
||
);
|
||
if (receivingDepts == null || receivingDepts.isEmpty()) {
|
||
return Collections.emptyList();
|
||
}
|
||
|
||
List<Long> deptIds = new ArrayList<>();
|
||
for (Department dept : receivingDepts) {
|
||
if (dept != null && dept.getId() != null) {
|
||
deptIds.add(dept.getId());
|
||
}
|
||
}
|
||
return deptIds;
|
||
}
|
||
|
||
private List<Long> 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<Long> taskItemIds) {
|
||
if (taskItemIds == null || taskItemIds.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
// 查询子任务和主任务信息
|
||
List<TaskItem> items = taskItemMapper.selectBatchIds(taskItemIds);
|
||
if (items.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
// 获取主任务信息
|
||
Set<Long> taskIds = new HashSet<>();
|
||
for (TaskItem item : items) {
|
||
if (item.getTaskId() != null) {
|
||
taskIds.add(item.getTaskId());
|
||
}
|
||
}
|
||
|
||
Map<Long, Task> taskMap = new HashMap<>();
|
||
if (!taskIds.isEmpty()) {
|
||
List<Task> tasks = taskMapper.selectBatchIds(taskIds);
|
||
for (Task task : tasks) {
|
||
taskMap.put(task.getId(), task);
|
||
}
|
||
}
|
||
|
||
// 批量通知
|
||
taskStatusNotificationService.batchNotifyTasksUnlocked(items, taskMap);
|
||
}
|
||
|
||
// ==================== 统计方法 ====================
|
||
|
||
/**
|
||
* 获取解锁状态统计
|
||
*/
|
||
public Map<String, Object> getUnlockStats() {
|
||
Map<String, Object> stats = new HashMap<>();
|
||
|
||
// 按部门统计
|
||
List<Department> depts = departmentMapper.selectList(
|
||
new LambdaQueryWrapper<Department>()
|
||
.eq(Department::getDeptType, "receiving")
|
||
.eq(Department::getStatus, 1)
|
||
);
|
||
|
||
List<Map<String, Object>> deptStats = new ArrayList<>();
|
||
long totalQueued = 0;
|
||
long totalUnlocked = 0;
|
||
for (Department dept : depts) {
|
||
Map<String, Object> 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;
|
||
}
|
||
}
|