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