feat 员工调动记录

This commit is contained in:
wkc
2026-02-11 10:42:38 +08:00
parent 78a9300644
commit 6db63cd8b1
40 changed files with 5557 additions and 29 deletions

View File

@@ -23,6 +23,12 @@
<artifactId>ruoyi-common</artifactId>
</dependency>
<!-- 系统模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
<!-- easyexcel工具 -->
<dependency>
<groupId>com.alibaba</groupId>

View File

@@ -5,10 +5,7 @@ import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.ccdi.domain.vo.CcdiBaseStaffVO;
import com.ruoyi.ccdi.domain.vo.ImportFailureVO;
import com.ruoyi.ccdi.domain.vo.ImportResultVO;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.*;
import com.ruoyi.ccdi.service.ICcdiBaseStaffImportService;
import com.ruoyi.ccdi.service.ICcdiBaseStaffService;
import com.ruoyi.ccdi.utils.EasyExcelUtil;
@@ -61,6 +58,17 @@ public class CcdiBaseStaffController extends BaseController {
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 查询员工下拉列表
*/
@Operation(summary = "查询员工下拉列表")
@PreAuthorize("@ss.hasPermi('ccdi:baseStaff:list')")
@GetMapping("/options")
public AjaxResult getStaffOptions(@RequestParam(required = false) String query) {
List<CcdiBaseStaffOptionVO> list = baseStaffService.selectStaffOptions(query);
return success(list);
}
/**
* 导出员工列表
*/

View File

@@ -0,0 +1,195 @@
package com.ruoyi.ccdi.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiStaffTransferExcel;
import com.ruoyi.ccdi.domain.vo.CcdiStaffTransferVO;
import com.ruoyi.ccdi.domain.vo.ImportResultVO;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.StaffTransferImportFailureVO;
import com.ruoyi.ccdi.service.ICcdiStaffTransferImportService;
import com.ruoyi.ccdi.service.ICcdiStaffTransferService;
import com.ruoyi.ccdi.utils.EasyExcelUtil;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.enums.BusinessType;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 员工调动记录Controller
*
* @author ruoyi
* @date 2026-02-10
*/
@Tag(name = "员工调动记录管理")
@RestController
@RequestMapping("/ccdi/staffTransfer")
public class CcdiStaffTransferController extends BaseController {
@Resource
private ICcdiStaffTransferService transferService;
@Resource
private ICcdiStaffTransferImportService transferImportService;
/**
* 查询员工调动记录列表
*/
@Operation(summary = "查询员工调动记录列表")
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:list')")
@GetMapping("/list")
public TableDataInfo list(CcdiStaffTransferQueryDTO queryDTO) {
// 使用MyBatis Plus分页
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiStaffTransferVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiStaffTransferVO> result = transferService.selectTransferPage(page, queryDTO);
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 导出员工调动记录列表
*/
@Operation(summary = "导出员工调动记录列表")
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:export')")
@Log(title = "员工调动记录", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, CcdiStaffTransferQueryDTO queryDTO) {
List<CcdiStaffTransferExcel> list = transferService.selectTransferListForExport(queryDTO);
EasyExcelUtil.exportExcel(response, list, CcdiStaffTransferExcel.class, "员工调动记录信息");
}
/**
* 获取员工调动记录详细信息
*/
@Operation(summary = "获取员工调动记录详细信息")
@Parameter(name = "id", description = "主键ID", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable Long id) {
return success(transferService.selectTransferById(id));
}
/**
* 新增员工调动记录
*/
@Operation(summary = "新增员工调动记录")
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:add')")
@Log(title = "员工调动记录", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody CcdiStaffTransferAddDTO addDTO) {
return toAjax(transferService.insertTransfer(addDTO));
}
/**
* 修改员工调动记录
*/
@Operation(summary = "修改员工调动记录")
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:edit')")
@Log(title = "员工调动记录", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody CcdiStaffTransferEditDTO editDTO) {
return toAjax(transferService.updateTransfer(editDTO));
}
/**
* 删除员工调动记录
*/
@Operation(summary = "删除员工调动记录")
@Parameter(name = "ids", description = "主键ID数组", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:remove')")
@Log(title = "员工调动记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(transferService.deleteTransferByIds(ids));
}
/**
* 下载带字典下拉框的导入模板
* 使用@DictDropdown注解自动添加下拉框
*/
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffTransferExcel.class, "员工调动记录信息");
}
/**
* 异步导入员工调动记录
*/
@Operation(summary = "异步导入员工调动记录")
@Parameter(name = "file", description = "导入文件", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:import')")
@Log(title = "员工调动记录", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
List<CcdiStaffTransferExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffTransferExcel.class);
if (list == null || list.isEmpty()) {
return error("至少需要一条数据");
}
// 提交异步任务
String taskId = transferService.importTransfer(list);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("导入任务已提交,正在后台处理");
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
/**
* 查询导入状态
*/
@Operation(summary = "查询导入状态")
@Parameter(name = "taskId", description = "任务ID", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:import')")
@GetMapping("/importStatus/{taskId}")
public AjaxResult getImportStatus(@PathVariable String taskId) {
ImportStatusVO statusVO = transferImportService.getImportStatus(taskId);
return success(statusVO);
}
/**
* 查询导入失败记录
*/
@Operation(summary = "查询导入失败记录")
@Parameter(name = "taskId", description = "任务ID", required = true)
@Parameter(name = "pageNum", description = "页码", required = false)
@Parameter(name = "pageSize", description = "每页条数", required = false)
@PreAuthorize("@ss.hasPermi('ccdi:staffTransfer:import')")
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
@PathVariable String taskId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
List<StaffTransferImportFailureVO> failures = transferImportService.getImportFailures(taskId);
// 手动分页
int fromIndex = (pageNum - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, failures.size());
List<StaffTransferImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
return getDataTable(pageData, failures.size());
}
}

View File

@@ -0,0 +1,84 @@
package com.ruoyi.ccdi.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工调动记录对象 ccdi_staff_transfer
*
* @author ruoyi
* @date 2026-02-10
*/
@Data
@TableName("ccdi_staff_transfer")
public class CcdiStaffTransfer implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 员工ID,关联ccdi_base_staff.staff_id */
private Long staffId;
/** 调动类型 */
private String transferType;
/** 调动子类型 */
private String transferSubType;
/** 调动前部门ID */
private Long deptIdBefore;
/** 调动前部门 */
private String deptNameBefore;
/** 调动前职级 */
private String gradeBefore;
/** 调动前岗位 */
private String positionBefore;
/** 调动前薪酬等级 */
private String salaryLevelBefore;
/** 调动后部门ID */
private Long deptIdAfter;
/** 调动后部门 */
private String deptNameAfter;
/** 调动后职级 */
private String gradeAfter;
/** 调动后岗位 */
private String positionAfter;
/** 调动后薪酬等级 */
private String salaryLevelAfter;
/** 调动日期 */
private Date transferDate;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/** 创建人 */
@TableField(fill = FieldFill.INSERT)
private String createdBy;
/** 更新人 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updatedBy;
}

View File

@@ -0,0 +1,98 @@
package com.ruoyi.ccdi.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工调动记录新增DTO
*
* @author ruoyi
* @date 2026-02-10
*/
@Data
@Schema(description = "员工调动记录新增")
public class CcdiStaffTransferAddDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 员工ID */
@NotNull(message = "员工ID不能为空")
@Schema(description = "员工ID")
private Long staffId;
/** 调动类型 */
@NotBlank(message = "调动类型不能为空")
@Size(max = 50, message = "调动类型长度不能超过50个字符")
@Schema(description = "调动类型")
private String transferType;
/** 调动子类型 */
@Size(max = 100, message = "调动子类型长度不能超过100个字符")
@Schema(description = "调动子类型")
private String transferSubType;
/** 调动前部门ID */
@NotNull(message = "调动前部门ID不能为空")
@Schema(description = "调动前部门ID")
private Long deptIdBefore;
/** 调动前部门 */
@Size(max = 200, message = "调动前部门长度不能超过200个字符")
@Schema(description = "调动前部门")
private String deptNameBefore;
/** 调动前职级 */
@Size(max = 50, message = "调动前职级长度不能超过50个字符")
@Schema(description = "调动前职级")
private String gradeBefore;
/** 调动前岗位 */
@Size(max = 100, message = "调动前岗位长度不能超过100个字符")
@Schema(description = "调动前岗位")
private String positionBefore;
/** 调动前薪酬等级 */
@Size(max = 50, message = "调动前薪酬等级长度不能超过50个字符")
@Schema(description = "调动前薪酬等级")
private String salaryLevelBefore;
/** 调动后部门ID */
@NotNull(message = "调动后部门ID不能为空")
@Schema(description = "调动后部门ID")
private Long deptIdAfter;
/** 调动后部门 */
@Size(max = 200, message = "调动后部门长度不能超过200个字符")
@Schema(description = "调动后部门")
private String deptNameAfter;
/** 调动后职级 */
@Size(max = 50, message = "调动后职级长度不能超过50个字符")
@Schema(description = "调动后职级")
private String gradeAfter;
/** 调动后岗位 */
@Size(max = 100, message = "调动后岗位长度不能超过100个字符")
@Schema(description = "调动后岗位")
private String positionAfter;
/** 调动后薪酬等级 */
@Size(max = 50, message = "调动后薪酬等级长度不能超过50个字符")
@Schema(description = "调动后薪酬等级")
private String salaryLevelAfter;
/** 调动日期 */
@NotNull(message = "调动日期不能为空")
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "调动日期")
private Date transferDate;
}

View File

@@ -0,0 +1,99 @@
package com.ruoyi.ccdi.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工调动记录修改DTO
*
* @author ruoyi
* @date 2026-02-10
*/
@Data
@Schema(description = "员工调动记录修改")
public class CcdiStaffTransferEditDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@NotNull(message = "主键ID不能为空")
@Schema(description = "主键ID")
private Long id;
/** 员工ID */
@NotNull(message = "员工ID不能为空")
@Schema(description = "员工ID")
private Long staffId;
/** 调动类型 */
@Size(max = 50, message = "调动类型长度不能超过50个字符")
@Schema(description = "调动类型")
private String transferType;
/** 调动子类型 */
@Size(max = 100, message = "调动子类型长度不能超过100个字符")
@Schema(description = "调动子类型")
private String transferSubType;
/** 调动前部门ID */
@Schema(description = "调动前部门ID")
private Long deptIdBefore;
/** 调动前部门 */
@Size(max = 200, message = "调动前部门长度不能超过200个字符")
@Schema(description = "调动前部门")
private String deptNameBefore;
/** 调动前职级 */
@Size(max = 50, message = "调动前职级长度不能超过50个字符")
@Schema(description = "调动前职级")
private String gradeBefore;
/** 调动前岗位 */
@Size(max = 100, message = "调动前岗位长度不能超过100个字符")
@Schema(description = "调动前岗位")
private String positionBefore;
/** 调动前薪酬等级 */
@Size(max = 50, message = "调动前薪酬等级长度不能超过50个字符")
@Schema(description = "调动前薪酬等级")
private String salaryLevelBefore;
/** 调动后部门ID */
@Schema(description = "调动后部门ID")
private Long deptIdAfter;
/** 调动后部门 */
@Size(max = 200, message = "调动后部门长度不能超过200个字符")
@Schema(description = "调动后部门")
private String deptNameAfter;
/** 调动后职级 */
@Size(max = 50, message = "调动后职级长度不能超过50个字符")
@Schema(description = "调动后职级")
private String gradeAfter;
/** 调动后岗位 */
@Size(max = 100, message = "调动后岗位长度不能超过100个字符")
@Schema(description = "调动后岗位")
private String positionAfter;
/** 调动后薪酬等级 */
@Size(max = 50, message = "调动后薪酬等级长度不能超过50个字符")
@Schema(description = "调动后薪酬等级")
private String salaryLevelAfter;
/** 调动日期 */
@NotNull(message = "调动日期不能为空")
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "调动日期")
private Date transferDate;
}

View File

@@ -0,0 +1,57 @@
package com.ruoyi.ccdi.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工调动记录查询DTO
*
* @author ruoyi
* @date 2026-02-10
*/
@Data
@Schema(description = "员工调动记录查询")
public class CcdiStaffTransferQueryDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 员工ID(模糊查询) */
@Schema(description = "员工ID")
private Long staffId;
/** 员工姓名(模糊查询) */
@Schema(description = "员工姓名")
private String staffName;
/** 调动类型(精确查询) */
@Schema(description = "调动类型")
private String transferType;
/** 调动子类型 */
@Schema(description = "调动子类型")
private String transferSubType;
/** 调动前部门ID */
@Schema(description = "调动前部门ID")
private Long deptIdBefore;
/** 调动后部门ID */
@Schema(description = "调动后部门ID")
private Long deptIdAfter;
/** 调动日期开始 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "调动日期开始")
private Date transferDateStart;
/** 调动日期结束 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "调动日期结束")
private Date transferDateEnd;
}

View File

@@ -0,0 +1,90 @@
package com.ruoyi.ccdi.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工调动记录唯一键DTO
* 用于唯一性校验员工ID + 调动前部门ID + 调动后部门ID + 调动日期
*
* @author ruoyi
* @date 2026-02-11
*/
@Data
@Schema(description = "员工调动记录唯一键")
public class TransferUniqueKey implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 员工ID
*/
@Schema(description = "员工ID")
private Long staffId;
/**
* 调动前部门ID
*/
@Schema(description = "调动前部门ID")
private Long deptIdBefore;
/**
* 调动后部门ID
*/
@Schema(description = "调动后部门ID")
private Long deptIdAfter;
/**
* 调动日期
*/
@Schema(description = "调动日期")
private Date transferDate;
/**
* 生成唯一标识字符串
* 格式: staffId_deptIdBefore_deptIdAfter_transferDate的时间戳
*
* @return 唯一标识字符串
*/
public String toUniqueString() {
return staffId + "_" +
deptIdBefore + "_" +
deptIdAfter + "_" +
(transferDate != null ? transferDate.getTime() : 0);
}
/**
* 从AddDTO构建唯一键
*
* @param addDTO 新增DTO
* @return 唯一键
*/
public static TransferUniqueKey from(CcdiStaffTransferAddDTO addDTO) {
TransferUniqueKey key = new TransferUniqueKey();
key.setStaffId(addDTO.getStaffId());
key.setDeptIdBefore(addDTO.getDeptIdBefore());
key.setDeptIdAfter(addDTO.getDeptIdAfter());
key.setTransferDate(addDTO.getTransferDate());
return key;
}
/**
* 从EditDTO构建唯一键
*
* @param editDTO 编辑DTO
* @return 唯一键
*/
public static TransferUniqueKey from(CcdiStaffTransferEditDTO editDTO) {
TransferUniqueKey key = new TransferUniqueKey();
key.setStaffId(editDTO.getStaffId());
key.setDeptIdBefore(editDTO.getDeptIdBefore());
key.setDeptIdAfter(editDTO.getDeptIdAfter());
key.setTransferDate(editDTO.getTransferDate());
return key;
}
}

View File

@@ -0,0 +1,90 @@
package com.ruoyi.ccdi.domain.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.annotation.Required;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工调动记录Excel导入导出对象
*
* @author ruoyi
* @date 2026-02-10
*/
@Data
public class CcdiStaffTransferExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 员工ID */
@ExcelProperty(value = "员工ID*", index = 0)
@ColumnWidth(15)
@Required
private Long staffId;
/** 调动类型 */
@ExcelProperty(value = "调动类型*", index = 1)
@ColumnWidth(15)
@DictDropdown(dictType = "ccdi_transfer_type")
@Required
private String transferType;
/** 调动子类型 */
@ExcelProperty(value = "调动子类型", index = 2)
@ColumnWidth(15)
private String transferSubType;
/** 调动前部门ID */
@ExcelProperty(value = "调动前部门ID*", index = 3)
@ColumnWidth(15)
@Required
private Long deptIdBefore;
/** 调动前职级 */
@ExcelProperty(value = "调动前职级", index = 4)
@ColumnWidth(15)
private String gradeBefore;
/** 调动前岗位 */
@ExcelProperty(value = "调动前岗位", index = 5)
@ColumnWidth(15)
private String positionBefore;
/** 调动前薪酬等级 */
@ExcelProperty(value = "调动前薪酬等级", index = 6)
@ColumnWidth(15)
private String salaryLevelBefore;
/** 调动后部门ID */
@ExcelProperty(value = "调动后部门ID*", index = 7)
@ColumnWidth(15)
@Required
private Long deptIdAfter;
/** 调动后职级 */
@ExcelProperty(value = "调动后职级", index = 8)
@ColumnWidth(15)
private String gradeAfter;
/** 调动后岗位 */
@ExcelProperty(value = "调动后岗位", index = 9)
@ColumnWidth(15)
private String positionAfter;
/** 调动后薪酬等级 */
@ExcelProperty(value = "调动后薪酬等级", index = 10)
@ColumnWidth(15)
private String salaryLevelAfter;
/** 调动日期 */
@ExcelProperty(value = "调动日期*", index = 11)
@ColumnWidth(15)
@Required
private Date transferDate;
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.ccdi.domain.vo;
import lombok.Data;
/**
* 员工选项VO用于下拉选择框
*
* @author ruoyi
* @date 2026-02-10
*/
@Data
public class CcdiBaseStaffOptionVO {
/**
* 员工ID
*/
private Long staffId;
/**
* 员工姓名
*/
private String name;
/**
* 部门ID
*/
private Long deptId;
/**
* 部门名称
*/
private String deptName;
}

View File

@@ -0,0 +1,106 @@
package com.ruoyi.ccdi.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工调动记录VO
*
* @author ruoyi
* @date 2026-02-10
*/
@Data
@Schema(description = "员工调动记录")
public class CcdiStaffTransferVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@Schema(description = "主键ID")
private Long id;
/** 员工ID */
@Schema(description = "员工ID")
private Long staffId;
/** 员工姓名 */
@Schema(description = "员工姓名")
private String staffName;
/** 调动类型 */
@Schema(description = "调动类型")
private String transferType;
/** 调动子类型 */
@Schema(description = "调动子类型")
private String transferSubType;
/** 调动前部门ID */
@Schema(description = "调动前部门ID")
private Long deptIdBefore;
/** 调动前部门 */
@Schema(description = "调动前部门")
private String deptNameBefore;
/** 调动前职级 */
@Schema(description = "调动前职级")
private String gradeBefore;
/** 调动前岗位 */
@Schema(description = "调动前岗位")
private String positionBefore;
/** 调动前薪酬等级 */
@Schema(description = "调动前薪酬等级")
private String salaryLevelBefore;
/** 调动后部门ID */
@Schema(description = "调动后部门ID")
private Long deptIdAfter;
/** 调动后部门 */
@Schema(description = "调动后部门")
private String deptNameAfter;
/** 调动后职级 */
@Schema(description = "调动后职级")
private String gradeAfter;
/** 调动后岗位 */
@Schema(description = "调动后岗位")
private String positionAfter;
/** 调动后薪酬等级 */
@Schema(description = "调动后薪酬等级")
private String salaryLevelAfter;
/** 调动日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "调动日期")
private Date transferDate;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private Date createTime;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新时间")
private Date updateTime;
/** 创建人 */
@Schema(description = "创建人")
private String createdBy;
/** 更新人 */
@Schema(description = "更新人")
private String updatedBy;
}

View File

@@ -0,0 +1,80 @@
package com.ruoyi.ccdi.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工调动记录导入失败记录VO
*
* @author ruoyi
* @date 2026-02-10
*/
@Data
@Schema(description = "员工调动记录导入失败记录")
public class StaffTransferImportFailureVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 员工ID */
@Schema(description = "员工ID")
private Long staffId;
/** 员工姓名 */
@Schema(description = "员工姓名")
private String staffName;
/** 调动类型 */
@Schema(description = "调动类型")
private String transferType;
/** 调动子类型 */
@Schema(description = "调动子类型")
private String transferSubType;
/** 调动前部门 */
@Schema(description = "调动前部门")
private String deptNameBefore;
/** 调动前职级 */
@Schema(description = "调动前职级")
private String gradeBefore;
/** 调动前岗位 */
@Schema(description = "调动前岗位")
private String positionBefore;
/** 调动前薪酬等级 */
@Schema(description = "调动前薪酬等级")
private String salaryLevelBefore;
/** 调动后部门 */
@Schema(description = "调动后部门")
private String deptNameAfter;
/** 调动后职级 */
@Schema(description = "调动后职级")
private String gradeAfter;
/** 调动后岗位 */
@Schema(description = "调动后岗位")
private String positionAfter;
/** 调动后薪酬等级 */
@Schema(description = "调动后薪酬等级")
private String salaryLevelAfter;
/** 调动日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "调动日期")
private Date transferDate;
/** 错误信息 */
@Schema(description = "错误信息")
private String errorMessage;
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.CcdiBaseStaff;
import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffQueryDTO;
import com.ruoyi.ccdi.domain.vo.CcdiBaseStaffOptionVO;
import com.ruoyi.ccdi.domain.vo.CcdiBaseStaffVO;
import org.apache.ibatis.annotations.Param;
@@ -36,4 +37,13 @@ public interface CcdiBaseStaffMapper extends BaseMapper<CcdiBaseStaff> {
* @return 影响行数
*/
int insertBatch(@Param("list") List<CcdiBaseStaff> list);
/**
* 查询员工选项(用于下拉选择框)
* <p>支持按员工ID或姓名模糊搜索只返回在职员工</p>
*
* @param query 搜索关键词员工ID或姓名可为空
* @return 员工选项列表最多返回100条
*/
List<CcdiBaseStaffOptionVO> selectStaffOptions(@Param("query") String query);
}

View File

@@ -0,0 +1,72 @@
package com.ruoyi.ccdi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.CcdiStaffTransfer;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferQueryDTO;
import com.ruoyi.ccdi.domain.dto.TransferUniqueKey;
import com.ruoyi.ccdi.domain.vo.CcdiStaffTransferVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 员工调动记录 数据层
*
* @author ruoyi
* @date 2026-02-10
*/
public interface CcdiStaffTransferMapper extends BaseMapper<CcdiStaffTransfer> {
/**
* 分页查询员工调动记录列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 员工调动记录VO分页结果
*/
Page<CcdiStaffTransferVO> selectTransferPage(@Param("page") Page<CcdiStaffTransferVO> page,
@Param("query") CcdiStaffTransferQueryDTO queryDTO);
/**
* 查询员工调动记录详情
*
* @param id 主键ID
* @return 员工调动记录VO
*/
CcdiStaffTransferVO selectTransferById(@Param("id") Long id);
/**
* 查询员工调动记录列表(用于导出)
*
* @param queryDTO 查询条件
* @return 员工调动记录VO列表
*/
List<CcdiStaffTransferVO> selectTransferListForExport(@Param("query") CcdiStaffTransferQueryDTO queryDTO);
/**
* 批量插入员工调动记录数据
*
* @param list 员工调动记录列表
* @return 插入行数
*/
int insertBatch(@Param("list") List<CcdiStaffTransfer> list);
/**
* 查询单条记录是否存在根据唯一键员工ID + 调动前部门ID + 调动后部门ID + 调动日期)
*
* @param key 唯一键
* @return 存在的记录不存在返回null
*/
CcdiStaffTransfer checkExists(@Param("key") TransferUniqueKey key);
/**
* 查询单条记录是否存在排除指定ID
*
* @param key 唯一键
* @param excludeId 排除的记录ID
* @return 存在的记录不存在返回null
*/
CcdiStaffTransfer checkExistsExcludeId(@Param("key") TransferUniqueKey key,
@Param("excludeId") Long excludeId);
}

View File

@@ -5,6 +5,7 @@ import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.ccdi.domain.vo.CcdiBaseStaffOptionVO;
import com.ruoyi.ccdi.domain.vo.CcdiBaseStaffVO;
import java.util.List;
@@ -83,4 +84,13 @@ public interface ICcdiBaseStaffService {
*/
String importBaseStaff(List<CcdiBaseStaffExcel> excelList, Boolean isUpdateSupport);
/**
* 查询员工下拉列表
* 支持按员工ID或姓名模糊搜索只返回在职员工
*
* @param query 搜索关键词(员工ID或姓名)
* @return 员工选项列表
*/
List<CcdiBaseStaffOptionVO> selectStaffOptions(String query);
}

View File

@@ -0,0 +1,41 @@
package com.ruoyi.ccdi.service;
import com.ruoyi.ccdi.domain.excel.CcdiStaffTransferExcel;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.StaffTransferImportFailureVO;
import java.util.List;
/**
* 员工调动记录异步导入 服务层
*
* @author ruoyi
* @date 2026-02-10
*/
public interface ICcdiStaffTransferImportService {
/**
* 异步导入员工调动记录数据
*
* @param excelList Excel实体列表
* @param taskId 任务ID
* @param userName 用户名
*/
void importTransferAsync(List<CcdiStaffTransferExcel> excelList, String taskId, String userName);
/**
* 查询导入失败记录
*
* @param taskId 任务ID
* @return 失败记录列表
*/
List<StaffTransferImportFailureVO> getImportFailures(String taskId);
/**
* 查询导入状态
*
* @param taskId 任务ID
* @return 导入状态信息
*/
ImportStatusVO getImportStatus(String taskId);
}

View File

@@ -0,0 +1,101 @@
package com.ruoyi.ccdi.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiStaffTransferExcel;
import com.ruoyi.ccdi.domain.vo.CcdiStaffTransferVO;
import com.ruoyi.common.exception.ServiceException;
import java.util.List;
/**
* 员工调动记录 服务层
*
* @author ruoyi
* @date 2026-02-10
*/
public interface ICcdiStaffTransferService {
/**
* 查询员工调动记录列表
*
* @param queryDTO 查询条件
* @return 员工调动记录VO集合
*/
List<CcdiStaffTransferVO> selectTransferList(CcdiStaffTransferQueryDTO queryDTO);
/**
* 分页查询员工调动记录列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 员工调动记录VO分页结果
*/
Page<CcdiStaffTransferVO> selectTransferPage(Page<CcdiStaffTransferVO> page, CcdiStaffTransferQueryDTO queryDTO);
/**
* 查询员工调动记录详情
*
* @param id 主键ID
* @return 员工调动记录VO
*/
CcdiStaffTransferVO selectTransferById(Long id);
/**
* 新增员工调动记录
*
* @param addDTO 新增DTO
* @return 结果
*/
int insertTransfer(CcdiStaffTransferAddDTO addDTO);
/**
* 修改员工调动记录
*
* @param editDTO 编辑DTO
* @return 结果
*/
int updateTransfer(CcdiStaffTransferEditDTO editDTO);
/**
* 批量删除员工调动记录
*
* @param ids 需要删除的主键ID
* @return 结果
*/
int deleteTransferByIds(Long[] ids);
/**
* 查询员工调动记录列表(用于导出)
*
* @param queryDTO 查询条件
* @return 员工调动记录Excel实体集合
*/
List<CcdiStaffTransferExcel> selectTransferListForExport(CcdiStaffTransferQueryDTO queryDTO);
/**
* 导入员工调动记录数据(异步)
*
* @param excelList Excel实体列表
* @return 任务ID
*/
String importTransfer(List<CcdiStaffTransferExcel> excelList);
/**
* 新增时校验唯一性
*
* @param addDTO 新增DTO
* @throws ServiceException 如果记录已存在
*/
void checkUniqueForAdd(CcdiStaffTransferAddDTO addDTO);
/**
* 编辑时校验唯一性
*
* @param editDTO 编辑DTO
* @throws ServiceException 如果记录已存在
*/
void checkUniqueForEdit(CcdiStaffTransferEditDTO editDTO);
}

View File

@@ -7,6 +7,7 @@ import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiBaseStaffQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.ccdi.domain.vo.CcdiBaseStaffOptionVO;
import com.ruoyi.ccdi.domain.vo.CcdiBaseStaffVO;
import com.ruoyi.ccdi.enums.EmployeeStatus;
import com.ruoyi.ccdi.mapper.CcdiBaseStaffMapper;
@@ -205,6 +206,18 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
return taskId;
}
/**
* 查询员工下拉列表
* 支持按员工ID或姓名模糊搜索只返回在职员工
*
* @param query 搜索关键词(员工ID或姓名)
* @return 员工选项列表
*/
@Override
public List<CcdiBaseStaffOptionVO> selectStaffOptions(String query) {
return baseStaffMapper.selectStaffOptions(query);
}
/**
* 构建查询条件
*/

View File

@@ -0,0 +1,313 @@
package com.ruoyi.ccdi.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.ccdi.domain.CcdiStaffTransfer;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferAddDTO;
import com.ruoyi.ccdi.domain.excel.CcdiStaffTransferExcel;
import com.ruoyi.ccdi.domain.vo.ImportResult;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.StaffTransferImportFailureVO;
import com.ruoyi.ccdi.mapper.CcdiStaffTransferMapper;
import com.ruoyi.ccdi.service.ICcdiStaffTransferImportService;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 员工调动记录异步导入服务层处理
*
* @author ruoyi
* @date 2026-02-10
*/
@Service
@EnableAsync
public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImportService {
@Resource
private CcdiStaffTransferMapper transferMapper;
@Resource
private SysDeptMapper deptMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
@Async
@Transactional
public void importTransferAsync(List<CcdiStaffTransferExcel> excelList, String taskId, String userName) {
List<CcdiStaffTransfer> newRecords = new ArrayList<>();
List<StaffTransferImportFailureVO> failures = new ArrayList<>();
// 批量查询已存在的唯一键组合
Set<String> existingKeys = getExistingTransferKeys(excelList);
// 用于检测Excel内部的重复键
Set<String> excelProcessedKeys = new HashSet<>();
// 分类数据
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
try {
// 转换为AddDTO进行验证
CcdiStaffTransferAddDTO addDTO = new CcdiStaffTransferAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 验证数据
validateTransferData(addDTO);
// 生成唯一键
String uniqueKey = buildUniqueKey(addDTO.getStaffId(), addDTO.getDeptIdBefore(),
addDTO.getDeptIdAfter(), addDTO.getTransferDate());
if (existingKeys.contains(uniqueKey)) {
// 数据库中已存在
throw new RuntimeException(String.format(
"该员工在%s的调动记录已存在从%s调往%s",
addDTO.getTransferDate(),
addDTO.getDeptNameBefore(),
addDTO.getDeptNameAfter()
));
} else if (excelProcessedKeys.contains(uniqueKey)) {
// Excel内部重复
throw new RuntimeException(String.format(
"该记录与Excel第%d行重复",
excelProcessedKeys.size() + 1
));
} else {
CcdiStaffTransfer transfer = new CcdiStaffTransfer();
// 从addDTO复制因为validateTransferData已经补全了部门名称
BeanUtils.copyProperties(addDTO, transfer);
transfer.setCreatedBy(userName);
transfer.setUpdatedBy(userName);
newRecords.add(transfer);
excelProcessedKeys.add(uniqueKey);
}
} catch (Exception e) {
StaffTransferImportFailureVO failure = new StaffTransferImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(e.getMessage());
failures.add(failure);
}
}
// 批量插入新数据
if (!newRecords.isEmpty()) {
saveBatch(newRecords, 500);
}
// 保存失败记录到Redis
if (!failures.isEmpty()) {
String failuresKey = "import:staffTransfer:" + taskId + ":failures";
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
}
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size());
result.setFailureCount(failures.size());
// 更新最终状态
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
updateImportStatus(taskId, finalStatus, result);
}
/**
* 批量查询已存在的调动记录唯一键
*
* @param excelList Excel数据列表
* @return 已存在的唯一键集合
*/
private Set<String> getExistingTransferKeys(List<CcdiStaffTransferExcel> excelList) {
// 提取所有有效的唯一键
Set<String> allKeys = excelList.stream()
.filter(excel -> excel.getStaffId() != null
&& excel.getDeptIdBefore() != null
&& excel.getDeptIdAfter() != null
&& excel.getTransferDate() != null)
.map(excel -> buildUniqueKey(excel.getStaffId(), excel.getDeptIdBefore(),
excel.getDeptIdAfter(), excel.getTransferDate()))
.collect(Collectors.toSet());
if (allKeys.isEmpty()) {
return Collections.emptySet();
}
// 查询数据库中已存在的记录
LambdaQueryWrapper<CcdiStaffTransfer> wrapper = new LambdaQueryWrapper<>();
wrapper.select(CcdiStaffTransfer::getStaffId,
CcdiStaffTransfer::getDeptIdBefore,
CcdiStaffTransfer::getDeptIdAfter,
CcdiStaffTransfer::getTransferDate);
List<CcdiStaffTransfer> existingTransfers = transferMapper.selectList(wrapper);
// 构建已存在的唯一键集合
return existingTransfers.stream()
.map(t -> buildUniqueKey(t.getStaffId(), t.getDeptIdBefore(),
t.getDeptIdAfter(), t.getTransferDate()))
.collect(Collectors.toSet());
}
/**
* 构建唯一键
*
* @param staffId 员工ID
* @param deptIdBefore 调动前部门ID
* @param deptIdAfter 调动后部门ID
* @param transferDate 调动日期
* @return 唯一键字符串
*/
private String buildUniqueKey(Long staffId, Long deptIdBefore, Long deptIdAfter, Date transferDate) {
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(transferDate);
return staffId + "_" + deptIdBefore + "_" + deptIdAfter + "_" + dateStr;
}
/**
* 验证员工调动记录数据
*
* @param addDTO 新增DTO
*/
private void validateTransferData(CcdiStaffTransferAddDTO addDTO) {
// 验证必填字段
if (addDTO.getStaffId() == null) {
throw new RuntimeException("员工ID不能为空");
}
if (StringUtils.isEmpty(addDTO.getTransferType())) {
throw new RuntimeException("调动类型不能为空");
}
if (addDTO.getTransferDate() == null) {
throw new RuntimeException("调动日期不能为空");
}
// 验证调动前部门ID
if (addDTO.getDeptIdBefore() == null) {
throw new RuntimeException("调动前部门ID不能为空");
}
// 验证调动后部门ID
if (addDTO.getDeptIdAfter() == null) {
throw new RuntimeException("调动后部门ID不能为空");
}
// 将调动类型从中文转换为码值
String transferTypeCode = DictUtils.getDictValue("ccdi_transfer_type", addDTO.getTransferType());
if (StringUtils.isEmpty(transferTypeCode)) {
throw new RuntimeException("调动类型[" + addDTO.getTransferType() + "]无效,请检查字典数据");
}
addDTO.setTransferType(transferTypeCode);
// 验证部门ID是否存在并获取部门名称
String deptNameBefore = getDeptNameById(addDTO.getDeptIdBefore());
addDTO.setDeptNameBefore(deptNameBefore);
String deptNameAfter = getDeptNameById(addDTO.getDeptIdAfter());
addDTO.setDeptNameAfter(deptNameAfter);
}
/**
* 根据部门ID查询部门名称
*
* @param deptId 部门ID
* @return 部门名称
* @throws RuntimeException 如果部门不存在
*/
private String getDeptNameById(Long deptId) {
if (deptId == null) {
return null;
}
SysDept dept = deptMapper.selectDeptById(deptId);
if (dept == null) {
throw new RuntimeException("部门ID " + deptId + " 不存在,请检查部门信息");
}
return dept.getDeptName();
}
/**
* 批量保存
*/
private void saveBatch(List<CcdiStaffTransfer> list, int batchSize) {
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<CcdiStaffTransfer> subList = list.subList(i, end);
transferMapper.insertBatch(subList);
}
}
/**
* 更新导入状态
*/
private void updateImportStatus(String taskId, String status, ImportResult result) {
String key = "import:staffTransfer:" + taskId;
Map<String, Object> statusData = new HashMap<>();
statusData.put("status", status);
statusData.put("totalCount", result.getTotalCount());
statusData.put("successCount", result.getSuccessCount());
statusData.put("failureCount", result.getFailureCount());
statusData.put("progress", 100);
statusData.put("endTime", System.currentTimeMillis());
if ("SUCCESS".equals(status)) {
statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据");
} else {
statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "");
}
redisTemplate.opsForHash().putAll(key, statusData);
}
@Override
public ImportStatusVO getImportStatus(String taskId) {
String key = "import:staffTransfer:" + taskId;
Boolean hasKey = redisTemplate.hasKey(key);
if (Boolean.FALSE.equals(hasKey)) {
throw new RuntimeException("任务不存在或已过期");
}
Map<Object, Object> statusMap = redisTemplate.opsForHash().entries(key);
ImportStatusVO statusVO = new ImportStatusVO();
statusVO.setTaskId((String) statusMap.get("taskId"));
statusVO.setStatus((String) statusMap.get("status"));
statusVO.setTotalCount((Integer) statusMap.get("totalCount"));
statusVO.setSuccessCount((Integer) statusMap.get("successCount"));
statusVO.setFailureCount((Integer) statusMap.get("failureCount"));
statusVO.setProgress((Integer) statusMap.get("progress"));
statusVO.setStartTime((Long) statusMap.get("startTime"));
statusVO.setEndTime((Long) statusMap.get("endTime"));
statusVO.setMessage((String) statusMap.get("message"));
return statusVO;
}
@Override
public List<StaffTransferImportFailureVO> getImportFailures(String taskId) {
String key = "import:staffTransfer:" + taskId + ":failures";
Object failuresObj = redisTemplate.opsForValue().get(key);
if (failuresObj == null) {
return Collections.emptyList();
}
return JSON.parseArray(JSON.toJSONString(failuresObj), StaffTransferImportFailureVO.class);
}
}

View File

@@ -0,0 +1,227 @@
package com.ruoyi.ccdi.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.CcdiStaffTransfer;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferQueryDTO;
import com.ruoyi.ccdi.domain.dto.TransferUniqueKey;
import com.ruoyi.ccdi.domain.excel.CcdiStaffTransferExcel;
import com.ruoyi.ccdi.domain.vo.CcdiStaffTransferVO;
import com.ruoyi.ccdi.mapper.CcdiBaseStaffMapper;
import com.ruoyi.ccdi.mapper.CcdiStaffTransferMapper;
import com.ruoyi.ccdi.service.ICcdiStaffTransferImportService;
import com.ruoyi.ccdi.service.ICcdiStaffTransferService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 员工调动记录 服务层处理
*
* @author ruoyi
* @date 2026-02-10
*/
@Service
public class CcdiStaffTransferServiceImpl implements ICcdiStaffTransferService {
@Resource
private CcdiStaffTransferMapper transferMapper;
@Resource
private ICcdiStaffTransferImportService transferImportService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private CcdiBaseStaffMapper staffMapper;
/**
* 查询员工调动记录列表
*
* @param queryDTO 查询条件
* @return 员工调动记录VO集合
*/
@Override
public java.util.List<CcdiStaffTransferVO> selectTransferList(CcdiStaffTransferQueryDTO queryDTO) {
Page<CcdiStaffTransferVO> page = new Page<>(1, Integer.MAX_VALUE);
Page<CcdiStaffTransferVO> resultPage = transferMapper.selectTransferPage(page, queryDTO);
return resultPage.getRecords();
}
/**
* 分页查询员工调动记录列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 员工调动记录VO分页结果
*/
@Override
public Page<CcdiStaffTransferVO> selectTransferPage(Page<CcdiStaffTransferVO> page, CcdiStaffTransferQueryDTO queryDTO) {
return transferMapper.selectTransferPage(page, queryDTO);
}
/**
* 查询员工调动记录列表(用于导出)
*
* @param queryDTO 查询条件
* @return 员工调动记录Excel实体集合
*/
@Override
public java.util.List<CcdiStaffTransferExcel> selectTransferListForExport(CcdiStaffTransferQueryDTO queryDTO) {
return transferMapper.selectTransferListForExport(queryDTO).stream().map(vo -> {
CcdiStaffTransferExcel excel = new CcdiStaffTransferExcel();
BeanUtils.copyProperties(vo, excel);
return excel;
}).collect(Collectors.toList());
}
/**
* 查询员工调动记录详情
*
* @param id 主键ID
* @return 员工调动记录VO
*/
@Override
public CcdiStaffTransferVO selectTransferById(Long id) {
return transferMapper.selectTransferById(id);
}
/**
* 新增员工调动记录
*
* @param addDTO 新增DTO
* @return 结果
*/
@Override
@Transactional
public int insertTransfer(CcdiStaffTransferAddDTO addDTO) {
// 唯一性校验
checkUniqueForAdd(addDTO);
CcdiStaffTransfer transfer = new CcdiStaffTransfer();
BeanUtils.copyProperties(addDTO, transfer);
int result = transferMapper.insert(transfer);
return result;
}
/**
* 修改员工调动记录
*
* @param editDTO 编辑DTO
* @return 结果
*/
@Override
@Transactional
public int updateTransfer(CcdiStaffTransferEditDTO editDTO) {
// 唯一性校验(排除当前记录)
checkUniqueForEdit(editDTO);
CcdiStaffTransfer transfer = new CcdiStaffTransfer();
BeanUtils.copyProperties(editDTO, transfer);
int result = transferMapper.updateById(transfer);
return result;
}
/**
* 批量删除员工调动记录
*
* @param ids 需要删除的主键ID
* @return 结果
*/
@Override
@Transactional
public int deleteTransferByIds(Long[] ids) {
return transferMapper.deleteBatchIds(java.util.List.of(ids));
}
/**
* 导入员工调动记录数据(异步)
*
* @param excelList Excel实体列表
* @return 任务ID
*/
@Override
@Transactional
public String importTransfer(java.util.List<CcdiStaffTransferExcel> excelList) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
throw new RuntimeException("至少需要一条数据");
}
// 生成任务ID
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
// 获取当前用户名
String userName = SecurityUtils.getUsername();
// 初始化Redis状态
String statusKey = "import:staffTransfer:" + taskId;
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", excelList.size());
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
statusData.put("startTime", startTime);
statusData.put("message", "正在处理...");
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
// 调用异步导入服务
transferImportService.importTransferAsync(excelList, taskId, userName);
return taskId;
}
/**
* 新增时校验唯一性
*
* @param addDTO 新增DTO
* @throws ServiceException 如果记录已存在
*/
@Override
public void checkUniqueForAdd(CcdiStaffTransferAddDTO addDTO) {
TransferUniqueKey key = TransferUniqueKey.from(addDTO);
CcdiStaffTransfer existing = transferMapper.checkExists(key);
if (existing != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateStr = sdf.format(addDTO.getTransferDate());
throw new ServiceException("该员工在 [" + dateStr + "] 的调动记录已存在(从[" +
addDTO.getDeptNameBefore() + "]调往[" + addDTO.getDeptNameAfter() + "]");
}
}
/**
* 编辑时校验唯一性
*
* @param editDTO 编辑DTO
* @throws ServiceException 如果记录已存在
*/
@Override
public void checkUniqueForEdit(CcdiStaffTransferEditDTO editDTO) {
TransferUniqueKey key = TransferUniqueKey.from(editDTO);
CcdiStaffTransfer existing = transferMapper.checkExistsExcludeId(key, editDTO.getId());
if (existing != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateStr = sdf.format(editDTO.getTransferDate());
throw new ServiceException("该员工在 [" + dateStr + "] 的调动记录已存在(从[" +
editDTO.getDeptNameBefore() + "]调往[" + editDTO.getDeptNameAfter() + "]");
}
}
}

View File

@@ -77,4 +77,25 @@
</foreach>
</insert>
<!-- 查询员工选项(用于下拉选择框) -->
<!-- 支持按员工ID或姓名模糊搜索只返回在职员工 -->
<select id="selectStaffOptions" resultType="com.ruoyi.ccdi.domain.vo.CcdiBaseStaffOptionVO">
SELECT
e.staff_id,
e.name,
e.dept_id,
d.dept_name
FROM ccdi_base_staff e
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id
<where>
e.status = '0'
<if test="query != null and query != ''">
AND (CAST(e.staff_id AS CHAR) LIKE CONCAT('%', #{query}, '%')
OR e.name LIKE CONCAT('%', #{query}, '%'))
</if>
</where>
ORDER BY e.staff_id
LIMIT 100
</select>
</mapper>

View File

@@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ccdi.mapper.CcdiStaffTransferMapper">
<!-- 员工调动记录ResultMap -->
<resultMap type="com.ruoyi.ccdi.domain.vo.CcdiStaffTransferVO" id="CcdiStaffTransferVOResult">
<id property="id" column="id"/>
<result property="staffId" column="staff_id"/>
<result property="staffName" column="staff_name"/>
<result property="transferType" column="transfer_type"/>
<result property="transferSubType" column="transfer_sub_type"/>
<result property="deptIdBefore" column="dept_id_before"/>
<result property="deptNameBefore" column="dept_name_before"/>
<result property="gradeBefore" column="grade_before"/>
<result property="positionBefore" column="position_before"/>
<result property="salaryLevelBefore" column="salary_level_before"/>
<result property="deptIdAfter" column="dept_id_after"/>
<result property="deptNameAfter" column="dept_name_after"/>
<result property="gradeAfter" column="grade_after"/>
<result property="positionAfter" column="position_after"/>
<result property="salaryLevelAfter" column="salary_level_after"/>
<result property="transferDate" column="transfer_date"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="createdBy" column="created_by"/>
<result property="updatedBy" column="updated_by"/>
</resultMap>
<!-- 分页查询员工调动记录列表 -->
<select id="selectTransferPage" resultMap="CcdiStaffTransferVOResult">
SELECT
t.id, t.staff_id, s.name as staff_name, t.transfer_type, t.transfer_sub_type,
t.dept_id_before, t.dept_name_before, t.grade_before, t.position_before, t.salary_level_before,
t.dept_id_after, t.dept_name_after, t.grade_after, t.position_after, t.salary_level_after,
t.transfer_date, t.created_by, t.create_time, t.updated_by, t.update_time
FROM ccdi_staff_transfer t
LEFT JOIN ccdi_base_staff s ON t.staff_id = s.staff_id
<where>
<if test="query.staffId != null">
AND t.staff_id = #{query.staffId}
</if>
<if test="query.staffName != null and query.staffName != ''">
AND s.name LIKE CONCAT('%', #{query.staffName}, '%')
</if>
<if test="query.transferType != null and query.transferType != ''">
AND t.transfer_type = #{query.transferType}
</if>
<if test="query.transferSubType != null and query.transferSubType != ''">
AND t.transfer_sub_type = #{query.transferSubType}
</if>
<if test="query.deptIdBefore != null">
AND t.dept_id_before = #{query.deptIdBefore}
</if>
<if test="query.deptIdAfter != null">
AND t.dept_id_after = #{query.deptIdAfter}
</if>
<if test="query.transferDateStart != null">
AND t.transfer_date &gt;= #{query.transferDateStart}
</if>
<if test="query.transferDateEnd != null">
AND t.transfer_date &lt;= #{query.transferDateEnd}
</if>
</where>
ORDER BY t.transfer_date DESC, t.create_time DESC
</select>
<!-- 查询员工调动记录详情 -->
<select id="selectTransferById" resultMap="CcdiStaffTransferVOResult">
SELECT
t.id, t.staff_id, s.name as staff_name, t.transfer_type, t.transfer_sub_type,
t.dept_id_before, t.dept_name_before, t.grade_before, t.position_before, t.salary_level_before,
t.dept_id_after, t.dept_name_after, t.grade_after, t.position_after, t.salary_level_after,
t.transfer_date, t.created_by, t.create_time, t.updated_by, t.update_time
FROM ccdi_staff_transfer t
LEFT JOIN ccdi_base_staff s ON t.staff_id = s.staff_id
WHERE t.id = #{id}
</select>
<!-- 查询员工调动记录列表(用于导出) -->
<select id="selectTransferListForExport" resultMap="CcdiStaffTransferVOResult">
SELECT
t.id, t.staff_id, s.name as staff_name, t.transfer_type, t.transfer_sub_type,
t.dept_id_before, t.dept_name_before, t.grade_before, t.position_before, t.salary_level_before,
t.dept_id_after, t.dept_name_after, t.grade_after, t.position_after, t.salary_level_after,
t.transfer_date, t.created_by, t.create_time, t.updated_by, t.update_time
FROM ccdi_staff_transfer t
LEFT JOIN ccdi_base_staff s ON t.staff_id = s.staff_id
<where>
<if test="query.staffId != null">
AND t.staff_id = #{query.staffId}
</if>
<if test="query.staffName != null and query.staffName != ''">
AND s.name LIKE CONCAT('%', #{query.staffName}, '%')
</if>
<if test="query.transferType != null and query.transferType != ''">
AND t.transfer_type = #{query.transferType}
</if>
<if test="query.transferSubType != null and query.transferSubType != ''">
AND t.transfer_sub_type = #{query.transferSubType}
</if>
<if test="query.deptIdBefore != null">
AND t.dept_id_before = #{query.deptIdBefore}
</if>
<if test="query.deptIdAfter != null">
AND t.dept_id_after = #{query.deptIdAfter}
</if>
<if test="query.transferDateStart != null">
AND t.transfer_date &gt;= #{query.transferDateStart}
</if>
<if test="query.transferDateEnd != null">
AND t.transfer_date &lt;= #{query.transferDateEnd}
</if>
</where>
ORDER BY t.transfer_date DESC, t.create_time DESC
</select>
<!-- 批量插入员工调动记录数据 -->
<insert id="insertBatch">
INSERT INTO ccdi_staff_transfer
(staff_id, transfer_type, transfer_sub_type, dept_id_before, dept_name_before, grade_before,
position_before, salary_level_before, dept_id_after, dept_name_after, grade_after,
position_after, salary_level_after, transfer_date, created_by, create_time, updated_by, update_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.staffId}, #{item.transferType}, #{item.transferSubType}, #{item.deptIdBefore},
#{item.deptNameBefore}, #{item.gradeBefore}, #{item.positionBefore}, #{item.salaryLevelBefore},
#{item.deptIdAfter}, #{item.deptNameAfter}, #{item.gradeAfter}, #{item.positionAfter},
#{item.salaryLevelAfter}, #{item.transferDate}, #{item.createdBy}, NOW(), #{item.updatedBy}, NOW())
</foreach>
</insert>
<!-- 查询单条记录是否存在(根据唯一键) -->
<select id="checkExists" resultType="com.ruoyi.ccdi.domain.CcdiStaffTransfer">
SELECT
id, staff_id, dept_id_before, dept_id_after, transfer_date
FROM ccdi_staff_transfer
WHERE staff_id = #{key.staffId}
AND dept_id_before = #{key.deptIdBefore}
AND dept_id_after = #{key.deptIdAfter}
AND transfer_date = #{key.transferDate}
LIMIT 1
</select>
<!-- 查询单条记录是否存在排除指定ID -->
<select id="checkExistsExcludeId" resultType="com.ruoyi.ccdi.domain.CcdiStaffTransfer">
SELECT
id, staff_id, dept_id_before, dept_id_after, transfer_date
FROM ccdi_staff_transfer
WHERE staff_id = #{key.staffId}
AND dept_id_before = #{key.deptIdBefore}
AND dept_id_after = #{key.deptIdAfter}
AND transfer_date = #{key.transferDate}
AND id != #{excludeId}
LIMIT 1
</select>
</mapper>