From f041543f02db9f8c59f4a9c1a5a7f1820964cae5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 7 Feb 2026 19:38:46 +0800 Subject: [PATCH] feat(settings): add offboarding record list and trace detail --- .../designhub/controller/UserController.java | 44 +++ .../dto/UserOffboardingRecordQuery.java | 28 ++ .../mapper/TaskTransferLogMapper.java | 56 ++++ .../service/UserOffboardingService.java | 70 ++++- .../vo/UserOffboardingRecordDetailVO.java | 39 +++ .../designhub/vo/UserOffboardingRecordVO.java | 37 +++ frontend/src/api/user.ts | 53 ++++ frontend/src/types/components.d.ts | 1 + frontend/src/views/Settings.vue | 254 +++++++++++++++++- 9 files changed, 570 insertions(+), 12 deletions(-) create mode 100644 backend/src/main/java/com/designhub/dto/UserOffboardingRecordQuery.java create mode 100644 backend/src/main/java/com/designhub/vo/UserOffboardingRecordDetailVO.java create mode 100644 backend/src/main/java/com/designhub/vo/UserOffboardingRecordVO.java diff --git a/backend/src/main/java/com/designhub/controller/UserController.java b/backend/src/main/java/com/designhub/controller/UserController.java index cdbd549..a96fcfd 100644 --- a/backend/src/main/java/com/designhub/controller/UserController.java +++ b/backend/src/main/java/com/designhub/controller/UserController.java @@ -1,20 +1,25 @@ package com.designhub.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.designhub.dto.UserOffboardingRecordQuery; import com.designhub.common.Result; import com.designhub.dto.UserOffboardingRequest; import com.designhub.entity.User; import com.designhub.service.UserOffboardingService; import com.designhub.service.UserService; import com.designhub.util.JwtUtil; +import com.designhub.vo.UserOffboardingRecordDetailVO; +import com.designhub.vo.UserOffboardingRecordVO; import com.designhub.vo.UserOffboardingExecuteVO; import com.designhub.vo.UserOffboardingPreviewVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -196,4 +201,43 @@ public class UserController { Long operatorId = jwtUtil.getUserIdFromToken(token); return Result.success(userOffboardingService.execute(request, operatorId)); } + + @Operation(summary = "分页查询离职交接记录") + @GetMapping("/offboarding/records") + public Result> getOffboardingRecords( + @RequestHeader("Authorization") String authHeader, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Long fromUserId, + @RequestParam(required = false) Long toUserId, + @RequestParam(required = false) Long operatorId, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime + ) { + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long currentUserId = jwtUtil.getUserIdFromToken(token); + UserOffboardingRecordQuery query = new UserOffboardingRecordQuery(); + query.setPageNum(pageNum); + query.setPageSize(pageSize); + query.setFromUserId(fromUserId); + query.setToUserId(toUserId); + query.setOperatorId(operatorId); + query.setKeyword(keyword); + query.setStartTime(startTime); + query.setEndTime(endTime); + Page page = userOffboardingService.getRecordPage(query, currentUserId); + return Result.success(page); + } + + @Operation(summary = "获取离职交接记录详情") + @GetMapping("/offboarding/records/{id}") + public Result getOffboardingRecordDetail( + @RequestHeader("Authorization") String authHeader, + @PathVariable Long id + ) { + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long currentUserId = jwtUtil.getUserIdFromToken(token); + return Result.success(userOffboardingService.getRecordDetail(id, currentUserId)); + } } diff --git a/backend/src/main/java/com/designhub/dto/UserOffboardingRecordQuery.java b/backend/src/main/java/com/designhub/dto/UserOffboardingRecordQuery.java new file mode 100644 index 0000000..e4a2f7d --- /dev/null +++ b/backend/src/main/java/com/designhub/dto/UserOffboardingRecordQuery.java @@ -0,0 +1,28 @@ +package com.designhub.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 离职交接记录查询参数 + */ +@Data +public class UserOffboardingRecordQuery { + + private Integer pageNum = 1; + + private Integer pageSize = 10; + + private Long fromUserId; + + private Long toUserId; + + private Long operatorId; + + private String keyword; + + private LocalDateTime startTime; + + private LocalDateTime endTime; +} diff --git a/backend/src/main/java/com/designhub/mapper/TaskTransferLogMapper.java b/backend/src/main/java/com/designhub/mapper/TaskTransferLogMapper.java index a4052ee..1b0ff3d 100644 --- a/backend/src/main/java/com/designhub/mapper/TaskTransferLogMapper.java +++ b/backend/src/main/java/com/designhub/mapper/TaskTransferLogMapper.java @@ -1,9 +1,65 @@ package com.designhub.mapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.designhub.dto.UserOffboardingRecordQuery; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.designhub.entity.TaskTransferLog; +import com.designhub.vo.UserOffboardingRecordDetailVO; +import com.designhub.vo.UserOffboardingRecordVO; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; @Mapper public interface TaskTransferLogMapper extends BaseMapper { + + @Select("") + IPage selectOffboardingRecordPage( + Page page, + @Param("query") UserOffboardingRecordQuery query + ); + + @Select("SELECT l.id, l.transfer_type, l.task_id, t.task_no, t.title AS task_title, t.status AS task_status, " + + " t.spu_info AS task_spu_info, l.from_user_id, COALESCE(fu.real_name, fu.username) AS from_user_name, " + + " l.to_user_id, COALESCE(tu.real_name, tu.username) AS to_user_name, " + + " l.operator_id, COALESCE(ou.real_name, ou.username) AS operator_name, " + + " l.reason, l.remark, l.old_deadline, l.new_deadline, l.old_priority, l.new_priority, " + + " l.delay_days, l.penalty_score, l.create_time " + + "FROM task_transfer_log l " + + "LEFT JOIN task t ON t.id = l.task_id " + + "LEFT JOIN sys_user fu ON fu.id = l.from_user_id " + + "LEFT JOIN sys_user tu ON tu.id = l.to_user_id " + + "LEFT JOIN sys_user ou ON ou.id = l.operator_id " + + "WHERE l.id = #{id} AND l.transfer_type = 'transfer' AND l.remark = '离职交接' " + + "LIMIT 1") + UserOffboardingRecordDetailVO selectOffboardingRecordDetail(@Param("id") Long id); } diff --git a/backend/src/main/java/com/designhub/service/UserOffboardingService.java b/backend/src/main/java/com/designhub/service/UserOffboardingService.java index 014725b..2600dd7 100644 --- a/backend/src/main/java/com/designhub/service/UserOffboardingService.java +++ b/backend/src/main/java/com/designhub/service/UserOffboardingService.java @@ -2,6 +2,8 @@ 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; @@ -12,6 +14,8 @@ 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; @@ -54,16 +58,7 @@ public class UserOffboardingService { @Transactional(rollbackFor = Exception.class) public UserOffboardingExecuteVO execute(UserOffboardingRequest request, Long operatorId) { - 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("仅管理员可执行离职交接"); - } + assertAdminOperator(operatorId, "仅管理员可执行离职交接"); OffboardingContext context = buildContext(request); LocalDateTime now = LocalDateTime.now(); @@ -96,6 +91,61 @@ public class UserOffboardingService { .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()); diff --git a/backend/src/main/java/com/designhub/vo/UserOffboardingRecordDetailVO.java b/backend/src/main/java/com/designhub/vo/UserOffboardingRecordDetailVO.java new file mode 100644 index 0000000..8a86a2c --- /dev/null +++ b/backend/src/main/java/com/designhub/vo/UserOffboardingRecordDetailVO.java @@ -0,0 +1,39 @@ +package com.designhub.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 离职交接记录详情 + */ +@Data +public class UserOffboardingRecordDetailVO { + + private Long id; + private String transferType; + private Long taskId; + private String taskNo; + private String taskTitle; + private String taskStatus; + private String taskSpuInfo; + + private Long fromUserId; + private String fromUserName; + private Long toUserId; + private String toUserName; + private Long operatorId; + private String operatorName; + + private String reason; + private String remark; + + private LocalDateTime oldDeadline; + private LocalDateTime newDeadline; + private Integer oldPriority; + private Integer newPriority; + private Integer delayDays; + private Integer penaltyScore; + + private LocalDateTime createTime; +} diff --git a/backend/src/main/java/com/designhub/vo/UserOffboardingRecordVO.java b/backend/src/main/java/com/designhub/vo/UserOffboardingRecordVO.java new file mode 100644 index 0000000..a31d5a4 --- /dev/null +++ b/backend/src/main/java/com/designhub/vo/UserOffboardingRecordVO.java @@ -0,0 +1,37 @@ +package com.designhub.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 离职交接记录 + */ +@Data +public class UserOffboardingRecordVO { + + private Long id; + private Long taskId; + private String taskNo; + private String taskTitle; + private String taskStatus; + + private Long fromUserId; + private String fromUserName; + private Long toUserId; + private String toUserName; + private Long operatorId; + private String operatorName; + + private String reason; + private String remark; + + private LocalDateTime oldDeadline; + private LocalDateTime newDeadline; + private Integer oldPriority; + private Integer newPriority; + private Integer delayDays; + private Integer penaltyScore; + + private LocalDateTime createTime; +} diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index 6637ac7..ffad892 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -242,3 +242,56 @@ export const executeUserOffboarding = async ( const response = await request.post('/users/offboarding/execute', payload); return response.data; }; + +export interface UserOffboardingRecordItem { + id: number; + taskId: number; + taskNo: string; + taskTitle: string; + taskStatus: string; + fromUserId: number; + fromUserName: string; + toUserId: number; + toUserName: string; + operatorId: number; + operatorName: string; + reason: string; + remark: string; + oldDeadline?: string; + newDeadline?: string; + oldPriority?: number; + newPriority?: number; + delayDays?: number; + penaltyScore?: number; + createTime: string; +} + +export interface UserOffboardingRecordDetail extends UserOffboardingRecordItem { + transferType: string; + taskSpuInfo?: string; +} + +export interface UserOffboardingRecordQuery { + pageNum?: number; + pageSize?: number; + fromUserId?: number; + toUserId?: number; + operatorId?: number; + keyword?: string; + startTime?: string; + endTime?: string; +} + +export const getUserOffboardingRecords = async ( + query: UserOffboardingRecordQuery +): Promise> => { + const response = await request.get('/users/offboarding/records', { params: query }); + return response.data; +}; + +export const getUserOffboardingRecordDetail = async ( + id: number +): Promise => { + const response = await request.get(`/users/offboarding/records/${id}`); + return response.data; +}; diff --git a/frontend/src/types/components.d.ts b/frontend/src/types/components.d.ts index dac455d..f36f28c 100644 --- a/frontend/src/types/components.d.ts +++ b/frontend/src/types/components.d.ts @@ -41,6 +41,7 @@ declare module 'vue' { ARadio: typeof import('ant-design-vue/es')['Radio'] ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] + ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] ARate: typeof import('ant-design-vue/es')['Rate'] ARow: typeof import('ant-design-vue/es')['Row'] ASelect: typeof import('ant-design-vue/es')['Select'] diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 484a1cf..a7ad741 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -677,6 +677,136 @@ /> + +
+
+

离职交接记录

+ + 重置 + + + 查询 + + +
+ +
+ + + + {{ item.realName || item.username }} + + + + + {{ item.realName || item.username }} + + + + + {{ item.realName || item.username }} + + + +
+ + + + +
+ + + + + + {{ offboardingRecordDetail.id }} + {{ formatDateTime(offboardingRecordDetail.createTime) }} + {{ offboardingRecordDetail.taskNo || '--' }} + {{ getTaskStatusLabel(offboardingRecordDetail.taskStatus) }} + {{ offboardingRecordDetail.taskTitle || '--' }} + {{ offboardingRecordDetail.taskSpuInfo || '--' }} + {{ offboardingRecordDetail.fromUserName || '--' }} + {{ offboardingRecordDetail.toUserName || '--' }} + {{ offboardingRecordDetail.operatorName || '--' }} + {{ offboardingRecordDetail.transferType || '--' }} + {{ formatDateTime(offboardingRecordDetail.oldDeadline) }} + {{ formatDateTime(offboardingRecordDetail.newDeadline) }} + {{ offboardingRecordDetail.oldPriority ?? '--' }} + {{ offboardingRecordDetail.newPriority ?? '--' }} + +
{{ offboardingRecordDetail.reason || '--' }}
+
+ +
{{ offboardingRecordDetail.remark || '--' }}
+
+
+
+
@@ -2991,9 +3121,13 @@ import { setAdmin, previewUserOffboarding, executeUserOffboarding, + getUserOffboardingRecords, + getUserOffboardingRecordDetail, type User, type UserOffboardingPreview, - type UserOffboardingRequest + type UserOffboardingRequest, + type UserOffboardingRecordItem, + type UserOffboardingRecordDetail } from '@/api/user' import { getAllS3Configs, @@ -3141,6 +3275,23 @@ const offboardingPreviewLoading = ref(false) const offboardingExecuting = ref(false) const offboardingPreview = ref(null) const offboardingPreviewDigest = ref('') +const offboardingRecordLoading = ref(false) +const offboardingRecords = ref([]) +const offboardingRecordPage = reactive({ + current: 1, + pageSize: 10, + total: 0 +}) +const offboardingRecordDateRange = ref([]) +const offboardingRecordFilters = reactive({ + keyword: '', + fromUserId: undefined as number | undefined, + toUserId: undefined as number | undefined, + operatorId: undefined as number | undefined +}) +const offboardingDetailVisible = ref(false) +const offboardingDetailLoading = ref(false) +const offboardingRecordDetail = ref(null) const offboardingForm = reactive({ fromUserId: undefined, toUserId: undefined, @@ -3387,6 +3538,17 @@ const offboardingTaskColumns = [ } ] +const offboardingRecordColumns = [ + { title: '时间', dataIndex: 'createTime', key: 'createTime', width: 170 }, + { title: '任务编号', dataIndex: 'taskNo', key: 'taskNo', width: 170 }, + { title: '任务标题', dataIndex: 'taskTitle', key: 'taskTitle', ellipsis: true }, + { title: '离职成员', dataIndex: 'fromUserName', key: 'fromUserName', width: 130 }, + { title: '接替成员', dataIndex: 'toUserName', key: 'toUserName', width: 130 }, + { title: '操作人', dataIndex: 'operatorName', key: 'operatorName', width: 130 }, + { title: '任务状态', dataIndex: 'taskStatus', key: 'taskStatus', width: 110 }, + { title: '操作', key: 'actions', width: 80, fixed: 'right' } +] + // 过滤出当前部门的用户(用于负责人选择) const departmentUsers = computed(() => { // 部门负责人应该可以选择任何用户,不限制于该部门的成员 @@ -4341,7 +4503,7 @@ const handleExecuteOffboarding = async () => { `日志${result.transferLogCount || 0}条` ].join(',') message.success(`交接执行成功:${summary}`) - await Promise.all([loadDepartmentUsers(), loadDepartments()]) + await Promise.all([loadDepartmentUsers(), loadDepartments(), loadOffboardingRecords(true)]) } catch (error: any) { console.error('离职交接执行失败:', error) message.error(error?.response?.data?.message || error?.message || '执行失败,请联系管理员') @@ -4350,6 +4512,67 @@ const handleExecuteOffboarding = async () => { } } +const loadOffboardingRecords = async (resetPage = false) => { + if (resetPage) { + offboardingRecordPage.current = 1 + } + offboardingRecordLoading.value = true + try { + const [startTime, endTime] = offboardingRecordDateRange.value || [] + const data = await getUserOffboardingRecords({ + pageNum: offboardingRecordPage.current, + pageSize: offboardingRecordPage.pageSize, + keyword: offboardingRecordFilters.keyword?.trim() || undefined, + fromUserId: offboardingRecordFilters.fromUserId, + toUserId: offboardingRecordFilters.toUserId, + operatorId: offboardingRecordFilters.operatorId, + startTime: startTime || undefined, + endTime: endTime || undefined + }) + offboardingRecords.value = data.records || [] + offboardingRecordPage.total = data.total || 0 + offboardingRecordPage.current = data.current || offboardingRecordPage.current + offboardingRecordPage.pageSize = data.size || offboardingRecordPage.pageSize + } catch (error: any) { + console.error('加载离职交接记录失败:', error) + message.error(error?.response?.data?.message || error?.message || '加载离职交接记录失败') + } finally { + offboardingRecordLoading.value = false + } +} + +const handleOffboardingRecordPageChange = (page: number, pageSize: number) => { + offboardingRecordPage.current = page + offboardingRecordPage.pageSize = pageSize + loadOffboardingRecords() +} + +const resetOffboardingRecordFilters = () => { + offboardingRecordFilters.keyword = '' + offboardingRecordFilters.fromUserId = undefined + offboardingRecordFilters.toUserId = undefined + offboardingRecordFilters.operatorId = undefined + offboardingRecordDateRange.value = [] + loadOffboardingRecords(true) +} + +const openOffboardingRecordDetail = async (record: UserOffboardingRecordItem) => { + if (!record?.id) { + return + } + offboardingDetailVisible.value = true + offboardingRecordDetail.value = null + offboardingDetailLoading.value = true + try { + offboardingRecordDetail.value = await getUserOffboardingRecordDetail(record.id) + } catch (error: any) { + console.error('加载离职交接详情失败:', error) + message.error(error?.response?.data?.message || error?.message || '加载详情失败') + } finally { + offboardingDetailLoading.value = false + } +} + // 部门搜索处理 const onDepartmentSearch = () => { // 搜索逻辑通过 computed 属性 filteredDepartments 自动处理 @@ -6272,6 +6495,7 @@ watch(activeTab, (value) => { if (!usersLoaded.value) { loadDepartmentUsers() } + loadOffboardingRecords() } // 切换到部门管理 tab 时,刷新部门数据以获取最新成员数 if (value === 'departments') { @@ -6408,6 +6632,32 @@ watch(activeTab, (value) => { } } +.offboarding-records { + margin-top: 16px; + + .offboarding-records-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; + } +} + +.offboarding-record-filters { + margin-bottom: 12px; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.offboarding-detail-text { + white-space: pre-wrap; + word-break: break-word; + color: $text-primary; + line-height: 1.6; +} + .balance-display { font-size: $font-size-xl; color: $color-info;