qiangdanxitong/backend/src/main/java/com/designhub/service/TaskApprovalService.java
Your Name 371cfd5b2a feat: 为任务卡片添加子任务进度统计功能
新增功能:
1. Task实体新增字段:
   - subTaskTotal: 子任务总数
   - subTaskCompleted: 已完成子任务数
   - subTaskInProgress: 进行中子任务数
   - subTaskPending: 待接单子任务数
   - subTasks: 子任务列表(包含处理人信息)

2. TaskService新增方法:
   - fillSubTaskStats(): 填充流水线任务的子任务统计信息
   - 自动在getMyPublishedTasks和getMyReceivedTasks中调用
   - 统计各状态子任务数量
   - 自动填充处理人信息

3. 同时修复流水线任务主任务状态同步问题:
   - 设计师接单第一个子任务时,主任务状态更新为in_progress
   - 确保需求方能正确看到任务进度

影响范围:
- 需求方查看"我发起的"任务时,可以看到子任务进度
- 设计师查看接收的任务时,可以看到子任务详情
- 为后续前端显示提供数据支持
2026-01-03 11:32:16 +08:00

565 lines
24 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.designhub.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.designhub.dto.PipelineTaskDTO;
import com.designhub.dto.TaskApprovalDTO;
import com.designhub.dto.TaskUpdateDTO;
import com.designhub.entity.*;
import com.designhub.exception.BusinessException;
import com.designhub.mapper.*;
import com.designhub.common.ResultCode;
import com.designhub.entity.Brand;
import com.designhub.entity.Category;
import com.designhub.entity.Platform;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 任务审核服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class TaskApprovalService {
private final TaskMapper taskMapper;
private final TaskApprovalLogMapper approvalLogMapper;
private final DepartmentMapper departmentMapper;
private final UserMapper userMapper;
private final BrandMapper brandMapper;
private final CategoryMapper categoryMapper;
private final PlatformMapper platformMapper;
private final NotificationService notificationService;
private final TeamScorePoolService teamScorePoolService;
private final TaskService taskService;
private final ConfigService configService;
private final ObjectMapper objectMapper;
/**
* 需求部门负责人审核通过
*/
@Transactional(rollbackFor = Exception.class)
public void approveDemandLevel(Long taskId, Long approverId, TaskApprovalDTO dto) {
// 1. 验证任务和权限
Task task = getTaskForApproval(taskId, "pending_demand_approval");
validateApprover(task, approverId, "demand_leader");
// 2. 应用任务修改(如果有)
if (dto.getTaskUpdates() != null) {
applyTaskUpdates(task, dto.getTaskUpdates());
logApproval(taskId, approverId, 1, "modify", dto.getComment());
}
// 3. 根据发布类型决定是否需要接单部门负责人审核
String releaseType = task.getReleaseType();
boolean needReceivingApproval = "default".equals(releaseType);
if (needReceivingApproval) {
// default模式:需要接单部门负责人审核
Long receivingLeaderId = determineReceivingLeader(task);
if (receivingLeaderId == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "无法确定接单部门负责人,请在系统设置中为接单部门配置负责人");
}
// 更新任务状态为待接单部门审核
task.setApprovalStatus("pending_receiving_approval");
task.setDemandApprovalTime(LocalDateTime.now());
task.setCurrentApproverId(receivingLeaderId);
task.setCurrentApproverRole("receiving_leader");
taskMapper.updateById(task);
// 记录审核日志
logApproval(taskId, approverId, 1, "approve", dto.getComment());
// 发送通知给接单部门负责人
User receivingLeader = userMapper.selectById(receivingLeaderId);
if (receivingLeader != null) {
notificationService.sendNotification(
receivingLeaderId,
"新任务待审核",
String.format("任务【%s】需要您审核并分配设计师", task.getTitle()),
"task_approval",
taskId
);
}
log.info("需求部门负责人审核通过,任务ID:{},审核人:{},等待接单部门负责人审核", taskId, approverId);
} else {
// recruitment或assigned模式:不需要接单部门负责人审核,直接发布
task.setDemandApprovalTime(LocalDateTime.now());
// 判断是否为流水线任务
if (task.getIsPipeline() != null && task.getIsPipeline() == 1) {
// 流水线任务:拆分子任务
splitIntoPipelineSubTasks(task);
// splitIntoPipelineSubTasks方法会根据releaseType设置任务状态
} else {
// 普通任务处理
if ("assigned".equals(releaseType) && task.getReceiverId() != null) {
// assigned模式:如果指定了接收人,直接分配给该设计师
User receiver = userMapper.selectById(task.getReceiverId());
if (receiver != null && receiver.getDeptId() != null) {
task.setReceiverDeptId(receiver.getDeptId());
task.setStatus("in_progress");
task.setStartTime(LocalDateTime.now());
task.setActualStartTime(LocalDateTime.now());
// 重新计算截止时间
if (task.getEstimatedHours() != null) {
int hours = task.getEstimatedHours().intValue();
if (hours > 0) {
task.setDeadline(LocalDateTime.now().plusHours(hours));
}
}
// 发送通知给被指定的设计师
notificationService.sendNotification(
task.getReceiverId(),
"新任务分配",
String.format("任务【%s】已分配给您", task.getTitle()),
"task_assigned",
taskId
);
log.info("assigned模式任务已直接分配给设计师:taskId={}, receiverId={}", taskId, task.getReceiverId());
} else {
throw new BusinessException(ResultCode.PARAM_ERROR, "指定的接收人不存在或部门信息不完整");
}
} else {
// recruitment模式:发布到任务大厅
task.setStatus("pending");
}
}
// 更新审核状态为已通过
task.setApprovalStatus("approved");
task.setCurrentApproverId(null);
task.setCurrentApproverRole(null);
taskMapper.updateById(task);
// 扣除部门积分
try {
teamScorePoolService.deductScore(task.getPublisherDeptId(), task.getScore());
} catch (Exception e) {
log.error("扣除部门积分失败", e);
throw new BusinessException(ResultCode.ERROR, "扣除部门积分失败:" + e.getMessage());
}
// 记录审核日志
logApproval(taskId, approverId, 1, "approve", dto.getComment());
log.info("需求部门负责人审核通过,任务ID:{},审核人:{},发布类型:{},已直接发布", taskId, approverId, releaseType);
}
}
/**
* 需求部门负责人驳回
*/
@Transactional(rollbackFor = Exception.class)
public void rejectByDemandLeader(Long taskId, Long approverId, String reason) {
Task task = getTaskForApproval(taskId, "pending_demand_approval");
validateApprover(task, approverId, "demand_leader");
task.setApprovalStatus("rejected_by_demand");
task.setStatus("draft");
task.setCurrentApproverId(null);
task.setCurrentApproverRole(null);
taskMapper.updateById(task);
logApproval(taskId, approverId, 1, "reject", reason);
// 通知发起人
notificationService.sendNotification(
task.getPublisherId(),
"任务已被驳回",
String.format("您的任务【%s】被需求部门负责人驳回,原因:%s", task.getTitle(), reason),
"task_reject",
taskId
);
log.info("需求部门负责人驳回任务,任务ID:{},审核人:{}", taskId, approverId);
}
/**
* 接单部门负责人审核通过
* @param taskId 任务ID
* @param approverId 审核人ID
* @param dto 审核数据(包含备注和任务修改)
* @param assignedReceiverId 指定的接收人ID(可选)
*/
@Transactional(rollbackFor = Exception.class)
public void approveReceivingLevel(Long taskId, Long approverId, TaskApprovalDTO dto, Long assignedReceiverId) {
Task task = getTaskForApproval(taskId, "pending_receiving_approval");
validateApprover(task, approverId, "receiving_leader");
// 应用任务修改(如果有)
if (dto.getTaskUpdates() != null) {
applyTaskUpdates(task, dto.getTaskUpdates());
logApproval(taskId, approverId, 2, "modify", dto.getComment());
}
// 如果指定了接收人,验证接收人是否存在并设置
if (assignedReceiverId != null) {
com.designhub.entity.User receiver = userMapper.selectById(assignedReceiverId);
if (receiver == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "指定的接收人不存在");
}
task.setReceiverId(assignedReceiverId);
task.setReceiverDeptId(receiver.getDeptId());
task.setReleaseType("assigned"); // 设置为指定模式
log.info("任务【{}】已指定给设计师【{}】", task.getTitle(), receiver.getRealName());
}
// 判断是否为流水线任务
if (task.getIsPipeline() != null && task.getIsPipeline() == 1) {
// 流水线任务:拆分子任务
splitIntoPipelineSubTasks(task);
} else {
// 普通任务:根据是否指定接收人设置状态
if (assignedReceiverId != null) {
task.setStatus("in_progress"); // 指定接单直接进入进行中状态
task.setStartTime(LocalDateTime.now());
task.setActualStartTime(LocalDateTime.now());
} else {
task.setStatus("pending"); // 未指定则发布到大厅
}
}
// 更新审核状态
task.setApprovalStatus("approved");
task.setReceivingApprovalTime(LocalDateTime.now());
task.setCurrentApproverId(null);
task.setCurrentApproverRole(null);
taskMapper.updateById(task);
// 扣除部门积分
try {
teamScorePoolService.deductScore(task.getPublisherDeptId(), task.getScore());
} catch (Exception e) {
log.error("扣除部门积分失败", e);
throw new BusinessException(ResultCode.ERROR, "扣除部门积分失败:" + e.getMessage());
}
// 记录审核日志
String logComment = dto.getComment();
if (assignedReceiverId != null) {
com.designhub.entity.User receiver = userMapper.selectById(assignedReceiverId);
logComment = (logComment != null ? logComment + ";" : "") +
"已指定给设计师:" + (receiver != null ? receiver.getRealName() : assignedReceiverId);
}
logApproval(taskId, approverId, 2, "approve", logComment);
// 如果指定了接收人,发送通知
if (assignedReceiverId != null) {
// TODO: 通知指定的设计师
log.info("任务【{}】已指定给设计师", task.getTitle());
} else {
// TODO: 通知任务大厅(所有接单部门成员)
log.info("任务【{}】已发布到任务大厅", task.getTitle());
}
log.info("接单部门负责人审核通过,任务ID:{},审核人:{},指定接收人:{}",
taskId, approverId, assignedReceiverId);
}
/**
* 接单部门负责人审核通过(重载方法,保持向后兼容)
*/
@Transactional(rollbackFor = Exception.class)
public void approveReceivingLevel(Long taskId, Long approverId, TaskApprovalDTO dto) {
approveReceivingLevel(taskId, approverId, dto, null);
}
/**
* 接单部门负责人驳回
*/
@Transactional(rollbackFor = Exception.class)
public void rejectByReceivingLeader(Long taskId, Long approverId, String reason) {
Task task = getTaskForApproval(taskId, "pending_receiving_approval");
validateApprover(task, approverId, "receiving_leader");
// 退回给需求部门负责人
Department demandDept = departmentMapper.selectById(task.getPublisherDeptId());
if (demandDept == null || demandDept.getLeaderId() == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "需求部门负责人不存在");
}
task.setApprovalStatus("rejected_by_receiving");
task.setStatus("draft");
task.setCurrentApproverId(demandDept.getLeaderId());
task.setCurrentApproverRole("demand_leader");
taskMapper.updateById(task);
logApproval(taskId, approverId, 2, "reject", reason);
// 通知需求部门负责人
notificationService.sendNotification(
demandDept.getLeaderId(),
"任务已被驳回",
String.format("任务【%s】被接单部门负责人驳回,原因:%s", task.getTitle(), reason),
"task_reject",
taskId
);
log.info("接单部门负责人驳回任务,任务ID:{},审核人:{}", taskId, approverId);
}
/**
* 获取待审核任务列表
*/
public List<Task> getPendingApprovalTasks(Long userId) {
List<Task> tasks = taskMapper.selectList(
new LambdaQueryWrapper<Task>()
.eq(Task::getCurrentApproverId, userId)
.in(Task::getApprovalStatus, "pending_demand_approval", "pending_receiving_approval")
.orderByDesc(Task::getCreateTime)
);
// 填充关联信息
for (Task task : tasks) {
// 填充发起人姓名
if (task.getPublisherId() != null) {
User publisher = userMapper.selectById(task.getPublisherId());
if (publisher != null) {
task.setPublisherName(publisher.getUsername());
}
}
// 填充发起部门名称
if (task.getPublisherDeptId() != null) {
Department dept = departmentMapper.selectById(task.getPublisherDeptId());
if (dept != null) {
task.setPublisherDeptName(dept.getDeptName());
}
}
// 填充品牌名称
if (task.getBrandId() != null) {
Brand brand = brandMapper.selectById(task.getBrandId());
if (brand != null) {
// Brand实体类中name字段映射到brand_name,使用getName()
task.setBrandName(brand.getName());
}
}
// 填充类目名称
if (task.getCategoryId() != null) {
Category category = categoryMapper.selectById(task.getCategoryId());
if (category != null) {
// Category实体类中name字段映射到category_name,使用getName()
task.setCategoryName(category.getName());
}
}
// 填充平台名称
if (task.getPlatformId() != null) {
Platform platform = platformMapper.selectById(task.getPlatformId());
if (platform != null) {
// Platform实体类中name字段映射到platform_name,使用getName()
task.setPlatformName(platform.getName());
}
}
// 填充当前审核人姓名
if (task.getCurrentApproverId() != null) {
User approver = userMapper.selectById(task.getCurrentApproverId());
if (approver != null) {
task.setCurrentApproverName(approver.getRealName() != null ? approver.getRealName() : approver.getUsername());
}
}
}
return tasks;
}
/**
* 获取任务并验证审核状态
*/
private Task getTaskForApproval(Long taskId, String expectedStatus) {
Task task = taskMapper.selectById(taskId);
if (task == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
}
if (!expectedStatus.equals(task.getApprovalStatus())) {
throw new BusinessException(ResultCode.TASK_STATUS_ERROR,
"任务当前状态不允许此操作,当前状态:" + task.getApprovalStatus());
}
return task;
}
/**
* 验证审核人权限
*/
private void validateApprover(Task task, Long approverId, String expectedRole) {
if (!approverId.equals(task.getCurrentApproverId())) {
throw new BusinessException(ResultCode.FORBIDDEN, "您无权审核此任务");
}
if (!expectedRole.equals(task.getCurrentApproverRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "审核角色不匹配");
}
}
/**
* 确定接单部门负责人
*/
private Long determineReceivingLeader(Task task) {
// 根据任务类型的allowedDeptIds确定接单部门
// 简化版本:取第一个任务类型的第一个允许部门
String taskTypeIds = task.getTaskTypeIds();
if (taskTypeIds == null || taskTypeIds.isEmpty()) {
return null;
}
String[] typeIdArray = taskTypeIds.split(",");
for (String typeIdStr : typeIdArray) {
try {
Long typeId = Long.parseLong(typeIdStr.trim());
DemandType demandType = configService.getDemandTypeById(typeId);
if (demandType != null && demandType.getAllowedDeptIds() != null) {
String[] deptIds = demandType.getAllowedDeptIds().split(",");
if (deptIds.length > 0) {
Long deptId = Long.parseLong(deptIds[0].trim());
Department dept = departmentMapper.selectById(deptId);
if (dept != null && dept.getLeaderId() != null) {
return dept.getLeaderId();
}
}
}
} catch (Exception e) {
log.error("解析任务类型或部门ID失败", e);
}
}
return null;
}
/**
* 应用任务更新
*/
private void applyTaskUpdates(Task task, TaskUpdateDTO updates) {
if (updates.getTitle() != null) {
task.setTitle(updates.getTitle());
}
if (updates.getTaskSubType() != null) {
task.setTaskSubType(updates.getTaskSubType());
}
if (updates.getBrandId() != null) {
task.setBrandId(updates.getBrandId());
}
if (updates.getPlatformId() != null) {
task.setPlatformId(updates.getPlatformId());
}
if (updates.getCategoryId() != null) {
task.setCategoryId(updates.getCategoryId());
}
if (updates.getSpuInfo() != null) {
task.setSpuInfo(updates.getSpuInfo());
}
if (updates.getRemark() != null) {
task.setRemark(updates.getRemark());
}
if (updates.getAttachmentUrl() != null) {
task.setAttachmentUrl(updates.getAttachmentUrl());
}
// 如果修改了任务类型和数量,需要重新计算积分
if (updates.getTaskTypes() != null && !updates.getTaskTypes().isEmpty()) {
String taskTypeIds = updates.getTaskTypes().stream()
.map(t -> String.valueOf(t.getTaskTypeId()))
.collect(Collectors.joining(","));
task.setTaskTypeIds(taskTypeIds);
// 如果只有一个任务类型,更新quantity字段
if (updates.getTaskTypes().size() == 1) {
task.setQuantity(updates.getTaskTypes().get(0).getQuantity());
}
// 重新计算积分
int totalScore = taskService.calculateTotalScore(updates.getTaskTypes());
task.setScore(totalScore);
}
}
/**
* 拆分为流水线子任务
*/
private void splitIntoPipelineSubTasks(Task task) {
try {
// 构建流水线任务DTO
PipelineTaskDTO dto = new PipelineTaskDTO();
dto.setTitle(task.getTitle());
dto.setTaskSubType(task.getTaskSubType());
dto.setBrandId(task.getBrandId());
dto.setPlatformId(task.getPlatformId());
dto.setCategoryId(task.getCategoryId());
dto.setSpuInfo(task.getSpuInfo());
dto.setRemark(task.getRemark());
dto.setAttachmentUrl(task.getAttachmentUrl());
dto.setPipelineMode(task.getPipelineMode());
dto.setReleaseType(task.getReleaseType()); // 传递发布类型
// 解析任务类型
String[] taskTypeIds = task.getTaskTypeIds().split(",");
List<PipelineTaskDTO.TaskItemDTO> items = new java.util.ArrayList<>();
// 简化版本:所有任务类型使用相同的数量(task.quantity)
int quantity = task.getQuantity() != null ? task.getQuantity() : 1;
for (String taskTypeIdStr : taskTypeIds) {
PipelineTaskDTO.TaskItemDTO item = new PipelineTaskDTO.TaskItemDTO();
item.setTaskTypeId(Long.parseLong(taskTypeIdStr.trim()));
item.setQuantity(quantity);
items.add(item);
}
dto.setItems(items);
// 调用TaskService创建流水线子任务
taskService.createPipelineTaskItems(task, dto);
// 根据发布类型设置主任务状态
if ("recruitment".equals(task.getReleaseType())) {
// recruitment模式:发布到任务大厅
task.setStatus("pending");
} else if ("assigned".equals(task.getReleaseType()) && task.getReceiverId() != null) {
// assigned模式:如果指定了接收人,状态已经是in_progress(在approveDemandLevel中已设置)
// 这里不需要再次设置
} else {
// default模式或其他模式:流水线任务的主任务状态设置为pending
// 子任务会根据is_locked字段控制可见性
task.setStatus("pending");
}
} catch (Exception e) {
log.error("拆分流水线任务失败", e);
throw new BusinessException(ResultCode.ERROR, "拆分流水线任务失败:" + e.getMessage());
}
}
/**
* 记录审核日志
*/
private void logApproval(Long taskId, Long approverId, Integer level, String action, String comment) {
try {
User approver = userMapper.selectById(approverId);
TaskApprovalLog log = new TaskApprovalLog();
log.setTaskId(taskId);
log.setApprovalLevel(level);
log.setApproverId(approverId);
log.setApproverName(approver != null ? approver.getRealName() : null);
log.setAction(action);
log.setComment(comment);
log.setCreateTime(LocalDateTime.now());
approvalLogMapper.insert(log);
} catch (Exception e) {
this.log.error("记录审核日志失败", e);
}
}
}