qiangdanxitong/backend/src/main/java/com/designhub/service/TaskApprovalService.java
Your Name fac68333a7 feat: 优化任务时效计算逻辑 - 从接单时开始计算
问题:
- 任务时效(deadline)应该从设计师接单时开始计算
- 而不是从任务创建或审核通过时计算

修改内容:

1. TaskService.grabTask()
   - 无论任务之前是否有deadline
   - 抢单时都重新计算deadline
   - 从接单时间(now)开始 + estimatedHours
   - 添加详细日志记录

2. TaskApprovalService.approveReceivingLevel()
   - 接单部门负责人指定设计师时
   - 重新计算deadline
   - 从指定时间(now)开始 + estimatedHours
   - 添加详细日志记录

3. TaskApprovalService.approveDemandLevel()
   - assigned模式下已有正确逻辑
   - 从分配时间开始计算deadline
   - 保持不变

计算逻辑:
- deadline = 接单时间 + estimatedHours(小时)
- 如果estimatedHours为空或0,不设置deadline
- 记录详细日志便于排查

影响范围:
- 设计师抢单:✅ 重新计算
- 负责人指派:✅ 重新计算
- 需求部门指定:✅ 已有逻辑
- 发布到大厅:❌ 不设置(等待接单后计算)

示例:
- 任务预估8小时
- 设计师12:00接单
- deadline = 12:00 + 8小时 = 20:00
2026-01-03 11:54:59 +08:00

608 lines
26 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;
private final DingTalkService dingTalkService;
/**
* 需求部门负责人审核通过
*/
@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"); // 指定接单直接进入进行中状态
LocalDateTime now = LocalDateTime.now();
task.setStartTime(now);
task.setActualStartTime(now);
// 重新计算deadline:从指定接单时间开始计算
if (task.getEstimatedHours() != null) {
int hours = task.getEstimatedHours().intValue();
if (hours > 0) {
task.setDeadline(now.plusHours(hours));
log.info("任务deadline已重新计算:从指定接单时间 {} 开始,预估 {} 小时,截止时间 {}",
now, hours, task.getDeadline());
}
}
} 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) {
log.info("任务【{}】已指定给设计师", task.getTitle());
} else {
log.info("任务【{}】已发布到任务大厅,准备发送钉钉群通知", task.getTitle());
// 任务发布到大厅,发送钉钉群机器人通知
try {
String title = "🎯 新任务发布";
StringBuilder content = new StringBuilder();
content.append("**任务标题:**").append(task.getTitle()).append("\n\n");
content.append("**任务编号:**").append(task.getTaskNo()).append("\n\n");
content.append("**积分:**").append(task.getScore()).append(" 分\n\n");
if (task.getBrandName() != null) {
content.append("**品牌:**").append(task.getBrandName()).append("\n\n");
}
if (task.getCategoryName() != null) {
content.append("**类目:**").append(task.getCategoryName()).append("\n\n");
}
if (task.getPlatformName() != null) {
content.append("**平台:**").append(task.getPlatformName()).append("\n\n");
}
content.append("**状态:**待接单\n\n");
content.append("---\n\n");
content.append("💡 赶快去任务大厅抢单吧!");
log.info("准备调用钉钉机器人发送消息,title={}", title);
boolean success = dingTalkService.sendRobotMessage(title, content.toString());
if (success) {
log.info("任务【{}】钉钉群通知发送成功", task.getTitle());
} else {
log.warn("任务【{}】钉钉群通知发送失败(返回false)", task.getTitle());
}
} catch (Exception e) {
log.error("发送钉钉群通知异常:taskId={}, error={}", taskId, e.getMessage(), e);
// 通知发送失败不影响主流程
}
}
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);
}
}
}