feat: emergency unlock queued tasks to hall
This commit is contained in:
parent
40de164628
commit
088ba915ca
@ -225,4 +225,26 @@ public class AdminTaskController {
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "应急解锁排队任务到大厅", description = "管理员可将排队中的任务(主任务编号/子任务编号)直接解锁为可抢单状态")
|
||||
@PostMapping("/unlock/manual")
|
||||
public Result<Map<String, Object>> emergencyUnlockToHall(
|
||||
@RequestBody EmergencyUnlockRequest request,
|
||||
@RequestHeader("Authorization") String authHeader
|
||||
) {
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
Long adminId = jwtUtil.getUserIdFromToken(token);
|
||||
Map<String, Object> result = adminTaskService.emergencyUnlockQueuedTaskToHall(
|
||||
request == null ? null : request.getTaskNoOrItemNo(),
|
||||
adminId,
|
||||
request == null ? null : request.getReason()
|
||||
);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class EmergencyUnlockRequest {
|
||||
private String taskNoOrItemNo;
|
||||
private String reason;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.designhub.controller;
|
||||
|
||||
import com.designhub.common.Result;
|
||||
import com.designhub.service.SystemConfigService;
|
||||
import com.designhub.service.TaskUnlockService;
|
||||
import com.designhub.service.TaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@ -39,9 +40,10 @@ public class OpenTaskController {
|
||||
private static final String KEY_IP_WHITELIST = "openapi.task_delete_ip_whitelist";
|
||||
|
||||
private final TaskService taskService;
|
||||
private final TaskUnlockService taskUnlockService;
|
||||
private final SystemConfigService systemConfigService;
|
||||
|
||||
@Operation(summary = "开放接口删除任务", description = "供外部机器人按任务编号删除任务")
|
||||
@Operation(summary = "开放接口删除/解锁任务", description = "供外部机器人按任务编号删除任务或解锁排队任务到大厅")
|
||||
@PostMapping("/delete")
|
||||
public Result<Map<String, Object>> deleteTaskByTaskNo(
|
||||
@RequestBody OpenDeleteTaskRequest requestBody,
|
||||
@ -83,22 +85,49 @@ public class OpenTaskController {
|
||||
return Result.error("签名已过期");
|
||||
}
|
||||
|
||||
String payload = requestBody.getAppKey() + "|" + requestBody.getTaskNo().trim() + "|" + requestBody.getTimestamp();
|
||||
String expectedSign = hmacSha256Hex(configuredSecret, payload);
|
||||
if (!secureEquals(expectedSign, requestBody.getSign().trim().toLowerCase(Locale.ROOT))) {
|
||||
String action = normalizeAction(requestBody.getAction());
|
||||
String sign = requestBody.getSign().trim().toLowerCase(Locale.ROOT);
|
||||
|
||||
// v1签名: appKey|taskNo|timestamp (兼容旧机器人,仅支持 delete)
|
||||
String payloadV1 = requestBody.getAppKey() + "|" + requestBody.getTaskNo().trim() + "|" + requestBody.getTimestamp();
|
||||
String expectedSignV1 = hmacSha256Hex(configuredSecret, payloadV1);
|
||||
|
||||
// v2签名: appKey|taskNo|action|timestamp (推荐,新功能 unlock 必须用 v2)
|
||||
String payloadV2 = requestBody.getAppKey() + "|" + requestBody.getTaskNo().trim() + "|" + action + "|" + requestBody.getTimestamp();
|
||||
String expectedSignV2 = hmacSha256Hex(configuredSecret, payloadV2);
|
||||
|
||||
boolean signOk;
|
||||
if ("unlock".equals(action)) {
|
||||
// unlock 必须包含 action 参与签名,防止篡改 action
|
||||
signOk = secureEquals(expectedSignV2, sign);
|
||||
} else {
|
||||
// delete 兼容 v1/v2
|
||||
signOk = secureEquals(expectedSignV1, sign) || secureEquals(expectedSignV2, sign);
|
||||
}
|
||||
|
||||
if (!signOk) {
|
||||
log.warn("开放删除接口签名不匹配: traceId={}, ip={}, taskNo={}",
|
||||
traceId, clientIp, requestBody.getTaskNo());
|
||||
return Result.error("签名校验失败");
|
||||
}
|
||||
|
||||
Map<String, Object> actionResult;
|
||||
if ("unlock".equals(action)) {
|
||||
actionResult = taskUnlockService.emergencyUnlockToHallByTaskNo(requestBody.getTaskNo().trim());
|
||||
} else {
|
||||
taskService.deleteTaskByTaskNo(requestBody.getTaskNo().trim());
|
||||
actionResult = new LinkedHashMap<>();
|
||||
actionResult.put("deleted", true);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("taskNo", requestBody.getTaskNo().trim());
|
||||
result.put("deleted", true);
|
||||
result.put("action", action);
|
||||
result.putAll(actionResult);
|
||||
result.put("traceId", traceId);
|
||||
result.put("time", Instant.now().toString());
|
||||
log.info("开放删除接口执行成功: traceId={}, ip={}, taskNo={}", traceId, clientIp, requestBody.getTaskNo());
|
||||
log.info("开放任务接口执行成功: traceId={}, ip={}, action={}, taskNo={}",
|
||||
traceId, clientIp, action, requestBody.getTaskNo());
|
||||
return Result.success(result);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
@ -174,11 +203,26 @@ public class OpenTaskController {
|
||||
return MessageDigest.isEqual(aa, bb);
|
||||
}
|
||||
|
||||
private String normalizeAction(String action) {
|
||||
String value = action == null ? "" : action.trim().toLowerCase(Locale.ROOT);
|
||||
if (!StringUtils.hasText(value) || "delete".equals(value)) {
|
||||
return "delete";
|
||||
}
|
||||
if ("unlock".equals(value)) {
|
||||
return "unlock";
|
||||
}
|
||||
throw new IllegalArgumentException("action参数错误,仅支持 delete/unlock");
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class OpenDeleteTaskRequest {
|
||||
private String appKey;
|
||||
private String taskNo;
|
||||
private String timestamp;
|
||||
private String sign;
|
||||
/**
|
||||
* delete(默认) / unlock(解锁排队任务到大厅)
|
||||
*/
|
||||
private String action;
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,14 @@ public interface TaskUnlockStatusMapper extends BaseMapper<TaskUnlockStatus> {
|
||||
"WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 1 AND ti.status = 'pending'")
|
||||
int countUnlockedPendingTasks(@Param("deptId") Long deptId);
|
||||
|
||||
/**
|
||||
* 获取指定部门排队中且当前待接单的任务数
|
||||
*/
|
||||
@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'")
|
||||
int countQueuedPendingTasks(@Param("deptId") Long deptId);
|
||||
|
||||
/**
|
||||
* 批量解锁任务
|
||||
*/
|
||||
|
||||
@ -43,6 +43,28 @@ public class AdminTaskService {
|
||||
private final TaskTransferLogMapper taskTransferLogMapper;
|
||||
private final TaskUnlockService taskUnlockService;
|
||||
|
||||
/**
|
||||
* 应急解锁排队任务到大厅(支持主任务编号/子任务编号)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> emergencyUnlockQueuedTaskToHall(String taskNoOrItemNo, Long adminId, String reason) {
|
||||
if (!isAdminUser(adminId)) {
|
||||
throw new BusinessException(ResultCode.FORBIDDEN, "只有管理员可以解锁排队任务");
|
||||
}
|
||||
if (!StringUtils.hasText(taskNoOrItemNo)) {
|
||||
throw new BusinessException(ResultCode.PARAM_ERROR, "任务编号不能为空");
|
||||
}
|
||||
|
||||
Map<String, Object> unlockResult = taskUnlockService.emergencyUnlockToHall(taskNoOrItemNo.trim());
|
||||
log.info("管理员应急解锁到大厅: adminId={}, key={}, reason={}, result={}",
|
||||
adminId,
|
||||
taskNoOrItemNo,
|
||||
StringUtils.hasText(reason) ? reason.trim() : "",
|
||||
unlockResult
|
||||
);
|
||||
return unlockResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员分配任务
|
||||
*/
|
||||
|
||||
@ -48,6 +48,7 @@ public class TaskApprovalService {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final DingTalkService dingTalkService;
|
||||
private final com.designhub.util.WorkHoursCalculator workHoursCalculator;
|
||||
private final TaskUnlockService taskUnlockService;
|
||||
|
||||
// ==================== 一级需求部门任务审批 ====================
|
||||
|
||||
@ -524,6 +525,15 @@ public class TaskApprovalService {
|
||||
task.setCurrentApproverRole(null);
|
||||
taskMapper.updateById(task);
|
||||
|
||||
if (task.getIsPipeline() != null && task.getIsPipeline() == 1 && finalReceiverId == null) {
|
||||
List<TaskItem> pipelineItems = taskItemMapper.selectList(
|
||||
new LambdaQueryWrapper<TaskItem>()
|
||||
.eq(TaskItem::getTaskId, task.getId())
|
||||
);
|
||||
taskUnlockService.onTaskApproved(task, pipelineItems);
|
||||
log.info("任务审核通过后已写入排队池: taskId={}, items={}", task.getId(), pipelineItems.size());
|
||||
}
|
||||
|
||||
// 扣除部门积分
|
||||
try {
|
||||
teamScorePoolService.deductScore(task.getPublisherDeptId(), task.getScore() != null ? task.getScore().intValue() : 0);
|
||||
|
||||
@ -31,6 +31,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@RequiredArgsConstructor
|
||||
public class TaskItemService {
|
||||
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TaskItemService.class);
|
||||
private static final Long GRAPHIC_VISUAL_DEPT_ID = 109L;
|
||||
private static final Long AI_VISUAL_DEPT_ID = 117L;
|
||||
|
||||
private final TaskItemMapper taskItemMapper;
|
||||
private final TaskMapper taskMapper;
|
||||
@ -279,6 +281,9 @@ public class TaskItemService {
|
||||
if (userDeptId == null || targetDeptId == null) {
|
||||
return false;
|
||||
}
|
||||
if (isSharedGrabDept(userDeptId, targetDeptId)) {
|
||||
return true;
|
||||
}
|
||||
Long currentDeptId = userDeptId;
|
||||
while (currentDeptId != null) {
|
||||
if (targetDeptId.equals(currentDeptId)) {
|
||||
@ -294,6 +299,27 @@ public class TaskItemService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isSharedGrabDept(Long userDeptId, Long targetDeptId) {
|
||||
return isDeptInTree(userDeptId, AI_VISUAL_DEPT_ID)
|
||||
&& isDeptInTree(targetDeptId, GRAPHIC_VISUAL_DEPT_ID);
|
||||
}
|
||||
|
||||
private boolean isDeptInTree(Long deptId, Long rootDeptId) {
|
||||
Long currentDeptId = deptId;
|
||||
while (currentDeptId != null) {
|
||||
if (rootDeptId.equals(currentDeptId)) {
|
||||
return true;
|
||||
}
|
||||
com.designhub.entity.Department currentDept = departmentMapper.selectById(currentDeptId);
|
||||
if (currentDept == null || currentDept.getParentId() == null
|
||||
|| currentDeptId.equals(currentDept.getParentId())) {
|
||||
break;
|
||||
}
|
||||
currentDeptId = currentDept.getParentId();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进行中的子任务列表
|
||||
* @param receiverId 接单人ID,null表示不按接单人过滤
|
||||
@ -940,6 +966,7 @@ public class TaskItemService {
|
||||
nextItem.setPrevDownloadUrl(prevDownloadUrlStr);
|
||||
nextItem.setUpdateTime(now);
|
||||
taskItemMapper.updateById(nextItem);
|
||||
markPipelineItemUnlocked(nextItem);
|
||||
|
||||
log.info("✅ 解锁预先指定的子任务: {} -> 设计师 {} (ID: {}),状态设为待接单",
|
||||
nextItem.getItemNo(), nextItem.getReceiverName(), nextItem.getReceiverId());
|
||||
@ -972,6 +999,7 @@ public class TaskItemService {
|
||||
nextItem.setPrevDownloadUrl(prevDownloadUrlStr);
|
||||
nextItem.setUpdateTime(now);
|
||||
taskItemMapper.updateById(nextItem);
|
||||
markPipelineItemUnlocked(nextItem);
|
||||
|
||||
log.info("✅ 解锁未指定的子任务: {} (部门序号: {}, ID: {}),待接单部门负责人审核",
|
||||
nextItem.getItemNo(), nextDeptIndex, nextItem.getId());
|
||||
@ -1998,6 +2026,18 @@ public class TaskItemService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void markPipelineItemUnlocked(TaskItem item) {
|
||||
Long targetDeptId = item.getReceiverDeptId();
|
||||
if (targetDeptId == null) {
|
||||
targetDeptId = getTargetDeptIdFromTaskType(item.getTaskTypeId());
|
||||
}
|
||||
if (targetDeptId == null) {
|
||||
log.warn("流水线子任务 {} 无法确定接单部门,跳过已解锁标记", item.getItemNo());
|
||||
return;
|
||||
}
|
||||
taskUnlockService.onPipelineUnlock(item, targetDeptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化工时显示(基于工作时间配置)
|
||||
*/
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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;
|
||||
@ -172,6 +173,214 @@ public class TaskUnlockService {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务被抢单后,检查并补充解锁
|
||||
*/
|
||||
@ -485,20 +694,6 @@ public class TaskUnlockService {
|
||||
public Map<String, Object> getUnlockStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// 总待解锁数
|
||||
long totalQueued = unlockStatusMapper.selectCount(
|
||||
new LambdaQueryWrapper<TaskUnlockStatus>()
|
||||
.eq(TaskUnlockStatus::getIsUnlocked, 0)
|
||||
);
|
||||
stats.put("totalQueued", totalQueued);
|
||||
|
||||
// 总已解锁数
|
||||
long totalUnlocked = unlockStatusMapper.selectCount(
|
||||
new LambdaQueryWrapper<TaskUnlockStatus>()
|
||||
.eq(TaskUnlockStatus::getIsUnlocked, 1)
|
||||
);
|
||||
stats.put("totalUnlocked", totalUnlocked);
|
||||
|
||||
// 按部门统计
|
||||
List<Department> depts = departmentMapper.selectList(
|
||||
new LambdaQueryWrapper<Department>()
|
||||
@ -507,14 +702,23 @@ public class TaskUnlockService {
|
||||
);
|
||||
|
||||
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", unlockStatusMapper.countQueuedTasks(dept.getId()));
|
||||
deptStat.put("unlockedPendingCount", unlockStatusMapper.countUnlockedPendingTasks(dept.getId()));
|
||||
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;
|
||||
|
||||
@ -2006,6 +2006,64 @@
|
||||
style="margin-top: 16px;"
|
||||
row-key="deptId"
|
||||
/>
|
||||
|
||||
<!-- 应急解锁 -->
|
||||
<a-divider orientation="left" style="margin-top: 24px;">应急解锁</a-divider>
|
||||
|
||||
<a-card title="应急解锁到大厅(管理员)" class="task-delete-card">
|
||||
<a-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
message="适用场景"
|
||||
description="当任务处于排队中但需要紧急处理时,可手动解锁到任务大厅,立即变为可抢单状态(仅影响待接单节点)。"
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
<a-form layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="10">
|
||||
<a-form-item label="任务编号 / 子任务编号">
|
||||
<a-input
|
||||
v-model:value="taskUnlockManualForm.taskNoOrItemNo"
|
||||
placeholder="例如:TASK202603041636485827 或 TASK202603041636485827-03-01"
|
||||
size="large"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="10">
|
||||
<a-form-item label="解锁原因(可选)">
|
||||
<a-input
|
||||
v-model:value="taskUnlockManualForm.reason"
|
||||
maxlength="200"
|
||||
show-count
|
||||
placeholder="例如:紧急插单,先处理此任务"
|
||||
size="large"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="4">
|
||||
<a-form-item label="操作">
|
||||
<a-popconfirm
|
||||
title="确认解锁到大厅?"
|
||||
description="将把排队中的待接单节点直接解锁为可抢单状态"
|
||||
ok-text="确认解锁"
|
||||
cancel-text="取消"
|
||||
@confirm="handleEmergencyUnlockToHall"
|
||||
>
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
:loading="unlockingQueuedTask"
|
||||
:disabled="!taskUnlockManualForm.taskNoOrItemNo"
|
||||
>
|
||||
解锁到大厅
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
@ -5787,6 +5845,11 @@ const taskUnlockConfig = reactive({
|
||||
const savingTaskUnlockConfig = ref(false)
|
||||
const loadingTaskUnlockStats = ref(false)
|
||||
const taskUnlockStats = ref<any>(null)
|
||||
const taskUnlockManualForm = reactive({
|
||||
taskNoOrItemNo: '',
|
||||
reason: ''
|
||||
})
|
||||
const unlockingQueuedTask = ref(false)
|
||||
|
||||
// 任务排队统计表格列
|
||||
const taskUnlockStatsColumns = [
|
||||
@ -5855,6 +5918,48 @@ const loadTaskUnlockStats = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 应急解锁到大厅
|
||||
const handleEmergencyUnlockToHall = async () => {
|
||||
const key = taskUnlockManualForm.taskNoOrItemNo?.trim()
|
||||
if (!key) {
|
||||
message.warning('请输入任务编号或子任务编号')
|
||||
return
|
||||
}
|
||||
|
||||
unlockingQueuedTask.value = true
|
||||
try {
|
||||
const response = await fetch('/api/admin/tasks/unlock/manual', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
taskNoOrItemNo: key,
|
||||
reason: taskUnlockManualForm.reason?.trim() || ''
|
||||
})
|
||||
})
|
||||
const result = await response.json()
|
||||
|
||||
if (result.code === 200) {
|
||||
const data = result.data || {}
|
||||
const unlockedCount = data.unlockedItemCount ?? 0
|
||||
message.success(data.message || `已解锁到大厅(解锁节点数:${unlockedCount})`)
|
||||
taskUnlockManualForm.taskNoOrItemNo = ''
|
||||
taskUnlockManualForm.reason = ''
|
||||
// 刷新统计
|
||||
await loadTaskUnlockStats()
|
||||
} else {
|
||||
message.error(result.message || '解锁失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('应急解锁失败:', error)
|
||||
message.error(error.message || '解锁失败')
|
||||
} finally {
|
||||
unlockingQueuedTask.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载延误扣分配置
|
||||
const loadDelayPenaltyConfig = async () => {
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user