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 lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 任务子项服务类(流水线任务) */ @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; /** * 获取可接单的子任务列表 */ public IPage getAvailableItems(Integer page, Integer pageSize, Long taskTypeId, String keyword) { Page pageParam = new Page<>(page, pageSize); return taskItemMapper.selectAvailableItems(pageParam, taskTypeId, keyword); } /** * 获取进行中的子任务列表 */ public IPage getInProgressItems(Integer page, Integer pageSize, Long receiverId) { Page pageParam = new Page<>(page, pageSize); return taskItemMapper.selectInProgressItems(pageParam, receiverId); } /** * 获取已完成的子任务列表 */ public IPage getCompletedItems(Integer page, Integer pageSize, Long receiverId) { Page pageParam = new Page<>(page, pageSize); return taskItemMapper.selectCompletedItems(pageParam, receiverId); } /** * 根据任务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(","); boolean isAllowed = false; for (String allowedId : allowedIds) { if (allowedId.trim().equals(String.valueOf(user.getDeptId()))) { isAllowed = true; 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, "该子任务已被锁定,请等待前序任务完成"); } if (item.getReceiverId() != null) { throw new BusinessException(ResultCode.TASK_ALREADY_TAKEN, "该子任务已被接单"); } // 乐观锁抢单 int result = taskItemMapper.grabItem(itemId, userId, user.getRealName()); if (result == 0) { throw new BusinessException(ResultCode.TASK_ALREADY_TAKEN, "抢单失败,该子任务可能已被他人接单"); } log.info("设计师 {} 成功抢单子任务 {}", userId, itemId); // 更新主任务状态 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); } } } /** * 更新子任务进度 */ @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之间"); } // 更新进度 item.setProgress(progress); item.setUpdateTime(LocalDateTime.now()); taskItemMapper.updateById(item); log.info("子任务 {} 进度已更新为 {}%", itemId, progress); } /** * 完结子任务(关键方法:包含自动解锁逻辑) * @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验证任务完成状态 String taskNo = item.getItemNo(); // 使用子任务编号作为任务号 try { downloadUrl = externalAssetService.checkTaskCompletion(taskNo); } catch (Exception e) { log.warn("调用第三方素材API失败: {}", e.getMessage()); // API调用失败时,提示用户 throw new BusinessException(ResultCode.ERROR, "素材API验证失败,请稍后重试或联系管理员"); } if (downloadUrl == null) { // 如果第三方API未返回下载链接,提示用户去上传素材 throw new BusinessException(ResultCode.ERROR, "素材未提交,请先前往 3D.YYDD.AI 提交任务素材后再完结任务"); } log.info("第三方素材API返回下载链接: {}", downloadUrl); } else { // 未启用第三方API,允许直接完结任务 log.info("第三方素材API未启用,直接完结任务: itemId={}", itemId); // 如果有传入deliveryPath,则使用它 if (deliveryPath != null && !deliveryPath.trim().isEmpty()) { downloadUrl = deliveryPath; } } // 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. 🔥 关键步骤:检查是否是顺序流水线,基于group_no解锁下一组 Task mainTask = taskMapper.selectById(item.getTaskId()); if (mainTask != null && "sequential".equals(mainTask.getPipelineMode())) { // 获取当前子任务的分组号 Integer currentGroupNo = item.getGroupNo(); if (currentGroupNo == null) { currentGroupNo = 1; // 兼容旧数据 } // 检查当前组的所有子任务是否都已完成 List currentGroupItems = taskItemMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(TaskItem::getTaskId, item.getTaskId()) .eq(TaskItem::getGroupNo, currentGroupNo) ); boolean allGroupCompleted = currentGroupItems.stream() .allMatch(i -> "completed".equals(i.getStatus())); if (allGroupCompleted) { log.info("✅ 第 {} 组所有子任务已完成,准备解锁下一组", currentGroupNo); // 🔥 收集当前组所有子任务的下载链接,传递给下一组 StringBuilder prevDownloadUrls = new StringBuilder(); for (TaskItem groupItem : currentGroupItems) { if (groupItem.getDownloadUrl() != null && !groupItem.getDownloadUrl().isEmpty()) { if (prevDownloadUrls.length() > 0) { prevDownloadUrls.append(","); } prevDownloadUrls.append(groupItem.getDownloadUrl()); } // 同时继承前面环节的下载链接 if (groupItem.getPrevDownloadUrl() != null && !groupItem.getPrevDownloadUrl().isEmpty()) { if (prevDownloadUrls.length() > 0) { prevDownloadUrls.append(","); } prevDownloadUrls.append(groupItem.getPrevDownloadUrl()); } } String prevDownloadUrlStr = prevDownloadUrls.toString(); log.info("前置环节素材下载链接: {}", prevDownloadUrlStr); // 解锁下一组的所有子任务 Integer nextGroupNo = currentGroupNo + 1; List nextGroupItems = taskItemMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(TaskItem::getTaskId, item.getTaskId()) .eq(TaskItem::getGroupNo, nextGroupNo) .eq(TaskItem::getStatus, "locked") ); if (!nextGroupItems.isEmpty()) { // 批量解锁下一组的所有子任务,状态设为待审核,并传递前置环节的下载链接 for (TaskItem nextItem : nextGroupItems) { nextItem.setStatus("pending_approval"); // 待接单部门负责人审核 nextItem.setIsLocked(0); nextItem.setPrevDownloadUrl(prevDownloadUrlStr); // 🔥 传递前置环节的下载链接 nextItem.setUpdateTime(LocalDateTime.now()); taskItemMapper.updateById(nextItem); log.info("✅ 解锁子任务: {} (组号: {}, ID: {}),待接单部门负责人审核,已传递前置素材链接", nextItem.getItemNo(), nextGroupNo, nextItem.getId()); } result.put("unlockedGroupNo", nextGroupNo); result.put("unlockedCount", nextGroupItems.size()); // 发送通知给接单部门负责人 if (mainTask.getReceiverDeptId() != null) { com.designhub.entity.Department receiverDept = departmentMapper.selectById(mainTask.getReceiverDeptId()); if (receiverDept != null && receiverDept.getLeaderId() != null) { Notification notification = new Notification(); notification.setReceiverId(receiverDept.getLeaderId()); notification.setNotificationType("task_approval"); notification.setTitle("新子任务组待审核"); notification.setContent("任务《" + mainTask.getTitle() + "》第 " + nextGroupNo + " 组共 " + nextGroupItems.size() + " 个子任务已解锁,请审核并分配!"); notification.setRelatedId(item.getTaskId()); notification.setRelatedType("task"); notification.setPriority(2); // 重要 notification.setPushDingtalk(1); // 推送到钉钉 notificationService.sendNotification(notification); log.info("已通知接单部门负责人(ID: {})审核第 {} 组子任务", receiverDept.getLeaderId(), nextGroupNo); } } log.info("第 {} 组 {} 个子任务已解锁,等待接单部门负责人审核", nextGroupNo, nextGroupItems.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.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); } result.put("mainTaskCompleted", true); } else { result.put("mainTaskCompleted", false); result.put("progress", completedCount + "/" + totalCount); } return result; } /** * 驳回子任务 */ @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) { if (mainTask.getReceiverDeptId() == null) { throw new BusinessException(ResultCode.FORBIDDEN, "任务未指定接单部门"); } com.designhub.entity.Department receiverDept = departmentMapper.selectById(mainTask.getReceiverDeptId()); if (receiverDept == null || !approverId.equals(receiverDept.getLeaderId())) { 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) { com.designhub.entity.User receiver = userService.getUserById(assignedReceiverId); if (receiver == null) { throw new BusinessException(ResultCode.PARAM_ERROR, "指定的接收人不存在"); } // 批量分配给指定设计师 for (TaskItem item : groupItems) { item.setStatus("in_progress"); item.setReceiverId(assignedReceiverId); item.setUpdateTime(LocalDateTime.now()); 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); // 发送钉钉群通知并@指定的设计师 try { int totalScore = groupItems.stream().mapToInt(item -> item.getTotalScore() != null ? item.getTotalScore() : 0).sum(); 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("-------------------------------- \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"); // 计算总积分 int totalScore = groupItems.stream().mapToInt(item -> item.getTotalScore() != null ? item.getTotalScore() : 0).sum(); content.append("总积分:").append(totalScore).append(" 分 \n"); // 任务需求类型 if (mainTask.getTaskTypeIds() != null && !mainTask.getTaskTypeIds().isEmpty()) { StringBuilder typeNames = new StringBuilder(); String[] typeIds = mainTask.getTaskTypeIds().split(","); for (String typeIdStr : typeIds) { try { Long typeId = Long.parseLong(typeIdStr.trim()); com.designhub.entity.DemandType demandType = configService.getDemandTypeById(typeId); if (demandType != null) { if (typeNames.length() > 0) { typeNames.append("、"); } typeNames.append(demandType.getName()); } } catch (NumberFormatException e) { log.warn("解析任务类型ID失败:{}", typeIdStr); } } if (typeNames.length() > 0) { content.append("需求类型:").append(typeNames.toString()).append(" \n"); } } content.append(" \n"); // 任务详情 if (mainTask.getBrandName() != null && !mainTask.getBrandName().isEmpty()) { content.append("品牌:").append(mainTask.getBrandName()).append(" \n"); } if (mainTask.getCategoryName() != null && !mainTask.getCategoryName().isEmpty()) { content.append("类目:").append(mainTask.getCategoryName()).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"); } content.append(" \n"); // 列出子任务详情 content.append("子任务列表: \n"); for (TaskItem item : groupItems) { content.append(" - ").append(item.getItemNo()) .append("(").append(item.getTotalScore() != null ? item.getTotalScore() : 0).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("-------------------------------- \n"); content.append("前往抢单:qd.yydd.ai \n"); content.append("--------------------------------"); dingTalkService.sendRobotMessage(title, content.toString()); log.info("第 {} 组 {} 个子任务已发布到任务大厅,已发送钉钉群通知", groupNo, groupItems.size()); } catch (Exception e) { log.error("发送钉钉群通知失败:taskId={}, groupNo={}", taskId, groupNo, e); // 通知发送失败不影响主流程 } } } /** * 驳回子任务组(接单部门负责人审核不通过) * 逻辑:驳回该组所有子任务,并将主任务及其所有子任务设为驳回状态 */ @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) { if (mainTask.getReceiverDeptId() == null) { throw new BusinessException(ResultCode.FORBIDDEN, "任务未指定接单部门"); } com.designhub.entity.Department receiverDept = departmentMapper.selectById(mainTask.getReceiverDeptId()); if (receiverDept == null || !rejectorId.equals(receiverDept.getLeaderId())) { 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) { // 查询所有有待审核子任务组的主任务 List pendingItems = taskItemMapper.selectList( new LambdaQueryWrapper() .eq(TaskItem::getStatus, "pending_approval") ); // 按主任务和组号分组 Map> groupMap = new HashMap<>(); for (TaskItem item : pendingItems) { Task mainTask = taskMapper.selectById(item.getTaskId()); // receiverDeptId为null表示管理员,可以看到所有任务 if (mainTask != null && (receiverDeptId == null || receiverDeptId.equals(mainTask.getReceiverDeptId()))) { 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()); // 前端使用groupId作为唯一标识 groupInfo.put("itemCount", 1); groupInfo.put("createTime", item.getCreateTime()); groupMap.put(key, groupInfo); } else { Map groupInfo = groupMap.get(key); groupInfo.put("itemCount", (Integer) groupInfo.get("itemCount") + 1); } } } return new java.util.ArrayList<>(groupMap.values()); } }