qiangdanxitong/backend/src/main/java/com/designhub/service/UserOffboardingService.java

500 lines
20 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.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<String> FINISHED_TASK_STATUSES = Arrays.asList("completed", "cancelled", "draft");
private static final List<String> 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<UserOffboardingRecordVO> getRecordPage(UserOffboardingRecordQuery query, Long operatorId) {
assertAdminOperator(operatorId, "仅管理员可查看离职交接记录");
UserOffboardingRecordQuery safeQuery = normalizeRecordQuery(query);
Page<UserOffboardingRecordVO> 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<Task> demandTasks = scope.includeDemand ? loadDemandTasks(fromUser.getId()) : new ArrayList<>();
List<Task> receiverTasks = scope.includeReceiver ? loadReceiverTasks(fromUser.getId()) : new ArrayList<>();
List<TaskItem> 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<Task> loadDemandTasks(Long fromUserId) {
LambdaQueryWrapper<Task> 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<Task> loadReceiverTasks(Long fromUserId) {
LambdaQueryWrapper<Task> wrapper = baseUnfinishedTaskQuery()
.eq(Task::getReceiverId, fromUserId);
return taskMapper.selectList(wrapper);
}
private List<TaskItem> loadReceiverTaskItems(Long fromUserId) {
LambdaQueryWrapper<TaskItem> wrapper = new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getReceiverId, fromUserId)
.and(w -> w.isNull(TaskItem::getStatus)
.or()
.notIn(TaskItem::getStatus, FINISHED_TASK_ITEM_STATUSES));
return taskItemMapper.selectList(wrapper);
}
private LambdaQueryWrapper<Task> baseUnfinishedTaskQuery() {
return new LambdaQueryWrapper<Task>()
.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<Long> taskIds = context.demandTasks.stream()
.filter(task -> context.fromUser.getId().equals(task.getPublisherId()))
.map(Task::getId)
.collect(Collectors.toList());
if (taskIds.isEmpty()) {
return 0;
}
LambdaUpdateWrapper<Task> wrapper = new LambdaUpdateWrapper<Task>()
.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<Long> taskIds = context.demandTasks.stream()
.filter(task -> context.fromUser.getId().equals(task.getPrimaryPublisherId()))
.map(Task::getId)
.collect(Collectors.toList());
if (taskIds.isEmpty()) {
return 0;
}
LambdaUpdateWrapper<Task> wrapper = new LambdaUpdateWrapper<Task>()
.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<Long> taskIds = context.demandTasks.stream()
.filter(task -> context.fromUser.getId().equals(task.getDetailCompleterId()))
.map(Task::getId)
.collect(Collectors.toList());
if (taskIds.isEmpty()) {
return 0;
}
LambdaUpdateWrapper<Task> wrapper = new LambdaUpdateWrapper<Task>()
.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<Long> taskIds = context.receiverTasks.stream()
.map(Task::getId)
.collect(Collectors.toList());
if (taskIds.isEmpty()) {
return 0;
}
LambdaUpdateWrapper<Task> wrapper = new LambdaUpdateWrapper<Task>()
.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<Long> 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<TaskItem> wrapper = new LambdaUpdateWrapper<TaskItem>()
.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<Long, TaskRoleSnapshot> 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<UserOffboardingPreviewVO.AffectedTaskSample> 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<Long> 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<UserOffboardingPreviewVO.AffectedTaskSample> buildSamples(OffboardingContext context) {
Map<Long, TaskRoleSnapshot> 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<Long, TaskRoleSnapshot> buildTaskRoleMap(OffboardingContext context) {
Map<Long, TaskRoleSnapshot> 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<Long> taskIds = context.receiverTaskItems.stream()
.map(TaskItem::getTaskId)
.filter(id -> id != null)
.collect(Collectors.toSet());
if (!taskIds.isEmpty()) {
Map<Long, Task> 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<Long, TaskRoleSnapshot> 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<Task> demandTasks;
private List<Task> receiverTasks;
private List<TaskItem> receiverTaskItems;
}
@Builder
private static class TaskRoleSnapshot {
private Task task;
private LinkedHashSet<String> roles;
}
}