diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java index 3a4b7351..4e800f6c 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java @@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentAddDTO; import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentEditDTO; import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentQueryDTO; import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel; +import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel; import com.ruoyi.info.collection.domain.vo.CcdiStaffRecruitmentVO; import com.ruoyi.info.collection.domain.vo.ImportResultVO; import com.ruoyi.info.collection.domain.vo.ImportStatusVO; @@ -128,6 +129,15 @@ public class CcdiStaffRecruitmentController extends BaseController { EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffRecruitmentExcel.class, "员工招聘信息"); } + /** + * 下载历史工作经历导入模板 + */ + @Operation(summary = "下载历史工作经历导入模板") + @PostMapping("/workImportTemplate") + public void workImportTemplate(HttpServletResponse response) { + EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffRecruitmentWorkExcel.class, "历史工作经历"); + } + /** * 异步导入招聘信息 */ @@ -155,6 +165,31 @@ public class CcdiStaffRecruitmentController extends BaseController { return AjaxResult.success("导入任务已提交,正在后台处理", result); } + /** + * 异步导入历史工作经历 + */ + @Operation(summary = "异步导入历史工作经历") + @Parameter(name = "file", description = "导入文件", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:import')") + @Log(title = "员工招聘历史工作经历", businessType = BusinessType.IMPORT) + @PostMapping("/importWorkData") + public AjaxResult importWorkData(@Parameter(description = "导入文件") MultipartFile file) throws Exception { + List list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffRecruitmentWorkExcel.class); + + if (list == null || list.isEmpty()) { + return error("至少需要一条数据"); + } + + String taskId = recruitmentService.importRecruitmentWork(list); + + ImportResultVO result = new ImportResultVO(); + result.setTaskId(taskId); + result.setStatus("PROCESSING"); + result.setMessage("历史工作经历导入任务已提交,正在后台处理"); + + return AjaxResult.success("历史工作经历导入任务已提交,正在后台处理", result); + } + /** * 查询导入状态 */ diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitment.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitment.java index 4fc163a2..cd8b71d8 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitment.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitment.java @@ -22,7 +22,7 @@ public class CcdiStaffRecruitment implements Serializable { @Serial private static final long serialVersionUID = 1L; - /** 招聘项目编号 */ + /** 招聘记录编号 */ @TableId(type = IdType.INPUT) private String recruitId; @@ -41,6 +41,9 @@ public class CcdiStaffRecruitment implements Serializable { /** 应聘人员姓名 */ private String candName; + /** 招聘类型:SOCIAL-社招,CAMPUS-校招 */ + private String recruitType; + /** 应聘人员学历 */ private String candEdu; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitmentWork.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitmentWork.java new file mode 100644 index 00000000..c78fa10c --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitmentWork.java @@ -0,0 +1,76 @@ +package com.ruoyi.info.collection.domain; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 招聘记录历史工作经历对象 ccdi_staff_recruitment_work + * + * @author ruoyi + * @date 2026-04-15 + */ +@Data +@TableName("ccdi_staff_recruitment_work") +public class CcdiStaffRecruitmentWork implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键 */ + @TableId(type = IdType.AUTO) + private Long id; + + /** 关联招聘记录编号 */ + private String recruitId; + + /** 排序号 */ + private Integer sortOrder; + + /** 工作单位 */ + private String companyName; + + /** 所属部门 */ + private String departmentName; + + /** 岗位名称 */ + private String positionName; + + /** 入职年月 */ + private String jobStartMonth; + + /** 离职年月 */ + private String jobEndMonth; + + /** 离职原因 */ + private String departureReason; + + /** 主要工作内容 */ + private String workContent; + + /** 备注 */ + private String remark; + + /** 创建人 */ + @TableField(fill = FieldFill.INSERT) + private String createdBy; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + /** 更新人 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updatedBy; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java index 7eed1ce1..b9338ffa 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java @@ -2,6 +2,7 @@ package com.ruoyi.info.collection.domain.dto; import com.ruoyi.info.collection.annotation.EnumValid; import com.ruoyi.info.collection.enums.AdmitStatus; +import com.ruoyi.info.collection.enums.RecruitType; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @@ -22,9 +23,9 @@ public class CcdiStaffRecruitmentAddDTO implements Serializable { @Serial private static final long serialVersionUID = 1L; - /** 招聘项目编号 */ - @NotBlank(message = "招聘项目编号不能为空") - @Size(max = 32, message = "招聘项目编号长度不能超过32个字符") + /** 招聘记录编号 */ + @NotBlank(message = "招聘记录编号不能为空") + @Size(max = 32, message = "招聘记录编号长度不能超过32个字符") private String recruitId; /** 招聘项目名称 */ @@ -51,6 +52,11 @@ public class CcdiStaffRecruitmentAddDTO implements Serializable { @Size(max = 20, message = "应聘人员姓名长度不能超过20个字符") private String candName; + /** 招聘类型 */ + @NotBlank(message = "招聘类型不能为空") + @EnumValid(enumClass = RecruitType.class, message = "招聘类型状态值不合法") + private String recruitType; + /** 应聘人员学历 */ @NotBlank(message = "应聘人员学历不能为空") @Size(max = 20, message = "应聘人员学历长度不能超过20个字符") diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentEditDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentEditDTO.java index 8c050f97..ded9c315 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentEditDTO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentEditDTO.java @@ -2,6 +2,7 @@ package com.ruoyi.info.collection.domain.dto; import com.ruoyi.info.collection.annotation.EnumValid; import com.ruoyi.info.collection.enums.AdmitStatus; +import com.ruoyi.info.collection.enums.RecruitType; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; @@ -23,8 +24,8 @@ public class CcdiStaffRecruitmentEditDTO implements Serializable { @Serial private static final long serialVersionUID = 1L; - /** 招聘项目编号 */ - @NotNull(message = "招聘项目编号不能为空") + /** 招聘记录编号 */ + @NotNull(message = "招聘记录编号不能为空") private String recruitId; /** 招聘项目名称 */ @@ -46,6 +47,10 @@ public class CcdiStaffRecruitmentEditDTO implements Serializable { @Size(max = 20, message = "应聘人员姓名长度不能超过20个字符") private String candName; + /** 招聘类型 */ + @EnumValid(enumClass = RecruitType.class, message = "招聘类型状态值不合法") + private String recruitType; + /** 应聘人员学历 */ @Size(max = 20, message = "应聘人员学历长度不能超过20个字符") private String candEdu; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentQueryDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentQueryDTO.java index dac83e21..719b74cf 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentQueryDTO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentQueryDTO.java @@ -26,6 +26,9 @@ public class CcdiStaffRecruitmentQueryDTO implements Serializable { /** 候选人姓名(模糊查询) */ private String candName; + /** 招聘类型(精确查询) */ + private String recruitType; + /** 证件号码(精确查询) */ private String candId; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiStaffRecruitmentWorkExcel.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiStaffRecruitmentWorkExcel.java new file mode 100644 index 00000000..2ee6ee9b --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiStaffRecruitmentWorkExcel.java @@ -0,0 +1,95 @@ +package com.ruoyi.info.collection.domain.excel; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import com.ruoyi.common.annotation.Required; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 招聘记录历史工作经历Excel导入对象 + * + * @author ruoyi + * @date 2026-04-20 + */ +@Data +public class CcdiStaffRecruitmentWorkExcel implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 招聘记录编号 */ + @ExcelProperty(value = "招聘记录编号", index = 0) + @ColumnWidth(20) + @Required + private String recruitId; + + /** 候选人姓名 */ + @ExcelProperty(value = "候选人姓名", index = 1) + @ColumnWidth(15) + @Required + private String candName; + + /** 招聘项目名称 */ + @ExcelProperty(value = "招聘项目名称", index = 2) + @ColumnWidth(25) + @Required + private String recruitName; + + /** 职位名称 */ + @ExcelProperty(value = "职位名称", index = 3) + @ColumnWidth(20) + @Required + private String posName; + + /** 排序号 */ + @ExcelProperty(value = "排序号", index = 4) + @ColumnWidth(10) + @Required + private Integer sortOrder; + + /** 工作单位 */ + @ExcelProperty(value = "工作单位", index = 5) + @ColumnWidth(25) + @Required + private String companyName; + + /** 所属部门 */ + @ExcelProperty(value = "所属部门", index = 6) + @ColumnWidth(18) + private String departmentName; + + /** 岗位 */ + @ExcelProperty(value = "岗位", index = 7) + @ColumnWidth(20) + @Required + private String positionName; + + /** 入职年月 */ + @ExcelProperty(value = "入职年月", index = 8) + @ColumnWidth(12) + @Required + private String jobStartMonth; + + /** 离职年月 */ + @ExcelProperty(value = "离职年月", index = 9) + @ColumnWidth(12) + private String jobEndMonth; + + /** 离职原因 */ + @ExcelProperty(value = "离职原因", index = 10) + @ColumnWidth(30) + private String departureReason; + + /** 工作内容 */ + @ExcelProperty(value = "工作内容", index = 11) + @ColumnWidth(35) + private String workContent; + + /** 备注 */ + @ExcelProperty(value = "备注", index = 12) + @ColumnWidth(25) + private String remark; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentVO.java index 78bee51c..3afe0cbc 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentVO.java @@ -5,6 +5,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; import java.util.Date; +import java.util.List; /** * 员工招聘信息VO @@ -18,7 +19,7 @@ public class CcdiStaffRecruitmentVO implements Serializable { @Serial private static final long serialVersionUID = 1L; - /** 招聘项目编号 */ + /** 招聘记录编号 */ private String recruitId; /** 招聘项目名称 */ @@ -36,6 +37,9 @@ public class CcdiStaffRecruitmentVO implements Serializable { /** 应聘人员姓名 */ private String candName; + /** 招聘类型 */ + private String recruitType; + /** 应聘人员学历 */ private String candEdu; @@ -57,6 +61,12 @@ public class CcdiStaffRecruitmentVO implements Serializable { /** 录用情况描述 */ private String admitStatusDesc; + /** 历史工作经历条数 */ + private Long workExperienceCount; + + /** 历史工作经历列表 */ + private List workExperienceList; + /** 面试官1姓名 */ private String interviewerName1; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentWorkVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentWorkVO.java new file mode 100644 index 00000000..6605238a --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentWorkVO.java @@ -0,0 +1,46 @@ +package com.ruoyi.info.collection.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 招聘记录历史工作经历VO + * + * @author ruoyi + * @date 2026-04-15 + */ +@Data +public class CcdiStaffRecruitmentWorkVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 排序号 */ + private Integer sortOrder; + + /** 工作单位 */ + private String companyName; + + /** 所属部门 */ + private String departmentName; + + /** 岗位名称 */ + private String positionName; + + /** 入职年月 */ + private String jobStartMonth; + + /** 离职年月 */ + private String jobEndMonth; + + /** 离职原因 */ + private String departureReason; + + /** 主要工作内容 */ + private String workContent; + + /** 备注 */ + private String remark; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java index 4f48e27f..4775dfff 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java @@ -19,6 +19,9 @@ public class RecruitmentImportFailureVO { @Schema(description = "招聘项目名称") private String recruitName; + @Schema(description = "职位名称") + private String posName; + @Schema(description = "应聘人员姓名") private String candName; @@ -28,6 +31,12 @@ public class RecruitmentImportFailureVO { @Schema(description = "录用情况") private String admitStatus; + @Schema(description = "工作单位") + private String companyName; + + @Schema(description = "岗位") + private String positionName; + @Schema(description = "错误信息") private String errorMessage; } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/RecruitType.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/RecruitType.java new file mode 100644 index 00000000..33e0741a --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/RecruitType.java @@ -0,0 +1,49 @@ +package com.ruoyi.info.collection.enums; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 招聘类型枚举 + * + * @author ruoyi + */ +public enum RecruitType { + + /** 社招 */ + SOCIAL("SOCIAL", "社招"), + + /** 校招 */ + CAMPUS("CAMPUS", "校招"); + + private final String code; + private final String desc; + + RecruitType(String code, String desc) { + this.code = code; + this.desc = desc; + } + + public String getCode() { + return code; + } + + public String getDesc() { + return desc; + } + + public static String getDescByCode(String code) { + for (RecruitType type : values()) { + if (type.code.equals(code)) { + return type.desc; + } + } + return null; + } + + public static String inferCode(String recruitName) { + if (StringUtils.isNotEmpty(recruitName) && recruitName.contains("校园")) { + return CAMPUS.code; + } + return SOCIAL.code; + } +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiStaffRecruitmentWorkMapper.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiStaffRecruitmentWorkMapper.java new file mode 100644 index 00000000..ca54dcbd --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiStaffRecruitmentWorkMapper.java @@ -0,0 +1,13 @@ +package com.ruoyi.info.collection.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork; + +/** + * 招聘记录历史工作经历 数据层 + * + * @author ruoyi + * @date 2026-04-15 + */ +public interface CcdiStaffRecruitmentWorkMapper extends BaseMapper { +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java index cc250511..cb1f251e 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java @@ -1,6 +1,7 @@ package com.ruoyi.info.collection.service; import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel; +import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel; import com.ruoyi.info.collection.domain.vo.ImportStatusVO; import com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO; @@ -25,6 +26,17 @@ public interface ICcdiStaffRecruitmentImportService { String taskId, String userName); + /** + * 异步导入招聘记录历史工作经历数据 + * + * @param excelList Excel数据列表 + * @param taskId 任务ID + * @param userName 用户名 + */ + void importRecruitmentWorkAsync(List excelList, + String taskId, + String userName); + /** * 查询导入状态 * diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java index bbf8a694..eaa09ab3 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java @@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentAddDTO; import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentEditDTO; import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentQueryDTO; import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel; +import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel; import com.ruoyi.info.collection.domain.vo.CcdiStaffRecruitmentVO; import java.util.List; @@ -81,4 +82,12 @@ public interface ICcdiStaffRecruitmentService { * @return 结果 */ String importRecruitment(List excelList); + + /** + * 导入招聘记录历史工作经历数据(异步) + * + * @param excelList Excel实体列表 + * @return 任务ID + */ + String importRecruitmentWork(List excelList); } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java index c43c2373..697c3a84 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java @@ -3,13 +3,17 @@ package com.ruoyi.info.collection.service.impl; import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.info.collection.domain.CcdiStaffRecruitment; +import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork; import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentAddDTO; import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel; +import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel; import com.ruoyi.info.collection.domain.vo.ImportResult; import com.ruoyi.info.collection.domain.vo.ImportStatusVO; import com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO; import com.ruoyi.info.collection.enums.AdmitStatus; +import com.ruoyi.info.collection.enums.RecruitType; import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentMapper; +import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentWorkMapper; import com.ruoyi.info.collection.service.ICcdiStaffRecruitmentImportService; import com.ruoyi.info.collection.utils.ImportLogUtils; import com.ruoyi.common.utils.IdCardUtil; @@ -43,6 +47,9 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm @Resource private CcdiStaffRecruitmentMapper recruitmentMapper; + @Resource + private CcdiStaffRecruitmentWorkMapper recruitmentWorkMapper; + @Resource private RedisTemplate redisTemplate; @@ -60,10 +67,10 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm List newRecords = new ArrayList<>(); List failures = new ArrayList<>(); - // 批量查询已存在的招聘项目编号 - ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的招聘项目编号", excelList.size()); + // 批量查询已存在的招聘记录编号 + ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的招聘记录编号", excelList.size()); Set existingRecruitIds = getExistingRecruitIds(excelList); - ImportLogUtils.logBatchQueryComplete(log, taskId, "招聘项目编号", existingRecruitIds.size()); + ImportLogUtils.logBatchQueryComplete(log, taskId, "招聘记录编号", existingRecruitIds.size()); // 用于检测Excel内部的重复ID Set excelProcessedIds = new HashSet<>(); @@ -76,19 +83,21 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm // 转换为AddDTO进行验证 CcdiStaffRecruitmentAddDTO addDTO = new CcdiStaffRecruitmentAddDTO(); BeanUtils.copyProperties(excel, addDTO); + addDTO.setRecruitType(RecruitType.inferCode(addDTO.getRecruitName())); // 验证数据 validateRecruitmentData(addDTO, existingRecruitIds); CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment(); BeanUtils.copyProperties(excel, recruitment); + recruitment.setRecruitType(addDTO.getRecruitType()); if (existingRecruitIds.contains(excel.getRecruitId())) { - // 招聘项目编号在数据库中已存在,直接报错 - throw new RuntimeException(String.format("招聘项目编号[%s]已存在,请勿重复导入", excel.getRecruitId())); + // 招聘记录编号在数据库中已存在,直接报错 + throw new RuntimeException(String.format("招聘记录编号[%s]已存在,请勿重复导入", excel.getRecruitId())); } else if (excelProcessedIds.contains(excel.getRecruitId())) { - // 招聘项目编号在Excel文件内部重复 - throw new RuntimeException(String.format("招聘项目编号[%s]在导入文件中重复,已跳过此条记录", excel.getRecruitId())); + // 招聘记录编号在Excel文件内部重复 + throw new RuntimeException(String.format("招聘记录编号[%s]在导入文件中重复,已跳过此条记录", excel.getRecruitId())); } else { recruitment.setCreatedBy(userName); recruitment.setUpdatedBy(userName); @@ -107,7 +116,7 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm failures.add(failure); // 记录验证失败日志 - String keyData = String.format("招聘项目编号=%s, 项目名称=%s, 应聘人员=%s", + String keyData = String.format("招聘记录编号=%s, 项目名称=%s, 应聘人员=%s", excel.getRecruitId(), excel.getRecruitName(), excel.getCandName()); ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData); } @@ -142,7 +151,85 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm // 记录导入完成 long duration = System.currentTimeMillis() - startTime; - ImportLogUtils.logImportComplete(log, taskId, "招聘信息", + ImportLogUtils.logImportComplete(log, taskId, "招聘信息", + excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration); + } + + @Override + @Async + @Transactional + public void importRecruitmentWorkAsync(List excelList, + String taskId, + String userName) { + long startTime = System.currentTimeMillis(); + ImportLogUtils.logImportStart(log, taskId, "招聘历史工作经历", excelList.size(), userName); + + List failures = new ArrayList<>(); + List validRecords = new ArrayList<>(); + Set failedRecruitIds = new HashSet<>(); + Set processedRecruitSortKeys = new HashSet<>(); + + Map recruitmentMap = getRecruitmentMap(excelList); + + for (int i = 0; i < excelList.size(); i++) { + CcdiStaffRecruitmentWorkExcel excel = excelList.get(i); + try { + CcdiStaffRecruitment recruitment = recruitmentMap.get(trim(excel.getRecruitId())); + validateRecruitmentWorkData(excel, recruitment, processedRecruitSortKeys); + + CcdiStaffRecruitmentWork work = new CcdiStaffRecruitmentWork(); + BeanUtils.copyProperties(excel, work); + work.setRecruitId(trim(excel.getRecruitId())); + work.setCreatedBy(userName); + work.setUpdatedBy(userName); + validRecords.add(work); + + ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(), + validRecords.size(), failures.size()); + } catch (Exception e) { + failedRecruitIds.add(trim(excel.getRecruitId())); + failures.add(buildWorkFailure(excel, e.getMessage())); + String keyData = String.format("招聘记录编号=%s, 候选人=%s, 工作单位=%s", + excel.getRecruitId(), excel.getCandName(), excel.getCompanyName()); + ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData); + } + } + + List importRecords = validRecords.stream() + .filter(work -> !failedRecruitIds.contains(work.getRecruitId())) + .toList(); + appendSkippedFailures(validRecords, failedRecruitIds, failures); + + if (!importRecords.isEmpty()) { + Set importRecruitIds = importRecords.stream() + .map(CcdiStaffRecruitmentWork::getRecruitId) + .collect(Collectors.toSet()); + LambdaQueryWrapper deleteWrapper = new LambdaQueryWrapper<>(); + deleteWrapper.in(CcdiStaffRecruitmentWork::getRecruitId, importRecruitIds); + recruitmentWorkMapper.delete(deleteWrapper); + + importRecords.forEach(recruitmentWorkMapper::insert); + } + + if (!failures.isEmpty()) { + try { + String failuresKey = "import:recruitment:" + taskId + ":failures"; + redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS); + ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size()); + } catch (Exception e) { + ImportLogUtils.logRedisError(log, taskId, "保存失败记录", e); + } + } + + ImportResult result = new ImportResult(); + result.setTotalCount(excelList.size()); + result.setSuccessCount(importRecords.size()); + result.setFailureCount(failures.size()); + String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS"; + updateImportStatus(taskId, finalStatus, result); + + long duration = System.currentTimeMillis() - startTime; + ImportLogUtils.logImportComplete(log, taskId, "招聘历史工作经历", excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration); } @@ -184,7 +271,7 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm } /** - * 批量查询已存在的招聘项目编号 + * 批量查询已存在的招聘记录编号 */ private Set getExistingRecruitIds(List excelList) { List recruitIds = excelList.stream() @@ -212,7 +299,7 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm Set existingRecruitIds) { // 验证必填字段 if (StringUtils.isEmpty(addDTO.getRecruitId())) { - throw new RuntimeException("招聘项目编号不能为空"); + throw new RuntimeException("招聘记录编号不能为空"); } if (StringUtils.isEmpty(addDTO.getRecruitName())) { throw new RuntimeException("招聘项目名称不能为空"); @@ -247,6 +334,9 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm if (StringUtils.isEmpty(addDTO.getAdmitStatus())) { throw new RuntimeException("录用情况不能为空"); } + if (StringUtils.isEmpty(addDTO.getRecruitType())) { + throw new RuntimeException("招聘类型不能为空"); + } // 验证证件号码格式 String idCardError = IdCardUtil.getErrorMessage(addDTO.getCandId()); @@ -263,6 +353,115 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm if (AdmitStatus.getDescByCode(addDTO.getAdmitStatus()) == null) { throw new RuntimeException("录用情况只能填写'录用'、'未录用'或'放弃'"); } + + if (RecruitType.getDescByCode(addDTO.getRecruitType()) == null) { + throw new RuntimeException("招聘类型只能填写'SOCIAL'或'CAMPUS'"); + } + } + + private Map getRecruitmentMap(List excelList) { + List recruitIds = excelList.stream() + .map(CcdiStaffRecruitmentWorkExcel::getRecruitId) + .map(this::trim) + .filter(StringUtils::isNotEmpty) + .distinct() + .toList(); + if (recruitIds.isEmpty()) { + return Collections.emptyMap(); + } + List recruitments = recruitmentMapper.selectBatchIds(recruitIds); + return recruitments.stream() + .collect(Collectors.toMap(CcdiStaffRecruitment::getRecruitId, item -> item)); + } + + private void validateRecruitmentWorkData(CcdiStaffRecruitmentWorkExcel excel, + CcdiStaffRecruitment recruitment, + Set processedRecruitSortKeys) { + if (StringUtils.isEmpty(trim(excel.getRecruitId()))) { + throw new RuntimeException("招聘记录编号不能为空"); + } + if (StringUtils.isEmpty(trim(excel.getCandName()))) { + throw new RuntimeException("候选人姓名不能为空"); + } + if (StringUtils.isEmpty(trim(excel.getRecruitName()))) { + throw new RuntimeException("招聘项目名称不能为空"); + } + if (StringUtils.isEmpty(trim(excel.getPosName()))) { + throw new RuntimeException("职位名称不能为空"); + } + if (excel.getSortOrder() == null || excel.getSortOrder() <= 0) { + throw new RuntimeException("排序号不能为空且必须大于0"); + } + if (StringUtils.isEmpty(trim(excel.getCompanyName()))) { + throw new RuntimeException("工作单位不能为空"); + } + if (StringUtils.isEmpty(trim(excel.getPositionName()))) { + throw new RuntimeException("岗位不能为空"); + } + if (StringUtils.isEmpty(trim(excel.getJobStartMonth()))) { + throw new RuntimeException("入职年月不能为空"); + } + validateMonth(excel.getJobStartMonth(), "入职年月"); + if (StringUtils.isNotEmpty(trim(excel.getJobEndMonth()))) { + validateMonth(excel.getJobEndMonth(), "离职年月"); + } + if (recruitment == null) { + throw new RuntimeException("招聘记录编号不存在,请先维护招聘主信息"); + } + if (!"SOCIAL".equals(recruitment.getRecruitType())) { + throw new RuntimeException("该招聘记录不是社招,不允许导入历史工作经历"); + } + if (!sameText(excel.getCandName(), recruitment.getCandName())) { + throw new RuntimeException("招聘记录编号与候选人姓名不匹配"); + } + if (!sameText(excel.getRecruitName(), recruitment.getRecruitName())) { + throw new RuntimeException("招聘记录编号与招聘项目名称不匹配"); + } + if (!sameText(excel.getPosName(), recruitment.getPosName())) { + throw new RuntimeException("招聘记录编号与职位名称不匹配"); + } + String duplicateKey = trim(excel.getRecruitId()) + "#" + excel.getSortOrder(); + if (!processedRecruitSortKeys.add(duplicateKey)) { + throw new RuntimeException("同一招聘记录编号下排序号重复"); + } + } + + private void validateMonth(String value, String fieldName) { + String month = trim(value); + if (!month.matches("^((19|20)\\d{2})-(0[1-9]|1[0-2])$")) { + throw new RuntimeException(fieldName + "格式不正确,应为YYYY-MM"); + } + } + + private boolean sameText(String first, String second) { + return Objects.equals(trim(first), trim(second)); + } + + private String trim(String value) { + return value == null ? null : value.trim(); + } + + private RecruitmentImportFailureVO buildWorkFailure(CcdiStaffRecruitmentWorkExcel excel, String errorMessage) { + RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(errorMessage); + return failure; + } + + private void appendSkippedFailures(List validRecords, + Set failedRecruitIds, + List failures) { + Set appendedRecruitIds = new HashSet<>(); + for (CcdiStaffRecruitmentWork work : validRecords) { + if (failedRecruitIds.contains(work.getRecruitId()) && appendedRecruitIds.add(work.getRecruitId())) { + RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO(); + failure.setRecruitId(work.getRecruitId()); + failure.setCompanyName(work.getCompanyName()); + failure.setPositionName(work.getPositionName()); + failure.setErrorMessage("同一招聘记录编号存在失败行,已跳过该编号下全部工作经历,避免覆盖旧数据"); + failures.add(failure); + } + } } /** diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java index a8c18b86..336dd1cb 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java @@ -1,13 +1,18 @@ package com.ruoyi.info.collection.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.info.collection.domain.CcdiStaffRecruitment; +import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork; import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentAddDTO; import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentEditDTO; import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentQueryDTO; import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel; +import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel; import com.ruoyi.info.collection.domain.vo.CcdiStaffRecruitmentVO; +import com.ruoyi.info.collection.domain.vo.CcdiStaffRecruitmentWorkVO; import com.ruoyi.info.collection.enums.AdmitStatus; +import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentWorkMapper; import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentMapper; import com.ruoyi.info.collection.service.ICcdiStaffRecruitmentImportService; import com.ruoyi.info.collection.service.ICcdiStaffRecruitmentService; @@ -19,6 +24,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,6 +43,9 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer @Resource private CcdiStaffRecruitmentMapper recruitmentMapper; + @Resource + private CcdiStaffRecruitmentWorkMapper recruitmentWorkMapper; + @Resource private ICcdiStaffRecruitmentImportService recruitmentImportService; @@ -96,7 +105,7 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer /** * 查询招聘信息详情 * - * @param recruitId 招聘项目编号 + * @param recruitId 招聘记录编号 * @return 招聘信息VO */ @Override @@ -104,6 +113,7 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer CcdiStaffRecruitmentVO vo = recruitmentMapper.selectRecruitmentById(recruitId); if (vo != null) { vo.setAdmitStatusDesc(AdmitStatus.getDescByCode(vo.getAdmitStatus())); + vo.setWorkExperienceList(selectWorkExperienceList(recruitId)); } return vo; } @@ -117,9 +127,9 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer @Override @Transactional public int insertRecruitment(CcdiStaffRecruitmentAddDTO addDTO) { - // 检查招聘项目编号唯一性 + // 检查招聘记录编号唯一性 if (recruitmentMapper.selectById(addDTO.getRecruitId()) != null) { - throw new RuntimeException("该招聘项目编号已存在"); + throw new RuntimeException("该招聘记录编号已存在"); } CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment(); @@ -148,12 +158,15 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer /** * 批量删除招聘信息 * - * @param recruitIds 需要删除的招聘项目编号 + * @param recruitIds 需要删除的招聘记录编号 * @return 结果 */ @Override @Transactional public int deleteRecruitmentByIds(String[] recruitIds) { + LambdaQueryWrapper workWrapper = new LambdaQueryWrapper<>(); + workWrapper.in(CcdiStaffRecruitmentWork::getRecruitId, List.of(recruitIds)); + recruitmentWorkMapper.delete(workWrapper); return recruitmentMapper.deleteBatchIds(List.of(recruitIds)); } @@ -197,4 +210,56 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer return taskId; } + + /** + * 导入招聘记录历史工作经历数据(异步) + * + * @param excelList Excel实体列表 + * @return 任务ID + */ + @Override + @Transactional + public String importRecruitmentWork(List excelList) { + if (StringUtils.isNull(excelList) || excelList.isEmpty()) { + throw new RuntimeException("至少需要一条数据"); + } + + String taskId = UUID.randomUUID().toString(); + long startTime = System.currentTimeMillis(); + String userName = SecurityUtils.getUsername(); + + String statusKey = "import:recruitment:" + taskId; + Map statusData = new HashMap<>(); + statusData.put("taskId", taskId); + statusData.put("status", "PROCESSING"); + statusData.put("totalCount", excelList.size()); + statusData.put("successCount", 0); + statusData.put("failureCount", 0); + statusData.put("progress", 0); + statusData.put("startTime", startTime); + statusData.put("message", "正在处理历史工作经历..."); + + redisTemplate.opsForHash().putAll(statusKey, statusData); + redisTemplate.expire(statusKey, 7, TimeUnit.DAYS); + + recruitmentImportService.importRecruitmentWorkAsync(excelList, taskId, userName); + + return taskId; + } + + private List selectWorkExperienceList(String recruitId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, recruitId) + .orderByAsc(CcdiStaffRecruitmentWork::getSortOrder) + .orderByDesc(CcdiStaffRecruitmentWork::getId); + List workList = recruitmentWorkMapper.selectList(wrapper); + if (workList == null || workList.isEmpty()) { + return new ArrayList<>(); + } + return workList.stream().map(work -> { + CcdiStaffRecruitmentWorkVO vo = new CcdiStaffRecruitmentWorkVO(); + BeanUtils.copyProperties(work, vo); + return vo; + }).toList(); + } } diff --git a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiStaffRecruitmentMapper.xml b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiStaffRecruitmentMapper.xml index d5482838..452bf01b 100644 --- a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiStaffRecruitmentMapper.xml +++ b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiStaffRecruitmentMapper.xml @@ -12,12 +12,14 @@ + + @@ -31,44 +33,53 @@ select + max(tr.model_code) as modelCode, coalesce(max(staff.name), max(relation.relation_name), max(tr.object_key), max(tr.object_type)) as title, max(case when tr.object_type = 'STAFF_ID_CARD' then '员工对象' diff --git a/docs/tests/plans/2026-04-21-evidence-minimal-feature-validation-checklist.md b/docs/tests/plans/2026-04-21-evidence-minimal-feature-validation-checklist.md new file mode 100644 index 00000000..9f07ad97 --- /dev/null +++ b/docs/tests/plans/2026-04-21-evidence-minimal-feature-validation-checklist.md @@ -0,0 +1,44 @@ +# 证据库最小改造验证清单 + +## 验证范围 + +- 流水证据:流水详情中小号「加入证据库」按钮、确认弹窗、保存入库。 +- 模型证据:模型详情异常对象卡片中小号「加入证据库」按钮、确认弹窗、保存入库。 +- 资产证据:资产详情中小号「加入证据库」按钮、确认弹窗、保存入库。 +- 证据线索:项目详情右上角小号「证据线索」入口、右侧抽屉列表、搜索框基础展示。 + +## 版式约束 + +- 不新增独立证据库页面。 +- 不改变原详情页主体字段、表格列、字号体系和业务阅读顺序。 +- 「加入证据库」只能作为低频辅助按钮出现,使用 mini 尺寸、弱边框、弱背景。 +- 「证据线索」只能作为轻入口和右侧抽屉,不遮挡或重排项目详情主体内容。 + +## 功能验证 + +- 项目详情页能正常打开,顶部「证据线索」按钮可打开抽屉。 +- 抽屉无证据时展示空状态,有证据时展示编号、类型、关联人员、摘要、来源、确认人、备注。 +- 模型详情点击「加入证据库」后,弹窗自动带出证据类型、关联人员、证据摘要。 +- 流水详情点击「加入证据库」后,弹窗自动带出流水证据摘要,`source_record_id` 使用 `md5(本方账号+本方名称+对方账号+对方名称+交易时间+金额+摘要)`。 +- 资产详情点击「加入证据库」后,弹窗自动带出资产证据摘要。 +- 模型证据 `source_record_id` 使用 `md5(人员身份证+模型编码)`,缺少人员身份证或模型编码时不允许入库。 +- 资产证据 `source_record_id` 使用 `md5(人员身份证+资产字段)`,当前资产负债聚合口径的资产字段为家庭总收入、家庭总负债、家庭总资产、风险等级编码。 +- 确认理由为空时不能提交。 +- 填写确认理由后可以提交,提交成功后自动打开或刷新证据线索抽屉。 +- 保存后的证据落库到 `ccdi_evidence`。 + +## 技术验证 + +- 后端 `ccdi-project` 编译通过。 +- 前端 `npm run build:prod` 通过。 +- 数据库表 `ccdi_evidence` 存在。 +- 流水证据 `source_record_id` 不依赖 `statementId/bankStatementId`,应为 32 位 MD5 指纹。 +- 模型证据、资产证据的 `source_record_id` 均不拼接项目 ID,项目归属仅存 `project_id` 字段。 +- 页面控制台不出现由本次改造引入的明显错误。 +- 不提交或误动无关文件。 + +## 本期不做 + +- 证据卡片「查看详情」真实跳转原记录。 +- 跨项目引用/复用 UI。 +- 重复证据拦截。 diff --git a/docs/tests/records/2026-04-20-staff-recruitment-work-experience-self-acceptance.md b/docs/tests/records/2026-04-20-staff-recruitment-work-experience-self-acceptance.md new file mode 100644 index 00000000..5407a8da --- /dev/null +++ b/docs/tests/records/2026-04-20-staff-recruitment-work-experience-self-acceptance.md @@ -0,0 +1,64 @@ +# 员工招聘功能自验收清单 + +验收日期:2026-04-20 + +## 验收范围 + +本次自验收覆盖员工招聘页面与接口联动所需的前后端能力,包括招聘类型、候选人历史工作经历、工作经历单独导入、详情/编辑页展示顺序、面试官字段展示一致性,以及基于现有招聘数据补充联调样例数据。 + +## 前端页面 + +- [x] 查询条件保持原有结构,仅新增“招聘类型”筛选项。 +- [x] 顶部操作区包含“新增”“导入”“导入工作经历”“导出”。 +- [x] 列表列按最新口径展示:招聘记录编号、招聘项目名称、职位名称、候选人姓名、录用情况、学历 / 毕业学校、招聘类型、历史工作经历、操作。 +- [x] 列表“操作”列包含“详情”“编辑”“删除”按钮。 +- [x] 招聘项目名称列已加宽,长名称不再只显示为“办结”一类截断残片。 +- [x] “学历 / 毕业学校”在列表合并展示,详情/编辑中仍保留学历、毕业院校、毕业年月、专业等候选人基础字段。 +- [x] 详情页模块顺序为:招聘岗位信息、录用情况、候选人情况、候选人历史工作经历、面试官信息。 +- [x] 编辑页模块顺序与详情页保持一致:招聘岗位信息、录用情况、候选人情况、面试官信息。 +- [x] 详情页“面试官信息”统一按四个字段展示:面试官1姓名、面试官1工号、面试官2姓名、面试官2工号。 +- [x] 详情页不再展示重复的“社招工作经历摘要”,只保留“候选人历史工作经历”。 +- [x] 工作经历导入使用独立入口、独立模板、独立上传接口。 + +## 后端接口与数据结构 + +- [x] 主表 `ccdi_staff_recruitment` 保留原有创建/更新人员字段命名,不改动既有审计字段口径。 +- [x] 主表新增 `recruit_type`,用于区分社招、校招。 +- [x] 历史工作经历使用独立表 `ccdi_staff_recruitment_work`,不把工作经历摘要字段放入主表。 +- [x] 列表查询聚合返回历史工作经历段数,避免前端列表加载完整经历明细。 +- [x] 详情查询返回完整历史工作经历列表。 +- [x] 删除招聘记录时同步删除对应历史工作经历。 +- [x] 工作经历导入以招聘记录编号为唯一匹配依据。 +- [x] 工作经历导入时,候选人姓名、招聘项目名称、职位名称仅用于人工核对和导入校验。 +- [x] 工作经历导入时,三个辅助字段与主表不一致则禁止导入。 +- [x] 工作经历导入只允许社招记录导入,校招记录禁止导入。 +- [x] 同一个招聘记录编号在工作经历导入文件中任意一行失败时,该招聘记录编号下本次所有工作经历均不覆盖入库。 + +## 数据库与联调样例数据 + +- [x] 已补充数据库迁移脚本:`sql/migration/2026-04-15-add-staff-recruitment-social-work-summary.sql`。 +- [x] 已补充现有数据联调样例脚本:`sql/migration/2026-04-20-seed-staff-recruitment-work-existing-data.sql`。 +- [x] 样例脚本不改动已有招聘项目名称、职位名称、候选人姓名、录用情况、面试官等原始业务信息。 +- [x] 样例脚本只在招聘类型为空时补充 `recruit_type`,并生成带标记的历史工作经历样例。 +- [x] 数据库验证结果:`SOCIAL = 4646`,`CAMPUS = 1355`。 +- [x] 数据库验证结果:已生成历史工作经历样例 `25` 条,覆盖社招招聘记录 `20` 条。 + +## 构建与验证 + +- [x] 后端编译通过:`mvn -pl ccdi-info-collection -am compile -DskipTests`。 +- [x] 前端生产构建通过:`npm run build:prod`。 +- [x] 前端构建仅存在体积提示类 warning,未出现编译错误。 +- [x] 前端预览截图已生成,覆盖列表、工作经历导入、详情面试官展示。 +- [x] 验证过程中启动的前端预览进程已停止,未保留 8088 端口监听。 + +## 预览截图 + +- 列表页:`C:\Users\20696\codex-preview\staff-recruitment-work-import-list.png` +- 工作经历导入弹窗:`C:\Users\20696\codex-preview\staff-recruitment-work-import-dialog.png` +- 详情页面试官四字段展示:`C:\Users\20696\codex-preview\staff-recruitment-detail-interviewer-separated.png` + +## 注意事项 + +- 当前机器无法通过 `bin/mysql_utf8_exec.sh` 调用 MySQL 客户端执行中文 SQL,实际数据库脚本执行采用本地 Maven 缓存中的 MySQL JDBC 驱动,并显式设置 `utf8mb4` 会话字符集。 +- 列表默认第一页如果主要是校招记录,“历史工作经历”可能显示为 `-`;筛选“社招”后可看到已补充的工作经历段数。 +- 仓库中存在与本次招聘功能无关的未跟踪 `docx` 文件,本次未处理、未纳入验收范围。 diff --git a/docs/tests/records/2026-04-21-evidence-minimal-feature-validation-record.md b/docs/tests/records/2026-04-21-evidence-minimal-feature-validation-record.md new file mode 100644 index 00000000..0e23d3f2 --- /dev/null +++ b/docs/tests/records/2026-04-21-evidence-minimal-feature-validation-record.md @@ -0,0 +1,51 @@ +# 证据库最小改造验证记录 + +## 验证时间 + +2026-04-21 + +## 验证环境 + +- 前端:`http://localhost:62319` +- 后端:`http://localhost:62318` +- 项目:`test` +- 项目 ID:`90337` + +## 验证结果 + +| 验证项 | 结果 | 说明 | +| --- | --- | --- | +| 后端编译 | 通过 | `mvn -pl ccdi-project -am compile -DskipTests` 成功 | +| 前端构建 | 通过 | `npm run build:prod` 成功,仅存在原有包体积 warning | +| 数据库表 | 通过 | `ccdi_evidence` 已存在 | +| 模型证据入库 | 通过 | 模型详情小号「加入证据库」可打开弹窗并保存,生成 `EV-001` | +| 流水证据入库 | 通过 | 流水详情小号「加入证据库」可打开弹窗并保存,当前代码已改为使用 32 位 MD5 指纹作为 `source_record_id` | +| 资产证据入库 | 通过 | 资产详情小号「加入证据库」可打开弹窗并保存,已验证旧规则测试数据 `EV-003` 与新指纹规则测试数据 `EV-004` | +| 证据线索抽屉 | 通过 | 抽屉展示三类证据,包含编号、类型、关联人员、摘要、来源、确认人、备注 | +| 前端控制台 | 通过 | 验证后未发现 error/warn | +| 模型/资产来源指纹更新 | 通过 | 已重启后端并通过 MCP 页面验证:模型证据、资产证据均可打开确认弹窗,本次未确认入库,避免新增测试数据 | +| 证据抽屉跳转入口 | 通过 | 本期不做原记录跳转,已移除抽屉卡片中的「查看流水详情」「查看模型详情」「查看资产详情」按钮 | + +## 落库核对 + +项目 `90337` 当前证据数: + +| 类型 | 数量 | +| --- | ---: | +| FLOW | 1 | +| MODEL | 1 | +| ASSET | 2 | +| 合计 | 4 | + +## 注意事项 + +- 本次验证产生了测试证据数据,如正式交付前需要干净环境,可按项目 ID 清理。 +- 历史已保存的测试证据可能保留旧来源标识,新保存的流水、模型、资产证据会按当前规则生成 MD5 指纹。 +- 当前代码已将模型证据来源标识改为 `md5(人员身份证+模型编码)`,资产证据来源标识改为 `md5(人员身份证+资产字段)`,均不拼接项目 ID。 +- 为让模型详情前端拿到模型编码,后端仅补充返回 `modelCode` 字段,不涉及表结构和接口路径变更。 + +## 后续边界 + +- 证据卡片「查看详情」本期不做真实跳转,当前抽屉不展示跳转按钮;后续如要定位原记录,可基于 `source_type`、`source_record_id`、`snapshot_json` 增加跳转逻辑。 +- 跨项目引用/复用 UI 本期不做;当前 `source_record_id` 已按不拼接项目 ID 的规则生成,后续具备按同一来源指纹做跨项目比对的基础。 +- 重复证据拦截本期不做;当前允许同一项目内重复确认,后续可按 `project_id + evidence_type + source_type + source_record_id` 增加唯一性提示或软拦截。 diff --git a/ruoyi-ui/src/api/ccdiEvidence.js b/ruoyi-ui/src/api/ccdiEvidence.js new file mode 100644 index 00000000..322ad8be --- /dev/null +++ b/ruoyi-ui/src/api/ccdiEvidence.js @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +// 保存证据 +export function saveEvidence(data) { + return request({ + url: '/ccdi/evidence', + method: 'post', + data + }) +} + +// 查询项目证据列表 +export function listEvidence(params) { + return request({ + url: '/ccdi/evidence/list', + method: 'get', + params + }) +} + +// 查询证据详情 +export function getEvidence(evidenceId) { + return request({ + url: '/ccdi/evidence/' + evidenceId, + method: 'get' + }) +} diff --git a/ruoyi-ui/src/permission.js b/ruoyi-ui/src/permission.js index 41863d00..12750c32 100644 --- a/ruoyi-ui/src/permission.js +++ b/ruoyi-ui/src/permission.js @@ -9,7 +9,7 @@ import { isRelogin } from '@/utils/request' NProgress.configure({ showSpinner: false }) -const whiteList = ['/login', '/register', '/prototype/account-library'] +const whiteList = ['/login', '/register', '/prototype/account-library', '/prototype/staff-recruitment'] const isWhiteList = (path) => { return whiteList.some(pattern => isPathMatch(pattern, path)) diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js index 89ca88a5..62b3fce1 100644 --- a/ruoyi-ui/src/router/index.js +++ b/ruoyi-ui/src/router/index.js @@ -85,6 +85,13 @@ export const constantRoutes = [ hidden: true, meta: { title: '账户库管理原型', noCache: true } }, + { + path: 'prototype/staff-recruitment', + component: () => import('@/views/ccdiStaffRecruitment/index'), + name: 'StaffRecruitmentPrototype', + hidden: true, + meta: { title: '招聘信息预览', noCache: true } + }, { path: 'ccdiAccountInfo', component: () => import('@/views/ccdiAccountInfo/index'), diff --git a/ruoyi-ui/src/utils/ccdiEvidence.js b/ruoyi-ui/src/utils/ccdiEvidence.js new file mode 100644 index 00000000..d740b096 --- /dev/null +++ b/ruoyi-ui/src/utils/ccdiEvidence.js @@ -0,0 +1,78 @@ +import md5 from "@/utils/md5"; + +export const FLOW_EVIDENCE_FINGERPRINT_RULE = + "md5(leAccountNo+leAccountName+customerAccountNo+customerAccountName+trxDate+displayAmount+userMemo)"; + +export const MODEL_EVIDENCE_FINGERPRINT_RULE = "md5(personIdCard+modelCode)"; + +export const ASSET_EVIDENCE_FINGERPRINT_RULE = + "md5(staffIdCard+totalIncome+totalDebt+totalAsset+riskLevelCode)"; + +function normalizeFingerprintValue(value) { + if (value === null || value === undefined) { + return ""; + } + return String(value).trim(); +} + +function resolveCounterpartyName(detail) { + return detail.customerAccountName || detail.customerName || detail.counterpartyName || ""; +} + +export function buildFlowEvidenceFingerprintSource(detail = {}) { + return [ + detail.leAccountNo, + detail.leAccountName, + detail.customerAccountNo, + resolveCounterpartyName(detail), + detail.trxDate, + detail.displayAmount, + detail.userMemo, + ] + .map(normalizeFingerprintValue) + .join(""); +} + +export function buildFlowEvidenceFingerprint(detail = {}) { + const source = buildFlowEvidenceFingerprintSource(detail); + return source ? md5(source) : ""; +} + +export function buildFlowEvidenceSnapshot(detail = {}) { + const evidenceFingerprint = buildFlowEvidenceFingerprint(detail); + return { + ...detail, + evidenceFingerprint, + evidenceFingerprintRule: FLOW_EVIDENCE_FINGERPRINT_RULE, + }; +} + +export function buildModelEvidenceFingerprint(personIdCard, modelCode) { + const idCard = normalizeFingerprintValue(personIdCard); + const code = normalizeFingerprintValue(modelCode); + return idCard && code ? md5(idCard + code) : ""; +} + +export function buildAssetEvidenceFingerprint(row = {}) { + const idCard = normalizeFingerprintValue(row.staffIdCard); + const assetSource = [ + row.totalIncome, + row.totalDebt, + row.totalAsset, + row.riskLevelCode, + ] + .map(normalizeFingerprintValue) + .join(""); + return idCard && assetSource ? md5(idCard + assetSource) : ""; +} + +export function buildAssetEvidenceSnapshot(row = {}, detail = {}, summary = {}) { + const evidenceFingerprint = buildAssetEvidenceFingerprint(row); + return { + row, + detail, + summary, + evidenceFingerprint, + evidenceFingerprintRule: ASSET_EVIDENCE_FINGERPRINT_RULE, + }; +} diff --git a/ruoyi-ui/src/utils/md5.js b/ruoyi-ui/src/utils/md5.js new file mode 100644 index 00000000..5442aff0 --- /dev/null +++ b/ruoyi-ui/src/utils/md5.js @@ -0,0 +1,161 @@ +function safeAdd(x, y) { + const lsw = (x & 0xffff) + (y & 0xffff); + const msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xffff); +} + +function rotateLeft(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)); +} + +function cmn(q, a, b, x, s, t) { + return safeAdd(rotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b); +} + +function ff(a, b, c, d, x, s, t) { + return cmn((b & c) | (~b & d), a, b, x, s, t); +} + +function gg(a, b, c, d, x, s, t) { + return cmn((b & d) | (c & ~d), a, b, x, s, t); +} + +function hh(a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); +} + +function ii(a, b, c, d, x, s, t) { + return cmn(c ^ (b | ~d), a, b, x, s, t); +} + +function wordsToRaw(input) { + let output = ""; + for (let i = 0; i < input.length * 32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff); + } + return output; +} + +function rawToWords(input) { + const output = []; + output[(input.length >> 2) - 1] = undefined; + for (let i = 0; i < output.length; i++) { + output[i] = 0; + } + for (let i = 0; i < input.length * 8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32); + } + return output; +} + +function calculate(words, len) { + words[len >> 5] |= 0x80 << (len % 32); + words[(((len + 64) >>> 9) << 4) + 14] = len; + + let a = 1732584193; + let b = -271733879; + let c = -1732584194; + let d = 271733878; + + for (let i = 0; i < words.length; i += 16) { + const olda = a; + const oldb = b; + const oldc = c; + const oldd = d; + + a = ff(a, b, c, d, words[i], 7, -680876936); + d = ff(d, a, b, c, words[i + 1], 12, -389564586); + c = ff(c, d, a, b, words[i + 2], 17, 606105819); + b = ff(b, c, d, a, words[i + 3], 22, -1044525330); + a = ff(a, b, c, d, words[i + 4], 7, -176418897); + d = ff(d, a, b, c, words[i + 5], 12, 1200080426); + c = ff(c, d, a, b, words[i + 6], 17, -1473231341); + b = ff(b, c, d, a, words[i + 7], 22, -45705983); + a = ff(a, b, c, d, words[i + 8], 7, 1770035416); + d = ff(d, a, b, c, words[i + 9], 12, -1958414417); + c = ff(c, d, a, b, words[i + 10], 17, -42063); + b = ff(b, c, d, a, words[i + 11], 22, -1990404162); + a = ff(a, b, c, d, words[i + 12], 7, 1804603682); + d = ff(d, a, b, c, words[i + 13], 12, -40341101); + c = ff(c, d, a, b, words[i + 14], 17, -1502002290); + b = ff(b, c, d, a, words[i + 15], 22, 1236535329); + + a = gg(a, b, c, d, words[i + 1], 5, -165796510); + d = gg(d, a, b, c, words[i + 6], 9, -1069501632); + c = gg(c, d, a, b, words[i + 11], 14, 643717713); + b = gg(b, c, d, a, words[i], 20, -373897302); + a = gg(a, b, c, d, words[i + 5], 5, -701558691); + d = gg(d, a, b, c, words[i + 10], 9, 38016083); + c = gg(c, d, a, b, words[i + 15], 14, -660478335); + b = gg(b, c, d, a, words[i + 4], 20, -405537848); + a = gg(a, b, c, d, words[i + 9], 5, 568446438); + d = gg(d, a, b, c, words[i + 14], 9, -1019803690); + c = gg(c, d, a, b, words[i + 3], 14, -187363961); + b = gg(b, c, d, a, words[i + 8], 20, 1163531501); + a = gg(a, b, c, d, words[i + 13], 5, -1444681467); + d = gg(d, a, b, c, words[i + 2], 9, -51403784); + c = gg(c, d, a, b, words[i + 7], 14, 1735328473); + b = gg(b, c, d, a, words[i + 12], 20, -1926607734); + + a = hh(a, b, c, d, words[i + 5], 4, -378558); + d = hh(d, a, b, c, words[i + 8], 11, -2022574463); + c = hh(c, d, a, b, words[i + 11], 16, 1839030562); + b = hh(b, c, d, a, words[i + 14], 23, -35309556); + a = hh(a, b, c, d, words[i + 1], 4, -1530992060); + d = hh(d, a, b, c, words[i + 4], 11, 1272893353); + c = hh(c, d, a, b, words[i + 7], 16, -155497632); + b = hh(b, c, d, a, words[i + 10], 23, -1094730640); + a = hh(a, b, c, d, words[i + 13], 4, 681279174); + d = hh(d, a, b, c, words[i], 11, -358537222); + c = hh(c, d, a, b, words[i + 3], 16, -722521979); + b = hh(b, c, d, a, words[i + 6], 23, 76029189); + a = hh(a, b, c, d, words[i + 9], 4, -640364487); + d = hh(d, a, b, c, words[i + 12], 11, -421815835); + c = hh(c, d, a, b, words[i + 15], 16, 530742520); + b = hh(b, c, d, a, words[i + 2], 23, -995338651); + + a = ii(a, b, c, d, words[i], 6, -198630844); + d = ii(d, a, b, c, words[i + 7], 10, 1126891415); + c = ii(c, d, a, b, words[i + 14], 15, -1416354905); + b = ii(b, c, d, a, words[i + 5], 21, -57434055); + a = ii(a, b, c, d, words[i + 12], 6, 1700485571); + d = ii(d, a, b, c, words[i + 3], 10, -1894986606); + c = ii(c, d, a, b, words[i + 10], 15, -1051523); + b = ii(b, c, d, a, words[i + 1], 21, -2054922799); + a = ii(a, b, c, d, words[i + 8], 6, 1873313359); + d = ii(d, a, b, c, words[i + 15], 10, -30611744); + c = ii(c, d, a, b, words[i + 6], 15, -1560198380); + b = ii(b, c, d, a, words[i + 13], 21, 1309151649); + a = ii(a, b, c, d, words[i + 4], 6, -145523070); + d = ii(d, a, b, c, words[i + 11], 10, -1120210379); + c = ii(c, d, a, b, words[i + 2], 15, 718787259); + b = ii(b, c, d, a, words[i + 9], 21, -343485551); + + a = safeAdd(a, olda); + b = safeAdd(b, oldb); + c = safeAdd(c, oldc); + d = safeAdd(d, oldd); + } + + return [a, b, c, d]; +} + +function rawToHex(input) { + const hex = "0123456789abcdef"; + let output = ""; + for (let i = 0; i < input.length; i++) { + const x = input.charCodeAt(i); + output += hex.charAt((x >>> 4) & 0x0f) + hex.charAt(x & 0x0f); + } + return output; +} + +function toUtf8Raw(input) { + return unescape(encodeURIComponent(input)); +} + +export default function md5(input) { + const value = input === null || input === undefined ? "" : String(input); + const raw = toUtf8Raw(value); + return rawToHex(wordsToRaw(calculate(rawToWords(raw), raw.length * 8))); +} diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue index dd3ef957..d7bc8dc7 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue @@ -283,10 +283,23 @@ :visible.sync="detailVisible" append-to-body custom-class="detail-dialog" - title="流水详情" width="980px" @close="closeDetailDialog" > +
@@ -394,6 +407,7 @@ import { getBankStatementOptions, getBankStatementDetail, } from "@/api/ccdiProjectBankStatement"; +import { buildFlowEvidenceFingerprint, buildFlowEvidenceSnapshot } from "@/utils/ccdiEvidence"; const TAB_MAP = { all: "all", @@ -518,6 +532,7 @@ export default { }, }, methods: { + buildFlowEvidenceFingerprint, async getList() { this.syncProjectId(); if (!this.queryParams.projectId) { @@ -638,6 +653,29 @@ export default { this.detailLoading = false; this.detailData = createEmptyDetailData(); }, + handleAddEvidence() { + const detail = this.detailData || {}; + const sourceRecordId = buildFlowEvidenceFingerprint(detail); + const amountText = this.formatSignedAmount(detail.displayAmount); + const counterparty = this.formatCounterpartyName(detail); + const hitTagText = Array.isArray(detail.hitTags) && detail.hitTags.length + ? `,命中${detail.hitTags.map((tag) => tag.ruleName).filter(Boolean).join("、")}标签` + : ""; + this.$emit("evidence-confirm", { + evidenceType: "FLOW", + relatedPersonName: this.resolveFlowRelatedPerson(detail), + relatedPersonId: detail.cretNo || "", + evidenceTitle: `${this.resolveFlowRelatedPerson(detail)} / ${this.formatField(detail.leAccountNo)}`, + evidenceSummary: `${this.formatField(detail.trxDate)},${this.resolveFlowRelatedPerson(detail)}账户与${counterparty}发生交易,金额${amountText}${hitTagText}。`, + sourceType: "BANK_STATEMENT", + sourceRecordId, + sourcePage: "流水详情", + snapshotJson: JSON.stringify(buildFlowEvidenceSnapshot(detail)), + }); + }, + resolveFlowRelatedPerson(detail) { + return this.formatField(detail.leAccountName) === "-" ? "关联人员" : this.formatField(detail.leAccountName); + }, handleExport() { if (this.total === 0) { return; @@ -751,6 +789,26 @@ export default { gap: 12px; } +.detail-dialog-title { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.evidence-corner-btn { + padding: 4px 9px; + font-size: 12px; + color: #5b7fb8; + border-color: #d6e4f7; + background: #f8fbff; + + &:hover { + color: #2474e8; + border-color: #9fc3ff; + background: #edf5ff; + } +} + .shell-sidebar, .shell-main { border: 1px solid #ebeef5; diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/EvidenceConfirmDialog.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/EvidenceConfirmDialog.vue new file mode 100644 index 00000000..5b598cd6 --- /dev/null +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/EvidenceConfirmDialog.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/EvidenceDrawer.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/EvidenceDrawer.vue new file mode 100644 index 00000000..a592b182 --- /dev/null +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/EvidenceDrawer.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue index 748942ca..cec09533 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue @@ -21,10 +21,24 @@ @@ -67,6 +81,7 @@ @@ -204,6 +244,39 @@ export default { overflow: hidden; } +.family-detail-wrapper { + padding: 12px 0; +} + +.family-detail-toolbar { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + margin-bottom: 10px; +} + +.family-detail-title { + margin-right: auto; + color: #64748b; + font-size: 13px; + font-weight: 600; +} + +.evidence-corner-btn { + padding: 4px 9px; + font-size: 12px; + color: #5b7fb8; + border-color: #d6e4f7; + background: #f8fbff; + + &:hover { + color: #2474e8; + border-color: #9fc3ff; + background: #edf5ff; + } +} + :deep(.family-table th) { background: #f8fafc; color: #64748b; diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue index de0adf9b..2e0cae60 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue @@ -33,7 +33,10 @@ @selection-change="handleRiskModelSelectionChange" @view-project-analysis="handleRiskModelProjectAnalysis" /> - +
diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue index a612ab4a..d5cf8d61 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue @@ -75,15 +75,28 @@
-
{{ item.title || "-" }}
-
{{ item.subtitle || "-" }}
+
+
+
{{ item.title || "-" }}
+
{{ item.subtitle || "-" }}
+
+ + 加入证据库 + +
@@ -97,7 +110,7 @@

{{ item.summary || "-" }}

{{ field.label }} @@ -113,6 +126,8 @@ @@ -252,12 +318,35 @@ export default { background: #f8fafc; } +.object-card__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + .object-card__title { font-size: 15px; font-weight: 600; color: #0f172a; } +.evidence-corner-btn { + flex: 0 0 auto; + padding: 4px 9px; + font-size: 12px; + color: #5b7fb8; + border-color: #d6e4f7; + background: #f8fbff; + + &:hover { + color: #2474e8; + border-color: #9fc3ff; + background: #edf5ff; + } +} + .object-card__subtitle { margin-top: 6px; font-size: 12px; diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue index 582126f7..5f7cedc3 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue @@ -46,7 +46,12 @@ - + diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue index df0464d6..e5186cc5 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue @@ -207,10 +207,23 @@ :visible.sync="detailVisible" append-to-body custom-class="detail-dialog" - title="流水详情" width="980px" @close="closeDetailDialog" > +
@@ -318,6 +331,7 @@ import { getOverviewSuspiciousTransactions, } from "@/api/ccdi/projectOverview"; import { getBankStatementDetail } from "@/api/ccdiProjectBankStatement"; +import { buildFlowEvidenceFingerprint, buildFlowEvidenceSnapshot } from "@/utils/ccdiEvidence"; const SUSPICIOUS_TYPE_OPTIONS = [ { value: "ALL", label: "全部可疑人员类型" }, @@ -428,6 +442,7 @@ export default { }, }, methods: { + buildFlowEvidenceFingerprint, async handleSuspiciousTypeChange(command) { this.currentSuspiciousType = command; this.suspiciousPageNum = 1; @@ -586,6 +601,31 @@ export default { this.detailLoading = false; this.detailData = createEmptyDetailData(); }, + handleAddEvidence() { + const detail = this.detailData || {}; + const sourceRecordId = buildFlowEvidenceFingerprint(detail); + const amountText = this.formatSignedAmount(detail.displayAmount); + const counterparty = this.formatCounterpartyName(detail); + const relatedPersonName = this.resolveFlowRelatedPerson(detail); + const hitTagText = Array.isArray(detail.hitTags) && detail.hitTags.length + ? `,命中${detail.hitTags.map((tag) => tag.ruleName).filter(Boolean).join("、")}标签` + : ""; + this.$emit("evidence-confirm", { + evidenceType: "FLOW", + relatedPersonName, + relatedPersonId: detail.cretNo || "", + evidenceTitle: `${relatedPersonName} / ${this.formatField(detail.leAccountNo)}`, + evidenceSummary: `${this.formatField(detail.trxDate)},${relatedPersonName}账户与${counterparty}发生交易,金额${amountText}${hitTagText}。`, + sourceType: "BANK_STATEMENT", + sourceRecordId, + sourcePage: "流水详情", + snapshotJson: JSON.stringify(buildFlowEvidenceSnapshot(detail)), + }); + }, + resolveFlowRelatedPerson(detail) { + const value = this.formatField(detail.leAccountName); + return value === "-" ? "关联人员" : value; + }, handleRiskDetailExport() { if (!this.projectId) { return; @@ -960,6 +1000,26 @@ export default { gap: 12px; } +.detail-dialog-title { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.evidence-corner-btn { + padding: 4px 9px; + font-size: 12px; + color: #5b7fb8; + border-color: #d6e4f7; + background: #f8fbff; + + &:hover { + color: #2474e8; + border-color: #9fc3ff; + background: #edf5ff; + } +} + :deep(.detail-dialog) { border-radius: 8px; diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue index 71cb7ef9..135d2e74 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue @@ -19,6 +19,7 @@ :project-id="projectId" :title="sectionTitle" :subtitle="sectionSubtitle" + @evidence-confirm="$emit('evidence-confirm', $event)" />
diff --git a/ruoyi-ui/src/views/ccdiProject/detail.vue b/ruoyi-ui/src/views/ccdiProject/detail.vue index 377b41fb..efd980d2 100644 --- a/ruoyi-ui/src/views/ccdiProject/detail.vue +++ b/ruoyi-ui/src/views/ccdiProject/detail.vue @@ -33,6 +33,15 @@
+ + 证据线索 + + + +
@@ -69,6 +90,8 @@ import ParamConfig from "./components/detail/ParamConfig"; import PreliminaryCheck from "./components/detail/PreliminaryCheck"; import SpecialCheck from "./components/detail/SpecialCheck"; import DetailQuery from "./components/detail/DetailQuery"; +import EvidenceConfirmDialog from "./components/detail/EvidenceConfirmDialog"; +import EvidenceDrawer from "./components/detail/EvidenceDrawer"; import { getProject } from "@/api/ccdiProject"; export default { @@ -79,6 +102,8 @@ export default { PreliminaryCheck, SpecialCheck, DetailQuery, + EvidenceConfirmDialog, + EvidenceDrawer, }, data() { return { @@ -102,6 +127,9 @@ export default { warningThreshold: 60, projectStatus: "0", }, + evidenceConfirmVisible: false, + evidenceDrawerVisible: false, + evidencePayload: {}, projectStatusPollingTimer: null, projectStatusPollingInterval: 1000, projectStatusPollingLoading: false, @@ -139,8 +167,10 @@ export default { // 初始化页面数据 this.initActiveTabFromRoute(); this.initPageData(); + this.$root.$on("ccdi-evidence-confirm", this.handleEvidenceConfirm); }, beforeDestroy() { + this.$root.$off("ccdi-evidence-confirm", this.handleEvidenceConfirm); this.stopProjectStatusPolling(); }, methods: { @@ -400,6 +430,21 @@ export default { handleRefreshProject() { this.initPageData(); }, + handleEvidenceConfirm(payload) { + this.evidencePayload = { + projectId: this.projectId, + ...(payload || {}), + }; + this.evidenceConfirmVisible = true; + }, + handleEvidenceSaved() { + this.evidenceDrawerVisible = true; + this.$nextTick(() => { + if (this.$refs.evidenceDrawer) { + this.$refs.evidenceDrawer.loadEvidence(); + } + }); + }, /** 导出报告 */ handleExport() { console.log("导出报告"); @@ -496,6 +541,21 @@ export default { .header-right { display: flex; align-items: center; + gap: 10px; + + .evidence-entry-btn { + padding: 6px 10px; + font-size: 12px; + color: #5b7fb8; + border-color: #d6e4f7; + background: #f8fbff; + + &:hover { + color: var(--ccdi-primary); + border-color: #9fc3ff; + background: #edf5ff; + } + } .nav-menu { // 移除默认背景色和边框 diff --git a/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue b/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue index 2a26d062..32301c7b 100644 --- a/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue +++ b/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue @@ -44,6 +44,16 @@ + + + + + 搜索 重置 @@ -53,6 +63,15 @@ 新增 + 导入 + 导入工作经历 + 导入工作经历 + + + 导出 + - - + + - - - - + + + + + + +