package com.designhub.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.designhub.dto.UserOffboardingRecordQuery; import com.designhub.dto.UserOffboardingRequest; import com.designhub.entity.Task; import com.designhub.entity.TaskItem; import com.designhub.entity.TaskTransferLog; import com.designhub.entity.User; import com.designhub.exception.BusinessException; import com.designhub.mapper.TaskItemMapper; import com.designhub.mapper.TaskMapper; import com.designhub.mapper.TaskTransferLogMapper; import com.designhub.mapper.UserMapper; import com.designhub.vo.UserOffboardingRecordDetailVO; import com.designhub.vo.UserOffboardingRecordVO; import com.designhub.vo.UserOffboardingExecuteVO; import com.designhub.vo.UserOffboardingPreviewVO; import lombok.Builder; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * 离职交接服务 */ @Service @RequiredArgsConstructor public class UserOffboardingService { private static final List FINISHED_TASK_STATUSES = Arrays.asList("completed", "cancelled", "draft"); private static final List FINISHED_TASK_ITEM_STATUSES = Arrays.asList("completed", "rejected", "cancelled"); private final UserMapper userMapper; private final TaskMapper taskMapper; private final TaskItemMapper taskItemMapper; private final TaskTransferLogMapper taskTransferLogMapper; public UserOffboardingPreviewVO preview(UserOffboardingRequest request) { OffboardingContext context = buildContext(request); return buildPreviewVO(context); } @Transactional(rollbackFor = Exception.class) public UserOffboardingExecuteVO execute(UserOffboardingRequest request, Long operatorId) { assertAdminOperator(operatorId, "仅管理员可执行离职交接"); OffboardingContext context = buildContext(request); LocalDateTime now = LocalDateTime.now(); long updatedPublisher = updatePublisherTasks(context, now); long updatedPrimary = updatePrimaryPublisherTasks(context, now); long updatedDetail = updateDetailCompleterTasks(context, now); long updatedReceiver = updateReceiverTasks(context, now); long updatedTaskItems = updateReceiverTaskItems(context, now); long transferLogs = writeTransferLogs(context, operatorId, now); boolean sourceUserDisabled = false; if (Boolean.TRUE.equals(context.request.getDisableFromUser())) { User disabled = new User(); disabled.setId(context.fromUser.getId()); disabled.setStatus(0); sourceUserDisabled = userMapper.updateById(disabled) > 0; } return UserOffboardingExecuteVO.builder() .preview(buildPreviewVO(context)) .updatedPublisherTasks(updatedPublisher) .updatedPrimaryPublisherTasks(updatedPrimary) .updatedDetailCompleterTasks(updatedDetail) .updatedReceiverTasks(updatedReceiver) .updatedReceiverTaskItems(updatedTaskItems) .transferLogCount(transferLogs) .sourceUserDisabled(sourceUserDisabled) .executedAt(now) .build(); } public Page getRecordPage(UserOffboardingRecordQuery query, Long operatorId) { assertAdminOperator(operatorId, "仅管理员可查看离职交接记录"); UserOffboardingRecordQuery safeQuery = normalizeRecordQuery(query); Page page = new Page<>(safeQuery.getPageNum(), safeQuery.getPageSize()); taskTransferLogMapper.selectOffboardingRecordPage(page, safeQuery); return page; } public UserOffboardingRecordDetailVO getRecordDetail(Long id, Long operatorId) { assertAdminOperator(operatorId, "仅管理员可查看离职交接记录"); if (id == null) { throw new BusinessException("记录ID不能为空"); } UserOffboardingRecordDetailVO detail = taskTransferLogMapper.selectOffboardingRecordDetail(id); if (detail == null) { throw new BusinessException("离职交接记录不存在"); } return detail; } private User assertAdminOperator(Long operatorId, String forbiddenMessage) { if (operatorId == null) { throw new BusinessException("操作人不能为空"); } User operator = userMapper.selectById(operatorId); if (operator == null) { throw new BusinessException("操作人不存在"); } if (operator.getIsAdmin() == null || operator.getIsAdmin() != 1) { throw new BusinessException(forbiddenMessage); } return operator; } private UserOffboardingRecordQuery normalizeRecordQuery(UserOffboardingRecordQuery query) { UserOffboardingRecordQuery safeQuery = query == null ? new UserOffboardingRecordQuery() : query; if (safeQuery.getPageNum() == null || safeQuery.getPageNum() < 1) { safeQuery.setPageNum(1); } if (safeQuery.getPageSize() == null || safeQuery.getPageSize() < 1) { safeQuery.setPageSize(10); } if (safeQuery.getPageSize() > 100) { safeQuery.setPageSize(100); } if (StringUtils.hasText(safeQuery.getKeyword())) { safeQuery.setKeyword(safeQuery.getKeyword().trim()); } if (safeQuery.getStartTime() != null && safeQuery.getEndTime() != null && safeQuery.getStartTime().isAfter(safeQuery.getEndTime())) { throw new BusinessException("开始时间不能晚于结束时间"); } return safeQuery; } private OffboardingContext buildContext(UserOffboardingRequest request) { UserOffboardingRequest safeRequest = normalizeAndValidate(request); User fromUser = userMapper.selectById(safeRequest.getFromUserId()); User toUser = userMapper.selectById(safeRequest.getToUserId()); if (fromUser == null) { throw new BusinessException("离职用户不存在"); } if (toUser == null) { throw new BusinessException("接替用户不存在"); } if (fromUser.getId().equals(toUser.getId())) { throw new BusinessException("离职用户与接替用户不能相同"); } if (toUser.getStatus() == null || toUser.getStatus() != 1) { throw new BusinessException("接替用户必须是启用状态"); } Scope scope = Scope.from(safeRequest.getScope()); List demandTasks = scope.includeDemand ? loadDemandTasks(fromUser.getId()) : new ArrayList<>(); List receiverTasks = scope.includeReceiver ? loadReceiverTasks(fromUser.getId()) : new ArrayList<>(); List receiverTaskItems = scope.includeReceiver ? loadReceiverTaskItems(fromUser.getId()) : new ArrayList<>(); return OffboardingContext.builder() .request(safeRequest) .scope(scope) .fromUser(fromUser) .toUser(toUser) .demandTasks(demandTasks) .receiverTasks(receiverTasks) .receiverTaskItems(receiverTaskItems) .build(); } private UserOffboardingRequest normalizeAndValidate(UserOffboardingRequest request) { if (request == null) { throw new BusinessException("请求参数不能为空"); } if (request.getFromUserId() == null || request.getToUserId() == null) { throw new BusinessException("离职用户和接替用户不能为空"); } if (!StringUtils.hasText(request.getScope())) { request.setScope("demand"); } Scope.from(request.getScope()); if (!StringUtils.hasText(request.getReason())) { request.setReason("离职交接"); } if (request.getDisableFromUser() == null) { request.setDisableFromUser(Boolean.TRUE); } return request; } private List loadDemandTasks(Long fromUserId) { LambdaQueryWrapper wrapper = baseUnfinishedTaskQuery() .and(w -> w.eq(Task::getPublisherId, fromUserId) .or() .eq(Task::getPrimaryPublisherId, fromUserId) .or() .eq(Task::getDetailCompleterId, fromUserId)); return taskMapper.selectList(wrapper); } private List loadReceiverTasks(Long fromUserId) { LambdaQueryWrapper wrapper = baseUnfinishedTaskQuery() .eq(Task::getReceiverId, fromUserId); return taskMapper.selectList(wrapper); } private List loadReceiverTaskItems(Long fromUserId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(TaskItem::getReceiverId, fromUserId) .and(w -> w.isNull(TaskItem::getStatus) .or() .notIn(TaskItem::getStatus, FINISHED_TASK_ITEM_STATUSES)); return taskItemMapper.selectList(wrapper); } private LambdaQueryWrapper baseUnfinishedTaskQuery() { return new LambdaQueryWrapper() .eq(Task::getDeleted, 0) .and(w -> w.isNull(Task::getStatus) .or() .notIn(Task::getStatus, FINISHED_TASK_STATUSES)); } private long updatePublisherTasks(OffboardingContext context, LocalDateTime now) { List taskIds = context.demandTasks.stream() .filter(task -> context.fromUser.getId().equals(task.getPublisherId())) .map(Task::getId) .collect(Collectors.toList()); if (taskIds.isEmpty()) { return 0; } LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() .in(Task::getId, taskIds) .eq(Task::getPublisherId, context.fromUser.getId()) .set(Task::getPublisherId, context.toUser.getId()) .set(Task::getPublisherDeptId, context.toUser.getDeptId()) .set(Task::getUpdateTime, now); return taskMapper.update(null, wrapper); } private long updatePrimaryPublisherTasks(OffboardingContext context, LocalDateTime now) { List taskIds = context.demandTasks.stream() .filter(task -> context.fromUser.getId().equals(task.getPrimaryPublisherId())) .map(Task::getId) .collect(Collectors.toList()); if (taskIds.isEmpty()) { return 0; } LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() .in(Task::getId, taskIds) .eq(Task::getPrimaryPublisherId, context.fromUser.getId()) .set(Task::getPrimaryPublisherId, context.toUser.getId()) .set(Task::getPrimaryPublisherDeptId, context.toUser.getDeptId()) .set(Task::getUpdateTime, now); return taskMapper.update(null, wrapper); } private long updateDetailCompleterTasks(OffboardingContext context, LocalDateTime now) { List taskIds = context.demandTasks.stream() .filter(task -> context.fromUser.getId().equals(task.getDetailCompleterId())) .map(Task::getId) .collect(Collectors.toList()); if (taskIds.isEmpty()) { return 0; } LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() .in(Task::getId, taskIds) .eq(Task::getDetailCompleterId, context.fromUser.getId()) .set(Task::getDetailCompleterId, context.toUser.getId()) .set(Task::getUpdateTime, now); return taskMapper.update(null, wrapper); } private long updateReceiverTasks(OffboardingContext context, LocalDateTime now) { List taskIds = context.receiverTasks.stream() .map(Task::getId) .collect(Collectors.toList()); if (taskIds.isEmpty()) { return 0; } LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() .in(Task::getId, taskIds) .eq(Task::getReceiverId, context.fromUser.getId()) .set(Task::getReceiverId, context.toUser.getId()) .set(Task::getReceiverDeptId, context.toUser.getDeptId()) .set(Task::getUpdateTime, now); return taskMapper.update(null, wrapper); } private long updateReceiverTaskItems(OffboardingContext context, LocalDateTime now) { List itemIds = context.receiverTaskItems.stream() .map(TaskItem::getId) .collect(Collectors.toList()); if (itemIds.isEmpty()) { return 0; } String receiverName = StringUtils.hasText(context.toUser.getRealName()) ? context.toUser.getRealName() : context.toUser.getUsername(); LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() .in(TaskItem::getId, itemIds) .eq(TaskItem::getReceiverId, context.fromUser.getId()) .set(TaskItem::getReceiverId, context.toUser.getId()) .set(TaskItem::getReceiverDeptId, context.toUser.getDeptId()) .set(TaskItem::getReceiverName, receiverName) .set(TaskItem::getUpdateTime, now); return taskItemMapper.update(null, wrapper); } private long writeTransferLogs(OffboardingContext context, Long operatorId, LocalDateTime now) { Map taskMap = buildTaskRoleMap(context); if (taskMap.isEmpty()) { return 0; } long count = 0; for (TaskRoleSnapshot snapshot : taskMap.values()) { Task task = snapshot.task; TaskTransferLog log = new TaskTransferLog(); log.setTaskId(task.getId()); log.setTransferType("transfer"); log.setFromUserId(context.fromUser.getId()); log.setToUserId(context.toUser.getId()); log.setOperatorId(operatorId); log.setOldDeadline(task.getDeadline()); log.setNewDeadline(task.getDeadline()); log.setOldPriority(task.getPriority()); log.setNewPriority(task.getPriority()); log.setReason(context.request.getReason() + "(" + String.join("、", snapshot.roles) + ")"); log.setRemark("离职交接"); log.setCreateTime(now); taskTransferLogMapper.insert(log); count++; } return count; } private UserOffboardingPreviewVO buildPreviewVO(OffboardingContext context) { List samples = buildSamples(context); long publisherCount = context.demandTasks.stream() .filter(task -> context.fromUser.getId().equals(task.getPublisherId())) .count(); long primaryCount = context.demandTasks.stream() .filter(task -> context.fromUser.getId().equals(task.getPrimaryPublisherId())) .count(); long detailCount = context.demandTasks.stream() .filter(task -> context.fromUser.getId().equals(task.getDetailCompleterId())) .count(); long receiverCount = context.receiverTasks.size(); Set affectedTaskIds = buildTaskRoleMap(context).keySet(); return UserOffboardingPreviewVO.builder() .fromUserId(context.fromUser.getId()) .fromUserName(resolveUserName(context.fromUser)) .toUserId(context.toUser.getId()) .toUserName(resolveUserName(context.toUser)) .scope(context.scope.raw) .affectedTaskCount(affectedTaskIds.size()) .publisherTaskCount(publisherCount) .primaryPublisherTaskCount(primaryCount) .detailCompleterTaskCount(detailCount) .receiverTaskCount(receiverCount) .receiverTaskItemCount(context.receiverTaskItems.size()) .samples(samples) .build(); } private List buildSamples(OffboardingContext context) { Map taskMap = buildTaskRoleMap(context); return taskMap.values().stream() .sorted(Comparator .comparing((TaskRoleSnapshot item) -> item.task.getUpdateTime() != null ? item.task.getUpdateTime() : LocalDateTime.MIN) .reversed()) .limit(20) .map(item -> UserOffboardingPreviewVO.AffectedTaskSample.builder() .taskId(item.task.getId()) .taskNo(item.task.getTaskNo()) .title(item.task.getTitle()) .status(item.task.getStatus()) .roles(String.join("、", item.roles)) .build()) .collect(Collectors.toList()); } private Map buildTaskRoleMap(OffboardingContext context) { Map map = new HashMap<>(); Long fromUserId = context.fromUser.getId(); for (Task task : context.demandTasks) { if (fromUserId.equals(task.getPublisherId())) { addRole(map, task, "发起人"); } if (fromUserId.equals(task.getPrimaryPublisherId())) { addRole(map, task, "一级发起人"); } if (fromUserId.equals(task.getDetailCompleterId())) { addRole(map, task, "明细完善人"); } } for (Task task : context.receiverTasks) { if (fromUserId.equals(task.getReceiverId())) { addRole(map, task, "接单人"); } } if (!context.receiverTaskItems.isEmpty()) { Set taskIds = context.receiverTaskItems.stream() .map(TaskItem::getTaskId) .filter(id -> id != null) .collect(Collectors.toSet()); if (!taskIds.isEmpty()) { Map taskById = taskMapper.selectBatchIds(taskIds).stream() .filter(task -> task != null && task.getId() != null) .collect(Collectors.toMap(Task::getId, task -> task, (left, right) -> left)); for (Long taskId : taskIds) { addRole(map, taskById.get(taskId), "子任务接单人"); } } } return map; } private void addRole(Map map, Task task, String role) { if (task == null || task.getId() == null) { return; } TaskRoleSnapshot snapshot = map.computeIfAbsent(task.getId(), key -> TaskRoleSnapshot.builder() .task(task) .roles(new LinkedHashSet<>()) .build()); snapshot.roles.add(role); } private String resolveUserName(User user) { if (user == null) { return ""; } if (StringUtils.hasText(user.getRealName())) { return user.getRealName(); } return user.getUsername(); } private enum Scope { DEMAND("demand", true, false), RECEIVER("receiver", false, true), ALL("all", true, true); private final String raw; private final boolean includeDemand; private final boolean includeReceiver; Scope(String raw, boolean includeDemand, boolean includeReceiver) { this.raw = raw; this.includeDemand = includeDemand; this.includeReceiver = includeReceiver; } public static Scope from(String raw) { String value = raw == null ? "" : raw.trim().toLowerCase(Locale.ROOT); for (Scope scope : values()) { if (scope.raw.equals(value)) { return scope; } } throw new BusinessException("不支持的交接范围: " + raw); } } @Builder private static class OffboardingContext { private UserOffboardingRequest request; private Scope scope; private User fromUser; private User toUser; private List demandTasks; private List receiverTasks; private List receiverTaskItems; } @Builder private static class TaskRoleSnapshot { private Task task; private LinkedHashSet roles; } }