package com.designhub.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.designhub.common.ResultCode; import com.designhub.entity.Task; import com.designhub.entity.TaskItem; import com.designhub.entity.Notification; import com.designhub.exception.BusinessException; import com.designhub.mapper.TaskItemMapper; import com.designhub.mapper.TaskMapper; import com.designhub.mapper.DepartmentMapper; import com.designhub.mapper.DemandTypeMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.fasterxml.jackson.databind.ObjectMapper; /** * 任务子项服务类(流水线任务) */ @Service @RequiredArgsConstructor public class TaskItemService { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TaskItemService.class); private final TaskItemMapper taskItemMapper; private final TaskMapper taskMapper; private final DepartmentMapper departmentMapper; private final UserService userService; private final NotificationService notificationService; private final DingTalkService dingTalkService; private final ConfigService configService; private final ExternalAssetService externalAssetService; private final SystemConfigService systemConfigService; private final DemandTypeMapper demandTypeMapper; private final com.designhub.mapper.UserMapper userMapper; private final ObjectMapper objectMapper; private final com.designhub.util.WorkHoursCalculator workHoursCalculator; /** * 获取可接单的子任务列表 */ public IPage getAvailableItems(Integer page, Integer pageSize, Long taskTypeId, String keyword) { return getAvailableItems(page, pageSize, taskTypeId, keyword, null); } /** * 获取可接单的子任务列表(支持按接单部门筛选) * @param filterDeptId 筛选可接单部门ID(可选) */ public IPage getAvailableItems(Integer page, Integer pageSize, Long taskTypeId, String keyword, Long filterDeptId) { Page pageParam = new Page<>(page, pageSize); // 如果没有指定筛选部门,直接查询 if (filterDeptId == null) { return taskItemMapper.selectAvailableItems(pageParam, taskTypeId, keyword); } // 🔥 获取筛选部门及其所有子部门的ID列表 Set filterDeptAndChildIds = getDeptAndChildIds(filterDeptId); log.info("筛选部门ID: {}, 包含子部门: {}", filterDeptId, filterDeptAndChildIds); // 获取该部门可接单的任务类型ID列表 List allDemandTypes = demandTypeMapper.selectList(null); Map demandTypeMap = allDemandTypes.stream() .collect(java.util.stream.Collectors.toMap(com.designhub.entity.DemandType::getId, dt -> dt)); List filterDeptAllowedTypeIds = allDemandTypes.stream() .filter(dt -> { String allowedDepts = dt.getAllowedDeptIds(); if ((allowedDepts == null || allowedDepts.isEmpty()) && dt.getParentId() != null) { com.designhub.entity.DemandType parentType = demandTypeMap.get(dt.getParentId()); if (parentType != null) { allowedDepts = parentType.getAllowedDeptIds(); } } // 只筛选明确指定了该部门(或其子部门)的任务类型 if (allowedDepts == null || allowedDepts.isEmpty()) { return false; } String[] deptIds = allowedDepts.split(","); for (String deptIdStr : deptIds) { try { Long deptId = Long.parseLong(deptIdStr.trim()); // 🔥 检查是否匹配筛选部门或其子部门 if (filterDeptAndChildIds.contains(deptId)) { return true; } } catch (NumberFormatException e) { // 忽略无效的部门ID } } return false; }) .map(com.designhub.entity.DemandType::getId) .collect(java.util.stream.Collectors.toList()); log.info("匹配的任务类型ID: {}", filterDeptAllowedTypeIds); // 如果没有找到匹配的任务类型,返回空结果 if (filterDeptAllowedTypeIds.isEmpty()) { return new Page<>(page, pageSize); } // 查询并筛选 return taskItemMapper.selectAvailableItemsByTaskTypes(pageParam, taskTypeId, keyword, filterDeptAllowedTypeIds); } /** * 获取指定部门及其所有子部门的ID集合 */ private Set getDeptAndChildIds(Long deptId) { Set result = new HashSet<>(); result.add(deptId); // 获取所有部门 List allDepts = departmentMapper.selectList(null); // 递归获取子部门 addChildDeptIds(result, deptId, allDepts); return result; } /** * 递归添加子部门ID */ private void addChildDeptIds(Set result, Long parentId, List allDepts) { for (com.designhub.entity.Department dept : allDepts) { if (parentId.equals(dept.getParentId())) { result.add(dept.getId()); // 递归获取子部门的子部门 addChildDeptIds(result, dept.getId(), allDepts); } } } /** * 获取进行中的子任务列表 * @param receiverId 接单人ID,null表示不按接单人过滤 * @param deptId 部门ID,用于部门负责人查看本部门所有任务 * @param keyword 搜索关键词 */ public IPage getInProgressItems(Integer page, Integer pageSize, Long receiverId, Long deptId, String keyword) { Page pageParam = new Page<>(page, pageSize); return taskItemMapper.selectInProgressItems(pageParam, receiverId, deptId, keyword); } /** * 获取进行中的子任务列表(向后兼容) */ public IPage getInProgressItems(Integer page, Integer pageSize, Long receiverId, Long deptId) { return getInProgressItems(page, pageSize, receiverId, deptId, null); } /** * 获取进行中的子任务列表(向后兼容) */ public IPage getInProgressItems(Integer page, Integer pageSize, Long receiverId) { return getInProgressItems(page, pageSize, receiverId, null, null); } /** * 获取进行中的子任务列表(部门负责人:按多个部门ID过滤) * @param deptIds 部门ID列表 * @param keyword 搜索关键词 */ public IPage getInProgressItemsByDeptIds(Integer page, Integer pageSize, List deptIds, String keyword) { Page pageParam = new Page<>(page, pageSize); return taskItemMapper.selectInProgressItemsByDeptIds(pageParam, deptIds, keyword); } /** * 获取已完成的子任务列表 * @param receiverId 接单人ID,null表示不按接单人过滤 * @param deptId 部门ID,用于部门负责人查看本部门所有任务 * @param keyword 搜索关键词 */ public IPage getCompletedItems(Integer page, Integer pageSize, Long receiverId, Long deptId, String keyword) { Page pageParam = new Page<>(page, pageSize); return taskItemMapper.selectCompletedItems(pageParam, receiverId, deptId, keyword); } /** * 获取已完成的子任务列表(向后兼容) */ public IPage getCompletedItems(Integer page, Integer pageSize, Long receiverId, Long deptId) { return getCompletedItems(page, pageSize, receiverId, deptId, null); } /** * 获取已完成的子任务列表(向后兼容) */ public IPage getCompletedItems(Integer page, Integer pageSize, Long receiverId) { return getCompletedItems(page, pageSize, receiverId, null, null); } /** * 获取已完成的子任务列表(部门负责人:按多个部门ID过滤) * @param deptIds 部门ID列表 * @param keyword 搜索关键词 */ public IPage getCompletedItemsByDeptIds(Integer page, Integer pageSize, List deptIds, String keyword) { Page pageParam = new Page<>(page, pageSize); return taskItemMapper.selectCompletedItemsByDeptIds(pageParam, deptIds, keyword); } /** * 根据任务ID获取所有子任务 */ public List getItemsByTaskId(Long taskId) { return taskItemMapper.selectByTaskId(taskId); } /** * 获取子任务详情 */ public TaskItem getItemById(Long itemId) { TaskItem item = taskItemMapper.selectById(itemId); if (item == null) { throw new BusinessException(ResultCode.NOT_FOUND, "子任务不存在"); } // 加载主任务信息 Task mainTask = taskMapper.selectById(item.getTaskId()); if (mainTask != null) { item.setMainTaskTitle(mainTask.getTitle()); item.setPipelineMode(mainTask.getPipelineMode()); item.setBrandName(mainTask.getBrandName()); item.setCategoryName(mainTask.getCategoryName()); item.setPlatformName(mainTask.getPlatformName()); item.setPublisherName(mainTask.getPublisherName()); item.setSpuInfo(mainTask.getSpuInfo()); item.setTaskSubType(mainTask.getTaskSubType()); item.setAttachmentUrl(mainTask.getAttachmentUrl()); } return item; } /** * 抢单子任务 */ @Transactional(rollbackFor = Exception.class) public void grabItem(Long itemId, Long userId) { log.info("设计师 {} 抢单子任务 {}", userId, itemId); // 获取用户信息 var user = userService.getUserById(userId); if (user == null) { throw new BusinessException(ResultCode.FORBIDDEN, "用户不存在"); } // 获取子任务信息 TaskItem item = taskItemMapper.selectById(itemId); if (item == null) { throw new BusinessException(ResultCode.NOT_FOUND, "子任务不存在"); } // 检查是否是管理员(管理员可以抢单,绕过所有限制) boolean isAdmin = user.getIsAdmin() != null && user.getIsAdmin() == 1; // 非管理员需要进行权限验证 if (!isAdmin) { // 验证用户部门权限 if (user.getDeptId() == null) { throw new BusinessException(ResultCode.FORBIDDEN, "您未分配部门,无法接单"); } com.designhub.entity.Department department = departmentMapper.selectById(user.getDeptId()); if (department == null) { throw new BusinessException(ResultCode.FORBIDDEN, "未找到用户所属部门,请联系管理员"); } // 检查1:需求部门不能抢单 if ("demand".equals(department.getDeptType())) { throw new BusinessException(ResultCode.FORBIDDEN, "需求部门成员不能接单,只能发起需求"); } // 检查2:接单部门类型验证 if (!"receiving".equals(department.getDeptType())) { throw new BusinessException(ResultCode.FORBIDDEN, "只有接单部门成员可以接收任务"); } // 检查3:部门负责人不能抢单 if (department.getLeaderId() != null && department.getLeaderId().equals(userId)) { throw new BusinessException(ResultCode.FORBIDDEN, "部门负责人不能接单,请分配给设计师"); } // 检查4:验证部门是否允许接该任务类型(支持三级分类继承二级分类的配置,支持子部门继承父部门权限) if (item.getTaskTypeId() != null) { com.designhub.entity.DemandType demandType = configService.getDemandTypeById(item.getTaskTypeId()); if (demandType != null) { // 获取允许的部门ID(支持任务类型继承) String allowedDeptIds = demandType.getAllowedDeptIds(); if ((allowedDeptIds == null || allowedDeptIds.isEmpty()) && demandType.getParentId() != null) { // 三级分类没有配置,尝试获取父级(二级分类)的配置 var parentType = configService.getDemandTypeById(demandType.getParentId()); if (parentType != null) { allowedDeptIds = parentType.getAllowedDeptIds(); } } if (allowedDeptIds != null && !allowedDeptIds.isEmpty()) { // 检查用户部门或其父部门是否在允许列表中(子部门继承父部门权限) String[] allowedIds = allowedDeptIds.split(","); Set allowedDeptIdSet = new HashSet<>(); for (String allowedId : allowedIds) { try { allowedDeptIdSet.add(Long.parseLong(allowedId.trim())); } catch (NumberFormatException e) { log.warn("解析允许部门ID失败: {}", allowedId); } } // 获取用户部门及其所有父部门ID boolean isAllowed = false; Long currentDeptId = user.getDeptId(); while (currentDeptId != null) { if (allowedDeptIdSet.contains(currentDeptId)) { isAllowed = true; break; } // 向上查找父部门 com.designhub.entity.Department currentDept = departmentMapper.selectById(currentDeptId); if (currentDept != null && currentDept.getParentId() != null) { currentDeptId = currentDept.getParentId(); } else { break; } } if (!isAllowed) { throw new BusinessException(ResultCode.FORBIDDEN, "您的部门无权接收此类型的任务"); } } } } } else { log.info("管理员 {} 抢单子任务 {}", user.getRealName(), itemId); } // 验证状态 if (!"pending".equals(item.getStatus())) { throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "该子任务不可接单"); } if (item.getIsLocked() != null && item.getIsLocked() == 1) { throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "该子任务已被锁定,请等待前序任务完成"); } // 🔥 检查是否是指定任务:如果子任务已预先指定了接收人 boolean isAssignedItem = false; if (item.getReceiverId() != null) { // 检查主任务是否是指定模式 Task mainTaskForCheck = taskMapper.selectById(item.getTaskId()); boolean isAssignedMode = mainTaskForCheck != null && "assigned".equals(mainTaskForCheck.getReleaseType()); if (isAssignedMode) { // 指定任务模式:只有被指定的设计师才能接单 if (!item.getReceiverId().equals(userId)) { throw new BusinessException(ResultCode.FORBIDDEN, "该子任务已指定给其他设计师"); } // 被指定的设计师可以接单,继续执行 isAssignedItem = true; log.info("指定任务模式:设计师 {} 是被指定的接收人,允许接单", userId); } else { // 非指定任务模式:已有接收人则不能再接单 throw new BusinessException(ResultCode.TASK_ALREADY_TAKEN, "该子任务已被接单"); } } // 计算deadline LocalDateTime now = LocalDateTime.now(); LocalDateTime deadline = null; if (item.getEstimatedHours() != null && item.getEstimatedHours().compareTo(java.math.BigDecimal.ZERO) > 0) { deadline = workHoursCalculator.calculateDeadline(now, item.getEstimatedHours()); log.info("子任务deadline已计算:从接单时间 {} 开始,预估 {} 小时,截止时间 {}", now, item.getEstimatedHours(), deadline); } // 🔥 根据是否是指定任务使用不同的抢单方法 int result; if (isAssignedItem) { // 指定任务:使用专用方法,只更新状态和deadline result = taskItemMapper.grabAssignedItem(itemId, userId, deadline); } else { // 普通任务:乐观锁抢单(同时设置接单部门ID和deadline) result = taskItemMapper.grabItem(itemId, userId, user.getRealName(), user.getDeptId(), deadline); } if (result == 0) { throw new BusinessException(ResultCode.TASK_ALREADY_TAKEN, "抢单失败,该子任务可能已被他人接单"); } log.info("设计师 {} 成功抢单子任务 {} (指定任务: {})", userId, itemId, isAssignedItem); // 更新主任务状态 Task mainTask = taskMapper.selectById(item.getTaskId()); if (mainTask != null) { // 如果主任务还是pending状态,更新为in_progress if ("pending".equals(mainTask.getStatus())) { mainTask.setStatus("in_progress"); mainTask.setStartTime(LocalDateTime.now()); mainTask.setActualStartTime(LocalDateTime.now()); taskMapper.updateById(mainTask); log.info("主任务 {} 状态已更新为 in_progress", mainTask.getId()); } // 发送通知给任务发布者 if (mainTask.getPublisherId() != null) { Notification notification = new Notification(); notification.setReceiverId(mainTask.getPublisherId()); notification.setNotificationType("task"); notification.setTitle("子任务已被接单"); notification.setContent(user.getRealName() + " 接单了子任务:" + item.getItemNo()); notification.setRelatedId(itemId); notification.setRelatedType("task_item"); notification.setPriority(1); // 普通 notification.setPushDingtalk(1); // 推送到钉钉 notificationService.sendNotification(notification); } // 🔥 一级需求任务:同时通知一级发起人 if (mainTask.getPrimaryPublisherId() != null && !mainTask.getPrimaryPublisherId().equals(mainTask.getPublisherId())) { Notification notification2 = new Notification(); notification2.setReceiverId(mainTask.getPrimaryPublisherId()); notification2.setNotificationType("task"); notification2.setTitle("子任务已被接单"); notification2.setContent(user.getRealName() + " 接单了子任务:" + item.getItemNo()); notification2.setRelatedId(itemId); notification2.setRelatedType("task_item"); notification2.setPriority(1); notification2.setPushDingtalk(1); notificationService.sendNotification(notification2); log.info("已通知一级发起人 {} 子任务被接单", mainTask.getPrimaryPublisherId()); } } } /** * 更新子任务进度 */ @Transactional(rollbackFor = Exception.class) public void updateProgress(Long itemId, Long userId, Integer progress) { log.info("设计师 {} 更新子任务 {} 进度为 {}%", userId, itemId, progress); TaskItem item = taskItemMapper.selectById(itemId); if (item == null) { throw new BusinessException(ResultCode.NOT_FOUND, "子任务不存在"); } // 验证权限 if (!userId.equals(item.getReceiverId())) { throw new BusinessException(ResultCode.FORBIDDEN, "无权更新该子任务"); } // 验证状态 if (!"in_progress".equals(item.getStatus())) { throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "只能更新进行中的子任务"); } // 验证进度值 if (progress < 0 || progress > 100) { throw new BusinessException(ResultCode.PARAM_ERROR, "进度值必须在0-100之间"); } // 记录更新前的进度 Integer oldProgress = item.getProgress() != null ? item.getProgress() : 0; // 更新进度 item.setProgress(progress); item.setUpdateTime(LocalDateTime.now()); taskItemMapper.updateById(item); log.info("子任务 {} 进度已更新为 {}%", itemId, progress); // 🔥 同步更新主任务的整体进度 updateMainTaskProgress(item.getTaskId()); // 获取主任务信息,通知需求者(发布者) try { Task mainTask = taskMapper.selectById(item.getTaskId()); if (mainTask != null && mainTask.getPublisherId() != null) { // 获取设计师姓名 String designerName = item.getReceiverName(); if (designerName == null && item.getReceiverId() != null) { com.designhub.entity.User designer = userService.getUserById(item.getReceiverId()); if (designer != null) { designerName = designer.getRealName() != null ? designer.getRealName() : designer.getUsername(); } } // 构建通知内容 StringBuilder content = new StringBuilder(); content.append("### 任务进度更新\n\n"); content.append("**任务名称**:").append(mainTask.getTitle()).append(" \n"); content.append("**子任务编号**:").append(item.getItemNo()).append(" \n"); content.append("**设计师**:").append(designerName != null ? designerName : "未知").append(" \n"); content.append("**进度变化**:").append(oldProgress).append("% → ").append(progress).append("% \n"); if (progress == 100) { content.append("\n🎉 子任务进度已达100%,请关注完结状态。"); } // 发送通知给需求者(发布者) notificationService.sendNotification( mainTask.getPublisherId(), "任务进度已更新", content.toString(), "PROGRESS_UPDATE", "task_item", itemId ); log.info("已通知需求者 {} 任务进度更新: 子任务={}, 进度={}%", mainTask.getPublisherId(), item.getItemNo(), progress); // 🔥 一级需求任务:同时通知一级发起人 if (mainTask.getPrimaryPublisherId() != null && !mainTask.getPrimaryPublisherId().equals(mainTask.getPublisherId())) { notificationService.sendNotification( mainTask.getPrimaryPublisherId(), "任务进度已更新", content.toString(), "PROGRESS_UPDATE", "task_item", itemId ); log.info("已通知一级发起人 {} 任务进度更新: 子任务={}, 进度={}%", mainTask.getPrimaryPublisherId(), item.getItemNo(), progress); } } } catch (Exception e) { // 通知失败不影响主流程 log.error("发送进度更新通知给需求者失败: itemId={}", itemId, e); } } /** * 完结子任务(关键方法:包含自动解锁逻辑) * @param itemId 子任务ID * @param userId 用户ID * @param deliveryPath 交付路径(可选,如果启用第三方API则自动获取) */ @Transactional(rollbackFor = Exception.class) public Map completeItem(Long itemId, Long userId, String deliveryPath) { log.info("设计师 {} 完结子任务 {}", userId, itemId); // 1. 获取子任务信息 TaskItem item = taskItemMapper.selectById(itemId); if (item == null) { throw new BusinessException(ResultCode.NOT_FOUND, "子任务不存在"); } // 2. 验证权限 if (!userId.equals(item.getReceiverId())) { throw new BusinessException(ResultCode.FORBIDDEN, "无权完结该子任务"); } // 3. 验证状态 if (!"in_progress".equals(item.getStatus())) { throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "只能完结进行中的子任务"); } // 4. 检查第三方素材API配置并验证 String downloadUrl = null; boolean externalApiEnabled = "true".equalsIgnoreCase( systemConfigService.getConfigValue("external_asset_api_enabled", "false")); if (!externalApiEnabled) { // 未启用第三方API,不允许完结任务 throw new BusinessException(ResultCode.ERROR, "请先在系统设置中启用并配置第三方素材API后才能完结任务"); } // 调用第三方素材API验证任务完成状态 String taskNo = item.getItemNo(); // 使用子任务编号作为任务号 try { downloadUrl = externalAssetService.checkTaskCompletion(taskNo); } catch (Exception e) { log.warn("调用第三方素材API失败: {}", e.getMessage()); // API调用失败时,提示用户 throw new BusinessException(ResultCode.ERROR, "素材API验证失败: " + e.getMessage()); } if (downloadUrl == null || downloadUrl.trim().isEmpty()) { // 如果第三方API未返回下载链接,提示用户去上传素材 throw new BusinessException(ResultCode.ERROR, "素材未提交,请先前往 3D.YYDD.AI 提交任务素材后再完结任务"); } log.info("第三方素材API返回下载链接: {}", downloadUrl); // 5. 更新子任务状态 item.setStatus("completed"); item.setProgress(100); item.setDeliveryPath(downloadUrl != null ? downloadUrl : deliveryPath); item.setDownloadUrl(downloadUrl); item.setCompleteTime(LocalDateTime.now()); item.setUpdateTime(LocalDateTime.now()); taskItemMapper.updateById(item); log.info("子任务 {} 已完成", itemId); // 5. 设计师立即获得积分(如果需要,可以调用积分服务) // scoreService.addScore(userId, item.getTotalScore()); Map result = new HashMap<>(); result.put("completedItem", item); // 6. 🔥 关键步骤:检查是否是顺序流水线,基于部门解锁下一个部门 // 新逻辑:同一部门内的所有子任务并行,部门间串行 Task mainTask = taskMapper.selectById(item.getTaskId()); if (mainTask != null && "sequential".equals(mainTask.getPipelineMode())) { // 从子任务编号解析部门序号(格式:TASK...-部门序号-部门内序号) int currentDeptIndex = parseDeptIndexFromItemNo(item.getItemNo()); log.info("当前子任务 {} 属于第 {} 个部门", item.getItemNo(), currentDeptIndex); // 获取当前部门的所有子任务(通过编号匹配) List allItems = taskItemMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(TaskItem::getTaskId, item.getTaskId()) ); // 筛选出同一部门的子任务 List currentDeptItems = allItems.stream() .filter(i -> parseDeptIndexFromItemNo(i.getItemNo()) == currentDeptIndex) .collect(java.util.stream.Collectors.toList()); // 检查当前部门的所有子任务是否都已完成 boolean allDeptCompleted = currentDeptItems.stream() .allMatch(i -> "completed".equals(i.getStatus())); log.info("当前部门共 {} 个子任务,已完成: {}", currentDeptItems.size(), currentDeptItems.stream().filter(i -> "completed".equals(i.getStatus())).count()); if (allDeptCompleted) { log.info("✅ 第 {} 个部门的所有子任务已完成,准备解锁下一个部门", currentDeptIndex); // 🔥 收集当前部门所有子任务的下载链接,传递给下一个部门 StringBuilder prevDownloadUrls = new StringBuilder(); for (TaskItem deptItem : currentDeptItems) { if (deptItem.getDownloadUrl() != null && !deptItem.getDownloadUrl().isEmpty()) { if (prevDownloadUrls.length() > 0) { prevDownloadUrls.append(","); } prevDownloadUrls.append(deptItem.getDownloadUrl()); } // 同时继承前面环节的下载链接 if (deptItem.getPrevDownloadUrl() != null && !deptItem.getPrevDownloadUrl().isEmpty()) { if (prevDownloadUrls.length() > 0) { prevDownloadUrls.append(","); } prevDownloadUrls.append(deptItem.getPrevDownloadUrl()); } } String prevDownloadUrlStr = prevDownloadUrls.toString(); log.info("前置部门素材下载链接: {}", prevDownloadUrlStr); // 找出下一个部门的所有子任务(状态为locked) int nextDeptIndex = currentDeptIndex + 1; List nextDeptItems = allItems.stream() .filter(i -> parseDeptIndexFromItemNo(i.getItemNo()) == nextDeptIndex) .filter(i -> "locked".equals(i.getStatus())) .collect(java.util.stream.Collectors.toList()); if (!nextDeptItems.isEmpty()) { // 批量解锁下一个部门的所有子任务,状态设为待审核,并传递前置环节的下载链接 for (TaskItem nextItem : nextDeptItems) { nextItem.setStatus("pending_approval"); // 待接单部门负责人审核 nextItem.setIsLocked(0); nextItem.setPrevDownloadUrl(prevDownloadUrlStr); // 🔥 传递前置环节的下载链接 nextItem.setUpdateTime(LocalDateTime.now()); taskItemMapper.updateById(nextItem); log.info("✅ 解锁子任务: {} (部门序号: {}, ID: {}),待接单部门负责人审核,已传递前置素材链接", nextItem.getItemNo(), nextDeptIndex, nextItem.getId()); } result.put("unlockedDeptIndex", nextDeptIndex); result.put("unlockedCount", nextDeptItems.size()); // 发送通知给接单部门负责人 // 从子任务的任务类型配置中获取allowed_dept_ids Set notifiedLeaderIds = new HashSet<>(); for (TaskItem nextItem : nextDeptItems) { Long targetDeptId = getTargetDeptIdFromTaskType(nextItem.getTaskTypeId()); if (targetDeptId != null) { com.designhub.entity.Department receiverDept = departmentMapper.selectById(targetDeptId); if (receiverDept != null && receiverDept.getLeaderId() != null && !notifiedLeaderIds.contains(receiverDept.getLeaderId())) { notifiedLeaderIds.add(receiverDept.getLeaderId()); Notification notification = new Notification(); notification.setReceiverId(receiverDept.getLeaderId()); notification.setNotificationType("task_approval"); notification.setTitle("新子任务组待审核"); notification.setContent("任务《" + mainTask.getTitle() + "》第 " + nextDeptIndex + " 个部门共 " + nextDeptItems.size() + " 个子任务已解锁,请审核并分配!"); notification.setRelatedId(item.getTaskId()); notification.setRelatedType("task"); notification.setPriority(2); // 重要 notification.setPushDingtalk(1); // 推送到钉钉 notificationService.sendNotification(notification); log.info("已通知接单部门负责人(部门: {}, 负责人ID: {})审核第 {} 个部门子任务", receiverDept.getDeptName(), receiverDept.getLeaderId(), nextDeptIndex); } } else { log.warn("子任务 {} 的任务类型未配置接单部门", nextItem.getItemNo()); } } log.info("第 {} 个部门 {} 个子任务已解锁,等待接单部门负责人审核,已通知 {} 个部门负责人", nextDeptIndex, nextDeptItems.size(), notifiedLeaderIds.size()); } else { log.info("没有更多部门的子任务需要解锁"); } } else { log.info("当前部门还有子任务未完成,暂不解锁下一个部门"); } } // 7. 检查主任务是否所有子任务都已完成 int completedCount = taskItemMapper.countCompletedItems(item.getTaskId()); int totalCount = taskItemMapper.countTotalItems(item.getTaskId()); if (completedCount == totalCount) { // 🔥 获取最后一组子任务的下载链接,保存到主任务 Integer maxGroupNo = taskItemMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(TaskItem::getTaskId, item.getTaskId()) .orderByDesc(TaskItem::getGroupNo) .last("LIMIT 1") ).stream().findFirst().map(TaskItem::getGroupNo).orElse(1); List lastGroupItems = taskItemMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(TaskItem::getTaskId, item.getTaskId()) .eq(TaskItem::getGroupNo, maxGroupNo) ); // 收集最后一组的下载链接 StringBuilder finalDownloadUrl = new StringBuilder(); for (TaskItem lastItem : lastGroupItems) { if (lastItem.getDownloadUrl() != null && !lastItem.getDownloadUrl().isEmpty()) { if (finalDownloadUrl.length() > 0) { finalDownloadUrl.append(","); } finalDownloadUrl.append(lastItem.getDownloadUrl()); } } // 所有子任务完成,更新主任务状态 mainTask.setStatus("completed"); mainTask.setProgress(100); // 🔥 设置主任务进度为100% mainTask.setCompleteTime(LocalDateTime.now()); mainTask.setUpdateTime(LocalDateTime.now()); // 🔥 保存最后一组的下载链接到主任务 if (finalDownloadUrl.length() > 0) { mainTask.setDeliveryPath(finalDownloadUrl.toString()); log.info("主任务最终素材下载链接: {}", finalDownloadUrl); } taskMapper.updateById(mainTask); log.info("🎉 主任务 {} 所有子任务已完成,主任务标记为完成", mainTask.getId()); // 发送通知给发布者 if (mainTask.getPublisherId() != null) { Notification notification = new Notification(); notification.setReceiverId(mainTask.getPublisherId()); notification.setNotificationType("task"); notification.setTitle("流水线任务已完成"); notification.setContent("您的任务《" + mainTask.getTitle() + "》所有子任务已完成!"); notification.setRelatedId(mainTask.getId()); notification.setRelatedType("task"); notification.setPriority(1); // 普通 notification.setPushDingtalk(1); // 推送到钉钉 notificationService.sendNotification(notification); } // 🔥 一级需求任务:同时通知一级发起人 if (mainTask.getPrimaryPublisherId() != null && !mainTask.getPrimaryPublisherId().equals(mainTask.getPublisherId())) { Notification notification2 = new Notification(); notification2.setReceiverId(mainTask.getPrimaryPublisherId()); notification2.setNotificationType("task"); notification2.setTitle("流水线任务已完成"); notification2.setContent("您发起的任务《" + mainTask.getTitle() + "》所有子任务已完成!"); notification2.setRelatedId(mainTask.getId()); notification2.setRelatedType("task"); notification2.setPriority(1); notification2.setPushDingtalk(1); notificationService.sendNotification(notification2); log.info("已通知一级发起人 {} 任务全部完成", mainTask.getPrimaryPublisherId()); } result.put("mainTaskCompleted", true); } else { // 🔥 子任务完成但主任务未全部完成,更新主任务整体进度 updateMainTaskProgress(item.getTaskId()); result.put("mainTaskCompleted", false); result.put("progress", completedCount + "/" + totalCount); } return result; } /** * 🔥 同步更新主任务的整体进度 * 根据所有子任务的进度计算主任务的平均进度 */ private void updateMainTaskProgress(Long taskId) { try { // 查询该任务的所有子任务 List allItems = taskItemMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(TaskItem::getTaskId, taskId) ); if (allItems.isEmpty()) { return; } // 计算所有子任务的平均进度 int totalProgress = 0; for (TaskItem item : allItems) { totalProgress += (item.getProgress() != null ? item.getProgress() : 0); } int avgProgress = totalProgress / allItems.size(); // 更新主任务进度 Task mainTask = taskMapper.selectById(taskId); if (mainTask != null) { mainTask.setProgress(avgProgress); mainTask.setUpdateTime(LocalDateTime.now()); taskMapper.updateById(mainTask); log.info("主任务 {} 进度已更新为 {}%(基于 {} 个子任务的平均进度)", taskId, avgProgress, allItems.size()); } } catch (Exception e) { log.error("更新主任务进度失败: taskId={}", taskId, e); } } /** * 驳回子任务 */ @Transactional(rollbackFor = Exception.class) public void rejectItem(Long itemId, String reason) { TaskItem item = taskItemMapper.selectById(itemId); if (item == null) { throw new BusinessException(ResultCode.NOT_FOUND, "子任务不存在"); } item.setStatus("rejected"); item.setUpdateTime(LocalDateTime.now()); taskItemMapper.updateById(item); // 发送通知给接单人 if (item.getReceiverId() != null) { Notification notification = new Notification(); notification.setReceiverId(item.getReceiverId()); notification.setNotificationType("task"); notification.setTitle("子任务被驳回"); notification.setContent("您的子任务 " + item.getItemNo() + " 被驳回。原因:" + reason); notification.setRelatedId(itemId); notification.setRelatedType("task_item"); notification.setPriority(1); // 普通 notification.setPushDingtalk(1); // 推送到钉钉 notificationService.sendNotification(notification); } log.info("子任务 {} 已驳回", itemId); } /** * 获取主任务的进度统计 */ public Map getTaskProgress(Long taskId) { int completedCount = taskItemMapper.countCompletedItems(taskId); int totalCount = taskItemMapper.countTotalItems(taskId); Map progress = new HashMap<>(); progress.put("completedCount", completedCount); progress.put("totalCount", totalCount); progress.put("percentage", totalCount > 0 ? (completedCount * 100 / totalCount) : 0); return progress; } /** * 审核子任务组(接单部门负责人审核) * @param taskId 主任务ID * @param groupNo 组号 * @param approverId 审核人ID * @param assignedReceiverId 指定的接收人ID(可选) */ @Transactional(rollbackFor = Exception.class) public void approveTaskItemGroup(Long taskId, Integer groupNo, Long approverId, Long assignedReceiverId) { log.info("接单部门负责人 {} 审核任务 {} 的第 {} 组子任务", approverId, taskId, groupNo); // 1. 验证主任务 Task mainTask = taskMapper.selectById(taskId); if (mainTask == null) { throw new BusinessException(ResultCode.NOT_FOUND, "主任务不存在"); } // 2. 验证审核人权限(必须是接单部门负责人或管理员) com.designhub.entity.User approver = userService.getUserById(approverId); boolean isAdmin = approver != null && Integer.valueOf(1).equals(approver.getIsAdmin()); if (!isAdmin) { // 获取该组的子任务,检查审核人是否是对应部门的负责人 List checkItems = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getTaskId, taskId) .eq(TaskItem::getGroupNo, groupNo) .eq(TaskItem::getStatus, "pending_approval") .last("LIMIT 1") ); if (checkItems.isEmpty()) { throw new BusinessException(ResultCode.PARAM_ERROR, "该组没有待审核的子任务"); } // 从子任务的任务类型配置获取接单部门 TaskItem firstItem = checkItems.get(0); Long targetDeptId = getTargetDeptIdFromTaskType(firstItem.getTaskTypeId()); boolean hasPermission = false; // 检查任务类型配置的接单部门 if (targetDeptId != null) { com.designhub.entity.Department receiverDept = departmentMapper.selectById(targetDeptId); if (receiverDept != null && approverId.equals(receiverDept.getLeaderId())) { hasPermission = true; } } // 兼容:也检查主任务的 receiver_dept_id if (!hasPermission && mainTask.getReceiverDeptId() != null) { com.designhub.entity.Department receiverDept = departmentMapper.selectById(mainTask.getReceiverDeptId()); if (receiverDept != null && approverId.equals(receiverDept.getLeaderId())) { hasPermission = true; } } if (!hasPermission) { throw new BusinessException(ResultCode.FORBIDDEN, "只有接单部门负责人可以审核子任务组"); } } else { log.info("管理员 {} 代审核子任务组", approver.getRealName()); } // 3. 获取该组的所有待审核子任务 List groupItems = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getTaskId, taskId) .eq(TaskItem::getGroupNo, groupNo) .eq(TaskItem::getStatus, "pending_approval") ); if (groupItems.isEmpty()) { throw new BusinessException(ResultCode.PARAM_ERROR, "该组没有待审核的子任务"); } // 4. 🔥 优先检查子任务是否已预先指定了接收人(在明细完善阶段指定的) // 如果子任务已有接收人且审核人没有重新指定,则使用预先指定的接收人 if (assignedReceiverId == null) { // 检查第一个子任务是否已有预先指定的接收人 TaskItem firstItem = groupItems.get(0); if (firstItem.getReceiverId() != null) { assignedReceiverId = firstItem.getReceiverId(); log.info("子任务组已预先指定接收人: receiverId={}, 自动使用", assignedReceiverId); } } // 5. 如果指定了接收人(包括预先指定的),验证接收人并直接分配 if (assignedReceiverId != null) { com.designhub.entity.User receiver = userService.getUserById(assignedReceiverId); if (receiver == null) { throw new BusinessException(ResultCode.PARAM_ERROR, "指定的接收人不存在"); } LocalDateTime now = LocalDateTime.now(); // 批量分配给指定设计师 for (TaskItem item : groupItems) { item.setStatus("in_progress"); item.setReceiverId(assignedReceiverId); item.setReceiverDeptId(receiver.getDeptId()); // 设置接单部门ID item.setUpdateTime(now); // 计算并设置deadline(使用工作时间计算) if (item.getEstimatedHours() != null && item.getEstimatedHours().compareTo(java.math.BigDecimal.ZERO) > 0) { LocalDateTime deadline = workHoursCalculator.calculateDeadline(now, item.getEstimatedHours()); item.setDeadline(deadline); log.info("子任务组审核通过,子任务 {} 设置deadline: {}", item.getId(), deadline); } taskItemMapper.updateById(item); } // 发送通知给指定设计师 Notification notification = new Notification(); notification.setReceiverId(assignedReceiverId); notification.setNotificationType("task"); notification.setTitle("子任务已指派"); notification.setContent("负责人为您指派了任务《" + mainTask.getTitle() + "》的第 " + groupNo + " 组共 " + groupItems.size() + " 个子任务"); notification.setRelatedId(taskId); notification.setRelatedType("task"); notification.setPriority(2); notification.setPushDingtalk(1); // 推送到钉钉 notificationService.sendNotification(notification); // 🔥 一级需求任务:通知一级发起人子任务已指派 if (mainTask.getPrimaryPublisherId() != null) { com.designhub.entity.User designer = userService.getUserById(assignedReceiverId); String designerName = designer != null ? designer.getRealName() : "未知"; Notification notification2 = new Notification(); notification2.setReceiverId(mainTask.getPrimaryPublisherId()); notification2.setNotificationType("task"); notification2.setTitle("子任务已指派"); notification2.setContent("任务《" + mainTask.getTitle() + "》的第 " + groupNo + " 组已指派给设计师 " + designerName); notification2.setRelatedId(taskId); notification2.setRelatedType("task"); notification2.setPriority(1); notification2.setPushDingtalk(1); notificationService.sendNotification(notification2); log.info("已通知一级发起人 {} 子任务组已指派", mainTask.getPrimaryPublisherId()); } // 发送钉钉群通知并@指定的设计师 try { java.math.BigDecimal totalScore = groupItems.stream() .map(item -> item.getTotalScore() != null ? item.getTotalScore() : java.math.BigDecimal.ZERO) .reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add); String title = "子任务组已指派"; StringBuilder content = new StringBuilder(); content.append("-------------------------------- \n"); content.append(" 【 子 任 务 组 已 指 派 】 \n"); content.append("-------------------------------- \n\n"); content.append("主任务:").append(mainTask.getTitle()).append(" \n"); content.append("任务编号:").append(mainTask.getTaskNo()).append(" \n"); content.append("任务组:第 ").append(groupNo).append(" 组 \n"); content.append("子任务数量:").append(groupItems.size()).append(" 个 \n"); content.append("总积分:").append(totalScore).append(" 分 \n"); content.append("指派给:").append(receiver.getRealName()).append(" \n"); if (mainTask.getPublisherName() != null && !mainTask.getPublisherName().isEmpty()) { content.append("发起人:").append(mainTask.getPublisherName()).append(" \n"); } content.append(" \n"); content.append("-------------------------------- \n"); content.append("请尽快处理!qd.yydd.ai \n"); content.append("--------------------------------"); // @指定的设计师 java.util.List atMobiles = receiver.getPhone() != null ? java.util.Arrays.asList(receiver.getPhone()) : null; dingTalkService.sendRobotMessage(title, content.toString(), atMobiles); log.info("子任务组指派群通知已发送,@手机号: {}", receiver.getPhone()); } catch (Exception e) { log.error("发送子任务组指派群通知失败", e); } log.info("第 {} 组 {} 个子任务已指派给设计师 {}", groupNo, groupItems.size(), receiver.getRealName()); } else { // 未指定接收人,发布到任务大厅 for (TaskItem item : groupItems) { item.setStatus("pending"); item.setUpdateTime(LocalDateTime.now()); taskItemMapper.updateById(item); } // 发送钉钉群机器人通知(与第一轮格式完全一致) // 注意:钉钉Markdown换行需要两个空格+换行符 try { String title = "新任务发布"; StringBuilder content = new StringBuilder(); // 基本信息 content.append("**任务名称**:").append(mainTask.getTitle()).append(" \n"); content.append("**任务编号**:").append(mainTask.getTaskNo()).append(" \n"); // 任务详情 if (mainTask.getBrandName() != null && !mainTask.getBrandName().isEmpty()) { content.append("**品牌**:").append(mainTask.getBrandName()).append(" \n"); } if (mainTask.getPlatformName() != null && !mainTask.getPlatformName().isEmpty()) { content.append("**平台**:").append(mainTask.getPlatformName()).append(" \n"); } if (mainTask.getSpuInfo() != null && !mainTask.getSpuInfo().isEmpty()) { content.append("**SPU**:").append(mainTask.getSpuInfo()).append(" \n"); } if (mainTask.getTaskSubType() != null && !mainTask.getTaskSubType().isEmpty()) { content.append("**任务类型**:").append(mainTask.getTaskSubType()).append(" \n"); } content.append(" \n"); // 任务明细(与第一轮格式完全一致) if (!groupItems.isEmpty()) { content.append("**任务明细**: \n"); for (TaskItem item : groupItems) { // 通过任务类型ID获取接单部门名称 String deptName = "未指定"; if (item.getTaskTypeId() != null) { try { com.designhub.entity.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()); com.designhub.entity.Department dept = departmentMapper.selectById(firstDeptId); if (dept != null) { deptName = dept.getDeptName(); } } } catch (Exception e) { log.warn("获取部门名称失败: {}", e.getMessage()); } } // 格式化工时显示(与第一轮一致) String hoursDisplay = formatEstimatedHours(item.getEstimatedHours()); content.append(" \n"); content.append("▸ **").append(deptName).append("** | ") .append(item.getTotalScore()).append("分 | ") .append(hoursDisplay).append(" \n"); // 解析并显示任务类型明细 if (item.getTypeDetails() != null && !item.getTypeDetails().isEmpty()) { try { @SuppressWarnings("unchecked") java.util.List> typeDetails = objectMapper.readValue(item.getTypeDetails(), java.util.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()); } } } } content.append(" \n"); // 发起人信息 if (mainTask.getPublisherName() != null && !mainTask.getPublisherName().isEmpty()) { content.append("**发起人**:").append(mainTask.getPublisherName()).append(" \n"); } if (mainTask.getPublisherDeptName() != null && !mainTask.getPublisherDeptName().isEmpty()) { content.append("**发起部门**:").append(mainTask.getPublisherDeptName()).append(" \n"); } content.append(" \n"); content.append("点击下方按钮查看详情"); log.info("准备调用钉钉机器人发送消息,title={}", title); boolean success = dingTalkService.sendRobotMessage(title, content.toString()); if (success) { log.info("第{}组子任务钉钉群通知发送成功", groupNo); } else { log.warn("第{}组子任务钉钉群通知发送失败(返回false)", groupNo); } } catch (Exception e) { log.error("发送钉钉群通知异常:taskId={}, groupNo={}, error={}", taskId, groupNo, e.getMessage(), e); // 通知发送失败不影响主流程 } } // 🔥 更新主任务状态为已审核通过 mainTask.setApprovalStatus("approved"); mainTask.setStatus("pending"); mainTask.setReceivingApprovalTime(LocalDateTime.now()); mainTask.setCurrentApproverId(null); mainTask.setCurrentApproverRole(null); mainTask.setUpdateTime(LocalDateTime.now()); taskMapper.updateById(mainTask); log.info("主任务 {} 接单部门审核通过,状态更新为 approved/pending", taskId); } /** * 驳回子任务组(接单部门负责人审核不通过) * 逻辑:驳回该组所有子任务,并将主任务及其所有子任务设为驳回状态 */ @Transactional(rollbackFor = Exception.class) public void rejectTaskGroup(Long taskId, Integer groupNo, Long rejectorId, String reason) { log.info("接单部门负责人 {} 驳回任务 {} 的第 {} 组子任务,原因:{}", rejectorId, taskId, groupNo, reason); // 1. 验证主任务 Task mainTask = taskMapper.selectById(taskId); if (mainTask == null) { throw new BusinessException(ResultCode.NOT_FOUND, "主任务不存在"); } // 2. 验证审核人权限(必须是接单部门负责人或管理员) com.designhub.entity.User rejector = userService.getUserById(rejectorId); boolean isAdmin = rejector != null && Integer.valueOf(1).equals(rejector.getIsAdmin()); if (!isAdmin) { boolean hasPermission = false; if (mainTask.getReceiverDeptId() != null) { // 如果任务已指定接单部门,检查用户是否是该部门的负责人 com.designhub.entity.Department receiverDept = departmentMapper.selectById(mainTask.getReceiverDeptId()); if (receiverDept != null && rejectorId.equals(receiverDept.getLeaderId())) { hasPermission = true; log.info("用户 {} 是任务接单部门 {} 的负责人", rejectorId, mainTask.getReceiverDeptId()); } } else { // 如果任务未指定接单部门,检查用户是否是任何接单部门的负责人 List receivingDepts = departmentMapper.selectList( new LambdaQueryWrapper() .eq(com.designhub.entity.Department::getLeaderId, rejectorId) .eq(com.designhub.entity.Department::getDeptType, "receiving") .eq(com.designhub.entity.Department::getStatus, 1) .eq(com.designhub.entity.Department::getDeleted, 0) ); if (!receivingDepts.isEmpty()) { hasPermission = true; log.info("用户 {} 是接单部门负责人(管理的接单部门:{}),允许驳回未指定接单部门的任务", rejectorId, receivingDepts.stream().map(com.designhub.entity.Department::getDeptName).collect(java.util.stream.Collectors.toList())); } } if (!hasPermission) { throw new BusinessException(ResultCode.FORBIDDEN, "只有接单部门负责人可以驳回子任务组"); } } else { log.info("管理员 {} 代驳回子任务组", rejector.getRealName()); } // 3. 更新主任务状态为 rejected mainTask.setStatus("rejected"); mainTask.setRejectReason(reason); mainTask.setUpdateTime(LocalDateTime.now()); taskMapper.updateById(mainTask); // 4. 更新该任务下所有子任务状态为 rejected taskItemMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() .eq(TaskItem::getTaskId, taskId) .set(TaskItem::getStatus, "rejected") .set(TaskItem::getUpdateTime, LocalDateTime.now()) ); log.info("任务 {} 及其所有子任务已被驳回", taskId); // 5. 发送通知给任务发布者 if (mainTask.getPublisherId() != null) { Notification notification = new Notification(); notification.setReceiverId(mainTask.getPublisherId()); notification.setNotificationType("task_rejected"); notification.setTitle("任务被驳回"); notification.setContent("您的任务《" + mainTask.getTitle() + "》在第 " + groupNo + " 组子任务审核时被驳回。原因:" + reason + "。请重新编辑并提交。"); notification.setRelatedId(taskId); notification.setRelatedType("task"); notification.setPriority(2); // 重要 notification.setPushDingtalk(1); // 推送到钉钉 notificationService.sendNotification(notification); } // 6. 查找该任务组中有处理人的子任务,发送群通知@处理人 try { List groupItems = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getTaskId, taskId) .eq(TaskItem::getGroupNo, groupNo) ); // 收集所有处理人的手机号 java.util.Set atMobiles = new java.util.HashSet<>(); java.util.Set receiverNames = new java.util.HashSet<>(); for (TaskItem item : groupItems) { if (item.getReceiverId() != null) { com.designhub.entity.User receiver = userService.getUserById(item.getReceiverId()); if (receiver != null) { if (receiver.getPhone() != null) { atMobiles.add(receiver.getPhone()); } if (receiver.getRealName() != null) { receiverNames.add(receiver.getRealName()); } } } } // 如果有处理人,发送群通知 if (!receiverNames.isEmpty()) { String title = "子任务组已驳回"; StringBuilder content = new StringBuilder(); content.append("-------------------------------- \n"); content.append(" 【 子 任 务 组 已 驳 回 】 \n"); content.append("-------------------------------- \n\n"); content.append("主任务:").append(mainTask.getTitle()).append(" \n"); content.append("任务编号:").append(mainTask.getTaskNo()).append(" \n"); content.append("任务组:第 ").append(groupNo).append(" 组 \n"); content.append("原处理人:").append(String.join("、", receiverNames)).append(" \n"); content.append("驳回原因:").append(reason).append(" \n"); content.append(" \n"); content.append("--------------------------------"); dingTalkService.sendRobotMessage(title, content.toString(), atMobiles.isEmpty() ? null : new java.util.ArrayList<>(atMobiles)); } } catch (Exception e) { log.error("发送子任务组驳回群通知失败", e); } } /** * 获取待审核的子任务组列表(按主任务分组) * @param receiverDeptId 接单部门ID,null表示查询所有(管理员) */ public List> getPendingApprovalGroups(Long receiverDeptId) { log.info("===== getPendingApprovalGroups 开始 ====="); log.info("查询参数 receiverDeptId: {}", receiverDeptId); // 查询所有有待审核子任务组的主任务 List pendingItems = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getStatus, "pending_approval") ); log.info("找到 {} 个 pending_approval 状态的子任务", pendingItems.size()); // 按主任务和组号分组 Map> groupMap = new HashMap<>(); for (TaskItem item : pendingItems) { log.info("处理子任务 ID: {}, item_no: {}, task_type_id: {}", item.getId(), item.getItemNo(), item.getTaskTypeId()); Task mainTask = taskMapper.selectById(item.getTaskId()); if (mainTask == null) { log.warn("子任务 {} 的主任务不存在,跳过", item.getId()); continue; } // 判断是否有权限查看该子任务 boolean hasPermission = false; if (receiverDeptId == null) { // 管理员可以看到所有任务 hasPermission = true; log.info("管理员模式,有权限"); } else { // 部门负责人:检查子任务的任务类型配置的 allowed_dept_ids 是否包含该部门 Long targetDeptId = getTargetDeptIdFromTaskType(item.getTaskTypeId()); log.info("子任务 {} 的目标部门ID: {}, 查询的部门ID: {}", item.getId(), targetDeptId, receiverDeptId); if (targetDeptId != null && targetDeptId.equals(receiverDeptId)) { hasPermission = true; log.info("匹配成功(通过任务类型配置)"); } // 兼容:也检查主任务的 receiver_dept_id if (!hasPermission && mainTask.getReceiverDeptId() != null && mainTask.getReceiverDeptId().equals(receiverDeptId)) { hasPermission = true; log.info("匹配成功(通过主任务接单部门)"); } } log.info("子任务 {} 权限检查结果: {}", item.getId(), hasPermission); if (hasPermission) { String key = item.getTaskId() + "-" + item.getGroupNo(); if (!groupMap.containsKey(key)) { Map groupInfo = new HashMap<>(); groupInfo.put("taskId", item.getTaskId()); groupInfo.put("taskTitle", mainTask.getTitle()); groupInfo.put("taskNo", mainTask.getTaskNo()); groupInfo.put("groupNo", item.getGroupNo()); groupInfo.put("groupId", item.getGroupNo()); groupInfo.put("itemCount", 1); groupInfo.put("createTime", item.getCreateTime()); // 添加主任务的完整信息 groupInfo.put("brandId", mainTask.getBrandId()); groupInfo.put("brandName", mainTask.getBrandName()); groupInfo.put("categoryId", mainTask.getCategoryId()); groupInfo.put("categoryName", mainTask.getCategoryName()); groupInfo.put("platformId", mainTask.getPlatformId()); groupInfo.put("platformName", mainTask.getPlatformName()); groupInfo.put("spuInfo", mainTask.getSpuInfo()); groupInfo.put("quantity", mainTask.getQuantity()); groupInfo.put("previewImage", mainTask.getPreviewImage()); groupInfo.put("attachmentUrl", mainTask.getAttachmentUrl()); groupInfo.put("remark", mainTask.getRemark()); // 使用子任务自己的typeDetails(子任务创建时已经保存了明细) groupInfo.put("typeDetails", item.getTypeDetails()); // 一级需求部门发起人信息(对于一级需求部门任务) groupInfo.put("primaryPublisherId", mainTask.getPrimaryPublisherId()); groupInfo.put("primaryPublisherDeptId", mainTask.getPrimaryPublisherDeptId()); if (mainTask.getPrimaryPublisherId() != null) { com.designhub.entity.User primaryPublisher = userMapper.selectById(mainTask.getPrimaryPublisherId()); if (primaryPublisher != null) { groupInfo.put("primaryPublisherName", primaryPublisher.getRealName()); } } if (mainTask.getPrimaryPublisherDeptId() != null) { com.designhub.entity.Department primaryDept = departmentMapper.selectById(mainTask.getPrimaryPublisherDeptId()); if (primaryDept != null) { groupInfo.put("primaryPublisherDeptName", primaryDept.getDeptName()); } } // 是否是一级需求部门任务 groupInfo.put("isPrimaryTask", mainTask.getIsPrimaryTask()); // 明细完善人信息(需求部门成员) groupInfo.put("detailCompleterId", mainTask.getDetailCompleterId()); if (mainTask.getDetailCompleterId() != null) { com.designhub.entity.User detailCompleter = userMapper.selectById(mainTask.getDetailCompleterId()); if (detailCompleter != null) { groupInfo.put("detailCompleterName", detailCompleter.getRealName()); } } // 发起人信息:对于一级需求任务,发起人显示为明细完善人;否则显示原始发起人 if (mainTask.getIsPrimaryTask() != null && mainTask.getIsPrimaryTask() == 1 && mainTask.getDetailCompleterId() != null) { // 一级需求任务:发起人 = 明细完善人 groupInfo.put("publisherId", mainTask.getDetailCompleterId()); com.designhub.entity.User detailCompleter = userMapper.selectById(mainTask.getDetailCompleterId()); if (detailCompleter != null) { groupInfo.put("publisherName", detailCompleter.getRealName()); groupInfo.put("publisherDeptName", detailCompleter.getDeptName()); } } else { // 普通任务:发起人 = 原始发起人 groupInfo.put("publisherId", mainTask.getPublisherId()); if (mainTask.getPublisherId() != null) { com.designhub.entity.User publisher = userMapper.selectById(mainTask.getPublisherId()); if (publisher != null) { groupInfo.put("publisherName", publisher.getRealName()); groupInfo.put("publisherDeptName", publisher.getDeptName()); } } } // 主任务的任务类型信息 groupInfo.put("taskTypeId", mainTask.getTaskTypeId()); // 使用taskSubType显示一级分类(新品/优化/改图等) String taskSubType = mainTask.getTaskSubType(); if (taskSubType != null && !taskSubType.isEmpty()) { // 转换为中文显示 String taskTypeName = switch (taskSubType) { case "new" -> "新品"; case "optimize" -> "优化"; case "redesign" -> "改图"; case "other" -> "其他"; default -> taskSubType; }; groupInfo.put("taskTypeName", taskTypeName); groupInfo.put("taskSubType", taskSubType); } else if (mainTask.getTaskTypeId() != null) { // 兼容:如果没有taskSubType,使用taskTypeId查询 com.designhub.entity.DemandType taskType = demandTypeMapper.selectById(mainTask.getTaskTypeId()); if (taskType != null) { groupInfo.put("taskTypeName", taskType.getName()); } } // 子任务的具体任务类型ID(用于审核流程) groupInfo.put("subTaskTypeId", item.getTaskTypeId()); // 添加审核人信息(目标部门负责人) Long targetDeptId = getTargetDeptIdFromTaskType(item.getTaskTypeId()); if (targetDeptId != null) { com.designhub.entity.Department targetDept = departmentMapper.selectById(targetDeptId); if (targetDept != null && targetDept.getLeaderId() != null) { com.designhub.entity.User leader = userMapper.selectById(targetDept.getLeaderId()); if (leader != null) { groupInfo.put("currentApproverName", leader.getRealName()); groupInfo.put("currentApproverRole", "receiving_leader"); groupInfo.put("approvalStatus", "pending_receiving_approval"); } } } // 计算子任务组总积分(BigDecimal类型) groupInfo.put("totalScore", item.getTotalScore() != null ? item.getTotalScore() : java.math.BigDecimal.ZERO); // 获取前置子任务的素材下载链接(流水线继承) if (item.getGroupNo() != null && item.getGroupNo() > 1) { // 查找前一组已完成的子任务的下载链接 List prevGroupItems = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getTaskId, item.getTaskId()) .eq(TaskItem::getGroupNo, item.getGroupNo() - 1) .eq(TaskItem::getStatus, "completed") ); if (!prevGroupItems.isEmpty()) { // 收集所有前置子任务的下载链接 List prevDownloadUrls = prevGroupItems.stream() .filter(pi -> pi.getDownloadUrl() != null && !pi.getDownloadUrl().isEmpty()) .map(TaskItem::getDownloadUrl) .collect(java.util.stream.Collectors.toList()); if (!prevDownloadUrls.isEmpty()) { groupInfo.put("prevDownloadUrls", prevDownloadUrls); } } } // 存储子任务列表,用于显示详情 List> itemsList = new java.util.ArrayList<>(); Map itemInfo = new HashMap<>(); itemInfo.put("id", item.getId()); itemInfo.put("itemNo", item.getItemNo()); itemInfo.put("taskTypeId", item.getTaskTypeId()); itemInfo.put("score", item.getTotalScore()); itemsList.add(itemInfo); groupInfo.put("items", itemsList); groupMap.put(key, groupInfo); } else { Map groupInfo = groupMap.get(key); groupInfo.put("itemCount", (Integer) groupInfo.get("itemCount") + 1); // 累加积分(BigDecimal类型) java.math.BigDecimal currentScore = (java.math.BigDecimal) groupInfo.get("totalScore"); java.math.BigDecimal itemScore = item.getTotalScore() != null ? item.getTotalScore() : java.math.BigDecimal.ZERO; groupInfo.put("totalScore", currentScore.add(itemScore)); // 添加到子任务列表 @SuppressWarnings("unchecked") List> itemsList = (List>) groupInfo.get("items"); Map itemInfo = new HashMap<>(); itemInfo.put("id", item.getId()); itemInfo.put("itemNo", item.getItemNo()); itemInfo.put("taskTypeId", item.getTaskTypeId()); itemInfo.put("score", item.getTotalScore()); itemsList.add(itemInfo); } } } return new java.util.ArrayList<>(groupMap.values()); } /** * 从子任务编号解析部门序号 * 编号格式:任务编号-部门序号-部门内序号,例如 TASK20260114-01-02 * @param itemNo 子任务编号 * @return 部门序号,解析失败返回1 */ private int parseDeptIndexFromItemNo(String itemNo) { if (itemNo == null || itemNo.isEmpty()) { return 1; } String[] parts = itemNo.split("-"); if (parts.length >= 3) { try { // 新格式:TASK...-部门序号-部门内序号 // 倒数第二段是部门序号 return Integer.parseInt(parts[parts.length - 2]); } catch (NumberFormatException e) { log.warn("解析子任务编号部门序号失败: {}", itemNo); } } else if (parts.length == 2) { // 旧格式:TASK...-序号,认为是第一个部门 return 1; } return 1; } /** * 从任务类型配置中获取目标接单部门ID * 如果当前类型未配置,则递归向上查找父级类型的配置 */ private Long getTargetDeptIdFromTaskType(Long taskTypeId) { log.debug("getTargetDeptIdFromTaskType 调用, taskTypeId: {}", taskTypeId); if (taskTypeId == null) { log.debug("taskTypeId 为空,返回 null"); return null; } com.designhub.entity.DemandType taskType = demandTypeMapper.selectById(taskTypeId); if (taskType == null) { log.warn("任务类型 {} 不存在", taskTypeId); return null; } log.debug("任务类型: id={}, name={}, parentId={}, allowedDeptIds={}", taskType.getId(), taskType.getName(), taskType.getParentId(), taskType.getAllowedDeptIds()); // 如果当前类型配置了allowed_dept_ids,返回第一个部门ID String allowedDeptIds = taskType.getAllowedDeptIds(); if (allowedDeptIds != null && !allowedDeptIds.isEmpty()) { try { String[] deptIds = allowedDeptIds.split(","); if (deptIds.length > 0) { Long result = Long.parseLong(deptIds[0].trim()); log.debug("找到 allowed_dept_ids: {}, 返回第一个: {}", allowedDeptIds, result); return result; } } catch (NumberFormatException e) { log.warn("解析allowed_dept_ids失败: {}", allowedDeptIds, e); } } // 如果当前类型未配置,递归查找父级 if (taskType.getParentId() != null) { log.debug("当前类型未配置 allowed_dept_ids, 递归查找父级: {}", taskType.getParentId()); return getTargetDeptIdFromTaskType(taskType.getParentId()); } log.debug("未找到配置的部门,返回 null"); return null; } /** * 格式化工时显示(基于工作时间配置) */ private String formatEstimatedHours(java.math.BigDecimal hours) { if (hours == null) { return "未设置"; } String formatted = workHoursCalculator.formatWorkHours(hours); // WorkHoursCalculator 返回 "--" 表示无效,这里保持 "未设置" 的语义 return "--".equals(formatted) ? "未设置" : formatted; } }