feat(settings): add admin task-item dept redirect tool

This commit is contained in:
Your Name 2026-02-08 10:00:15 +08:00
parent f041543f02
commit f484e17e73
4 changed files with 261 additions and 2 deletions

View File

@ -138,6 +138,23 @@ public class AdminTaskController {
return Result.success(result);
}
@Operation(summary = "任务节点调拨到指定接单部门")
@PostMapping("/items/redirect-dept")
public Result<Map<String, Object>> redirectTaskItemToDept(
@RequestBody RedirectTaskItemDeptRequest request,
@RequestHeader("Authorization") String authHeader
) {
String token = jwtUtil.extractTokenFromHeader(authHeader);
Long adminId = jwtUtil.getUserIdFromToken(token);
Map<String, Object> result = adminTaskService.redirectTaskItemToDept(
request.getItemNo(),
request.getTargetDeptId(),
adminId,
request.getReason()
);
return Result.success(result);
}
// DTO类
public static class TransferRequest {
private Long newDesignerId;
@ -166,6 +183,13 @@ public class AdminTaskController {
public void setReason(String reason) { this.reason = reason; }
}
@Data
public static class RedirectTaskItemDeptRequest {
private String itemNo;
private Long targetDeptId;
private String reason;
}
// ==================== 任务排队解锁相关接口 ====================
@Operation(summary = "获取任务解锁状态统计")
@ -202,4 +226,3 @@ public class AdminTaskController {
return Result.success(result);
}
}

View File

@ -4,13 +4,16 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.designhub.common.ResultCode;
import com.designhub.entity.Department;
import com.designhub.entity.Task;
import com.designhub.entity.TaskTransferLog;
import com.designhub.entity.User;
import com.designhub.exception.BusinessException;
import com.designhub.mapper.TaskTransferLogMapper;
import com.designhub.mapper.TaskMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@ -37,6 +40,8 @@ public class AdminTaskService {
private final DepartmentService departmentService;
private final RoleService roleService;
private final WorkHoursCalculator workHoursCalculator;
private final TaskTransferLogMapper taskTransferLogMapper;
private final TaskUnlockService taskUnlockService;
/**
* 管理员分配任务
@ -400,6 +405,119 @@ public class AdminTaskService {
return task;
}
/**
* 管理员将子任务直接切换到指定接单部门可接单
*/
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> redirectTaskItemToDept(String itemNo, Long targetDeptId, Long adminId, String reason) {
if (!isAdminUser(adminId)) {
throw new BusinessException(ResultCode.FORBIDDEN, "只有管理员可以操作任务节点调拨");
}
if (!StringUtils.hasText(itemNo)) {
throw new BusinessException(ResultCode.PARAM_ERROR, "子任务编号不能为空");
}
if (targetDeptId == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "目标部门不能为空");
}
TaskItem item = taskItemMapper.selectOne(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getItemNo, itemNo.trim())
.last("LIMIT 1")
);
if (item == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "子任务不存在");
}
Task task = taskMapper.selectById(item.getTaskId());
if (task == null || task.getDeleted() != null && task.getDeleted() == 1) {
throw new BusinessException(ResultCode.NOT_FOUND, "主任务不存在");
}
Department targetDept = departmentService.getDepartmentById(targetDeptId);
if (targetDept == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "目标部门不存在");
}
if (targetDept.getStatus() == null || targetDept.getStatus() != 1) {
throw new BusinessException(ResultCode.PARAM_ERROR, "目标部门已禁用");
}
if (!"receiving".equals(targetDept.getDeptType())) {
throw new BusinessException(ResultCode.PARAM_ERROR, "目标部门必须是接单部门");
}
if ("completed".equals(item.getStatus())) {
throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "已完成子任务不允许调拨");
}
if ("in_progress".equals(item.getStatus())) {
throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "进行中子任务请先转移接单人");
}
Long oldDeptId = item.getReceiverDeptId();
Long oldReceiverId = item.getReceiverId();
LocalDateTime oldDeadline = item.getDeadline();
String oldStatus = item.getStatus();
item.setStatus("pending");
item.setReceiverDeptId(targetDeptId);
item.setReceiverId(null);
item.setReceiverName(null);
item.setClaimTime(null);
item.setDeadline(null);
item.setIsLocked(0);
item.setUpdateTime(LocalDateTime.now());
taskItemMapper.updateById(item);
// 排队记录重建到新部门,确保可见且可抢
taskUnlockService.deleteUnlockStatus(item.getId());
if (taskUnlockService.isUnlockEnabled() && !"assigned".equals(task.getReleaseType())) {
taskUnlockService.onPipelineUnlock(item, targetDeptId);
}
String transferReason = StringUtils.hasText(reason) ? reason.trim() : "管理员手动调拨子任务到指定接单部门";
TaskTransferLog transferLog = new TaskTransferLog();
transferLog.setTaskId(task.getId());
transferLog.setTransferType("transfer");
transferLog.setFromUserId(oldReceiverId);
transferLog.setToUserId(null);
transferLog.setOperatorId(adminId);
transferLog.setOldDeadline(oldDeadline);
transferLog.setNewDeadline(null);
transferLog.setOldPriority(task.getPriority());
transferLog.setNewPriority(task.getPriority());
transferLog.setReason(transferReason);
transferLog.setRemark(String.format("节点调拨:%s(%s)->%s(%s), itemNo=%s, oldStatus=%s",
oldDeptId == null ? "-" : oldDeptId,
resolveDeptName(oldDeptId),
targetDeptId,
targetDept.getDeptName(),
item.getItemNo(),
oldStatus
));
transferLog.setCreateTime(LocalDateTime.now());
taskTransferLogMapper.insert(transferLog);
Map<String, Object> result = new HashMap<>();
result.put("taskId", task.getId());
result.put("taskNo", task.getTaskNo());
result.put("itemId", item.getId());
result.put("itemNo", item.getItemNo());
result.put("oldStatus", oldStatus);
result.put("newStatus", item.getStatus());
result.put("oldDeptId", oldDeptId);
result.put("oldDeptName", resolveDeptName(oldDeptId));
result.put("newDeptId", targetDeptId);
result.put("newDeptName", targetDept.getDeptName());
return result;
}
private String resolveDeptName(Long deptId) {
if (deptId == null) {
return "";
}
Department department = departmentService.getDepartmentById(deptId);
return department != null ? department.getDeptName() : "";
}
/**
* 获取优先级文本
*/
@ -517,4 +635,3 @@ public class AdminTaskService {
return result;
}
}

View File

@ -71,3 +71,13 @@ export const cancelTask = (taskId: number, reason: string) => {
})
}
/**
* 任务节点调拨到指定接单部门
*/
export const redirectTaskItemToDept = (itemNo: string, targetDeptId: number, reason?: string) => {
return request.post<Record<string, any>>('/admin/tasks/items/redirect-dept', {
itemNo,
targetDeptId,
reason
})
}

View File

@ -2037,6 +2037,78 @@
/>
</div>
</a-card>
<a-card title="任务节点调拨(管理员)" class="task-delete-card" style="margin-bottom: 24px;">
<a-alert
type="info"
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="taskItemDispatchForm.itemNo"
placeholder="例如:TASK202601161454431928-03-01"
size="large"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="目标接单部门">
<a-select
v-model:value="taskItemDispatchForm.targetDeptId"
placeholder="请选择接单部门"
size="large"
show-search
:filter-option="(input: string, option: any) => (option?.children || '').toLowerCase().includes(input.toLowerCase())"
>
<a-select-option
v-for="dept in receivingDepartments"
:key="dept.id"
:value="dept.id"
>
{{ dept.deptName }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :md="6">
<a-form-item label="操作">
<a-popconfirm
title="确认执行节点调拨?"
description="将清空当前接单人并改为可接单状态"
ok-text="确认调拨"
cancel-text="取消"
@confirm="handleRedirectTaskItemToDept"
>
<a-button
type="primary"
danger
block
size="large"
:loading="redirectingTaskItem"
:disabled="!taskItemDispatchForm.itemNo || !taskItemDispatchForm.targetDeptId"
>
执行调拨
</a-button>
</a-popconfirm>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="调拨原因(可选)">
<a-input
v-model:value="taskItemDispatchForm.reason"
maxlength="200"
show-count
placeholder="例如:原接单部门无人处理,转平面视觉部抢单"
/>
</a-form-item>
</a-form>
</a-card>
<a-alert
message="危险操作"
@ -3146,6 +3218,7 @@ import {
type ThirdPartyDbConfig
} from '@/api/thirdPartyDb'
import { deleteTaskByTaskNo } from '@/api/task'
import { redirectTaskItemToDept } from '@/api/admin'
import request from '@/utils/request'
type LimitPeriod = 'DAILY' | 'WEEKLY' | 'MONTHLY'
@ -3332,6 +3405,12 @@ const taskDeleteForm = reactive({
taskNo: ''
})
const deletingTask = ref(false)
const taskItemDispatchForm = reactive({
itemNo: '',
targetDeptId: undefined as number | undefined,
reason: ''
})
const redirectingTaskItem = ref(false)
// 盲盒模式相关
const blindBoxMode = ref(false)
@ -3392,6 +3471,36 @@ const handleDeleteTask = async () => {
}
}
const handleRedirectTaskItemToDept = async () => {
const itemNo = taskItemDispatchForm.itemNo.trim()
if (!itemNo) {
message.warning('请输入子任务编号')
return
}
if (!taskItemDispatchForm.targetDeptId) {
message.warning('请选择目标接单部门')
return
}
redirectingTaskItem.value = true
try {
const response = await redirectTaskItemToDept(
itemNo,
taskItemDispatchForm.targetDeptId,
taskItemDispatchForm.reason?.trim() || undefined
)
const data = response?.data || {}
const deptName = data.newDeptName || '目标部门'
message.success(`调拨成功:${itemNo} 已切换到 ${deptName} 可接单`)
taskItemDispatchForm.itemNo = ''
taskItemDispatchForm.reason = ''
} catch (error: any) {
message.error(error?.response?.data?.message || error?.message || '调拨失败')
} finally {
redirectingTaskItem.value = false
}
}
// 第三方数据库配置相关
const thirdPartyDbConfigs = ref<ThirdPartyDbConfig[]>([])
const loadingThirdPartyDb = ref(false)