1230 lines
60 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.designhub.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.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;
/**
* 任务子项服务类(流水线任务)
*/
@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;
/**
* 获取可接单的子任务列表
*/
public IPage<TaskItem> getAvailableItems(Integer page, Integer pageSize, Long taskTypeId, String keyword) {
Page<TaskItem> pageParam = new Page<>(page, pageSize);
return taskItemMapper.selectAvailableItems(pageParam, taskTypeId, keyword);
}
/**
* 获取进行中的子任务列表
* @param receiverId 接单人ID,null表示不按接单人过滤
* @param deptId 部门ID,用于部门负责人查看本部门所有任务
* @param keyword 搜索关键词
*/
public IPage<TaskItem> getInProgressItems(Integer page, Integer pageSize, Long receiverId, Long deptId, String keyword) {
Page<TaskItem> pageParam = new Page<>(page, pageSize);
return taskItemMapper.selectInProgressItems(pageParam, receiverId, deptId, keyword);
}
/**
* 获取进行中的子任务列表(向后兼容)
*/
public IPage<TaskItem> getInProgressItems(Integer page, Integer pageSize, Long receiverId, Long deptId) {
return getInProgressItems(page, pageSize, receiverId, deptId, null);
}
/**
* 获取进行中的子任务列表(向后兼容)
*/
public IPage<TaskItem> getInProgressItems(Integer page, Integer pageSize, Long receiverId) {
return getInProgressItems(page, pageSize, receiverId, null, null);
}
/**
* 获取进行中的子任务列表(部门负责人:按多个部门ID过滤)
* @param deptIds 部门ID列表
* @param keyword 搜索关键词
*/
public IPage<TaskItem> getInProgressItemsByDeptIds(Integer page, Integer pageSize, List<Long> deptIds, String keyword) {
Page<TaskItem> pageParam = new Page<>(page, pageSize);
return taskItemMapper.selectInProgressItemsByDeptIds(pageParam, deptIds, keyword);
}
/**
* 获取已完成的子任务列表
* @param receiverId 接单人ID,null表示不按接单人过滤
* @param deptId 部门ID,用于部门负责人查看本部门所有任务
* @param keyword 搜索关键词
*/
public IPage<TaskItem> getCompletedItems(Integer page, Integer pageSize, Long receiverId, Long deptId, String keyword) {
Page<TaskItem> pageParam = new Page<>(page, pageSize);
return taskItemMapper.selectCompletedItems(pageParam, receiverId, deptId, keyword);
}
/**
* 获取已完成的子任务列表(向后兼容)
*/
public IPage<TaskItem> getCompletedItems(Integer page, Integer pageSize, Long receiverId, Long deptId) {
return getCompletedItems(page, pageSize, receiverId, deptId, null);
}
/**
* 获取已完成的子任务列表(向后兼容)
*/
public IPage<TaskItem> getCompletedItems(Integer page, Integer pageSize, Long receiverId) {
return getCompletedItems(page, pageSize, receiverId, null, null);
}
/**
* 获取已完成的子任务列表(部门负责人:按多个部门ID过滤)
* @param deptIds 部门ID列表
* @param keyword 搜索关键词
*/
public IPage<TaskItem> getCompletedItemsByDeptIds(Integer page, Integer pageSize, List<Long> deptIds, String keyword) {
Page<TaskItem> pageParam = new Page<>(page, pageSize);
return taskItemMapper.selectCompletedItemsByDeptIds(pageParam, deptIds, keyword);
}
/**
* 根据任务ID获取所有子任务
*/
public List<TaskItem> 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, "该子任务已被接单");
}
// 乐观锁抢单(同时设置接单部门ID)
int result = taskItemMapper.grabItem(itemId, userId, user.getRealName(), user.getDeptId());
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之间");
}
// 记录更新前的进度
Integer oldProgress = item.getProgress() != null ? item.getProgress() : 0;
// 更新进度
item.setProgress(progress);
item.setUpdateTime(LocalDateTime.now());
taskItemMapper.updateById(item);
log.info("子任务 {} 进度已更新为 {}%", itemId, progress);
// 获取主任务信息,通知需求者(发布者)
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);
}
} catch (Exception e) {
// 通知失败不影响主流程
log.error("发送进度更新通知给需求者失败: itemId={}", itemId, e);
}
}
/**
* 完结子任务(关键方法:包含自动解锁逻辑)
* @param itemId 子任务ID
* @param userId 用户ID
* @param deliveryPath 交付路径(可选,如果启用第三方API则自动获取)
*/
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> 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<String, Object> 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<TaskItem> currentGroupItems = taskItemMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TaskItem>()
.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<TaskItem> nextGroupItems = taskItemMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TaskItem>()
.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());
// 发送通知给接单部门负责人
// 从子任务的任务类型配置中获取allowed_dept_ids
Set<Long> notifiedLeaderIds = new HashSet<>();
for (TaskItem nextItem : nextGroupItems) {
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() + "》第 " + nextGroupNo + " 组共 " +
nextGroupItems.size() + " 个子任务已解锁,请审核并分配!");
notification.setRelatedId(item.getTaskId());
notification.setRelatedType("task");
notification.setPriority(2); // 重要
notification.setPushDingtalk(1); // 推送到钉钉
notificationService.sendNotification(notification);
log.info("已通知接单部门负责人(部门: {}, 负责人ID: {})审核第 {} 组子任务",
receiverDept.getDeptName(), receiverDept.getLeaderId(), nextGroupNo);
}
} else {
log.warn("子任务 {} 的任务类型未配置接单部门", nextItem.getItemNo());
}
}
log.info("第 {} 组 {} 个子任务已解锁,等待接单部门负责人审核,已通知 {} 个部门负责人",
nextGroupNo, nextGroupItems.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<TaskItem>()
.eq(TaskItem::getTaskId, item.getTaskId())
.orderByDesc(TaskItem::getGroupNo)
.last("LIMIT 1")
).stream().findFirst().map(TaskItem::getGroupNo).orElse(1);
List<TaskItem> lastGroupItems = taskItemMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TaskItem>()
.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<String, Object> getTaskProgress(Long taskId) {
int completedCount = taskItemMapper.countCompletedItems(taskId);
int totalCount = taskItemMapper.countTotalItems(taskId);
Map<String, Object> 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<TaskItem> checkItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.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<TaskItem> groupItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.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.setReceiverDeptId(receiver.getDeptId()); // 设置接单部门ID
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<String> 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<TaskItem>()
.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<TaskItem> groupItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, taskId)
.eq(TaskItem::getGroupNo, groupNo)
);
// 收集所有处理人的手机号
java.util.Set<String> atMobiles = new java.util.HashSet<>();
java.util.Set<String> 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<Map<String, Object>> getPendingApprovalGroups(Long receiverDeptId) {
log.info("===== getPendingApprovalGroups 开始 =====");
log.info("查询参数 receiverDeptId: {}", receiverDeptId);
// 查询所有有待审核子任务组的主任务
List<TaskItem> pendingItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getStatus, "pending_approval")
);
log.info("找到 {} 个 pending_approval 状态的子任务", pendingItems.size());
// 按主任务和组号分组
Map<String, Map<String, Object>> 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<String, Object> 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("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", item.getTaskTypeId());
if (item.getTaskTypeId() != null) {
com.designhub.entity.DemandType taskType = demandTypeMapper.selectById(item.getTaskTypeId());
if (taskType != null) {
groupInfo.put("taskTypeName", taskType.getName());
}
}
// 添加审核人信息(目标部门负责人)
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");
}
}
}
// 计算子任务组总积分
groupInfo.put("totalScore", item.getTotalScore() != null ? item.getTotalScore() : 0);
// 获取前置子任务的素材下载链接(流水线继承)
if (item.getGroupNo() != null && item.getGroupNo() > 1) {
// 查找前一组已完成的子任务的下载链接
List<TaskItem> prevGroupItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, item.getTaskId())
.eq(TaskItem::getGroupNo, item.getGroupNo() - 1)
.eq(TaskItem::getStatus, "completed")
);
if (!prevGroupItems.isEmpty()) {
// 收集所有前置子任务的下载链接
List<String> 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<Map<String, Object>> itemsList = new java.util.ArrayList<>();
Map<String, Object> 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<String, Object> groupInfo = groupMap.get(key);
groupInfo.put("itemCount", (Integer) groupInfo.get("itemCount") + 1);
// 累加积分
Integer currentScore = (Integer) groupInfo.get("totalScore");
Integer itemScore = item.getTotalScore() != null ? item.getTotalScore() : 0;
groupInfo.put("totalScore", currentScore + itemScore);
// 添加到子任务列表
@SuppressWarnings("unchecked")
List<Map<String, Object>> itemsList = (List<Map<String, Object>>) groupInfo.get("items");
Map<String, Object> 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());
}
/**
* 从任务类型配置中获取目标接单部门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;
}
}