Compare commits

...

16 Commits

115 changed files with 4414 additions and 567 deletions

View File

@@ -51,6 +51,7 @@
"msg": "查询成功",
"rows": [
{
"id": 1002,
"recruitId": "REC20250205001",
"recruitName": "2025春季校园招聘",
"posName": "Java开发工程师",
@@ -80,19 +81,19 @@
### 1.2 查询招聘信息详情
**接口描述:** 根据招聘项目编号查询详细信息
**接口描述:** 根据招聘信息主键ID查询详细信息
**请求方式:** `GET`
**接口路径:** `/ccdi/staffRecruitment/{recruitId}`
**接口路径:** `/ccdi/staffRecruitment/{id}`
**权限标识:** `ccdi:staffRecruitment:query`
**路径参数:**
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|-----------|--------|----|--------|----------------|
| recruitId | String | 是 | 招聘项目编号 | REC20250205001 |
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|------|------|----|--------------|-----|
| id | Long | 是 | 招聘信息主键ID | 1002 |
**响应示例:**
@@ -101,6 +102,7 @@
"code": 200,
"msg": "操作成功",
"data": {
"id": 1002,
"recruitId": "REC20250205001",
"recruitName": "2025春季校园招聘",
"posName": "Java开发工程师",
@@ -237,15 +239,15 @@
**请求方式:** `DELETE`
**接口路径:** `/ccdi/staffRecruitment/{recruitIds}`
**接口路径:** `/ccdi/staffRecruitment/{ids}`
**权限标识:** `ccdi:staffRecruitment:remove`
**路径参数:**
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|------------|----------|----|------------------|-------------------------------|
| recruitIds | String[] | 是 | 招聘项目编号数组,多个用逗号分隔 | REC20250205001,REC20250205002 |
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|------|------|----|-----------------------|----------|
| ids | Long[] | 是 | 招聘信息主键ID数组,多个用逗号分隔 | 1002,1003 |
**响应示例:**
@@ -276,7 +278,7 @@
| 序号 | 字段名 | 说明 | 必填 |
|----|----------|-----------|----|
| 1 | 招聘项目编号 | 唯一标识 | 是 |
| 1 | 招聘项目编号 | 允许重复 | 是 |
| 2 | 招聘项目名称 | - | 是 |
| 3 | 职位名称 | - | 是 |
| 4 | 职位类别 | - | 是 |
@@ -326,7 +328,7 @@
```json
{
"code": 500,
"msg": "很抱歉,导入完成!成功 8 条,失败 2 条,错误如下:<br/>1、招聘项目编号 REC001 导入失败:该招聘项目编号已存在<br/>2、招聘项目编号 REC002 导入失败:证件号码格式不正确"
"msg": "很抱歉,导入完成!成功 8 条,失败 2 条,错误如下:<br/>1、招聘项目编号 REC001 导入失败:历史工作经历匹配到多条招聘主信息<br/>2、招聘项目编号 REC002 导入失败:证件号码格式不正确"
}
```
@@ -375,14 +377,14 @@ Excel导入导出对象,使用EasyExcel注解。
| 401 | 未授权,请先登录 |
| 403 | 无权限访问 |
| 404 | 资源不存在 |
| 409 | 主键冲突 |
| 409 | 数据冲突 |
| 500 | 服务器内部错误 |
### 常见业务错误
| 错误信息 | 说明 |
|------------|--------------------|
| 该招聘项目编号已存在 | 新增时recruitId重复 |
| 历史工作经历匹配到多条招聘主信息 | 招聘项目编号重复且候选人、项目名、职位名仍无法唯一匹配从表归属 |
| 招聘项目编号不能为空 | recruitId字段为空 |
| 证件号码格式不正确 | 身份证号格式验证失败 |
| 毕业年月格式不正确 | candGrad不是YYYYMM格式 |

View File

@@ -1,22 +1,23 @@
4.员工招聘信息表ccdi_staff_recruitment,,,,,,
序号,字段名,类型,默认值,是否可为空,是否主键,注释
1,recruit_id,VARCHAR(32),,,,招聘项目编号
2,recruit_name,VARCHAR(100),,,,招聘项目名称
3,pos_name,VARCHAR(100),,,,职位名称
4,pos_category,VARCHAR(50),,,,职位类别
5,pos_desc,TEXT,,,,职位描述
6,cand_name,VARCHAR(20),,,,应聘人员姓名
7,cand_edu,VARCHAR(20),,,,应聘人员学历
8,cand_id,VARCHAR(18),,,,应聘人员证件号码
9,cand_school,VARCHAR(50),,,,应聘人员毕业院校
10,cand_major,VARCHAR(30),,,,应聘人员专业
11,cand_grad,VARCHAR(6),,,,应聘人员毕业年月
12,admit_status,VARCHAR(10),,,,记录录用情况:录用、未录用、放弃等
13,interviewer_name1,VARCHAR(20),,,,面试官1姓名
14,interviewer_id1,VARCHAR(10),,,,面试官1工号
13,interviewer_name2,VARCHAR(20),,,,面试官2姓名
14,interviewer_id2,VARCHAR(10),,,,面试官2工号
16,created_by,VARCHAR(20),-,,,记录创建人
17,updated_by,VARCHAR(20),-,,,记录更新
18,create_time,VARCHAR(10),0000-00-00,,,创建时间
19,update_time,VARCHAR(10),0000-00-00,,,更新时间
1,id,BIGINT,,,,主键ID
2,recruit_id,VARCHAR(32),,,,招聘项目编号(允许重复)
3,recruit_name,VARCHAR(100),,,,招聘项目名称
4,pos_name,VARCHAR(100),,,,职位名称
5,pos_category,VARCHAR(50),,,,职位类别
6,pos_desc,TEXT,,,,职位描述
7,cand_name,VARCHAR(20),,,,应聘人员姓名
8,cand_edu,VARCHAR(20),,,,应聘人员学历
9,cand_id,VARCHAR(18),,,,应聘人员证件号码
10,cand_school,VARCHAR(50),,,,应聘人员毕业院校
11,cand_major,VARCHAR(30),,,,应聘人员专业
12,cand_grad,VARCHAR(6),,,,应聘人员毕业年月
13,admit_status,VARCHAR(10),,,,记录录用情况:录用、未录用、放弃等
14,interviewer_name1,VARCHAR(20),,,,面试官1姓名
15,interviewer_id1,VARCHAR(10),,,,面试官1工号
16,interviewer_name2,VARCHAR(20),,,,面试官2姓名
17,interviewer_id2,VARCHAR(10),,,,面试官2工号
18,created_by,VARCHAR(20),-,,,记录创建
19,updated_by,VARCHAR(20),-,,,记录更新人
20,create_time,VARCHAR(10),0000-00-00,,,创建时间
21,update_time,VARCHAR(10),0000-00-00,,,更新时间
1 4.员工招聘信息表:ccdi_staff_recruitment
2 序号 字段名 类型 默认值 是否可为空 是否主键 注释
3 1 recruit_id id VARCHAR(32) BIGINT 招聘项目编号 主键ID
4 2 recruit_name recruit_id VARCHAR(100) VARCHAR(32) 招聘项目名称 招聘项目编号(允许重复)
5 3 pos_name recruit_name VARCHAR(100) 职位名称 招聘项目名称
6 4 pos_category pos_name VARCHAR(50) VARCHAR(100) 职位类别 职位名称
7 5 pos_desc pos_category TEXT VARCHAR(50) 职位描述 职位类别
8 6 cand_name pos_desc VARCHAR(20) TEXT 应聘人员姓名 职位描述
9 7 cand_edu cand_name VARCHAR(20) 应聘人员学历 应聘人员姓名
10 8 cand_id cand_edu VARCHAR(18) VARCHAR(20) 应聘人员证件号码 应聘人员学历
11 9 cand_school cand_id VARCHAR(50) VARCHAR(18) 应聘人员毕业院校 应聘人员证件号码
12 10 cand_major cand_school VARCHAR(30) VARCHAR(50) 应聘人员专业 应聘人员毕业院校
13 11 cand_grad cand_major VARCHAR(6) VARCHAR(30) 应聘人员毕业年月 应聘人员专业
14 12 admit_status cand_grad VARCHAR(10) VARCHAR(6) 记录录用情况:录用、未录用、放弃等 应聘人员毕业年月
15 13 interviewer_name1 admit_status VARCHAR(20) VARCHAR(10) 面试官1姓名 记录录用情况:录用、未录用、放弃等
16 14 interviewer_id1 interviewer_name1 VARCHAR(10) VARCHAR(20) 面试官1工号 面试官1姓名
17 13 15 interviewer_name2 interviewer_id1 VARCHAR(20) VARCHAR(10) 面试官2姓名 面试官1工号
18 14 16 interviewer_id2 interviewer_name2 VARCHAR(10) VARCHAR(20) 面试官2工号 面试官2姓名
19 16 17 created_by interviewer_id2 VARCHAR(20) VARCHAR(10) - 记录创建人 面试官2工号
20 17 18 updated_by created_by VARCHAR(20) - 记录更新人 记录创建人
21 18 19 create_time updated_by VARCHAR(10) VARCHAR(20) 0000-00-00 - 创建时间 记录更新人
22 19 20 update_time create_time VARCHAR(10) 0000-00-00 更新时间 创建时间
23 21 update_time VARCHAR(10) 0000-00-00 更新时间

3
assets/图谱.txt Normal file
View File

@@ -0,0 +1,3 @@
关系图谱http://64.202.65.112:8082/atlas/refactor/#/home/graph/downloadService?id=lanxitest&mode=K_EXPAND&type=NORMAL&atlasToken=2C914E5E1FBFBC4AD15163E0AB03B800&params={"vId":"rel_node/15942f5b84bada01ccd25f5e5678ac22"}
资金流图谱http://64.202.65.112:8082/atlas/refactor/#/home/graph/downloadService?id=ccdi_lanxi_trans&mode=K_EXPAND&type=NORMAL&atlasToken=F4BBA291A285858BAF4526C6EC312388&params={"vId":"idno_node/f2f797081494c5c0555a3bbf0f57c5e7"}

View File

@@ -7,7 +7,6 @@ import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.*;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
@@ -47,9 +46,6 @@ public class CcdiBaseStaffController extends BaseController {
@Resource
private ICcdiBaseStaffImportService importAsyncService;
@Resource
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
/**
* 查询员工列表
*/
@@ -161,14 +157,7 @@ public class CcdiBaseStaffController extends BaseController {
return error("至少需要一条数据");
}
BaseStaffImportSubmitResultVO result = new BaseStaffImportSubmitResultVO();
if (hasStaffRows) {
result.setStaffTaskId(baseStaffService.importBaseStaff(staffList));
}
if (hasAssetRows) {
result.setAssetTaskId(baseStaffAssetImportService.importAssetInfo(assetList));
}
result.setMessage(buildImportSubmitMessage(hasStaffRows, hasAssetRows));
BaseStaffImportSubmitResultVO result = baseStaffService.importBaseStaffWithAssets(staffList, assetList);
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
@@ -215,13 +204,4 @@ public class CcdiBaseStaffController extends BaseController {
return getDataTable(pageData, failures.size());
}
private String buildImportSubmitMessage(boolean hasStaffRows, boolean hasAssetRows) {
if (hasStaffRows && hasAssetRows) {
return "已提交员工信息和员工资产信息导入任务";
}
if (hasStaffRows) {
return "已提交员工信息导入任务";
}
return "已提交员工资产信息导入任务";
}
}

View File

@@ -10,7 +10,6 @@ import com.ruoyi.info.collection.domain.vo.CcdiStaffFmyRelationVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportFailureVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO;
import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
@@ -51,9 +50,6 @@ public class CcdiStaffFmyRelationController extends BaseController {
@Resource
private ICcdiStaffFmyRelationImportService relationImportService;
@Resource
private ICcdiAssetInfoImportService assetInfoImportService;
/**
* 查询员工亲属关系列表
*/
@@ -157,15 +153,7 @@ public class CcdiStaffFmyRelationController extends BaseController {
return error("至少需要一条数据");
}
StaffFmyRelationImportSubmitResultVO result = new StaffFmyRelationImportSubmitResultVO();
if (hasRelationRows) {
result.setRelationTaskId(relationService.importRelation(relationList));
}
if (hasAssetRows) {
result.setAssetTaskId(assetInfoImportService.importAssetInfo(assetList));
}
result.setMessage(buildImportSubmitMessage(hasRelationRows, hasAssetRows));
StaffFmyRelationImportSubmitResultVO result = relationService.importRelationWithAssets(relationList, assetList);
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
@@ -211,13 +199,4 @@ public class CcdiStaffFmyRelationController extends BaseController {
return getDataTable(pageData, failures.size());
}
private String buildImportSubmitMessage(boolean hasRelationRows, boolean hasAssetRows) {
if (hasRelationRows && hasAssetRows) {
return "已提交员工亲属关系和亲属资产信息导入任务";
}
if (hasRelationRows) {
return "已提交员工亲属关系导入任务";
}
return "已提交亲属资产信息导入任务";
}
}

View File

@@ -69,9 +69,9 @@ public class CcdiStaffRecruitmentController extends BaseController {
*/
@Operation(summary = "获取招聘信息详细信息")
@PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:query')")
@GetMapping(value = "/{recruitId}")
public AjaxResult getInfo(@PathVariable String recruitId) {
return success(recruitmentService.selectRecruitmentById(recruitId));
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable Long id) {
return success(recruitmentService.selectRecruitmentById(id));
}
/**
@@ -102,9 +102,9 @@ public class CcdiStaffRecruitmentController extends BaseController {
@Operation(summary = "删除招聘信息")
@PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:remove')")
@Log(title = "员工招聘信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{recruitIds}")
public AjaxResult remove(@PathVariable String[] recruitIds) {
return toAjax(recruitmentService.deleteRecruitmentByIds(recruitIds));
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(recruitmentService.deleteRecruitmentByIds(ids));
}
/**

View File

@@ -94,6 +94,6 @@ public class CcdiEnterpriseBaseInfo implements Serializable {
/** 风险等级1-高风险, 2-中风险, 3-低风险 */
private String riskLevel;
/** 企业来源GENERAL-一般企业, EMP_RELATION-员工关系人, CREDIT_CUSTOMER-信贷客户, INTERMEDIARY-中介, BOTH-兼有 */
/** 企业来源GENERAL-一般企业, EMP_RELATION-员工关系人, CREDIT_CUSTOMER-信贷客户, SUPPLIER-供应商, INTERMEDIARY-中介, BOTH-兼有 */
private String entSource;
}

View File

@@ -22,8 +22,11 @@ public class CcdiStaffRecruitment implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 招聘记录编号 */
@TableId(type = IdType.INPUT)
private String recruitId;
/** 招聘项目名称 */

View File

@@ -28,6 +28,9 @@ public class CcdiStaffRecruitmentWork implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联招聘信息主键ID */
private Long recruitmentId;
/** 关联招聘记录编号 */
private String recruitId;

View File

@@ -33,6 +33,10 @@ public class CcdiCustFmyRelationQueryDTO implements Serializable {
@Schema(description = "关系人姓名")
private String relationName;
/** 关系人身份证号 */
@Schema(description = "关系人身份证号")
private String relationCertNo;
/** 状态 */
@Schema(description = "状态0-无效1-有效")
private Integer status;

View File

@@ -37,6 +37,10 @@ public class CcdiStaffFmyRelationQueryDTO implements Serializable {
@Schema(description = "关系人姓名")
private String relationName;
/** 关系人身份证号 */
@Schema(description = "关系人身份证号")
private String relationCertNo;
/** 状态 */
@Schema(description = "状态0-无效1-有效")
private Integer status;

View File

@@ -3,6 +3,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.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
@@ -10,6 +11,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 员工招聘信息新增DTO
@@ -102,4 +104,8 @@ public class CcdiStaffRecruitmentAddDTO implements Serializable {
/** 面试官2工号 */
@Size(max = 10, message = "面试官2工号长度不能超过10个字符")
private String interviewerId2;
/** 历史工作经历列表 */
@Valid
private List<CcdiStaffRecruitmentWorkEditDTO> workExperienceList;
}

View File

@@ -26,8 +26,13 @@ public class CcdiStaffRecruitmentEditDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@NotNull(message = "招聘信息ID不能为空")
private Long id;
/** 招聘记录编号 */
@NotNull(message = "招聘记录编号不能为空")
@NotBlank(message = "招聘记录编号不能为空")
@Size(max = 32, message = "招聘记录编号长度不能超过32个字符")
private String recruitId;
/** 招聘项目名称 */

View File

@@ -2,6 +2,7 @@ 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.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -25,6 +26,7 @@ public class CcdiAccountInfoExcel implements Serializable {
@ExcelProperty(value = "证件号*", index = 1)
@ColumnWidth(24)
@TextFormat
private String ownerId;
@ExcelProperty(value = "账户姓名*", index = 2)
@@ -33,6 +35,7 @@ public class CcdiAccountInfoExcel implements Serializable {
@ExcelProperty(value = "账户号码*", index = 3)
@ColumnWidth(28)
@TextFormat
private String accountNo;
@ExcelProperty(value = "账户类型*", index = 4)
@@ -49,6 +52,7 @@ public class CcdiAccountInfoExcel implements Serializable {
@ExcelProperty(value = "银行代码", index = 7)
@ColumnWidth(16)
@TextFormat
private String bankCode;
@ExcelProperty(value = "币种", index = 8)

View File

@@ -26,14 +26,14 @@ public class CcdiBaseStaffAssetInfoExcel implements Serializable {
/** 员工身份证号 */
@ExcelProperty(value = "员工身份证号*", index = 0)
@ColumnWidth(22)
@ColumnWidth(24)
@Required
@TextFormat
private String personId;
/** 资产大类 */
@ExcelProperty(value = "资产大类*", index = 1)
@ColumnWidth(16)
@ColumnWidth(18)
@Required
private String assetMainType;
@@ -51,39 +51,39 @@ public class CcdiBaseStaffAssetInfoExcel implements Serializable {
/** 产权占比 */
@ExcelProperty(value = "产权占比", index = 4)
@ColumnWidth(12)
@ColumnWidth(14)
private BigDecimal ownershipRatio;
/** 购买/评估日期 */
@ExcelProperty(value = "购买/评估日期", index = 5)
@ColumnWidth(16)
@ColumnWidth(20)
private Date purchaseEvalDate;
/** 资产原值 */
@ExcelProperty(value = "资产原值", index = 6)
@ColumnWidth(16)
@ColumnWidth(18)
private BigDecimal originalValue;
/** 当前估值 */
@ExcelProperty(value = "当前估值*", index = 7)
@ColumnWidth(16)
@ColumnWidth(18)
@Required
private BigDecimal currentValue;
/** 估值截止日期 */
@ExcelProperty(value = "估值截止日期", index = 8)
@ColumnWidth(16)
@ColumnWidth(20)
private Date valuationDate;
/** 资产状态 */
@ExcelProperty(value = "资产状态*", index = 9)
@ColumnWidth(14)
@ColumnWidth(16)
@DictDropdown(dictType = "ccdi_asset_status")
@Required
private String assetStatus;
/** 备注 */
@ExcelProperty(value = "备注", index = 10)
@ColumnWidth(28)
@ColumnWidth(32)
private String remarks;
}

View File

@@ -4,6 +4,7 @@ import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.annotation.Required;
import com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -25,54 +26,56 @@ public class CcdiBaseStaffExcel implements Serializable {
/** 姓名 */
@ExcelProperty(value = "姓名", index = 0)
@ColumnWidth(15)
@ColumnWidth(16)
@Required
private String name;
/** 员工ID */
@ExcelProperty(value = "员工ID", index = 1)
@ColumnWidth(15)
@ColumnWidth(18)
@Required
private Long staffId;
/** 所属部门ID */
@ExcelProperty(value = "所属部门ID", index = 2)
@ColumnWidth(15)
@ColumnWidth(20)
@Required
private Long deptId;
/** 身份证号 */
@ExcelProperty(value = "身份证号", index = 3)
@ColumnWidth(20)
@ColumnWidth(24)
@Required
@TextFormat
private String idCard;
/** 电话 */
@ExcelProperty(value = "电话", index = 4)
@ColumnWidth(15)
@ColumnWidth(18)
@Required
@TextFormat
private String phone;
/** 年收入 */
@ExcelProperty(value = "年收入(元/年)", index = 5)
@ColumnWidth(18)
@ColumnWidth(20)
private BigDecimal annualIncome;
/** 入职时间 */
@ExcelProperty(value = "入职时间", index = 6)
@ColumnWidth(15)
@ColumnWidth(18)
private Date hireDate;
/** 是否党员 */
@ExcelProperty(value = "是否党员", index = 7)
@ColumnWidth(12)
@ColumnWidth(16)
@DictDropdown(dictType = "ccdi_yes_no_flag")
@Required
private Integer partyMember;
/** 状态 */
@ExcelProperty(value = "状态", index = 8)
@ColumnWidth(10)
@ColumnWidth(14)
@DictDropdown(dictType = "ccdi_employee_status")
@Required
private String status;

View File

@@ -3,6 +3,7 @@ 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 com.ruoyi.common.annotation.TextFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -26,6 +27,7 @@ public class CcdiCustEnterpriseRelationExcel implements Serializable {
@ExcelProperty(value = "身份证号", index = 0)
@ColumnWidth(20)
@Required
@TextFormat
@Schema(description = "身份证号")
private String personId;
@@ -33,6 +35,7 @@ public class CcdiCustEnterpriseRelationExcel implements Serializable {
@ExcelProperty(value = "统一社会信用代码", index = 1)
@ColumnWidth(25)
@Required
@TextFormat
@Schema(description = "统一社会信用代码")
private String socialCreditCode;

View File

@@ -4,6 +4,7 @@ import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.annotation.Required;
import com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -26,6 +27,7 @@ public class CcdiCustFmyRelationExcel implements Serializable {
@ExcelProperty(value = "信贷客户身份证号*", index = 0)
@ColumnWidth(20)
@Required
@TextFormat
private String personId;
/** 关系类型 */
@@ -63,16 +65,19 @@ public class CcdiCustFmyRelationExcel implements Serializable {
@ExcelProperty(value = "关系人证件号码*", index = 6)
@ColumnWidth(20)
@Required
@TextFormat
private String relationCertNo;
/** 手机号码1 */
@ExcelProperty(value = "手机号码1", index = 7)
@ColumnWidth(15)
@TextFormat
private String mobilePhone1;
/** 手机号码2 */
@ExcelProperty(value = "手机号码2", index = 8)
@ColumnWidth(15)
@TextFormat
private String mobilePhone2;
/** 微信名称1 */

View File

@@ -3,6 +3,7 @@ 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.DictDropdown;
import com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -23,6 +24,7 @@ public class CcdiEnterpriseBaseInfoExcel implements Serializable {
@ExcelProperty(value = "统一社会信用代码*", index = 0)
@ColumnWidth(24)
@TextFormat
private String socialCreditCode;
@ExcelProperty(value = "企业名称*", index = 1)
@@ -66,6 +68,7 @@ public class CcdiEnterpriseBaseInfoExcel implements Serializable {
@ExcelProperty(value = "法定代表人证件号码", index = 10)
@ColumnWidth(24)
@TextFormat
private String legalCertNo;
@ExcelProperty(value = "股东1", index = 11)

View File

@@ -2,6 +2,8 @@ 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 com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -17,17 +19,21 @@ public class CcdiIntermediaryEnterpriseRelationExcel implements Serializable {
private static final long serialVersionUID = 1L;
/** 中介本人证件号码 */
@ExcelProperty(value = "中介本人证件号码*", index = 0)
@ExcelProperty(value = "中介本人证件号码", index = 0)
@ColumnWidth(24)
@Required
@TextFormat
private String ownerPersonId;
/** 统一社会信用代码 */
@ExcelProperty(value = "统一社会信用代码*", index = 1)
@ExcelProperty(value = "统一社会信用代码", index = 1)
@ColumnWidth(24)
@Required
@TextFormat
private String socialCreditCode;
/** 关联职务 */
@ExcelProperty(value = "关联职务", index = 2)
/** 关联职务 */
@ExcelProperty(value = "关联职务", index = 2)
@ColumnWidth(20)
private String relationPersonPost;

View File

@@ -3,6 +3,7 @@ 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.DictDropdown;
import com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -29,6 +30,7 @@ public class CcdiIntermediaryEntityExcel implements Serializable {
/** 统一社会信用代码 */
@ExcelProperty(value = "统一社会信用代码*", index = 1)
@ColumnWidth(20)
@TextFormat
private String socialCreditCode;
/** 主体类型 */
@@ -77,6 +79,7 @@ public class CcdiIntermediaryEntityExcel implements Serializable {
/** 法定代表人证件号码 */
@ExcelProperty(value = "法定代表人证件号码", index = 10)
@ColumnWidth(20)
@TextFormat
private String legalCertNo;
/** 股东1 */

View File

@@ -3,6 +3,7 @@ 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.DictDropdown;
import com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -52,11 +53,13 @@ public class CcdiIntermediaryPersonExcel implements Serializable {
/** 证件号码 */
@ExcelProperty(value = "证件号码*", index = 5)
@ColumnWidth(20)
@TextFormat
private String personId;
/** 手机号码 */
@ExcelProperty(value = "手机号码", index = 6)
@ColumnWidth(15)
@TextFormat
private String mobile;
/** 微信号 */
@@ -77,6 +80,7 @@ public class CcdiIntermediaryPersonExcel implements Serializable {
/** 企业统一信用码 */
@ExcelProperty(value = "企业统一信用码", index = 10)
@ColumnWidth(20)
@TextFormat
private String socialCreditCode;
/** 职位 */
@@ -87,6 +91,7 @@ public class CcdiIntermediaryPersonExcel implements Serializable {
/** 关联中介本人证件号码 */
@ExcelProperty(value = "关联中介本人证件号码", index = 12)
@ColumnWidth(24)
@TextFormat
private String relatedNumId;
/** 备注 */

View File

@@ -3,6 +3,7 @@ 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 com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -26,6 +27,7 @@ public class CcdiPurchaseTransactionExcel implements Serializable {
@ExcelProperty(value = "采购事项ID", index = 0)
@ColumnWidth(20)
@Required
@TextFormat
private String purchaseId;
/** 采购类别 */
@@ -138,6 +140,7 @@ public class CcdiPurchaseTransactionExcel implements Serializable {
@ExcelProperty(value = "申请人工号", index = 21)
@ColumnWidth(15)
@Required
@TextFormat
private String applicantId;
/** 申请人姓名 */
@@ -155,6 +158,7 @@ public class CcdiPurchaseTransactionExcel implements Serializable {
/** 采购负责人工号 */
@ExcelProperty(value = "采购负责人工号", index = 24)
@ColumnWidth(15)
@TextFormat
private String purchaseLeaderId;
/** 采购负责人姓名 */

View File

@@ -4,6 +4,7 @@ import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.annotation.Required;
import com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -21,6 +22,7 @@ public class CcdiPurchaseTransactionSupplierExcel implements Serializable {
@ExcelProperty(value = "采购事项ID", index = 0)
@ColumnWidth(20)
@Required
@TextFormat
private String purchaseId;
@ExcelProperty(value = "供应商名称", index = 1)
@@ -30,6 +32,7 @@ public class CcdiPurchaseTransactionSupplierExcel implements Serializable {
@ExcelProperty(value = "供应商统一信用代码", index = 2)
@ColumnWidth(25)
@TextFormat
private String supplierUscc;
@ExcelProperty(value = "供应商联系人", index = 3)
@@ -38,10 +41,12 @@ public class CcdiPurchaseTransactionSupplierExcel implements Serializable {
@ExcelProperty(value = "供应商联系电话", index = 4)
@ColumnWidth(18)
@TextFormat
private String contactPhone;
@ExcelProperty(value = "供应商银行账户", index = 5)
@ColumnWidth(20)
@TextFormat
private String supplierBankAccount;
@ExcelProperty(value = "是否中标", index = 6)

View File

@@ -3,6 +3,7 @@ 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 com.ruoyi.common.annotation.TextFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -26,6 +27,7 @@ public class CcdiStaffEnterpriseRelationExcel implements Serializable {
@ExcelProperty(value = "亲属身份证号", index = 0)
@ColumnWidth(20)
@Required
@TextFormat
@Schema(description = "亲属身份证号")
private String personId;
@@ -33,6 +35,7 @@ public class CcdiStaffEnterpriseRelationExcel implements Serializable {
@ExcelProperty(value = "统一社会信用代码", index = 1)
@ColumnWidth(25)
@Required
@TextFormat
@Schema(description = "统一社会信用代码")
private String socialCreditCode;

View File

@@ -28,6 +28,7 @@ public class CcdiStaffFmyRelationExcel implements Serializable {
@ExcelProperty(value = "员工身份证号*", index = 0)
@ColumnWidth(20)
@Required
@TextFormat
private String personId;
/** 关系类型 */
@@ -71,11 +72,13 @@ public class CcdiStaffFmyRelationExcel implements Serializable {
/** 手机号码1 */
@ExcelProperty(value = "手机号码1", index = 7)
@ColumnWidth(15)
@TextFormat
private String mobilePhone1;
/** 手机号码2 */
@ExcelProperty(value = "手机号码2", index = 8)
@ColumnWidth(15)
@TextFormat
private String mobilePhone2;
/** 家庭成员年收入 */

View File

@@ -4,6 +4,7 @@ import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.annotation.Required;
import com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -21,10 +22,11 @@ public class CcdiStaffRecruitmentExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 招聘项目编号 */
@ExcelProperty(value = "招聘项目编号", index = 0)
/** 招聘记录编号 */
@ExcelProperty(value = "招聘记录编号", index = 0)
@ColumnWidth(20)
@Required
@TextFormat
private String recruitId;
/** 招聘项目名称 */
@@ -51,66 +53,76 @@ public class CcdiStaffRecruitmentExcel implements Serializable {
@Required
private String posDesc;
/** 应聘人员姓名 */
@ExcelProperty(value = "应聘人员姓名", index = 5)
@ColumnWidth(15)
@Required
private String candName;
/** 应聘人员学历 */
@ExcelProperty(value = "应聘人员学历", index = 6)
@ColumnWidth(15)
@Required
private String candEdu;
/** 应聘人员证件号码 */
@ExcelProperty(value = "应聘人员证件号码", index = 7)
@ColumnWidth(20)
@Required
private String candId;
/** 应聘人员毕业院校 */
@ExcelProperty(value = "应聘人员毕业院校", index = 8)
@ColumnWidth(20)
@Required
private String candSchool;
/** 应聘人员专业 */
@ExcelProperty(value = "应聘人员专业", index = 9)
@ColumnWidth(15)
@Required
private String candMajor;
/** 应聘人员毕业年月 */
@ExcelProperty(value = "应聘人员毕业年月", index = 10)
@ColumnWidth(15)
@Required
private String candGrad;
/** 录用情况 */
@ExcelProperty(value = "录用情况", index = 11)
@ExcelProperty(value = "录用情况", index = 5)
@ColumnWidth(10)
@DictDropdown(dictType = "ccdi_admit_status")
@Required
private String admitStatus;
/** 候选人姓名 */
@ExcelProperty(value = "候选人姓名", index = 6)
@ColumnWidth(15)
@Required
private String candName;
/** 招聘类型 */
@ExcelProperty(value = "招聘类型", index = 7)
@ColumnWidth(12)
@DictDropdown(dictType = "ccdi_recruit_type")
@Required
private String recruitType;
/** 应聘人员学历 */
@ExcelProperty(value = "学历", index = 8)
@ColumnWidth(15)
@Required
private String candEdu;
/** 应聘人员证件号码 */
@ExcelProperty(value = "证件号码", index = 9)
@ColumnWidth(20)
@Required
@TextFormat
private String candId;
/** 应聘人员毕业年月 */
@ExcelProperty(value = "毕业年月", index = 10)
@ColumnWidth(15)
@Required
private String candGrad;
/** 应聘人员毕业院校 */
@ExcelProperty(value = "毕业院校", index = 11)
@ColumnWidth(20)
@Required
private String candSchool;
/** 应聘人员专业 */
@ExcelProperty(value = "专业", index = 12)
@ColumnWidth(15)
@Required
private String candMajor;
/** 面试官1姓名 */
@ExcelProperty(value = "面试官1姓名", index = 12)
@ExcelProperty(value = "面试官1姓名", index = 13)
@ColumnWidth(15)
private String interviewerName1;
/** 面试官1工号 */
@ExcelProperty(value = "面试官1工号", index = 13)
@ExcelProperty(value = "面试官1工号", index = 14)
@ColumnWidth(15)
@TextFormat
private String interviewerId1;
/** 面试官2姓名 */
@ExcelProperty(value = "面试官2姓名", index = 14)
@ExcelProperty(value = "面试官2姓名", index = 15)
@ColumnWidth(15)
private String interviewerName2;
/** 面试官2工号 */
@ExcelProperty(value = "面试官2工号", index = 15)
@ExcelProperty(value = "面试官2工号", index = 16)
@ColumnWidth(15)
@TextFormat
private String interviewerId2;
}

View File

@@ -3,6 +3,7 @@ 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 com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
@@ -24,6 +25,7 @@ public class CcdiStaffRecruitmentWorkExcel implements Serializable {
@ExcelProperty(value = "招聘记录编号", index = 0)
@ColumnWidth(20)
@Required
@TextFormat
private String recruitId;
/** 候选人姓名 */
@@ -61,20 +63,20 @@ public class CcdiStaffRecruitmentWorkExcel implements Serializable {
@ColumnWidth(18)
private String departmentName;
/** 岗位 */
@ExcelProperty(value = "岗位", index = 7)
/** 岗位名称 */
@ExcelProperty(value = "岗位名称", index = 7)
@ColumnWidth(20)
@Required
private String positionName;
/** 入职年月 */
@ExcelProperty(value = "入职年月", index = 8)
@ExcelProperty(value = "入职时间", index = 8)
@ColumnWidth(12)
@Required
private String jobStartMonth;
/** 离职年月 */
@ExcelProperty(value = "离职年月", index = 9)
@ExcelProperty(value = "离职时间", index = 9)
@ColumnWidth(12)
private String jobEndMonth;
@@ -83,8 +85,8 @@ public class CcdiStaffRecruitmentWorkExcel implements Serializable {
@ColumnWidth(30)
private String departureReason;
/** 工作内容 */
@ExcelProperty(value = "工作内容", index = 11)
/** 主要工作内容 */
@ExcelProperty(value = "主要工作内容", index = 11)
@ColumnWidth(35)
private String workContent;

View File

@@ -19,6 +19,9 @@ public class CcdiStaffRecruitmentVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long id;
/** 招聘记录编号 */
private String recruitId;

View File

@@ -22,7 +22,7 @@ public class IntermediaryEnterpriseRelationImportFailureVO implements Serializab
@Schema(description = "统一社会信用代码")
private String socialCreditCode;
@Schema(description = "关联职务")
@Schema(description = "关联职务")
private String relationPersonPost;
@Schema(description = "备注")

View File

@@ -10,6 +10,7 @@ public enum EnterpriseSource {
GENERAL("GENERAL", "一般企业"),
EMP_RELATION("EMP_RELATION", "员工关系人"),
CREDIT_CUSTOMER("CREDIT_CUSTOMER", "信贷客户"),
SUPPLIER("SUPPLIER", "供应商"),
INTERMEDIARY("INTERMEDIARY", "中介"),
BOTH("BOTH", "兼有");

View File

@@ -1,15 +1,28 @@
package com.ruoyi.info.collection.handler;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.ruoyi.common.annotation.Required;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import java.lang.reflect.Field;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* EasyExcel必填字段标注处理器
@@ -18,13 +31,18 @@ import java.util.*;
* @author ruoyi
*/
@Slf4j
public class RequiredFieldWriteHandler implements SheetWriteHandler {
public class RequiredFieldWriteHandler implements CellWriteHandler {
/**
* 实体类Class对象
*/
private final Class<?> modelClass;
/**
* 必填字段列索引集合
*/
private final Set<Integer> requiredColumns;
/**
* 构造函数
*
@@ -32,39 +50,30 @@ public class RequiredFieldWriteHandler implements SheetWriteHandler {
*/
public RequiredFieldWriteHandler(Class<?> modelClass) {
this.modelClass = modelClass;
this.requiredColumns = parseRequiredFields();
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
// 获取工作表
Sheet sheet = writeSheetHolder.getSheet();
// 获取表头行第1行索引为0
Row headerRow = sheet.getRow(0);
if (headerRow == null) {
log.warn("表头行不存在,跳过必填字段标注");
public void afterCellDispose(WriteSheetHolder writeSheetHolder,
WriteTableHolder writeTableHolder,
List<WriteCellData<?>> cellDataList,
Cell cell,
Head head,
Integer relativeRowIndex,
Boolean isHead) {
if (!Boolean.TRUE.equals(isHead) || cell == null || !requiredColumns.contains(cell.getColumnIndex())) {
return;
}
// 创建红色字体样式
Workbook workbook = writeWorkbookHolder.getWorkbook();
Workbook workbook = cell.getSheet().getWorkbook();
CellStyle redStyle = createRedFontStyle(workbook);
// 解析实体类中的必填字段
Set<Integer> requiredColumns = parseRequiredFields();
// 为必填字段的表头添加红色星号
for (Integer columnIndex : requiredColumns) {
Cell cell = headerRow.getCell(columnIndex);
if (cell != null) {
String originalValue = cell.getStringCellValue();
// 添加红色星号
cell.setCellValue(originalValue + "*");
// 应用红色样式到星号
cell.setCellStyle(redStyle);
log.info("为列[{}]的表头添加必填标记(*)", columnIndex);
}
String originalValue = cell.getStringCellValue();
if (originalValue != null && !originalValue.endsWith("*")) {
cell.setCellValue(originalValue + "*");
}
cell.setCellStyle(redStyle);
log.info("为列[{}]的表头添加必填标记(*)", cell.getColumnIndex());
}
/**

View File

@@ -30,10 +30,10 @@ public interface CcdiStaffRecruitmentMapper extends BaseMapper<CcdiStaffRecruitm
/**
* 查询招聘信息详情
*
* @param recruitId 招聘项目编号
* @param id 主键ID
* @return 招聘信息VO
*/
CcdiStaffRecruitmentVO selectRecruitmentById(@Param("recruitId") String recruitId);
CcdiStaffRecruitmentVO selectRecruitmentById(@Param("id") Long id);
/**
* 批量插入招聘信息数据

View File

@@ -5,6 +5,8 @@ import com.ruoyi.info.collection.domain.vo.AssetImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 亲属资产信息异步导入 服务层
@@ -31,6 +33,19 @@ public interface ICcdiAssetInfoImportService {
*/
void importAssetInfoAsync(List<CcdiAssetInfoExcel> excelList, String taskId, String userName);
/**
* 同步执行亲属资产导入可附加同一文件亲属关系Sheet成功导入的归属映射
*
* @param excelList Excel实体列表
* @param taskId 任务ID
* @param userName 用户名
* @param extraOwnerMappings 附加归属映射key为亲属证件号value为归属员工证件号集合
*/
void importAssetInfoSync(List<CcdiAssetInfoExcel> excelList,
String taskId,
String userName,
Map<String, Set<String>> extraOwnerMappings);
/**
* 查询导入状态
*

View File

@@ -5,6 +5,8 @@ import com.ruoyi.info.collection.domain.vo.BaseStaffAssetImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 员工资产信息异步导入 服务层
@@ -31,6 +33,19 @@ public interface ICcdiBaseStaffAssetImportService {
*/
void importAssetInfoAsync(List<CcdiBaseStaffAssetInfoExcel> excelList, String taskId, String userName);
/**
* 同步执行员工资产导入可附加同一文件员工Sheet成功导入的归属映射
*
* @param excelList Excel实体列表
* @param taskId 任务ID
* @param userName 用户名
* @param extraOwnerMappings 附加归属映射key为资产持有人证件号value为归属员工证件号集合
*/
void importAssetInfoSync(List<CcdiBaseStaffAssetInfoExcel> excelList,
String taskId,
String userName,
Map<String, Set<String>> extraOwnerMappings);
/**
* 查询导入状态
*

View File

@@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.vo.ImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import java.util.List;
import java.util.Set;
/**
* @Author: wkc
@@ -19,6 +20,15 @@ public interface ICcdiBaseStaffImportService {
*/
void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, String taskId);
/**
* 同步执行员工导入并返回本轮成功员工身份证号
*
* @param excelList Excel数据列表
* @param taskId 任务ID
* @return 成功导入的身份证号集合
*/
Set<String> importBaseStaffSync(List<CcdiBaseStaffExcel> excelList, String taskId);
/**
* 查询导入状态
*

View File

@@ -4,7 +4,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.BaseStaffImportSubmitResultVO;
import com.ruoyi.info.collection.domain.vo.CcdiBaseStaffOptionVO;
import com.ruoyi.info.collection.domain.vo.CcdiBaseStaffVO;
@@ -83,6 +85,16 @@ public interface ICcdiBaseStaffService {
*/
String importBaseStaff(List<CcdiBaseStaffExcel> excelList);
/**
* 导入员工信息和员工资产双Sheet数据
*
* @param staffList 员工信息Sheet
* @param assetList 员工资产Sheet
* @return 提交结果
*/
BaseStaffImportSubmitResultVO importBaseStaffWithAssets(List<CcdiBaseStaffExcel> staffList,
List<CcdiBaseStaffAssetInfoExcel> assetList);
/**
* 查询员工下拉列表
* 支持按员工ID或姓名模糊搜索只返回在职员工

View File

@@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportFailureVO;
import java.util.List;
import java.util.Map;
/**
* 员工亲属关系异步导入 服务层
@@ -23,6 +24,16 @@ public interface ICcdiStaffFmyRelationImportService {
*/
void importRelationAsync(List<CcdiStaffFmyRelationExcel> excelList, String taskId, String userName);
/**
* 同步执行员工亲属关系导入并返回本轮成功关系映射
*
* @param excelList Excel实体列表
* @param taskId 任务ID
* @param userName 用户名
* @return key为亲属证件号value为归属员工证件号
*/
Map<String, String> importRelationSync(List<CcdiStaffFmyRelationExcel> excelList, String taskId, String userName);
/**
* 查询导入失败记录
*

View File

@@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiStaffFmyRelationVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO;
import java.util.List;
@@ -81,4 +83,14 @@ public interface ICcdiStaffFmyRelationService {
* @return 任务ID
*/
String importRelation(List<CcdiStaffFmyRelationExcel> excelList);
/**
* 导入员工亲属关系和亲属资产双Sheet数据
*
* @param relationList 员工亲属关系Sheet
* @param assetList 亲属资产Sheet
* @return 提交结果
*/
StaffFmyRelationImportSubmitResultVO importRelationWithAssets(List<CcdiStaffFmyRelationExcel> relationList,
List<CcdiAssetInfoExcel> assetList);
}

View File

@@ -46,10 +46,10 @@ public interface ICcdiStaffRecruitmentService {
/**
* 查询招聘信息详情
*
* @param recruitId 招聘项目编号
* @param id 主键ID
* @return 招聘信息VO
*/
CcdiStaffRecruitmentVO selectRecruitmentById(String recruitId);
CcdiStaffRecruitmentVO selectRecruitmentById(Long id);
/**
* 新增招聘信息
@@ -70,10 +70,10 @@ public interface ICcdiStaffRecruitmentService {
/**
* 批量删除招聘信息
*
* @param recruitIds 需要删除的招聘项目编号
* @param ids 需要删除的招聘信息ID
* @return 结果
*/
int deleteRecruitmentByIds(String[] recruitIds);
int deleteRecruitmentByIds(Long[] ids);
/**
* 导入招聘信息数据

View File

@@ -82,6 +82,15 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
@Async
@Transactional
public void importAssetInfoAsync(List<CcdiAssetInfoExcel> excelList, String taskId, String userName) {
importAssetInfoSync(excelList, taskId, userName, Map.of());
}
@Override
@Transactional
public void importAssetInfoSync(List<CcdiAssetInfoExcel> excelList,
String taskId,
String userName,
Map<String, Set<String>> extraOwnerMappings) {
List<CcdiAssetInfo> successList = new ArrayList<>();
List<AssetImportFailureVO> failures = new ArrayList<>();
@@ -92,6 +101,7 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
.toList();
Map<String, Set<String>> ownerMap = buildOwnerMap(personIds);
mergeOwnerMappings(ownerMap, extraOwnerMappings);
for (int i = 0; i < excelList.size(); i++) {
CcdiAssetInfoExcel excel = excelList.get(i);
@@ -189,6 +199,18 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
}
}
private void mergeOwnerMappings(Map<String, Set<String>> result, Map<String, Set<String>> mappings) {
if (mappings == null || mappings.isEmpty()) {
return;
}
for (Map.Entry<String, Set<String>> entry : mappings.entrySet()) {
if (StringUtils.isEmpty(entry.getKey()) || entry.getValue() == null || entry.getValue().isEmpty()) {
continue;
}
result.computeIfAbsent(entry.getKey(), key -> new java.util.LinkedHashSet<>()).addAll(entry.getValue());
}
}
private void validateExcel(CcdiAssetInfoExcel excel) {
if (StringUtils.isEmpty(excel.getPersonId())) {
throw new RuntimeException("亲属证件号不能为空");

View File

@@ -81,6 +81,15 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
@Async
@Transactional
public void importAssetInfoAsync(List<CcdiBaseStaffAssetInfoExcel> excelList, String taskId, String userName) {
importAssetInfoSync(excelList, taskId, userName, Map.of());
}
@Override
@Transactional
public void importAssetInfoSync(List<CcdiBaseStaffAssetInfoExcel> excelList,
String taskId,
String userName,
Map<String, Set<String>> extraOwnerMappings) {
List<CcdiAssetInfo> successList = new ArrayList<>();
List<BaseStaffAssetImportFailureVO> failures = new ArrayList<>();
@@ -91,6 +100,7 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
.toList();
Map<String, Set<String>> ownerMap = buildOwnerMap(personIds);
mergeOwnerMappings(ownerMap, extraOwnerMappings);
Set<String> existingAssetKeys = buildExistingAssetKeys(personIds);
Set<String> importedAssetKeys = new java.util.LinkedHashSet<>();
@@ -207,6 +217,18 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
}
}
private void mergeOwnerMappings(Map<String, Set<String>> result, Map<String, Set<String>> mappings) {
if (mappings == null || mappings.isEmpty()) {
return;
}
for (Map.Entry<String, Set<String>> entry : mappings.entrySet()) {
if (StringUtils.isEmpty(entry.getKey()) || entry.getValue() == null || entry.getValue().isEmpty()) {
continue;
}
result.computeIfAbsent(entry.getKey(), key -> new java.util.LinkedHashSet<>()).addAll(entry.getValue());
}
}
private void validateExcel(CcdiBaseStaffAssetInfoExcel excel) {
if (StringUtils.isEmpty(excel.getPersonId())) {
throw new RuntimeException("员工身份证号不能为空");

View File

@@ -23,6 +23,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
@@ -51,6 +52,12 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
@Override
@Async
public void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, String taskId) {
importBaseStaffSync(excelList, taskId);
}
@Override
@Transactional
public Set<String> importBaseStaffSync(List<CcdiBaseStaffExcel> excelList, String taskId) {
long startTime = System.currentTimeMillis();
// 记录导入开始
@@ -153,6 +160,11 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
long duration = System.currentTimeMillis() - startTime;
ImportLogUtils.logImportComplete(log, taskId, "员工基础信息",
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
return newRecords.stream()
.map(CcdiBaseStaff::getIdCard)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**

View File

@@ -6,15 +6,19 @@ import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.BaseStaffImportSubmitResultVO;
import com.ruoyi.info.collection.domain.vo.CcdiAssetInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiBaseStaffOptionVO;
import com.ruoyi.info.collection.domain.vo.CcdiBaseStaffVO;
import com.ruoyi.info.collection.enums.EmployeeStatus;
import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
import com.ruoyi.info.collection.service.ICcdiAssetInfoService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
@@ -46,6 +50,12 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
@Resource
private ICcdiAssetInfoService assetInfoService;
@Resource
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
@Resource
private CcdiDualSheetImportOrchestrationService dualSheetImportOrchestrationService;
/**
* 查询员工列表
*
@@ -218,28 +228,52 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
@Transactional
public String importBaseStaff(List<CcdiBaseStaffExcel> excelList) {
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
// 初始化Redis状态
String statusKey = "import:baseStaff:" + taskId;
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", excelList.size());
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
statusData.put("startTime", startTime);
statusData.put("message", "正在处理...");
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, java.util.concurrent.TimeUnit.DAYS);
initializeImportStatus("import:baseStaff:", taskId, excelList.size());
importAsyncService.importBaseStaffAsync(excelList, taskId);
return taskId;
}
@Override
@Transactional
public BaseStaffImportSubmitResultVO importBaseStaffWithAssets(List<CcdiBaseStaffExcel> staffList,
List<CcdiBaseStaffAssetInfoExcel> assetList) {
boolean hasStaffRows = staffList != null && !staffList.isEmpty();
boolean hasAssetRows = assetList != null && !assetList.isEmpty();
if (!hasStaffRows && !hasAssetRows) {
throw new RuntimeException("至少需要一条数据");
}
BaseStaffImportSubmitResultVO result = new BaseStaffImportSubmitResultVO();
result.setMessage(buildImportSubmitMessage(hasStaffRows, hasAssetRows));
if (hasStaffRows && !hasAssetRows) {
result.setStaffTaskId(importBaseStaff(staffList));
return result;
}
if (!hasStaffRows) {
result.setAssetTaskId(baseStaffAssetImportService.importAssetInfo(assetList));
return result;
}
String staffTaskId = UUID.randomUUID().toString();
String assetTaskId = UUID.randomUUID().toString();
initializeImportStatus("import:baseStaff:", staffTaskId, staffList.size());
initializeImportStatus("import:baseStaffAsset:", assetTaskId, assetList.size());
result.setStaffTaskId(staffTaskId);
result.setAssetTaskId(assetTaskId);
dualSheetImportOrchestrationService.importBaseStaffWithAssetsAsync(
staffList,
staffTaskId,
assetList,
assetTaskId,
currentUserName()
);
return result;
}
/**
* 查询员工下拉列表
* 支持按员工ID或姓名模糊搜索只返回在职员工
@@ -252,6 +286,40 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
return baseStaffMapper.selectStaffOptions(query);
}
private void initializeImportStatus(String keyPrefix, String taskId, int totalCount) {
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", totalCount);
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
statusData.put("startTime", System.currentTimeMillis());
statusData.put("message", "正在处理...");
String statusKey = keyPrefix + taskId;
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, java.util.concurrent.TimeUnit.DAYS);
}
private String buildImportSubmitMessage(boolean hasStaffRows, boolean hasAssetRows) {
if (hasStaffRows && hasAssetRows) {
return "已提交员工信息和员工资产信息导入任务";
}
if (hasStaffRows) {
return "已提交员工信息导入任务";
}
return "已提交员工资产信息导入任务";
}
private String currentUserName() {
try {
return SecurityUtils.getUsername();
} catch (Exception e) {
return "system";
}
}
/**
* 构建查询条件
*/

View File

@@ -7,8 +7,11 @@ import com.ruoyi.info.collection.domain.excel.CcdiCustEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.CustEnterpriseRelationImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiCustEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.ICcdiCustEnterpriseRelationImportService;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
@@ -43,6 +46,9 @@ public class CcdiCustEnterpriseRelationImportServiceImpl implements ICcdiCustEnt
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private EnterpriseAutoFillService enterpriseAutoFillService;
@Override
@Async
@Transactional
@@ -127,6 +133,15 @@ public class CcdiCustEnterpriseRelationImportServiceImpl implements ICcdiCustEnt
// 批量插入新数据
if (!newRecords.isEmpty()) {
enterpriseAutoFillService.ensureExistsBatch(newRecords.stream()
.map(item -> new EnterpriseAutoFillService.EnterpriseFillItem(
item.getSocialCreditCode(),
item.getEnterpriseName(),
EnterpriseSource.CREDIT_CUSTOMER.getCode(),
DataSource.IMPORT.getCode(),
userName
))
.toList());
ImportLogUtils.logBatchOperationStart(log, taskId, "插入",
(newRecords.size() + 499) / 500, 500);
saveBatch(newRecords, 500);

View File

@@ -8,9 +8,12 @@ import com.ruoyi.info.collection.domain.dto.CcdiCustEnterpriseRelationEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiCustEnterpriseRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiCustEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiCustEnterpriseRelationVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiCustEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.ICcdiCustEnterpriseRelationImportService;
import com.ruoyi.info.collection.service.ICcdiCustEnterpriseRelationService;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
@@ -43,6 +46,9 @@ public class CcdiCustEnterpriseRelationServiceImpl implements ICcdiCustEnterpris
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private EnterpriseAutoFillService enterpriseAutoFillService;
/**
* 查询信贷客户实体关联列表
*
@@ -135,6 +141,14 @@ public class CcdiCustEnterpriseRelationServiceImpl implements ICcdiCustEnterpris
relation.setDataSource("MANUAL");
}
enterpriseAutoFillService.ensureExists(new EnterpriseAutoFillService.EnterpriseFillItem(
addDTO.getSocialCreditCode(),
addDTO.getEnterpriseName(),
EnterpriseSource.CREDIT_CUSTOMER.getCode(),
DataSource.MANUAL.getCode(),
SecurityUtils.getUsername()
));
int result = relationMapper.insert(relation);
return result;

View File

@@ -0,0 +1,90 @@
package com.ruoyi.info.collection.service.impl;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService;
import jakarta.annotation.Resource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 双Sheet导入后台顺序编排。
*/
@Service
public class CcdiDualSheetImportOrchestrationService {
@Resource
private ICcdiBaseStaffImportService baseStaffImportService;
@Resource
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
@Resource
private ICcdiStaffFmyRelationImportService relationImportService;
@Resource
private ICcdiAssetInfoImportService assetInfoImportService;
@Async
public void importBaseStaffWithAssetsAsync(List<CcdiBaseStaffExcel> staffList,
String staffTaskId,
List<CcdiBaseStaffAssetInfoExcel> assetList,
String assetTaskId,
String userName) {
Set<String> successIdCards = baseStaffImportService.importBaseStaffSync(staffList, staffTaskId);
baseStaffAssetImportService.importAssetInfoSync(
assetList,
assetTaskId,
userName,
buildSelfOwnerMappings(successIdCards)
);
}
@Async
public void importRelationWithAssetsAsync(List<CcdiStaffFmyRelationExcel> relationList,
String relationTaskId,
List<CcdiAssetInfoExcel> assetList,
String assetTaskId,
String userName) {
Map<String, String> successRelationMappings = relationImportService.importRelationSync(relationList, relationTaskId, userName);
assetInfoImportService.importAssetInfoSync(
assetList,
assetTaskId,
userName,
buildRelationOwnerMappings(successRelationMappings)
);
}
private Map<String, Set<String>> buildSelfOwnerMappings(Set<String> idCards) {
Map<String, Set<String>> result = new LinkedHashMap<>();
if (idCards == null || idCards.isEmpty()) {
return result;
}
for (String idCard : idCards) {
result.computeIfAbsent(idCard, key -> new LinkedHashSet<>()).add(idCard);
}
return result;
}
private Map<String, Set<String>> buildRelationOwnerMappings(Map<String, String> relationMappings) {
Map<String, Set<String>> result = new LinkedHashMap<>();
if (relationMappings == null || relationMappings.isEmpty()) {
return result;
}
for (Map.Entry<String, String> entry : relationMappings.entrySet()) {
result.computeIfAbsent(entry.getKey(), key -> new LinkedHashSet<>()).add(entry.getValue());
}
return result;
}
}

View File

@@ -131,14 +131,18 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
if (!excel.getSocialCreditCode().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) {
throw new RuntimeException("统一社会信用代码格式不正确");
}
String riskLevel = EnterpriseRiskLevel.resolveCode(StringUtils.trim(excel.getRiskLevel()));
if (riskLevel == null) {
throw new RuntimeException("风险等级不在允许范围内");
}
String entSource = EnterpriseSource.resolveCode(StringUtils.trim(excel.getEntSource()));
if (entSource == null) {
throw new RuntimeException("企业来源不在允许范围内");
}
String riskLevel = EnterpriseRiskLevel.resolveCode(StringUtils.trim(excel.getRiskLevel()));
if (riskLevel == null) {
if (EnterpriseSource.INTERMEDIARY.getCode().equals(entSource) && StringUtils.isEmpty(excel.getRiskLevel())) {
riskLevel = "1";
} else {
throw new RuntimeException("风险等级不在允许范围内");
}
}
if (existingCreditCodes.contains(excel.getSocialCreditCode())) {
throw new RuntimeException(String.format("统一社会信用代码[%s]已存在,请勿重复导入", excel.getSocialCreditCode()));

View File

@@ -3,16 +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.CcdiBizIntermediary;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation;
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.IntermediaryEnterpriseRelationImportFailureVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.ICcdiIntermediaryEnterpriseRelationImportService;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.IdCardUtil;
import com.ruoyi.common.utils.StringUtils;
@@ -54,10 +55,10 @@ public class CcdiIntermediaryEnterpriseRelationImportServiceImpl implements ICcd
private CcdiBizIntermediaryMapper intermediaryMapper;
@Resource
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedisTemplate<String, Object> redisTemplate;
private EnterpriseAutoFillService enterpriseAutoFillService;
@Override
@Async
@@ -67,7 +68,6 @@ public class CcdiIntermediaryEnterpriseRelationImportServiceImpl implements ICcd
ImportLogUtils.logImportStart(log, taskId, "中介实体关联关系", excelList.size(), userName);
Map<String, String> ownerBizIdByPersonId = getOwnerBizIdByPersonId(excelList);
Set<String> existingEnterpriseCodes = getExistingEnterpriseCodes(excelList);
Set<String> existingCombinations = getExistingRelationCombinations(ownerBizIdByPersonId, excelList);
List<CcdiIntermediaryEnterpriseRelation> successRecords = new ArrayList<>();
@@ -79,15 +79,14 @@ public class CcdiIntermediaryEnterpriseRelationImportServiceImpl implements ICcd
try {
validateExcel(excel);
String ownerBizId = ownerBizIdByPersonId.get(excel.getOwnerPersonId());
String ownerPersonId = trim(excel.getOwnerPersonId());
String socialCreditCode = trim(excel.getSocialCreditCode());
String ownerBizId = ownerBizIdByPersonId.get(ownerPersonId);
if (StringUtils.isEmpty(ownerBizId)) {
throw new RuntimeException("中介本人不存在,请先导入或维护中介本人信息");
}
if (!existingEnterpriseCodes.contains(excel.getSocialCreditCode())) {
throw new RuntimeException("统一社会信用代码不存在于系统机构表");
}
String combination = ownerBizId + "|" + excel.getSocialCreditCode();
String combination = ownerBizId + "|" + socialCreditCode;
if (existingCombinations.contains(combination)) {
throw new RuntimeException("中介实体关联关系已存在,请勿重复导入");
}
@@ -98,6 +97,9 @@ public class CcdiIntermediaryEnterpriseRelationImportServiceImpl implements ICcd
CcdiIntermediaryEnterpriseRelation relation = new CcdiIntermediaryEnterpriseRelation();
BeanUtils.copyProperties(excel, relation);
relation.setIntermediaryBizId(ownerBizId);
relation.setSocialCreditCode(socialCreditCode);
relation.setRelationPersonPost(trim(excel.getRelationPersonPost()));
relation.setRemark(trim(excel.getRemark()));
relation.setCreatedBy(userName);
relation.setUpdatedBy(userName);
successRecords.add(relation);
@@ -109,6 +111,15 @@ public class CcdiIntermediaryEnterpriseRelationImportServiceImpl implements ICcd
}
if (!successRecords.isEmpty()) {
enterpriseAutoFillService.ensureExistsBatch(successRecords.stream()
.map(item -> new EnterpriseAutoFillService.EnterpriseFillItem(
item.getSocialCreditCode(),
null,
EnterpriseSource.INTERMEDIARY.getCode(),
DataSource.IMPORT.getCode(),
userName
))
.toList());
saveBatch(successRecords, 500);
}
if (!failures.isEmpty()) {
@@ -159,6 +170,7 @@ public class CcdiIntermediaryEnterpriseRelationImportServiceImpl implements ICcd
private Map<String, String> getOwnerBizIdByPersonId(List<CcdiIntermediaryEnterpriseRelationExcel> excelList) {
List<String> ownerPersonIds = excelList.stream()
.map(CcdiIntermediaryEnterpriseRelationExcel::getOwnerPersonId)
.map(this::trim)
.filter(StringUtils::isNotEmpty)
.distinct()
.collect(Collectors.toList());
@@ -173,32 +185,16 @@ public class CcdiIntermediaryEnterpriseRelationImportServiceImpl implements ICcd
.collect(Collectors.toMap(CcdiBizIntermediary::getPersonId, CcdiBizIntermediary::getBizId, (left, right) -> left));
}
private Set<String> getExistingEnterpriseCodes(List<CcdiIntermediaryEnterpriseRelationExcel> excelList) {
List<String> socialCreditCodes = excelList.stream()
.map(CcdiIntermediaryEnterpriseRelationExcel::getSocialCreditCode)
.filter(StringUtils::isNotEmpty)
.distinct()
.collect(Collectors.toList());
if (socialCreditCodes.isEmpty()) {
return Collections.emptySet();
}
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, socialCreditCodes);
return enterpriseBaseInfoMapper.selectList(wrapper).stream()
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
.collect(Collectors.toSet());
}
private Set<String> getExistingRelationCombinations(Map<String, String> ownerBizIdByPersonId,
List<CcdiIntermediaryEnterpriseRelationExcel> excelList) {
List<String> combinations = excelList.stream()
.map(excel -> {
String ownerBizId = ownerBizIdByPersonId.get(excel.getOwnerPersonId());
if (StringUtils.isEmpty(ownerBizId) || StringUtils.isEmpty(excel.getSocialCreditCode())) {
String ownerBizId = ownerBizIdByPersonId.get(trim(excel.getOwnerPersonId()));
String socialCreditCode = trim(excel.getSocialCreditCode());
if (StringUtils.isEmpty(ownerBizId) || StringUtils.isEmpty(socialCreditCode)) {
return null;
}
return ownerBizId + "|" + excel.getSocialCreditCode();
return ownerBizId + "|" + socialCreditCode;
})
.filter(StringUtils::isNotEmpty)
.distinct()
@@ -210,24 +206,33 @@ public class CcdiIntermediaryEnterpriseRelationImportServiceImpl implements ICcd
}
private void validateExcel(CcdiIntermediaryEnterpriseRelationExcel excel) {
if (StringUtils.isEmpty(excel.getOwnerPersonId())) {
String ownerPersonId = trim(excel.getOwnerPersonId());
String socialCreditCode = trim(excel.getSocialCreditCode());
String relationPersonPost = trim(excel.getRelationPersonPost());
String remark = trim(excel.getRemark());
if (StringUtils.isEmpty(ownerPersonId)) {
throw new RuntimeException("中介本人证件号码不能为空");
}
if (StringUtils.isEmpty(excel.getSocialCreditCode())) {
if (StringUtils.isEmpty(socialCreditCode)) {
throw new RuntimeException("统一社会信用代码不能为空");
}
String ownerPersonIdError = IdCardUtil.getErrorMessage(excel.getOwnerPersonId());
String ownerPersonIdError = IdCardUtil.getErrorMessage(ownerPersonId);
if (ownerPersonIdError != null) {
throw new RuntimeException("中介本人证件号码" + ownerPersonIdError);
}
if (StringUtils.isNotEmpty(excel.getRelationPersonPost()) && excel.getRelationPersonPost().length() > 100) {
throw new RuntimeException("关联职务长度不能超过100个字符");
if (StringUtils.isNotEmpty(relationPersonPost) && relationPersonPost.length() > 100) {
throw new RuntimeException("关联职务长度不能超过100个字符");
}
if (StringUtils.isNotEmpty(excel.getRemark()) && excel.getRemark().length() > 500) {
if (StringUtils.isNotEmpty(remark) && remark.length() > 500) {
throw new RuntimeException("备注长度不能超过500个字符");
}
}
private String trim(String value) {
return value == null ? null : value.trim();
}
private IntermediaryEnterpriseRelationImportFailureVO createFailureVO(CcdiIntermediaryEnterpriseRelationExcel excel,
String errorMessage) {
IntermediaryEnterpriseRelationImportFailureVO failure = new IntermediaryEnterpriseRelationImportFailureVO();

View File

@@ -14,6 +14,8 @@ import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEntityDetailVO;
import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryPersonDetailVO;
import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryRelativeVO;
import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
@@ -22,6 +24,7 @@ import com.ruoyi.info.collection.service.ICcdiIntermediaryEnterpriseRelationImpo
import com.ruoyi.info.collection.service.ICcdiIntermediaryEntityImportService;
import com.ruoyi.info.collection.service.ICcdiIntermediaryPersonImportService;
import com.ruoyi.info.collection.service.ICcdiIntermediaryService;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
@@ -69,6 +72,9 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private EnterpriseAutoFillService enterpriseAutoFillService;
/**
* 分页查询中介列表
* 使用XML联合查询实现,支持个人中介和实体中介的灵活查询
@@ -302,6 +308,13 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
CcdiIntermediaryEnterpriseRelation relation = new CcdiIntermediaryEnterpriseRelation();
BeanUtils.copyProperties(addDTO, relation);
relation.setIntermediaryBizId(owner.getBizId());
enterpriseAutoFillService.ensureExists(new EnterpriseAutoFillService.EnterpriseFillItem(
addDTO.getSocialCreditCode(),
null,
EnterpriseSource.INTERMEDIARY.getCode(),
DataSource.MANUAL.getCode(),
SecurityUtils.getUsername()
));
return enterpriseRelationMapper.insert(relation);
}
@@ -317,6 +330,13 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
CcdiIntermediaryEnterpriseRelation relation = new CcdiIntermediaryEnterpriseRelation();
BeanUtils.copyProperties(editDTO, relation);
relation.setIntermediaryBizId(existing.getIntermediaryBizId());
enterpriseAutoFillService.ensureExists(new EnterpriseAutoFillService.EnterpriseFillItem(
editDTO.getSocialCreditCode(),
null,
EnterpriseSource.INTERMEDIARY.getCode(),
DataSource.MANUAL.getCode(),
SecurityUtils.getUsername()
));
return enterpriseRelationMapper.updateById(relation);
}
@@ -520,9 +540,6 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
private void validateEnterpriseRelation(String bizId, String socialCreditCode, Long excludeId) {
requireIntermediaryPerson(bizId);
if (enterpriseBaseInfoMapper.selectById(socialCreditCode) == null) {
throw new RuntimeException("关联机构不存在");
}
boolean exists = enterpriseRelationMapper.existsByIntermediaryBizIdAndSocialCreditCode(bizId, socialCreditCode);
if (exists) {
if (excludeId == null) {

View File

@@ -9,9 +9,12 @@ import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExc
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.PurchaseTransactionImportFailureVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionMapper;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionSupplierMapper;
import com.ruoyi.info.collection.service.ICcdiPurchaseTransactionImportService;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
@@ -53,6 +56,9 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private EnterpriseAutoFillService enterpriseAutoFillService;
@Override
@Async
@Transactional
@@ -183,6 +189,7 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
// 批量插入新数据
if (!newTransactions.isEmpty()) {
autoFillSupplierEnterprises(newSuppliers, userName);
ImportLogUtils.logBatchOperationStart(log, taskId, "插入",
(newTransactions.size() + 499) / 500, 500);
saveBatch(newTransactions, 500);
@@ -328,6 +335,19 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
}
}
private void autoFillSupplierEnterprises(List<CcdiPurchaseTransactionSupplier> supplierList, String userName) {
enterpriseAutoFillService.ensureExistsBatch(supplierList.stream()
.filter(item -> StringUtils.isNotEmpty(item.getSupplierUscc()))
.map(item -> new EnterpriseAutoFillService.EnterpriseFillItem(
item.getSupplierUscc(),
item.getSupplierName(),
EnterpriseSource.SUPPLIER.getCode(),
DataSource.IMPORT.getCode(),
userName
))
.toList());
}
/**
* 验证采购交易数据
*

View File

@@ -11,10 +11,13 @@ import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionSupplierVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionMapper;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionSupplierMapper;
import com.ruoyi.info.collection.service.ICcdiPurchaseTransactionImportService;
import com.ruoyi.info.collection.service.ICcdiPurchaseTransactionService;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
@@ -55,6 +58,9 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private EnterpriseAutoFillService enterpriseAutoFillService;
/**
* 查询采购交易列表
*
@@ -134,6 +140,7 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
BeanUtils.copyProperties(addDTO, transaction);
fillWinnerSummary(transaction, supplierList);
autoFillSupplierEnterprises(supplierList, DataSource.MANUAL.getCode(), SecurityUtils.getUsername());
int result = transactionMapper.insert(transaction);
saveSuppliers(supplierList);
@@ -331,6 +338,21 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
}
}
private void autoFillSupplierEnterprises(List<CcdiPurchaseTransactionSupplier> supplierList,
String dataSource,
String userName) {
enterpriseAutoFillService.ensureExistsBatch(supplierList.stream()
.filter(item -> StringUtils.isNotEmpty(item.getSupplierUscc()))
.map(item -> new EnterpriseAutoFillService.EnterpriseFillItem(
item.getSupplierUscc(),
item.getSupplierName(),
EnterpriseSource.SUPPLIER.getCode(),
dataSource,
userName
))
.toList());
}
private List<CcdiPurchaseTransactionSupplierVO> selectSupplierListByPurchaseId(String purchaseId) {
return supplierMapper.selectList(
new LambdaQueryWrapper<CcdiPurchaseTransactionSupplier>()

View File

@@ -9,9 +9,12 @@ import com.ruoyi.info.collection.domain.excel.CcdiStaffEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.StaffEnterpriseRelationImportFailureVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.ICcdiStaffEnterpriseRelationImportService;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
@@ -49,6 +52,9 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
@Resource
private CcdiStaffFmyRelationMapper familyRelationMapper;
@Resource
private EnterpriseAutoFillService enterpriseAutoFillService;
@Override
@Async
@Transactional
@@ -147,6 +153,15 @@ public class CcdiStaffEnterpriseRelationImportServiceImpl implements ICcdiStaffE
// 批量插入新数据
if (!newRecords.isEmpty()) {
enterpriseAutoFillService.ensureExistsBatch(newRecords.stream()
.map(item -> new EnterpriseAutoFillService.EnterpriseFillItem(
item.getSocialCreditCode(),
item.getEnterpriseName(),
EnterpriseSource.EMP_RELATION.getCode(),
DataSource.IMPORT.getCode(),
userName
))
.toList());
ImportLogUtils.logBatchOperationStart(log, taskId, "插入",
(newRecords.size() + 499) / 500, 500);
saveBatch(newRecords, 500);

View File

@@ -10,10 +10,13 @@ import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiStaffEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationOptionVO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.ICcdiStaffEnterpriseRelationImportService;
import com.ruoyi.info.collection.service.ICcdiStaffEnterpriseRelationService;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
@@ -49,6 +52,9 @@ public class CcdiStaffEnterpriseRelationServiceImpl implements ICcdiStaffEnterpr
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private EnterpriseAutoFillService enterpriseAutoFillService;
/**
* 查询员工实体关系列表
*
@@ -144,6 +150,14 @@ public class CcdiStaffEnterpriseRelationServiceImpl implements ICcdiStaffEnterpr
relation.setDataSource("MANUAL");
}
enterpriseAutoFillService.ensureExists(new EnterpriseAutoFillService.EnterpriseFillItem(
addDTO.getSocialCreditCode(),
addDTO.getEnterpriseName(),
EnterpriseSource.EMP_RELATION.getCode(),
DataSource.MANUAL.getCode(),
SecurityUtils.getUsername()
));
int result = relationMapper.insert(relation);
return result;

View File

@@ -57,6 +57,12 @@ public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelat
@Async
@Transactional
public void importRelationAsync(List<CcdiStaffFmyRelationExcel> excelList, String taskId, String userName) {
importRelationSync(excelList, taskId, userName);
}
@Override
@Transactional
public Map<String, String> importRelationSync(List<CcdiStaffFmyRelationExcel> excelList, String taskId, String userName) {
long startTime = System.currentTimeMillis();
// 记录导入开始
@@ -213,6 +219,15 @@ public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelat
long duration = System.currentTimeMillis() - startTime;
ImportLogUtils.logImportComplete(log, taskId, "员工亲属关系",
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
return newRecords.stream()
.filter(item -> StringUtils.isNotEmpty(item.getRelationCertNo()) && StringUtils.isNotEmpty(item.getPersonId()))
.collect(Collectors.toMap(
CcdiStaffFmyRelation::getRelationCertNo,
CcdiStaffFmyRelation::getPersonId,
(left, right) -> left,
LinkedHashMap::new
));
}
/**

View File

@@ -6,11 +6,14 @@ import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiAssetInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiStaffFmyRelationVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService;
import com.ruoyi.info.collection.service.ICcdiAssetInfoService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationService;
@@ -51,9 +54,15 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
@Resource
private ICcdiAssetInfoService assetInfoService;
@Resource
private ICcdiAssetInfoImportService assetInfoImportService;
@Resource
private CcdiStaffEnterpriseRelationMapper staffEnterpriseRelationMapper;
@Resource
private CcdiDualSheetImportOrchestrationService dualSheetImportOrchestrationService;
/**
* 查询员工亲属关系列表
*
@@ -207,25 +216,11 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
// 生成任务ID
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
// 获取当前用户名
String userName = SecurityUtils.getUsername();
// 初始化Redis状态
String statusKey = "import:staffFmyRelation:" + taskId;
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", excelList.size());
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
statusData.put("startTime", startTime);
statusData.put("message", "正在处理...");
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
initializeImportStatus("import:staffFmyRelation:", taskId, excelList.size());
// 调用异步导入服务
relationImportService.importRelationAsync(excelList, taskId, userName);
@@ -233,6 +228,79 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
return taskId;
}
@Override
@Transactional
public StaffFmyRelationImportSubmitResultVO importRelationWithAssets(List<CcdiStaffFmyRelationExcel> relationList,
List<CcdiAssetInfoExcel> assetList) {
boolean hasRelationRows = relationList != null && !relationList.isEmpty();
boolean hasAssetRows = assetList != null && !assetList.isEmpty();
if (!hasRelationRows && !hasAssetRows) {
throw new RuntimeException("至少需要一条数据");
}
StaffFmyRelationImportSubmitResultVO result = new StaffFmyRelationImportSubmitResultVO();
result.setMessage(buildImportSubmitMessage(hasRelationRows, hasAssetRows));
if (hasRelationRows && !hasAssetRows) {
result.setRelationTaskId(importRelation(relationList));
return result;
}
if (!hasRelationRows) {
result.setAssetTaskId(assetInfoImportService.importAssetInfo(assetList));
return result;
}
String relationTaskId = UUID.randomUUID().toString();
String assetTaskId = UUID.randomUUID().toString();
initializeImportStatus("import:staffFmyRelation:", relationTaskId, relationList.size());
initializeImportStatus("import:assetInfo:", assetTaskId, assetList.size());
result.setRelationTaskId(relationTaskId);
result.setAssetTaskId(assetTaskId);
dualSheetImportOrchestrationService.importRelationWithAssetsAsync(
relationList,
relationTaskId,
assetList,
assetTaskId,
currentUserName()
);
return result;
}
private void initializeImportStatus(String keyPrefix, String taskId, int totalCount) {
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", totalCount);
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
statusData.put("startTime", System.currentTimeMillis());
statusData.put("message", "正在处理...");
String statusKey = keyPrefix + taskId;
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
}
private String buildImportSubmitMessage(boolean hasRelationRows, boolean hasAssetRows) {
if (hasRelationRows && hasAssetRows) {
return "已提交员工亲属关系和亲属资产信息导入任务";
}
if (hasRelationRows) {
return "已提交员工亲属关系导入任务";
}
return "已提交亲属资产信息导入任务";
}
private String currentUserName() {
try {
return SecurityUtils.getUsername();
} catch (Exception e) {
return "system";
}
}
private CcdiAssetInfoVO toAssetInfoVO(CcdiAssetInfo assetInfo) {
CcdiAssetInfoVO assetInfoVO = new CcdiAssetInfoVO();
BeanUtils.copyProperties(assetInfo, assetInfoVO);

View File

@@ -165,12 +165,8 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
return new MainImportResult(Collections.emptyMap(), 0);
}
Set<String> existingRecruitIds = getExistingRecruitIds(
mainRows.stream().map(MainImportRow::data).toList()
);
Set<String> processedRecruitIds = new HashSet<>();
List<CcdiStaffRecruitment> newRecords = new ArrayList<>();
Map<String, CcdiStaffRecruitment> importedRecruitmentMap = new LinkedHashMap<>();
Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> importedRecruitmentMap = new LinkedHashMap<>();
int successCount = 0;
for (int index = 0; index < mainRows.size(); index++) {
MainImportRow mainRow = mainRows.get(index);
@@ -178,36 +174,22 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
try {
CcdiStaffRecruitmentAddDTO addDTO = new CcdiStaffRecruitmentAddDTO();
BeanUtils.copyProperties(excel, addDTO);
addDTO.setRecruitType(RecruitType.inferCode(addDTO.getRecruitName()));
addDTO.setRecruitType(normalizeRecruitType(excel.getRecruitType()));
validateRecruitmentData(addDTO, mainRow.sheetRowNum());
String recruitId = trim(excel.getRecruitId());
if (existingRecruitIds.contains(recruitId)) {
throw buildValidationException(
MAIN_SHEET_NAME,
List.of(mainRow.sheetRowNum()),
String.format("招聘记录编号[%s]已存在,请勿重复导入", recruitId)
);
}
if (!processedRecruitIds.add(recruitId)) {
throw buildValidationException(
MAIN_SHEET_NAME,
List.of(mainRow.sheetRowNum()),
String.format("招聘记录编号[%s]在导入文件中重复,已跳过此条记录", recruitId)
);
}
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
BeanUtils.copyProperties(excel, recruitment);
recruitment.setRecruitId(recruitId);
recruitment.setRecruitType(addDTO.getRecruitType());
recruitment.setCreatedBy(userName);
recruitment.setUpdatedBy(userName);
newRecords.add(recruitment);
importedRecruitmentMap.put(recruitId, recruitment);
recruitmentMapper.insert(recruitment);
successCount++;
addRecruitment(importedRecruitmentMap, recruitment);
ImportLogUtils.logProgress(log, taskId, index + 1, mainRows.size(), newRecords.size(), failures.size());
ImportLogUtils.logProgress(log, taskId, index + 1, mainRows.size(), successCount, failures.size());
} catch (Exception exception) {
FailureMeta failureMeta = resolveFailureMeta(exception, List.of(mainRow.sheetRowNum()), MAIN_SHEET_NAME);
failures.add(buildFailure(excel, failureMeta.sheetName(), failureMeta.sheetRowNum(), exception.getMessage()));
@@ -221,16 +203,11 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
}
}
if (!newRecords.isEmpty()) {
ImportLogUtils.logBatchOperationStart(log, taskId, "插入招聘信息", (newRecords.size() + 499) / 500, 500);
saveBatch(newRecords, 500);
}
return new MainImportResult(importedRecruitmentMap, newRecords.size());
return new MainImportResult(importedRecruitmentMap, successCount);
}
private int importWorkSheet(List<WorkImportRow> workRows,
Map<String, CcdiStaffRecruitment> importedRecruitmentMap,
Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> importedRecruitmentMap,
List<RecruitmentImportFailureVO> failures,
String userName,
String taskId) {
@@ -238,7 +215,7 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
return 0;
}
Map<String, CcdiStaffRecruitment> existingRecruitmentMap =
Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> existingRecruitmentMap =
getExistingRecruitmentMap(workRows, importedRecruitmentMap);
Map<String, List<WorkImportRow>> groupedRows = groupWorkRows(workRows);
int successCount = 0;
@@ -248,15 +225,18 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
processedGroups++;
WorkImportRow firstRow = recruitWorkRows.get(0);
String recruitId = trim(firstRow.data().getRecruitId());
CcdiStaffRecruitment recruitment = importedRecruitmentMap.get(recruitId);
if (recruitment == null) {
recruitment = existingRecruitmentMap.get(recruitId);
}
try {
RecruitmentMatchKey matchKey = buildMatchKey(firstRow.data());
CcdiStaffRecruitment recruitment = resolveMatchedRecruitment(
matchKey,
importedRecruitmentMap,
existingRecruitmentMap,
extractWorkRowNums(recruitWorkRows)
);
validateWorkGroup(recruitWorkRows, recruitment);
if (StringUtils.isNotEmpty(recruitId) && hasExistingWorkHistory(recruitId)) {
if (recruitment != null && hasExistingWorkHistory(recruitment.getId())) {
throw buildValidationException(
WORK_SHEET_NAME,
extractWorkRowNums(recruitWorkRows),
@@ -264,7 +244,7 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
);
}
List<CcdiStaffRecruitmentWork> entities = buildWorkEntities(recruitWorkRows, userName);
List<CcdiStaffRecruitmentWork> entities = buildWorkEntities(recruitWorkRows, recruitment, userName);
entities.forEach(entity -> recruitmentWorkMapper.insert(entity));
successCount += recruitWorkRows.size();
@@ -299,33 +279,59 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
}
private String buildWorkGroupKey(WorkImportRow workRow) {
String recruitId = trim(workRow.data().getRecruitId());
if (StringUtils.isNotEmpty(recruitId)) {
return recruitId;
RecruitmentMatchKey key = buildMatchKey(workRow.data());
if (key.isComplete()) {
return key.value();
}
return "__ROW__" + workRow.sheetRowNum();
}
private Map<String, CcdiStaffRecruitment> getExistingRecruitmentMap(List<WorkImportRow> workRows,
Map<String, CcdiStaffRecruitment> importedRecruitmentMap) {
private Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> getExistingRecruitmentMap(
List<WorkImportRow> workRows,
Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> importedRecruitmentMap
) {
LinkedHashSet<String> recruitIds = workRows.stream()
.filter(row -> !importedRecruitmentMap.containsKey(buildMatchKey(row.data())))
.map(row -> trim(row.data().getRecruitId()))
.filter(StringUtils::isNotEmpty)
.filter(recruitId -> !importedRecruitmentMap.containsKey(recruitId))
.collect(Collectors.toCollection(LinkedHashSet::new));
if (recruitIds.isEmpty()) {
return Collections.emptyMap();
}
List<CcdiStaffRecruitment> recruitments = recruitmentMapper.selectBatchIds(recruitIds);
return recruitments.stream().collect(Collectors.toMap(CcdiStaffRecruitment::getRecruitId, item -> item));
List<CcdiStaffRecruitment> recruitments = selectRecruitmentsByRecruitIds(recruitIds);
Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> result = new LinkedHashMap<>();
recruitments.forEach(item -> addRecruitment(result, item));
return result;
}
private List<CcdiStaffRecruitmentWork> buildWorkEntities(List<WorkImportRow> workRows, String userName) {
private CcdiStaffRecruitment resolveMatchedRecruitment(
RecruitmentMatchKey matchKey,
Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> importedRecruitmentMap,
Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> existingRecruitmentMap,
List<Integer> rowNums
) {
List<CcdiStaffRecruitment> matchedRecruitments = new ArrayList<>();
matchedRecruitments.addAll(importedRecruitmentMap.getOrDefault(matchKey, Collections.emptyList()));
matchedRecruitments.addAll(existingRecruitmentMap.getOrDefault(matchKey, Collections.emptyList()));
if (matchedRecruitments.size() > 1) {
throw buildValidationException(
WORK_SHEET_NAME,
rowNums,
String.format("招聘记录编号[%s]匹配到多条招聘主信息,无法确定历史工作经历归属", matchKey.recruitId())
);
}
return matchedRecruitments.isEmpty() ? null : matchedRecruitments.get(0);
}
private List<CcdiStaffRecruitmentWork> buildWorkEntities(List<WorkImportRow> workRows,
CcdiStaffRecruitment recruitment,
String userName) {
List<CcdiStaffRecruitmentWork> entities = new ArrayList<>();
for (WorkImportRow workRow : workRows) {
CcdiStaffRecruitmentWork entity = new CcdiStaffRecruitmentWork();
BeanUtils.copyProperties(workRow.data(), entity);
entity.setRecruitId(trim(workRow.data().getRecruitId()));
entity.setRecruitmentId(recruitment.getId());
entity.setRecruitId(recruitment.getRecruitId());
entity.setCreatedBy(userName);
entity.setUpdatedBy(userName);
entities.add(entity);
@@ -333,29 +339,9 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
return entities;
}
private Set<String> getExistingRecruitIds(List<CcdiStaffRecruitmentExcel> recruitmentList) {
List<String> recruitIds = recruitmentList.stream()
.map(CcdiStaffRecruitmentExcel::getRecruitId)
.map(this::trim)
.filter(StringUtils::isNotEmpty)
.toList();
if (recruitIds.isEmpty()) {
return Collections.emptySet();
}
LambdaQueryWrapper<CcdiStaffRecruitment> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CcdiStaffRecruitment::getRecruitId, recruitIds);
List<CcdiStaffRecruitment> existingRecruitments = recruitmentMapper.selectList(wrapper);
return existingRecruitments.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
}
private boolean hasExistingWorkHistory(String recruitId) {
private boolean hasExistingWorkHistory(Long recruitmentId) {
LambdaQueryWrapper<CcdiStaffRecruitmentWork> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, recruitId);
wrapper.eq(CcdiStaffRecruitmentWork::getRecruitmentId, recruitmentId);
return recruitmentWorkMapper.selectCount(wrapper) > 0;
}
@@ -376,22 +362,22 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位描述不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandName())) {
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员姓名不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "候选人姓名不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandEdu())) {
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员学历不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "学历不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandId())) {
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "证件号码不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandSchool())) {
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员毕业院校不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "毕业院校不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandMajor())) {
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员专业不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "专业不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandGrad())) {
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员毕业年月不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "毕业年月不能为空");
}
if (StringUtils.isEmpty(addDTO.getAdmitStatus())) {
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "录用情况不能为空");
@@ -414,10 +400,23 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
}
if (RecruitType.getDescByCode(addDTO.getRecruitType()) == null) {
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘类型只能填写'SOCIAL'或'CAMPUS'");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘类型只能填写'SOCIAL/社招'或'CAMPUS/校招'");
}
}
private String normalizeRecruitType(String recruitType) {
String value = trim(recruitType);
if (StringUtils.isEmpty(value)) {
return value;
}
for (RecruitType type : RecruitType.values()) {
if (type.getCode().equals(value) || type.getDesc().equals(value)) {
return type.getCode();
}
}
return value;
}
private void validateWorkGroup(List<WorkImportRow> workRows, CcdiStaffRecruitment recruitment) {
Set<Integer> processedSortOrders = new HashSet<>();
for (WorkImportRow workRow : workRows) {
@@ -451,14 +450,14 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "工作单位不能为空");
}
if (StringUtils.isEmpty(trim(excel.getPositionName()))) {
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "岗位不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "岗位名称不能为空");
}
if (StringUtils.isEmpty(trim(excel.getJobStartMonth()))) {
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "入职年月不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "入职时间不能为空");
}
validateMonth(excel.getJobStartMonth(), "入职年月", sheetRowNum);
validateMonth(excel.getJobStartMonth(), "入职时间", sheetRowNum);
if (StringUtils.isNotEmpty(trim(excel.getJobEndMonth()))) {
validateMonth(excel.getJobEndMonth(), "离职年月", sheetRowNum);
validateMonth(excel.getJobEndMonth(), "离职时间", sheetRowNum);
}
if (recruitment == null) {
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不存在,请先维护招聘主信息");
@@ -555,30 +554,36 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
redisTemplate.opsForHash().putAll(key, statusData);
}
private void saveBatch(List<CcdiStaffRecruitment> list, int batchSize) {
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<CcdiStaffRecruitment> subList = list.subList(i, end);
List<String> recruitIds = subList.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.toList();
if (recruitIds.isEmpty()) {
continue;
}
List<CcdiStaffRecruitment> existingRecords = recruitmentMapper.selectBatchIds(recruitIds);
Set<String> existingIds = existingRecords.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
List<CcdiStaffRecruitment> toInsert = subList.stream()
.filter(record -> !existingIds.contains(record.getRecruitId()))
.toList();
if (!toInsert.isEmpty()) {
recruitmentMapper.insertBatch(toInsert);
}
private List<CcdiStaffRecruitment> selectRecruitmentsByRecruitIds(Set<String> recruitIds) {
if (recruitIds == null || recruitIds.isEmpty()) {
return Collections.emptyList();
}
LambdaQueryWrapper<CcdiStaffRecruitment> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CcdiStaffRecruitment::getRecruitId, recruitIds);
return recruitmentMapper.selectList(wrapper);
}
private void addRecruitment(Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> map,
CcdiStaffRecruitment recruitment) {
map.computeIfAbsent(buildMatchKey(recruitment), key -> new ArrayList<>()).add(recruitment);
}
private RecruitmentMatchKey buildMatchKey(CcdiStaffRecruitment recruitment) {
return new RecruitmentMatchKey(
trim(recruitment.getRecruitId()),
trim(recruitment.getCandName()),
trim(recruitment.getRecruitName()),
trim(recruitment.getPosName())
);
}
private RecruitmentMatchKey buildMatchKey(CcdiStaffRecruitmentWorkExcel excel) {
return new RecruitmentMatchKey(
trim(excel.getRecruitId()),
trim(excel.getCandName()),
trim(excel.getRecruitName()),
trim(excel.getPosName())
);
}
private List<MainImportRow> buildMainImportRows(List<CcdiStaffRecruitmentExcel> recruitmentList) {
@@ -628,10 +633,25 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
private record WorkImportRow(CcdiStaffRecruitmentWorkExcel data, int sheetRowNum) {}
private record MainImportResult(Map<String, CcdiStaffRecruitment> importedRecruitmentMap, int successCount) {}
private record MainImportResult(Map<RecruitmentMatchKey, List<CcdiStaffRecruitment>> importedRecruitmentMap,
int successCount) {}
private record FailureMeta(String sheetName, String sheetRowNum) {}
private record RecruitmentMatchKey(String recruitId, String candName, String recruitName, String posName) {
private boolean isComplete() {
return StringUtils.isNotEmpty(recruitId)
&& StringUtils.isNotEmpty(candName)
&& StringUtils.isNotEmpty(recruitName)
&& StringUtils.isNotEmpty(posName);
}
private String value() {
return String.join("|", recruitId, candName, recruitName, posName);
}
}
private static class ImportValidationException extends RuntimeException {
private final String sheetName;

View File

@@ -1,6 +1,7 @@
package com.ruoyi.info.collection.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.CcdiStaffRecruitment;
import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork;
@@ -27,6 +28,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -108,15 +110,15 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
/**
* 查询招聘信息详情
*
* @param recruitId 招聘记录编号
* @param id 主键ID
* @return 招聘信息VO
*/
@Override
public CcdiStaffRecruitmentVO selectRecruitmentById(String recruitId) {
CcdiStaffRecruitmentVO vo = recruitmentMapper.selectRecruitmentById(recruitId);
public CcdiStaffRecruitmentVO selectRecruitmentById(Long id) {
CcdiStaffRecruitmentVO vo = recruitmentMapper.selectRecruitmentById(id);
if (vo != null) {
vo.setAdmitStatusDesc(AdmitStatus.getDescByCode(vo.getAdmitStatus()));
vo.setWorkExperienceList(selectWorkExperienceList(recruitId));
vo.setWorkExperienceList(selectWorkExperienceList(vo.getId()));
}
return vo;
}
@@ -130,15 +132,14 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
@Override
@Transactional
public int insertRecruitment(CcdiStaffRecruitmentAddDTO addDTO) {
// 检查招聘记录编号唯一性
if (recruitmentMapper.selectById(addDTO.getRecruitId()) != null) {
throw new RuntimeException("该招聘记录编号已存在");
}
String recruitId = trim(addDTO.getRecruitId());
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
BeanUtils.copyProperties(addDTO, recruitment);
int result = recruitmentMapper.insert(recruitment);
recruitment.setRecruitId(recruitId);
int result = recruitmentMapper.insert(recruitment);
insertWorkExperienceList(recruitment.getId(), recruitId, addDTO.getRecruitType(), addDTO.getWorkExperienceList());
return result;
}
@@ -151,9 +152,20 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
@Override
@Transactional
public int updateRecruitment(CcdiStaffRecruitmentEditDTO editDTO) {
CcdiStaffRecruitment existing = recruitmentMapper.selectById(editDTO.getId());
if (existing == null) {
throw new RuntimeException("招聘信息不存在");
}
String recruitId = trim(editDTO.getRecruitId());
editDTO.setRecruitId(recruitId);
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
BeanUtils.copyProperties(editDTO, recruitment);
int result = recruitmentMapper.updateById(recruitment);
if (!Objects.equals(existing.getRecruitId(), recruitId)) {
updateWorkRecruitId(editDTO.getId(), recruitId);
}
replaceWorkExperienceList(editDTO);
return result;
@@ -162,16 +174,19 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
/**
* 批量删除招聘信息
*
* @param recruitIds 需要删除的招聘记录编号
* @param ids 需要删除的招聘信息ID
* @return 结果
*/
@Override
@Transactional
public int deleteRecruitmentByIds(String[] recruitIds) {
LambdaQueryWrapper<CcdiStaffRecruitmentWork> workWrapper = new LambdaQueryWrapper<>();
workWrapper.in(CcdiStaffRecruitmentWork::getRecruitId, List.of(recruitIds));
recruitmentWorkMapper.delete(workWrapper);
return recruitmentMapper.deleteBatchIds(List.of(recruitIds));
public int deleteRecruitmentByIds(Long[] ids) {
List<Long> idList = Arrays.asList(ids);
if (!idList.isEmpty()) {
LambdaQueryWrapper<CcdiStaffRecruitmentWork> workWrapper = new LambdaQueryWrapper<>();
workWrapper.in(CcdiStaffRecruitmentWork::getRecruitmentId, idList);
recruitmentWorkMapper.delete(workWrapper);
}
return recruitmentMapper.deleteBatchIds(idList);
}
/**
@@ -216,9 +231,9 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
return taskId;
}
private List<CcdiStaffRecruitmentWorkVO> selectWorkExperienceList(String recruitId) {
private List<CcdiStaffRecruitmentWorkVO> selectWorkExperienceList(Long recruitmentId) {
LambdaQueryWrapper<CcdiStaffRecruitmentWork> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, recruitId)
wrapper.eq(CcdiStaffRecruitmentWork::getRecruitmentId, recruitmentId)
.orderByAsc(CcdiStaffRecruitmentWork::getSortOrder)
.orderByDesc(CcdiStaffRecruitmentWork::getId);
List<CcdiStaffRecruitmentWork> workList = recruitmentWorkMapper.selectList(wrapper);
@@ -232,9 +247,20 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
}).toList();
}
private void updateWorkRecruitId(Long recruitmentId, String newRecruitId) {
LambdaUpdateWrapper<CcdiStaffRecruitmentWork> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(CcdiStaffRecruitmentWork::getRecruitmentId, recruitmentId)
.set(CcdiStaffRecruitmentWork::getRecruitId, newRecruitId);
recruitmentWorkMapper.update(null, updateWrapper);
}
private String trim(String value) {
return value == null ? null : value.trim();
}
private void replaceWorkExperienceList(CcdiStaffRecruitmentEditDTO editDTO) {
LambdaQueryWrapper<CcdiStaffRecruitmentWork> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, editDTO.getRecruitId());
deleteWrapper.eq(CcdiStaffRecruitmentWork::getRecruitmentId, editDTO.getId());
if (!Objects.equals(RecruitType.SOCIAL.getCode(), editDTO.getRecruitType())) {
recruitmentWorkMapper.delete(deleteWrapper);
@@ -246,12 +272,28 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
}
recruitmentWorkMapper.delete(deleteWrapper);
List<CcdiStaffRecruitmentWork> workList = buildWorkExperienceEntities(editDTO);
List<CcdiStaffRecruitmentWork> workList = buildWorkExperienceEntities(
editDTO.getId(),
editDTO.getRecruitId(),
editDTO.getWorkExperienceList()
);
workList.forEach(recruitmentWorkMapper::insert);
}
private List<CcdiStaffRecruitmentWork> buildWorkExperienceEntities(CcdiStaffRecruitmentEditDTO editDTO) {
List<CcdiStaffRecruitmentWorkEditDTO> workExperienceList = editDTO.getWorkExperienceList();
private void insertWorkExperienceList(Long recruitmentId,
String recruitId,
String recruitType,
List<CcdiStaffRecruitmentWorkEditDTO> workExperienceList) {
if (!Objects.equals(RecruitType.SOCIAL.getCode(), recruitType)) {
return;
}
List<CcdiStaffRecruitmentWork> workList = buildWorkExperienceEntities(recruitmentId, recruitId, workExperienceList);
workList.forEach(recruitmentWorkMapper::insert);
}
private List<CcdiStaffRecruitmentWork> buildWorkExperienceEntities(Long recruitmentId,
String recruitId,
List<CcdiStaffRecruitmentWorkEditDTO> workExperienceList) {
if (workExperienceList == null || workExperienceList.isEmpty()) {
return new ArrayList<>();
}
@@ -264,7 +306,8 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
}
CcdiStaffRecruitmentWork work = new CcdiStaffRecruitmentWork();
BeanUtils.copyProperties(item, work);
work.setRecruitId(editDTO.getRecruitId());
work.setRecruitmentId(recruitmentId);
work.setRecruitId(recruitId);
work.setSortOrder(i + 1);
entityList.add(work);
}

View File

@@ -0,0 +1,130 @@
package com.ruoyi.info.collection.service.support;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 关联业务实体库自动补全服务。
*/
@Service
public class EnterpriseAutoFillService {
private static final int BATCH_SIZE = 500;
@Resource
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
public record EnterpriseFillItem(
String socialCreditCode,
String enterpriseName,
String entSource,
String dataSource,
String userName
) {
}
@Transactional
public void ensureExists(EnterpriseFillItem item) {
ensureExistsBatch(List.of(item));
}
@Transactional
public void ensureExistsBatch(List<EnterpriseFillItem> items) {
if (StringUtils.isEmpty(items)) {
return;
}
Map<String, EnterpriseFillItem> normalizedItems = normalizeItems(items);
if (normalizedItems.isEmpty()) {
return;
}
Set<String> existingCodes = enterpriseBaseInfoMapper.selectBatchIds(new ArrayList<>(normalizedItems.keySet()))
.stream()
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
.collect(Collectors.toSet());
List<CcdiEnterpriseBaseInfo> missingEntities = normalizedItems.entrySet().stream()
.filter(entry -> !existingCodes.contains(entry.getKey()))
.map(entry -> buildEntity(entry.getKey(), entry.getValue()))
.toList();
if (missingEntities.isEmpty()) {
return;
}
insertBatchIgnoreDuplicate(missingEntities);
}
private Map<String, EnterpriseFillItem> normalizeItems(List<EnterpriseFillItem> items) {
Map<String, EnterpriseFillItem> normalizedItems = new LinkedHashMap<>();
for (EnterpriseFillItem item : items) {
if (item == null || StringUtils.isEmpty(item.socialCreditCode())) {
continue;
}
String socialCreditCode = item.socialCreditCode().trim();
normalizedItems.putIfAbsent(socialCreditCode, new EnterpriseFillItem(
socialCreditCode,
trimToNull(item.enterpriseName()),
trimToNull(item.entSource()),
trimToNull(item.dataSource()),
trimToNull(item.userName())
));
}
return normalizedItems;
}
private CcdiEnterpriseBaseInfo buildEntity(String socialCreditCode, EnterpriseFillItem item) {
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
entity.setSocialCreditCode(socialCreditCode);
entity.setEnterpriseName(item.enterpriseName());
entity.setEntSource(item.entSource());
entity.setDataSource(item.dataSource());
entity.setRiskLevel(EnterpriseSource.INTERMEDIARY.getCode().equals(item.entSource()) ? "1" : null);
entity.setCreatedBy(item.userName());
entity.setUpdatedBy(item.userName());
return entity;
}
private void insertBatchIgnoreDuplicate(List<CcdiEnterpriseBaseInfo> entities) {
try {
for (int i = 0; i < entities.size(); i += BATCH_SIZE) {
int end = Math.min(i + BATCH_SIZE, entities.size());
enterpriseBaseInfoMapper.insertBatch(entities.subList(i, end));
}
} catch (DuplicateKeyException ex) {
insertOneByOneIgnoreDuplicate(entities);
}
}
private void insertOneByOneIgnoreDuplicate(List<CcdiEnterpriseBaseInfo> entities) {
for (CcdiEnterpriseBaseInfo entity : entities) {
if (enterpriseBaseInfoMapper.selectById(entity.getSocialCreditCode()) != null) {
continue;
}
try {
enterpriseBaseInfoMapper.insert(entity);
} catch (DuplicateKeyException duplicate) {
if (enterpriseBaseInfoMapper.selectById(entity.getSocialCreditCode()) == null) {
throw duplicate;
}
}
}
}
private String trimToNull(String value) {
if (StringUtils.isEmpty(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -158,6 +158,7 @@ public class EasyExcelUtil {
templateWriter(response, clazz)
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new TextFormatWriteHandler(clazz))
.doWrite(List.of());
} catch (IOException e) {
throw new RuntimeException("下载导入模板失败", e);
@@ -180,7 +181,8 @@ public class EasyExcelUtil {
setResponseHeader(response, sheetName + "模板");
var writerBuilder = templateWriter(response, clazz)
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy());
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new TextFormatWriteHandler(clazz));
// 注册所有自定义处理器
for (WriteHandler handler : handlers) {
writerBuilder.registerWriteHandler(handler);
@@ -288,7 +290,6 @@ public class EasyExcelUtil {
private static <T> WriteSheet buildTemplateSheet(int sheetNo, Class<T> clazz, String sheetName) {
return EasyExcel.writerSheet(sheetNo, sheetName)
.head(clazz)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
.registerWriteHandler(new TextFormatWriteHandler(clazz))
.registerWriteHandler(new RequiredFieldWriteHandler(clazz))

View File

@@ -53,6 +53,9 @@
<if test="query.relationName != null and query.relationName != ''">
AND r.relation_name LIKE CONCAT('%', #{query.relationName}, '%')
</if>
<if test="query.relationCertNo != null and query.relationCertNo != ''">
AND r.relation_cert_no LIKE CONCAT('%', #{query.relationCertNo}, '%')
</if>
ORDER BY r.create_time DESC
</select>

View File

@@ -61,6 +61,9 @@
<if test="query.relationName != null and query.relationName != ''">
AND r.relation_name LIKE CONCAT('%', #{query.relationName}, '%')
</if>
<if test="query.relationCertNo != null and query.relationCertNo != ''">
AND r.relation_cert_no LIKE CONCAT('%', #{query.relationCertNo}, '%')
</if>
<if test="query.status != null">
AND r.status = #{query.status}
</if>
@@ -115,6 +118,9 @@
<if test="query.relationName != null and query.relationName != ''">
AND r.relation_name LIKE CONCAT('%', #{query.relationName}, '%')
</if>
<if test="query.relationCertNo != null and query.relationCertNo != ''">
AND r.relation_cert_no LIKE CONCAT('%', #{query.relationCertNo}, '%')
</if>
<if test="query.status != null">
AND r.status = #{query.status}
</if>

View File

@@ -6,7 +6,8 @@
<!-- 招聘信息ResultMap -->
<resultMap type="com.ruoyi.info.collection.domain.vo.CcdiStaffRecruitmentVO" id="CcdiStaffRecruitmentVOResult">
<id property="recruitId" column="recruit_id"/>
<id property="id" column="id"/>
<result property="recruitId" column="recruit_id"/>
<result property="recruitName" column="recruit_name"/>
<result property="posName" column="pos_name"/>
<result property="posCategory" column="pos_category"/>
@@ -33,17 +34,17 @@
<!-- 分页查询招聘信息列表 -->
<select id="selectRecruitmentPage" resultMap="CcdiStaffRecruitmentVOResult">
SELECT
r.recruit_id, r.recruit_name, r.pos_name, r.pos_category, r.pos_desc,
r.id, r.recruit_id, r.recruit_name, r.pos_name, r.pos_category, r.pos_desc,
r.cand_name, r.recruit_type, r.cand_edu, r.cand_id, r.cand_school, r.cand_major, r.cand_grad,
r.admit_status, COALESCE(w.work_experience_count, 0) AS work_experience_count,
r.interviewer_name1, r.interviewer_id1, r.interviewer_name2, r.interviewer_id2,
r.created_by, r.create_time, r.updated_by, r.update_time
FROM ccdi_staff_recruitment r
LEFT JOIN (
SELECT recruit_id COLLATE utf8mb4_general_ci AS recruit_id, COUNT(1) AS work_experience_count
SELECT recruitment_id, COUNT(1) AS work_experience_count
FROM ccdi_staff_recruitment_work
GROUP BY recruit_id COLLATE utf8mb4_general_ci
) w ON w.recruit_id COLLATE utf8mb4_general_ci = r.recruit_id COLLATE utf8mb4_general_ci
GROUP BY recruitment_id
) w ON w.recruitment_id = r.id
<where>
<if test="query.recruitName != null and query.recruitName != ''">
AND r.recruit_name LIKE CONCAT('%', #{query.recruitName}, '%')
@@ -78,16 +79,16 @@
<!-- 查询招聘信息详情 -->
<select id="selectRecruitmentById" resultMap="CcdiStaffRecruitmentVOResult">
SELECT
recruit_id, recruit_name, pos_name, pos_category, pos_desc,
id, recruit_id, recruit_name, pos_name, pos_category, pos_desc,
cand_name, recruit_type, cand_edu, cand_id, cand_school, cand_major, cand_grad,
admit_status, interviewer_name1, interviewer_id1, interviewer_name2, interviewer_id2,
created_by, create_time, updated_by, update_time
FROM ccdi_staff_recruitment
WHERE recruit_id = #{recruitId}
WHERE id = #{id}
</select>
<!-- 批量插入招聘信息数据 -->
<insert id="insertBatch">
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
INSERT INTO ccdi_staff_recruitment
(recruit_id, recruit_name, pos_name, pos_category, pos_desc,
cand_name, recruit_type, cand_edu, cand_id, cand_school, cand_major, cand_grad,
@@ -124,7 +125,7 @@
interviewer_id2 = #{item.interviewerId2},
updated_by = #{item.updatedBy},
update_time = NOW()
WHERE recruit_id = #{item.recruitId}
WHERE id = #{item.id}
</foreach>
</update>

View File

@@ -96,6 +96,26 @@ class CcdiAssetInfoImportServiceImplTest {
assertEquals("320101199001010011", captor.getValue().get(0).getPersonId());
}
@Test
void importAssetInfoSync_shouldResolveFamilyIdFromCurrentWorkbookRelation() {
CcdiAssetInfoExcel excel = buildExcel("320101199001010033", "股权");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(assetInfoMapper.selectOwnerCandidatesByRelationCertNos(List.of("320101199001010033")))
.thenReturn(List.of());
service.importAssetInfoSync(
List.of(excel),
"task-current-workbook",
"tester",
Map.of("320101199001010033", Set.of("320101199009090099"))
);
ArgumentCaptor<List<CcdiAssetInfo>> captor = ArgumentCaptor.forClass(List.class);
verify(assetInfoMapper).insertBatch(captor.capture());
assertEquals("320101199009090099", captor.getValue().get(0).getFamilyId());
assertEquals("320101199001010033", captor.getValue().get(0).getPersonId());
}
@Test
void importAssetInfoAsync_shouldFailWhenEmployeeIdCardIsUsedForFamilyAssetImport() {
CcdiAssetInfoExcel excel = buildExcel("320101199001010011", "房产");

View File

@@ -19,6 +19,7 @@ import org.springframework.data.redis.core.ValueOperations;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -79,6 +80,26 @@ class CcdiBaseStaffAssetImportServiceImplTest {
assertEquals("320101199001010011", captor.getValue().get(0).getPersonId());
}
@Test
void importAssetInfoSync_shouldImportWhenOwnerComesFromCurrentWorkbook() {
CcdiBaseStaffAssetInfoExcel excel = buildExcel("320101199001010033", "存款");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(assetInfoMapper.selectOwnerCandidatesByBaseStaffIdCards(List.of("320101199001010033")))
.thenReturn(List.of());
service.importAssetInfoSync(
List.of(excel),
"task-current-workbook",
"tester",
Map.of("320101199001010033", Set.of("320101199001010033"))
);
ArgumentCaptor<List<CcdiAssetInfo>> captor = ArgumentCaptor.forClass(List.class);
verify(assetInfoMapper).insertBatch(captor.capture());
assertEquals("320101199001010033", captor.getValue().get(0).getFamilyId());
assertEquals("320101199001010033", captor.getValue().get(0).getPersonId());
}
@Test
void importAssetInfoAsync_shouldFailWhenFamilyCertificateIsUsed() {
CcdiBaseStaffAssetInfoExcel excel = buildExcel("320101199201010022", "车辆");

View File

@@ -1,14 +1,13 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiBizIntermediary;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation;
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.vo.IntermediaryEnterpriseRelationImportFailureVO;
import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.impl.CcdiIntermediaryEnterpriseRelationImportServiceImpl;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@@ -23,6 +22,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
@@ -42,7 +42,7 @@ class CcdiIntermediaryEnterpriseRelationImportServiceImplTest {
private CcdiBizIntermediaryMapper intermediaryMapper;
@Mock
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
private EnterpriseAutoFillService enterpriseAutoFillService;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@@ -58,11 +58,11 @@ class CcdiIntermediaryEnterpriseRelationImportServiceImplTest {
CcdiIntermediaryEnterpriseRelationExcel excel = buildExcel("320101199001010014", "91330100MA27X12345");
prepareFailureRedisMocks();
when(intermediaryMapper.selectList(any())).thenReturn(List.of());
when(enterpriseBaseInfoMapper.selectList(any())).thenReturn(List.of(enterprise("91330100MA27X12345")));
service.importAsync(List.of(excel), "task-owner-miss", "tester");
verify(relationMapper, never()).insertBatch(any());
verify(enterpriseAutoFillService, never()).ensureExistsBatch(any());
IntermediaryEnterpriseRelationImportFailureVO failure =
firstFailure("import:intermediary-enterprise-relation:task-owner-miss:failures");
assertEquals("320101199001010014", failure.getOwnerPersonId());
@@ -70,20 +70,20 @@ class CcdiIntermediaryEnterpriseRelationImportServiceImplTest {
}
@Test
void importEnterpriseRelationAsync_shouldFailWhenEnterpriseDoesNotExist() {
void importEnterpriseRelationAsync_shouldAutoFillWhenEnterpriseDoesNotExist() {
CcdiIntermediaryEnterpriseRelationExcel excel = buildExcel("320101199001010014", "91330100MA27X12345");
prepareFailureRedisMocks();
prepareStatusRedisMock();
when(intermediaryMapper.selectList(any())).thenReturn(List.of(owner("owner-biz", "320101199001010014")));
when(enterpriseBaseInfoMapper.selectList(any())).thenReturn(List.of());
when(relationMapper.batchExistsByCombinations(any())).thenReturn(List.of());
service.importAsync(List.of(excel), "task-ent-miss", "tester");
verify(relationMapper, never()).insertBatch(any());
IntermediaryEnterpriseRelationImportFailureVO failure =
firstFailure("import:intermediary-enterprise-relation:task-ent-miss:failures");
assertEquals("91330100MA27X12345", failure.getSocialCreditCode());
assertTrue(failure.getErrorMessage().contains("机构表"));
ArgumentCaptor<List<CcdiIntermediaryEnterpriseRelation>> relationCaptor = ArgumentCaptor.forClass(List.class);
verify(relationMapper).insertBatch(relationCaptor.capture());
assertEquals(1, relationCaptor.getValue().size());
assertEquals("owner-biz", relationCaptor.getValue().get(0).getIntermediaryBizId());
assertEquals("91330100MA27X12345", relationCaptor.getValue().get(0).getSocialCreditCode());
assertIntermediaryAutoFill("91330100MA27X12345");
}
@Test
@@ -96,10 +96,6 @@ class CcdiIntermediaryEnterpriseRelationImportServiceImplTest {
owner("owner-biz-1", "320101199001010014"),
owner("owner-biz-2", "320101199003030035")
));
when(enterpriseBaseInfoMapper.selectList(any())).thenReturn(List.of(
enterprise("91330100MA27X12345"),
enterprise("91330100MA27X12346")
));
when(relationMapper.batchExistsByCombinations(any())).thenReturn(List.of("owner-biz-1|91330100MA27X12345"));
service.importAsync(List.of(duplicateInDb, duplicateInFile1, duplicateInFile2), "task-duplicate", "tester");
@@ -108,6 +104,7 @@ class CcdiIntermediaryEnterpriseRelationImportServiceImplTest {
verify(relationMapper).insertBatch(captor.capture());
assertEquals(1, captor.getValue().size());
assertEquals("owner-biz-2", captor.getValue().get(0).getIntermediaryBizId());
assertIntermediaryAutoFill("91330100MA27X12346");
IntermediaryEnterpriseRelationImportFailureVO failure =
firstFailure("import:intermediary-enterprise-relation:task-duplicate:failures");
assertTrue(failure.getErrorMessage().contains("重复") || failure.getErrorMessage().contains("已存在"));
@@ -118,7 +115,6 @@ class CcdiIntermediaryEnterpriseRelationImportServiceImplTest {
CcdiIntermediaryEnterpriseRelationExcel excel = buildExcel("320101199001010014", "91330100MA27X12345");
prepareStatusRedisMock();
when(intermediaryMapper.selectList(any())).thenReturn(List.of(owner("owner-biz", "320101199001010014")));
when(enterpriseBaseInfoMapper.selectList(any())).thenReturn(List.of(enterprise("91330100MA27X12345")));
when(relationMapper.batchExistsByCombinations(any())).thenReturn(List.of());
service.importAsync(List.of(excel), "task-success", "tester");
@@ -127,6 +123,7 @@ class CcdiIntermediaryEnterpriseRelationImportServiceImplTest {
verify(relationMapper).insertBatch(captor.capture());
assertEquals(1, captor.getValue().size());
assertEquals("owner-biz", captor.getValue().get(0).getIntermediaryBizId());
assertIntermediaryAutoFill("91330100MA27X12345");
verify(valueOperations, never()).set(any(), any(), any(Long.class), any(TimeUnit.class));
}
@@ -163,10 +160,15 @@ class CcdiIntermediaryEnterpriseRelationImportServiceImplTest {
return owner;
}
private CcdiEnterpriseBaseInfo enterprise(String socialCreditCode) {
CcdiEnterpriseBaseInfo enterprise = new CcdiEnterpriseBaseInfo();
enterprise.setSocialCreditCode(socialCreditCode);
enterprise.setEnterpriseName("机构" + socialCreditCode.substring(socialCreditCode.length() - 2));
return enterprise;
private void assertIntermediaryAutoFill(String socialCreditCode) {
ArgumentCaptor<List<EnterpriseAutoFillService.EnterpriseFillItem>> captor = ArgumentCaptor.forClass(List.class);
verify(enterpriseAutoFillService).ensureExistsBatch(captor.capture());
assertEquals(1, captor.getValue().size());
EnterpriseAutoFillService.EnterpriseFillItem item = captor.getValue().get(0);
assertEquals(socialCreditCode, item.socialCreditCode());
assertNull(item.enterpriseName());
assertEquals("INTERMEDIARY", item.entSource());
assertEquals("IMPORT", item.dataSource());
assertEquals("tester", item.userName());
}
}

View File

@@ -2,6 +2,8 @@ package com.ruoyi.info.collection.service;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.info.collection.domain.CcdiStaffEnterpriseRelation;
import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation;
import com.ruoyi.info.collection.domain.dto.CcdiStaffEnterpriseRelationAddDTO;
@@ -10,8 +12,10 @@ import com.ruoyi.info.collection.domain.vo.CcdiStaffEnterpriseRelationOptionVO;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.service.impl.CcdiStaffEnterpriseRelationServiceImpl;
import com.ruoyi.info.collection.service.support.EnterpriseAutoFillService;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -20,13 +24,17 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -55,8 +63,17 @@ class CcdiStaffEnterpriseRelationServiceImplTest {
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Mock
private EnterpriseAutoFillService enterpriseAutoFillService;
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void insertRelation_shouldAllowValidFamily() {
mockLoginUser("tester");
CcdiStaffEnterpriseRelationAddDTO addDTO = buildAddDto();
CcdiStaffFmyRelation familyRelation = new CcdiStaffFmyRelation();
familyRelation.setRelationCertNo(addDTO.getPersonId());
@@ -75,6 +92,13 @@ class CcdiStaffEnterpriseRelationServiceImplTest {
assertEquals(1, captor.getValue().getStatus());
assertEquals("MANUAL", captor.getValue().getDataSource());
assertEquals(1, captor.getValue().getIsEmpFamily());
verify(enterpriseAutoFillService).ensureExists(argThat(item ->
"91310000123456789A".equals(item.socialCreditCode())
&& "测试企业".equals(item.enterpriseName())
&& "EMP_RELATION".equals(item.entSource())
&& "MANUAL".equals(item.dataSource())
&& "tester".equals(item.userName())
));
}
@Test
@@ -153,4 +177,13 @@ class CcdiStaffEnterpriseRelationServiceImplTest {
assistant.setCurrentNamespace(namespace);
TableInfoHelper.initTableInfo(assistant, entityClass);
}
private void mockLoginUser(String userName) {
SysUser user = new SysUser();
user.setUserName(userName);
LoginUser loginUser = new LoginUser(1L, 1L, user, Set.of());
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(loginUser, null, List.of());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}

View File

@@ -1,6 +1,8 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiStaffRecruitment;
import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel;
import com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO;
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentMapper;
@@ -27,6 +29,7 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -55,7 +58,7 @@ class CcdiStaffRecruitmentImportServiceImplTest {
void shouldFailWholeWorkGroupWhenExistingHistoryExists() {
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(recruitmentMapper.selectBatchIds(any())).thenReturn(List.of(buildRecruitment("RC001")));
when(recruitmentMapper.selectList(any())).thenReturn(List.of(buildRecruitment("RC001")));
when(recruitmentWorkMapper.selectCount(any())).thenReturn(1L);
CcdiStaffRecruitmentWorkExcel workRow = new CcdiStaffRecruitmentWorkExcel();
@@ -86,13 +89,81 @@ class CcdiStaffRecruitmentImportServiceImplTest {
assertEquals("招聘记录编号[RC001]已存在历史工作经历,不允许重复导入", failure.getErrorMessage());
}
@Test
void shouldAllowDuplicateRecruitIdsWhenImportingMainSheet() {
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(recruitmentMapper.insert(any(CcdiStaffRecruitment.class))).thenReturn(1);
CcdiStaffRecruitmentExcel first = buildRecruitmentExcel("RC001", "张三");
CcdiStaffRecruitmentExcel second = buildRecruitmentExcel("RC001", "李四");
service.importRecruitmentAsync(List.of(first, second), Collections.emptyList(), "task-2", "admin");
verify(recruitmentMapper, times(2)).insert(any(CcdiStaffRecruitment.class));
verify(valueOperations, never()).set(eq("import:recruitment:task-2:failures"), any(), anyLong(), any());
}
@Test
void shouldAttachWorkToMatchedRecruitmentWhenRecruitIdIsDuplicated() {
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
CcdiStaffRecruitment matched = buildRecruitment(10L, "RC001", "张三");
CcdiStaffRecruitment other = buildRecruitment(11L, "RC001", "李四");
when(recruitmentMapper.selectList(any())).thenReturn(List.of(matched, other));
when(recruitmentWorkMapper.selectCount(any())).thenReturn(0L);
CcdiStaffRecruitmentWorkExcel workRow = buildWorkExcel("RC001", "张三");
service.importRecruitmentAsync(Collections.emptyList(), List.of(workRow), "task-3", "admin");
ArgumentCaptor<CcdiStaffRecruitmentWork> workCaptor = ArgumentCaptor.forClass(CcdiStaffRecruitmentWork.class);
verify(recruitmentWorkMapper).insert(workCaptor.capture());
assertEquals(10L, workCaptor.getValue().getRecruitmentId());
assertEquals("RC001", workCaptor.getValue().getRecruitId());
}
private CcdiStaffRecruitment buildRecruitment(String recruitId) {
return buildRecruitment(1L, recruitId, "张三");
}
private CcdiStaffRecruitment buildRecruitment(Long id, String recruitId, String candName) {
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
recruitment.setId(id);
recruitment.setRecruitId(recruitId);
recruitment.setRecruitType("SOCIAL");
recruitment.setCandName("张三");
recruitment.setCandName(candName);
recruitment.setRecruitName("社会招聘项目");
recruitment.setPosName("Java工程师");
return recruitment;
}
private CcdiStaffRecruitmentExcel buildRecruitmentExcel(String recruitId, String candName) {
CcdiStaffRecruitmentExcel excel = new CcdiStaffRecruitmentExcel();
excel.setRecruitId(recruitId);
excel.setRecruitName("社会招聘项目");
excel.setPosName("Java工程师");
excel.setPosCategory("技术类");
excel.setPosDesc("负责系统开发");
excel.setAdmitStatus("录用");
excel.setCandName(candName);
excel.setRecruitType("SOCIAL");
excel.setCandEdu("本科");
excel.setCandId(candName.equals("张三") ? "110105199001010010" : "110105199002020026");
excel.setCandGrad("202110");
excel.setCandSchool("四川大学");
excel.setCandMajor("法学");
return excel;
}
private CcdiStaffRecruitmentWorkExcel buildWorkExcel(String recruitId, String candName) {
CcdiStaffRecruitmentWorkExcel workRow = new CcdiStaffRecruitmentWorkExcel();
workRow.setRecruitId(recruitId);
workRow.setCandName(candName);
workRow.setRecruitName("社会招聘项目");
workRow.setPosName("Java工程师");
workRow.setSortOrder(1);
workRow.setCompanyName("测试科技");
workRow.setPositionName("开发工程师");
workRow.setJobStartMonth("2022-01");
return workRow;
}
}

View File

@@ -2,12 +2,22 @@ package com.ruoyi.info.collection.utils;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.excel.CcdiAccountInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.excel.CcdiCustEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.excel.CcdiCustFmyRelationExcel;
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEntityExcel;
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffEnterpriseRelationExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.Row;
@@ -99,6 +109,63 @@ class EasyExcelUtilTemplateTest {
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
Sheet sheet = workbook.getSheetAt(0);
assertTrue(hasValidationOnColumn(sheet, 7), "是否党员列应包含下拉校验");
assertHeaderValue(sheet, 0, "姓名*");
assertHeaderValue(sheet, 1, "员工ID*");
assertHeaderValue(sheet, 2, "所属部门ID*");
assertHeaderValue(sheet, 3, "身份证号*");
assertHeaderValue(sheet, 4, "电话*");
assertHeaderValue(sheet, 7, "是否党员*");
assertHeaderValue(sheet, 8, "状态*");
assertTextColumn(sheet, 3);
assertTextColumn(sheet, 4);
}
}
@Test
void importTemplateWithDictDropdown_shouldKeepBaseStaffDualSheetColumnWidths() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
try (MockedStatic<DictUtils> mocked = mockStatic(DictUtils.class)) {
mocked.when(() -> DictUtils.getDictCache("ccdi_employee_status"))
.thenReturn(List.of(
buildDictData("在职", "0"),
buildDictData("离职", "1")
));
mocked.when(() -> DictUtils.getDictCache("ccdi_yes_no_flag"))
.thenReturn(List.of(
buildDictData("", "1"),
buildDictData("", "0")
));
mocked.when(() -> DictUtils.getDictCache("ccdi_asset_status"))
.thenReturn(List.of(
buildDictData("正常"),
buildDictData("冻结"),
buildDictData("处置中")
));
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiBaseStaffExcel.class,
"员工信息",
CcdiBaseStaffAssetInfoExcel.class,
"员工资产信息",
"员工信息维护导入模板"
);
}
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
Sheet staffSheet = workbook.getSheet("员工信息");
Sheet assetSheet = workbook.getSheet("员工资产信息");
assertNotNull(staffSheet);
assertNotNull(assetSheet);
assertColumnWidthsAtLeast(staffSheet, new int[] {16, 18, 20, 24, 18, 20, 18, 16, 14});
assertColumnWidthsAtLeast(assetSheet, new int[] {24, 18, 18, 24, 14, 20, 18, 18, 20, 16, 32});
assertHeaderValue(staffSheet, 0, "姓名*");
assertHeaderValue(staffSheet, 8, "状态*");
assertHeaderValue(assetSheet, 0, "员工身份证号*");
assertHeaderValue(assetSheet, 1, "资产大类*");
assertHeaderValue(assetSheet, 10, "备注");
}
}
@@ -113,6 +180,11 @@ class EasyExcelUtilTemplateTest {
buildDictData("未录用"),
buildDictData("放弃")
));
mocked.when(() -> DictUtils.getDictCache("ccdi_recruit_type"))
.thenReturn(List.of(
buildDictData("社招", "SOCIAL"),
buildDictData("校招", "CAMPUS")
));
EasyExcelUtil.importTemplateWithDictDropdown(
response,
@@ -128,6 +200,8 @@ class EasyExcelUtilTemplateTest {
assertEquals(2, workbook.getNumberOfSheets(), "招聘导入模板应输出双Sheet");
assertEquals("招聘信息", workbook.getSheetAt(0).getSheetName());
assertEquals("历史工作经历", workbook.getSheetAt(1).getSheetName());
assertTrue(hasValidationOnColumn(workbook.getSheetAt(0), 5), "录用情况列应包含下拉校验");
assertTrue(hasValidationOnColumn(workbook.getSheetAt(0), 7), "招聘类型列应包含下拉校验");
}
}
@@ -155,12 +229,119 @@ class EasyExcelUtilTemplateTest {
}
}
@Test
void infoImportTemplates_shouldFormatIdentifierAndContactColumnsAsText() throws Exception {
try (MockedStatic<DictUtils> mocked = mockStatic(DictUtils.class)) {
mockCommonDicts(mocked);
assertPlainTemplateTextColumns(CcdiAccountInfoExcel.class, "账户库管理", 1, 3, 7);
assertSingleTemplateTextColumns(CcdiAssetInfoExcel.class, "亲属资产信息", 0);
assertSingleTemplateTextColumns(CcdiBaseStaffAssetInfoExcel.class, "员工资产信息", 0);
assertDualTemplateTextColumns(
CcdiBaseStaffExcel.class,
"员工信息",
new int[] {3, 4},
CcdiBaseStaffAssetInfoExcel.class,
"员工资产信息",
new int[] {0},
"员工信息维护导入模板"
);
assertSingleTemplateTextColumns(CcdiCustEnterpriseRelationExcel.class, "信贷客户实体关联信息", 0, 1);
assertSingleTemplateTextColumns(CcdiCustFmyRelationExcel.class, "信贷客户家庭关系", 0, 6, 7, 8);
assertSingleTemplateTextColumns(CcdiEnterpriseBaseInfoExcel.class, "实体库管理", 0, 10);
assertSingleTemplateTextColumns(CcdiIntermediaryPersonExcel.class, "个人中介信息", 5, 6, 10, 12);
assertSingleTemplateTextColumns(CcdiIntermediaryEntityExcel.class, "实体中介信息", 1, 10);
assertSingleTemplateTextColumns(CcdiIntermediaryEnterpriseRelationExcel.class, "中介实体关联关系信息", 0, 1);
assertDualTemplateTextColumns(
CcdiPurchaseTransactionExcel.class,
"招投标主信息",
new int[] {0, 21, 24},
CcdiPurchaseTransactionSupplierExcel.class,
"供应商信息",
new int[] {0, 2, 4, 5},
"招投标信息维护导入模板"
);
assertSingleTemplateTextColumns(CcdiStaffEnterpriseRelationExcel.class, "员工亲属实体关联", 0, 1);
assertSingleTemplateTextColumns(CcdiStaffFmyRelationExcel.class, "员工亲属关系信息", 0, 6, 7, 8);
assertDualTemplateTextColumns(
CcdiStaffRecruitmentExcel.class,
"招聘信息",
new int[] {0, 9, 14, 16},
CcdiStaffRecruitmentWorkExcel.class,
"历史工作经历",
new int[] {0},
"招聘信息管理导入模板"
);
}
}
private void assertTextColumn(Sheet sheet, int columnIndex) {
CellStyle style = sheet.getColumnStyle(columnIndex);
assertNotNull(style, "文本列应设置默认样式");
assertEquals("@", style.getDataFormatString(), "证件号列应使用文本格式");
}
private void assertHeaderValue(Sheet sheet, int columnIndex, String expectedValue) {
assertEquals(expectedValue, sheet.getRow(0).getCell(columnIndex).getStringCellValue());
}
private void assertTextColumns(Sheet sheet, int... columnIndexes) {
for (int columnIndex : columnIndexes) {
assertTextColumn(sheet, columnIndex);
}
}
private void assertPlainTemplateTextColumns(Class<?> clazz, String sheetName, int... columnIndexes) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
EasyExcelUtil.importTemplateExcel(response, clazz, sheetName);
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
assertTextColumns(workbook.getSheetAt(0), columnIndexes);
}
}
private void assertSingleTemplateTextColumns(Class<?> clazz, String sheetName, int... columnIndexes) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
EasyExcelUtil.importTemplateWithDictDropdown(response, clazz, sheetName);
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
assertTextColumns(workbook.getSheetAt(0), columnIndexes);
}
}
private <T1, T2> void assertDualTemplateTextColumns(Class<T1> firstClazz,
String firstSheetName,
int[] firstColumnIndexes,
Class<T2> secondClazz,
String secondSheetName,
int[] secondColumnIndexes,
String fileName) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
EasyExcelUtil.importTemplateWithDictDropdown(
response,
firstClazz,
firstSheetName,
secondClazz,
secondSheetName,
fileName
);
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
assertTextColumns(workbook.getSheet(firstSheetName), firstColumnIndexes);
assertTextColumns(workbook.getSheet(secondSheetName), secondColumnIndexes);
}
}
private void assertColumnWidthsAtLeast(Sheet sheet, int[] expectedWidths) {
for (int columnIndex = 0; columnIndex < expectedWidths.length; columnIndex++) {
int currentColumnIndex = columnIndex;
int expectedWidth = expectedWidths[currentColumnIndex];
int expected = expectedWidth * 256;
assertTrue(sheet.getColumnWidth(currentColumnIndex) >= expected,
() -> sheet.getSheetName() + "" + (currentColumnIndex + 1) + "列宽度应不小于" + expectedWidth);
}
}
private boolean hasValidationOnColumn(Sheet sheet, int columnIndex) {
for (DataValidation validation : sheet.getDataValidations()) {
for (CellRangeAddress address : validation.getRegions().getCellRangeAddresses()) {
@@ -172,6 +353,33 @@ class EasyExcelUtilTemplateTest {
return false;
}
private void mockCommonDicts(MockedStatic<DictUtils> mocked) {
mocked.when(() -> DictUtils.getDictCache("ccdi_asset_status"))
.thenReturn(List.of(buildDictData("正常")));
mocked.when(() -> DictUtils.getDictCache("ccdi_employee_status"))
.thenReturn(List.of(buildDictData("在职", "1")));
mocked.when(() -> DictUtils.getDictCache("ccdi_yes_no_flag"))
.thenReturn(List.of(buildDictData("", "1")));
mocked.when(() -> DictUtils.getDictCache("ccdi_relation_type"))
.thenReturn(List.of(buildDictData("配偶")));
mocked.when(() -> DictUtils.getDictCache("ccdi_indiv_gender"))
.thenReturn(List.of(buildDictData("")));
mocked.when(() -> DictUtils.getDictCache("ccdi_certificate_type"))
.thenReturn(List.of(buildDictData("居民身份证")));
mocked.when(() -> DictUtils.getDictCache("ccdi_entity_type"))
.thenReturn(List.of(buildDictData("有限责任公司")));
mocked.when(() -> DictUtils.getDictCache("ccdi_enterprise_nature"))
.thenReturn(List.of(buildDictData("民营企业")));
mocked.when(() -> DictUtils.getDictCache("ccdi_person_type"))
.thenReturn(List.of(buildDictData("中介")));
mocked.when(() -> DictUtils.getDictCache("ccdi_person_sub_type"))
.thenReturn(List.of(buildDictData("本人")));
mocked.when(() -> DictUtils.getDictCache("ccdi_admit_status"))
.thenReturn(List.of(buildDictData("录用")));
mocked.when(() -> DictUtils.getDictCache("ccdi_recruit_type"))
.thenReturn(List.of(buildDictData("社招", "SOCIAL")));
}
private SysDictData buildDictData(String label) {
return buildDictData(label, label);
}

View File

@@ -14,9 +14,11 @@ import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.file.FileUtils;
import jakarta.servlet.http.HttpServletResponse;
import java.awt.Color;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -32,6 +34,7 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
@@ -43,6 +46,11 @@ public class CcdiProjectOverviewReportPdfExporter {
private static final String CONTENT_TYPE = "application/pdf";
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private static final DecimalFormat MONEY_FORMAT = new DecimalFormat("#,##0.00");
private final String pdfFontPath;
public CcdiProjectOverviewReportPdfExporter(@Value("${ccdi.report.pdf-font-path:}") String pdfFontPath) {
this.pdfFontPath = pdfFontPath;
}
public void export(HttpServletResponse response, CcdiProjectOverviewReportVO report) throws IOException {
response.setContentType(CONTENT_TYPE);
@@ -51,9 +59,8 @@ public class CcdiProjectOverviewReportPdfExporter {
safeFileName(report.getProject().getProjectName()) + "_初核结果报告.pdf"
);
try (PDDocument document = new PDDocument()) {
PDType0Font font = loadChineseFont(document);
PdfPageWriter writer = new PdfPageWriter(document, font);
try (PDDocument document = new PDDocument(); LoadedChineseFont loadedFont = loadChineseFont(document)) {
PdfPageWriter writer = new PdfPageWriter(document, loadedFont.font());
writer.newPage();
writeCover(writer, report);
writeUploadSubjects(writer, report.getUploadSubjects());
@@ -204,44 +211,113 @@ public class CcdiProjectOverviewReportPdfExporter {
);
}
private PDType0Font loadChineseFont(PDDocument document) throws IOException {
List<String> candidates = List.of(
"C:/Windows/Fonts/NotoSansSC-VF.ttf",
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/simsunb.ttf",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttf",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
);
for (String path : candidates) {
File file = new File(path);
if (!file.exists() || !file.isFile()) {
continue;
}
String lowerPath = path.toLowerCase();
if (lowerPath.endsWith(".ttf")) {
return PDType0Font.load(document, file);
}
if (lowerPath.endsWith(".ttc")) {
PDType0Font font = loadFirstCollectionFont(document, file);
if (font != null) {
return font;
}
}
private LoadedChineseFont loadChineseFont(PDDocument document) throws IOException {
if (pdfFontPath == null || pdfFontPath.isBlank()) {
throw new ServiceException("未配置PDF中文字体路径无法导出PDF报告");
}
throw new ServiceException("未找到可用中文字体无法导出PDF报告");
String path = pdfFontPath.trim();
File file = new File(path);
if (!file.exists() || !file.isFile()) {
throw new ServiceException("配置的PDF中文字体路径不可用" + path);
}
String lowerPath = path.toLowerCase();
if (lowerPath.endsWith(".ttf")) {
return new LoadedChineseFont(PDType0Font.load(document, file), null);
}
if (lowerPath.endsWith(".ttc")) {
LoadedChineseFont loadedFont = loadPreferredCollectionFont(document, file);
if (loadedFont != null) {
return loadedFont;
}
throw new ServiceException("配置的PDF中文字体不支持导出" + path);
}
if (lowerPath.endsWith(".otf")) {
return new LoadedChineseFont(loadOpenTypeFont(document, file), null);
}
throw new ServiceException("配置的PDF中文字体格式不支持" + path);
}
private PDType0Font loadFirstCollectionFont(PDDocument document, File file) throws IOException {
private PDType0Font loadOpenTypeFont(PDDocument document, File file) throws IOException {
try (var inputStream = Files.newInputStream(file.toPath())) {
return PDType0Font.load(document, inputStream, false);
}
}
private LoadedChineseFont loadPreferredCollectionFont(PDDocument document, File file) throws IOException {
LoadedChineseFont font = loadCollectionFontByName(document, file);
return font == null ? loadFirstCollectionFont(document, file) : font;
}
private LoadedChineseFont loadCollectionFontByName(PDDocument document, File file) throws IOException {
TrueTypeCollection collection = new TrueTypeCollection(file);
AtomicReference<PDType0Font> font = new AtomicReference<>();
try (TrueTypeCollection collection = new TrueTypeCollection(file)) {
try {
collection.processAllFonts(typeFont -> {
if (font.get() == null) {
if (font.get() == null && supportsTrueTypeSubset(typeFont)
&& isSimplifiedChineseFont(typeFont.getName())) {
font.set(PDType0Font.load(document, typeFont, true));
}
});
if (font.get() != null) {
return new LoadedChineseFont(font.get(), collection);
}
} finally {
if (font.get() == null) {
collection.close();
}
}
return null;
}
private boolean isSimplifiedChineseFont(String fontName) {
if (fontName == null) {
return false;
}
String lowerName = fontName.toLowerCase();
return lowerName.contains("sc")
|| lowerName.contains("hans")
|| lowerName.contains("gb")
|| lowerName.contains("hei")
|| lowerName.contains("song")
|| lowerName.contains("yahei")
|| lowerName.contains("simsun")
|| lowerName.contains("simhei");
}
private LoadedChineseFont loadFirstCollectionFont(PDDocument document, File file) throws IOException {
TrueTypeCollection collection = new TrueTypeCollection(file);
AtomicReference<PDType0Font> font = new AtomicReference<>();
try {
collection.processAllFonts(typeFont -> {
if (font.get() == null && supportsTrueTypeSubset(typeFont)) {
font.set(PDType0Font.load(document, typeFont, true));
}
});
if (font.get() != null) {
return new LoadedChineseFont(font.get(), collection);
}
} finally {
if (font.get() == null) {
collection.close();
}
}
return null;
}
private boolean supportsTrueTypeSubset(org.apache.fontbox.ttf.TrueTypeFont typeFont) throws IOException {
return typeFont.getTableMap().containsKey("glyf");
}
private record LoadedChineseFont(PDType0Font font, Closeable fontSource) implements Closeable {
@Override
public void close() throws IOException {
if (fontSource != null) {
fontSource.close();
}
}
return font.get();
}
private List<IndexedUploadSubject> indexedRows(List<CcdiProjectOverviewReportUploadSubjectVO> rows) {

View File

@@ -12,6 +12,8 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.common.exception.ServiceException;
import java.io.File;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.List;
@@ -19,13 +21,14 @@ import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectOverviewReportPdfExporterTest {
@Test
void shouldExportOverviewReportPdf() throws Exception {
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter();
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter(resolveTestFontPath());
MockHttpServletResponse response = new MockHttpServletResponse();
exporter.export(response, buildReport());
@@ -36,6 +39,41 @@ class CcdiProjectOverviewReportPdfExporterTest {
assertTrue(response.getContentAsByteArray().length > 1000);
}
@Test
void shouldRejectBlankPdfFontPath() {
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter("");
MockHttpServletResponse response = new MockHttpServletResponse();
ServiceException exception = assertThrows(ServiceException.class, () -> exporter.export(response, buildReport()));
assertEquals("未配置PDF中文字体路径无法导出PDF报告", exception.getMessage());
}
@Test
void shouldRejectUnavailablePdfFontPath() {
String missingPath = "/tmp/ccdi-missing-pdf-font-" + System.nanoTime() + ".ttc";
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter(missingPath);
MockHttpServletResponse response = new MockHttpServletResponse();
ServiceException exception = assertThrows(ServiceException.class, () -> exporter.export(response, buildReport()));
assertTrue(exception.getMessage().contains(missingPath));
}
private String resolveTestFontPath() {
List<String> candidates = List.of(
"/System/Library/Fonts/STHeiti Medium.ttc",
"/System/Library/Fonts/STHeiti Light.ttc",
"/System/Library/Fonts/Hiragino Sans GB.ttc",
"/System/Library/Fonts/Supplemental/Songti.ttc",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
);
return candidates.stream()
.filter(path -> new File(path).isFile())
.findFirst()
.orElse(candidates.get(0));
}
private CcdiProjectOverviewReportVO buildReport() {
CcdiProjectOverviewReportVO report = new CcdiProjectOverviewReportVO();
CcdiProject project = new CcdiProject();

504
deploy/ccdi_function.sh Executable file
View File

@@ -0,0 +1,504 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
# ==================== 生产配置区:按服务器实际路径修改 ====================
# JDK 安装目录。留空时使用服务器已有 JAVA_HOME仍为空时使用 PATH 中的 java。
BACKEND_JAVA_HOME=""
# 后端 Jar 所在目录。生产目录结构为脚本在外层Jar 位于 backend/ruoyi-admin.jar。
APP_HOME="${SCRIPT_DIR}/backend"
# 后端 Jar 文件名。
JAR_NAME="ruoyi-admin.jar"
# Spring Profile。
SPRING_PROFILES_ACTIVE="uat"
# JVM 参数。
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
# 额外应用启动参数,例如:--server.port=8080
APP_ARGS=""
# 停止进程等待秒数。
STOP_WAIT_SECONDS=30
# ==================== 以下为脚本逻辑,一般不需要修改 ====================
if [ "${APP_HOME#/}" = "${APP_HOME}" ]; then
APP_HOME="${SCRIPT_DIR}/${APP_HOME}"
fi
APP_HOME="${APP_HOME%/}"
JAR_PATH="${APP_HOME}/${JAR_NAME}"
FRONTEND_DIR="${SCRIPT_DIR}/frontend"
LOG_DIR="${APP_HOME}/logs"
CONSOLE_LOG="${LOG_DIR}/backend-console.log"
PID_FILE="${LOG_DIR}/backend-java.pid"
APP_MARKER="-Dccdi.backend.prod.home=${APP_HOME}"
JAVA_CMD="java"
RELATIVE_JAR_PATH=""
TIMESTAMP=$(date '+%Y%m%d%H%M%S')
BACKUP_ROOT="${SCRIPT_DIR}/backups"
BACKUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}"
WORK_ROOT="${SCRIPT_DIR}/.deploy-work"
WORK_DIR="${WORK_ROOT}/release-${TIMESTAMP}"
RELEASE_ZIP=""
RELEASE_JAR=""
RELEASE_DIST_ZIP=""
FRONTEND_SOURCE_DIR=""
case "${APP_HOME}" in
"${SCRIPT_DIR}"/*)
RELATIVE_JAR_PATH="${APP_HOME#${SCRIPT_DIR}/}/${JAR_NAME}"
;;
esac
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
log_info() {
printf '[%s] %s\n' "$(timestamp)" "$1"
}
log_error() {
printf '[%s] %s\n' "$(timestamp)" "$1" >&2
}
usage() {
cat <<'EOF'
用法: ./ccdi_function.sh [deploy|restart|stop] [上线压缩包路径]
命令:
deploy 备份 backend/ 与 frontend/,部署上线包,然后重启后端并持续输出日志
restart 停止旧后端进程,启动 backend/ruoyi-admin.jar并持续输出日志
stop 停止当前脚本目录对应的 backend/ruoyi-admin.jar 进程
上线包要求:
未传上线压缩包路径时,脚本会自动使用当前脚本目录下唯一的 .zip 文件。
上线压缩包根层必须包含 ruoyi-admin.jar 和 dist.zip。
dist.zip 解压后必须包含 dist/index.html。
生产目录示例:
ccdi_function.sh
backend/
frontend/
ccdi_YYYYMMDD.zip
说明:
restart 和 deploy 启动成功后会持续输出 backend/logs/backend-console.log。
按 Ctrl+C 仅退出日志查看,不会停止已启动的后端进程。
EOF
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
resolve_path() {
input_path="$1"
case "${input_path}" in
/*)
printf '%s\n' "${input_path}"
;;
*)
input_dir=$(dirname "${input_path}")
input_base=$(basename "${input_path}")
printf '%s/%s\n' "$(CDPATH= cd -- "${input_dir}" && pwd)" "${input_base}"
;;
esac
}
resolve_java_cmd() {
configured_java_home="${BACKEND_JAVA_HOME}"
if [ -z "${configured_java_home}" ]; then
configured_java_home="${JAVA_HOME:-}"
fi
if [ -n "${configured_java_home}" ]; then
configured_java_home="${configured_java_home%/}"
if [ ! -x "${configured_java_home}/bin/java" ]; then
log_error "配置的 JAVA_HOME 无效,未找到可执行文件: ${configured_java_home}/bin/java"
exit 1
fi
JAVA_HOME="${configured_java_home}"
export JAVA_HOME
JAVA_CMD="${JAVA_HOME}/bin/java"
else
require_command "java"
JAVA_CMD="java"
fi
log_info "使用 Java 命令: ${JAVA_CMD}"
}
get_process_table() {
if ! ps -ef 2>/dev/null; then
log_error "执行 ps -ef 失败,无法扫描旧进程"
return 1
fi
}
is_backend_process_line() {
process_line="$1"
case "${process_line}" in
*"<defunct>"*)
return 1
;;
*" -jar ${JAR_PATH}"*)
return 0
;;
*"${APP_MARKER}"*)
return 0
;;
esac
if [ -n "${RELATIVE_JAR_PATH}" ]; then
case "${process_line}" in
*" -jar ${RELATIVE_JAR_PATH}"*)
return 0
;;
esac
fi
return 1
}
is_managed_pid() {
check_pid="$1"
if [ -z "${check_pid}" ] || ! kill -0 "${check_pid}" 2>/dev/null; then
return 1
fi
if ! process_table=$(get_process_table); then
return 1
fi
while IFS= read -r process_line; do
set -- ${process_line}
line_pid="${2:-}"
if [ "${line_pid}" = "${check_pid}" ] && is_backend_process_line "${process_line}"; then
return 0
fi
done <<EOF
${process_table}
EOF
return 1
}
collect_pids() {
all_pids=""
unique_pids=""
if ! process_table=$(get_process_table); then
return 1
fi
if [ -f "${PID_FILE}" ]; then
pid_file_value=$(sed -n '1p' "${PID_FILE}" 2>/dev/null || true)
if is_managed_pid "${pid_file_value}"; then
all_pids="${all_pids} ${pid_file_value}"
fi
fi
while IFS= read -r process_line; do
set -- ${process_line}
scan_pid="${2:-}"
case "${scan_pid}" in
''|*[!0-9]*)
continue
;;
esac
if is_backend_process_line "${process_line}"; then
all_pids="${all_pids} ${scan_pid}"
fi
done <<EOF
${process_table}
EOF
for scan_pid in ${all_pids}; do
case " ${unique_pids} " in
*" ${scan_pid} "*)
;;
*)
unique_pids="${unique_pids} ${scan_pid}"
;;
esac
done
printf '%s\n' "${unique_pids}" | xargs 2>/dev/null || true
}
start_backend() {
resolve_java_cmd
if [ ! -f "${JAR_PATH}" ]; then
log_error "未找到后端 Jar: ${JAR_PATH}"
exit 1
fi
if ! running_pids=$(collect_pids); then
log_error "扫描后端进程失败"
exit 1
fi
if [ -n "${running_pids}" ]; then
log_error "检测到后端已在运行: ${running_pids}"
exit 1
fi
mkdir -p "${LOG_DIR}"
printf '\n===== %s start =====\n' "$(timestamp)" >>"${CONSOLE_LOG}"
profile_arg=""
if [ -n "${SPRING_PROFILES_ACTIVE}" ]; then
profile_arg="--spring.profiles.active=${SPRING_PROFILES_ACTIVE}"
fi
log_info "开始启动后端 Jar: ${JAR_PATH}"
nohup "${JAVA_CMD}" "${APP_MARKER}" ${JAVA_OPTS} -jar "${JAR_PATH}" ${profile_arg} ${APP_ARGS} >>"${CONSOLE_LOG}" 2>&1 &
echo $! >"${PID_FILE}"
sleep 3
starter_pid=$(sed -n '1p' "${PID_FILE}" 2>/dev/null || true)
if [ -z "${starter_pid}" ] || ! kill -0 "${starter_pid}" 2>/dev/null; then
log_error "启动命令未保持运行,请检查日志: ${CONSOLE_LOG}"
exit 1
fi
log_info "后端启动完成PID: ${starter_pid}"
}
stop_backend() {
if ! pids=$(collect_pids); then
log_error "扫描后端进程失败"
exit 1
fi
if [ -z "${pids}" ]; then
log_info "未发现运行中的后端进程"
rm -f "${PID_FILE}"
return 0
fi
log_info "准备停止后端进程: ${pids}"
for pid in ${pids}; do
kill -TERM "${pid}" 2>/dev/null || true
done
elapsed=0
remaining_pids="${pids}"
while [ -n "${remaining_pids}" ] && [ "${elapsed}" -lt "${STOP_WAIT_SECONDS}" ]; do
sleep 1
elapsed=$((elapsed + 1))
remaining_pids=""
for pid in ${pids}; do
if kill -0 "${pid}" 2>/dev/null; then
remaining_pids="${remaining_pids} ${pid}"
fi
done
remaining_pids=$(printf '%s\n' "${remaining_pids}" | xargs 2>/dev/null || true)
done
if [ -n "${remaining_pids}" ]; then
log_info "仍有进程未退出,执行强制停止: ${remaining_pids}"
for pid in ${remaining_pids}; do
kill -KILL "${pid}" 2>/dev/null || true
done
fi
rm -f "${PID_FILE}"
log_info "后端停止完成"
}
follow_logs() {
require_command "tail"
mkdir -p "${LOG_DIR}"
touch "${CONSOLE_LOG}"
log_info "持续输出日志中,按 Ctrl+C 仅退出日志查看,不会停止后端进程"
tail -n 200 -F "${CONSOLE_LOG}"
}
restart_action() {
stop_backend
start_backend
follow_logs
}
resolve_release_zip() {
if [ -n "${RELEASE_ZIP}" ]; then
RELEASE_ZIP=$(resolve_path "${RELEASE_ZIP}")
else
mkdir -p "${WORK_ROOT}"
candidate_file="${WORK_ROOT}/zip-candidates-${TIMESTAMP}.txt"
find "${SCRIPT_DIR}" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip' | sort >"${candidate_file}"
candidate_count=$(wc -l <"${candidate_file}" | tr -d ' ')
if [ "${candidate_count}" -eq 0 ]; then
log_error "未在脚本目录找到上线压缩包,请传入压缩包路径"
exit 1
fi
if [ "${candidate_count}" -gt 1 ]; then
log_error "脚本目录存在多个上线压缩包,请显式传入压缩包路径"
cat "${candidate_file}" >&2
exit 1
fi
RELEASE_ZIP=$(sed -n '1p' "${candidate_file}")
rm -f "${candidate_file}"
fi
if [ ! -f "${RELEASE_ZIP}" ]; then
log_error "上线压缩包不存在: ${RELEASE_ZIP}"
exit 1
fi
}
assert_deploy_layout() {
if [ ! -d "${APP_HOME}" ]; then
log_error "未找到后端目录: ${APP_HOME}"
exit 1
fi
if [ ! -d "${FRONTEND_DIR}" ]; then
log_error "未找到前端目录: ${FRONTEND_DIR}"
exit 1
fi
}
backup_dir() {
source_dir="$1"
target_dir="$2"
mkdir -p "${target_dir}"
if [ -n "$(find "${source_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
cp -a "${source_dir}/." "${target_dir}/"
log_info "已备份 ${source_dir}${target_dir}"
else
log_info "目录为空,已创建空备份目录: ${target_dir}"
fi
}
backup_current_files() {
mkdir -p "${BACKUP_DIR}"
backup_dir "${APP_HOME}" "${BACKUP_DIR}/backend"
backup_dir "${FRONTEND_DIR}" "${BACKUP_DIR}/frontend"
}
extract_release_package() {
mkdir -p "${WORK_DIR}/release" "${WORK_DIR}/frontend"
log_info "开始解压上线压缩包: ${RELEASE_ZIP}"
unzip -q "${RELEASE_ZIP}" -d "${WORK_DIR}/release"
RELEASE_JAR="${WORK_DIR}/release/${JAR_NAME}"
RELEASE_DIST_ZIP="${WORK_DIR}/release/dist.zip"
if [ ! -f "${RELEASE_JAR}" ]; then
log_error "上线压缩包根层缺少 ${JAR_NAME}"
exit 1
fi
if [ ! -f "${RELEASE_DIST_ZIP}" ]; then
log_error "上线压缩包根层缺少 dist.zip"
exit 1
fi
unzip -q "${RELEASE_DIST_ZIP}" -d "${WORK_DIR}/frontend"
FRONTEND_SOURCE_DIR="${WORK_DIR}/frontend/dist"
if [ ! -f "${FRONTEND_SOURCE_DIR}/index.html" ]; then
log_error "dist.zip 解压后未找到 dist/index.html"
exit 1
fi
}
deploy_backend() {
target_jar="${APP_HOME}/${JAR_NAME}"
deploying_jar="${APP_HOME}/.${JAR_NAME}.deploying"
cp "${RELEASE_JAR}" "${deploying_jar}"
mv "${deploying_jar}" "${target_jar}"
log_info "后端 Jar 已部署: ${target_jar}"
}
deploy_frontend() {
find "${FRONTEND_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -a "${FRONTEND_SOURCE_DIR}/." "${FRONTEND_DIR}/"
log_info "前端文件已部署到: ${FRONTEND_DIR}"
}
cleanup_work_dir() {
rm -rf "${WORK_DIR}"
}
deploy_action() {
RELEASE_ZIP="${1:-}"
require_command "find"
require_command "unzip"
resolve_release_zip
assert_deploy_layout
backup_current_files
trap cleanup_work_dir 0
extract_release_package
deploy_backend
deploy_frontend
cleanup_work_dir
trap - 0
log_info "部署完成,备份目录: ${BACKUP_DIR}"
log_info "开始重启后端并输出日志"
restart_action
}
main() {
action="${1:-help}"
case "${action}" in
deploy)
shift
if [ "$#" -gt 1 ]; then
usage
exit 1
fi
deploy_action "${1:-}"
;;
restart)
shift
if [ "$#" -ne 0 ]; then
usage
exit 1
fi
restart_action
;;
stop)
shift
if [ "$#" -ne 0 ]; then
usage
exit 1
fi
stop_backend
;;
-h|--help|help)
usage
;;
*)
usage
exit 1
;;
esac
}
main "$@"

View File

@@ -2,6 +2,10 @@ FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends fontconfig fonts-wqy-microhei \
&& rm -rf /var/lib/apt/lists/*
COPY backend/ruoyi-admin.jar /app/ruoyi-admin.jar
RUN mkdir -p /app/data/ruoyi /app/logs

View File

@@ -0,0 +1,99 @@
# 项目详情专项排查图谱嵌入设计
## 背景
项目详情页的“专项排查”页面当前已有“图谱外链展示”占位卡片,需要替换为真实 iframe 图谱展示能力。图谱分为“资金图谱”和“关系图谱”两个页签,对应 `assets/图谱.txt` 中的两个外部链接。
本次设计只涉及前端页面展示,不新增后端接口,不保存证件号,不调整项目详情主导航。
## 目标
- 在项目详情的“专项排查”页面内嵌入图谱展示区域。
- 图谱区域包含共用证件号输入框和“资金图谱 / 关系图谱”两个页签。
- 用户输入证件号后,前端执行标准化与 md5 加密,并替换图谱链接 `vId``/` 后的 hash 字符串。
- 未输入证件号前不加载 iframe只显示输入提示。
## 页面位置
`ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue` 中,将现有“图谱外链展示”占位卡片替换为独立组件:
- 上方:员工家庭资产负债专项核查
- 中间:图谱展示组件
- 下方:采购、招聘、调动扩展查询
组件建议命名为 `GraphAtlasSection.vue`,放在 `ruoyi-ui/src/views/ccdiProject/components/detail/` 目录下,并由 `SpecialCheck.vue` 引入。
## 交互设计
图谱组件顶部展示一行查询区:
- 证件号输入框
- “查询图谱”按钮
触发方式:
- 输入框回车触发查询
- 点击“查询图谱”按钮触发查询
图谱页签顺序:
1. 资金图谱
2. 关系图谱
未查询状态下iframe 不渲染,展示空状态提示“请输入证件号后查询图谱”。查询成功后,当前页签渲染对应 iframe。切换页签时复用同一个已加密证件号重新生成 iframe 地址。
## 数据处理
证件号处理流程:
1. 对用户输入执行 `trim`,去掉前后空格。
2. 如果标准化后的证件号末尾是小写 `x`,转换为大写 `X`
3. 使用现有 `@/utils/md5` 工具生成 md5。
4. 按当前页签替换图谱链接 `params.vId``/` 后的 hash。
图谱 `vId` 规则:
- 资金图谱:`idno_node/<md5证件号>`
- 关系图谱:`rel_node/<md5证件号>`
链接来源固定为 `assets/图谱.txt` 的两个外部图谱地址。本次按前端常量维护,不引入后端配置化。
## 异常处理
- 证件号为空时,点击按钮或回车提示“请输入证件号”,不加载 iframe。
- 证件号非空时不做身份证格式校验,只执行标准化和 md5避免引入需求外的业务规则。
- iframe 只负责设置 `src`,不读取跨域内容。
- 外部图谱服务的 token、网络或页面错误由 iframe 内部页面展示,当前系统不额外增加兜底跳转或降级逻辑。
- 当前阶段外部图谱页面可能无法正常打开,验收以 iframe 链接拼接正确为准,不以 iframe 内容成功渲染为通过条件。
## 样式要求
- 图谱区域延续专项排查页面现有白底卡片风格。
- iframe 区域设置稳定的最小高度,避免图谱区域过矮。
- 大屏下图谱区域尽量占用可视高度,方便查看节点关系。
- 查询区、页签、iframe 容器之间保持清晰间距,不影响上下两个专项排查区块。
## 测试设计
实现完成后按项目规则进行真实页面验证:
1. 进入真实项目详情页,切换到“专项排查”。
2. 确认图谱模块位于资产负债核查和扩展查询之间。
3. 未输入证件号时点击“查询图谱”,确认提示“请输入证件号”,且页面不加载 iframe。
4. 未输入证件号时在输入框按回车,确认同样提示“请输入证件号”,且页面不加载 iframe。
5. 输入证件号后点击按钮,确认资金图谱 iframe 地址中的 `vId``idno_node/<md5证件号>`
6. 修改证件号后在输入框按回车,确认资金图谱 iframe 地址刷新为新的 `idno_node/<md5证件号>`
7. 切换到关系图谱确认沿用同一个证件号iframe 地址中的 `vId``rel_node/<md5证件号>`
8. 使用带前后空格、末尾小写 `x` 的证件号重复验证,确认按 `trim + X` 标准化后生成 md5。
9. 当前阶段不要求外部图谱内容实际打开成功,以上 iframe `src` 链接正确即可判定图谱拼接链路通过。
10. 测试结束后关闭本轮启动的前端进程。
前端命令执行前必须先在 `ruoyi-ui` 目录执行 `nvm use`
## 非目标
- 不新增后端接口。
- 不将图谱链接写入数据库。
- 不保存用户输入的证件号或 md5 结果。
- 不新增项目详情顶部一级菜单。
- 不为外部图谱服务增加 token 刷新、降级链接或兼容逻辑。

View File

@@ -0,0 +1,572 @@
# 项目详情专项排查图谱嵌入前端实施计划
> **执行说明:** 按当前项目 `AGENTS.md` 规则执行。未得到用户明确授权时默认不开启 subagent如用户明确要求使用 subagent必须按项目规则指定模型与推理强度。Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在项目详情“专项排查”页面内,将现有图谱占位卡片替换为可输入证件号并生成 iframe 链接的资金图谱/关系图谱展示区。
**Architecture:** 只做前端最短路径改造。新增一个独立 Vue 2 组件负责证件号标准化、md5、tab 状态和 iframe `src` 拼接;`SpecialCheck.vue` 只负责引入组件并替换现有占位卡片。外部图谱当前可能打不开,验收只检查 iframe 链接是否按规则生成。
**Tech Stack:** Vue 2、Element UI、SCSS、`@/utils/md5``browser-use:browser` 实际页面验证。
---
## Scope Check
本计划只覆盖项目详情“专项排查”页内的图谱 iframe 嵌入,不新增后端接口,不保存证件号,不新增项目详情一级菜单,不做身份证格式校验,不增加外部图谱服务兜底或降级逻辑。
## File Structure
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue`
- 责任:维护共用证件号输入、资金/关系图谱 tab、证件号标准化、md5、iframe 链接生成和空状态展示。
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- 责任:移除现有“图谱外链展示”占位卡片,引入并渲染 `GraphAtlasSection`,并让图谱在资产负债核查空数据状态下仍可展示,清理不再使用的占位样式。
- Create: `docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md`
- 责任:记录本次前端实现内容、影响范围和验证情况。
- Do not commit: `output/browser-use/` 或其它临时测试文件。
## Test Data
用于链接断言的固定样例:
- 输入:`110101199003074211`
- md5`9b1b5eba4a26c9a68ff1ca06f40bee1b`
- 资金图谱期望 `vId``idno_node/9b1b5eba4a26c9a68ff1ca06f40bee1b`
- 关系图谱期望 `vId``rel_node/9b1b5eba4a26c9a68ff1ca06f40bee1b`
标准化样例:
- 输入:` 33078219900101123x `
- 标准化后:`33078219900101123X`
- md5`233c8519f86a57b1f00ec88a32152ce3`
## Task 1: Establish Current Failure Baseline
**Files:**
- Read: `docs/design/2026-05-07-project-special-check-graph-atlas-design.md`
- Read: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- [ ] **Step 1: Confirm the spec**
Run:
```bash
sed -n '1,140p' docs/design/2026-05-07-project-special-check-graph-atlas-design.md
```
Expected: spec confirms 专项排查内嵌 iframe、两个 tab、共用证件号、`trim + x 转 X + md5`、只校验 iframe `src`
- [ ] **Step 2: Confirm current placeholder**
Run:
```bash
rg -n "图谱外链展示|graph-placeholder" ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue
```
Expected: current page still has placeholder card and no证件号 input, no graph iframe.
- [ ] **Step 3: Record baseline failure**
Acceptance baseline:
- Current page cannot input证件号 in the graph area.
- Current page cannot produce `idno_node/<md5>` or `rel_node/<md5>` iframe links.
Expected: FAIL relative to the new spec. This is the intended pre-implementation failure.
## Task 2: Create GraphAtlasSection Component
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue`
- [ ] **Step 1: Create the component with minimal behavior**
Create `GraphAtlasSection.vue` with this implementation:
```vue
<template>
<section class="graph-atlas-card">
<div class="graph-atlas-header">
<div>
<div class="graph-atlas-title">图谱展示</div>
<div class="graph-atlas-subtitle">输入证件号后查看资金图谱和关系图谱</div>
</div>
</div>
<div class="graph-atlas-query">
<el-input
v-model="certNoInput"
class="graph-atlas-input"
clearable
placeholder="请输入证件号"
size="small"
@keyup.enter.native="handleQuery"
/>
<el-button
type="primary"
size="small"
icon="el-icon-search"
@click="handleQuery"
>
查询图谱
</el-button>
</div>
<el-tabs v-model="activeGraphTab" class="graph-atlas-tabs">
<el-tab-pane
v-for="item in graphTabs"
:key="item.name"
:label="item.label"
:name="item.name"
/>
</el-tabs>
<div v-if="!certNoHash" class="graph-atlas-empty">
<el-empty description="请输入证件号后查询图谱" />
</div>
<div v-else class="graph-atlas-frame-wrapper">
<iframe
:key="`${activeGraphTab}-${certNoHash}`"
class="graph-atlas-frame"
:src="currentGraphUrl"
title="专项排查图谱"
/>
</div>
</section>
</template>
<script>
import md5 from "@/utils/md5";
const GRAPH_ATLAS_CONFIG = {
fund: {
label: "资金图谱",
urlTemplate: 'http://64.202.65.112:8082/atlas/refactor/#/home/graph/downloadService?id=ccdi_lanxi_trans&mode=K_EXPAND&type=NORMAL&atlasToken=F4BBA291A285858BAF4526C6EC312388&params={"vId":"idno_node/{hash}"}',
},
relationship: {
label: "关系图谱",
urlTemplate: 'http://64.202.65.112:8082/atlas/refactor/#/home/graph/downloadService?id=lanxitest&mode=K_EXPAND&type=NORMAL&atlasToken=2C914E5E1FBFBC4AD15163E0AB03B800&params={"vId":"rel_node/{hash}"}',
},
};
export default {
name: "GraphAtlasSection",
data() {
return {
certNoInput: "",
certNoHash: "",
activeGraphTab: "fund",
graphTabs: [
{ name: "fund", label: GRAPH_ATLAS_CONFIG.fund.label },
{ name: "relationship", label: GRAPH_ATLAS_CONFIG.relationship.label },
],
};
},
computed: {
currentGraphUrl() {
if (!this.certNoHash) {
return "";
}
const config = GRAPH_ATLAS_CONFIG[this.activeGraphTab];
return config.urlTemplate.replace("{hash}", this.certNoHash);
},
},
methods: {
handleQuery() {
const certNo = this.normalizeCertNo(this.certNoInput);
if (!certNo) {
this.certNoHash = "";
this.$message.warning("请输入证件号");
return;
}
this.certNoHash = md5(certNo);
},
normalizeCertNo(value) {
const certNo = value === null || value === undefined ? "" : String(value).trim();
if (certNo.endsWith("x")) {
return `${certNo.slice(0, -1)}X`;
}
return certNo;
},
},
};
</script>
<style lang="scss" scoped>
.graph-atlas-card {
margin-top: 16px;
padding: 20px;
background: #fff;
border: 1px solid var(--ccdi-border);
border-radius: 14px;
box-shadow: var(--ccdi-shadow);
}
.graph-atlas-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.graph-atlas-title {
font-size: 16px;
font-weight: 600;
color: var(--ccdi-text-primary);
}
.graph-atlas-subtitle {
margin-top: 4px;
font-size: 12px;
color: var(--ccdi-text-muted);
}
.graph-atlas-query {
display: flex;
align-items: center;
gap: 10px;
margin-top: 16px;
}
.graph-atlas-input {
width: 320px;
}
.graph-atlas-tabs {
margin-top: 16px;
}
.graph-atlas-empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 360px;
border: 1px dashed #d9e3ee;
background: #f8fbfe;
}
.graph-atlas-frame-wrapper {
overflow: hidden;
border: 1px solid #d9e3ee;
background: #fff;
}
.graph-atlas-frame {
display: block;
width: 100%;
height: calc(100vh - 280px);
min-height: 560px;
max-height: 760px;
border: 0;
}
@media (max-width: 768px) {
.graph-atlas-query {
align-items: stretch;
flex-direction: column;
}
.graph-atlas-input {
width: 100%;
}
.graph-atlas-frame {
min-height: 480px;
}
}
</style>
```
- [ ] **Step 2: Check component syntax**
Run:
```bash
cd ruoyi-ui
nvm use
npm run build:prod
```
Expected: this may still fail before wiring if the file is not imported, but it must not report syntax errors in `GraphAtlasSection.vue` once wired in Task 3.
## Task 3: Replace SpecialCheck Placeholder
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- [ ] **Step 1: Import and register the new component**
Modify the script section:
```js
import ExtendedQuerySection from "./ExtendedQuerySection";
import FamilyAssetLiabilitySection from "./FamilyAssetLiabilitySection";
import GraphAtlasSection from "./GraphAtlasSection";
export default {
name: "SpecialCheck",
components: {
ExtendedQuerySection,
FamilyAssetLiabilitySection,
GraphAtlasSection,
},
```
- [ ] **Step 2: Restructure the content area and replace the placeholder block**
The graph must render after the专项排查 data request finishes, even when the family asset/liability list is empty. Replace the current loading/empty/loaded template split with this structure:
```vue
<div v-if="pageState === 'loading'" class="special-check-state">
<div class="state-card">
<el-skeleton animated :rows="6" />
</div>
</div>
<div v-else class="special-check-page">
<div v-if="pageState === 'empty'" class="special-check-state">
<div class="state-card">
<el-empty description="暂无员工家庭资产负债核查数据" />
</div>
</div>
<family-asset-liability-section
v-else
:rows="currentData.rows"
:loading="false"
:project-id="projectId"
:title="sectionTitle"
:subtitle="sectionSubtitle"
@evidence-confirm="$emit('evidence-confirm', $event)"
/>
<graph-atlas-section />
</div>
```
Keep the existing `<div v-if="projectId" class="special-check-extended-wrapper">` below this block unchanged, so the final order is: asset/liability loaded table or empty state -> graph atlas -> extended query.
- [ ] **Step 3: Remove obsolete placeholder styles**
Delete these obsolete scoped style blocks from `SpecialCheck.vue`:
```scss
.graph-placeholder-card { ... }
.graph-placeholder-header { ... }
.graph-placeholder-title { ... }
.graph-placeholder-subtitle { ... }
.graph-placeholder-body { ... }
.graph-placeholder-text { ... }
```
Keep `.special-check-extended-wrapper` unchanged so the扩展查询 spacing remains stable.
- [ ] **Step 4: Build the frontend**
Run:
```bash
cd ruoyi-ui
nvm use
npm run build:prod
```
Expected: build completes successfully with no Vue template or module resolution errors.
## Task 4: Browser Verification
**Files:**
- Verify: `ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue`
- Verify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- Do not commit: `output/browser-use/`
- [ ] **Step 1: Start frontend for real page testing**
Run:
```bash
cd ruoyi-ui
nvm use
npm run dev
```
Expected: dev server starts successfully. Record the local URL. If the default port is occupied, use the URL printed by Vue CLI.
- [ ] **Step 2: Use browser-use on a real project detail page**
Use `browser-use:browser`, not a prototype page.
Navigate to a real project detail route, for example:
```text
http://localhost:<port>/ccdiProject/detail/<projectId>?tab=special
```
Expected:
- “图谱展示” appears in the专项排查 page after the data request finishes.
- If the project has资产负债核查 rows, the graph is below员工家庭资产负债专项核查 and above采购/招聘/调动扩展查询.
- If the project has no资产负债核查 rows, the graph still appears below the existing empty state and above采购/招聘/调动扩展查询.
- [ ] **Step 3: Verify empty input behavior**
Actions:
1. Confirm iframe is absent before any query.
2. Click “查询图谱” with empty input.
3. Press Enter in the empty input.
Expected:
- Page shows “请输入证件号”.
- iframe is still absent.
- [ ] **Step 4: Verify funds graph URL by button**
Actions:
1. Input `110101199003074211`.
2. Click “查询图谱”.
3. Inspect the iframe raw attribute with `document.querySelector(".graph-atlas-frame").getAttribute("src")`. If the tool only exposes normalized `iframe.src`, run `decodeURIComponent(src)` before checking the JSON `params` fragment.
Expected raw iframe `src`, or decoded normalized `src`, contains:
```text
id=ccdi_lanxi_trans
atlasToken=F4BBA291A285858BAF4526C6EC312388
"vId":"idno_node/9b1b5eba4a26c9a68ff1ca06f40bee1b"
```
External graph content may fail to load; this is acceptable. The `src` is the pass condition.
- [ ] **Step 5: Verify funds graph URL by Enter**
Actions:
1. Replace input with ` 33078219900101123x `.
2. Press Enter.
3. Inspect the iframe raw attribute with `document.querySelector(".graph-atlas-frame").getAttribute("src")`. If the tool only exposes normalized `iframe.src`, run `decodeURIComponent(src)` before checking the JSON `params` fragment.
Expected raw iframe `src`, or decoded normalized `src`, contains:
```text
"vId":"idno_node/233c8519f86a57b1f00ec88a32152ce3"
```
This proves `trim + x 转 X + md5`.
- [ ] **Step 6: Verify relationship graph URL**
Actions:
1. Switch to “关系图谱”.
2. Inspect the iframe raw attribute with `document.querySelector(".graph-atlas-frame").getAttribute("src")`. If the tool only exposes normalized `iframe.src`, run `decodeURIComponent(src)` before checking the JSON `params` fragment.
Expected raw iframe `src`, or decoded normalized `src`, contains:
```text
id=lanxitest
atlasToken=2C914E5E1FBFBC4AD15163E0AB03B800
"vId":"rel_node/233c8519f86a57b1f00ec88a32152ce3"
```
External graph content may fail to load; this is acceptable. The `src` is the pass condition.
- [ ] **Step 7: Stop frontend dev process**
Stop the process started in Step 1.
Expected: no test frontend process remains running.
## Task 5: Implementation Record
**Files:**
- Create: `docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md`
- [ ] **Step 1: Write the implementation report**
Create the report with this structure:
```markdown
# 项目详情专项排查图谱嵌入前端实施记录
## 修改内容
- 新增 `GraphAtlasSection.vue`,实现证件号输入、资金图谱/关系图谱页签、md5 后 iframe 链接拼接。
- 调整 `SpecialCheck.vue`,将原图谱占位卡片替换为图谱展示组件。
## 影响范围
- 仅影响项目详情页“专项排查”中的图谱展示区域。
- 不涉及后端接口、数据库、权限、菜单和项目详情顶部一级导航。
## 验证情况
- `cd ruoyi-ui && nvm use && npm run build:prod`:记录结果。
- 真实项目详情页专项排查浏览器验证:记录项目地址、输入样例、资金图谱 iframe `src`、关系图谱 iframe `src`
- 说明当前外部图谱内容打不开属于预期,验收只检查 iframe 链接正确。
## 遗留事项
- 无。
```
- [ ] **Step 2: Check report path**
Run:
```bash
ls docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md
```
Expected: file exists under `docs/reports/implementation/`.
## Task 6: Final Git Check
**Files:**
- Stage when ready:
- `ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- `docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md`
- [ ] **Step 1: Inspect worktree**
Run:
```bash
git status --short
```
Expected: existing unrelated dirty files may still be present. Do not stage unrelated files such as existing Docker, application config, SQL, PDF exporter, or other report changes unless the user explicitly requests them.
- [ ] **Step 2: Inspect relevant diff**
Run:
```bash
git diff -- ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md
```
Expected: diff only contains the graph atlas feature and implementation report.
- [ ] **Step 3: Stage only this task**
Run:
```bash
git add -- ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md
git diff --cached --name-status
```
Expected: staged files are exactly the three paths above.
- [ ] **Step 4: Commit if requested**
Run only when the user asks to commit:
```bash
git commit -m "接入专项排查图谱展示"
```
Expected: Chinese commit message, no unrelated files included.

View File

@@ -0,0 +1,36 @@
# 关联业务自动补入实体库实施记录
## 修改内容
- 新增 `EnterpriseAutoFillService`,统一处理关联业务按统一社会信用代码补入 `ccdi_enterprise_base_info`
- 员工实体关系、信贷客户实体关系、中介关系、招投标供应商新增与导入成功数据,均在保存关联关系前按缺失实体自动补入实体库。
- 企业名称按“允许为空,有值则入库”的规则处理;供应商场景使用供应商名称补入,其余当前无企业名称来源的场景补入 `NULL`
- 新增企业来源枚举 `SUPPLIER/供应商`,前端实体库企业来源下拉复用后端枚举接口,无需新增前端硬编码。
- 中介来源实体默认风险等级为高风险;中介实体关系不再要求实体库预先存在。
- 新增迁移脚本 `sql/migration/2026-04-26-make-enterprise-name-nullable-for-auto-fill.sql`,将 `ccdi_enterprise_base_info.enterprise_name` 调整为可空。
## 影响范围
- 后端模块:`ccdi-info-collection`
- 相关业务:员工关系实体、信贷客户实体、中介关系、招投标供应商、实体库导入、企业来源枚举
- 数据库表:`ccdi_enterprise_base_info`
## 验证情况
- 后端单元验证已通过:
```bash
mvn -pl ccdi-info-collection -am -Dtest=CcdiEnumControllerTest,EnterpriseAutoFillServiceTest,CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest,CcdiCustEnterpriseRelationServiceImplTest,CcdiCustEnterpriseRelationImportServiceImplTest,CcdiIntermediaryServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiEnterpriseBaseInfoImportServiceImplTest,CcdiPurchaseTransactionServiceImplTest,CcdiPurchaseTransactionImportServiceImplTest,CcdiPurchaseTransactionFeatureContractTest -Dsurefire.failIfNoSpecifiedTests=false test
```
- 启动当前工作树后端成功,监听 `62318`
- 使用 `nvm use` 切换到 Node `v14.21.3`,启动前端开发服务到 `http://localhost:8080/`
- 使用 browser-use 打开真实业务页面:
- `http://localhost:8080/maintain/enterpriseBaseInfo` 加载正常,企业来源下拉出现“供应商”。
- `http://localhost:8080/maintain/purchaseTransaction` 加载正常,供应商明细相关页面可访问。
- 后端枚举接口 `GET /ccdi/enum/enterpriseSource` 已返回 `{ "value": "SUPPLIER", "label": "供应商" }`
## 未执行事项
- 远程联调库当前 `ccdi_enterprise_base_info.enterprise_name` 仍为 `Null=NO`
- 本次未直接执行远程库结构变更;提权执行迁移脚本被安全审查拦截。真实写入空企业名称的补库验证需先由授权人员执行迁移脚本。

View File

@@ -0,0 +1,30 @@
# 员工信息维护导入模板列宽调整实施记录
## 背景
员工信息维护页面下载的导入模板中,空模板会被自动列宽策略压缩,导致每列显示过窄,不便于直接填写身份证号、电话、金额、日期等字段。
## 修改内容
- 调整 `CcdiBaseStaffExcel` 中员工信息 Sheet 的各列 `@ColumnWidth`,放宽姓名、员工 ID、部门 ID、身份证号、电话、年收入、入职时间、是否党员、状态列宽。
- 调整 `CcdiBaseStaffAssetInfoExcel` 中员工资产信息 Sheet 的各列 `@ColumnWidth`,放宽员工身份证号、资产分类、产权占比、日期、估值、资产状态和备注列宽。
- 双 Sheet 导入模板生成时不再使用空数据自动匹配列宽策略,改为按 Excel DTO 上声明的列宽输出,避免空模板被压窄。
- 补充单元测试,校验员工信息维护双 Sheet 导入模板实际生成的列宽不低于 DTO 声明宽度。
## 影响范围
- 影响接口:`POST /ccdi/baseStaff/importTemplate`
- 影响模板:
- `员工信息` Sheet
- `员工资产信息` Sheet
- 不涉及导入解析、字段校验、数据库结构和前端交互逻辑。
## 验证情况
已执行:
```bash
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest -Dsurefire.failIfNoSpecifiedTests=false test
```
验证结果:模板工具测试通过,覆盖员工信息维护双 Sheet 模板列宽、下拉框、文本格式以及既有模板生成逻辑。

View File

@@ -0,0 +1,31 @@
# 员工信息维护导入模板必填表头标记实施记录
## 背景
员工信息维护导入模板中,标注了 `@Required` 的字段表头未显示 `*`。排查发现 `RequiredFieldWriteHandler` 在 Sheet 创建完成后立即读取表头行,但此时 EasyExcel 尚未写入表头,因此会跳过必填标记处理。
## 修改内容
-`RequiredFieldWriteHandler` 从 Sheet 创建后处理改为表头单元格写入完成后处理。
- 对标注了 `@Required` 的表头追加 `*` 并应用原有红色加粗样式。
- 增加防重复处理,表头已经以 `*` 结尾时不再重复追加。
- 补充员工信息维护模板测试,校验员工信息 Sheet 必填表头输出为 `姓名*``员工ID*``所属部门ID*``身份证号*``电话*``是否党员*``状态*`,并校验员工资产信息 Sheet 已有 `*` 不重复追加。
## 影响范围
- 影响接口:`POST /ccdi/baseStaff/importTemplate`
- 影响模板:
- `员工信息` Sheet
- `员工资产信息` Sheet
- 处理器为共享模板处理器,其他使用 `@Required``RequiredFieldWriteHandler` 的导入模板会恢复必填表头 `*` 标记。
- 不涉及导入解析、校验逻辑、数据库结构和前端交互。
## 验证情况
已执行:
```bash
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest -Dsurefire.failIfNoSpecifiedTests=false test
```
验证结果:测试通过,员工信息维护导入模板的必填表头星号、列宽、下拉框、文本格式均通过断言。

View File

@@ -0,0 +1,32 @@
# 员工信息维护导入模板文本格式实施记录
## 背景
- 员工信息维护导入模板中,“身份证号”“电话”列需要使用文本单元格格式,避免 Excel 将长数字或手机号自动识别为数值格式。
- 当前模板生成链路已支持 `@TextFormat` 注解并由 `TextFormatWriteHandler` 统一处理,但员工信息 Sheet 的对应字段未标注该注解。
## 修改内容
-`CcdiBaseStaffExcel` 中为“身份证号”字段增加 `@TextFormat`
-`CcdiBaseStaffExcel` 中为“电话”字段增加 `@TextFormat`
- 更新 `EasyExcelUtilTemplateTest`,在员工信息模板测试中断言第 3 列“身份证号”和第 4 列“电话”的默认列格式均为文本格式 `@`
## 影响范围
- 影响接口:`/ccdi/baseStaff/importTemplate`
- 影响文件:
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiBaseStaffExcel.java`
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java`
- 不改变导入字段、字段顺序、导入校验规则和前端页面逻辑。
## 验证情况
- 已执行定向模板单测:
```bash
MAVEN_OPTS="-javaagent:$HOME/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.8/byte-buddy-agent-1.17.8.jar" mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest -Dsurefire.failIfNoSpecifiedTests=false test
```
- 验证结果:通过,`EasyExcelUtilTemplateTest` 共 5 个用例0 失败0 错误。
- 测试日志确认员工信息 Sheet 第 3 列、第 4 列均已执行文本格式设置。
- 本次未启动前端或后端进程,无需额外清理运行进程。

View File

@@ -0,0 +1,83 @@
# 四类关联业务自动补入实体库回测记录
## 保存路径确认
- 本次实施记录保存于 `docs/reports/implementation/`
## 回测范围
- 员工亲属实体关联自动补入实体库。
- 信贷客户实体关联自动补入实体库。
- 中介关联机构自动补入实体库。
- 招投标供应商自动补入实体库。
## 本次调整
- 修正中介实体关联导入单元测试的旧断言:实体库缺失时不再失败,成功行应调用 `EnterpriseAutoFillService` 自动补入,失败行不补入。
- 修正本地供应商契约测试在 Maven 模块目录下读取仓库根 SQL 的路径口径。
- 业务代码未调整。
## 单元回归
执行命令:
```bash
mvn -pl ccdi-info-collection -am -Dtest=CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiIntermediaryServiceImplTest,CcdiPurchaseTransactionFeatureContractTest,CcdiEnterpriseBaseInfoImportServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
```
执行结果:
- `Tests run: 24, Failures: 0, Errors: 0, Skipped: 0`
- `BUILD SUCCESS`
## 真实接口回测
后端启动:
```bash
sh bin/restart_java_backend.sh restart
```
本轮使用测试前缀 `RT20260506`,先通过 `bin/mysql_utf8_exec.sh` 写入员工亲属、信贷客户关系人、中介本人三类前置数据,再调用真实后端接口:
| 来源 | 接口 | 统一社会信用代码 | 接口结果 |
| --- | --- | --- | --- |
| 员工亲属 | `POST /ccdi/staffEnterpriseRelation` | `91330100RT20260501` | 操作成功 |
| 信贷客户 | `POST /ccdi/custEnterpriseRelation` | `91330100RT20260502` | 操作成功 |
| 中介 | `POST /ccdi/intermediary/RT20260506IM001/enterprise-relation` | `91330100RT20260503` | 操作成功 |
| 供应商 | `POST /ccdi/purchaseTransaction` | `91330100RT20260504` | 操作成功 |
## 数据库回查
实体库回查结果:
| social_credit_code | enterprise_name | ent_source | data_source | risk_level | created_by |
| --- | --- | --- | --- | --- | --- |
| `91330100RT20260501` | 回测员工亲属自动补入企业 | `EMP_RELATION` | `MANUAL` | `NULL` | `admin` |
| `91330100RT20260502` | 回测信贷客户自动补入企业 | `CREDIT_CUSTOMER` | `MANUAL` | `NULL` | `admin` |
| `91330100RT20260503` | `NULL` | `INTERMEDIARY` | `MANUAL` | `1` | `admin` |
| `91330100RT20260504` | 回测供应商自动补入企业 | `SUPPLIER` | `MANUAL` | `NULL` | `admin` |
关系表回查:
- `ccdi_staff_enterprise_relation`1 条
- `ccdi_cust_enterprise_relation`1 条
- `ccdi_intermediary_enterprise_relation`1 条
- `ccdi_purchase_transaction_supplier`1 条
## 清理结果
执行 `bin/mysql_utf8_exec.sh output/sql/2026-05-06-enterprise-autofill-retrotest-cleanup.sql` 后回查:
- `enterprise_rows=0`
- `staff_relation_rows=0`
- `cust_relation_rows=0`
- `intermediary_relation_rows=0`
- `purchase_rows=0`
- `supplier_rows=0`
## 结论
- 四类来源均可通过真实接口自动补入 `ccdi_enterprise_base_info`
- 来源映射符合预期:员工亲属 `EMP_RELATION`、信贷客户 `CREDIT_CUSTOMER`、中介 `INTERMEDIARY`、供应商 `SUPPLIER`
- 中介来源风险等级自动落为 `1`,其余三类风险等级为空,符合既定规则。

View File

@@ -0,0 +1,102 @@
# 关联业务导入接口自动补入实体库回测记录
## 基本信息
- 回测日期2026-05-06
- 回测范围:员工亲属实体关联、信贷客户实体关联、中介实体关联、招投标供应商四个导入接口
- 文档保存路径:`docs/reports/implementation/2026-05-06-enterprise-autofill-import-retrotest.md`
- 测试文件目录:`output/spreadsheet/import-retrotest/`
- SQL 文件目录:`output/sql/`
## 回测准备
1. 使用 `sh bin/restart_java_backend.sh restart` 启动后端,后端运行端口为 `62318`
2. 使用 `bin/mysql_utf8_exec.sh output/sql/2026-05-06-enterprise-autofill-import-retrotest-seed.sql` 写入前置数据。
3. 通过 `/login/test` 获取 `admin` 登录态。
4. 从真实后端接口下载当前导入模板,并基于模板填充第 2 行测试数据:
- `/ccdi/staffEnterpriseRelation/importTemplate`
- `/ccdi/custEnterpriseRelation/importTemplate`
- `/ccdi/intermediary/importEnterpriseRelationTemplate`
- `/ccdi/purchaseTransaction/importTemplate`
## 导入接口回测结果
| 类型 | 导入接口 | 任务 ID | 状态 | 成功数 | 失败数 |
| --- | --- | --- | --- | --- | --- |
| 员工亲属 | `/ccdi/staffEnterpriseRelation/importData` | `934cf8b8-de56-4478-be9c-e2261652c993` | `SUCCESS` | 1 | 0 |
| 信贷客户 | `/ccdi/custEnterpriseRelation/importData` | `5f21cd94-307e-4681-9659-64f3d270ef0a` | `SUCCESS` | 1 | 0 |
| 中介 | `/ccdi/intermediary/importEnterpriseRelationData` | `9738837f-d657-4078-9d1d-6269ac35ecdc` | `SUCCESS` | 1 | 0 |
| 供应商 | `/ccdi/purchaseTransaction/importData` | `0cc37465-98b6-4c98-88be-8b28cbe0f8db` | `SUCCESS` | 1 | 0 |
四个接口均使用 multipart `file` 参数上传基于真实模板生成的 Excel 文件,并轮询对应 `importStatus` 接口至最终状态。
## 实体库回查结果
使用 `bin/mysql_utf8_exec.sh output/sql/2026-05-06-enterprise-autofill-import-retrotest-verify.sql` 回查实体库与业务表。
| 统一社会信用代码 | 企业名称 | 企业来源 | 数据来源 | 风险等级 | 创建人 |
| --- | --- | --- | --- | --- | --- |
| `91330100RT20260601` | 导入回测员工亲属自动补入企业 | `EMP_RELATION` | `IMPORT` | `NULL` | `admin` |
| `91330100RT20260602` | 导入回测信贷客户自动补入企业 | `CREDIT_CUSTOMER` | `IMPORT` | `NULL` | `admin` |
| `91330100RT20260603` | `NULL` | `INTERMEDIARY` | `IMPORT` | `1` | `admin` |
| `91330100RT20260604` | 导入回测供应商自动补入企业 | `SUPPLIER` | `IMPORT` | `NULL` | `admin` |
业务表回查结果:
| 检查项 | 记录数 |
| --- | --- |
| `staff_relation` | 1 |
| `cust_relation` | 1 |
| `intermediary_relation` | 1 |
| `purchase_supplier` | 1 |
## 清理结果
使用 `bin/mysql_utf8_exec.sh output/sql/2026-05-06-enterprise-autofill-import-retrotest-cleanup.sql` 删除本轮导入数据、实体库补入数据和测试前置数据。
| 检查项 | 剩余记录数 |
| --- | --- |
| `enterprise_base_info` | 0 |
| `staff_relation` | 0 |
| `cust_relation` | 0 |
| `intermediary_relation` | 0 |
| `purchase_supplier` | 0 |
| `purchase_transaction` | 0 |
| `staff_family_seed` | 0 |
| `intermediary_seed` | 0 |
## 保留导入数据复测
按用户要求保留导入 Excel 模板文件与导入数据后,重新执行一轮导入。本轮没有执行清理 SQL。
1. 确认 `output/spreadsheet/import-retrotest/` 下模板文件和导入文件均保留。
2. 使用 `bin/mysql_utf8_exec.sh output/sql/2026-05-06-enterprise-autofill-import-retrotest-seed-keep-data.sql` 仅补充导入所需前置数据,该脚本不包含删除语句。
3. 复用已保留的四个导入 Excel 文件上传导入接口。
4. 使用 `bin/mysql_utf8_exec.sh output/sql/2026-05-06-enterprise-autofill-import-retrotest-verify.sql` 回查实体库与业务表,确认导入数据保留。
| 类型 | 导入接口 | 任务 ID | 状态 | 成功数 | 失败数 |
| --- | --- | --- | --- | --- | --- |
| 员工亲属 | `/ccdi/staffEnterpriseRelation/importData` | `da75f851-bc25-4930-8e0f-81dbfeb7e658` | `SUCCESS` | 1 | 0 |
| 信贷客户 | `/ccdi/custEnterpriseRelation/importData` | `7eaa18da-aa69-41f6-90f5-acb74d63d1e6` | `SUCCESS` | 1 | 0 |
| 中介 | `/ccdi/intermediary/importEnterpriseRelationData` | `ed9931f2-950e-4778-bf5d-d57d702015b8` | `SUCCESS` | 1 | 0 |
| 供应商 | `/ccdi/purchaseTransaction/importData` | `0582c091-785c-4c71-af24-9f03375b86ea` | `SUCCESS` | 1 | 0 |
当前保留数据回查结果:
| 统一社会信用代码 | 企业名称 | 企业来源 | 数据来源 | 风险等级 | 创建人 |
| --- | --- | --- | --- | --- | --- |
| `91330100RT20260601` | 导入回测员工亲属自动补入企业 | `EMP_RELATION` | `IMPORT` | `NULL` | `admin` |
| `91330100RT20260602` | 导入回测信贷客户自动补入企业 | `CREDIT_CUSTOMER` | `IMPORT` | `NULL` | `admin` |
| `91330100RT20260603` | `NULL` | `INTERMEDIARY` | `IMPORT` | `1` | `admin` |
| `91330100RT20260604` | 导入回测供应商自动补入企业 | `SUPPLIER` | `IMPORT` | `NULL` | `admin` |
| 检查项 | 当前记录数 |
| --- | --- |
| `staff_relation` | 1 |
| `cust_relation` | 1 |
| `intermediary_relation` | 1 |
| `purchase_supplier` | 1 |
## 结论
员工亲属、信贷客户、中介、供应商四个导入接口均可以正常触发实体库自动补入。实体库来源、数据来源和风险等级与当前实现一致。按最新要求,导入 Excel 模板文件与导入数据已保留。

View File

@@ -0,0 +1,29 @@
# 员工亲属关系与信贷客户家庭关系关系人身份证号展示实施记录
## 修改内容
- 在【员工亲属关系维护】列表新增“关系人身份证号”列,展示接口返回的 `relationCertNo`
- 在【信贷客户家庭关系】列表新增“关系人身份证号”列,展示接口返回的 `relationCertNo`
- 两个页面查询区新增“关系人身份证号”筛选项,支持按关系人身份证号模糊查询。
- 后端查询 DTO 与 MyBatis 分页 SQL 补充 `relationCertNo` 查询条件,保持页面查询条件与接口过滤逻辑一致。
## 影响范围
- 前端页面:
- `ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue`
- `ruoyi-ui/src/views/ccdiCustFmyRelation/index.vue`
- 后端查询:
- `CcdiStaffFmyRelationQueryDTO`
- `CcdiCustFmyRelationQueryDTO`
- `CcdiStaffFmyRelationMapper.xml`
- `CcdiCustFmyRelationMapper.xml`
## 验证情况
- 已确认两个分页接口原 SQL 与 VO 均包含 `relation_cert_no` / `relationCertNo` 字段。
- 已执行 `mvn -pl ccdi-info-collection -am compile -DskipTests`,编译通过。
- 已按 `ruoyi-ui/.nvmrc` 使用 Node `14.21.3` 执行 `npm run build:prod`,构建通过,仅存在既有资源体积告警。
- 已通过 `browser-use` 打开真实前端页面验证:
- `/maintain/staffFmyRelation` 展示“关系人身份证号”筛选项与表格列,并可按 `330101196501010011` 查询到对应员工亲属关系记录。
- `/maintain/custFmyRelation` 展示“关系人身份证号”筛选项与表格列,并可按 `330101197806060077` 查询到对应信贷客户家庭关系记录。
- 浏览器控制台未发现错误日志。

View File

@@ -0,0 +1,54 @@
# 信息模块导入模板文本格式覆盖实施记录
## 背景
- 信息模块多个导入模板中存在证件号、身份证号、电话、手机号、统一社会信用代码、银行账户、账户号码、银行代码、工号、跨 Sheet 关联编号等需要保真录入的字段。
- 这类字段如果使用 Excel 默认常规格式,可能被自动识别为数值或科学计数法,导致前导零、长号码或编码内容失真。
## 修改内容
- 为信息模块导入模板 DTO 中的保真字段补充 `@TextFormat`
- 账户库:证件号、账户号码、银行代码。
- 员工信息维护:身份证号、电话;员工资产 Sheet 的员工身份证号保持文本格式。
- 员工亲属关系、信贷客户家庭关系:主身份证号、关系人证件号码、手机号码 1、手机号码 2。
- 员工亲属实体关联、信贷客户实体关联:身份证号、统一社会信用代码。
- 实体库管理、实体中介:统一社会信用代码、法定代表人证件号码。
- 个人中介:证件号码、手机号码、企业统一信用码、关联中介本人证件号码。
- 中介实体关联关系:中介本人证件号码、统一社会信用代码。
- 招投标信息维护:采购事项 ID、申请人工号、采购负责人工号、供应商统一信用代码、供应商联系电话、供应商银行账户。
- 招聘信息维护:招聘记录编号、证件号码、面试官工号;历史工作经历 Sheet 的招聘记录编号。
- 更新普通导入模板生成方法 `EasyExcelUtil.importTemplateExcel(...)`,使不带字典下拉框的模板也会注册 `TextFormatWriteHandler`
- 扩展 `EasyExcelUtilTemplateTest`,实际生成各导入模板 workbook 并断言上述列的默认列格式为文本格式 `@`
## 影响范围
- 后端模板生成工具:
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java`
- 信息模块导入模板 DTO
- `CcdiAccountInfoExcel`
- `CcdiBaseStaffExcel`
- `CcdiCustEnterpriseRelationExcel`
- `CcdiCustFmyRelationExcel`
- `CcdiEnterpriseBaseInfoExcel`
- `CcdiIntermediaryEnterpriseRelationExcel`
- `CcdiIntermediaryEntityExcel`
- `CcdiIntermediaryPersonExcel`
- `CcdiPurchaseTransactionExcel`
- `CcdiPurchaseTransactionSupplierExcel`
- `CcdiStaffEnterpriseRelationExcel`
- `CcdiStaffFmyRelationExcel`
- `CcdiStaffRecruitmentExcel`
- `CcdiStaffRecruitmentWorkExcel`
- 不改变导入字段顺序、字段类型、业务校验、字典下拉框和页面交互。
## 验证情况
- 已执行定向模板测试:
```bash
MAVEN_OPTS="-javaagent:$HOME/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.8/byte-buddy-agent-1.17.8.jar" mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest -Dsurefire.failIfNoSpecifiedTests=false test
```
- 验证结果:通过,`EasyExcelUtilTemplateTest` 共 7 个用例0 失败0 错误。
- 测试过程已实际生成单 Sheet 与双 Sheet 导入模板,并检查保真字段列格式为文本格式。
- 本次未启动前端或后端进程,无需清理测试进程。

View File

@@ -0,0 +1,67 @@
# 招聘信息与中介实体关系导入模板修复实施记录
## 背景
- 【招聘信息维护】当前页面与数据库口径已调整为“招聘记录编号”,原导入模板仍存在旧字段名称和字段顺序不一致问题,导致按模板导入失败。
- 【中介库管理】“导入中介实体关联关系”按钮对应模板字段与新增关联机构弹窗不一致,且导入提示仍要求统一社会信用代码必须已存在于机构表。
## 修改内容
### 招聘信息维护
- 更新招聘主 Sheet 模板字段顺序,使其与新增页字段顺序一致:
- 招聘记录编号、招聘项目名称、职位名称、职位类别、职位描述、录用情况、候选人姓名、招聘类型、学历、证件号码、毕业年月、毕业院校、专业、面试官1姓名、面试官1工号、面试官2姓名、面试官2工号。
- 更新历史工作经历 Sheet 字段文案:
- 岗位名称、入职时间、离职时间、主要工作内容。
- 导入逻辑不再从招聘项目名称推断招聘类型,改为读取模板中的“招聘类型”字段。
- 招聘类型支持填写编码或页面文案:
- `SOCIAL` / `社招`
- `CAMPUS` / `校招`
- 同步调整导入校验提示,使错误信息与当前页面字段保持一致。
- 前端导入弹窗增加招聘类型填写说明。
### 中介库管理
- 更新“导入中介实体关联关系”模板字段:
- 中介本人证件号码、统一社会信用代码、关联职务、备注。
- 将必填标识改为 `@Required`,避免字段标题携带 `*` 后与页面字段不一致。
- 导入逻辑统一 trim 证件号、统一社会信用代码、关联职务、备注,避免空格导致查询不到中介本人或重复判断失效。
- 失败记录字段文案由“关联人职务”统一为“关联职务”。
- 前端导入说明调整为:
- 中介本人证件号码用于定位新增弹窗中的所属中介;
- 其余字段与新增关联机构弹窗一致;
- 统一社会信用代码未存在于实体库时会自动补入。
## 影响范围
- 后端导入模板与导入解析:
- `CcdiStaffRecruitmentExcel`
- `CcdiStaffRecruitmentWorkExcel`
- `CcdiStaffRecruitmentImportServiceImpl`
- `CcdiIntermediaryEnterpriseRelationExcel`
- `CcdiIntermediaryEnterpriseRelationImportServiceImpl`
- `IntermediaryEnterpriseRelationImportFailureVO`
- 前端导入弹窗与失败记录展示:
- 招聘信息维护导入提示
- 中介库管理导入中介实体关联关系提示
- 中介实体关系导入失败记录字段标签
## 验证情况
- 后端编译:`mvn -pl ccdi-info-collection -am compile -DskipTests` 通过。
- 前端构建:`ruoyi-ui` 下执行 `nvm use` 后,`npm run build:prod` 通过。
- 真实模板下载:
- `/ccdi/staffRecruitment/importTemplate` 下载模板成功,表头已变为当前字段顺序。
- `/ccdi/intermediary/importEnterpriseRelationTemplate` 下载模板成功,表头已变为“中介本人证件号码、统一社会信用代码、关联职务、备注”。
- 真实接口导入:
- 招聘信息基于下载模板造数,主 Sheet 与历史工作经历 Sheet 共 2 行导入成功,详情接口回查历史工作经历 1 条。
- 中介实体关联关系基于下载模板造数,导入成功 1 条,列表可回查关联职务。
- 清理情况:
- 已删除本轮成功导入的招聘记录及历史工作经历。
- 已删除本轮成功导入的中介实体关联关系。
- 已删除本轮由导入自动补入的实体库测试数据。
- 真实页面检查:
- 使用 `browser-use` 打开真实页面 `/maintain/staffRecruitment`,确认招聘信息导入弹窗显示新的双 Sheet 和招聘类型说明。
- 使用 `browser-use` 打开真实页面 `/maintain/intermediary`,确认“导入中介实体关联关系”按钮可打开导入弹窗,字段说明与新增关联机构口径一致。
- `browser-use` 当前不支持文件上传,页面文件选择动作无法在浏览器插件内完成;文件上传动作已通过同一真实导入接口完成验证。
- 测试完成后已停止本轮启动的后端与前端进程。

View File

@@ -0,0 +1,60 @@
# 员工资产导入与实体库自动补入后端实施记录
## 基本信息
- 实施日期2026-05-06
- 实施范围:后端
- 关联计划:`docs/plans/backend/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-implementation-plan.md`
## 修改内容
### 双 Sheet 导入任务编排
- 员工信息维护导入入口改为由服务层统一编排员工主 Sheet 与员工资产 Sheet。
- 员工亲属关系维护导入入口改为由服务层统一编排亲属关系主 Sheet 与亲属资产 Sheet。
- 当两个 Sheet 都有数据时,仍返回两个任务 ID并按主 Sheet 导入成功结果为资产 Sheet 提供同文件内的归属映射。
- 当只导入资产 Sheet 时,仅生成并返回资产导入任务 ID不生成员工或亲属关系主任务 ID。
- 当两个 Sheet 都为空时,保持返回“至少需要一条数据”。
### 实体库自动补入
- 新增统一的实体库自动补入服务,按统一社会信用代码去重,只插入实体库不存在的记录。
- 员工企业关系、信贷客户企业关系新增和导入时自动补入实体库。
- 中介新增、编辑和导入时取消“实体库必须已存在”的阻断校验;实体库缺失时按中介来源自动补入,不要求提供机构名称。
- 招投标供应商新增、编辑和导入时,对合法统一社会信用代码的供应商自动补入实体库。
- 新增企业来源枚举 `SUPPLIER`,用于标识供应商来源。
### 测试补充
- 同步调整员工信息维护、员工亲属关系维护导入 Controller 单测,按新的统一编排入口断言返回任务 ID。
- 补充员工资产导入单测,验证同一模板中本轮成功导入的员工身份证号可作为员工资产归属。
- 补充亲属资产导入单测,验证同一模板中本轮成功导入的亲属关系可作为亲属资产归属。
- 补充员工亲属实体关联新增单测,验证成功新增时调用实体库自动补入服务,来源为 `EMP_RELATION`、数据来源为 `MANUAL`
## 影响范围
- `/ccdi/baseStaff/importData`
- `/ccdi/staffFmyRelation/importData`
- 员工企业关系、信贷客户企业关系、中介、招投标供应商的新增/编辑/导入实体库联动逻辑
- 实体库基础信息表 `ccdi_enterprise_base_info`
## 验证记录
- 执行 `mvn -pl ccdi-info-collection -am -DskipTests compile`结果BUILD SUCCESS。
- 执行 `mvn -DskipTests compile`结果BUILD SUCCESS。
- 代码路径核对:员工主 Sheet 为空时仅调用员工资产导入服务并返回 `assetTaskId`;亲属关系主 Sheet 为空时仅调用亲属资产导入服务并返回 `assetTaskId`
- 复测执行 `mvn -pl ccdi-info-collection -am -Dtest=CcdiBaseStaffControllerTest,CcdiStaffFmyRelationControllerTest,CcdiBaseStaffAssetImportServiceImplTest,CcdiAssetInfoImportServiceImplTest,CcdiBaseStaffDualImportServiceTest,CcdiStaffFmyRelationImportServiceImplTest,CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`结果BUILD SUCCESSTests run: 39, Failures: 0, Errors: 0, Skipped: 0。
- 复测执行 `mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false test`结果BUILD FAILURE本次问题相关用例均已通过剩余失败为中介实体关联测试未注入自动补入服务以及 `CcdiPurchaseTransactionFeatureContractTest` 依赖的 `sql/ccdi_purchase_transaction.sql` 文件不存在。
- 使用 `bin/restart_java_backend.sh` 重启后端并通过 `/login/test` 探活结果HTTP 200。
- 通过真实接口下载当前导入模板,基于模板生成测试文件,执行 `/ccdi/baseStaff/importData`:员工任务 `8ea63988-deb2-4791-a24a-f15ca2c8cd6e` 与员工资产任务 `f281beca-bb58-4076-86db-6f9f948bbaf0` 均为 `SUCCESS`,成功 1 条、失败 0 条。
- 回查 `ccdi_base_staff``ccdi_asset_info`:员工主数据写入成功;员工资产 `family_id``person_id` 均为本轮员工身份证号,第二个 Sheet 未再出现“未找到资产归属员工”。
- 执行 `/ccdi/staffFmyRelation/importData`:亲属关系任务 `702466a9-0113-4e89-bcf1-8d760ee34543` 与亲属资产任务 `f11906d4-b9f3-4656-834c-fc9dc1a27704` 均为 `SUCCESS`,成功 1 条、失败 0 条。
- 回查 `ccdi_staff_fmy_relation``ccdi_asset_info`:亲属关系主数据写入成功;亲属资产 `family_id` 为员工身份证号、`person_id` 为亲属身份证号,第二个 Sheet 已正确关联到本轮亲属主数据。
- 执行 `/ccdi/staffEnterpriseRelation/importData`:员工亲属实体关联任务 `6361fc94-0d32-4da0-b1a0-7419b399710d``SUCCESS`,成功 1 条、失败 0 条。
- 回查 `ccdi_staff_enterprise_relation``ccdi_enterprise_base_info`:实体关联写入成功;实体库自动生成对应企业,`ent_source=EMP_RELATION``data_source=IMPORT`
- 验证结束后执行清理 SQL回查本轮员工、亲属、资产、亲属实体关联和实体库测试数据计数均为 0。
## 备注
- 本次为后端逻辑调整,未修改前端页面代码。
- 现有前端已按 `staffTaskId` / `relationTaskId` / `assetTaskId` 是否存在分别启动轮询,可直接兼容只返回资产任务 ID 的结果。

View File

@@ -0,0 +1,68 @@
# 2026-05-07 NAS Docker 部署实施记录
## 保存路径确认
- 目标目录:`docs/reports/implementation/`
- 文档用途:记录本次 NAS Docker 部署操作、影响范围与验证结果
- 路径检查结果:符合仓库实施记录归档规范
## 本次操作
- 在本地仓库 `/Users/wkc/Desktop/ccdi/ccdi` 执行 NAS Docker 部署。
- 按仓库前端规则先执行 `source ~/.nvm/nvm.sh && nvm use`,确认 Node 版本为 `v14.21.3`、npm 版本为 `6.14.18`
- 执行部署脚本:`deploy/deploy-to-nas.sh`
- 脚本自动完成:
- 后端打包:`mvn clean package -DskipTests`
- 前端打包:`npm run build:prod`
- 组装部署目录:`.deploy/ccdi-package`
- 上传到 NAS 并执行远端 `docker compose up -d --build`
- 部署目标:
- SSH`116.62.17.81:9444`
- 远端目录:`/volume1/webapp/ccdi`
## 影响范围
- 远端部署目录 `/volume1/webapp/ccdi` 已刷新为本次构建产物。
- 远端 Docker 服务已重建并启动:
- `ccdi-backend`
- `ccdi-frontend`
- `ccdi-lsfx-mock`
- 本次操作未修改业务代码。
## 验证结果
### 本地构建验证
- Maven 聚合打包成功,`ruoyi-admin/target/ruoyi-admin.jar` 已生成。
- Vue 生产构建成功,`ruoyi-ui/dist` 已生成。
- 前端构建存在资源体积告警,未出现构建失败。
### 远端容器验证
- `docker compose ps` 结果:
- `ccdi-backend``Up About a minute`
- `ccdi-frontend``Up About a minute`
- `ccdi-lsfx-mock``Up About a minute`
- 端口映射结果:
- `62318 -> backend:8080`
- `62319 -> frontend:80`
- `62320 -> mock:8000`
### NAS 本机访问验证
- `http://127.0.0.1:62319/` 返回 `200`
- `http://127.0.0.1:62318/swagger-ui/index.html` 返回 `200`
- `http://127.0.0.1:62320/docs` 返回 `200`
### 公网访问验证
- `http://116.62.17.81:62319/` 返回 `200`
- `http://116.62.17.81:62318/swagger-ui/index.html` 返回 `200`
- `http://116.62.17.81:62320/docs` 返回 `200`
### 后端日志验证
- 后端启动 profile`nas`
- TongWeb `8080` 已启动
- `RuoYiApplication` 启动完成
- 日志输出“若依启动成功”

View File

@@ -0,0 +1,31 @@
# 生产 MySQL 初始化文件同步实施记录
## 保存路径确认
- 实施记录保存路径:`docs/reports/implementation/2026-05-07-production-mysql-init-sync.md`
- 本次为生产 MySQL 初始化 SQL 同步,使用实施记录目录保存。
## 修改内容
1. 更新 `sql/ccdi_prod_init_20260428.sql`,补入招聘信息维护所需的 `ccdi_recruit_type` 字典类型。
2. 补入 `ccdi_recruit_type` 字典数据:
- `SOCIAL`:社招
- `CAMPUS`:校招
3. 核对招聘信息相关结构,确认生产初始化 SQL 中已包含:
- `ccdi_staff_recruitment.id` 自增主键
- `ccdi_staff_recruitment.recruit_id` 普通索引
- `ccdi_staff_recruitment_work.recruitment_id` 非空字段
- `idx_recruitment_id``idx_recruitment_id_sort_order` 索引
## 影响范围
- 影响生产空库初始化脚本。
- 新生产环境导入初始化 SQL 后,招聘信息导入模板可直接读取招聘类型字典下拉数据。
- 不影响现有数据库的增量迁移脚本;存量库仍按 `sql/migration/2026-05-07-add-staff-recruitment-type-dict.sql` 执行。
## 验证情况
- 已检查生产初始化 SQL 中包含 `ccdi_recruit_type` 字典类型与 `SOCIAL``CAMPUS` 两条字典数据。
- 已检查招聘主从表结构已同步最新主键与关联字段形态。
- 已执行 `git diff --check`,未发现空白或补丁格式问题。
- 本次未执行数据库导入验证,避免误将生产初始化脚本导入当前开发库造成清库风险。

View File

@@ -0,0 +1,51 @@
# 项目总览 PDF 中文字体配置化修复实施记录
## 保存路径确认
- 文档类型:实施记录
- 保存路径:`docs/reports/implementation/`
## 问题说明
- 异常信息:`com.ruoyi.common.exception.ServiceException: 未找到可用中文字体无法导出PDF报告`
- 触发链路:结果总览导出 PDF 时,`CcdiProjectOverviewReportPdfExporter` 需要通过 PDFBox 加载中文字体;原实现依赖代码内置候选路径,部署环境字体路径不可控时会导致导出失败。
## 修改内容
- 修改 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewReportPdfExporter.java`
- 删除 Java 代码内置多路径候选列表。
- 通过 `ccdi.report.pdf-font-path` 读取 profile 配置中的单个字体路径。
- 配置为空时提示 `未配置PDF中文字体路径无法导出PDF报告`
- 配置文件不存在、格式不支持或字体不可导出时,错误信息包含配置路径。
- `.ttc` 字体集合优先选择支持 TrueType 子集化的简体中文字体名,未命中时再使用集合内首个支持子集化的字体。
- 字体集合源在 `document.save()` 完成后再关闭,避免 PDFBox 保存阶段继续读取字体时源文件已关闭。
- 增加 `.otf` 字体文件加载支持。
- 修改 `ruoyi-admin/src/main/resources/application-dev.yml`
- 增加本地 macOS 字体路径:`/System/Library/Fonts/STHeiti Medium.ttc`
- 修改 `ruoyi-admin/src/main/resources/application-nas.yml`
- 修改 `ruoyi-admin/src/main/resources/application-uat.yml`
- 修改 `ruoyi-admin/src/main/resources/application-pro.yml`
- 增加 Linux 字体路径:`/usr/share/fonts/truetype/wqy/wqy-microhei.ttc`
- 未修改 `application.yml`,不提供全局默认值。
- 修改 `docker/backend/Dockerfile`
- 后端镜像安装 `fontconfig``fonts-wqy-microhei`,确保 NAS Docker 部署环境具备可用中文字体。
- 修改 `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewReportPdfExporterTest.java`
- 正常导出用例显式传入测试环境字体路径。
- 新增空配置与不存在路径的异常覆盖。
## 影响范围
- 影响结果总览 PDF 导出功能。
- 不改变报告数据组装、表格内容、前端下载接口和文件名规则。
- 不在 `application.yml` 增加默认配置,缺少 profile 配置时导出时报错。
- Docker 镜像构建会新增中文字体包安装步骤。
## 验证记录
- 已执行:`mvn -pl ccdi-project -Dtest=CcdiProjectOverviewReportPdfExporterTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果:失败,失败原因是当前工作区已有其他源码编译错误,未进入 PDF 导出测试。
- 主要错误包括:`getDataTable` 调用参数不匹配、流水接口字段和方法签名不匹配。
- 已执行:手工编译 PDF 导出器及其依赖 VO 到 `ccdi-project/target/classes` 后,运行 `mvn -pl ccdi-project -Dtest=CcdiProjectOverviewReportPdfExporterTest -Dsurefire.failIfNoSpecifiedTests=false -Dmaven.main.skip=true test`
- 结果:失败,失败原因是 Maven 仍尝试编译 `ccdi-project` 下全量测试类,而当前 `target/classes` 缺少其他测试依赖的业务类。
- 已执行:手工编译 `CcdiProjectOverviewReportPdfExporterTest` 后,通过 JUnit Platform Launcher 指定运行该测试类
- 结果:通过,`Tests found: 3``Tests succeeded: 3``Tests failed: 0`

View File

@@ -0,0 +1,36 @@
# 项目详情专项排查图谱嵌入前端实施记录
## 修改内容
- 新增 `GraphAtlasSection.vue`,实现共用证件号输入、资金图谱/关系图谱页签、证件号标准化、md5 加密和 iframe 链接拼接。
- 调整 `SpecialCheck.vue`,将原“图谱外链展示”占位卡片替换为图谱展示组件。
- 调整专项排查空数据展示结构,资产负债核查无数据时仍展示图谱模块,保证图谱入口属于专项排查页面本身。
## 影响范围
- 仅影响项目详情页“专项排查”中的图谱展示区域。
- 不涉及后端接口、数据库、权限、菜单和项目详情顶部一级导航。
- 不保存证件号或 md5 结果,不新增身份证格式校验,不为外部图谱服务增加兜底或降级逻辑。
## 验证情况
- `cd ruoyi-ui && nvm use && npm run build:prod`:构建通过;仅出现既有包体积 warning。
- 真实项目详情页专项排查浏览器验证:
- 验证地址:`http://localhost:8081/ccdiProject/detail/90338?tab=special`
- 验证项目:`验收测试-20260423-项目P0-01`
- 资产负债核查有数据时,图谱模块展示在“员工家庭资产负债专项核查”和“拓展查询”之间。
- 未输入证件号时点击“查询图谱”:提示“请输入证件号”,未生成 iframe。
- 未输入证件号时在输入框回车:提示“请输入证件号”,未生成 iframe。
- 输入 `110101199003074211` 后点击“查询图谱”,资金图谱 iframe `src` 包含 `id=ccdi_lanxi_trans``atlasToken=F4BBA291A285858BAF4526C6EC312388``"vId":"idno_node/9b1b5eba4a26c9a68ff1ca06f40bee1b"`
- 输入 ` 33078219900101123x ` 后回车,资金图谱 iframe `src` 包含 `"vId":"idno_node/233c8519f86a57b1f00ec88a32152ce3"`,确认已执行 `trim + x 转 X + md5`
- 切换到“关系图谱”后iframe `src` 包含 `id=lanxitest``atlasToken=2C914E5E1FBFBC4AD15163E0AB03B800``"vId":"rel_node/233c8519f86a57b1f00ec88a32152ce3"`
- 真实项目详情页专项排查空数据浏览器验证:
- 验证地址:`http://localhost:8081/ccdiProject/detail/90336?tab=special`
- 验证项目:`456`
- 页面展示“暂无员工家庭资产负债核查数据”后,仍展示“图谱展示”模块和证件号输入框,并位于“拓展查询”上方。
- 当前外部图谱内容打不开属于预期,本次验收只检查 iframe 链接正确。
- 测试结束后已关闭本轮启动的前端 dev server`8081` 端口无监听进程。
## 遗留事项
- 无。

View File

@@ -0,0 +1,23 @@
# 员工亲属实体关联列表展示统信码实施记录
## 保存路径确认
- 本次为页面展示调整,实施记录按项目规则保存到 `docs/reports/implementation/`
## 修改内容
-`ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue` 的员工亲属实体关联列表中新增“统一社会信用代码”列。
- 列数据直接使用列表接口已返回的 `socialCreditCode` 字段,不新增后端字段、不调整接口契约、不变更数据库结构。
## 影响范围
- 仅影响员工亲属实体关联页面列表展示。
- 新增、编辑、详情、导入、失败记录、查询条件等原有逻辑不变。
## 验证情况
- 执行 `source ~/.nvm/nvm.sh && nvm use && node -v && npm -v && npm run build:prod`Node 版本为 `v14.21.3`,构建成功;仅存在既有包体积告警。
- 临时启动前端 `npm_config_port=8080 npm run dev`,通过 `browser-use` 打开真实页面 `http://localhost:8080/maintain/staffEnterpriseRelation`
- 页面表头已展示“统一社会信用代码”列,列表数据行可见对应统信码,例如 `913312341234123411``91330100RT20260601`
- 浏览器控制台未发现错误日志。
- 页面验证结束后已关闭本次临时启动的前端进程。

View File

@@ -0,0 +1,44 @@
# 招聘信息新增弹窗工作经历维护实施记录
## 背景
招聘信息维护页面的编辑弹窗已支持维护社招候选人的历史工作经历,但新增弹窗隐藏了同一维护区域,且新增提交时未携带 `workExperienceList`,导致新增招聘记录时无法同步维护候选人工作经历。
## 修改内容
- 前端 `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
- 将候选人历史工作经历维护区域从“仅编辑弹窗展示”调整为“社招新增和编辑弹窗均展示”。
- 新增提交时保留 `workExperienceList`,不再删除工作经历数据。
- 工作经历校验同步覆盖新增和编辑场景,仍仅对社招记录生效。
- 后端 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java`
- 新增 `workExperienceList` 入参,并使用 `@Valid` 复用现有工作经历字段校验。
- 后端 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java`
- 新增招聘信息保存后,如为社招且提交了工作经历,按新增记录主键写入 `ccdi_staff_recruitment_work`
- 复用编辑场景的工作经历实体构造逻辑,保持字段、排序号和必填过滤规则一致。
## 影响范围
- 仅影响招聘信息维护页面的新增弹窗和新增接口。
- 编辑弹窗、详情展示、导入功能和校招新增逻辑不改变。
## 验证记录
- 后端相关模块编译通过:
- `mvn -pl ccdi-info-collection -am test -DskipTests`
- 前端生产构建通过:
- `cd ruoyi-ui && source ~/.nvm/nvm.sh && nvm use && node -v && npm run build:prod`
- 使用 Node `v14.21.3`,构建仅保留既有资源体积 warning。
- 真实接口新增与编辑验证通过:
- 新增社招招聘记录 `AUTO-WORK-202605070053`,新增详情回查 `workExperienceList` 为 1 条。
- 编辑同一条招聘记录,工作经历由 1 条覆盖为 2 条,详情回查包含 `编辑后公司B``编辑新增公司C`
- 主键关联数据库回查通过:
- 使用 `bin/mysql_utf8_exec.sh output/sql/2026-05-07-staff-recruitment-work-pk-check.sql` 回查,主表自增主键 `id=6006` 下关联 2 条子表记录。
- 回查 `work_links``6006:1:编辑后公司B | 6006:2:编辑新增公司C``orphan_work_count=0`
- 真实页面验证通过:
- 使用 `browser-use` 打开真实页面 `http://localhost:1025/maintain/staffRecruitment`
- 新增弹窗已展示“候选人历史工作经历”区域,点击“新增经历”后出现工作单位、入职时间等输入列。
- 编辑同一条测试记录时,编辑弹窗按主键回显 2 条历史工作经历,列表页显示历史工作经历为 `2段`
- 清理验证通过:
- 测试结束后调用删除接口清理 `id=6006`,再次数据库回查 `orphan_work_count=0`,无测试工作经历孤儿数据残留。

View File

@@ -0,0 +1,43 @@
# 招聘项目编号允许重复实施记录
## 保存路径确认
- 实施记录保存路径:`docs/reports/implementation/2026-05-07-staff-recruitment-allow-duplicate-recruit-id.md`
- 本次为招聘信息主从表关联修正,使用实施记录目录保存。
## 修改内容
1. 移除新增、编辑、导入时对 `recruit_id` 的重复拦截,招聘项目编号允许重复。
2. `ccdi_staff_recruitment_work` 新增 `recruitment_id`,历史工作经历改为关联招聘主表自增 `id`
3. 列表历史工作经历条数、详情历史工作经历、编辑保存和删除清理均改为按 `recruitment_id` 处理。
4. 导入模板字段保持不变,历史工作经历导入时通过“招聘项目编号 + 候选人姓名 + 招聘项目名称 + 职位名称”匹配主记录;匹配多条时返回失败,避免错误归属。
5. 更新初始化 SQL、增量 SQL、数据库字段说明和 API 文档。
## 影响范围
- 后端服务:招聘信息 CRUD、双 Sheet 导入、历史工作经历查询与清理。
- 数据库表:`ccdi_staff_recruitment``ccdi_staff_recruitment_work`
- 前端页面:接口仍传主表 `id`,页面字段无新增。
- 导入功能:模板不新增主键列。
## 验证情况
1. 数据库迁移已执行:
- `sh bin/mysql_utf8_exec.sh sql/migration/2026-05-07-allow-duplicate-staff-recruitment-id.sql`
- 回查确认 `ccdi_staff_recruitment.recruit_id` 为普通索引,`ccdi_staff_recruitment_work.recruitment_id` 为非空字段并已建立索引。
2. 后端定向测试通过:
- `mvn -pl ccdi-info-collection -am -Dtest=CcdiStaffRecruitmentImportServiceImplTest,CcdiStaffRecruitmentDualImportContractTest -Dsurefire.failIfNoSpecifiedTests=false test`
3. 前端构建通过:
- `cd ruoyi-ui && nvm use && npm run build:prod`
- 构建仅保留既有资源体积 warning。
4. 真实页面验证:
- 已按项目规则优先尝试 `browser-use`,但当前 Codex 环境没有可用 in-app browser pane运行时返回 `No active Codex browser pane available`
- 使用本机 Playwright 兜底打开真实页面 `http://localhost:8080/maintain/staffRecruitment` 验证。
- 通过接口新增两条相同招聘项目编号 `RC-DUP-20260507003554` 的招聘记录,均返回操作成功。
- 给第一条记录插入 1 条历史工作经历后,页面列表展示两条同编号记录:`TestA``1段``TestB``0段`
- 打开 `TestA` 详情可见 `Company A` 历史工作经历;打开 `TestB` 详情显示“暂无历史工作经历”,未发生串数据。
- 后端日志确认列表按 `recruitment_id` 聚合历史工作经历,详情查询分别按 `recruitment_id = 6003``recruitment_id = 6004` 查询从表。
5. 清理情况:
- 已删除本轮测试主表和从表造数。
- 已关闭 Playwright 浏览器。
- 已停止本轮启动的前端 `8080` 与后端 `62318` 进程,端口回查无监听。

View File

@@ -0,0 +1,47 @@
# 招聘信息自增主键实施记录
## 保存路径确认
- 实施记录保存路径:`docs/reports/implementation/2026-05-07-staff-recruitment-auto-id-primary-key.md`
- 本次为招聘信息主键结构调整,使用实施记录目录保存。
## 修改内容
1. `ccdi_staff_recruitment` 新增 `id` 自增主键,`recruit_id` 调整为普通业务编号。
2. 后端实体 `CcdiStaffRecruitment` 的 MyBatis Plus 主键从 `recruitId` 切换为 `id`
3. 招聘信息详情、编辑、删除接口改为按 `id` 定位记录。
4. 招聘信息列表和详情 SQL 返回 `id` 字段,前端列表行操作改为传递 `id`
5. 导入链路继续在模板中填写 `recruit_id`,后续补充改为由后端解析到招聘主表 `id`
6. 补充迁移脚本 `sql/migration/2026-05-07-add-staff-recruitment-auto-id.sql`,同步更新初始化 SQL 与数据库字段说明。
7. 同步更新招聘信息 API 文档中详情、编辑、删除的主键参数说明。
## 影响范围
- 后端接口:`/ccdi/staffRecruitment/{id}``PUT /ccdi/staffRecruitment``DELETE /ccdi/staffRecruitment/{ids}`
- 前端页面:`ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
- 数据库表:`ccdi_staff_recruitment`
- 导入功能:招聘主信息与历史工作经历模板字段不变,后续补充改为落库时关联招聘主表 `id`
## 数据库执行
- 已通过 `bin/mysql_utf8_exec.sh sql/migration/2026-05-07-add-staff-recruitment-auto-id.sql` 执行本地 dev 库迁移。
- 回查结果:
- `id``PRIMARY``auto_increment`
- `recruit_id`:普通索引,允许重复。
## 验证情况
1. 后端定向测试通过:
- `mvn -pl ccdi-info-collection -am -Dtest=CcdiStaffRecruitmentImportServiceImplTest,CcdiStaffRecruitmentDualImportContractTest -Dsurefire.failIfNoSpecifiedTests=false test`
2. 前端生产构建通过:
- `cd ruoyi-ui && nvm use && npm run build:prod`
- 构建仅保留既有资源体积 warning。
3. 后端重启验证通过:
- `sh bin/restart_java_backend.sh restart`
- 应用启动成功。
4. 真实页面验证通过:
- 使用 browser-use 打开 `http://localhost:8080/maintain/staffRecruitment`
- 登录后招聘信息维护列表正常展示。
- 点击 `RC2025001001` 详情,后端日志显示详情 SQL 使用 `WHERE id = ?` 查询主表。
- 历史工作经历后续补充改为使用招聘主表 `id` 查询,避免编号重复时串数据。
5. 测试后已关闭本轮启动的前端 `8080` 与后端 `62318` 进程,端口回查无监听。

View File

@@ -0,0 +1,34 @@
# 招聘信息毕业年月日期选择框实施记录
## 文档信息
- 保存路径:`docs/reports/implementation/2026-05-07-staff-recruitment-grad-month-picker.md`
- 实施日期2026-05-07
- 关联范围:招聘信息维护前端页面
## 本次修改内容
1. 定位招聘信息新增/编辑共用弹窗中的“毕业年月”字段。
2. 将“毕业年月”由普通输入框调整为 `el-date-picker` 月份选择组件。
3. 保持字段提交格式为 `YYYYMM`,继续匹配后端 `candGrad` 字段与现有格式校验规则。
4. 将该字段表单校验触发方式调整为 `change`,确保选择月份后立即触发组件对应校验。
## 影响范围
- 前端:`ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
- 后端接口、数据库字段、导入逻辑均未修改。
## 验证情况
1. 前端构建校验:
- 执行命令:`source ~/.nvm/nvm.sh && nvm use && npm run build:prod`
- 结果:构建成功;仅存在项目既有的 bundle size warnings。
2. 真实业务页面验证:
- 执行方式:通过 `browser-use` 打开 `http://127.0.0.1:1026/maintain/staffRecruitment`,进入“信息维护 / 招聘信息维护”真实业务路由,点击“新增”打开“添加招聘信息”弹窗。
- 结果:弹窗中“毕业年月”字段渲染为 `el-date-editor el-date-editor--month`,输入框占位文本为“请选择毕业年月”,符合日期选择框组件要求。
3. 字段格式校验:
- 执行方式:使用 Element UI 同源日期格式化工具验证 `yyyyMM` 格式解析与输出。
- 结果:`202110` 可解析为有效日期,`2024-06` 对应输出为 `202406`,与后端现有字段格式保持一致。
4. 测试进程清理:
- 已关闭本次临时启动的前端 dev server 与登录 cookie 辅助服务。
- 后端 `62318` 端口为测试前已存在进程,本次未启动,未做关闭处理。

View File

@@ -0,0 +1,35 @@
# 招聘信息维护导入模板招聘类型下拉框实施记录
## 背景
招聘信息维护导入模板中,“招聘类型”列原本为普通文本输入,用户需要手工填写招聘类型。现要求该列改为下拉框,仅选择“社招”或“校招”。
## 修改内容
-`CcdiStaffRecruitmentExcel``recruitType` 字段上增加 `@DictDropdown(dictType = "ccdi_recruit_type")`,让双 Sheet 导入模板生成时自动为“招聘类型”列添加 Excel 下拉框。
- 新增 `sql/migration/2026-05-07-add-staff-recruitment-type-dict.sql`,初始化 `ccdi_recruit_type` 字典:
- `社招` -> `SOCIAL`
- `校招` -> `CAMPUS`
- 更新招聘导入模板单测,断言“录用情况”和“招聘类型”列都存在下拉校验。
## 影响范围
- 后端模板生成:`/ccdi/staffRecruitment/importTemplate`
- 字典数据:`sys_dict_type``sys_dict_data` 中的 `ccdi_recruit_type`
- 导入解析与入库逻辑未改动;当前导入逻辑已支持把“社招/校招”归一为 `SOCIAL/CAMPUS`
## 验证情况
- 已执行 `sh bin/mysql_utf8_exec.sh sql/migration/2026-05-07-add-staff-recruitment-type-dict.sql`,字典 SQL 执行成功。
- 数据库回查确认 `ccdi_recruit_type` 字典存在,字典项为 `社招/SOCIAL``校招/CAMPUS`
- 已执行 `mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest -Dsurefire.failIfNoSpecifiedTests=false test`7 个模板相关测试全部通过。
- 单测日志确认招聘主 Sheet 第 5 列 `ccdi_admit_status`、第 7 列 `ccdi_recruit_type` 均成功添加下拉框。
- 已执行 `sh bin/restart_java_backend.sh restart` 重新构建并重启后端;随后通过后台 `screen` 启动脚本保持后端运行。
- 已通过真实接口 `/ccdi/staffRecruitment/importTemplate` 下载模板,工作簿包含“招聘信息”和“历史工作经历”两个 Sheet。
- 已检查真实模板 `xl/worksheets/sheet1.xml`,数据校验包含:
- `F2:F1000``录用,未录用,放弃`
- `H2:H1000``社招,校招`
## 注意事项
- 当前后端已完成重启,模板下载接口可以读取到新增字典项。

View File

@@ -0,0 +1,81 @@
# 生产统一脚本实施记录
## 保存路径确认
- 本次为生产部署与启停脚本改动,实施记录保存到 `docs/reports/implementation/`,符合仓库实施文档目录规范。
- 生产脚本保存到 `deploy/ccdi_function.sh`,与既有生产部署脚本位于同一目录,便于上线包或生产目录统一维护。
## 修改目标
- 新增一个生产统一入口脚本 `ccdi_function.sh`
- 脚本需要支持 `deploy``restart``stop`
- `deploy` 集成生产备份、部署、重启流程,参考 `deploy/deploy-release-prod.sh`
- `restart``stop` 集成生产后端进程控制流程,参考 `deploy/start-java-backend-prod.sh`
## 修改内容
- 新增 `deploy/ccdi_function.sh`
- 使用 `/bin/sh` 写法,避免服务器执行 `sh ccdi_function.sh` 时出现 Bash 专属语法兼容问题。
- 保留脚本顶部生产配置区,可直接配置 `BACKEND_JAVA_HOME``APP_HOME``JAR_NAME``SPRING_PROFILES_ACTIVE``JAVA_OPTS``APP_ARGS``STOP_WAIT_SECONDS`
- `deploy` 支持显式传入上线包路径:`./ccdi_function.sh deploy /path/to/ccdi_YYYYMMDD.zip`
- `deploy` 未传上线包路径时,自动使用脚本同级目录下唯一的 `.zip` 文件,并排除 `dist.zip`
- 上线包根层校验 `ruoyi-admin.jar``dist.zip`
- `dist.zip` 解压后校验 `dist/index.html`
- 部署前备份当前 `backend/``frontend/``backups/YYYYMMDDHHMMSS/`
- 后端部署为原子替换 `backend/ruoyi-admin.jar`
- 前端部署为清空 `frontend/` 后复制 `dist/` 内文件。
- `deploy` 完成文件部署后直接调用脚本内 `restart_action`,不再依赖外部 `start-java-backend-prod.sh`
- `restart` 流程为 `stop_backend``start_backend``follow_logs`
- `stop` 使用 `ps -ef` 扫描当前生产目录对应的 `backend/ruoyi-admin.jar` 进程,忽略 `<defunct>` 行,并清理 PID 文件。
- `restart``deploy` 启动成功后持续输出 `backend/logs/backend-console.log`,按 `Ctrl+C` 仅退出日志查看,不停止后端进程。
## 使用方式
生产目录示例:
```text
上线目录/
├── ccdi_function.sh
├── backend/
├── frontend/
└── ccdi_YYYYMMDD.zip
```
部署:
```bash
./ccdi_function.sh deploy
./ccdi_function.sh deploy /path/to/ccdi_YYYYMMDD.zip
```
重启:
```bash
./ccdi_function.sh restart
```
停止:
```bash
./ccdi_function.sh stop
```
## 验证记录
- 执行 `sh -n deploy/ccdi_function.sh`
- 结果:通过
- 说明:脚本 Shell 语法正确。
- 执行 `sh deploy/ccdi_function.sh --help`
- 结果:通过
- 说明:帮助信息正常输出,包含 `deploy``restart``stop` 三个入口。
- 使用 `/tmp` 构造最小生产目录、旧 `backend/`、旧 `frontend/`、上线压缩包、假 Java 和假日志输出命令后执行 `deploy`
- 结果:通过
- 说明:已验证旧文件备份、新 Jar 覆盖、前端 `dist/` 文件部署、部署后重启流程。
- 在同一 `/tmp` 验证环境执行 `stop`
- 结果:通过
- 说明:已验证 `stop` 可停止由统一脚本启动的后端进程。
## 影响范围
- 仅新增生产统一脚本与本实施记录。
- 不修改 Java 业务代码、前端业务代码、数据库脚本和既有生产脚本。

View File

@@ -0,0 +1,70 @@
# 2026-05-08 NAS Docker 部署实施记录
## 保存路径确认
- 目标目录:`docs/reports/implementation/`
- 文档用途:记录本次 NAS Docker 部署操作、影响范围与验证结果
- 路径检查结果:符合仓库实施记录归档规范
## 本次操作
- 在本地仓库 `/Users/wkc/Desktop/ccdi/ccdi` 执行 NAS Docker 部署。
- 按前端规则执行 `source ~/.nvm/nvm.sh && nvm use`,确认 Node 版本为 `v14.21.3`、npm 版本为 `6.14.18`
- 执行部署脚本:`deploy/deploy-to-nas.sh`
- 脚本完成以下动作:
- 后端打包:`mvn clean package -DskipTests`
- 前端打包:`npm run build:prod`
- 组装部署目录:`.deploy/ccdi-package`
- 上传到 NAS`/volume1/webapp/ccdi`
- 远端执行 `docker compose up -d --build`
- 部署目标:
- SSH`116.62.17.81:9444`
- 远端目录:`/volume1/webapp/ccdi`
## 影响范围
- 远端部署目录 `/volume1/webapp/ccdi` 已刷新为本次构建产物。
- 远端 Docker 镜像已重新构建。
- 远端 Docker 服务已重建并启动:
- `ccdi-backend`
- `ccdi-frontend`
- `ccdi-lsfx-mock`
## 验证结果
### 本地构建验证
- Maven 聚合打包成功,`ruoyi-admin/target/ruoyi-admin.jar` 已生成。
- Vue 生产构建成功,`ruoyi-ui/dist` 已生成。
- 前端构建存在资源体积告警,未出现构建失败。
### 远端容器验证
- `docker compose ps` 结果:
- `ccdi-backend``Up`
- `ccdi-frontend``Up`
- `ccdi-lsfx-mock``Up`
- 端口映射结果:
- `62318 -> backend:8080`
- `62319 -> frontend:80`
- `62320 -> mock:8000`
### NAS 本机访问验证
- `http://127.0.0.1:62319/` 返回 `200`
- `http://127.0.0.1:62318/swagger-ui/index.html` 返回 `200`
- `http://127.0.0.1:62320/docs` 返回 `200`
### 公网访问验证
- `http://116.62.17.81:62319/` 返回 `200`
- `http://116.62.17.81:62318/swagger-ui/index.html` 返回 `200`
- `http://116.62.17.81:62320/docs` 返回 `200`
- `POST http://116.62.17.81:62318/login/test?username=admin&password=admin123` 返回 `200`
### 后端日志验证
- 后端启动 profile`nas`
- TongWeb `8080` 已启动。
- `RuoYiApplication` 启动完成。
- 日志输出“若依启动成功”。

View File

@@ -3,6 +3,10 @@ ruoyi:
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: D:/ruoyi/uploadPath
ccdi:
report:
pdf-font-path: /System/Library/Fonts/STHeiti Medium.ttc
server:
# 服务器的HTTP端口默认为8080

View File

@@ -3,6 +3,10 @@ ruoyi:
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: backend/uploadPath
ccdi:
report:
pdf-font-path: /usr/share/fonts/truetype/wqy/wqy-microhei.ttc
# 开发环境配置
server:

View File

@@ -3,6 +3,10 @@ ruoyi:
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: backend/uploadPath
ccdi:
report:
pdf-font-path: /usr/share/fonts/truetype/wqy/wqy-microhei.ttc
# 开发环境配置
server:
@@ -31,9 +35,9 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:mysql://158.234.199.250:3306/ccdi?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: dbicm
password: Kfcx@1234
url: jdbc:mysql://64.116.19.156:3306/ccdi?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: lx_ai
password: lx-ai@9520
# 从库数据源
slave:
# 从数据源开关/默认关闭
@@ -88,13 +92,13 @@ spring:
# redis 配置
redis:
# 地址
host: r-kz640f6b20dac724.redis.rds.ops.dc-tst-zj96596.com
host: 64.116.19.155
# 端口默认为6379
port: 6379
# 数据库索引
database: 9
database: 0
# 密码
password: Kfcx@1234
password: lx-ai@9520
# 连接超时时间
timeout: 10s
lettuce:
@@ -111,15 +115,13 @@ spring:
# 流水分析平台配置
lsfx:
api:
base-url: http://158.234.196.5:82/c4c3
# 生产环境
# base-url: http://64.202.32.176/c4c3
base-url: http://64.202.32.176/c4c3
# 认证配置
app-id: remote_app
app-secret: dXj6eHRmPv # 见知提供的密钥
client-id: c2017e8d105c435a96f86373635b6a09 # 测试环境固定值
client-id: 8f28807eaa2940018a7eec6e60900365 # 测试环境固定值
# 接口路径配置
endpoints:
@@ -128,7 +130,6 @@ lsfx:
fetch-inner-flow: /watson/api/project/getJZFileOrZjrcuFile
check-parse-status: /watson/api/project/upload/getpendings
get-bank-statement: /watson/api/project/getBSByLogId
# 新增接口
get-file-upload-status: /watson/api/project/bs/upload
delete-files: /watson/api/project/batchDeleteUploadFile
@@ -143,4 +144,4 @@ lsfx:
credit-parse:
api:
url: http://192.168.0.111:62320/xfeature-mngs/conversation/htmlEval
url: http://64.202.94.120:8081/xfeature-mngs/conversation/htmlEval

Some files were not shown because too many files have changed in this diff Show More