From 89071e0c09188d75c3d537f82204ae19713ac7fe Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期一, 12 十月 2026 10:09:33 +0800
Subject: [PATCH] feat(hrm): 合同生成与模板下载、员工转正、导入导出字段对齐

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ByteArrayMultipartFile.java        |   71 +++++++
 yudao-module-hrm/src/main/resources/template/contract/original/service.doc                                              |    0 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java         |   18 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java          |    6 
 yudao-module-hrm/src/main/resources/template/contract/contract_service.docx                                             |    0 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java                 |   64 +++++++
 yudao-module-hrm/src/main/resources/template/contract/original/labor_simple.docx                                        |    0 
 yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java                            |    3 
 yudao-module-hrm/src/main/resources/template/contract/contract_labor_simple.docx                                        |    0 
 yudao-module-hrm/pom.xml                                                                                                |    7 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentServiceImpl.java   |  204 ++++++++++++++++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportExcelVO.java   |   17 +
 yudao-module-hrm/src/main/resources/template/contract/original/labor_standard.docx                                      |    0 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java |   84 +++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java                     |   15 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentService.java       |   26 ++
 yudao-module-hrm/src/main/resources/template/contract/contract_labor_standard.docx                                      |    0 
 17 files changed, 512 insertions(+), 3 deletions(-)

diff --git a/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java b/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
index 275c031..2b2a428 100644
--- a/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
+++ b/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, "钖叕缁撴瀯姝e湪琚憳宸ヤ娇鐢紝鏃犳硶鍒犻櫎");
     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, "鐝涓嶅瓨鍦�");
diff --git a/yudao-module-hrm/pom.xml b/yudao-module-hrm/pom.xml
index df07d9c..0f56438 100644
--- a/yudao-module-hrm/pom.xml
+++ b/yudao-module-hrm/pom.xml
@@ -70,6 +70,13 @@
             <artifactId>yudao-spring-boot-starter-excel</artifactId>
         </dependency>
 
+        <!-- Word 鍚堝悓妯℃澘娓叉煋锛坧oi-tl锛� -->
+        <dependency>
+            <groupId>com.deepoove</groupId>
+            <artifactId>poi-tl</artifactId>
+            <version>1.12.2</version>
+        </dependency>
+
         <!-- Test 娴嬭瘯鐩稿叧 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java
index 0530349..ebc01fe 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java
+++ b/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 = "鐢熸垚骞朵笅杞藉悎鍚屾枃妗o紙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')")
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java
index 4d4829a..0e07916 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java
+++ b/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 = "浠呫�庡姵鍔ㄥ悎鍚屻�忓憳宸ュ彲杞锛涘姵鍔″悎鍚屼笉杞銆傝浆姝e彧鏀圭姸鎬�+杞鏃ユ湡锛屼笉鍚皟宀�")
+    @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 = "鑾峰緱寰呰浆姝f彁閱掑垪琛�", 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));
+    }
+
 }
\ No newline at end of file
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportExcelVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportExcelVO.java
index 977f0c1..20f930f 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeImportExcelVO.java
+++ b/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;
+
 }
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java
index 383b2b5..687c923 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java
+++ b/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 = "寮�鎴烽摱琛�")
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java
index 90424ba..9fff42d 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java
+++ b/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);
 
+    /**
+     * 鍛樺伐杞锛氫粎銆屽姵鍔ㄥ悎鍚屻�嶅彲杞锛堝姵鍔″悎鍚屼笉鍙級锛岀姸鎬� 璇曠敤 -> 鍦ㄨ亴锛岃褰曡浆姝f棩鏈�
+     *
+     * @param id 鍛樺伐ID
+     */
+    void regularizeEmployee(Long id);
+
+    /**
+     * 鑾峰緱寰呰浆姝f彁閱掑垪琛細璇曠敤涓�佷笖褰撳墠涓哄姵鍔ㄥ悎鍚屻�佽瘯鐢ㄦ湡鍦� days 澶╁唴鍒版湡鐨勫憳宸�
+     *
+     * @param days 鎻愬墠澶╂暟
+     * @return 鍛樺伐鍒楄〃
+     */
+    List<HrmEmployeeDO> getRegularizeReminderList(Integer days);
+
 }
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
index 92a709e..3864fd3 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
+++ b/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;
+    }
+
 }
\ No newline at end of file
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ByteArrayMultipartFile.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ByteArrayMultipartFile.java
new file mode 100644
index 0000000..4fc2c2e
--- /dev/null
+++ b/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);
+        }
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentService.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentService.java
new file mode 100644
index 0000000..ae7a69a
--- /dev/null
+++ b/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);
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/contract/ContractDocumentServiceImpl.java
new file mode 100644
index 0000000..dbb8373
--- /dev/null
+++ b/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骞碝M鏈坉d鏃�");
+
+    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();
+    }
+
+}
diff --git a/yudao-module-hrm/src/main/resources/template/contract/contract_labor_simple.docx b/yudao-module-hrm/src/main/resources/template/contract/contract_labor_simple.docx
new file mode 100644
index 0000000..dec68c1
--- /dev/null
+++ b/yudao-module-hrm/src/main/resources/template/contract/contract_labor_simple.docx
Binary files differ
diff --git a/yudao-module-hrm/src/main/resources/template/contract/contract_labor_standard.docx b/yudao-module-hrm/src/main/resources/template/contract/contract_labor_standard.docx
new file mode 100644
index 0000000..3dc11f4
--- /dev/null
+++ b/yudao-module-hrm/src/main/resources/template/contract/contract_labor_standard.docx
Binary files differ
diff --git a/yudao-module-hrm/src/main/resources/template/contract/contract_service.docx b/yudao-module-hrm/src/main/resources/template/contract/contract_service.docx
new file mode 100644
index 0000000..1973d91
--- /dev/null
+++ b/yudao-module-hrm/src/main/resources/template/contract/contract_service.docx
Binary files differ
diff --git a/yudao-module-hrm/src/main/resources/template/contract/original/labor_simple.docx b/yudao-module-hrm/src/main/resources/template/contract/original/labor_simple.docx
new file mode 100644
index 0000000..b74e9f4
--- /dev/null
+++ b/yudao-module-hrm/src/main/resources/template/contract/original/labor_simple.docx
Binary files differ
diff --git a/yudao-module-hrm/src/main/resources/template/contract/original/labor_standard.docx b/yudao-module-hrm/src/main/resources/template/contract/original/labor_standard.docx
new file mode 100644
index 0000000..3ac5a24
--- /dev/null
+++ b/yudao-module-hrm/src/main/resources/template/contract/original/labor_standard.docx
Binary files differ
diff --git a/yudao-module-hrm/src/main/resources/template/contract/original/service.doc b/yudao-module-hrm/src/main/resources/template/contract/original/service.doc
new file mode 100644
index 0000000..b1e1073
--- /dev/null
+++ b/yudao-module-hrm/src/main/resources/template/contract/original/service.doc
Binary files differ

--
Gitblit v1.9.3