tuqin
11 小时以前 89071e0c09188d75c3d537f82204ae19713ac7fe
feat(hrm): 合同生成与模板下载、员工转正、导入导出字段对齐

- 员工合同:新增『生成劳动合同/劳务合同』(poi-tl 渲染,回存为合同附件)与『下载合同空白模板』(原样原件)
- 员工:新增『转正』(仅劳动合同可转正,状态试用→在职,不含调岗)与待转正提醒接口
- 员工导入/导出/模板:列名与字典对齐(手机号/用户角色/性别 hrm_gender),补齐员工类别、基本工资、开户银行、银行账号、居住地址
已修改8个文件
已添加9个文件
515 ■■■■■ 文件已修改
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportExcelVO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ByteArrayMultipartFile.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentServiceImpl.java 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/resources/template/contract/contract_labor_simple.docx 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/resources/template/contract/contract_labor_standard.docx 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/resources/template/contract/contract_service.docx 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/resources/template/contract/original/labor_simple.docx 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/resources/template/contract/original/labor_standard.docx 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/resources/template/contract/original/service.doc 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -125,6 +125,9 @@
    ErrorCode SALARY_STRUCTURE_IN_USE = new ErrorCode(1_030_005_026, "薪酬结构正在被员工使用,无法删除");
    ErrorCode OVERTIME_WAGE_CONFIG_NOT_EXISTS = new ErrorCode(1_030_005_027, "岗位加班工资配置不存在");
    ErrorCode OVERTIME_WAGE_CONFIG_POST_EXISTS = new ErrorCode(1_030_005_028, "该岗位已配置加班工资,请勿重复添加");
    ErrorCode CONTRACT_TEMPLATE_TYPE_NOT_SUPPORT = new ErrorCode(1_030_005_029, "不支持的合同模板类型");
    ErrorCode CONTRACT_TEMPLATE_LOAD_FAILED = new ErrorCode(1_030_005_030, "合同模板加载失败");
    ErrorCode EMPLOYEE_REGULARIZE_NEED_LABOR_CONTRACT = new ErrorCode(1_030_005_031, "该员工当前合同不是劳动合同,无法转正");
    // ========== ç­æ¬¡ç®¡ç† 1-030-009-000 ==========
    ErrorCode SHIFT_NOT_EXISTS = new ErrorCode(1_030_009_000, "班次不存在");
yudao-module-hrm/pom.xml
@@ -70,6 +70,13 @@
            <artifactId>yudao-spring-boot-starter-excel</artifactId>
        </dependency>
        <!-- Word åˆåŒæ¨¡æ¿æ¸²æŸ“(poi-tl) -->
        <dependency>
            <groupId>com.deepoove</groupId>
            <artifactId>poi-tl</artifactId>
            <version>1.12.2</version>
        </dependency>
        <!-- Test æµ‹è¯•相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java
@@ -11,6 +11,11 @@
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractTerminateReqVO;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeContractService;
import cn.iocoder.yudao.module.hrm.service.employee.contract.ContractDocumentService;
import cn.iocoder.yudao.module.hrm.service.employee.contract.ByteArrayMultipartFile;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.service.storage.SystemStorageBlobService;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -22,12 +27,19 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.CONTRACT_TEMPLATE_LOAD_FAILED;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.CONTRACT_TEMPLATE_TYPE_NOT_SUPPORT;
@Tag(name = "管理后台 - HRM å‘˜å·¥åˆåŒ")
@RestController
@@ -37,6 +49,12 @@
    @Resource
    private HrmEmployeeContractService contractService;
    @Resource
    private ContractDocumentService contractDocumentService;
    @Resource
    private SystemStorageBlobService storageBlobService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建员工合同")
@@ -92,6 +110,72 @@
        return success(contractService.getExpiringCount());
    }
    @PostMapping("/generate")
    @Operation(summary = "生成合同并回存为合同附件")
    @Parameter(name = "id", description = "合同ID", required = true)
    @Parameter(name = "templateType", description = "模板类型:1-劳动合同(简版) 2-劳动合同(标准) 3-劳务合同", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:employee-contract:query')")
    public CommonResult<Long> generateContract(@RequestParam("id") Long id,
                                               @RequestParam("templateType") Integer templateType) {
        byte[] bytes = contractDocumentService.generateDocx(id, templateType);
        String fileName = contractDocumentService.buildFileName(id, templateType);
        MultipartFile file = new ByteArrayMultipartFile("file", fileName,
                "application/vnd.openxmlformats-officedocument.wordprocessingml.document", bytes);
        Long blobId = storageBlobService.upload(Collections.singletonList(file), false).get(0).getId();
        storageAttachmentApi.bindAttachments("file", "hrm_employee_contract", id, Collections.singletonList(blobId));
        return success(blobId);
    }
    @GetMapping("/generate-docx")
    @Operation(summary = "生成并下载合同文档(docx)")
    @Parameter(name = "id", description = "合同ID", required = true)
    @Parameter(name = "templateType", description = "模板类型:1-劳动合同(简版) 2-劳动合同(标准) 3-劳务合同", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:employee-contract:query')")
    public void generateDocx(@RequestParam("id") Long id,
                             @RequestParam("templateType") Integer templateType,
                             HttpServletResponse response) throws IOException {
        byte[] bytes = contractDocumentService.generateDocx(id, templateType);
        String fileName = URLEncoder.encode(contractDocumentService.buildFileName(id, templateType), StandardCharsets.UTF_8);
        response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        response.setHeader("Content-Disposition", "attachment; filename=" + fileName + "; filename*=UTF-8''" + fileName);
        response.getOutputStream().write(bytes);
    }
    @GetMapping("/download-template")
    @Operation(summary = "下载合同空白模板", description = "1-劳动合同(简版) 2-劳动合同(标准) 3-劳务合同")
    @Parameter(name = "type", description = "模板类型:1-劳动合同(简版) 2-劳动合同(标准) 3-劳务合同", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:employee-contract:query')")
    public void downloadTemplate(@RequestParam("type") Integer type, HttpServletResponse response) throws IOException {
        String path;
        String fileName;
        String contentType;
        if (type == 1) {
            path = "template/contract/original/labor_simple.docx";
            fileName = "劳动合同(简版).docx";
            contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
        } else if (type == 2) {
            path = "template/contract/original/labor_standard.docx";
            fileName = "劳动合同(标准).docx";
            contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
        } else if (type == 3) {
            path = "template/contract/original/service.doc";
            fileName = "劳务合同.doc";
            contentType = "application/msword";
        } else {
            throw exception(CONTRACT_TEMPLATE_TYPE_NOT_SUPPORT);
        }
        try (InputStream in = getClass().getClassLoader().getResourceAsStream(path)) {
            if (in == null) {
                throw exception(CONTRACT_TEMPLATE_LOAD_FAILED);
            }
            byte[] bytes = in.readAllBytes();
            String enc = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
            response.setContentType(contentType);
            response.setHeader("Content-Disposition", "attachment; filename=" + enc + "; filename*=UTF-8''" + enc);
            response.getOutputStream().write(bytes);
        }
    }
    @GetMapping("/import-template")
    @Operation(summary = "下载员工合同导入模板")
    @PreAuthorize("@ss.hasPermission('hrm:employee-contract:import')")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java
@@ -222,4 +222,22 @@
        return success(employeeService.importEmployeeList(list, importReqVO));
    }
    @PutMapping("/regular")
    @Operation(summary = "员工转正", description = "仅『劳动合同』员工可转正;劳务合同不转正。转正只改状态+转正日期,不含调岗")
    @Parameter(name = "id", description = "员工ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:employee:update')")
    public CommonResult<Boolean> regularizeEmployee(@RequestParam("id") Long id) {
        employeeService.regularizeEmployee(id);
        return success(true);
    }
    @GetMapping("/regularize-reminders")
    @Operation(summary = "获得待转正提醒列表", description = "试用中、且当前为劳动合同、试用期在 days å¤©å†…到期的员工")
    @PreAuthorize("@ss.hasPermission('hrm:employee:query')")
    public CommonResult<List<HrmEmployeeRespVO>> getRegularizeReminders(
            @RequestParam(value = "days", required = false, defaultValue = "7") Integer days) {
        List<HrmEmployeeDO> list = employeeService.getRegularizeReminderList(days);
        return success(BeanUtils.toBean(list, HrmEmployeeRespVO.class));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportExcelVO.java
@@ -8,6 +8,7 @@
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
@@ -45,6 +46,10 @@
    @DictFormat("hrm_employee_status")
    private Integer employeeStatus;
    @ExcelProperty(value = "员工类别", converter = DictConvert.class)
    @DictFormat("hrm_employee_category")
    private Integer employeeCategory;
    @ExcelProperty("身份证号")
    private String idCard;
@@ -72,4 +77,16 @@
    @ExcelProperty("备注")
    private String remark;
    @ExcelProperty("基本工资")
    private BigDecimal baseSalary;
    @ExcelProperty("开户银行")
    private String bankName;
    @ExcelProperty("银行账号")
    private String bankAccount;
    @ExcelProperty("居住地址")
    private String address;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java
@@ -46,7 +46,7 @@
    @Schema(description = "性别")
    @ExcelProperty(value = "性别", converter = DictConvert.class)
    @DictFormat("system_user_sex")
    @DictFormat("hrm_gender")
    private Integer gender;
    @Schema(description = "出生日期")
@@ -54,7 +54,7 @@
    private LocalDate birthday;
    @Schema(description = "手机号码")
    @ExcelProperty("手机号码")
    @ExcelProperty("手机号")
    private String phone;
    @Schema(description = "邮箱")
@@ -126,7 +126,7 @@
    private List<Long> roleIds;
    @Schema(description = "角色名称列表")
    @ExcelProperty("角色")
    @ExcelProperty("用户角色")
    private String roleNames;
    @Schema(description = "开户银行")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java
@@ -115,4 +115,19 @@
    HrmEmployeeImportRespVO importEmployeeList(List<HrmEmployeeImportExcelVO> importEmployees,
                                               HrmEmployeeImportReqVO importReqVO);
    /**
     * å‘˜å·¥è½¬æ­£ï¼šä»…「劳动合同」可转正(劳务合同不可),状态 è¯•用 -> åœ¨èŒï¼Œè®°å½•转正日期
     *
     * @param id å‘˜å·¥ID
     */
    void regularizeEmployee(Long id);
    /**
     * èŽ·å¾—å¾…è½¬æ­£æé†’åˆ—è¡¨ï¼šè¯•ç”¨ä¸­ã€ä¸”å½“å‰ä¸ºåŠ³åŠ¨åˆåŒã€è¯•ç”¨æœŸåœ¨ days å¤©å†…到期的员工
     *
     * @param days æå‰å¤©æ•°
     * @return å‘˜å·¥åˆ—表
     */
    List<HrmEmployeeDO> getRegularizeReminderList(Integer days);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEducationSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEmergencyContactSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportExcelVO;
@@ -16,10 +17,12 @@
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSimpleRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeWorkHistorySaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeContractDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeEducationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeEmergencyContactDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeWorkHistoryDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeContractMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeEducationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeEmergencyContactMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
@@ -42,6 +45,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -68,6 +72,8 @@
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Resource
    private HrmEmployeeContractMapper contractMapper;
    @Resource
    private HrmEmployeeEducationMapper educationMapper;
    @Resource
@@ -475,6 +481,11 @@
        reqVO.setEducation(importEmployee.getEducation());
        reqVO.setPoliticalStatus(importEmployee.getPoliticalStatus());
        reqVO.setRemark(importEmployee.getRemark());
        reqVO.setEmployeeCategory(importEmployee.getEmployeeCategory());
        reqVO.setBaseSalary(importEmployee.getBaseSalary());
        reqVO.setBankName(importEmployee.getBankName());
        reqVO.setBankAccount(importEmployee.getBankAccount());
        reqVO.setAddress(importEmployee.getAddress());
        return reqVO;
    }
@@ -530,4 +541,57 @@
        return roleIds;
    }
    // ========== è½¬æ­£ ==========
    @Override
    public void regularizeEmployee(Long id) {
        validateEmployee(id);
        HrmEmployeeContractDO contract = findCurrentContract(id);
        // ä»…「劳动合同」可转正;劳务合同不转正
        if (contract == null || !Integer.valueOf(1).equals(contract.getContractType())) {
            throw exception(EMPLOYEE_REGULARIZE_NEED_LABOR_CONTRACT);
        }
        HrmEmployeeDO updateObj = new HrmEmployeeDO();
        updateObj.setId(id);
        updateObj.setEmployeeStatus(1); // 1-在职
        updateObj.setRegularDate(LocalDate.now());
        employeeMapper.updateById(updateObj);
    }
    @Override
    public List<HrmEmployeeDO> getRegularizeReminderList(Integer days) {
        int aheadDays = days == null ? 7 : days;
        LocalDate limit = LocalDate.now().plusDays(aheadDays);
        // è¯•用中的员工
        List<HrmEmployeeDO> trials = employeeMapper.selectList(
                new LambdaQueryWrapperX<HrmEmployeeDO>().eq(HrmEmployeeDO::getEmployeeStatus, 2));
        List<HrmEmployeeDO> result = new ArrayList<>();
        for (HrmEmployeeDO employee : trials) {
            HrmEmployeeContractDO contract = findCurrentContract(employee.getId());
            // ä»…劳动合同、且试用期即将到期
            if (contract == null || !Integer.valueOf(1).equals(contract.getContractType())) {
                continue;
            }
            LocalDate probationEnd = contract.getProbationEndDate();
            if (probationEnd != null && !probationEnd.isAfter(limit)) {
                result.add(employee);
            }
        }
        return result;
    }
    /** æŸ¥è¯¢å‘˜å·¥å½“前生效合同 */
    private HrmEmployeeContractDO findCurrentContract(Long employeeId) {
        List<HrmEmployeeContractDO> list = contractMapper.selectListByEmployee(employeeId);
        if (CollUtil.isEmpty(list)) {
            return null;
        }
        for (HrmEmployeeContractDO contract : list) {
            if (Boolean.TRUE.equals(contract.getIsCurrent())) {
                return contract;
            }
        }
        return null;
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ByteArrayMultipartFile.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.hrm.service.employee.contract;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
 * å­—节数组版 MultipartFile:把内存中的字节包装成 MultipartFile,
 * ä¾¿äºŽå¤ç”¨ system çš„ SystemStorageBlobService.upload(...) å­˜å‚¨æ–‡ä»¶å¹¶æ‹¿åˆ° blobId。
 */
public class ByteArrayMultipartFile implements MultipartFile {
    private final String name;
    private final String originalFilename;
    private final String contentType;
    private final byte[] content;
    public ByteArrayMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
        this.name = name;
        this.originalFilename = originalFilename;
        this.contentType = contentType;
        this.content = content == null ? new byte[0] : content;
    }
    @Override
    public String getName() {
        return name;
    }
    @Override
    public String getOriginalFilename() {
        return originalFilename;
    }
    @Override
    public String getContentType() {
        return contentType;
    }
    @Override
    public boolean isEmpty() {
        return content.length == 0;
    }
    @Override
    public long getSize() {
        return content.length;
    }
    @Override
    public byte[] getBytes() {
        return content;
    }
    @Override
    public InputStream getInputStream() {
        return new ByteArrayInputStream(content);
    }
    @Override
    public void transferTo(File dest) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(dest)) {
            fos.write(content);
        }
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.hrm.service.employee.contract;
/**
 * å‘˜å·¥åˆåŒæ–‡æ¡£ç”Ÿæˆ Service
 *
 * æŒ‰åˆåŒ + å‘˜å·¥æ•°æ®ï¼Œå¥—用 Word æ¨¡æ¿ç”ŸæˆåŠ³åŠ¨åˆåŒ / åŠ³åŠ¡åˆåŒ docx。
 *
 * @author è¶…级管理员
 */
public interface ContractDocumentService {
    /**
     * ç”ŸæˆåˆåŒ docx
     *
     * @param contractId   åˆåŒç¼–号
     * @param templateType æ¨¡æ¿ç±»åž‹ï¼š1-劳动合同(简版) 2-劳动合同(标准) 3-劳务合同
     * @return docx å­—节
     */
    byte[] generateDocx(Long contractId, Integer templateType);
    /**
     * ç”Ÿæˆæ–‡ä»¶å
     */
    String buildFileName(Long contractId, Integer templateType);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,204 @@
package cn.iocoder.yudao.module.hrm.service.employee.contract;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeContractDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeContractMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import com.deepoove.poi.XWPFTemplate;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.CONTRACT_TEMPLATE_LOAD_FAILED;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.CONTRACT_TEMPLATE_TYPE_NOT_SUPPORT;
/**
 * å‘˜å·¥åˆåŒæ–‡æ¡£ç”Ÿæˆ Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Slf4j
@Service
public class ContractDocumentServiceImpl implements ContractDocumentService {
    /** åŠ³åŠ¨åˆåŒï¼ˆç®€ç‰ˆÂ·ç¤¾ä¿æŠ˜çŽ°ï¼‰ */
    public static final int TYPE_LABOR_SIMPLE = 1;
    /** åŠ³åŠ¨åˆåŒï¼ˆæ ‡å‡†Â·å®˜æ–¹åˆ¶å¼ï¼‰ */
    public static final int TYPE_LABOR_STANDARD = 2;
    /** åŠ³åŠ¡åˆåŒ */
    public static final int TYPE_SERVICE = 3;
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyå¹´MM月dd日");
    private static final Map<Integer, String> TEMPLATE_PATHS = Map.of(
            TYPE_LABOR_SIMPLE, "template/contract/contract_labor_simple.docx",
            TYPE_LABOR_STANDARD, "template/contract/contract_labor_standard.docx",
            TYPE_SERVICE, "template/contract/contract_service.docx");
    private static final Map<Integer, String> TEMPLATE_NAMES = Map.of(
            TYPE_LABOR_SIMPLE, "劳动合同(简版)",
            TYPE_LABOR_STANDARD, "劳动合同(标准)",
            TYPE_SERVICE, "劳务合同");
    @Resource
    private HrmEmployeeContractMapper contractMapper;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Override
    public byte[] generateDocx(Long contractId, Integer templateType) {
        HrmEmployeeContractDO contract = contractMapper.selectById(contractId);
        if (contract == null) {
            throw exception(CONTRACT_TEMPLATE_LOAD_FAILED);
        }
        HrmEmployeeDO employee = contract.getEmployeeId() == null ? null
                : employeeMapper.selectById(contract.getEmployeeId());
        Map<String, Object> vars = buildVariables(contract, employee);
        String path = TEMPLATE_PATHS.get(templateType);
        if (path == null) {
            throw exception(CONTRACT_TEMPLATE_TYPE_NOT_SUPPORT);
        }
        try (InputStream in = getClass().getClassLoader().getResourceAsStream(path)) {
            if (in == null) {
                throw exception(CONTRACT_TEMPLATE_LOAD_FAILED);
            }
            XWPFTemplate template = XWPFTemplate.compile(in).render(vars);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            template.write(out);
            template.close();
            return out.toByteArray();
        } catch (Exception e) {
            log.error("[generateDocx][合同({}) æ¨¡æ¿({}) ç”Ÿæˆå¤±è´¥]", contractId, templateType, e);
            throw exception(CONTRACT_TEMPLATE_LOAD_FAILED);
        }
    }
    @Override
    public String buildFileName(Long contractId, Integer templateType) {
        HrmEmployeeContractDO contract = contractMapper.selectById(contractId);
        String no = contract != null && contract.getContractNo() != null ? contract.getContractNo() : String.valueOf(contractId);
        String name = TEMPLATE_NAMES.getOrDefault(templateType, "合同");
        return no + "-" + name + ".docx";
    }
    private Map<String, Object> buildVariables(HrmEmployeeContractDO contract, HrmEmployeeDO employee) {
        Map<String, Object> vars = new HashMap<>();
        // ç”²æ–¹ï¼ˆå…¬å¸ï¼‰â€”— ç›®å‰å–签约主体,其余留空后续可接入公司配置
        vars.put("companyName", nvl(contract.getSignCompany()));
        vars.put("companyAddress", "");
        vars.put("legalPerson", "");
        vars.put("companyPhone", "");
        vars.put("creditCode", "");
        // ä¹™æ–¹ï¼ˆå‘˜å·¥ï¼‰
        vars.put("employeeName", employee != null ? nvl(employee.getName()) : "");
        vars.put("gender", employee != null ? genderText(employee.getGender()) : "");
        vars.put("age", employee != null ? nvl(employee.getAge()) : "");
        vars.put("idCard", employee != null ? nvl(employee.getIdCard()) : "");
        vars.put("householdAddress", employee != null ? nvl(employee.getAddress()) : "");
        vars.put("address", employee != null ? nvl(employee.getAddress()) : "");
        vars.put("phone", employee != null ? nvl(employee.getPhone()) : "");
        vars.put("emergencyPhone", "");
        // åˆåŒ
        vars.put("contractNo", nvl(contract.getContractNo()));
        vars.put("postName", "");
        vars.put("postDuty", "");
        vars.put("workPlace", "");
        vars.put("contractStart", formatDate(contract.getStartDate()));
        vars.put("contractEnd", formatDate(contract.getEndDate()));
        vars.put("contractMonths", monthsBetween(contract.getStartDate(), contract.getEndDate()));
        vars.put("probationStart", formatDate(contract.getProbationStartDate()));
        vars.put("probationEnd", formatDate(contract.getProbationEndDate()));
        vars.put("probationMonths", monthsBetween(contract.getProbationStartDate(), contract.getProbationEndDate()));
        vars.put("probationDays", daysBetween(contract.getProbationStartDate(), contract.getProbationEndDate()));
        vars.put("birthday", employee != null ? formatDate(employee.getBirthday()) : "");
        // å¹´æœˆæ—¥ï¼ˆä¾›ã€Œ___å¹´___月___日」逐格填写)
        vars.put("contractStartY", y(contract.getStartDate()));
        vars.put("contractStartM", mo(contract.getStartDate()));
        vars.put("contractStartD", d(contract.getStartDate()));
        vars.put("contractEndY", y(contract.getEndDate()));
        vars.put("contractEndM", mo(contract.getEndDate()));
        vars.put("contractEndD", d(contract.getEndDate()));
        vars.put("probationStartY", y(contract.getProbationStartDate()));
        vars.put("probationStartM", mo(contract.getProbationStartDate()));
        vars.put("probationStartD", d(contract.getProbationStartDate()));
        vars.put("probationEndY", y(contract.getProbationEndDate()));
        vars.put("probationEndM", mo(contract.getProbationEndDate()));
        vars.put("probationEndD", d(contract.getProbationEndDate()));
        vars.put("probationSalary", nvl(contract.getProbationSalary()));
        vars.put("regularSalary", nvl(contract.getRegularSalary()));
        vars.put("baseSalary", nvl(contract.getRegularSalary()));
        vars.put("performanceSalary", "");
        vars.put("socialSecuritySalary", "");
        vars.put("signDate", formatDate(contract.getSignDate()));
        // åŠ³åŠ¡åˆåŒé‡Œçš„å¯é€‰é¡¹
        vars.put("payType", "一");
        vars.put("payDay", "");
        vars.put("workVolume", "");
        vars.put("workAmount", "");
        vars.put("piecePrice", "");
        vars.put("noticeCompensation", "");
        vars.put("copies", "两");
        vars.put("eachCopies", "一");
        return vars;
    }
    private static String nvl(Object o) {
        return o == null ? "" : o.toString();
    }
    private static String formatDate(LocalDate d) {
        return d == null ? "" : d.format(DATE_FMT);
    }
    private static String monthsBetween(LocalDate start, LocalDate end) {
        if (start == null || end == null) {
            return "";
        }
        long months = ChronoUnit.MONTHS.between(start, end);
        return String.valueOf(Math.max(months, 0));
    }
    private static String daysBetween(LocalDate start, LocalDate end) {
        if (start == null || end == null) {
            return "";
        }
        long days = ChronoUnit.DAYS.between(start, end);
        return String.valueOf(Math.max(days, 0));
    }
    private static String y(LocalDate date) {
        return date == null ? "" : String.format("%04d", date.getYear());
    }
    private static String mo(LocalDate date) {
        return date == null ? "" : String.format("%02d", date.getMonthValue());
    }
    private static String d(LocalDate date) {
        return date == null ? "" : String.format("%02d", date.getDayOfMonth());
    }
    private static String genderText(Integer sex) {
        if (sex == null) {
            return "";
        }
        return sex == 1 ? "男" : sex == 2 ? "女" : "";
    }
    // å…¼å®¹ BigDecimal ç›´æŽ¥ toString
    private static String nvl(BigDecimal v) {
        return v == null ? "" : v.toPlainString();
    }
}
yudao-module-hrm/src/main/resources/template/contract/contract_labor_simple.docx
Binary files differ
yudao-module-hrm/src/main/resources/template/contract/contract_labor_standard.docx
Binary files differ
yudao-module-hrm/src/main/resources/template/contract/contract_service.docx
Binary files differ
yudao-module-hrm/src/main/resources/template/contract/original/labor_simple.docx
Binary files differ
yudao-module-hrm/src/main/resources/template/contract/original/labor_standard.docx
Binary files differ
yudao-module-hrm/src/main/resources/template/contract/original/service.doc
Binary files differ