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 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 itemsToUpdate; if ("sequential".equals(pipelineMode)) { // 顺序模式:只更新第一个部门的子任务(status='pending' 且 isLocked=0) itemsToUpdate = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getTaskId, taskId) .eq(TaskItem::getStatus, "pending") .eq(TaskItem::getIsLocked, 0) ); log.info("顺序模式:更新第一个部门的子任务状态"); } else { // 并行模式:更新所有部门的子任务(status='pending' 且 isLocked=0) itemsToUpdate = taskItemMapper.selectList( new LambdaQueryWrapper() .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 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 notifiedReceiverIds = new java.util.HashSet<>(); if (task.getIsPipeline() != null && task.getIsPipeline() == 1) { // 流水线任务:查询第一个部门的所有子任务,给每个设计师发送通知 List firstDeptItems = taskItemMapper.selectList( new LambdaQueryWrapper() .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 taskItems = taskItemMapper.selectList( new LambdaQueryWrapper() .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> typeDetails = objectMapper.readValue(item.getTypeDetails(), List.class); for (java.util.Map 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 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 getPendingApprovalTasks(Long userId) { // 检查用户是否是管理员 User user = userMapper.selectById(userId); boolean isAdmin = user != null && Integer.valueOf(1).equals(user.getIsAdmin()); LambdaQueryWrapper 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 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 taskItems = taskItemMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .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 taskTypes) { // 获取该任务的所有子任务 List taskItems = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getTaskId, taskId) ); if (taskItems == null || taskItems.isEmpty()) { log.info("任务 {} 没有子任务,跳过子任务更新", taskId); return; } // 构建 taskTypeId -> (quantity, unitScore) 的映射 java.util.Map 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 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 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 getApprovalLogs(Long taskId) { return approvalLogMapper.selectList( new LambdaQueryWrapper() .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() .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 receiverItems = taskItemMapper.selectList( new LambdaQueryWrapper() .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> typeDetails = objectMapper.readValue(item.getTypeDetails(), List.class); for (java.util.Map 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 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); } } }