qiangdanxitong/backend/src/main/java/com/designhub/service/TaskApprovalService.java
Your Name 1d66f0d7f5 修复子任务积分计算错误的bug
问题:当子任务包含多个任务类型时,updateTaskItemsQuantityAndScore方法
只用第一个任务类型的单价计算总积分,导致积分显示错误。

解决:
1. 先更新typeDetails JSON,再从更新后的typeDetails中重新计算总积分
2. 新增calculateTotalScoreFromTypeDetails方法从JSON中累加所有类型积分
3. 新增calculateTotalQuantityFromTypeDetails方法计算总数量
4. 同时更新平均单价以保持数据一致性
2026-01-27 20:57:28 +08:00

1549 lines
74 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.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 任务审核服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class TaskApprovalService {
private final TaskMapper taskMapper;
private final TaskItemMapper taskItemMapper;
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;
@org.springframework.beans.factory.annotation.Autowired
@org.springframework.context.annotation.Lazy
private TaskService taskService;
private final ConfigService configService;
private final ObjectMapper objectMapper;
private final DingTalkService dingTalkService;
private final com.designhub.util.WorkHoursCalculator workHoursCalculator;
// ==================== 一级需求部门任务审批 ====================
/**
* 审核一级需求部门任务(需求部门负责人审核)
* @param taskId 任务ID
* @param approverId 审核人ID(需求部门负责人)
* @param completerId 指定的明细完善人ID
* @param comment 审核备注
*/
@Transactional(rollbackFor = Exception.class)
public void approvePrimaryTask(Long taskId, Long approverId, Long completerId, String comment) {
// 1. 验证任务和权限
Task task = getTaskForApproval(taskId, "pending_primary_approval");
validateApprover(task, approverId, "demand_leader");
// 2. 验证完善人
if (completerId == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "请指定明细完善人");
}
User completer = userMapper.selectById(completerId);
if (completer == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "指定的完善人不存在");
}
// 3. 更新任务状态为待完善明细
task.setApprovalStatus("pending_detail_completion");
task.setDetailCompleterId(completerId);
task.setDemandApprovalTime(LocalDateTime.now());
task.setCurrentApproverId(completerId);
task.setCurrentApproverRole("detail_completer");
taskMapper.updateById(task);
// 4. 记录审核日志
logApproval(taskId, approverId, 1, "approve_primary", comment);
// 5. 发送通知给完善人
notificationService.sendNotification(
completerId,
"新任务待完善明细",
String.format("任务【%s】需要您完善任务明细", task.getTitle()),
"task_detail_completion",
taskId
);
// 6. 发送通知给一级部门发起人
if (task.getPrimaryPublisherId() != null) {
notificationService.sendNotification(
task.getPrimaryPublisherId(),
"任务审核通过",
String.format("您发起的任务【%s】已通过审核,正在等待明细完善", task.getTitle()),
"task_approved",
taskId
);
}
// 7. 发送钉钉通知给完善人
try {
if (completer.getDingUserId() != null) {
String title = "新任务待完善明细";
StringBuilder content = new StringBuilder();
content.append("任务名称:").append(task.getTitle()).append("\n");
content.append("任务编号:").append(task.getTaskNo()).append("\n");
content.append("请尽快完善任务明细");
dingTalkService.sendWorkNotification(
java.util.Collections.singletonList(completer.getDingUserId()),
title,
content.toString()
);
}
} catch (Exception e) {
log.error("发送钉钉通知失败", e);
}
log.info("一级需求任务审核通过,任务ID:{},审核人:{},完善人:{}", taskId, approverId, completerId);
}
/**
* 驳回一级需求部门任务
*/
@Transactional(rollbackFor = Exception.class)
public void rejectPrimaryTask(Long taskId, Long approverId, String reason) {
Task task = getTaskForApproval(taskId, "pending_primary_approval");
validateApprover(task, approverId, "demand_leader");
task.setApprovalStatus("rejected_by_demand");
task.setStatus("draft");
task.setRejectReason(reason);
task.setCurrentApproverId(null);
task.setCurrentApproverRole(null);
taskMapper.updateById(task);
logApproval(taskId, approverId, 1, "reject_primary", reason);
// 通知一级部门发起人
if (task.getPrimaryPublisherId() != null) {
notificationService.sendNotification(
task.getPrimaryPublisherId(),
"任务已被驳回",
String.format("您发起的任务【%s】被驳回,原因:%s", task.getTitle(), reason),
"task_reject",
taskId
);
// 发送钉钉通知
try {
User publisher = userMapper.selectById(task.getPrimaryPublisherId());
if (publisher != null && publisher.getDingUserId() != null) {
String title = "任务已被驳回";
String content = String.format("任务【%s】被驳回,原因:%s", task.getTitle(), reason);
dingTalkService.sendWorkNotification(
java.util.Collections.singletonList(publisher.getDingUserId()),
title,
content
);
}
} catch (Exception e) {
log.error("发送钉钉通知失败", e);
}
}
log.info("一级需求任务被驳回,任务ID:{},审核人:{},原因:{}", taskId, approverId, reason);
}
/**
* 完善任务明细并提交
* @param taskId 任务ID
* @param completerId 完善人ID
* @param taskItems 任务明细(子任务列表)
*/
@Transactional(rollbackFor = Exception.class)
public void completeTaskDetail(Long taskId, Long completerId, List<PipelineTaskDTO.TaskItemDTO> taskItems) {
Task task = getTaskForApproval(taskId, "pending_detail_completion");
// 验证完善人权限
if (!completerId.equals(task.getDetailCompleterId())) {
throw new BusinessException(ResultCode.FORBIDDEN, "您不是该任务的指定完善人");
}
// 验证任务明细
if (taskItems == null || taskItems.isEmpty()) {
throw new BusinessException(ResultCode.PARAM_ERROR, "请填写任务明细");
}
// 设置任务为流水线任务
task.setIsPipeline(1);
task.setPipelineMode("sequential"); // 默认顺序模式
// 计算总积分和工时(一级任务的积分已经由一级部门设置,这里不重新计算)
// 但需要验证明细的积分总和不超过预估积分
// 标记明细已完善
task.setDetailCompleted(1);
// 更新发布人为完善人(这样后续审核流程中发布人是需求部门的人)
task.setPublisherId(completerId);
User completer = userMapper.selectById(completerId);
if (completer != null && completer.getDeptId() != null) {
task.setPublisherDeptId(completer.getDeptId());
}
// 进入原有需求部门审核流程
task.setApprovalStatus("pending_demand_approval");
Department demandDept = departmentMapper.selectById(task.getPublisherDeptId());
if (demandDept != null && demandDept.getLeaderId() != null) {
task.setCurrentApproverId(demandDept.getLeaderId());
task.setCurrentApproverRole("demand_leader");
}
taskMapper.updateById(task);
// 创建子任务
taskService.createTaskItemsFromDTO(task.getId(), taskItems);
// 记录日志
logApproval(taskId, completerId, 0, "complete_detail", "任务明细已完善");
// 通知一级部门发起人
if (task.getPrimaryPublisherId() != null) {
notificationService.sendNotification(
task.getPrimaryPublisherId(),
"任务明细已完善",
String.format("任务【%s】明细已完善,进入正常审核流程", task.getTitle()),
"task_detail_completed",
taskId
);
}
// 通知需求部门负责人
if (demandDept != null && demandDept.getLeaderId() != null) {
notificationService.sendNotification(
demandDept.getLeaderId(),
"新任务待审核",
String.format("任务【%s】需要您审核", task.getTitle()),
"task_approval",
taskId
);
}
log.info("一级需求任务明细已完善,任务ID:{},完善人:{}", taskId, completerId);
}
/**
* 需求部门负责人审核通过
*/
@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();
// 确定接单部门负责人
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);
// 🔥 如果是一级需求部门任务且是流水线任务,将子任务状态更新为pending_approval
// 这样接单部门负责人才能看到子任务组进行审核
if (task.getIsPrimaryTask() != null && task.getIsPrimaryTask() == 1
&& task.getIsPipeline() != null && task.getIsPipeline() == 1) {
String pipelineMode = task.getPipelineMode();
if (pipelineMode == null || pipelineMode.isEmpty()) {
pipelineMode = "sequential"; // 默认顺序模式
}
List<TaskItem> itemsToUpdate;
if ("sequential".equals(pipelineMode)) {
// 顺序模式:只更新第一个部门的子任务(status='pending' 且 isLocked=0)
itemsToUpdate = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, taskId)
.eq(TaskItem::getStatus, "pending")
.eq(TaskItem::getIsLocked, 0)
);
log.info("顺序模式:更新第一个部门的子任务状态");
} else {
// 并行模式:更新所有部门的子任务(status='pending' 且 isLocked=0)
itemsToUpdate = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, taskId)
.eq(TaskItem::getStatus, "pending")
.eq(TaskItem::getIsLocked, 0)
);
log.info("并行模式:更新所有部门的子任务状态");
}
// 将子任务状态更新为pending_approval(待审核)
for (TaskItem item : itemsToUpdate) {
item.setStatus("pending_approval");
item.setUpdateTime(LocalDateTime.now());
taskItemMapper.updateById(item);
log.info("一级需求任务子任务 {} 状态已更新为pending_approval", item.getItemNo());
}
if (!itemsToUpdate.isEmpty()) {
log.info("一级需求任务 {} 共 {} 个子任务已设置为待审核状态(模式:{})",
taskId, itemsToUpdate.size(), pipelineMode);
}
}
// 记录审核日志
logApproval(taskId, approverId, 1, "approve", dto.getComment());
// 发送通知给接单部门负责人
User receivingLeader = userMapper.selectById(receivingLeaderId);
if (receivingLeader != null) {
// 根据是否预先指定了设计师,调整通知内容
String notificationContent;
if ("assigned".equals(releaseType) && task.getReceiverId() != null) {
User preAssignedDesigner = userMapper.selectById(task.getReceiverId());
String designerName = preAssignedDesigner != null ? preAssignedDesigner.getRealName() : "未知";
notificationContent = String.format("任务【%s】需要您审核确认(已预先指定设计师:%s)", task.getTitle(), designerName);
} else {
notificationContent = String.format("任务【%s】需要您审核并分配设计师", task.getTitle());
}
notificationService.sendNotification(
receivingLeaderId,
"新任务待审核",
notificationContent,
"task_approval",
taskId
);
}
// 🔥 一级需求任务:通知一级发起人需求部门审核已通过
if (task.getPrimaryPublisherId() != null) {
notificationService.sendNotification(
task.getPrimaryPublisherId(),
"任务审核进度更新",
String.format("您发起的任务【%s】已通过需求部门审核,进入接单部门审核阶段", task.getTitle()),
"task_approval_progress",
taskId
);
}
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.setRejectReason(reason); // 保存驳回原因
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
);
// 🔥 一级需求任务:同时通知一级发起人
if (task.getPrimaryPublisherId() != null
&& !task.getPrimaryPublisherId().equals(task.getPublisherId())) {
notificationService.sendNotification(
task.getPrimaryPublisherId(),
"任务已被驳回",
String.format("您发起的任务【%s】被需求部门负责人驳回,原因:%s", task.getTitle(), reason),
"task_reject",
taskId
);
}
// 如果任务有指定的处理人,发送群通知告知驳回
if (task.getReceiverId() != null) {
try {
com.designhub.entity.User receiver = userMapper.selectById(task.getReceiverId());
if (receiver != null) {
String title = "任务已驳回";
StringBuilder content = new StringBuilder();
content.append("-------------------------------- \n");
content.append(" 【 任 务 已 驳 回 】 \n");
content.append("-------------------------------- \n\n");
content.append("任务名称:").append(task.getTitle()).append(" \n");
content.append("任务编号:").append(task.getTaskNo()).append(" \n");
content.append("原处理人:").append(receiver.getRealName()).append(" \n");
content.append("驳回原因:").append(reason).append(" \n");
content.append(" \n");
content.append("--------------------------------");
java.util.List<String> atMobiles = receiver.getPhone() != null ?
java.util.Arrays.asList(receiver.getPhone()) : null;
dingTalkService.sendRobotMessage(title, content.toString(), atMobiles);
}
} catch (Exception e) {
log.error("发送任务驳回群通知失败", e);
}
}
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());
}
// 检查任务是否已预先指定了设计师
boolean isPreAssigned = "assigned".equals(task.getReleaseType()) && task.getReceiverId() != null;
Long finalReceiverId = null;
if (isPreAssigned) {
// 任务已预先指定设计师,不允许修改
if (assignedReceiverId != null && !assignedReceiverId.equals(task.getReceiverId())) {
log.warn("任务【{}】已预先指定设计师,忽略接单部门负责人的指定操作", task.getTitle());
}
finalReceiverId = task.getReceiverId();
log.info("任务【{}】使用预先指定的设计师: {}", task.getTitle(), finalReceiverId);
} else 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"); // 设置为指定模式
finalReceiverId = assignedReceiverId;
log.info("任务【{}】已指定给设计师【{}】", task.getTitle(), receiver.getRealName());
}
// 判断是否为流水线任务
if (task.getIsPipeline() != null && task.getIsPipeline() == 1) {
// 流水线任务:先保存主任务的receiverId(如果已设置),确保updatePipelineSubTasksForAssigned能读取到
if (finalReceiverId != null) {
task.setStatus("pending"); // 临时状态,splitIntoPipelineSubTasks会更新
taskMapper.updateById(task);
log.info("流水线任务主任务receiverId已保存:taskId={}, receiverId={}", task.getId(), finalReceiverId);
}
// 拆分子任务
splitIntoPipelineSubTasks(task);
} else {
// 普通任务:根据是否有接收人设置状态
if (finalReceiverId != null) {
// 如果接收人部门ID还没设置,需要设置
if (task.getReceiverDeptId() == null) {
com.designhub.entity.User receiver = userMapper.selectById(finalReceiverId);
if (receiver != null) {
task.setReceiverDeptId(receiver.getDeptId());
}
}
task.setStatus("in_progress"); // 指定接单直接进入进行中状态
LocalDateTime now = LocalDateTime.now();
task.setStartTime(now);
task.setActualStartTime(now);
// 重新计算deadline:从接单时间开始计算(使用工作时间计算)
if (task.getEstimatedHours() != null && task.getEstimatedHours().compareTo(java.math.BigDecimal.ZERO) > 0) {
task.setDeadline(workHoursCalculator.calculateDeadline(now, task.getEstimatedHours()));
log.info("任务deadline已重新计算:从接单时间 {} 开始,预估 {} 小时,截止时间 {}",
now, task.getEstimatedHours(), 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() != null ? task.getScore().intValue() : 0);
} catch (Exception e) {
log.error("扣除部门积分失败", e);
throw new BusinessException(ResultCode.ERROR, "扣除部门积分失败:" + e.getMessage());
}
// 记录审核日志
String logComment = dto.getComment();
if (finalReceiverId != null) {
com.designhub.entity.User receiver = userMapper.selectById(finalReceiverId);
String assignNote = isPreAssigned ? "(预先指定)" : "";
logComment = (logComment != null ? logComment + ";" : "") +
"已指定给设计师" + assignNote + ":" + (receiver != null ? receiver.getRealName() : finalReceiverId);
}
logApproval(taskId, approverId, 2, "approve", logComment);
// 🔥 一级需求任务:通知一级发起人任务已通过审核并发布
if (task.getPrimaryPublisherId() != null) {
String statusDesc = finalReceiverId != null ? "已指派给设计师" : "已发布到任务大厅";
notificationService.sendNotification(
task.getPrimaryPublisherId(),
"任务审核通过",
String.format("您发起的任务【%s】已通过全部审核,%s", task.getTitle(), statusDesc),
"task_approved",
taskId
);
log.info("已通知一级发起人 {} 任务审核通过", task.getPrimaryPublisherId());
}
// 发送钉钉通知
// 对于流水线任务,需要给第一个部门的所有子任务设计师发送通知
java.util.Set<Long> notifiedReceiverIds = new java.util.HashSet<>();
if (task.getIsPipeline() != null && task.getIsPipeline() == 1) {
// 流水线任务:查询第一个部门的所有子任务,给每个设计师发送通知
List<TaskItem> firstDeptItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, task.getId())
.eq(TaskItem::getStatus, "in_progress")
.isNotNull(TaskItem::getReceiverId)
);
for (TaskItem item : firstDeptItems) {
if (item.getReceiverId() != null && !notifiedReceiverIds.contains(item.getReceiverId())) {
notifiedReceiverIds.add(item.getReceiverId());
sendTaskAssignedNotification(task, item.getReceiverId(), isPreAssigned);
}
}
log.info("流水线任务【{}】已通知 {} 个设计师", task.getTitle(), notifiedReceiverIds.size());
} else if (finalReceiverId != null) {
// 普通任务:只通知一个设计师
sendTaskAssignedNotification(task, finalReceiverId, isPreAssigned);
notifiedReceiverIds.add(finalReceiverId);
log.info("普通任务【{}】已通知设计师: {}", task.getTitle(), finalReceiverId);
}
// 如果没有指定设计师(发布到大厅),发送群通知
if (notifiedReceiverIds.isEmpty()) {
log.info("任务【{}】已发布到任务大厅,准备发送钉钉群通知", task.getTitle());
// 任务发布到大厅,发送钉钉群机器人通知
// 注意:钉钉Markdown换行需要两个空格+换行符
try {
String title = "新任务发布";
StringBuilder content = new StringBuilder();
// 基本信息
content.append("**任务名称**:").append(task.getTitle()).append(" \n");
content.append("**任务编号**:").append(task.getTaskNo()).append(" \n");
// 任务详情
if (task.getBrandName() != null && !task.getBrandName().isEmpty()) {
content.append("**品牌**:").append(task.getBrandName()).append(" \n");
}
if (task.getPlatformName() != null && !task.getPlatformName().isEmpty()) {
content.append("**平台**:").append(task.getPlatformName()).append(" \n");
}
if (task.getSpuInfo() != null && !task.getSpuInfo().isEmpty()) {
content.append("**SPU**:").append(task.getSpuInfo()).append(" \n");
}
if (task.getTaskSubType() != null && !task.getTaskSubType().isEmpty()) {
content.append("**任务类型**:").append(task.getTaskSubType()).append(" \n");
}
content.append(" \n");
// 查询子任务获取任务类型明细
List<TaskItem> taskItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, task.getId())
.orderByAsc(TaskItem::getGroupNo)
);
// 时间格式化器:MM月dd日 HH:mm
DateTimeFormatter groupDeadlineFormatter = DateTimeFormatter.ofPattern("MM月dd日 HH:mm");
if (!taskItems.isEmpty()) {
content.append("**任务明细**: \n");
for (TaskItem item : taskItems) {
// 通过任务类型ID获取接单部门名称
String deptName = "未指定";
if (item.getTaskTypeId() != null) {
try {
DemandType demandType = configService.getDemandTypeById(item.getTaskTypeId());
if (demandType != null && demandType.getAllowedDeptIds() != null && !demandType.getAllowedDeptIds().isEmpty()) {
String[] deptIds = demandType.getAllowedDeptIds().split(",");
Long firstDeptId = Long.parseLong(deptIds[0].trim());
Department dept = departmentMapper.selectById(firstDeptId);
if (dept != null) {
deptName = dept.getDeptName();
}
}
} catch (Exception e) {
log.warn("获取部门名称失败: {}", e.getMessage());
}
}
// 格式化工时显示
String hoursDisplay = formatEstimatedHours(item.getEstimatedHours());
// 格式化截止时间
String groupDeadlineDisplay = "";
if (item.getDeadline() != null) {
groupDeadlineDisplay = " | 截止: " + item.getDeadline().format(groupDeadlineFormatter);
}
content.append(" \n");
content.append("▸ **").append(deptName).append("** | ")
.append(item.getTotalScore()).append("分 | ")
.append(hoursDisplay).append(groupDeadlineDisplay).append(" \n");
// 解析并显示任务类型明细
if (item.getTypeDetails() != null && !item.getTypeDetails().isEmpty()) {
try {
@SuppressWarnings("unchecked")
List<java.util.Map<String, Object>> typeDetails =
objectMapper.readValue(item.getTypeDetails(), List.class);
for (java.util.Map<String, Object> detail : typeDetails) {
String typeName = String.valueOf(detail.get("typeName"));
Object qtyObj = detail.get("quantity");
Object scoreObj = detail.get("totalScore");
int qty = qtyObj instanceof Number ? ((Number) qtyObj).intValue() : 1;
String score = scoreObj != null ? String.valueOf(scoreObj) : "0";
content.append("  └ ").append(typeName)
.append(" ×").append(qty)
.append(" = ").append(score).append("分 \n");
}
} catch (Exception e) {
log.warn("解析任务类型明细失败: {}", e.getMessage());
}
}
}
} else {
// 没有子任务时显示总积分和工时
content.append("**任务积分**:").append(task.getScore()).append(" 分 \n");
if (task.getEstimatedHours() != null) {
content.append("**预估工时**:").append(formatEstimatedHours(task.getEstimatedHours())).append(" \n");
}
}
content.append(" \n");
// 发起人信息
if (task.getPublisherName() != null && !task.getPublisherName().isEmpty()) {
content.append("**发起人**:").append(task.getPublisherName()).append(" \n");
}
if (task.getPublisherDeptName() != null && !task.getPublisherDeptName().isEmpty()) {
content.append("**发起部门**:").append(task.getPublisherDeptName()).append(" \n");
}
content.append(" \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());
Long targetApproverId = null;
String targetApproverRole = "demand_leader";
if (demandDept != null && demandDept.getLeaderId() != null) {
targetApproverId = demandDept.getLeaderId();
} else {
// 一级需求部门可能没有负责人,退回给发起人
targetApproverId = task.getPublisherId();
targetApproverRole = "publisher";
log.warn("需求部门{}没有负责人,任务退回给发起人{}", task.getPublisherDeptId(), task.getPublisherId());
}
task.setApprovalStatus("rejected_by_receiving");
task.setStatus("draft");
task.setRejectReason(reason); // 保存驳回原因
task.setCurrentApproverId(targetApproverId);
task.setCurrentApproverRole(targetApproverRole);
taskMapper.updateById(task);
logApproval(taskId, approverId, 2, "reject", reason);
// 1. 通知任务发起人(明细完善人)
notificationService.sendNotification(
task.getPublisherId(),
"任务已被驳回",
String.format("您的任务【%s】被接单部门负责人驳回,原因:%s,请修改后重新提交", task.getTitle(), reason),
"task_reject",
taskId
);
log.info("已通知任务发起人: publisherId={}", task.getPublisherId());
// 🔥 1.5 一级需求任务:同时通知一级发起人
if (task.getPrimaryPublisherId() != null
&& !task.getPrimaryPublisherId().equals(task.getPublisherId())) {
notificationService.sendNotification(
task.getPrimaryPublisherId(),
"任务已被驳回",
String.format("您发起的任务【%s】被接单部门负责人驳回,原因:%s", task.getTitle(), reason),
"task_reject",
taskId
);
log.info("已通知一级发起人: primaryPublisherId={}", task.getPrimaryPublisherId());
}
// 2. 如果需求部门负责人存在且不是发起人,也通知需求部门负责人
if (demandDept != null && demandDept.getLeaderId() != null && !demandDept.getLeaderId().equals(task.getPublisherId())) {
notificationService.sendNotification(
demandDept.getLeaderId(),
"任务已被驳回",
String.format("任务【%s】被接单部门负责人驳回,原因:%s", task.getTitle(), reason),
"task_reject",
taskId
);
log.info("已通知需求部门负责人: leaderId={}", demandDept.getLeaderId());
}
// 3. 如果任务有指定的处理人,发送群通知告知驳回
if (task.getReceiverId() != null) {
try {
com.designhub.entity.User receiver = userMapper.selectById(task.getReceiverId());
if (receiver != null) {
String title = "任务已驳回";
StringBuilder content = new StringBuilder();
content.append("-------------------------------- \n");
content.append(" 【 任 务 已 驳 回 】 \n");
content.append("-------------------------------- \n\n");
content.append("任务名称:").append(task.getTitle()).append(" \n");
content.append("任务编号:").append(task.getTaskNo()).append(" \n");
content.append("原处理人:").append(receiver.getRealName()).append(" \n");
content.append("驳回原因:").append(reason).append(" \n");
content.append(" \n");
content.append("--------------------------------");
java.util.List<String> atMobiles = receiver.getPhone() != null ?
java.util.Arrays.asList(receiver.getPhone()) : null;
dingTalkService.sendRobotMessage(title, content.toString(), atMobiles);
}
} catch (Exception e) {
log.error("发送任务驳回群通知失败", e);
}
}
log.info("接单部门负责人驳回任务,任务ID:{},审核人:{}", taskId, approverId);
}
/**
* 获取待审核任务列表
* 管理员可以看到所有待审核任务
*/
public List<Task> getPendingApprovalTasks(Long userId) {
// 检查用户是否是管理员
User user = userMapper.selectById(userId);
boolean isAdmin = user != null && Integer.valueOf(1).equals(user.getIsAdmin());
LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
// 包括一级需求审核和明细完善状态
wrapper.in(Task::getApprovalStatus, "pending_primary_approval", "pending_detail_completion", "pending_demand_approval", "pending_receiving_approval");
// 非管理员只能看到分配给自己的待审核任务
if (!isAdmin) {
wrapper.eq(Task::getCurrentApproverId, userId);
}
wrapper.orderByDesc(Task::getCreateTime);
List<Task> tasks = taskMapper.selectList(wrapper);
// 填充关联信息
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());
} else {
log.warn("任务 {} 品牌ID {} 不存在", task.getId(), task.getBrandId());
}
}
// 填充类目名称
if (task.getCategoryId() != null) {
Category category = categoryMapper.selectById(task.getCategoryId());
if (category != null) {
// Category实体类中name字段映射到category_name,使用getName()
task.setCategoryName(category.getName());
} else {
log.warn("任务 {} 类目ID {} 不存在", task.getId(), task.getCategoryId());
}
}
// 填充平台名称
if (task.getPlatformId() != null) {
Platform platform = platformMapper.selectById(task.getPlatformId());
if (platform != null) {
// Platform实体类中name字段映射到platform_name,使用getName()
task.setPlatformName(platform.getName());
} else {
log.warn("任务 {} 平台ID {} 不存在", task.getId(), task.getPlatformId());
}
}
// 填充当前审核人姓名
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) {
// 检查是否是管理员,管理员可以代审核所有任务
User approver = userMapper.selectById(approverId);
if (approver != null && Integer.valueOf(1).equals(approver.getIsAdmin())) {
log.info("管理员 {} 代审核任务 {}", approver.getRealName(), task.getId());
return; // 管理员绕过权限验证
}
if (!approverId.equals(task.getCurrentApproverId())) {
throw new BusinessException(ResultCode.FORBIDDEN, "您无权审核此任务");
}
if (!expectedRole.equals(task.getCurrentApproverRole())) {
throw new BusinessException(ResultCode.FORBIDDEN, "审核角色不匹配");
}
}
/**
* 确定接单部门负责人(支持三级分类继承二级分类的配置)
* 🔥 优先使用子任务上设置的接单部门
*/
public Long determineReceivingLeader(Task task) {
// 🔥 步骤1:优先从子任务获取接单部门
// 流水线任务:查找第一个子任务的 receiver_dept_id
if (task.getIsPipeline() != null && task.getIsPipeline() == 1) {
List<TaskItem> taskItems = taskItemMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, task.getId())
.orderByAsc(TaskItem::getSortOrder)
);
if (taskItems != null && !taskItems.isEmpty()) {
// 查找第一个有 receiver_dept_id 的子任务
for (TaskItem item : taskItems) {
if (item.getReceiverDeptId() != null) {
Department dept = departmentMapper.selectById(item.getReceiverDeptId());
if (dept != null && dept.getLeaderId() != null) {
log.info("从子任务 {} 获取接单部门负责人: 部门={}, 负责人ID={}",
item.getItemNo(), dept.getDeptName(), dept.getLeaderId());
return dept.getLeaderId();
}
}
}
}
}
// 🔥 步骤2:如果主任务有 receiver_dept_id,使用它
if (task.getReceiverDeptId() != null) {
Department dept = departmentMapper.selectById(task.getReceiverDeptId());
if (dept != null && dept.getLeaderId() != null) {
log.info("从主任务获取接单部门负责人: 部门={}, 负责人ID={}",
dept.getDeptName(), dept.getLeaderId());
return dept.getLeaderId();
}
}
// 🔥 步骤3:降级方案 - 根据任务类型的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) {
// 获取允许的部门ID(支持继承)
String allowedDeptIds = demandType.getAllowedDeptIds();
if ((allowedDeptIds == null || allowedDeptIds.isEmpty()) && demandType.getParentId() != null) {
// 三级分类没有配置,尝试获取父级(二级分类)的配置
DemandType parentType = configService.getDemandTypeById(demandType.getParentId());
if (parentType != null) {
allowedDeptIds = parentType.getAllowedDeptIds();
}
}
if (allowedDeptIds != null && !allowedDeptIds.isEmpty()) {
String[] deptIds = allowedDeptIds.split(",");
if (deptIds.length > 0) {
Long deptId = Long.parseLong(deptIds[0].trim());
Department dept = departmentMapper.selectById(deptId);
if (dept != null && dept.getLeaderId() != null) {
log.info("从任务类型配置获取接单部门负责人(降级方案): 部门={}, 负责人ID={}",
dept.getDeptName(), dept.getLeaderId());
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());
}
// 重新计算积分
java.math.BigDecimal totalScore = taskService.calculateTotalScoreDecimal(updates.getTaskTypes());
task.setScore(totalScore);
// 🔥 同步更新子任务(task_item)的数量和积分
// 这是修复接单部门审核页面调整任务明细数量后无法生效的问题
updateTaskItemsQuantityAndScore(task.getId(), updates.getTaskTypes());
}
}
/**
* 更新子任务的数量和积分
* 当审核人调整任务明细数量时,同步更新对应的子任务
*/
private void updateTaskItemsQuantityAndScore(Long taskId, List<TaskUpdateDTO.TaskTypeUpdateDTO> taskTypes) {
// 获取该任务的所有子任务
List<TaskItem> taskItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, taskId)
);
if (taskItems == null || taskItems.isEmpty()) {
log.info("任务 {} 没有子任务,跳过子任务更新", taskId);
return;
}
// 构建 taskTypeId -> (quantity, unitScore) 的映射
java.util.Map<Long, TaskUpdateDTO.TaskTypeUpdateDTO> typeUpdateMap = new java.util.HashMap<>();
for (TaskUpdateDTO.TaskTypeUpdateDTO typeUpdate : taskTypes) {
typeUpdateMap.put(typeUpdate.getTaskTypeId(), typeUpdate);
}
// 遍历子任务,更新数量和积分
for (TaskItem item : taskItems) {
boolean needUpdate = false;
// 🔥 先更新 typeDetails JSON 字段(如果存在)
// 这样后面可以从更新后的 typeDetails 中正确计算总积分
if (item.getTypeDetails() != null && !item.getTypeDetails().isEmpty()) {
String updatedTypeDetails = updateTypeDetailsJson(item.getTypeDetails(), typeUpdateMap);
if (!item.getTypeDetails().equals(updatedTypeDetails)) {
item.setTypeDetails(updatedTypeDetails);
item.setUpdateTime(LocalDateTime.now());
needUpdate = true;
log.info("更新子任务typeDetails: itemId={}, newTypeDetails={}", item.getId(), updatedTypeDetails);
// 🔥 从更新后的 typeDetails 中重新计算总积分和总数量
// 这是修复多任务类型子任务积分计算错误的关键
java.math.BigDecimal newTotalScore = calculateTotalScoreFromTypeDetails(updatedTypeDetails);
int newTotalQuantity = calculateTotalQuantityFromTypeDetails(updatedTypeDetails);
if (newTotalScore != null) {
item.setTotalScore(newTotalScore);
log.info("从typeDetails重新计算子任务总积分: itemId={}, newTotalScore={}", item.getId(), newTotalScore);
}
if (newTotalQuantity > 0) {
item.setQuantity(newTotalQuantity);
// 重新计算平均单价
if (newTotalScore != null && newTotalQuantity > 0) {
item.setUnitScore(newTotalScore.divide(new java.math.BigDecimal(newTotalQuantity), 2, java.math.RoundingMode.HALF_UP));
}
log.info("从typeDetails重新计算子任务总数量: itemId={}, newTotalQuantity={}", item.getId(), newTotalQuantity);
}
}
} else {
// 如果没有 typeDetails,使用原来的单类型逻辑
if (item.getTaskTypeId() != null && typeUpdateMap.containsKey(item.getTaskTypeId())) {
TaskUpdateDTO.TaskTypeUpdateDTO typeUpdate = typeUpdateMap.get(item.getTaskTypeId());
Integer newQuantity = typeUpdate.getQuantity();
if (newQuantity != null && !newQuantity.equals(item.getQuantity())) {
// 获取单价积分
java.math.BigDecimal unitScore = item.getUnitScore();
if (unitScore == null) {
DemandType demandType = configService.getDemandTypeById(item.getTaskTypeId());
if (demandType != null && demandType.getUnitScore() != null) {
unitScore = demandType.getUnitScore();
} else {
unitScore = java.math.BigDecimal.ZERO;
}
}
item.setQuantity(newQuantity);
item.setTotalScore(unitScore.multiply(new java.math.BigDecimal(newQuantity)));
item.setUpdateTime(LocalDateTime.now());
needUpdate = true;
log.info("更新单类型子任务数量和积分: itemId={}, taskTypeId={}, newQuantity={}, newTotalScore={}",
item.getId(), item.getTaskTypeId(), newQuantity, item.getTotalScore());
}
}
}
if (needUpdate) {
taskItemMapper.updateById(item);
}
}
log.info("完成子任务数量和积分更新: taskId={}, 子任务数量={}", taskId, taskItems.size());
}
/**
* 从 typeDetails JSON 中计算总积分
*/
private java.math.BigDecimal calculateTotalScoreFromTypeDetails(String typeDetails) {
try {
com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(typeDetails);
if (!rootNode.isArray()) {
return null;
}
java.math.BigDecimal totalScore = java.math.BigDecimal.ZERO;
for (com.fasterxml.jackson.databind.JsonNode itemNode : rootNode) {
if (itemNode.has("totalScore")) {
totalScore = totalScore.add(new java.math.BigDecimal(itemNode.get("totalScore").asDouble()));
}
}
return totalScore.setScale(2, java.math.RoundingMode.HALF_UP);
} catch (Exception e) {
log.error("计算typeDetails总积分失败: {}", e.getMessage());
return null;
}
}
/**
* 从 typeDetails JSON 中计算总数量
*/
private int calculateTotalQuantityFromTypeDetails(String typeDetails) {
try {
com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(typeDetails);
if (!rootNode.isArray()) {
return 0;
}
int totalQuantity = 0;
for (com.fasterxml.jackson.databind.JsonNode itemNode : rootNode) {
if (itemNode.has("quantity")) {
totalQuantity += itemNode.get("quantity").asInt();
}
}
return totalQuantity;
} catch (Exception e) {
log.error("计算typeDetails总数量失败: {}", e.getMessage());
return 0;
}
}
/**
* 更新 typeDetails JSON 中的数量
*/
private String updateTypeDetailsJson(String typeDetails, java.util.Map<Long, TaskUpdateDTO.TaskTypeUpdateDTO> typeUpdateMap) {
try {
// 解析 JSON 数组
com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(typeDetails);
if (!rootNode.isArray()) {
return typeDetails;
}
com.fasterxml.jackson.databind.node.ArrayNode arrayNode = (com.fasterxml.jackson.databind.node.ArrayNode) rootNode;
boolean hasChanges = false;
for (int i = 0; i < arrayNode.size(); i++) {
com.fasterxml.jackson.databind.JsonNode itemNode = arrayNode.get(i);
if (itemNode.has("typeId")) {
Long typeId = itemNode.get("typeId").asLong();
if (typeUpdateMap.containsKey(typeId)) {
TaskUpdateDTO.TaskTypeUpdateDTO typeUpdate = typeUpdateMap.get(typeId);
Integer newQuantity = typeUpdate.getQuantity();
if (newQuantity != null) {
// 修改数量
com.fasterxml.jackson.databind.node.ObjectNode objectNode = (com.fasterxml.jackson.databind.node.ObjectNode) itemNode;
int oldQuantity = objectNode.has("quantity") ? objectNode.get("quantity").asInt() : 0;
if (oldQuantity != newQuantity) {
objectNode.put("quantity", newQuantity);
// 重新计算 totalScore
if (objectNode.has("unitScore")) {
double unitScore = objectNode.get("unitScore").asDouble();
objectNode.put("totalScore", unitScore * newQuantity);
}
hasChanges = true;
log.info("更新typeDetails中的任务类型: typeId={}, oldQuantity={}, newQuantity={}",
typeId, oldQuantity, newQuantity);
}
}
}
}
}
if (hasChanges) {
return objectMapper.writeValueAsString(arrayNode);
}
return typeDetails;
} catch (Exception e) {
log.error("更新typeDetails JSON失败: {}", e.getMessage());
return typeDetails;
}
}
/**
* 拆分为流水线子任务
*/
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模式:如果指定了接收人,主任务直接进入进行中状态
task.setStatus("in_progress");
LocalDateTime now = LocalDateTime.now();
task.setStartTime(now);
task.setActualStartTime(now);
// 重新计算deadline:从接单时间开始计算(使用工作时间计算)
if (task.getEstimatedHours() != null && task.getEstimatedHours().compareTo(java.math.BigDecimal.ZERO) > 0) {
task.setDeadline(workHoursCalculator.calculateDeadline(now, task.getEstimatedHours()));
}
// 更新子任务状态为in_progress(第一组或全部,取决于流水线模式)
taskService.updatePipelineSubTasksForAssigned(task.getId(), dto.getPipelineMode());
log.info("指派流水线任务已设置为进行中:taskId={}, receiverId={}", task.getId(), task.getReceiverId());
} else {
// default模式或其他模式:流水线任务的主任务状态设置为pending
// 子任务会根据is_locked字段控制可见性
task.setStatus("pending");
}
} catch (Exception e) {
log.error("拆分流水线任务失败", e);
throw new BusinessException(ResultCode.ERROR, "拆分流水线任务失败:" + e.getMessage());
}
}
/**
* 获取任务的审批日志列表
* @param taskId 任务ID
* @return 审批日志列表
*/
public List<TaskApprovalLog> getApprovalLogs(Long taskId) {
return approvalLogMapper.selectList(
new LambdaQueryWrapper<TaskApprovalLog>()
.eq(TaskApprovalLog::getTaskId, taskId)
.orderByAsc(TaskApprovalLog::getCreateTime)
);
}
/**
* 记录审核日志
*/
public 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);
}
}
/**
* 检查一级需求任务的一级审批是否已通过
* 用于判断任务在明细完善后再次编辑时,是否应该跳过一级审批
* @param taskId 任务ID
* @return true 如果一级审批已通过
*/
public boolean hasPrimaryApprovalPassed(Long taskId) {
// 查询是否有 approve_primary 的审批日志记录
Long count = approvalLogMapper.selectCount(
new LambdaQueryWrapper<TaskApprovalLog>()
.eq(TaskApprovalLog::getTaskId, taskId)
.eq(TaskApprovalLog::getAction, "approve_primary")
);
return count != null && count > 0;
}
/**
* 格式化预估工时显示(基于工作时间配置)
*/
private String formatEstimatedHours(java.math.BigDecimal hours) {
if (hours == null) {
return "未设置";
}
String formatted = workHoursCalculator.formatWorkHours(hours);
// WorkHoursCalculator 返回 "--" 表示无效,这里保持 "未设置" 的语义
return "--".equals(formatted) ? "未设置" : formatted;
}
/**
* 发送任务指派通知给设计师
* @param task 任务
* @param receiverId 接收人ID
* @param isPreAssigned 是否预先指定
*/
private void sendTaskAssignedNotification(Task task, Long receiverId, boolean isPreAssigned) {
try {
com.designhub.entity.User receiver = userMapper.selectById(receiverId);
if (receiver == null) {
log.warn("接收人不存在: {}", receiverId);
return;
}
String receiverName = receiver.getRealName();
String receiverPhone = receiver.getPhone();
// 构建通知内容
StringBuilder content = new StringBuilder();
// 基本信息
content.append("**任务名称**:").append(task.getTitle()).append(" \n");
content.append("**任务编号**:").append(task.getTaskNo()).append(" \n");
// 任务详情
if (task.getBrandName() != null && !task.getBrandName().isEmpty()) {
content.append("**品牌**:").append(task.getBrandName()).append(" \n");
}
if (task.getPlatformName() != null && !task.getPlatformName().isEmpty()) {
content.append("**平台**:").append(task.getPlatformName()).append(" \n");
}
if (task.getSpuInfo() != null && !task.getSpuInfo().isEmpty()) {
content.append("**SPU**:").append(task.getSpuInfo()).append(" \n");
}
if (task.getTaskSubType() != null && !task.getTaskSubType().isEmpty()) {
content.append("**任务类型**:").append(task.getTaskSubType()).append(" \n");
}
content.append(" \n");
// 查询该设计师负责的子任务
List<TaskItem> receiverItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, task.getId())
.eq(TaskItem::getReceiverId, receiverId)
.orderByAsc(TaskItem::getGroupNo)
);
// 时间格式化器:MM月dd日 HH:mm
DateTimeFormatter deadlineFormatter = DateTimeFormatter.ofPattern("MM月dd日 HH:mm");
if (!receiverItems.isEmpty()) {
content.append("**您负责的任务明细**: \n");
for (TaskItem item : receiverItems) {
// 通过任务类型ID获取接单部门名称
String deptName = "未指定";
if (item.getTaskTypeId() != null) {
try {
DemandType demandType = configService.getDemandTypeById(item.getTaskTypeId());
if (demandType != null && demandType.getAllowedDeptIds() != null && !demandType.getAllowedDeptIds().isEmpty()) {
String[] deptIds = demandType.getAllowedDeptIds().split(",");
Long firstDeptId = Long.parseLong(deptIds[0].trim());
Department dept = departmentMapper.selectById(firstDeptId);
if (dept != null) {
deptName = dept.getDeptName();
}
}
} catch (Exception e) {
log.warn("获取部门名称失败: {}", e.getMessage());
}
}
// 格式化工时显示
String hoursDisplay = formatEstimatedHours(item.getEstimatedHours());
// 格式化截止时间
String deadlineDisplay = "";
if (item.getDeadline() != null) {
deadlineDisplay = " | 截止: " + item.getDeadline().format(deadlineFormatter);
}
content.append(" \n");
content.append("▸ **").append(deptName).append("** | ")
.append(item.getTotalScore()).append("分 | ")
.append(hoursDisplay).append(deadlineDisplay).append(" \n");
// 解析并显示任务类型明细
if (item.getTypeDetails() != null && !item.getTypeDetails().isEmpty()) {
try {
@SuppressWarnings("unchecked")
List<java.util.Map<String, Object>> typeDetails =
objectMapper.readValue(item.getTypeDetails(), List.class);
for (java.util.Map<String, Object> detail : typeDetails) {
String typeName = String.valueOf(detail.get("typeName"));
Object qtyObj = detail.get("quantity");
Object scoreObj = detail.get("totalScore");
int qty = qtyObj instanceof Number ? ((Number) qtyObj).intValue() : 1;
String score = scoreObj != null ? String.valueOf(scoreObj) : "0";
content.append("  └ ").append(typeName)
.append(" ×").append(qty)
.append(" = ").append(score).append("分 \n");
}
} catch (Exception e) {
log.warn("解析任务类型明细失败: {}", e.getMessage());
}
}
}
} else {
// 没有子任务时显示总积分和工时
content.append("**任务积分**:").append(task.getScore()).append(" 分 \n");
if (task.getEstimatedHours() != null) {
content.append("**预估工时**:").append(formatEstimatedHours(task.getEstimatedHours())).append(" \n");
}
}
content.append(" \n");
// 发起人信息
if (task.getPublisherName() != null && !task.getPublisherName().isEmpty()) {
content.append("**发起人**:").append(task.getPublisherName()).append(" \n");
}
if (task.getPublisherDeptName() != null && !task.getPublisherDeptName().isEmpty()) {
content.append("**发起部门**:").append(task.getPublisherDeptName()).append(" \n");
}
// 指派信息
content.append("**指派给**:").append(receiverName).append(" \n");
content.append(" \n");
content.append("点击下方按钮查看详情");
// 发送个人钉钉通知给设计师
Notification notification = new Notification();
notification.setReceiverId(receiverId);
notification.setTitle("任务已指派");
notification.setContent(content.toString());
notification.setNotificationType("TASK_ASSIGNED");
notification.setRelatedType("task");
notification.setRelatedId(task.getId());
notification.setPushDingtalk(1); // 推送到钉钉
notificationService.sendNotification(notification);
log.info("任务指派个人通知已发送给设计师: userId={}, name={}", receiverId, receiverName);
// 只有非预先指定的任务才发送群通知(预先指定的任务只发送个人通知)
if (!isPreAssigned) {
java.util.List<String> atMobiles = receiverPhone != null ?
java.util.Arrays.asList(receiverPhone) : null;
dingTalkService.sendRobotMessage("任务已指派", content.toString(), atMobiles);
log.info("任务指派群通知已发送,@手机号: {}", receiverPhone);
} else {
log.info("任务【{}】为预先指定任务,只发送个人通知,不发送群通知", task.getTitle());
}
} catch (Exception e) {
log.error("发送任务指派通知失败: receiverId={}", receiverId, e);
}
}
}