500 lines
20 KiB
Java
500 lines
20 KiB
Java
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;
|
||
}
|
||
}
|