2 コミット 2d99385ff9 ... 4a6a6728e4

作者 SHA1 メッセージ 日付
  weizhong 4a6a6728e4 Merge branch 'master' of http://47.99.164.66:3000/hzdyjs-xg/all-project 1 年間 前
  weizhong ae3caa26b8 “rs模块新增登记、党籍v0.1版” 1 年間 前
29 ファイル変更、3116 行追加、230 行削除
  1. 7 0
      backend/ruoyi-modules/ruoyi-rs/pom.xml
  2. 269 1
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java
  3. 61 1
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java
  4. 0 6
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsJob.java
  5. 197 2
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java
  6. 18 0
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasRecord.java
  7. 0 5
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java
  8. 0 1
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingRecord.java
  9. 158 7
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java
  10. 162 0
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java
  11. 88 0
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasRecordVo.java
  12. 48 0
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java
  13. 299 6
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java
  14. 21 1
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasRecordServiceImpl.java
  15. 1 2
      backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsJobMapper.xml
  16. 289 5
      backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml
  17. 195 2
      backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasRecordMapper.xml
  18. 0 19
      docs/原型文件/人事管理/recruitment-prototype.html
  19. 7 0
      docs/开发文档/API/API文档总览.md
  20. 2 6
      docs/开发文档/API/人事招聘模块API.md
  21. 87 5
      docs/开发文档/API/出国申请管理API.md
  22. 1 1
      docs/开发文档/招聘门户系统最终完成报告.md
  23. 1 1
      docs/需求文档/人事管理/培训管理.md
  24. 3 3
      docs/需求文档/人事管理/招聘页面需求.md
  25. 254 11
      web/src/api/rs/overseas.ts
  26. 0 6
      web/src/views/rs/index.vue
  27. 55 24
      web/src/views/rs/overseas/approval.vue
  28. 831 97
      web/src/views/rs/overseas/index.vue
  29. 62 18
      web/src/views/rs/training/index.vue

+ 7 - 0
backend/ruoyi-modules/ruoyi-rs/pom.xml

@@ -116,6 +116,13 @@
             <artifactId>itextpdf</artifactId>
             <version>5.5.13.3</version>
         </dependency>
+        
+        <!-- iText亚洲字体支持包 - 支持中文 -->
+        <dependency>
+            <groupId>com.itextpdf</groupId>
+            <artifactId>itext-asian</artifactId>
+            <version>5.2.0</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 269 - 1
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java

@@ -27,14 +27,23 @@ import org.dromara.common.excel.utils.ExcelUtil;
 import org.dromara.common.satoken.utils.LoginHelper;
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import org.dromara.rs.domain.bo.RsOverseasApplicationBo;
+import org.dromara.rs.domain.bo.GenerateRegistrationFormBo;
 import org.dromara.rs.domain.vo.RsOverseasApplicationVo;
 import org.dromara.rs.domain.vo.RsOverseasApprovalVo;
+import org.dromara.rs.domain.vo.RegistrationFormVO;
 import org.dromara.rs.domain.query.RsOverseasApplicationQuery;
 import org.dromara.rs.service.IRsOverseasApplicationService;
 import org.dromara.rs.service.IRsOverseasApprovalService;
+import org.dromara.rs.service.IRegistrationFormService;
 import java.util.Date;
 import java.util.Map;
 import java.util.HashMap;
+import org.springframework.web.multipart.MultipartFile;
+import org.dromara.common.oss.core.OssClient;
+import org.dromara.common.oss.factory.OssFactory;
+import org.dromara.common.oss.entity.UploadResult;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
 
 /**
  * 出国申请Controller
@@ -53,6 +62,9 @@ public class RsOverseasApplicationController extends BaseController {
     @Autowired
     private IRsOverseasApprovalService overseasApprovalService;
 
+    @Autowired
+    private IRegistrationFormService registrationFormService;
+
     /**
      * 查询出国申请列表
      */
@@ -139,11 +151,27 @@ public class RsOverseasApplicationController extends BaseController {
         ExcelUtil.exportExcel(list, "出国申请数据", RsOverseasApplicationVo.class, response);
     }
 
+    /**
+     * 下载登记备案表模板
+     */
+    @GetMapping("/downloadRegistrationTemplate")
+    public void downloadRegistrationTemplate(HttpServletResponse response) {
+        registrationFormService.downloadRegistrationTemplate(response);
+    }
+
+    /**
+     * 下载保留党籍申请模板
+     */
+    @GetMapping("/downloadPartyMembershipTemplate")
+    public void downloadPartyMembershipTemplate(HttpServletResponse response) {
+        registrationFormService.downloadPartyMembershipTemplate(response);
+    }
+
     /**
      * 获取出国申请详细信息
      */
     @SaCheckPermission("rs:overseas:query")
-    @GetMapping("/{applicationId}")
+    @GetMapping("/detail/{applicationId}")
     public R<RsOverseasApplicationVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long applicationId) {
         return R.ok(applicationService.queryById(applicationId));
     }
@@ -155,6 +183,14 @@ public class RsOverseasApplicationController extends BaseController {
     @Log(title = "出国申请", businessType = BusinessType.INSERT)
     @PostMapping
     public R<Void> add(@Validated(AddGroup.class) @RequestBody RsOverseasApplicationBo bo) {
+        // 🔍 调试日志:检查接收到的关键字段
+        System.out.println("========== Controller 接收到的数据 ==========");
+        System.out.println("emergencyContact: " + bo.getEmergencyContact());
+        System.out.println("destination: " + bo.getDestination());
+        System.out.println("idNumber: " + bo.getIdNumber());
+        System.out.println("unitContact: " + bo.getUnitContact());
+        System.out.println("===============================================");
+        
         // 自动设置申请人信息
         bo.setApplicantId(LoginHelper.getUserId());
         bo.setApplicantName(LoginHelper.getUsername());
@@ -192,6 +228,48 @@ public class RsOverseasApplicationController extends BaseController {
     @Log(title = "提交出国申请", businessType = BusinessType.UPDATE)
     @PutMapping("/submit/{applicationId}")
     public R<Void> submit(@NotNull(message = "申请ID不能为空") @PathVariable Long applicationId) {
+        // 获取申请详情进行校验
+        RsOverseasApplicationVo application = applicationService.queryById(applicationId);
+        if (application == null) {
+            return R.fail("申请不存在");
+        }
+        
+        // 1. 校验出国登记前置条件(近3个月是否有已通过的出国登记)
+        java.util.Map<String, Object> registrationValidation = 
+            applicationService.validateRegistrationPrerequisite(application.getApplicantId());
+        Boolean registrationValid = (Boolean) registrationValidation.get("isValid");
+        if (!registrationValid) {
+            String message = (String) registrationValidation.get("message");
+            return R.fail(message);
+        }
+        
+        // 2. 校验党籍保留前置条件(中共党员且出国天数>30天)
+        java.util.Map<String, Object> retentionValidation = 
+            applicationService.validatePartyMembershipRetentionPrerequisite(
+                application.getApplicantId(), 
+                application.getPoliticalStatus(), 
+                application.getDuration()
+            );
+        Boolean retentionValid = (Boolean) retentionValidation.get("isValid");
+        if (!retentionValid) {
+            String message = (String) retentionValidation.get("message");
+            return R.fail(message);
+        }
+        
+        // 3. 校验是否上传了盖章文件
+        if (application.getStampedFileUploadStatus() == null || 
+            !"1".equals(application.getStampedFileUploadStatus())) {
+            return R.fail("请先下载登记备案附件填写盖章后上传,才能提交申请!");
+        }
+        
+        // 4. 如果需要党籍保留且已通过,检查是否上传了保留党籍附件
+        Boolean needRetention = (Boolean) retentionValidation.get("needRetention");
+        if (needRetention && retentionValid) {
+            // TODO: 添加保留党籍附件上传校验(需要在数据库和实体类中添加字段)
+            // 当前版本暂不强制校验保留党籍附件
+        }
+        
+        // 所有校验通过,执行提交操作
         return toAjax(applicationService.submitApplication(applicationId));
     }
 
@@ -206,6 +284,35 @@ public class RsOverseasApplicationController extends BaseController {
         return toAjax(applicationService.resubmitApplication(applicationId, bo));
     }
 
+    /**
+     * 校验出国申请前置条件
+     * 用于前端在填写表单时实时提示用户
+     */
+    @SaCheckPermission("rs:overseas:query")
+    @GetMapping("/validatePrerequisites")
+    public R<java.util.Map<String, Object>> validatePrerequisites(
+            @RequestParam(required = false) String politicalStatus,
+            @RequestParam(required = false) Integer duration) {
+        
+        Long applicantId = LoginHelper.getUserId();
+        java.util.Map<String, Object> result = new java.util.HashMap<>();
+        
+        // 1. 校验出国登记前置条件
+        java.util.Map<String, Object> registrationValidation = 
+            applicationService.validateRegistrationPrerequisite(applicantId);
+        result.put("registrationValidation", registrationValidation);
+        
+        // 2. 校验党籍保留前置条件(如果提供了政治面貌和出国天数)
+        if (politicalStatus != null && duration != null) {
+            java.util.Map<String, Object> retentionValidation = 
+                applicationService.validatePartyMembershipRetentionPrerequisite(
+                    applicantId, politicalStatus, duration);
+            result.put("retentionValidation", retentionValidation);
+        }
+        
+        return R.ok(result);
+    }
+
     /**
      * 取消出国申请
      */
@@ -298,4 +405,165 @@ public class RsOverseasApplicationController extends BaseController {
             this.approvalOpinion = approvalOpinion;
         }
     }
+
+    // ==================== PDF生成相关接口 ====================
+
+    /**
+     * 生成出国申请登记表PDF
+     */
+    @SaCheckPermission("rs:overseas:generate")
+    @PostMapping("/generateRegistrationForm")
+    @Log(title = "生成出国申请登记表", businessType = BusinessType.OTHER)
+    public R<RegistrationFormVO> generateRegistrationForm(@RequestBody @Validated GenerateRegistrationFormBo bo) {
+        RegistrationFormVO result = registrationFormService.generateRegistrationFormPdf(bo);
+        return R.ok(result);
+    }
+
+    /**
+     * 下载生成的PDF文档
+     */
+    @SaCheckPermission("rs:overseas:download")
+    @GetMapping("/downloadPdf/{documentId}")
+    public void downloadPdf(@PathVariable String documentId, HttpServletResponse response) {
+        registrationFormService.downloadPdf(documentId, response);
+    }
+
+    /**
+     * 预览生成的PDF文档
+     */
+    @SaCheckPermission("rs:overseas:preview")
+    @GetMapping("/previewPdf/{documentId}")
+    public void previewPdf(@PathVariable String documentId, HttpServletResponse response) {
+        registrationFormService.downloadPdf(documentId, response);
+    }
+
+    // ==================== 盖章文件上传接口 ====================
+
+    /**
+     * 上传登记备案表盖章文件
+     */
+    @SaCheckPermission("rs:overseas:add")
+    @PostMapping("/uploadStampedFile")
+    @Log(title = "上传盖章文件", businessType = BusinessType.OTHER)
+    public R<Map<String, Object>> uploadStampedFile(@RequestParam("file") MultipartFile file,
+                                                     @RequestParam("applicationId") Long applicationId) {
+        try {
+            // 验证文件格式
+            String contentType = file.getContentType();
+            if (!isValidFileType(contentType)) {
+                return R.fail("不支持的文件格式,请上传PDF、JPG或PNG文件");
+            }
+
+            // 验证文件大小(10MB)
+            long maxSize = 10 * 1024 * 1024; // 10MB
+            if (file.getSize() > maxSize) {
+                return R.fail("文件大小超过10MB限制");
+            }
+
+            // 生成文件名
+            String originalFilename = file.getOriginalFilename();
+            String fileName = "stamped_" + applicationId + "_" + System.currentTimeMillis() 
+                            + getFileExtension(originalFilename);
+
+            // 上传到OSS
+            OssClient ossClient = OssFactory.instance();
+            String filePath = "overseas/stamped/" + fileName;
+            UploadResult uploadResult = ossClient.upload(file.getInputStream(), filePath, 
+                                                       file.getSize(), file.getContentType());
+
+            // 更新申请记录 - 只更新盖章文件相关字段
+            applicationService.updateStampedFileInfo(applicationId, uploadResult.getUrl(), 
+                originalFilename, new Date(), "1");
+
+            // 返回结果
+            Map<String, Object> result = new HashMap<>();
+            result.put("filePath", uploadResult.getUrl());
+            result.put("fileName", originalFilename);
+            result.put("uploadTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
+
+            return R.ok("盖章文件上传成功", result);
+
+        } catch (IOException e) {
+            e.printStackTrace();
+            return R.fail("文件上传失败:" + e.getMessage());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return R.fail("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 上传党籍保留附件
+     */
+    @PostMapping("/uploadPartyRetentionFile")
+    @Log(title = "上传党籍保留附件", businessType = BusinessType.OTHER)
+    public R<Map<String, Object>> uploadPartyRetentionFile(@RequestParam("file") MultipartFile file,
+                                                            @RequestParam("applicationId") Long applicationId) {
+        try {
+            // 验证文件格式
+            String contentType = file.getContentType();
+            if (!isValidFileType(contentType)) {
+                return R.fail("不支持的文件格式,请上传PDF、JPG或PNG文件");
+            }
+
+            // 验证文件大小(10MB)
+            long maxSize = 10 * 1024 * 1024; // 10MB
+            if (file.getSize() > maxSize) {
+                return R.fail("文件大小超过10MB限制");
+            }
+
+            // 生成文件名
+            String originalFilename = file.getOriginalFilename();
+            String fileName = "party_retention_" + applicationId + "_" + System.currentTimeMillis() 
+                            + getFileExtension(originalFilename);
+
+            // 上传到OSS
+            OssClient ossClient = OssFactory.instance();
+            String filePath = "overseas/party-retention/" + fileName;
+            UploadResult uploadResult = ossClient.upload(file.getInputStream(), filePath, 
+                                                       file.getSize(), file.getContentType());
+
+            // 更新申请记录 - 只更新党籍保留附件相关字段
+            applicationService.updatePartyRetentionFileInfo(applicationId, uploadResult.getUrl(), 
+                originalFilename, new Date(), "1");
+
+            // 返回结果
+            Map<String, Object> result = new HashMap<>();
+            result.put("filePath", uploadResult.getUrl());
+            result.put("fileName", originalFilename);
+            result.put("uploadTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
+
+            return R.ok("党籍保留附件上传成功", result);
+
+        } catch (IOException e) {
+            e.printStackTrace();
+            return R.fail("文件上传失败:" + e.getMessage());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return R.fail("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 验证文件类型
+     */
+    private boolean isValidFileType(String contentType) {
+        if (contentType == null) return false;
+        return contentType.equals("application/pdf") ||
+               contentType.equals("image/jpeg") ||
+               contentType.equals("image/jpg") ||
+               contentType.equals("image/png");
+    }
+
+    /**
+     * 获取文件扩展名
+     */
+    private String getFileExtension(String filename) {
+        if (filename == null || !filename.contains(".")) {
+            return ".pdf";
+        }
+        return filename.substring(filename.lastIndexOf("."));
+    }
+
+
 } 

+ 61 - 1
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java

@@ -30,6 +30,11 @@ import org.dromara.rs.domain.bo.RsOverseasRecordBo;
 import org.dromara.rs.domain.vo.RsOverseasRecordVo;
 import org.dromara.rs.domain.query.RsOverseasRecordQuery;
 import org.dromara.rs.service.IRsOverseasRecordService;
+import org.dromara.rs.util.OverseasReceiptPdfUtil;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 
 /**
  * 出国记录Controller
@@ -61,8 +66,16 @@ public class RsOverseasRecordController extends BaseController {
     @GetMapping("/my/list")
     public TableDataInfo<RsOverseasRecordVo> myList(RsOverseasRecordQuery query) {
         Long userId = LoginHelper.getUserId();
+        System.out.println("========== 出国记录Controller: 接收到查询请求 ==========");
+        System.out.println("当前用户ID: " + userId);
+        System.out.println("原始查询参数: " + query);
         query.setApplicantId(userId);
-        return recordService.queryMyRecordPageList(query);
+        System.out.println("设置申请人ID后: " + query);
+        
+        TableDataInfo<RsOverseasRecordVo> result = recordService.queryMyRecordPageList(query);
+        System.out.println("Controller返回结果: total=" + result.getTotal() + ", rows=" + (result.getRows() != null ? result.getRows().size() : 0));
+        System.out.println("========== 出国记录Controller: 查询完成 ==========");
+        return result;
     }
 
     /**
@@ -147,6 +160,53 @@ public class RsOverseasRecordController extends BaseController {
         return toAjax(recordService.deleteWithValidByIds(recordIds, true));
     }
 
+    /**
+     * 下载因私出国(境)情况回执PDF
+     */
+    @SaCheckPermission("rs:overseas:query")
+    @Log(title = "下载出国情况回执", businessType = BusinessType.EXPORT)
+    @GetMapping("/downloadReceipt/{recordId}")
+    public void downloadReceipt(@NotNull(message = "记录ID不能为空") @PathVariable Long recordId, 
+                                 HttpServletResponse response) {
+        try {
+            // 查询出国记录详情
+            RsOverseasRecordVo record = recordService.queryById(recordId);
+            if (record == null) {
+                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                response.setContentType("application/json;charset=UTF-8");
+                response.getWriter().write("{\"code\":404,\"msg\":\"出国记录不存在\"}");
+                return;
+            }
+            
+            // 生成PDF
+            byte[] pdfBytes = OverseasReceiptPdfUtil.generateReceiptPdf(record);
+            
+            // 设置响应头
+            String fileName = String.format("因私出国情况回执_%s_%s.pdf", 
+                record.getApplicantName(), 
+                new java.text.SimpleDateFormat("yyyyMMdd").format(new java.util.Date()));
+            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
+            
+            response.setContentType("application/pdf");
+            response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);
+            response.setContentLength(pdfBytes.length);
+            
+            // 输出PDF
+            response.getOutputStream().write(pdfBytes);
+            response.getOutputStream().flush();
+            
+        } catch (Exception e) {
+            try {
+                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                response.setContentType("application/json;charset=UTF-8");
+                response.getWriter().write("{\"code\":500,\"msg\":\"PDF生成失败: " + e.getMessage() + "\"}");
+            } catch (IOException ioException) {
+                // 记录日志
+                System.err.println("PDF生成失败: " + e.getMessage());
+            }
+        }
+    }
+
     /**
      * 初始化测试数据(临时接口,用于解决出国记录列表显示问题)
      */

+ 0 - 6
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsJob.java

@@ -44,12 +44,6 @@ public class RsJob extends BaseEntity {
     @ExcelProperty(value = "部门")
     private String department;
 
-    /**
-     * 薪资范围
-     */
-    @ExcelProperty(value = "薪资范围")
-    private String salaryRange;
-
     /**
      * 工作地点
      */

+ 197 - 2
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasApplication.java

@@ -35,6 +35,12 @@ public class RsOverseasApplication extends BaseEntity {
     @ExcelProperty(value = "申请ID")
     private Long applicationId;
 
+    /**
+     * 编号(自动生成的登记编号)
+     */
+    @ExcelProperty(value = "编号")
+    private String serialNumber;
+
     /**
      * 申请人ID
      */
@@ -48,13 +54,140 @@ public class RsOverseasApplication extends BaseEntity {
     private String applicantName;
 
     /**
-     * 所属部门
+     * 性别:0-男,1-女
+     */
+    @ExcelProperty(value = "性别")
+    private String gender;
+
+    /**
+     * 出生日期
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @ExcelProperty(value = "出生日期")
+    private Date birthDate;
+
+    /**
+     * 照片地址(2寸免冠白色或淡蓝色底彩照)
+     */
+    @ExcelProperty(value = "照片")
+    private String photo;
+
+    /**
+     * 公民身份号码(18位身份证号)
+     */
+    @ExcelProperty(value = "公民身份号码")
+    private String idNumber;
+
+    /**
+     * 政治面貌
+     */
+    @ExcelProperty(value = "政治面貌")
+    private String politicalStatus;
+
+    /**
+     * 户籍所在地(填写至县市区)
+     */
+    @ExcelProperty(value = "户籍所在地")
+    private String registeredAddress;
+
+    /**
+     * 所属部门(工作部门/教学系部)
      */
     @ExcelProperty(value = "所属部门")
     private String department;
 
     /**
-     * 联系电话
+     * 职务职称
+     */
+    @ExcelProperty(value = "职务职称")
+    private String position;
+
+    /**
+     * 职称
+     */
+    @ExcelProperty(value = "职称")
+    private String professionalTitle;
+
+    /**
+     * 行政职级
+     */
+    @ExcelProperty(value = "行政职级")
+    private String administrativeLevel;
+
+    /**
+     * 人员类别代码(根据报备人员范围中的情形项填写)
+     */
+    @ExcelProperty(value = "人员类别代码")
+    private String personnelCategory;
+
+    /**
+     * 登记备案责任单位
+     */
+    @ExcelProperty(value = "登记备案责任单位")
+    private String registrationUnit;
+
+    /**
+     * 登记备案责任单位负责人签名
+     */
+    @ExcelProperty(value = "登记备案责任单位负责人签名")
+    private String unitLeaderSignature;
+
+    /**
+     * 登记备案责任单位联系人
+     */
+    @ExcelProperty(value = "登记备案责任单位联系人")
+    private String unitContact;
+
+    /**
+     * 办公电话
+     */
+    @ExcelProperty(value = "办公电话")
+    private String officePhone;
+
+    /**
+     * 申领证件名称
+     */
+    @ExcelProperty(value = "申领证件名称")
+    private String documentName;
+
+    /**
+     * 证件号码
+     */
+    @ExcelProperty(value = "证件号码")
+    private String documentNumber;
+
+    /**
+     * 前往国家
+     */
+    @ExcelProperty(value = "前往国家")
+    private String destinationCountry;
+
+    /**
+     * 出境事由
+     */
+    @ExcelProperty(value = "出境事由")
+    private String exitReason;
+
+    /**
+     * 探望对象姓名及与申请者关系
+     */
+    @ExcelProperty(value = "探望对象关系")
+    private String visitRelationship;
+
+    /**
+     * 备注
+     */
+    @ExcelProperty(value = "备注")
+    private String remarks;
+
+    /**
+     * 移动电话
+     */
+    @ExcelProperty(value = "移动电话")
+    private String mobilePhone;
+
+    /**
+     * 联系电话(保留原字段兼容性)
      */
     @ExcelProperty(value = "联系电话")
     private String contactPhone;
@@ -177,6 +310,68 @@ public class RsOverseasApplication extends BaseEntity {
     @ExcelProperty(value = "申请人备注")
     private String applicantRemark;
 
+    /**
+     * 应急联系人及电话
+     */
+    @ExcelProperty(value = "应急联系人及电话")
+    private String emergencyContact;
+
+    /**
+     * 手写签字(base64格式)
+     */
+    @ExcelProperty(value = "手写签字")
+    private String signature;
+
+    /**
+     * 登记备案表盖章文件路径
+     */
+    @ExcelProperty(value = "登记备案表盖章文件路径")
+    private String stampedRegistrationFilePath;
+
+    /**
+     * 登记备案表盖章文件名
+     */
+    @ExcelProperty(value = "登记备案表盖章文件名")
+    private String stampedRegistrationFileName;
+
+    /**
+     * 盖章文件上传时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "盖章文件上传时间")
+    private Date stampedFileUploadTime;
+
+    /**
+     * 盖章文件上传状态:0-未上传,1-已上传
+     */
+    @ExcelProperty(value = "盖章文件上传状态")
+    private String stampedFileUploadStatus;
+
+    /**
+     * 党籍保留附件文件路径
+     */
+    @ExcelProperty(value = "党籍保留附件文件路径")
+    private String partyRetentionFilePath;
+
+    /**
+     * 党籍保留附件文件名
+     */
+    @ExcelProperty(value = "党籍保留附件文件名")
+    private String partyRetentionFileName;
+
+    /**
+     * 党籍保留附件上传时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "党籍保留附件上传时间")
+    private Date partyRetentionUploadTime;
+
+    /**
+     * 党籍保留附件上传状态:0-未上传,1-已上传
+     */
+    @ExcelProperty(value = "党籍保留附件上传状态")
+    private String partyRetentionUploadStatus;
+
     /**
      * 基础字段:创建者
      */

+ 18 - 0
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsOverseasRecord.java

@@ -59,12 +59,24 @@ public class RsOverseasRecord extends BaseEntity {
     @ExcelProperty(value = "所属部门")
     private String department;
 
+    /**
+     * 职务
+     */
+    @ExcelProperty(value = "职务")
+    private String position;
+
     /**
      * 联系电话
      */
     @ExcelProperty(value = "联系电话")
     private String contactPhone;
 
+    /**
+     * 联系邮箱
+     */
+    @ExcelProperty(value = "联系邮箱")
+    private String contactEmail;
+
     /**
      * 出国目的
      */
@@ -115,6 +127,12 @@ public class RsOverseasRecord extends BaseEntity {
     @ExcelProperty(value = "实际花费金额")
     private BigDecimal actualBudget;
 
+    /**
+     * 经费来源
+     */
+    @ExcelProperty(value = "经费来源")
+    private String budgetSource;
+
     /**
      * 邀请方信息
      */

+ 0 - 5
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java

@@ -155,11 +155,6 @@ public class RsTrainingApplication extends BaseEntity {
      */
     private String tenantId;
     
-    /**
-     * 创建部门
-     */
-    private Long createDept;
-    
     /**
      * 拒绝时间
      */

+ 0 - 1
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingRecord.java

@@ -116,7 +116,6 @@ public class RsTrainingRecord extends BaseEntity {
     private String feedback;
 
     private String tenantId;
-    private Long createDept;
     
     /**
      * 部门ID

+ 158 - 7
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/bo/RsOverseasApplicationBo.java

@@ -9,6 +9,7 @@ import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.DecimalMin;
 import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Pattern;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import com.fasterxml.jackson.annotation.JsonFormat;
@@ -35,6 +36,11 @@ public class RsOverseasApplicationBo extends BaseEntity {
     @NotNull(message = "申请ID不能为空", groups = { EditGroup.class })
     private Long applicationId;
 
+    /**
+     * 编号(自动生成的登记编号)
+     */
+    private String serialNumber;
+
     /**
      * 申请人ID
      */
@@ -44,28 +50,125 @@ public class RsOverseasApplicationBo extends BaseEntity {
     /**
      * 申请人姓名
      */
+    @NotBlank(message = "申请人姓名不能为空", groups = { AddGroup.class, EditGroup.class })
     private String applicantName;
 
+    /**
+     * 性别:0-男,1-女
+     */
+    @NotBlank(message = "性别不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String gender;
+
+    /**
+     * 出生日期
+     */
+    @NotNull(message = "出生日期不能为空", groups = { AddGroup.class, EditGroup.class })
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date birthDate;
+
+    /**
+     * 照片地址
+     */
+    private String photo;
+
+    /**
+     * 公民身份号码(18位身份证号)
+     * 注意:此字段为可选字段,不进行格式校验(因为可能使用护照等其他证件)
+     */
+    private String idNumber;
+
+    /**
+     * 政治面貌
+     */
+    @NotBlank(message = "政治面貌不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String politicalStatus;
+
+    /**
+     * 户籍所在地(填写至县市区)
+     */
+    @NotBlank(message = "户籍所在地不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String registeredAddress;
+
     /**
      * 所属部门
      */
+    @NotBlank(message = "所属部门不能为空", groups = { AddGroup.class, EditGroup.class })
     private String department;
 
     /**
-     * 联系电话
+     * 职务职称
+     */
+    @NotBlank(message = "职务职称不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String position;
+
+    /**
+     * 职称
+     */
+    private String professionalTitle;
+
+    /**
+     * 行政职级
+     */
+    private String administrativeLevel;
+
+    /**
+     * 人员类别代码(根据报备人员范围中的情形项填写)
+     */
+    @NotBlank(message = "人员类别代码不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String personnelCategory;
+
+    /**
+     * 登记备案责任单位
+     */
+    private String registrationUnit;
+
+    /**
+     * 登记备案责任单位负责人签名
+     */
+    private String unitLeaderSignature;
+
+    /**
+     * 登记备案责任单位联系人
+     */
+    @NotBlank(message = "登记备案责任单位联系人不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String unitContact;
+
+    /**
+     * 办公电话
+     */
+    @Pattern(regexp = "^(\\d{3,4}-)?\\d{7,8}$", message = "办公电话格式不正确", groups = { AddGroup.class, EditGroup.class })
+    private String officePhone;
+
+    /**
+     * 申领证件名称
+     */
+    private String documentName;
+
+    /**
+     * 证件号码
+     */
+    private String documentNumber;
+
+    /**
+     * 移动电话
+     */
+    @NotBlank(message = "移动电话不能为空", groups = { AddGroup.class, EditGroup.class })
+    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "移动电话格式不正确", groups = { AddGroup.class, EditGroup.class })
+    private String mobilePhone;
+
+    /**
+     * 联系电话(保留兼容性)
      */
     private String contactPhone;
 
     /**
      * 出国目的
      */
-    @NotBlank(message = "出国目的不能为空", groups = { AddGroup.class, EditGroup.class })
     private String purpose;
 
     /**
      * 目的详细说明
      */
-    @NotBlank(message = "目的详细说明不能为空", groups = { AddGroup.class, EditGroup.class })
     private String purposeDetail;
 
     /**
@@ -74,6 +177,21 @@ public class RsOverseasApplicationBo extends BaseEntity {
     @NotBlank(message = "目的地国家不能为空", groups = { AddGroup.class, EditGroup.class })
     private String destination;
 
+    /**
+     * 前往国家
+     */
+    private String destinationCountry;
+
+    /**
+     * 出境事由
+     */
+    private String exitReason;
+
+    /**
+     * 探望对象姓名及与申请者关系
+     */
+    private String visitRelationship;
+
     /**
      * 开始日期
      */
@@ -97,8 +215,6 @@ public class RsOverseasApplicationBo extends BaseEntity {
     /**
      * 预算金额
      */
-    @NotNull(message = "预算金额不能为空", groups = { AddGroup.class, EditGroup.class })
-    @DecimalMin(value = "0.01", message = "预算金额必须大于0", groups = { AddGroup.class, EditGroup.class })
     private BigDecimal budget;
 
     /**
@@ -119,8 +235,6 @@ public class RsOverseasApplicationBo extends BaseEntity {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date approvalTime;
 
-
-
     /**
      * 目的地
      */
@@ -146,6 +260,22 @@ public class RsOverseasApplicationBo extends BaseEntity {
      */
     private String applicantRemark;
 
+    /**
+     * 应急联系人及电话
+     */
+    @NotBlank(message = "应急联系人及电话不能为空")
+    private String emergencyContact;
+
+    /**
+     * 手写签字(base64格式)
+     */
+    private String signature;
+
+    /**
+     * 备注
+     */
+    private String remarks;
+
     /**
      * 管理员备注
      */
@@ -165,4 +295,25 @@ public class RsOverseasApplicationBo extends BaseEntity {
      * 申请状态
      */
     private String status;
+
+    /**
+     * 登记备案表盖章文件路径
+     */
+    private String stampedRegistrationFilePath;
+
+    /**
+     * 登记备案表盖章文件名
+     */
+    private String stampedRegistrationFileName;
+
+    /**
+     * 盖章文件上传时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date stampedFileUploadTime;
+
+    /**
+     * 盖章文件上传状态:0-未上传,1-已上传
+     */
+    private String stampedFileUploadStatus;
 } 

+ 162 - 0
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasApplicationVo.java

@@ -28,6 +28,11 @@ public class RsOverseasApplicationVo implements Serializable {
      */
     private Long applicationId;
 
+    /**
+     * 编号(自动生成的登记编号)
+     */
+    private String serialNumber;
+
     /**
      * 申请人ID
      */
@@ -38,11 +43,117 @@ public class RsOverseasApplicationVo implements Serializable {
      */
     private String applicantName;
 
+    /**
+     * 性别
+     */
+    private String gender;
+
+    /**
+     * 性别文本
+     */
+    private String genderText;
+
+    /**
+     * 出生日期
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date birthDate;
+
+    /**
+     * 照片地址
+     */
+    private String photo;
+
+    /**
+     * 公民身份号码
+     */
+    private String idNumber;
+
+    /**
+     * 政治面貌
+     */
+    private String politicalStatus;
+
+    /**
+     * 政治面貌文本
+     */
+    private String politicalStatusText;
+
+    /**
+     * 户籍所在地
+     */
+    private String registeredAddress;
+
     /**
      * 所属部门
      */
     private String department;
 
+    /**
+     * 职务职称
+     */
+    private String position;
+
+    /**
+     * 职称
+     */
+    private String professionalTitle;
+
+    /**
+     * 行政职级
+     */
+    private String administrativeLevel;
+
+    /**
+     * 行政职级文本
+     */
+    private String administrativeLevelText;
+
+    /**
+     * 人员类别代码
+     */
+    private String personnelCategory;
+
+    /**
+     * 人员类别代码文本
+     */
+    private String personnelCategoryText;
+
+    /**
+     * 登记备案责任单位
+     */
+    private String registrationUnit;
+
+    /**
+     * 登记备案责任单位负责人签名
+     */
+    private String unitLeaderSignature;
+
+    /**
+     * 登记备案责任单位联系人
+     */
+    private String unitContact;
+
+    /**
+     * 办公电话
+     */
+    private String officePhone;
+
+    /**
+     * 申领证件名称
+     */
+    private String documentName;
+
+    /**
+     * 证件号码
+     */
+    private String documentNumber;
+
+    /**
+     * 移动电话
+     */
+    private String mobilePhone;
+
     /**
      * 联系电话
      */
@@ -68,6 +179,21 @@ public class RsOverseasApplicationVo implements Serializable {
      */
     private String destination;
 
+    /**
+     * 前往国家
+     */
+    private String destinationCountry;
+
+    /**
+     * 出境事由
+     */
+    private String exitReason;
+
+    /**
+     * 探望对象姓名及与申请者关系
+     */
+    private String visitRelationship;
+
     /**
      * 目的地
      */
@@ -157,11 +283,47 @@ public class RsOverseasApplicationVo implements Serializable {
      */
     private String applicantRemark;
 
+    /**
+     * 应急联系人及电话
+     */
+    private String emergencyContact;
+
+    /**
+     * 手写签字(base64格式)
+     */
+    private String signature;
+
+    /**
+     * 备注
+     */
+    private String remarks;
+
     /**
      * 管理员备注
      */
     private String adminRemark;
 
+    /**
+     * 登记备案表盖章文件路径
+     */
+    private String stampedRegistrationFilePath;
+
+    /**
+     * 登记备案表盖章文件名
+     */
+    private String stampedRegistrationFileName;
+
+    /**
+     * 盖章文件上传时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date stampedFileUploadTime;
+
+    /**
+     * 盖章文件上传状态:0-未上传,1-已上传
+     */
+    private String stampedFileUploadStatus;
+
     /**
      * 租户编号
      */

+ 88 - 0
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/vo/RsOverseasRecordVo.java

@@ -47,11 +47,89 @@ public class RsOverseasRecordVo implements Serializable {
      * 所属部门
      */
     private String department;
+    
+    /**
+     * 职位
+     */
+    private String position;
 
     /**
      * 联系电话
      */
     private String contactPhone;
+    
+    /**
+     * 联系邮箱
+     */
+    private String contactEmail;
+    
+    // ========== 以下字段来自关联的出国申请表 ==========
+    
+    /**
+     * 性别(来自申请表)
+     */
+    private String gender;
+    
+    /**
+     * 性别文本(来自申请表)
+     */
+    private String genderText;
+    
+    /**
+     * 出生日期(来自申请表)
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date birthDate;
+    
+    /**
+     * 政治面貌(来自申请表)
+     */
+    private String politicalStatus;
+    
+    /**
+     * 政治面貌文本(来自申请表)
+     */
+    private String politicalStatusText;
+    
+    /**
+     * 职称(来自申请表)
+     */
+    private String professionalTitle;
+    
+    /**
+     * 申领证件名称(来自申请表)
+     */
+    private String documentName;
+    
+    /**
+     * 证件号码(来自申请表)
+     */
+    private String documentNumber;
+    
+    /**
+     * 移动电话(来自申请表)
+     */
+    private String mobilePhone;
+    
+    /**
+     * 前往国家(来自申请表)
+     */
+    private String destinationCountry;
+    
+    /**
+     * 出境事由(来自申请表)
+     */
+    private String exitReason;
+    
+    /**
+     * 应急联系人及电话(来自申请表)
+     */
+    private String emergencyContact;
+    
+    /**
+     * 备注(来自申请表)
+     */
+    private String remarks;
 
     /**
      * 出国目的
@@ -120,6 +198,16 @@ public class RsOverseasRecordVo implements Serializable {
      */
     private String transportationPlan;
 
+    /**
+     * 申请状态(来自申请表)
+     */
+    private String status;
+
+    /**
+     * 申请状态文本(来自申请表)
+     */
+    private String statusText;
+
     /**
      * 完成状态
      */

+ 48 - 0
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApplicationService.java

@@ -158,4 +158,52 @@ public interface IRsOverseasApplicationService {
      * @return 是否成功
      */
     Boolean resubmitApplication(Long applicationId, RsOverseasApplicationBo applicationData);
+
+    /**
+     * 更新盖章文件信息
+     *
+     * @param applicationId 申请ID
+     * @param filePath 文件路径
+     * @param fileName 文件名
+     * @param uploadTime 上传时间
+     * @param uploadStatus 上传状态
+     * @return 是否成功
+     */
+    Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName, 
+                                 java.util.Date uploadTime, String uploadStatus);
+
+    /**
+     * 更新党籍保留附件信息
+     *
+     * @param applicationId 申请ID
+     * @param filePath 文件路径
+     * @param fileName 文件名
+     * @param uploadTime 上传时间
+     * @param uploadStatus 上传状态
+     * @return 是否成功
+     */
+    Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName, 
+                                        java.util.Date uploadTime, String uploadStatus);
+
+    /**
+     * 校验出国登记前置条件
+     * 检查近3个月内是否有已通过的出国登记记录
+     *
+     * @param applicantId 申请人ID
+     * @return 校验结果Map,包含 isValid(是否通过校验)和 message(提示信息)
+     */
+    java.util.Map<String, Object> validateRegistrationPrerequisite(Long applicantId);
+
+    /**
+     * 校验党籍保留前置条件
+     * 检查是否为中共党员且出国天数>30天,如果是则需要检查党籍保留申请
+     *
+     * @param applicantId 申请人ID
+     * @param politicalStatus 政治面貌
+     * @param duration 出国天数
+     * @return 校验结果Map,包含 isValid(是否通过校验)、needRetention(是否需要党籍保留)和 message(提示信息)
+     */
+    java.util.Map<String, Object> validatePartyMembershipRetentionPrerequisite(Long applicantId, 
+                                                                               String politicalStatus, 
+                                                                               Integer duration);
 } 

+ 299 - 6
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java

@@ -2,7 +2,10 @@ package org.dromara.rs.service.impl;
 
 import java.util.Arrays;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Calendar;
 
 import org.dromara.common.core.utils.DateUtils;
 import org.dromara.common.core.utils.StringUtils;
@@ -12,12 +15,17 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.dromara.rs.mapper.RsOverseasApplicationMapper;
 import org.dromara.rs.mapper.RsOverseasApprovalMapper;
+import org.dromara.rs.mapper.RsOverseasRegistrationMapper;
+import org.dromara.rs.mapper.RsPartyMembershipRetentionMapper;
 import org.dromara.rs.domain.RsOverseasApplication;
 import org.dromara.rs.domain.RsOverseasApproval;
+import org.dromara.rs.domain.RsOverseasRegistration;
+import org.dromara.rs.domain.RsPartyMembershipRetention;
 import org.dromara.rs.domain.bo.RsOverseasApplicationBo;
 import org.dromara.rs.domain.vo.RsOverseasApplicationVo;
 import org.dromara.rs.domain.query.RsOverseasApplicationQuery;
@@ -47,6 +55,12 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
     @Autowired
     private IRsOverseasRecordService overseasRecordService;
 
+    @Autowired
+    private RsOverseasRegistrationMapper registrationMapper;
+
+    @Autowired
+    private RsPartyMembershipRetentionMapper retentionMapper;
+
     /**
      * 查询出国申请分页列表
      */
@@ -198,7 +212,71 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
             update.setDuration(days);
         }
         validEntityBeforeSave(update);
-        return baseMapper.updateById(update) > 0;
+        
+        // 使用LambdaUpdateWrapper安全更新,只更新非null字段
+        LambdaUpdateWrapper<RsOverseasApplication> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(RsOverseasApplication::getApplicationId, update.getApplicationId());
+        
+        // ==================== 基本信息 ====================
+        if (update.getApplicantId() != null) updateWrapper.set(RsOverseasApplication::getApplicantId, update.getApplicantId());
+        if (update.getApplicantName() != null) updateWrapper.set(RsOverseasApplication::getApplicantName, update.getApplicantName());
+        if (update.getGender() != null) updateWrapper.set(RsOverseasApplication::getGender, update.getGender());
+        if (update.getBirthDate() != null) updateWrapper.set(RsOverseasApplication::getBirthDate, update.getBirthDate());
+        if (update.getPhoto() != null) updateWrapper.set(RsOverseasApplication::getPhoto, update.getPhoto());
+        if (update.getIdNumber() != null) updateWrapper.set(RsOverseasApplication::getIdNumber, update.getIdNumber());
+        if (update.getPoliticalStatus() != null) updateWrapper.set(RsOverseasApplication::getPoliticalStatus, update.getPoliticalStatus());  // 政治面貌 ⬅️ 关键修复
+        if (update.getRegisteredAddress() != null) updateWrapper.set(RsOverseasApplication::getRegisteredAddress, update.getRegisteredAddress());
+        
+        // ==================== 工作信息 ====================
+        if (update.getDepartment() != null) updateWrapper.set(RsOverseasApplication::getDepartment, update.getDepartment());
+        if (update.getPosition() != null) updateWrapper.set(RsOverseasApplication::getPosition, update.getPosition());  // 职务
+        if (update.getProfessionalTitle() != null) updateWrapper.set(RsOverseasApplication::getProfessionalTitle, update.getProfessionalTitle());  // 职称
+        if (update.getAdministrativeLevel() != null) updateWrapper.set(RsOverseasApplication::getAdministrativeLevel, update.getAdministrativeLevel());
+        if (update.getPersonnelCategory() != null) updateWrapper.set(RsOverseasApplication::getPersonnelCategory, update.getPersonnelCategory());
+        
+        // ==================== 单位和联系信息 ====================
+        if (update.getRegistrationUnit() != null) updateWrapper.set(RsOverseasApplication::getRegistrationUnit, update.getRegistrationUnit());
+        if (update.getUnitLeaderSignature() != null) updateWrapper.set(RsOverseasApplication::getUnitLeaderSignature, update.getUnitLeaderSignature());
+        if (update.getUnitContact() != null) updateWrapper.set(RsOverseasApplication::getUnitContact, update.getUnitContact());
+        if (update.getOfficePhone() != null) updateWrapper.set(RsOverseasApplication::getOfficePhone, update.getOfficePhone());
+        if (update.getMobilePhone() != null) updateWrapper.set(RsOverseasApplication::getMobilePhone, update.getMobilePhone());
+        if (update.getContactPhone() != null) updateWrapper.set(RsOverseasApplication::getContactPhone, update.getContactPhone());
+        if (update.getEmergencyContact() != null) updateWrapper.set(RsOverseasApplication::getEmergencyContact, update.getEmergencyContact());  // 应急联系人 ⬅️ 关键修复
+        
+        // ==================== 出国目的和行程 ====================
+        if (update.getPurpose() != null) updateWrapper.set(RsOverseasApplication::getPurpose, update.getPurpose());
+        if (update.getPurposeDetail() != null) updateWrapper.set(RsOverseasApplication::getPurposeDetail, update.getPurposeDetail());
+        if (update.getDestination() != null) updateWrapper.set(RsOverseasApplication::getDestination, update.getDestination());
+        if (update.getDestinationCountry() != null) updateWrapper.set(RsOverseasApplication::getDestinationCountry, update.getDestinationCountry());
+        if (update.getDestinationCity() != null) updateWrapper.set(RsOverseasApplication::getDestinationCity, update.getDestinationCity());
+        if (update.getStartDate() != null) updateWrapper.set(RsOverseasApplication::getStartDate, update.getStartDate());
+        if (update.getEndDate() != null) updateWrapper.set(RsOverseasApplication::getEndDate, update.getEndDate());
+        if (update.getDuration() != null) updateWrapper.set(RsOverseasApplication::getDuration, update.getDuration());
+        if (update.getBudget() != null) updateWrapper.set(RsOverseasApplication::getBudget, update.getBudget());
+        
+        // ==================== 证件和文档 ====================
+        if (update.getInvitation() != null) updateWrapper.set(RsOverseasApplication::getInvitation, update.getInvitation());
+        if (update.getDocumentName() != null) updateWrapper.set(RsOverseasApplication::getDocumentName, update.getDocumentName());
+        if (update.getDocumentNumber() != null) updateWrapper.set(RsOverseasApplication::getDocumentNumber, update.getDocumentNumber());
+        if (update.getItinerary() != null) updateWrapper.set(RsOverseasApplication::getItinerary, update.getItinerary());
+        if (update.getAccommodationPlan() != null) updateWrapper.set(RsOverseasApplication::getAccommodationPlan, update.getAccommodationPlan());
+        if (update.getTransportationPlan() != null) updateWrapper.set(RsOverseasApplication::getTransportationPlan, update.getTransportationPlan());
+        
+        // ==================== 出境事由和备注 ====================
+        if (update.getExitReason() != null) updateWrapper.set(RsOverseasApplication::getExitReason, update.getExitReason());  // 出境事由 ⬅️ 关键字段
+        if (update.getVisitRelationship() != null) updateWrapper.set(RsOverseasApplication::getVisitRelationship, update.getVisitRelationship());
+        if (update.getRemarks() != null) updateWrapper.set(RsOverseasApplication::getRemarks, update.getRemarks());
+        if (update.getApplicantRemark() != null) updateWrapper.set(RsOverseasApplication::getApplicantRemark, update.getApplicantRemark());
+        if (update.getAdminRemark() != null) updateWrapper.set(RsOverseasApplication::getAdminRemark, update.getAdminRemark());  // 管理员备注 ⬅️ 补充遗漏字段
+        if (update.getSignature() != null) updateWrapper.set(RsOverseasApplication::getSignature, update.getSignature());  // 手写签名 ⬅️ 补充遗漏字段
+        
+        // ==================== 盖章文件相关字段 ====================
+        if (update.getStampedRegistrationFilePath() != null) updateWrapper.set(RsOverseasApplication::getStampedRegistrationFilePath, update.getStampedRegistrationFilePath());
+        if (update.getStampedRegistrationFileName() != null) updateWrapper.set(RsOverseasApplication::getStampedRegistrationFileName, update.getStampedRegistrationFileName());
+        if (update.getStampedFileUploadTime() != null) updateWrapper.set(RsOverseasApplication::getStampedFileUploadTime, update.getStampedFileUploadTime());
+        if (update.getStampedFileUploadStatus() != null) updateWrapper.set(RsOverseasApplication::getStampedFileUploadStatus, update.getStampedFileUploadStatus());
+        
+        return baseMapper.update(null, updateWrapper) > 0;
     }
 
     /**
@@ -465,9 +543,7 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
             }
         }
         
-        if (entity.getBudget() != null && entity.getBudget().doubleValue() <= 0) {
-            throw new RuntimeException("预算金额必须大于0");
-        }
+        // 预算金额校验已删除(业务不需要预算金额字段)
     }
 
     /**
@@ -498,23 +574,66 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
     private RsOverseasApplication buildEntityFromBo(RsOverseasApplicationBo bo) {
         RsOverseasApplication entity = new RsOverseasApplication();
         entity.setApplicationId(bo.getApplicationId());
+        
+        // ==================== 基本信息 ====================
         entity.setApplicantId(bo.getApplicantId());
         entity.setApplicantName(bo.getApplicantName());
+        entity.setGender(bo.getGender());  // 性别
+        entity.setBirthDate(bo.getBirthDate());  // 出生日期
+        entity.setPhoto(bo.getPhoto());  // 照片
+        entity.setIdNumber(bo.getIdNumber());  // 公民身份号码
+        entity.setPoliticalStatus(bo.getPoliticalStatus());  // 政治面貌 ⬅️ 关键修复
+        entity.setRegisteredAddress(bo.getRegisteredAddress());  // 户籍所在地
+        
+        // ==================== 工作信息 ====================
         entity.setDepartment(bo.getDepartment());
-        entity.setContactPhone(bo.getContactPhone());
+        entity.setPosition(bo.getPosition());  // 职务
+        entity.setProfessionalTitle(bo.getProfessionalTitle());  // 职称
+        entity.setAdministrativeLevel(bo.getAdministrativeLevel());  // 行政职级
+        entity.setPersonnelCategory(bo.getPersonnelCategory());  // 人员类别代码
+        
+        // ==================== 单位和联系信息 ====================
+        entity.setRegistrationUnit(bo.getRegistrationUnit());  // 登记备案责任单位
+        entity.setUnitLeaderSignature(bo.getUnitLeaderSignature());  // 单位负责人签名
+        entity.setUnitContact(bo.getUnitContact());  // 单位联系人
+        entity.setOfficePhone(bo.getOfficePhone());  // 办公电话
+        entity.setMobilePhone(bo.getMobilePhone());  // 移动电话
+        entity.setContactPhone(bo.getContactPhone());  // 联系电话(兼容字段)
+        entity.setEmergencyContact(bo.getEmergencyContact());  // 应急联系人及电话 ⬅️ 关键修复
+        
+        // ==================== 出国目的和行程 ====================
         entity.setPurpose(bo.getPurpose());
         entity.setPurposeDetail(bo.getPurposeDetail());
         entity.setDestination(bo.getDestination());
+        entity.setDestinationCountry(bo.getDestinationCountry());  // 前往国家
         entity.setDestinationCity(bo.getDestinationCity());
         entity.setStartDate(bo.getStartDate());
         entity.setEndDate(bo.getEndDate());
         entity.setDuration(bo.getDuration());
         entity.setBudget(bo.getBudget());
+        
+        // ==================== 证件和文档 ====================
         entity.setInvitation(bo.getInvitation());
+        entity.setDocumentName(bo.getDocumentName());  // 申领证件名称
+        entity.setDocumentNumber(bo.getDocumentNumber());  // 证件号码
         entity.setItinerary(bo.getItinerary());
         entity.setAccommodationPlan(bo.getAccommodationPlan());
         entity.setTransportationPlan(bo.getTransportationPlan());
-        entity.setApplicantRemark(bo.getApplicantRemark());
+        
+        // ==================== 出境事由和备注 ====================
+        entity.setExitReason(bo.getExitReason());  // 出境事由 ⬅️ 关键字段
+        entity.setVisitRelationship(bo.getVisitRelationship());  // 探望对象关系
+        entity.setRemarks(bo.getRemarks());  // 备注
+        entity.setApplicantRemark(bo.getApplicantRemark());  // 申请人备注
+        entity.setAdminRemark(bo.getAdminRemark());  // 管理员备注 ⬅️ 补充遗漏字段
+        entity.setSignature(bo.getSignature());  // 手写签名 ⬅️ 补充遗漏字段
+        
+        // ==================== 盖章文件相关字段 ====================
+        entity.setStampedRegistrationFilePath(bo.getStampedRegistrationFilePath());
+        entity.setStampedRegistrationFileName(bo.getStampedRegistrationFileName());
+        entity.setStampedFileUploadTime(bo.getStampedFileUploadTime());
+        entity.setStampedFileUploadStatus(bo.getStampedFileUploadStatus());
+        
         return entity;
     }
 
@@ -543,4 +662,178 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
         validatedByIds(ids, isValid);
         return baseMapper.deleteBatchIds(ids) > 0;
     }
+
+    /**
+     * 更新盖章文件信息
+     */
+    @Override
+    @Transactional
+    public Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName, 
+                                       java.util.Date uploadTime, String uploadStatus) {
+        return baseMapper.update(null, 
+            new LambdaUpdateWrapper<RsOverseasApplication>()
+                .eq(RsOverseasApplication::getApplicationId, applicationId)
+                .set(RsOverseasApplication::getStampedRegistrationFilePath, filePath)
+                .set(RsOverseasApplication::getStampedRegistrationFileName, fileName)
+                .set(RsOverseasApplication::getStampedFileUploadTime, uploadTime)
+                .set(RsOverseasApplication::getStampedFileUploadStatus, uploadStatus)
+        ) > 0;
+    }
+
+    /**
+     * 更新党籍保留附件信息
+     */
+    @Override
+    @Transactional
+    public Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName, 
+                                               java.util.Date uploadTime, String uploadStatus) {
+        return baseMapper.update(null, 
+            new LambdaUpdateWrapper<RsOverseasApplication>()
+                .eq(RsOverseasApplication::getApplicationId, applicationId)
+                .set(RsOverseasApplication::getPartyRetentionFilePath, filePath)
+                .set(RsOverseasApplication::getPartyRetentionFileName, fileName)
+                .set(RsOverseasApplication::getPartyRetentionUploadTime, uploadTime)
+                .set(RsOverseasApplication::getPartyRetentionUploadStatus, uploadStatus)
+        ) > 0;
+    }
+
+    /**
+     * 校验出国登记前置条件
+     * 检查当前自然年内是否有已通过的出国登记记录
+     */
+    @Override
+    public Map<String, Object> validateRegistrationPrerequisite(Long applicantId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 计算当前自然年的开始日期(1月1日 00:00:00)
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.MONTH, Calendar.JANUARY);
+        calendar.set(Calendar.DAY_OF_MONTH, 1);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        Date currentYearStart = calendar.getTime();
+        
+        int currentYear = calendar.get(Calendar.YEAR);
+        
+        // 查询当前自然年内的出国登记记录(使用createBy字段)
+        LambdaQueryWrapper<RsOverseasRegistration> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.eq(RsOverseasRegistration::getCreateBy, applicantId)
+                   .ge(RsOverseasRegistration::getCreateTime, currentYearStart)
+                   .orderByDesc(RsOverseasRegistration::getCreateTime)
+                   .last("LIMIT 1");
+        
+        RsOverseasRegistration latestRegistration = registrationMapper.selectOne(queryWrapper);
+        
+        if (latestRegistration == null) {
+            // 当前自然年内没有登记记录
+            result.put("isValid", false);
+            result.put("message", "请先提交出国登记申请,审核通过后再提交出国申请");
+            log.warn("用户{}在{}年内没有出国登记记录", applicantId, currentYear);
+            return result;
+        }
+        
+        String status = latestRegistration.getStatus();
+        if ("2".equals(status)) {
+            // 状态为已通过(审批通过)- 出国登记状态2表示已通过
+            result.put("isValid", true);
+            result.put("message", "出国登记已通过审核");
+            result.put("registrationId", latestRegistration.getRegistrationId());
+            log.info("用户{}的出国登记已通过审核,registrationId: {}", applicantId, latestRegistration.getRegistrationId());
+        } else if ("1".equals(status)) {
+            // 状态为待审核
+            result.put("isValid", false);
+            result.put("message", "出国登记审核中,请审核通过后再提交出国申请");
+            log.warn("用户{}的出国登记还在审核中,status: {}", applicantId, status);
+        } else if ("3".equals(status)) {
+            // 状态为已拒绝
+            result.put("isValid", false);
+            result.put("message", "出国登记已被拒绝,请重新提交出国登记并审核通过后再提交出国申请");
+            log.warn("用户{}的出国登记已被拒绝,status: {}", applicantId, status);
+        } else {
+            // 其他状态(草稿等)
+            result.put("isValid", false);
+            result.put("message", "出国登记状态异常,请先提交出国登记申请");
+            log.warn("用户{}的出国登记状态异常,status: {}", applicantId, status);
+        }
+        
+        return result;
+    }
+
+    /**
+     * 校验党籍保留前置条件
+     * 检查是否为中共党员且出国天数>30天,如果是则需要检查当前自然年内的党籍保留申请
+     */
+    @Override
+    public Map<String, Object> validatePartyMembershipRetentionPrerequisite(Long applicantId, 
+                                                                           String politicalStatus, 
+                                                                           Integer duration) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 判断是否需要党籍保留申请
+        boolean needRetention = "中共党员".equals(politicalStatus) && duration != null && duration > 30;
+        result.put("needRetention", needRetention);
+        
+        if (!needRetention) {
+            // 不需要党籍保留申请
+            result.put("isValid", true);
+            result.put("message", "无需党籍保留申请");
+            return result;
+        }
+        
+        // 需要党籍保留申请,检查当前自然年内是否有党籍保留申请(使用createBy字段)
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.MONTH, Calendar.JANUARY);
+        calendar.set(Calendar.DAY_OF_MONTH, 1);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        Date currentYearStart = calendar.getTime();
+        
+        int currentYear = calendar.get(Calendar.YEAR);
+        
+        LambdaQueryWrapper<RsPartyMembershipRetention> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.eq(RsPartyMembershipRetention::getCreateBy, applicantId)
+                   .ge(RsPartyMembershipRetention::getCreateTime, currentYearStart)
+                   .orderByDesc(RsPartyMembershipRetention::getCreateTime)
+                   .last("LIMIT 1");
+        
+        RsPartyMembershipRetention latestRetention = retentionMapper.selectOne(queryWrapper);
+        
+        if (latestRetention == null) {
+            // 当前自然年内没有党籍保留申请
+            result.put("isValid", false);
+            result.put("message", "您是中共党员且出国天数超过30天,请先提交出国党籍保留申请,审核通过后再提交出国申请");
+            log.warn("用户{}需要党籍保留但在{}年内未提交申请", applicantId, currentYear);
+            return result;
+        }
+        
+        String status = latestRetention.getStatus();
+        if ("2".equals(status)) {
+            // 状态为已通过(审批通过)
+            result.put("isValid", true);
+            result.put("message", "党籍保留申请已通过审核");
+            result.put("retentionId", latestRetention.getRetentionId());
+            log.info("用户{}的党籍保留申请已通过审核,retentionId: {}", applicantId, latestRetention.getRetentionId());
+        } else if ("0".equals(status) || "1".equals(status)) {
+            // 状态为草稿或待审核
+            result.put("isValid", false);
+            result.put("message", "党籍保留申请审核中,请审核通过后再提交出国申请");
+            log.warn("用户{}的党籍保留申请还在审核中,status: {}", applicantId, status);
+        } else if ("3".equals(status)) {
+            // 状态为已拒绝
+            result.put("isValid", false);
+            result.put("message", "党籍保留申请已被拒绝,请重新提交党籍保留申请并审核通过后再提交出国申请");
+            log.warn("用户{}的党籍保留申请已被拒绝,status: {}", applicantId, status);
+        } else {
+            // 其他状态
+            result.put("isValid", false);
+            result.put("message", "党籍保留申请状态异常,请重新提交党籍保留申请");
+            log.warn("用户{}的党籍保留申请状态异常,status: {}", applicantId, status);
+        }
+        
+        return result;
+    }
 } 

+ 21 - 1
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasRecordServiceImpl.java

@@ -87,20 +87,36 @@ public class RsOverseasRecordServiceImpl implements IRsOverseasRecordService {
      */
     @Override
     public TableDataInfo<RsOverseasRecordVo> queryMyRecordPageList(RsOverseasRecordQuery query) {
+        log.info("============= 出国记录列表查询开始 =============");
+        log.info("查询参数: {}", query);
+        log.info("申请人ID: {}", query.getApplicantId());
+        log.info("分页参数: pageNum={}, pageSize={}", query.getPageNum(), query.getPageSize());
+        
         // 使用联合查询方法,包含已通过审批的申请数据
         List<RsOverseasRecordVo> list = baseMapper.selectMyOverseasRecordList(query);
         
+        log.info("查询结果数量: {}", list != null ? list.size() : 0);
+        if (list != null && !list.isEmpty()) {
+            log.info("第一条记录: {}", list.get(0));
+        } else {
+            log.warn("⚠️ 查询结果为空!请检查数据库中是否有数据");
+        }
+        
         // 手动分页处理
         int pageNum = query.getPageNum();
         int pageSize = query.getPageSize();
         int start = (pageNum - 1) * pageSize;
         int end = Math.min(start + pageSize, list.size());
         
+        log.info("分页计算: start={}, end={}, totalSize={}", start, end, list.size());
+        
         List<RsOverseasRecordVo> pageList;
         if (start > list.size()) {
             pageList = Arrays.asList();
+            log.warn("⚠️ 起始位置超出数据范围,返回空列表");
         } else {
             pageList = list.subList(start, end);
+            log.info("分页后数据量: {}", pageList.size());
         }
         
         // 构建分页对象
@@ -108,7 +124,11 @@ public class RsOverseasRecordServiceImpl implements IRsOverseasRecordService {
         page.setRecords(pageList);
         page.setTotal(list.size());
         
-        return TableDataInfo.build(page);
+        TableDataInfo<RsOverseasRecordVo> result = TableDataInfo.build(page);
+        log.info("返回结果: total={}, rows={}", result.getTotal(), result.getRows() != null ? result.getRows().size() : 0);
+        log.info("============= 出国记录列表查询结束 =============");
+        
+        return result;
     }
 
     /**

+ 1 - 2
backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsJobMapper.xml

@@ -8,7 +8,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <id property="jobId" column="job_id"/>
         <result property="title" column="title"/>
         <result property="department" column="department"/>
-        <result property="salaryRange" column="salary_range"/>
         <result property="location" column="location"/>
         <result property="description" column="description"/>
         <result property="requirements" column="requirements"/>
@@ -27,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectRsJobVo">
-        select j.job_id, j.title, j.department, j.salary_range, j.location, j.description, j.requirements, 
+        select j.job_id, j.title, j.department, j.location, j.description, j.requirements, 
         j.is_active, j.view_count, j.tenant_id, j.create_by, j.create_time, j.update_by, j.update_time, j.remark,
         (select count(1) from rs_application a where a.job_id = j.job_id and a.deleted = '0' and a.tenant_id = j.tenant_id) as applicant_count
         from rs_job j

+ 289 - 5
backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml

@@ -6,12 +6,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     
     <resultMap type="RsOverseasApplication" id="RsOverseasApplicationResult">
         <id property="applicationId" column="application_id"/>
+        <result property="serialNumber" column="serial_number"/>
         <result property="applicantId" column="applicant_id"/>
         <result property="applicantName" column="applicant_name"/>
+        <result property="gender" column="gender"/>
+        <result property="birthDate" column="birth_date"/>
+        <result property="photo" column="photo"/>
+        <result property="idNumber" column="id_number"/>
+        <result property="politicalStatus" column="political_status"/>
+        <result property="registeredAddress" column="registered_address"/>
         <result property="department" column="department"/>
         <result property="position" column="position"/>
+        <result property="professionalTitle" column="professional_title"/>
+        <result property="administrativeLevel" column="administrative_level"/>
+        <result property="personnelCategory" column="personnel_category"/>
+        <result property="registrationUnit" column="registration_unit"/>
+        <result property="unitLeaderSignature" column="unit_leader_signature"/>
+        <result property="unitContact" column="unit_contact"/>
+        <result property="officePhone" column="office_phone"/>
+        <result property="mobilePhone" column="mobile_phone"/>
         <result property="contactPhone" column="contact_phone"/>
         <result property="contactEmail" column="contact_email"/>
+        <result property="documentName" column="document_name"/>
+        <result property="documentNumber" column="document_number"/>
+        <result property="destinationCountry" column="destination_country"/>
+        <result property="exitReason" column="exit_reason"/>
+        <result property="visitRelationship" column="visit_relationship"/>
+        <result property="remarks" column="remarks"/>
         <result property="purpose" column="purpose"/>
         <result property="purposeDetail" column="purpose_detail"/>
         <result property="destination" column="destination"/>
@@ -31,6 +52,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="approvalTime" column="approval_time"/>
         <result property="adminRemark" column="admin_remark"/>
         <result property="applicantRemark" column="applicant_remark"/>
+        <result property="emergencyContact" column="emergency_contact"/>
+        <result property="signature" column="signature"/>
+        <result property="stampedRegistrationFilePath" column="stamped_registration_file_path"/>
+        <result property="stampedRegistrationFileName" column="stamped_registration_file_name"/>
+        <result property="stampedFileUploadTime" column="stamped_file_upload_time"/>
+        <result property="stampedFileUploadStatus" column="stamped_file_upload_status"/>
         <result property="tenantId" column="tenant_id"/>
         <result property="createBy" column="create_by"/>
         <result property="createTime" column="create_time"/>
@@ -43,15 +70,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="statusText" column="status_text"/>
         <result property="purposeText" column="purpose_text"/>
         <result property="approvalLevelText" column="approval_level_text"/>
+        <result property="genderText" column="gender_text"/>
+        <result property="politicalStatusText" column="political_status_text"/>
+        <result property="administrativeLevelText" column="administrative_level_text"/>
+        <result property="personnelCategoryText" column="personnel_category_text"/>
     </resultMap>
 
     <sql id="selectOverseasApplicationVo">
-        select oa.application_id, oa.applicant_id, oa.applicant_name, oa.department, oa.position, 
-        oa.contact_phone, oa.contact_email, oa.purpose, oa.purpose_detail, oa.destination, oa.destination_city,
+        select oa.application_id, oa.serial_number, oa.applicant_id, oa.applicant_name, 
+        oa.gender, oa.birth_date, oa.photo, oa.id_number, oa.political_status, oa.registered_address,
+        oa.department, oa.position, oa.professional_title, oa.administrative_level, oa.personnel_category,
+        oa.registration_unit, oa.unit_leader_signature, oa.unit_contact, oa.office_phone, oa.mobile_phone,
+        oa.contact_phone, oa.contact_email, oa.document_name, oa.document_number, 
+        oa.destination_country, oa.exit_reason, oa.visit_relationship, oa.remarks,
+        oa.purpose, oa.purpose_detail, oa.destination, oa.destination_city,
         oa.start_date, oa.end_date, oa.duration, oa.budget, oa.invitation, oa.itinerary,
         oa.accommodation_plan, oa.transportation_plan, oa.status, oa.current_approver, oa.approval_level,
-        oa.apply_time, oa.approval_time, oa.admin_remark, oa.applicant_remark, oa.tenant_id,
-        oa.create_by, oa.create_time, oa.update_by, oa.update_time, oa.deleted,
+        oa.apply_time, oa.approval_time, oa.admin_remark, oa.applicant_remark, 
+        oa.emergency_contact, oa.signature,
+        oa.stamped_registration_file_path, oa.stamped_registration_file_name, 
+        oa.stamped_file_upload_time, oa.stamped_file_upload_status,
+        oa.tenant_id, oa.create_by, oa.create_time, oa.update_by, oa.update_time, oa.deleted,
         CASE 
             WHEN oa.status IS NULL OR oa.status = '' THEN '草稿'
             WHEN oa.status = '0' THEN '待审核'
@@ -75,7 +114,52 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             WHEN '2' THEN '分管领导'
             WHEN '3' THEN '校领导'
             ELSE '未知级别'
-        END as approval_level_text
+        END as approval_level_text,
+        CASE oa.gender 
+            WHEN '0' THEN '男'
+            WHEN '1' THEN '女'
+            ELSE '未知'
+        END as gender_text,
+        CASE oa.political_status 
+            WHEN '1' THEN '中共党员'
+            WHEN '2' THEN '中共预备党员'
+            WHEN '3' THEN '共青团员'
+            WHEN '4' THEN '民革党员'
+            WHEN '5' THEN '民盟盟员'
+            WHEN '6' THEN '民建会员'
+            WHEN '7' THEN '民进会员'
+            WHEN '8' THEN '农工党党员'
+            WHEN '9' THEN '致公党党员'
+            WHEN '10' THEN '九三学社社员'
+            WHEN '11' THEN '台盟盟员'
+            WHEN '12' THEN '无党派人士'
+            WHEN '13' THEN '群众'
+            ELSE oa.political_status
+        END as political_status_text,
+        CASE oa.administrative_level 
+            WHEN '0' THEN '无'
+            WHEN '1' THEN '一级巡视员'
+            WHEN '2' THEN '二级巡视员'
+            WHEN '3' THEN '一级调研员'
+            WHEN '4' THEN '二级调研员'
+            WHEN '5' THEN '三级调研员'
+            WHEN '6' THEN '四级调研员'
+            WHEN '7' THEN '一级主任科员'
+            WHEN '8' THEN '二级主任科员'
+            WHEN '9' THEN '三级主任科员'
+            WHEN '10' THEN '四级主任科员'
+            WHEN '11' THEN '一级科员'
+            WHEN '12' THEN '二级科员'
+            ELSE oa.administrative_level
+        END as administrative_level_text,
+        CASE oa.personnel_category 
+            WHEN '1' THEN '1-厅局级'
+            WHEN '2' THEN '2-县处级'
+            WHEN '3' THEN '3-乡科级'
+            WHEN '4' THEN '4-一般干部'
+            WHEN '5' THEN '5-其他重要岗位人员'
+            ELSE oa.personnel_category
+        END as personnel_category_text
         from rs_overseas_application oa
     </sql>
 
@@ -216,4 +300,204 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by oa.apply_time asc
     </select>
     
+    <!-- 插入出国申请 - 包含所有字段 -->
+    <insert id="insertOverseasApplication" parameterType="RsOverseasApplication" useGeneratedKeys="true" keyProperty="applicationId">
+        insert into rs_overseas_application
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="serialNumber != null and serialNumber != ''">serial_number,</if>
+            <if test="applicantId != null">applicant_id,</if>
+            <if test="applicantName != null and applicantName != ''">applicant_name,</if>
+            <if test="gender != null and gender != ''">gender,</if>
+            <if test="birthDate != null">birth_date,</if>
+            <if test="photo != null and photo != ''">photo,</if>
+            <if test="idNumber != null and idNumber != ''">id_number,</if>
+            <if test="politicalStatus != null and politicalStatus != ''">political_status,</if>
+            <if test="registeredAddress != null and registeredAddress != ''">registered_address,</if>
+            <if test="department != null and department != ''">department,</if>
+            <if test="position != null and position != ''">position,</if>
+            <if test="professionalTitle != null and professionalTitle != ''">professional_title,</if>
+            <if test="administrativeLevel != null and administrativeLevel != ''">administrative_level,</if>
+            <if test="personnelCategory != null and personnelCategory != ''">personnel_category,</if>
+            <if test="registrationUnit != null and registrationUnit != ''">registration_unit,</if>
+            <if test="unitLeaderSignature != null and unitLeaderSignature != ''">unit_leader_signature,</if>
+            <if test="unitContact != null and unitContact != ''">unit_contact,</if>
+            <if test="officePhone != null and officePhone != ''">office_phone,</if>
+            <if test="mobilePhone != null and mobilePhone != ''">mobile_phone,</if>
+            <if test="contactPhone != null and contactPhone != ''">contact_phone,</if>
+            <if test="contactEmail != null and contactEmail != ''">contact_email,</if>
+            <if test="documentName != null and documentName != ''">document_name,</if>
+            <if test="documentNumber != null and documentNumber != ''">document_number,</if>
+            <if test="destinationCountry != null and destinationCountry != ''">destination_country,</if>
+            <if test="exitReason != null and exitReason != ''">exit_reason,</if>
+            <if test="visitRelationship != null and visitRelationship != ''">visit_relationship,</if>
+            <if test="remarks != null and remarks != ''">remarks,</if>
+            <if test="purpose != null and purpose != ''">purpose,</if>
+            <if test="purposeDetail != null and purposeDetail != ''">purpose_detail,</if>
+            <if test="destination != null and destination != ''">destination,</if>
+            <if test="destinationCity != null and destinationCity != ''">destination_city,</if>
+            <if test="startDate != null">start_date,</if>
+            <if test="endDate != null">end_date,</if>
+            <if test="duration != null">duration,</if>
+            <if test="budget != null">budget,</if>
+            <if test="invitation != null and invitation != ''">invitation,</if>
+            <if test="itinerary != null and itinerary != ''">itinerary,</if>
+            <if test="accommodationPlan != null and accommodationPlan != ''">accommodation_plan,</if>
+            <if test="transportationPlan != null and transportationPlan != ''">transportation_plan,</if>
+            <if test="status != null and status != ''">status,</if>
+            <if test="currentApprover != null and currentApprover != ''">current_approver,</if>
+            <if test="approvalLevel != null and approvalLevel != ''">approval_level,</if>
+            <if test="applyTime != null">apply_time,</if>
+            <if test="approvalTime != null">approval_time,</if>
+            <if test="adminRemark != null and adminRemark != ''">admin_remark,</if>
+            <if test="applicantRemark != null and applicantRemark != ''">applicant_remark,</if>
+            <if test="emergencyContact != null">emergency_contact,</if>
+            <if test="signature != null and signature != ''">signature,</if>
+            <if test="stampedRegistrationFilePath != null and stampedRegistrationFilePath != ''">stamped_registration_file_path,</if>
+            <if test="stampedRegistrationFileName != null and stampedRegistrationFileName != ''">stamped_registration_file_name,</if>
+            <if test="stampedFileUploadTime != null">stamped_file_upload_time,</if>
+            <if test="stampedFileUploadStatus != null and stampedFileUploadStatus != ''">stamped_file_upload_status,</if>
+            <if test="tenantId != null and tenantId != ''">tenant_id,</if>
+            <if test="deleted != null and deleted != ''">deleted,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="serialNumber != null and serialNumber != ''">#{serialNumber},</if>
+            <if test="applicantId != null">#{applicantId},</if>
+            <if test="applicantName != null and applicantName != ''">#{applicantName},</if>
+            <if test="gender != null and gender != ''">#{gender},</if>
+            <if test="birthDate != null">#{birthDate},</if>
+            <if test="photo != null and photo != ''">#{photo},</if>
+            <if test="idNumber != null and idNumber != ''">#{idNumber},</if>
+            <if test="politicalStatus != null and politicalStatus != ''">#{politicalStatus},</if>
+            <if test="registeredAddress != null and registeredAddress != ''">#{registeredAddress},</if>
+            <if test="department != null and department != ''">#{department},</if>
+            <if test="position != null and position != ''">#{position},</if>
+            <if test="professionalTitle != null and professionalTitle != ''">#{professionalTitle},</if>
+            <if test="administrativeLevel != null and administrativeLevel != ''">#{administrativeLevel},</if>
+            <if test="personnelCategory != null and personnelCategory != ''">#{personnelCategory},</if>
+            <if test="registrationUnit != null and registrationUnit != ''">#{registrationUnit},</if>
+            <if test="unitLeaderSignature != null and unitLeaderSignature != ''">#{unitLeaderSignature},</if>
+            <if test="unitContact != null and unitContact != ''">#{unitContact},</if>
+            <if test="officePhone != null and officePhone != ''">#{officePhone},</if>
+            <if test="mobilePhone != null and mobilePhone != ''">#{mobilePhone},</if>
+            <if test="contactPhone != null and contactPhone != ''">#{contactPhone},</if>
+            <if test="contactEmail != null and contactEmail != ''">#{contactEmail},</if>
+            <if test="documentName != null and documentName != ''">#{documentName},</if>
+            <if test="documentNumber != null and documentNumber != ''">#{documentNumber},</if>
+            <if test="destinationCountry != null and destinationCountry != ''">#{destinationCountry},</if>
+            <if test="exitReason != null and exitReason != ''">#{exitReason},</if>
+            <if test="visitRelationship != null and visitRelationship != ''">#{visitRelationship},</if>
+            <if test="remarks != null and remarks != ''">#{remarks},</if>
+            <if test="purpose != null and purpose != ''">#{purpose},</if>
+            <if test="purposeDetail != null and purposeDetail != ''">#{purposeDetail},</if>
+            <if test="destination != null and destination != ''">#{destination},</if>
+            <if test="destinationCity != null and destinationCity != ''">#{destinationCity},</if>
+            <if test="startDate != null">#{startDate},</if>
+            <if test="endDate != null">#{endDate},</if>
+            <if test="duration != null">#{duration},</if>
+            <if test="budget != null">#{budget},</if>
+            <if test="invitation != null and invitation != ''">#{invitation},</if>
+            <if test="itinerary != null and itinerary != ''">#{itinerary},</if>
+            <if test="accommodationPlan != null and accommodationPlan != ''">#{accommodationPlan},</if>
+            <if test="transportationPlan != null and transportationPlan != ''">#{transportationPlan},</if>
+            <if test="status != null and status != ''">#{status},</if>
+            <if test="currentApprover != null and currentApprover != ''">#{currentApprover},</if>
+            <if test="approvalLevel != null and approvalLevel != ''">#{approvalLevel},</if>
+            <if test="applyTime != null">#{applyTime},</if>
+            <if test="approvalTime != null">#{approvalTime},</if>
+            <if test="adminRemark != null and adminRemark != ''">#{adminRemark},</if>
+            <if test="applicantRemark != null and applicantRemark != ''">#{applicantRemark},</if>
+            <if test="emergencyContact != null">#{emergencyContact},</if>
+            <if test="signature != null and signature != ''">#{signature},</if>
+            <if test="stampedRegistrationFilePath != null and stampedRegistrationFilePath != ''">#{stampedRegistrationFilePath},</if>
+            <if test="stampedRegistrationFileName != null and stampedRegistrationFileName != ''">#{stampedRegistrationFileName},</if>
+            <if test="stampedFileUploadTime != null">#{stampedFileUploadTime},</if>
+            <if test="stampedFileUploadStatus != null and stampedFileUploadStatus != ''">#{stampedFileUploadStatus},</if>
+            <if test="tenantId != null and tenantId != ''">#{tenantId},</if>
+            <if test="deleted != null and deleted != ''">#{deleted},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+        </trim>
+    </insert>
+
+    <!-- 更新出国申请 - 包含所有字段 -->
+    <update id="updateOverseasApplication" parameterType="RsOverseasApplication">
+        update rs_overseas_application
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="serialNumber != null and serialNumber != ''">serial_number = #{serialNumber},</if>
+            <if test="applicantId != null">applicant_id = #{applicantId},</if>
+            <if test="applicantName != null and applicantName != ''">applicant_name = #{applicantName},</if>
+            <if test="gender != null and gender != ''">gender = #{gender},</if>
+            <if test="birthDate != null">birth_date = #{birthDate},</if>
+            <if test="photo != null">photo = #{photo},</if>
+            <if test="idNumber != null">id_number = #{idNumber},</if>
+            <if test="politicalStatus != null and politicalStatus != ''">political_status = #{politicalStatus},</if>
+            <if test="registeredAddress != null">registered_address = #{registeredAddress},</if>
+            <if test="department != null and department != ''">department = #{department},</if>
+            <if test="position != null">position = #{position},</if>
+            <if test="professionalTitle != null">professional_title = #{professionalTitle},</if>
+            <if test="administrativeLevel != null">administrative_level = #{administrativeLevel},</if>
+            <if test="personnelCategory != null">personnel_category = #{personnelCategory},</if>
+            <if test="registrationUnit != null">registration_unit = #{registrationUnit},</if>
+            <if test="unitLeaderSignature != null">unit_leader_signature = #{unitLeaderSignature},</if>
+            <if test="unitContact != null">unit_contact = #{unitContact},</if>
+            <if test="officePhone != null">office_phone = #{officePhone},</if>
+            <if test="mobilePhone != null">mobile_phone = #{mobilePhone},</if>
+            <if test="contactPhone != null">contact_phone = #{contactPhone},</if>
+            <if test="contactEmail != null">contact_email = #{contactEmail},</if>
+            <if test="documentName != null">document_name = #{documentName},</if>
+            <if test="documentNumber != null">document_number = #{documentNumber},</if>
+            <if test="destinationCountry != null">destination_country = #{destinationCountry},</if>
+            <if test="exitReason != null">exit_reason = #{exitReason},</if>
+            <if test="visitRelationship != null">visit_relationship = #{visitRelationship},</if>
+            <if test="remarks != null">remarks = #{remarks},</if>
+            <if test="purpose != null">purpose = #{purpose},</if>
+            <if test="purposeDetail != null">purpose_detail = #{purposeDetail},</if>
+            <if test="destination != null">destination = #{destination},</if>
+            <if test="destinationCity != null">destination_city = #{destinationCity},</if>
+            <if test="startDate != null">start_date = #{startDate},</if>
+            <if test="endDate != null">end_date = #{endDate},</if>
+            <if test="duration != null">duration = #{duration},</if>
+            <if test="budget != null">budget = #{budget},</if>
+            <if test="invitation != null">invitation = #{invitation},</if>
+            <if test="itinerary != null">itinerary = #{itinerary},</if>
+            <if test="accommodationPlan != null">accommodation_plan = #{accommodationPlan},</if>
+            <if test="transportationPlan != null">transportation_plan = #{transportationPlan},</if>
+            <if test="status != null and status != ''">status = #{status},</if>
+            <if test="currentApprover != null">current_approver = #{currentApprover},</if>
+            <if test="approvalLevel != null">approval_level = #{approvalLevel},</if>
+            <if test="applyTime != null">apply_time = #{applyTime},</if>
+            <if test="approvalTime != null">approval_time = #{approvalTime},</if>
+            <if test="adminRemark != null">admin_remark = #{adminRemark},</if>
+            <if test="applicantRemark != null">applicant_remark = #{applicantRemark},</if>
+            <if test="emergencyContact != null">emergency_contact = #{emergencyContact},</if>
+            <if test="signature != null">signature = #{signature},</if>
+            <if test="stampedRegistrationFilePath != null">stamped_registration_file_path = #{stampedRegistrationFilePath},</if>
+            <if test="stampedRegistrationFileName != null">stamped_registration_file_name = #{stampedRegistrationFileName},</if>
+            <if test="stampedFileUploadTime != null">stamped_file_upload_time = #{stampedFileUploadTime},</if>
+            <if test="stampedFileUploadStatus != null">stamped_file_upload_status = #{stampedFileUploadStatus},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where application_id = #{applicationId}
+    </update>
+
+    <!-- 删除出国申请(逻辑删除) -->
+    <update id="deleteOverseasApplicationById" parameterType="Long">
+        update rs_overseas_application set deleted = '1' where application_id = #{applicationId}
+    </update>
+
+    <!-- 批量删除出国申请(逻辑删除) -->
+    <update id="deleteOverseasApplicationByIds" parameterType="Long">
+        update rs_overseas_application set deleted = '1' where application_id in
+        <foreach item="applicationId" collection="array" open="(" separator="," close=")">
+            #{applicationId}
+        </foreach>
+    </update>
+    
 </mapper> 

+ 195 - 2
backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasRecordMapper.xml

@@ -40,6 +40,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     
     <resultMap id="RsOverseasRecordVoResult" type="org.dromara.rs.domain.vo.RsOverseasRecordVo" extends="RsOverseasRecordResult">
         <result property="dataSource" column="data_source"/>
+        <!-- 来自申请表的扩展字段 -->
+        <result property="gender" column="gender"/>
+        <result property="genderText" column="gender_text"/>
+        <result property="birthDate" column="birth_date"/>
+        <result property="politicalStatus" column="political_status"/>
+        <result property="politicalStatusText" column="political_status_text"/>
+        <result property="professionalTitle" column="professional_title"/>
+        <result property="documentName" column="document_name"/>
+        <result property="documentNumber" column="document_number"/>
+        <result property="mobilePhone" column="mobile_phone"/>
+        <result property="destinationCountry" column="destination_country"/>
+        <result property="exitReason" column="exit_reason"/>
+        <result property="emergencyContact" column="emergency_contact"/>
+        <result property="remarks" column="remarks"/>
+        <result property="purposeText" column="purpose_text"/>
+        <result property="completionStatusText" column="completion_status_text"/>
     </resultMap>
 
     <sql id="selectOverseasRecordVo">
@@ -52,10 +68,74 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         'record' as data_source
         from rs_overseas_record ors
     </sql>
+    
+    <!-- 查询出国记录详情(关联申请表数据) -->
+    <sql id="selectOverseasRecordDetailVo">
+        select 
+            ors.record_id, ors.application_id, ors.applicant_id, ors.applicant_name, ors.department, 
+            ors.position, ors.contact_phone, ors.contact_email, ors.purpose, ors.purpose_detail, ors.destination, ors.destination_city,
+            ors.actual_start_date, ors.actual_end_date, ors.actual_duration, ors.actual_budget,
+            ors.invitation, ors.itinerary, ors.accommodation_plan, ors.transportation_plan,
+            ors.completion_status, ors.achievement, ors.report_content, ors.certificate_path,
+            ors.tenant_id, ors.create_dept, ors.create_by, ors.create_time, ors.update_by, ors.update_time, ors.deleted,
+            -- 来自申请表的扩展字段
+            oa.gender,
+            CASE oa.gender 
+                WHEN '0' THEN '男'
+                WHEN '1' THEN '女'
+                ELSE oa.gender
+            END as gender_text,
+            oa.birth_date,
+            oa.political_status,
+            CASE oa.political_status 
+                WHEN '1' THEN '中共党员'
+                WHEN '2' THEN '中共预备党员'
+                WHEN '3' THEN '共青团员'
+                WHEN '4' THEN '民革党员'
+                WHEN '5' THEN '民盟盟员'
+                WHEN '6' THEN '民建会员'
+                WHEN '7' THEN '民进会员'
+                WHEN '8' THEN '农工党党员'
+                WHEN '9' THEN '致公党党员'
+                WHEN '10' THEN '九三学社社员'
+                WHEN '11' THEN '台盟盟员'
+                WHEN '12' THEN '无党派人士'
+                WHEN '13' THEN '群众'
+                ELSE oa.political_status
+            END as political_status_text,
+            oa.professional_title,
+            oa.document_name,
+            oa.document_number,
+            oa.mobile_phone,
+            oa.destination_country,
+            oa.exit_reason,
+            oa.emergency_contact,
+            oa.remarks,
+            -- 出国目的文本转换
+            CASE ors.purpose 
+                WHEN 'academic_exchange' THEN '学术交流'
+                WHEN 'conference' THEN '学术会议'
+                WHEN 'training' THEN '培训学习'
+                WHEN 'cooperation' THEN '合作项目'
+                WHEN 'research' THEN '科研访问'
+                WHEN 'other' THEN '其他'
+                ELSE ors.purpose
+            END as purpose_text,
+            -- 完成状态文本转换
+            CASE ors.completion_status 
+                WHEN '0' THEN '未完成'
+                WHEN '1' THEN '进行中'
+                WHEN '2' THEN '已完成'
+                ELSE '未知状态'
+            END as completion_status_text,
+            'record' as data_source
+        from rs_overseas_record ors
+        left join rs_overseas_application oa on ors.application_id = oa.application_id and oa.deleted = '0'
+    </sql>
 
     <!-- 联合查询:出国记录 + 已完成审批的申请 -->
     <sql id="selectOverseasRecordWithApprovedApplicationsVo">
-        -- 查询已创建的出国记录
+        -- 查询已创建的出国记录(关联申请表获取扩展字段)
         select 
             ors.record_id, ors.application_id, ors.applicant_id, ors.applicant_name, ors.department, 
             ors.position, ors.contact_phone, ors.contact_email, ors.purpose, ors.purpose_detail, ors.destination, ors.destination_city,
@@ -64,8 +144,68 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             ors.invitation, ors.itinerary, ors.accommodation_plan, ors.transportation_plan,
             ors.completion_status, ors.achievement, ors.report_content, ors.certificate_path,
             ors.tenant_id, ors.create_dept, ors.create_by, ors.create_time, ors.update_by, ors.update_time, ors.deleted,
+            -- 来自申请表的扩展字段
+            oa.gender,
+            CASE oa.gender 
+                WHEN '0' THEN '男'
+                WHEN '1' THEN '女'
+                ELSE oa.gender
+            END as gender_text,
+            oa.birth_date,
+            oa.political_status,
+            CASE oa.political_status 
+                WHEN '1' THEN '中共党员'
+                WHEN '2' THEN '中共预备党员'
+                WHEN '3' THEN '共青团员'
+                WHEN '4' THEN '民革党员'
+                WHEN '5' THEN '民盟盟员'
+                WHEN '6' THEN '民建会员'
+                WHEN '7' THEN '民进会员'
+                WHEN '8' THEN '农工党党员'
+                WHEN '9' THEN '致公党党员'
+                WHEN '10' THEN '九三学社社员'
+                WHEN '11' THEN '台盟盟员'
+                WHEN '12' THEN '无党派人士'
+                WHEN '13' THEN '群众'
+                ELSE oa.political_status
+            END as political_status_text,
+            oa.professional_title,
+            oa.document_name,
+            oa.document_number,
+            oa.mobile_phone,
+            oa.destination_country,
+            oa.exit_reason,
+            oa.emergency_contact,
+            oa.remarks,
+            -- 申请状态(来自申请表)
+            oa.status,
+            -- 申请状态文本转换
+            CASE oa.status 
+                WHEN '0' THEN '待审核'
+                WHEN '1' THEN '已通过'
+                WHEN '2' THEN '已拒绝'
+                ELSE oa.status
+            END as status_text,
+            -- 出国目的文本转换
+            CASE ors.purpose 
+                WHEN 'academic_exchange' THEN '学术交流'
+                WHEN 'conference_participation' THEN '参加会议'
+                WHEN 'collaborative_research' THEN '合作研究'
+                WHEN 'short_term_training' THEN '短期培训'
+                WHEN 'other' THEN '其他'
+                ELSE ors.purpose
+            END as purpose_text,
+            -- 完成状态文本转换
+            CASE ors.completion_status 
+                WHEN '0' THEN '未完成'
+                WHEN '1' THEN '进行中'
+                WHEN '2' THEN '已完成'
+                WHEN '3' THEN '已取消'
+                ELSE ors.completion_status
+            END as completion_status_text,
             'record' as data_source
         from rs_overseas_record ors
+        left join rs_overseas_application oa on ors.application_id = oa.application_id and oa.deleted = '0'
         where ors.deleted = '0'
         
         UNION ALL
@@ -80,6 +220,59 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             oa.invitation, oa.itinerary, oa.accommodation_plan, oa.transportation_plan,
             '1' as completion_status, NULL as achievement, NULL as report_content, NULL as certificate_path,
             oa.tenant_id, NULL as create_dept, oa.create_by, oa.create_time, oa.update_by, oa.update_time, oa.deleted,
+            -- 来自申请表的字段(UNION ALL 第二部分直接从申请表获取)
+            oa.gender,
+            CASE oa.gender 
+                WHEN '0' THEN '男'
+                WHEN '1' THEN '女'
+                ELSE oa.gender
+            END as gender_text,
+            oa.birth_date,
+            oa.political_status,
+            CASE oa.political_status 
+                WHEN '1' THEN '中共党员'
+                WHEN '2' THEN '中共预备党员'
+                WHEN '3' THEN '共青团员'
+                WHEN '4' THEN '民革党员'
+                WHEN '5' THEN '民盟盟员'
+                WHEN '6' THEN '民建会员'
+                WHEN '7' THEN '民进会员'
+                WHEN '8' THEN '农工党党员'
+                WHEN '9' THEN '致公党党员'
+                WHEN '10' THEN '九三学社社员'
+                WHEN '11' THEN '台盟盟员'
+                WHEN '12' THEN '无党派人士'
+                WHEN '13' THEN '群众'
+                ELSE oa.political_status
+            END as political_status_text,
+            oa.professional_title,
+            oa.document_name,
+            oa.document_number,
+            oa.mobile_phone,
+            oa.destination_country,
+            oa.exit_reason,
+            oa.emergency_contact,
+            oa.remarks,
+            -- 申请状态(来自申请表)
+            oa.status,
+            -- 申请状态文本转换(已通过审批的申请)
+            CASE oa.status 
+                WHEN '0' THEN '待审核'
+                WHEN '1' THEN '已通过'
+                WHEN '2' THEN '已拒绝'
+                ELSE oa.status
+            END as status_text,
+            -- 出国目的文本转换
+            CASE oa.purpose 
+                WHEN 'academic_exchange' THEN '学术交流'
+                WHEN 'conference_participation' THEN '参加会议'
+                WHEN 'collaborative_research' THEN '合作研究'
+                WHEN 'short_term_training' THEN '短期培训'
+                WHEN 'other' THEN '其他'
+                ELSE oa.purpose
+            END as purpose_text,
+            -- 完成状态文本(已通过审批未创建记录的状态为"进行中")
+            '进行中' as completion_status_text,
             'approved_application' as data_source
         from rs_overseas_application oa
         where oa.deleted = '0'
@@ -140,7 +333,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </select>
     
     <select id="selectOverseasRecordById" parameterType="Long" resultMap="RsOverseasRecordVoResult">
-        <include refid="selectOverseasRecordVo"/>
+        <include refid="selectOverseasRecordDetailVo"/>
         where ors.record_id = #{recordId} and ors.deleted = 0
     </select>
     

+ 0 - 19
docs/原型文件/人事管理/recruitment-prototype.html

@@ -612,18 +612,6 @@
                       </select>
                     </div>
                   </div>
-                  <div class="el-col el-col-6" style="padding: 0 10px;">
-                    <div class="el-form-item">
-                      <label>薪资范围</label>
-                      <select>
-                        <option value="">全部</option>
-                        <option value="5k-10k">5k-10k</option>
-                        <option value="10k-15k">10k-15k</option>
-                        <option value="15k-20k">15k-20k</option>
-                        <option value="20k+">20k以上</option>
-                      </select>
-                    </div>
-                  </div>
                   <div class="el-col el-col-6" style="padding: 0 10px;">
                     <div class="el-form-item">
                       <label>工作地点</label>
@@ -813,7 +801,6 @@
                     </th>
                     <th>职位名称</th>
                     <th>部门</th>
-                    <th>薪资范围</th>
                     <th>工作地点</th>
                     <th>发布日期</th>
                     <th>状态</th>
@@ -826,7 +813,6 @@
                     <td><input type="checkbox" style="width: auto;"></td>
                     <td>高级Java开发工程师</td>
                     <td>教学部门</td>
-                    <td>15k-25k</td>
                     <td>杭州</td>
                     <td>2025-07-01</td>
                     <td><span class="badge badge-active">招聘中</span></td>
@@ -911,11 +897,6 @@
               </select>
             </div>
             
-            <div class="form-group">
-              <label for="salary-range">薪资范围</label>
-              <input type="text" id="salary-range" placeholder="例如:15k-25k">
-            </div>
-            
             <div class="form-group">
               <label for="location">工作地点</label>
               <input type="text" id="location" placeholder="请输入工作地点">

+ 7 - 0
docs/开发文档/API/API文档总览.md

@@ -143,6 +143,13 @@
 
 ## 🔗 API文档导航
 
+### 👨‍💼 人事管理模块
+
+| API文档 | 描述 | 状态 | 更新时间 |
+|---------|------|------|----------|
+| [出国申请管理API](./出国申请管理API.md) | 出国申请审批流程管理(三级审批) | ✅ 完成 | 2025-01-28 |
+| **[出国登记管理API](./出国登记管理API.md)** | **出国登记信息管理(已优化字段)** | **✅ 完成** | **2025-09-30** |
+
 ### 👥 学工管理模块
 
 | API文档 | 描述 | 状态 | 更新时间 |

+ 2 - 6
docs/开发文档/API/人事招聘模块API.md

@@ -21,7 +21,6 @@
 | job_id | bigint(20) | 是 | 职位ID,主键 |
 | title | varchar(100) | 是 | 职位名称 |
 | department | varchar(50) | 是 | 部门 |
-| salary_range | varchar(50) | 是 | 薪资范围 |
 | location | varchar(100) | 是 | 工作地点 |
 | description | text | 是 | 职位描述 |
 | requirements | text | 是 | 任职要求 |
@@ -130,7 +129,6 @@
       "jobId": 1,
       "title": "软件工程师",
       "department": "tech",
-      "salaryRange": "15k-25k",
       "location": "北京",
       "isActive": "0",
       "publishTime": "2025-07-01 10:00:00",
@@ -181,7 +179,6 @@
     "jobId": 1,
     "title": "软件工程师",
     "department": "tech",
-    "salaryRange": "15k-25k",
     "location": "北京",
     "description": "负责系统设计和开发...",
     "requirements": "本科及以上学历,3年以上相关经验...",
@@ -208,7 +205,7 @@
 |-------|------|------|------|
 | title | string | 是 | 职位名称 |
 | department | string | 是 | 部门 |
-| salaryRange | string | 是 | 薪资范围 |
+
 | location | string | 是 | 工作地点 |
 | description | string | 是 | 职位描述 |
 | requirements | string | 是 | 任职要求 |
@@ -236,7 +233,6 @@
 | jobId | long | 是 | 职位ID |
 | title | string | 是 | 职位名称 |
 | department | string | 是 | 部门 |
-| salaryRange | string | 是 | 薪资范围 |
 | location | string | 是 | 工作地点 |
 | description | string | 是 | 职位描述 |
 | requirements | string | 是 | 任职要求 |
@@ -382,7 +378,7 @@ rs/
 
 1. 进入职位管理页面 (`/rs/job`)
 2. 点击"新增"按钮添加职位
-3. 填写职位信息,包括职位名称、部门、薪资范围等
+3. 填写职位信息,包括职位名称、部门、工作地点等
 4. 点击"确定"保存职位信息
 
 ### 申请管理页面

+ 87 - 5
docs/开发文档/API/出国申请管理API.md

@@ -639,11 +639,21 @@ referer: http://localhost
 
 ### 完成状态 (completionStatus)
 
-| 编码 | 含义     | 描述                     |
-| ---- | -------- | ------------------------ |
-| 0    | 进行中   | 出国活动正在进行         |
-| 1    | 已完成   | 出国活动已完成           |
-| 2    | 提前结束 | 出国活动提前结束         |
+> **⚠️ 重要说明**:完成状态(completionStatus)与申请状态(status)是两个不同的概念!
+> - **申请状态(status)**:表示审批流程的状态(待审核/已通过/已拒绝)
+> - **完成状态(completionStatus)**:表示出国任务执行的状态(未完成/进行中/已完成/已取消)
+
+| 编码 | 含义     | 描述                     | 使用场景 |
+| ---- | -------- | ------------------------ | -------- |
+| 0    | 未完成   | 出国活动尚未开始         | 已通过审批但尚未出发 |
+| 1    | 进行中   | 出国活动正在进行         | 已出发但未返回(默认值) |
+| 2    | 已完成   | 出国活动已完成           | 已返回并完成回执 |
+| 3    | 已取消   | 出国活动取消             | 计划取消 |
+
+**字段关系示例**:
+- 出国记录中的申请若已通过审批(`status='1'`),默认完成状态为"进行中"(`completionStatus='1'`)
+- 出国记录列表显示"完成状态"字段,详情弹窗显示"申请状态"字段
+- 所有出国记录的申请状态都应该是"已通过"(`status='1'`),因为只有通过审批的申请才会出现在记录中
 
 ---
 
@@ -714,6 +724,78 @@ referer: http://localhost
 | v1.2 | 2025-01-09 | 新增修复记录接口,修复审批通过后记录创建问题 | weizzzz |
 | v1.3 | 2025-01-08 | 调整我的申请列表接口,显示完整申请历史(包括已通过申请) | weizzzz |
 | v1.4 | 2025-01-08 | 修正我的申请列表接口,排除已通过申请(已通过申请显示在出国记录中) | weizzzz |
+| v1.5 | 2025-01-28 | 新增盖章文件上传功能,用户需上传盖章文件后才能提交申请 | weizzzz |
+
+### 8. 上传登记备案表盖章文件
+
+**接口地址**:`POST /rs/overseas/application/uploadStampedFile`  
+**权限要求**:`rs:overseas:add`
+
+#### 功能说明
+
+用于上传填写并加盖公章后的登记备案表文件。支持PDF、JPG、PNG格式,文件大小不超过10MB。上传成功后,用户才能提交出国申请。
+
+#### 请求参数
+
+**Content-Type**: `multipart/form-data`
+
+| 参数名        | 类型   | 必填 | 说明                           |
+|---------------|--------|------|--------------------------------|
+| file          | File   | 是   | 盖章文件,支持PDF、JPG、PNG格式 |
+| applicationId | Long   | 是   | 申请ID                         |
+
+#### 请求示例
+
+```bash
+POST /rs/overseas/application/uploadStampedFile
+Content-Type: multipart/form-data
+Authorization: Bearer {token}
+clientid: ruoyi-web
+referer: http://localhost
+
+form-data:
+  file: [选择的盖章文件]
+  applicationId: 1
+```
+
+#### 响应示例
+
+**成功响应**:
+```json
+{
+  "code": 200,
+  "msg": "盖章文件上传成功",
+  "data": {
+    "filePath": "overseas/stamped/stamped_1_1704076800000.pdf",
+    "fileName": "出国境登记表_张三.pdf",
+    "uploadTime": "2025-01-28 14:30:00"
+  }
+}
+```
+
+**失败响应**:
+```json
+{
+  "code": 500,
+  "msg": "不支持的文件格式,请上传PDF、JPG或PNG文件"
+}
+```
+
+#### 业务规则
+
+- 文件格式:只支持PDF、JPG、PNG格式
+- 文件大小:不超过10MB
+- 数量限制:每个申请只能上传一个盖章文件
+- 上传成功后,系统自动更新申请记录的盖章文件信息
+- 必须先上传盖章文件,才能提交出国申请
+
+#### 数据库更新
+
+上传成功后,系统会自动更新以下字段:
+- `stamped_registration_file_path`: 文件存储路径
+- `stamped_registration_file_name`: 原始文件名
+- `stamped_file_upload_time`: 上传时间
+- `stamped_file_upload_status`: 上传状态(设为'1')
 
 ---
 

+ 1 - 1
docs/开发文档/招聘门户系统最终完成报告.md

@@ -97,7 +97,7 @@ rs_job                 # 招聘岗位表
 ├── job_id            # 岗位ID
 ├── title             # 岗位名称
 ├── department        # 部门
-├── salary_range      # 薪资范围
+
 ├── location          # 工作地点
 ├── description       # 岗位描述
 ├── requirements      # 任职要求

+ 1 - 1
docs/需求文档/人事管理/培训管理.md

@@ -58,7 +58,7 @@
 - 出国申请流程:申请提交 → 部门审核 → 领导审批 → 出国执行
 
 #### 2. **详细字段需求**
-- **招聘职位**:职位名称、部门、岗位要求、薪资范围、工作地点等
+- **招聘职位**:职位名称、部门、岗位要求、工作地点等
 - **简历信息**:基本信息、教育背景、工作经历、技能证书、附件等
 - **培训记录**:培训名称、培训时间、学时、培训类别、证书等
 - **出国申请**:出国目的、目的地、时间、预算、行程安排等

+ 3 - 3
docs/需求文档/人事管理/招聘页面需求.md

@@ -9,11 +9,11 @@ cursor
 
 2. 核心功能:
    - 应聘者能浏览公开职位
-   - HR能发布新职位(含薪资范围/要求等字段)
+   - HR能发布新职位(含工作地点/要求等字段)
    - 响应式布局(支持移动端)
 
 3. 数据字段参考:
-   - 职位:title, department, salary_range, location, description, requirements
+   - 职位:title, department, location, description, requirements
    - 简历:personal_info, education[], work_experience[], skills
 
 4. 角色权限:
@@ -73,7 +73,7 @@ cursor
    - /apply 简历表单(分教育/工作经历多步骤)
 
 2. 主要功能组件:
-   - 职位搜索过滤器(部门/薪资范围)
+   - 职位搜索过滤器(部门/工作地点)
    - 简历PDF上传组件
    - 申请状态跟踪面板
 

+ 254 - 11
web/src/api/rs/overseas.ts

@@ -24,29 +24,57 @@ export interface OverseasApplicationQuery {
 // 出国申请数据类型
 export interface OverseasApplicationVO {
   applicationId: number | string;
+  serialNumber?: string; // 编号
   applicantId: number;
   applicantName: string;
+  gender?: string; // 性别
+  birthDate?: string; // 出生日期
+  photo?: string; // 照片地址
+  idNumber?: string; // 身份证号
+  politicalStatus?: string; // 政治面貌
+  registeredAddress?: string; // 户籍所在地
   department: string;
+  position?: string; // 职务
+  professionalTitle?: string; // 职称
+  administrativeLevel?: string; // 行政职级
+  personnelCategory?: string; // 人员类别代码
+  registrationUnit?: string; // 登记备案责任单位
+  unitLeaderSignature?: string; // 单位负责人签名
+  unitContact?: string; // 单位联系人
+  officePhone?: string; // 办公电话
+  mobilePhone?: string; // 移动电话
   contactPhone: string;
+  documentName?: string; // 申领证件名称
+  documentNumber?: string; // 证件号码
+  destinationCountry?: string; // 前往国家
+  exitReason?: string; // 出境事由
+  visitRelationship?: string; // 探望对象姓名及与申请者关系
+  remarks?: string; // 备注
+  emergencyContact?: string; // 应急联系人及电话
+  signature?: string; // 手写签字
+  stampedRegistrationFilePath?: string; // 登记备案表盖章文件路径
+  stampedRegistrationFileName?: string; // 登记备案表盖章文件名
+  stampedFileUploadTime?: string; // 盖章文件上传时间
+  stampedFileUploadStatus?: string; // 盖章文件上传状态
   purpose: string;
   purposeDetail: string;
   destination: string;
-  destinationCity: string;
+  destinationCity?: string;
   startDate: string;
   endDate: string;
   duration: number;
   budget: number;
-  invitation: string;
+  invitation?: string;
   itinerary: string;
-  accommodationPlan: string;
-  transportationPlan: string;
+  accommodationPlan?: string;
+  transportationPlan?: string;
   status: string;
-  currentApprover: string;
+  currentApprover?: string;
   approvalLevel: string;
   applyTime: string;
-  approvalTime: string;
-  adminRemark: string;
-  applicantRemark: string;
+  approvalTime?: string;
+  adminRemark?: string;
+  applicantRemark?: string;
   createTime: string;
   updateTime: string;
   createBy: string;
@@ -55,15 +83,50 @@ export interface OverseasApplicationVO {
   statusText: string;
   purposeText: string;
   approvalLevelText: string;
+  genderText?: string;
+  politicalStatusText?: string;
+  administrativeLevelText?: string;
+  personnelCategoryText?: string;
 }
 
 // 出国申请表单类型
 export interface OverseasApplicationForm {
   applicationId?: number | string;
+  serialNumber?: string; // 编号
   applicantId?: number;
   applicantName?: string;
-  department?: string; // 自动匹配,前端不需要输入
-  contactPhone?: string; // 联系电话
+  gender?: string; // 性别
+  birthDate?: string; // 出生日期
+  photo?: string; // 照片地址
+  idNumber?: string; // 身份证号
+  politicalStatus?: string; // 政治面貌
+  registeredAddress?: string; // 户籍所在地
+  department?: string; // 所属部门/现工作部门
+  position?: string; // 职务
+  professionalTitle?: string; // 职称
+  administrativeLevel?: string; // 行政职级
+  personnelCategory?: string; // 人员类别代码
+  unitLeaderSignature?: string; // 单位负责人签名
+  unitContact?: string; // 单位联系人
+  officePhone?: string; // 办公电话
+  mobilePhone?: string; // 移动电话
+  contactPhone?: string; // 联系电话(保留兼容性)
+  documentName?: string; // 申领证件名称
+  documentNumber?: string; // 证件号码
+  destinationCountry?: string; // 前往国家
+  exitReason?: string; // 出境事由
+  visitRelationship?: string; // 探望对象姓名及与申请者关系
+  remarks?: string; // 备注
+  emergencyContact?: string; // 应急联系人及电话
+  signature?: string; // 手写签字(base64格式)
+  stampedRegistrationFilePath?: string; // 登记备案表盖章文件路径
+  stampedRegistrationFileName?: string; // 登记备案表盖章文件名
+  stampedFileUploadTime?: string; // 盖章文件上传时间
+  stampedFileUploadStatus?: string; // 盖章文件上传状态:0-未上传,1-已上传
+  partyRetentionFilePath?: string; // 党籍保留附件文件路径
+  partyRetentionFileName?: string; // 党籍保留附件文件名
+  partyRetentionUploadTime?: string; // 党籍保留附件上传时间
+  partyRetentionUploadStatus?: string; // 党籍保留附件上传状态:0-未上传,1-已上传
   purpose: string;
   purposeDetail: string;
   destination: string;
@@ -111,7 +174,9 @@ export interface OverseasRecordVO {
   applicantId: number;
   applicantName: string;
   department: string;
+  position: string;
   contactPhone: string;
+  contactEmail: string;
   purpose: string;
   purposeDetail: string;
   destination: string;
@@ -134,7 +199,23 @@ export interface OverseasRecordVO {
   updateBy: string;
   // 显示字段
   purposeText: string;
+  status?: string; // 申请状态(来自申请表)
+  statusText?: string; // 申请状态文本(来自申请表)
   completionStatusText: string;
+  // 来自申请表的扩展字段
+  gender?: string;
+  genderText?: string;
+  birthDate?: string;
+  politicalStatus?: string;
+  politicalStatusText?: string;
+  professionalTitle?: string;
+  documentName?: string;
+  documentNumber?: string;
+  mobilePhone?: string;
+  destinationCountry?: string;
+  exitReason?: string;
+  emergencyContact?: string;
+  remarks?: string;
 }
 
 // 出国记录表单类型
@@ -255,7 +336,7 @@ export function listOverseasApplicationByStatus(status: string) {
 // 查询出国申请详细
 export function getOverseasApplication(applicationId: string | number) {
   return request({
-    url: `/rs/overseas/application/${applicationId}`,
+    url: `/rs/overseas/application/detail/${applicationId}`,
     method: 'get'
   });
 }
@@ -295,6 +376,18 @@ export function resubmitOverseasApplication(applicationId: string | number, data
   });
 }
 
+// 校验出国申请前置条件
+export function validateApplicationPrerequisites(politicalStatus?: string, duration?: number) {
+  return request({
+    url: '/rs/overseas/application/validatePrerequisites',
+    method: 'get',
+    params: {
+      politicalStatus,
+      duration
+    }
+  });
+}
+
 // 取消出国申请
 export function cancelOverseasApplication(applicationId: string | number, reason?: string) {
   return request({
@@ -520,3 +613,153 @@ export function fixApprovedOverseasRecords() {
     method: 'post'
   });
 }
+
+// ==================== PDF生成相关接口 ====================
+
+// 生成登记表请求参数类型
+export interface GenerateRegistrationFormRequest {
+  applicationId: number;
+  documentNumber?: string;
+  format?: string;
+}
+
+// 登记表生成响应类型
+export interface RegistrationFormResponse {
+  documentId: string;
+  fileName: string;
+  downloadUrl: string;
+  previewUrl: string;
+  fileSize: number;
+  generateTime: string;
+  documentNumber: string;
+  applicationId: number;
+}
+
+// 生成出国申请登记表PDF
+export function generateRegistrationForm(data: GenerateRegistrationFormRequest) {
+  return request({
+    url: '/rs/overseas/application/generateRegistrationForm',
+    method: 'post',
+    data: data
+  });
+}
+
+// 下载生成的PDF文档
+export function downloadRegistrationFormPdf(documentId: string) {
+  return request({
+    url: `/rs/overseas/application/downloadPdf/${documentId}`,
+    method: 'get',
+    responseType: 'blob'
+  });
+}
+
+// 预览生成的PDF文档
+export function previewRegistrationFormPdf(documentId: string) {
+  return request({
+    url: `/rs/overseas/application/previewPdf/${documentId}`,
+    method: 'get',
+    responseType: 'blob'
+  });
+}
+
+// 下载登记备案附件模板
+export function downloadRegistrationTemplate() {
+  return request({
+    url: '/rs/overseas/application/downloadRegistrationTemplate',
+    method: 'get',
+    responseType: 'blob'
+  });
+}
+
+// 下载保留党籍申请模板
+export function downloadPartyMembershipTemplate() {
+  return request({
+    url: '/rs/overseas/application/downloadPartyMembershipTemplate',
+    method: 'get',
+    responseType: 'blob'
+  });
+}
+
+// ==================== 出国申请附件相关API ====================
+
+// 附件数据类型
+export interface OverseasAttachmentVO {
+  attachmentId: number | string;
+  applicationId: number;
+  attachmentTitle: string;
+  attachmentContent?: string;
+  fileName: string;
+  fileOriginalName: string;
+  filePath: string;
+  fileSize: number;
+  fileSizeFormatted?: string;
+  fileType?: string;
+  mimeType?: string;
+  uploadTime?: string;
+  uploadUserId?: number;
+  uploadUserName?: string;
+  createTime?: string;
+  updateTime?: string;
+}
+
+// 附件BO
+export interface OverseasAttachmentBo {
+  attachmentId?: number | string;
+  applicationId: number;
+  attachmentTitle: string;
+  attachmentContent?: string;
+}
+
+// 查询出国申请的附件列表
+export function listOverseasAttachments(applicationId: string | number) {
+  return request({
+    url: `/rs/overseas/attachment/list/${applicationId}`,
+    method: 'get'
+  });
+}
+
+// 查询附件详情
+export function getOverseasAttachment(attachmentId: string | number) {
+  return request({
+    url: `/rs/overseas/attachment/${attachmentId}`,
+    method: 'get'
+  });
+}
+
+// 上传附件
+export function uploadOverseasAttachment(data: FormData) {
+  return request({
+    url: '/rs/overseas/attachment/upload',
+    method: 'post',
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    },
+    data: data
+  });
+}
+
+// 更新附件信息(标题和内容)
+export function updateOverseasAttachment(data: OverseasAttachmentBo) {
+  return request({
+    url: '/rs/overseas/attachment',
+    method: 'put',
+    data: data
+  });
+}
+
+// 删除附件
+export function deleteOverseasAttachment(attachmentId: string | number) {
+  return request({
+    url: `/rs/overseas/attachment/${attachmentId}`,
+    method: 'delete'
+  });
+}
+
+// 批量删除附件
+export function deleteOverseasAttachments(attachmentIds: Array<string | number>) {
+  return request({
+    url: '/rs/overseas/attachment/batch',
+    method: 'delete',
+    data: attachmentIds
+  });
+}

+ 0 - 6
web/src/views/rs/index.vue

@@ -75,7 +75,6 @@
                     <el-tag type="info">{{ getDepartmentLabel(scope.row.department) }}</el-tag>
                   </template>
                 </el-table-column>
-                <el-table-column label="薪资范围" align="center" prop="salaryRange" />
                 <el-table-column label="工作地点" align="center" prop="location" />
                 <el-table-column label="状态" align="center" prop="isActive" width="120">
                   <template #default="scope">
@@ -316,9 +315,6 @@
             <el-option v-for="dict in departmentOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
           </el-select>
         </el-form-item>
-        <el-form-item label="薪资范围" prop="salaryRange">
-          <el-input v-model="form.salaryRange" placeholder="请输入薪资范围,如:15k-25k" />
-        </el-form-item>
         <el-form-item label="工作地点" prop="location">
           <el-input v-model="form.location" placeholder="请输入工作地点" />
         </el-form-item>
@@ -376,7 +372,6 @@
       <el-descriptions :column="2" border>
         <el-descriptions-item label="职位名称">{{ viewForm.title }}</el-descriptions-item>
         <el-descriptions-item label="部门">{{ getDepartmentLabel(viewForm.department) }}</el-descriptions-item>
-        <el-descriptions-item label="薪资范围">{{ viewForm.salaryRange }}</el-descriptions-item>
         <el-descriptions-item label="工作地点">{{ viewForm.location }}</el-descriptions-item>
         <el-descriptions-item label="状态">
           <el-tag :type="viewForm.isActive === '0' ? 'success' : 'info'">
@@ -818,7 +813,6 @@ const rules = reactive<Record<string, any>>({
     { min: 2 as number, max: 50 as number, message: '长度在 2 到 50 个字符', trigger: 'blur' }
   ],
   department: [{ required: true, message: '部门不能为空', trigger: 'change' }],
-  salaryRange: [{ required: true, message: '薪资范围不能为空', trigger: 'blur' }],
   location: [{ required: true, message: '工作地点不能为空', trigger: 'blur' }]
 });
 

+ 55 - 24
web/src/views/rs/overseas/approval.vue

@@ -70,20 +70,20 @@
 
               <!-- 待审批申请表格 -->
               <el-table v-loading="pendingLoading" border :data="pendingList" @row-dblclick="handleViewApplication" v-hasPermi="['rs:overseas:list']">
-                <el-table-column label="申请人" align="center" prop="applicantName" width="100" :show-overflow-tooltip="true" />
-                <el-table-column label="所属部门" align="center" prop="department" width="140" :show-overflow-tooltip="true" />
-                <el-table-column label="出国目的" align="center" prop="purposeText" width="100" :show-overflow-tooltip="true" />
-                <el-table-column label="目的地" align="center" prop="destination" width="120" :show-overflow-tooltip="true" />
-                <el-table-column label="出国时间" align="center" min-width="220" :show-overflow-tooltip="true">
-                  <template #default="scope"> {{ formatDate(scope.row.startDate) }} 至 {{ formatDate(scope.row.endDate) }} </template>
+                <el-table-column label="姓名" align="center" prop="applicantName" min-width="100" :show-overflow-tooltip="true" />
+                <el-table-column label="现工作部门" align="center" prop="department" min-width="140" :show-overflow-tooltip="true" />
+                <el-table-column label="职称" align="center" prop="professionalTitle" min-width="110" :show-overflow-tooltip="true" />
+                <el-table-column label="前往国家" align="center" prop="destination" min-width="110" :show-overflow-tooltip="true" />
+                <el-table-column label="出境事由" align="center" prop="exitReason" min-width="100" :show-overflow-tooltip="true" />
+                <el-table-column label="出国天数" align="center" prop="duration" min-width="90">
+                  <template #default="scope">{{ scope.row.duration || '-' }}天</template>
                 </el-table-column>
-                <el-table-column label="天数" align="center" prop="duration" width="60" />
-                <el-table-column label="申请时间" align="center" prop="applyTime" width="160" :show-overflow-tooltip="true" />
-                <el-table-column label="申请状态" align="center" width="100">
+                <el-table-column label="申请状态" align="center" prop="statusText" min-width="100">
                   <template #default="scope">
                     <el-tag :type="getStatusTagType(scope.row.status)">{{ scope.row.statusText }}</el-tag>
                   </template>
                 </el-table-column>
+                <el-table-column label="申请时间" align="center" prop="applyTime" min-width="160" :show-overflow-tooltip="true" />
                 <el-table-column label="操作" align="center" width="180">
                   <template #default="scope">
                     <el-tooltip content="查看详情" placement="top">
@@ -237,25 +237,56 @@
     </el-card>
 
     <!-- 申请详情对话框 -->
-    <el-dialog title="申请详情" v-model="viewDialog.visible" width="800px" append-to-body>
+    <el-dialog title="出国申请详情" v-model="viewDialog.visible" width="900px" append-to-body>
       <el-descriptions v-if="viewData" :column="2" border>
-        <el-descriptions-item label="申请人">{{ viewData.applicantName }}</el-descriptions-item>
-        <el-descriptions-item label="所属部门">{{ viewData.department }}</el-descriptions-item>
-        <el-descriptions-item label="联系电话">{{ viewData.contactPhone }}</el-descriptions-item>
-
-        <el-descriptions-item label="出国目的">{{ viewData.purposeText }}</el-descriptions-item>
-        <el-descriptions-item label="目的地">{{ viewData.destination }}</el-descriptions-item>
-        <el-descriptions-item label="开始日期">{{ formatDate(viewData.startDate) }}</el-descriptions-item>
-        <el-descriptions-item label="结束日期">{{ formatDate(viewData.endDate) }}</el-descriptions-item>
-        <el-descriptions-item label="出国天数">{{ viewData.duration }}天</el-descriptions-item>
-        <el-descriptions-item label="预算金额">¥{{ formatMoney(viewData.budget) }}</el-descriptions-item>
+        <!-- 基本信息 -->
+        <el-descriptions-item label="姓名">{{ viewData.applicantName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="性别">{{ viewData.gender === '0' ? '男' : viewData.gender === '1' ? '女' : '-' }}</el-descriptions-item>
+        
+        <el-descriptions-item label="出生年月">{{ formatDate(viewData.birthDate) || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="政治面貌">{{ viewData.politicalStatusText || '-' }}</el-descriptions-item>
+        
+        <!-- 工作信息 -->
+        <el-descriptions-item label="现工作部门">{{ viewData.department || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="职称">{{ viewData.professionalTitle || '-' }}</el-descriptions-item>
+        
+        <el-descriptions-item label="职务">{{ viewData.position || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="申领证件名称">{{ viewData.documentName || '-' }}</el-descriptions-item>
+        
+        <!-- 证件信息 -->
+        <el-descriptions-item label="证件号码">{{ viewData.documentNumber || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="前往国家">{{ viewData.destinationCountry || viewData.destination || '-' }}</el-descriptions-item>
+        
+        <!-- 出境信息 -->
+        <el-descriptions-item label="出境事由">{{ viewData.exitReason || '-' }}</el-descriptions-item>
+        <!-- 探亲信息(如果是探亲) -->
+        <el-descriptions-item label="探望对象关系" v-if="viewData.exitReason === '探亲'">
+          {{ viewData.visitRelationship || '-' }}
+        </el-descriptions-item>
+        
+        <!-- 行程安排 -->
+        <el-descriptions-item label="行程安排" :span="2">{{ viewData.itinerary || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="出国天数">{{ viewData.duration || 0 }}天</el-descriptions-item>
+        
+        <!-- 备注 -->
+        <el-descriptions-item label="备注" v-if="viewData.remarks">{{ viewData.remarks }}</el-descriptions-item>
+        
+        <!-- 出国时间 -->
+        <el-descriptions-item label="出发日期">{{ formatDate(viewData.startDate) || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="返回日期">{{ formatDate(viewData.endDate) || '-' }}</el-descriptions-item>
+        
+        <!-- 联系信息 -->
+        <el-descriptions-item label="联系电话">{{ viewData.mobilePhone || viewData.contactPhone || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="应急联系人及电话">{{ viewData.emergencyContact || '-' }}</el-descriptions-item>
+        
+        <!-- 申请状态 -->
         <el-descriptions-item label="申请状态">
           <el-tag :type="getStatusTagType(viewData.status)">{{ viewData.statusText }}</el-tag>
         </el-descriptions-item>
-        <el-descriptions-item label="申请时间" :span="2">{{ viewData.applyTime }}</el-descriptions-item>
-        <el-descriptions-item label="目的详细说明" :span="2">{{ viewData.purposeDetail }}</el-descriptions-item>
-        <el-descriptions-item label="行程安排" :span="2">{{ viewData.itinerary }}</el-descriptions-item>
-        <el-descriptions-item label="申请人备注" :span="2" v-if="viewData.applicantRemark">{{ viewData.applicantRemark }}</el-descriptions-item>
+        <el-descriptions-item label="申请时间">{{ viewData.applyTime || '-' }}</el-descriptions-item>
+        
+        <!-- 管理员备注 -->
+        <el-descriptions-item label="管理员备注" :span="2" v-if="viewData.adminRemark">{{ viewData.adminRemark }}</el-descriptions-item>
       </el-descriptions>
 
       <!-- 查看审批记录 -->

ファイルの差分が大きいため隠しています
+ 831 - 97
web/src/views/rs/overseas/index.vue


+ 62 - 18
web/src/views/rs/training/index.vue

@@ -254,7 +254,7 @@
                 </template>
               </el-table-column>
 
-              <el-table-column label="操作" align="center" width="200">
+              <el-table-column label="操作" align="center" width="150">
                 <template #default="scope">
                   <el-tooltip content="查看" placement="top">
                     <el-button
@@ -265,15 +265,6 @@
                       v-hasPermi="['rs:trainingrecord:query']"
                     ></el-button>
                   </el-tooltip>
-                  <el-tooltip content="编辑" placement="top">
-                    <el-button
-                      link
-                      type="primary"
-                      icon="Edit"
-                      @click.stop="handleUpdateRecord(scope.row)"
-                      v-hasPermi="['rs:trainingrecord:edit']"
-                    ></el-button>
-                  </el-tooltip>
                   <el-tooltip content="删除" placement="top">
                     <el-button
                       link
@@ -631,14 +622,11 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="同行人" prop="companions">
-              <el-select v-model="applicationForm.companions" multiple placeholder="请选择同行人员" style="width: 100%" clearable filterable>
-                <el-option
-                  v-for="teacher in teacherList"
-                  :key="teacher.teacherId"
-                  :label="teacher.teacherName"
-                  :value="teacher.teacherId"
-                />
-              </el-select>
+              <TeacherMultiSelector 
+                v-model="selectedCompanions" 
+                placeholder="请选择同行人员" 
+                @change="handleCompanionsChange"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -1035,6 +1023,8 @@ import { getToken } from '@/utils/auth';
 import { getInfo as getUserInfo } from '@/api/login';
 import { useUserStore } from '@/store/modules/user';
 import { listClassTeacherCounselorRel } from '@/api/xg/classTeacherCounselorRel';
+import TeacherMultiSelector from '@/components/TeacherMultiSelector/index.vue';
+import { TeacherVO } from '@/api/system/teacher/types';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const userStore = useUserStore();
@@ -1392,6 +1382,9 @@ const applicationForm = reactive<{
   attachmentIds: [] as string[] // 附件ID列表
 });
 
+// 同行人选择器相关
+const selectedCompanions = ref<TeacherVO[]>([]);
+
 // 培训申请表单校验规则
 const applicationRules = reactive<any>({
   // 情况基本信息验证
@@ -1863,9 +1856,57 @@ function resetApplicationForm() {
   applicationForm.reason = '';
   applicationForm.attachmentIds = [];
   applicationAttachmentFileList.value = [];
+  selectedCompanions.value = [];
   applicationFormRef.value?.resetFields();
 }
 
+/** 处理同行人变化 */
+function handleCompanionsChange(teachers: TeacherVO[]) {
+  selectedCompanions.value = teachers;
+  applicationForm.companions = teachers.map(teacher => teacher.userId);
+}
+
+/** 加载编辑时的同行人数据 */
+async function loadCompanionsForEdit() {
+  if (applicationForm.companions && applicationForm.companions.length > 0) {
+    try {
+      // 这里需要根据userId获取教师信息
+      // 由于TeacherMultiSelector组件需要完整的TeacherVO对象
+      // 我们需要调用API获取教师详细信息
+      const teacherPromises = applicationForm.companions.map(async (userId) => {
+        try {
+          // 这里需要调用获取教师详情的API
+          // 暂时使用teacherList中的数据进行匹配
+          const teacher = teacherList.value.find(t => t.teacherId === userId);
+          if (teacher) {
+            return {
+              userId: teacher.teacherId,
+              nickName: teacher.teacherName,
+              userName: teacher.teacherName,
+              phonenumber: '',
+              email: '',
+              sex: '0',
+              deptName: ''
+            } as TeacherVO;
+          }
+          return null;
+        } catch (error) {
+          console.error('获取教师信息失败:', error);
+          return null;
+        }
+      });
+      
+      const teachers = await Promise.all(teacherPromises);
+      selectedCompanions.value = teachers.filter(t => t !== null) as TeacherVO[];
+    } catch (error) {
+      console.error('加载同行人数据失败:', error);
+      selectedCompanions.value = [];
+    }
+  } else {
+    selectedCompanions.value = [];
+  }
+}
+
 /** 取消培训申请对话框 */
 function cancelApplicationDialog() {
   applicationDialogVisible.value = false;
@@ -2161,6 +2202,9 @@ function handleEditApplication(row: any) {
 
   // 保存当前编辑的申请ID
   applicationForm.applicationId = row.applicationId;
+  
+  // 加载同行人数据到选择器
+  loadCompanionsForEdit();
 }
 
 /** 撤回培训申请 */

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません