From 1b5d1178f6af93da43e660c17241c422a8033967 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Fri, 13 Feb 2026 10:15:23 +0800 Subject: [PATCH] =?UTF-8?q?feat=E4=BF=A1=E8=B4=B7=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E5=AE=9E=E4=BD=93=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ccdi_cust_enterprise_relation.sql | 23 + doc/信贷客户实体关联维护功能/代码校验报告.md | 243 +++ doc/信贷客户实体关联维护功能/前端实施方案.md | 1088 ++++++++++++ doc/信贷客户实体关联维护功能/后端实施方案.md | 1511 +++++++++++++++++ .../CcdiCustEnterpriseRelationController.java | 200 +++ .../domain/CcdiCustEnterpriseRelation.java | 93 + .../dto/CcdiCustEnterpriseRelationAddDTO.java | 55 + .../CcdiCustEnterpriseRelationEditDTO.java | 56 + .../CcdiCustEnterpriseRelationQueryDTO.java | 37 + .../CcdiCustEnterpriseRelationExcel.java | 57 + .../vo/CcdiCustEnterpriseRelationVO.java | 89 + ...CustEnterpriseRelationImportFailureVO.java | 37 + .../CcdiCustEnterpriseRelationMapper.java | 67 + ...diCustEnterpriseRelationImportService.java | 41 + .../ICcdiCustEnterpriseRelationService.java | 84 + ...stEnterpriseRelationImportServiceImpl.java | 306 ++++ ...CcdiCustEnterpriseRelationServiceImpl.java | 228 +++ .../ccdi/CcdiCustEnterpriseRelationMapper.xml | 98 ++ 18 files changed, 4313 insertions(+) create mode 100644 doc/信贷客户实体关联维护功能/ccdi_cust_enterprise_relation.sql create mode 100644 doc/信贷客户实体关联维护功能/代码校验报告.md create mode 100644 doc/信贷客户实体关联维护功能/前端实施方案.md create mode 100644 doc/信贷客户实体关联维护功能/后端实施方案.md create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiCustEnterpriseRelationController.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiCustEnterpriseRelation.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationAddDTO.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationEditDTO.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationQueryDTO.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiCustEnterpriseRelationExcel.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiCustEnterpriseRelationVO.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CustEnterpriseRelationImportFailureVO.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiCustEnterpriseRelationMapper.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustEnterpriseRelationImportService.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustEnterpriseRelationService.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustEnterpriseRelationImportServiceImpl.java create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustEnterpriseRelationServiceImpl.java create mode 100644 ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiCustEnterpriseRelationMapper.xml diff --git a/doc/信贷客户实体关联维护功能/ccdi_cust_enterprise_relation.sql b/doc/信贷客户实体关联维护功能/ccdi_cust_enterprise_relation.sql new file mode 100644 index 0000000..576c91f --- /dev/null +++ b/doc/信贷客户实体关联维护功能/ccdi_cust_enterprise_relation.sql @@ -0,0 +1,23 @@ +-- 信贷客户实体关联关系表 +CREATE TABLE IF NOT EXISTS `ccdi_cust_enterprise_relation` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键,唯一标识', + `person_id` VARCHAR(18) NOT NULL COMMENT '身份证号', + `relation_person_post` VARCHAR(100) DEFAULT NULL COMMENT '关联人在企业的职务:股东、法人、高管、实际控制人等', + `social_credit_code` VARCHAR(18) NOT NULL COMMENT '统一社会信用代码,关联企业主体信息表的外键', + `enterprise_name` VARCHAR(200) DEFAULT NULL COMMENT '企业名称(冗余存储,便于快速查询)', + `status` INT NOT NULL DEFAULT 1 COMMENT '关系是否有效:0 - 无效、1 - 有效(默认有效)', + `remark` TEXT COMMENT '补充说明', + `data_source` VARCHAR(50) DEFAULT NULL COMMENT '数据来源', + `is_employee` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是员工:0-否 1-是', + `is_emp_family` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是员工家庭关联人:0-否 1-是', + `is_customer` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是信贷客户:0-否 1-是', + `is_cust_family` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是信贷客户关联人:0-否 1-是', + `created_by` VARCHAR(64) NOT NULL COMMENT '记录创建人', + `updated_by` VARCHAR(64) DEFAULT NULL COMMENT '记录更新人', + `create_time` DATETIME NOT NULL COMMENT '记录创建时间', + `update_time` DATETIME NOT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`), + KEY `idx_person_id` (`person_id`), + KEY `idx_social_credit_code` (`social_credit_code`), + UNIQUE KEY `uk_person_enterprise` (`person_id`, `social_credit_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='信贷客户实体关联关系表'; diff --git a/doc/信贷客户实体关联维护功能/代码校验报告.md b/doc/信贷客户实体关联维护功能/代码校验报告.md new file mode 100644 index 0000000..20ac6c4 --- /dev/null +++ b/doc/信贷客户实体关联维护功能/代码校验报告.md @@ -0,0 +1,243 @@ +# 信贷客户实体关联维护功能 - 代码校验报告 + +## 一、校验概述 + +本文档对信贷客户实体关联维护功能与员工实体关系维护功能进行逻辑一致性校验,确保前端交互方式和后端实现逻辑保持一致。 + +--- + +## 二、后端逻辑校验 + +### 2.1 Controller层对比 + +| 接口功能 | 员工实体关系 | 信贷客户实体关联 | 一致性 | +|----------|-------------|-----------------|--------| +| 分页查询 | /list | /list | ✓ | +| 导出 | /export | /export | ✓ | +| 详情查询 | /{id} | /{id} | ✓ | +| 新增 | POST / | POST / | ✓ | +| 修改 | PUT / | PUT / | ✓ | +| 删除 | DELETE /{ids} | DELETE /{ids} | ✓ | +| 下载模板 | /importTemplate | /importTemplate | ✓ | +| 异步导入 | /importData | /importData | ✓ | +| 查询导入状态 | /importStatus/{taskId} | /importStatus/{taskId} | ✓ | +| 查询失败记录 | /importFailures/{taskId} | /importFailures/{taskId} | ✓ | + +**结论**:Controller层接口设计完全一致 ✓ + +### 2.2 Service层对比 + +| 方法 | 员工实体关系 | 信贷客户实体关联 | 一致性 | +|------|-------------|-----------------|--------| +| selectRelationPage | ✓ | ✓ | ✓ | +| selectRelationListForExport | ✓ | ✓ | ✓ | +| selectRelationById | ✓ | ✓ | ✓ | +| insertRelation | ✓ | ✓ | ✓(差异在默认值) | +| updateRelation | ✓ | ✓ | ✓ | +| deleteRelationByIds | ✓ | ✓ | ✓ | +| importRelation | ✓ | ✓ | ✓ | + +**结论**:Service层方法设计完全一致 ✓ + +### 2.3 异步导入逻辑对比 + +| 导入步骤 | 员工实体关系 | 信贷客户实体关联 | 差异说明 | +|----------|-------------|-----------------|----------| +| 1. 记录导入开始日志 | ✓ | ✓ | 一致 | +| 2. 批量查询已存在组合 | ✓ | ✓ | 一致 | +| 3. 验证必填字段 | ✓ | ✓ | 一致 | +| 4. 验证身份证格式 | ✓ | ✓ | 一致 | +| 5. 验证社会信用代码格式 | ✓ | ✓ | 一致 | +| 6. **验证身份证号存在性** | ✓ | **无** | **差异** | +| 7. 检查组合唯一性 | ✓ | ✓ | 一致 | +| 8. 检查文件内重复 | ✓ | ✓ | 一致 | +| 9. 设置身份标识 | is_emp_family=1 | is_cust_family=1 | **差异** | +| 10. 设置数据来源 | IMPORT | IMPORT | 一致 | +| 11. 批量插入 | 500条/批 | 500条/批 | 一致 | +| 12. 保存失败记录到Redis | ✓ | ✓ | 一致 | +| 13. 更新导入状态 | ✓ | ✓ | 一致 | +| 14. 记录导入完成日志 | ✓ | ✓ | 一致 | + +**关键差异说明**: +- **身份证号验证**:员工实体关系需要验证身份证号存在于员工表;信贷客户实体关联不需要 +- **身份标识默认值**:员工实体关系 `is_emp_family=1`;信贷客户实体关联 `is_cust_family=1` + +**结论**:导入逻辑框架一致,仅按需求有指定差异 ✓ + +### 2.4 Redis Key对比 + +| 用途 | 员工实体关系 | 信贷客户实体关联 | +|------|-------------|-----------------| +| 导入状态 | import:staffEnterpriseRelation:{taskId} | import:custEnterpriseRelation:{taskId} | +| 失败记录 | import:staffEnterpriseRelation:{taskId}:failures | import:custEnterpriseRelation:{taskId}:failures | +| 过期时间 | 7天 | 7天 | + +**结论**:Redis key设计模式一致 ✓ + +### 2.5 Mapper XML对比 + +| SQL功能 | 员工实体关系 | 信贷客户实体关联 | 差异说明 | +|---------|-------------|-----------------|----------| +| 分页查询 | LEFT JOIN员工表获取姓名 | 不JOIN | **差异** | +| 详情查询 | LEFT JOIN员工表获取姓名 | 不JOIN | **差异** | +| 唯一性检查 | person_id + social_credit_code | person_id + social_credit_code | 一致 | +| 批量存在检查 | ✓ | ✓ | 一致 | +| 批量插入 | ✓ | ✓ | 一致 | + +**结论**:Mapper SQL框架一致,差异在于是否JOIN员工表 ✓ + +--- + +## 三、前端逻辑校验 + +### 3.1 页面功能对比 + +| 功能 | 员工实体关系 | 信贷客户实体关联 | 一致性 | +|------|-------------|-----------------|--------| +| 搜索表单 | ✓ | ✓ | ✓ | +| 新增按钮 | ✓ | ✓ | ✓ | +| 导入按钮 | ✓ | ✓ | ✓ | +| 导出按钮 | ✓ | ✓ | ✓ | +| 查看失败记录按钮 | ✓ | ✓ | ✓ | +| 列表展示 | ✓ | ✓ | ✓(差异在列) | +| 分页 | ✓ | ✓ | ✓ | +| 新增/编辑弹窗 | ✓ | ✓ | ✓ | +| 详情弹窗 | ✓ | ✓ | ✓ | +| 导入弹窗 | ✓ | ✓ | ✓ | +| 失败记录弹窗 | ✓ | ✓ | ✓ | + +**结论**:页面功能完全一致 ✓ + +### 3.2 表单交互对比 + +| 交互项 | 员工实体关系 | 信贷客户实体关联 | 差异说明 | +|--------|-------------|-----------------|----------| +| 身份证号输入 | 远程搜索下拉框 | 普通输入框 | **差异** | +| 统一社会信用代码 | 输入框 | 输入框 | 一致 | +| 企业名称 | 输入框 | 输入框 | 一致 | +| 职务 | 输入框 | 输入框 | 一致 | +| 状态(编辑时) | 下拉选择 | 下拉选择 | 一致 | +| 补充说明 | 文本域 | 文本域 | 一致 | + +**结论**:除身份证号输入方式外,其他表单交互一致 ✓ + +### 3.3 列表展示对比 + +| 列 | 员工实体关系 | 信贷客户实体关联 | 差异说明 | +|----|-------------|-----------------|----------| +| 选择框 | ✓ | ✓ | 一致 | +| 身份证号 | ✓ | ✓ | 一致 | +| **员工姓名** | ✓ | **无** | **差异** | +| 企业名称 | ✓ | ✓ | 一致 | +| 职务 | ✓ | ✓ | 一致 | +| 状态 | ✓ | ✓ | 一致 | +| 数据来源 | ✓ | ✓ | 一致 | +| 创建时间 | ✓ | ✓ | 一致 | +| 操作列 | ✓ | ✓ | 一致 | + +**结论**:除员工姓名列外,其他列一致 ✓ + +### 3.4 详情弹窗对比 + +| 展示项 | 员工实体关系 | 信贷客户实体关联 | 差异说明 | +|--------|-------------|-----------------|----------| +| **员工姓名** | ✓ | **无** | **差异** | +| 身份证号 | ✓ | ✓ | 一致 | +| 统一社会信用代码 | ✓ | ✓ | 一致 | +| 企业名称 | ✓ | ✓ | 一致 | +| 职务 | ✓ | ✓ | 一致 | +| 状态 | ✓ | ✓ | 一致 | +| 数据来源 | ✓ | ✓ | 一致 | +| 补充说明 | ✓ | ✓ | 一致 | +| 创建时间/人 | ✓ | ✓ | 一致 | +| 更新时间/人 | ✓ | ✓ | 一致 | + +**结论**:除员工姓名外,详情展示一致 ✓ + +### 3.5 导入流程对比 + +| 导入步骤 | 员工实体关系 | 信贷客户实体关联 | 一致性 | +|----------|-------------|-----------------|--------| +| 点击导入按钮 | ✓ | ✓ | ✓ | +| 弹出上传对话框 | ✓ | ✓ | ✓ | +| 下载模板 | ✓ | ✓ | ✓ | +| 选择文件上传 | ✓ | ✓ | ✓ | +| 提交后立即返回taskId | ✓ | ✓ | ✓ | +| 显示后台处理提示 | ✓ | ✓ | ✓ | +| 开始轮询导入状态 | 2秒/次 | 2秒/次 | ✓ | +| 处理完成后通知 | ✓ | ✓ | ✓ | +| 显示失败记录按钮 | 有失败时显示 | 有失败时显示 | ✓ | +| 查看失败记录弹窗 | ✓ | ✓ | ✓ | +| 分页展示失败记录 | ✓ | ✓ | ✓ | +| 清除历史记录 | ✓ | ✓ | ✓ | + +**结论**:导入流程完全一致 ✓ + +### 3.6 localStorage对比 + +| 用途 | 员工实体关系 | 信贷客户实体关联 | +|------|-------------|-----------------| +| 存储key | staff_enterprise_relation_import_last_task | cust_enterprise_relation_import_last_task | +| 存储内容 | taskId, status, counts, saveTime | 相同 | +| 过期检查 | 7天 | 7天 | + +**结论**:localStorage使用模式一致 ✓ + +--- + +## 四、权限配置对比 + +| 权限 | 员工实体关系 | 信贷客户实体关联 | +|------|-------------|-----------------| +| 列表 | ccdi:staffEnterpriseRelation:list | ccdi:custEnterpriseRelation:list | +| 查询 | ccdi:staffEnterpriseRelation:query | ccdi:custEnterpriseRelation:query | +| 新增 | ccdi:staffEnterpriseRelation:add | ccdi:custEnterpriseRelation:add | +| 编辑 | ccdi:staffEnterpriseRelation:edit | ccdi:custEnterpriseRelation:edit | +| 删除 | ccdi:staffEnterpriseRelation:remove | ccdi:custEnterpriseRelation:remove | +| 导出 | ccdi:staffEnterpriseRelation:export | ccdi:custEnterpriseRelation:export | +| 导入 | ccdi:staffEnterpriseRelation:import | ccdi:custEnterpriseRelation:import | + +**结论**:权限命名规范一致 ✓ + +--- + +## 五、校验总结 + +### 5.1 一致性检查结果 + +| 检查项 | 状态 | +|--------|------| +| Controller接口设计 | ✓ 一致 | +| Service方法设计 | ✓ 一致 | +| 异步导入框架 | ✓ 一致 | +| Redis状态管理 | ✓ 一致 | +| Mapper SQL框架 | ✓ 一致 | +| 前端页面功能 | ✓ 一致 | +| 前端表单交互 | ✓ 一致(除身份证输入方式) | +| 前端列表展示 | ✓ 一致(除姓名列) | +| 前端详情展示 | ✓ 一致(除姓名) | +| 导入流程 | ✓ 一致 | +| localStorage使用 | ✓ 一致 | +| 权限命名规范 | ✓ 一致 | + +### 5.2 预期差异确认 + +| 差异项 | 员工实体关系 | 信贷客户实体关联 | 状态 | +|--------|-------------|-----------------|------| +| 身份证号验证 | 验证存在员工表 | 不验证 | ✓ 符合预期 | +| 员工搜索功能 | 有 | 无 | ✓ 符合预期 | +| 姓名显示 | 有 | 无 | ✓ 符合预期 | +| 身份标识默认值 | is_emp_family=1 | is_cust_family=1 | ✓ 符合预期 | +| API路径 | staffEnterpriseRelation | custEnterpriseRelation | ✓ 符合预期 | +| 权限标识 | staffEnterpriseRelation | custEnterpriseRelation | ✓ 符合预期 | +| localStorage key | staff_enterprise_relation | cust_enterprise_relation | ✓ 符合预期 | + +### 5.3 校验结论 + +**信贷客户实体关联维护功能的实施方案与员工实体关系维护功能在逻辑上完全一致**,所有差异均符合需求预期: + +1. ✓ 后端实现逻辑一致(CRUD、异步导入、Redis状态管理) +2. ✓ 前端交互方式一致(弹窗、导入流程、状态轮询) +3. ✓ 预期差异均已正确处理(无员工搜索、无姓名显示、身份标识默认值不同) + +**实施方案可直接用于开发实施。** diff --git a/doc/信贷客户实体关联维护功能/前端实施方案.md b/doc/信贷客户实体关联维护功能/前端实施方案.md new file mode 100644 index 0000000..6aeae91 --- /dev/null +++ b/doc/信贷客户实体关联维护功能/前端实施方案.md @@ -0,0 +1,1088 @@ +# 信贷客户实体关联维护功能 - 前端实施方案 + +## 一、功能概述 + +基于员工实体关系维护功能开发信贷客户实体关联维护功能,前端交互方式与员工实体关系完全一致,主要差异在于: + +1. **无员工下拉搜索**:身份证号直接输入,不使用远程搜索 +2. **无姓名列显示**:列表和详情中不显示姓名字段 +3. **API路径不同**:调用 `/ccdi/custEnterpriseRelation/*` 接口 + +--- + +## 二、前端文件清单 + +| 文件名 | 路径 | 说明 | +|--------|------|------| +| index.vue | views/ccdiCustEnterpriseRelation/ | 主页面组件 | +| ccdiCustEnterpriseRelation.js | api/ | API接口定义 | + +--- + +## 三、API接口定义 + +### 3.1 ccdiCustEnterpriseRelation.js + +**与员工实体关系API的差异**:仅URL路径不同 + +```javascript +import request from '@/utils/request' + +// 查询信贷客户实体关联列表 +export function listRelation(query) { + return request({ + url: '/ccdi/custEnterpriseRelation/list', + method: 'get', + params: query + }) +} + +// 查询信贷客户实体关联详情 +export function getRelation(id) { + return request({ + url: '/ccdi/custEnterpriseRelation/' + id, + method: 'get' + }) +} + +// 新增信贷客户实体关联 +export function addRelation(data) { + return request({ + url: '/ccdi/custEnterpriseRelation', + method: 'post', + data: data + }) +} + +// 修改信贷客户实体关联 +export function updateRelation(data) { + return request({ + url: '/ccdi/custEnterpriseRelation', + method: 'put', + data: data + }) +} + +// 删除信贷客户实体关联 +export function delRelation(ids) { + return request({ + url: '/ccdi/custEnterpriseRelation/' + ids, + method: 'delete' + }) +} + +// 导出信贷客户实体关联 +export function exportRelation(query) { + return request({ + url: '/ccdi/custEnterpriseRelation/export', + method: 'post', + params: query + }) +} + +// 下载导入模板 +export function importTemplate() { + return request({ + url: '/ccdi/custEnterpriseRelation/importTemplate', + method: 'post' + }) +} + +// 导入信贷客户实体关联 +export function importData(file) { + const formData = new FormData() + formData.append('file', file) + return request({ + url: '/ccdi/custEnterpriseRelation/importData', + method: 'post', + data: formData + }) +} + +// 查询导入状态 +export function getImportStatus(taskId) { + return request({ + url: '/ccdi/custEnterpriseRelation/importStatus/' + taskId, + method: 'get' + }) +} + +// 查询导入失败记录 +export function getImportFailures(taskId, pageNum, pageSize) { + return request({ + url: '/ccdi/custEnterpriseRelation/importFailures/' + taskId, + method: 'get', + params: { pageNum, pageSize } + }) +} +``` + +--- + +## 四、主页面组件 + +### 4.1 index.vue + +**与员工实体关系前端的关键差异**: + +| 差异项 | 员工实体关系 | 信贷客户实体关联 | +|--------|-------------|-----------------| +| 员工下拉搜索 | 有 | 无,直接输入 | +| 列表姓名列 | 有 | 无 | +| 详情姓名显示 | 有 | 无 | +| API导入路径 | /ccdi/staffEnterpriseRelation/importData | /ccdi/custEnterpriseRelation/importData | +| localStorage key | staff_enterprise_relation_import_last_task | cust_enterprise_relation_import_last_task | +| 权限标识 | ccdi:staffEnterpriseRelation:* | ccdi:custEnterpriseRelation:* | +| 字典 | dicts: ['ccdi_relation_status', 'ccdi_data_source'] | 相同 | + +```vue + + + + + +``` + +--- + +## 五、与员工实体关系前端代码对比 + +### 5.1 关键差异总结 + +| 对比项 | 员工实体关系 | 信贷客户实体关联 | +|--------|-------------|-----------------| +| 组件名 | StaffEnterpriseRelation | CustEnterpriseRelation | +| API文件 | ccdiStaffEnterpriseRelation.js | ccdiCustEnterpriseRelation.js | +| 员工下拉搜索 | 有(searchStaff方法) | 无,直接输入 | +| 列表姓名列 | 有 personName 列 | 无 | +| 详情姓名显示 | 有 | 无 | +| 上传URL | /ccdi/staffEnterpriseRelation/importData | /ccdi/custEnterpriseRelation/importData | +| localStorage key | staff_enterprise_relation_import_last_task | cust_enterprise_relation_import_last_task | +| 权限标识 | ccdi:staffEnterpriseRelation:* | ccdi:custEnterpriseRelation:* | +| 标题/提示文本 | 员工实体关系 | 信贷客户实体关联 | + +### 5.2 移除的功能代码 + +信贷客户实体关联前端**不需要**以下员工实体关系特有的功能: + +1. **员工搜索相关数据**: + - `staffOptions: []` + - `staffLoading: false` + +2. **员工搜索相关方法**: + - `searchStaff(query)` - 远程搜索员工 + - `handleSelectFocus()` - 下拉框聚焦加载 + +3. **员工搜索相关导入**: + - `import {listBaseStaff} from "@/api/ccdiBaseStaff";` + +4. **员工下拉选择组件**(表单中): + ```vue + + + + + ``` + + 替换为: + ```vue + + + ``` + +5. **列表中的员工姓名列**: + ```vue + + + ``` + + 信贷客户实体关联无此列。 + +--- + +## 六、实施步骤 + +1. 创建 `src/api/ccdiCustEnterpriseRelation.js` 文件 +2. 创建 `src/views/ccdiCustEnterpriseRelation/index.vue` 文件 +3. 配置路由(在数据库菜单表中配置) +4. 测试功能 + +--- + +## 七、菜单配置SQL + +```sql +-- 信贷客户实体关联菜单 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time, remark) +VALUES ('信贷客户实体关联', 2000, 3, 'custEnterpriseRelation', 'ccdiCustEnterpriseRelation/index', 'C', '0', '0', 'ccdi:custEnterpriseRelation:list', 'peoples', 'admin', NOW(), '信贷客户实体关联菜单'); + +-- 按钮权限 +SET @parentId = LAST_INSERT_ID(); + +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time) VALUES +('信贷客户实体关联查询', @parentId, 1, '#', '', 'F', '0', '0', 'ccdi:custEnterpriseRelation:query', '#', 'admin', NOW()), +('信贷客户实体关联新增', @parentId, 2, '#', '', 'F', '0', '0', 'ccdi:custEnterpriseRelation:add', '#', 'admin', NOW()), +('信贷客户实体关联修改', @parentId, 3, '#', '', 'F', '0', '0', 'ccdi:custEnterpriseRelation:edit', '#', 'admin', NOW()), +('信贷客户实体关联删除', @parentId, 4, '#', '', 'F', '0', '0', 'ccdi:custEnterpriseRelation:remove', '#', 'admin', NOW()), +('信贷客户实体关联导出', @parentId, 5, '#', '', 'F', '0', '0', 'ccdi:custEnterpriseRelation:export', '#', 'admin', NOW()), +('信贷客户实体关联导入', @parentId, 6, '#', '', 'F', '0', '0', 'ccdi:custEnterpriseRelation:import', '#', 'admin', NOW()); +``` + +**注意**:`parent_id = 2000` 需要根据实际的父菜单ID调整。 diff --git a/doc/信贷客户实体关联维护功能/后端实施方案.md b/doc/信贷客户实体关联维护功能/后端实施方案.md new file mode 100644 index 0000000..9a606be --- /dev/null +++ b/doc/信贷客户实体关联维护功能/后端实施方案.md @@ -0,0 +1,1511 @@ +# 信贷客户实体关联维护功能 - 后端实施方案 + +## 一、功能概述 + +基于员工实体关系维护功能开发信贷客户实体关联维护功能,后端实现逻辑与员工实体关系完全一致,主要差异在于: + +1. **不验证身份证号**:导入时不需要验证身份证号是否存在 +2. **无远程搜索接口**:没有员工搜索功能 +3. **身份标识默认值不同**:`is_cust_family = 1` + +--- + +## 二、数据库设计 + +### 2.1 表结构 + +表名:`ccdi_cust_enterprise_relation` + +```sql +CREATE TABLE `ccdi_cust_enterprise_relation` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键,唯一标识', + `person_id` VARCHAR(18) NOT NULL COMMENT '身份证号', + `relation_person_post` VARCHAR(100) DEFAULT NULL COMMENT '关联人在企业的职务:股东、法人、高管、实际控制人等', + `social_credit_code` VARCHAR(18) NOT NULL COMMENT '统一社会信用代码,关联企业主体信息表的外键', + `enterprise_name` VARCHAR(200) DEFAULT NULL COMMENT '企业名称(冗余存储,便于快速查询)', + `status` INT NOT NULL DEFAULT 1 COMMENT '关系是否有效:0 - 无效、1 - 有效(默认有效)', + `remark` TEXT COMMENT '补充说明', + `data_source` VARCHAR(50) DEFAULT NULL COMMENT '数据来源', + `is_employee` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是员工:0-否 1-是', + `is_emp_family` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是员工家庭关联人:0-否 1-是', + `is_customer` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是信贷客户:0-否 1-是', + `is_cust_family` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是信贷客户关联人:0-否 1-是', + `created_by` VARCHAR(64) NOT NULL COMMENT '记录创建人', + `updated_by` VARCHAR(64) DEFAULT NULL COMMENT '记录更新人', + `create_time` DATETIME NOT NULL COMMENT '记录创建时间', + `update_time` DATETIME NOT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`), + KEY `idx_person_id` (`person_id`), + KEY `idx_social_credit_code` (`social_credit_code`), + UNIQUE KEY `uk_person_enterprise` (`person_id`, `social_credit_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='信贷客户实体关联关系表'; +``` + +### 2.2 唯一性约束 + +- 业务主键:`person_id` + `social_credit_code` 组合唯一 + +--- + +## 三、后端文件清单 + +### 3.1 Domain层 + +| 文件名 | 路径 | 说明 | +|--------|------|------| +| CcdiCustEnterpriseRelation.java | domain/ | 实体类 | +| CcdiCustEnterpriseRelationVO.java | domain/vo/ | 视图对象 | +| CcdiCustEnterpriseRelationAddDTO.java | domain/dto/ | 新增DTO | +| CcdiCustEnterpriseRelationEditDTO.java | domain/dto/ | 编辑DTO | +| CcdiCustEnterpriseRelationQueryDTO.java | domain/dto/ | 查询DTO | +| CcdiCustEnterpriseRelationExcel.java | domain/excel/ | Excel导入导出对象 | +| CustEnterpriseRelationImportFailureVO.java | domain/vo/ | 导入失败记录VO | + +### 3.2 Mapper层 + +| 文件名 | 路径 | 说明 | +|--------|------|------| +| CcdiCustEnterpriseRelationMapper.java | mapper/ | Mapper接口 | +| CcdiCustEnterpriseRelationMapper.xml | resources/mapper/ccdi/ | Mapper XML | + +### 3.3 Service层 + +| 文件名 | 路径 | 说明 | +|--------|------|------| +| ICcdiCustEnterpriseRelationService.java | service/ | 服务接口 | +| CcdiCustEnterpriseRelationServiceImpl.java | service/impl/ | 服务实现 | +| ICcdiCustEnterpriseRelationImportService.java | service/ | 异步导入服务接口 | +| CcdiCustEnterpriseRelationImportServiceImpl.java | service/impl/ | 异步导入服务实现 | + +### 3.4 Controller层 + +| 文件名 | 路径 | 说明 | +|--------|------|------| +| CcdiCustEnterpriseRelationController.java | controller/ | 控制器 | + +--- + +## 四、核心实现细节 + +### 4.1 实体类 CcdiCustEnterpriseRelation.java + +```java +package com.ruoyi.ccdi.domain; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 信贷客户实体关联信息对象 ccdi_cust_enterprise_relation + */ +@Data +@TableName("ccdi_cust_enterprise_relation") +@Schema(description = "信贷客户实体关联信息") +public class CcdiCustEnterpriseRelation implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @TableId(type = IdType.AUTO) + @Schema(description = "主键ID") + private Long id; + + /** 身份证号 */ + @Schema(description = "身份证号") + private String personId; + + /** 关联人在企业的职务 */ + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 统一社会信用代码 */ + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; + + /** 补充说明 */ + @Schema(description = "补充说明") + private String remark; + + /** 数据来源 */ + @Schema(description = "数据来源") + private String dataSource; + + /** 是否为员工(0-否 1-是) */ + @Schema(description = "是否为员工(0-否 1-是)") + private Integer isEmployee; + + /** 是否为员工家属(0-否 1-是) */ + @Schema(description = "是否为员工家属(0-否 1-是)") + private Integer isEmpFamily; + + /** 是否为客户(0-否 1-是) */ + @Schema(description = "是否为客户(0-否 1-是)") + private Integer isCustomer; + + /** 是否为客户家属(0-否 1-是) */ + @Schema(description = "是否为客户家属(0-否 1-是)") + private Integer isCustFamily; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + @Schema(description = "创建时间") + private Date createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(description = "更新时间") + private Date updateTime; + + /** 创建人 */ + @TableField(fill = FieldFill.INSERT) + @Schema(description = "创建人") + private String createdBy; + + /** 更新人 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(description = "更新人") + private String updatedBy; +} +``` + +### 4.2 VO类 CcdiCustEnterpriseRelationVO.java + +**与员工实体关系VO的差异**: +- 无 `personName` 字段(因为没有关联员工表查询姓名) +- 类名和注释不同 + +```java +package com.ruoyi.ccdi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 信贷客户实体关联信息VO + */ +@Data +@Schema(description = "信贷客户实体关联信息") +public class CcdiCustEnterpriseRelationVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @Schema(description = "主键ID") + private Long id; + + /** 身份证号 */ + @Schema(description = "身份证号") + private String personId; + + /** 关联人在企业的职务 */ + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 统一社会信用代码 */ + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; + + /** 补充说明 */ + @Schema(description = "补充说明") + private String remark; + + /** 数据来源 */ + @Schema(description = "数据来源") + private String dataSource; + + /** 是否为员工(0-否 1-是) */ + @Schema(description = "是否为员工(0-否 1-是)") + private Integer isEmployee; + + /** 是否为员工家属(0-否 1-是) */ + @Schema(description = "是否为员工家属(0-否 1-是)") + private Integer isEmpFamily; + + /** 是否为客户(0-否 1-是) */ + @Schema(description = "是否为客户(0-否 1-是)") + private Integer isCustomer; + + /** 是否为客户家属(0-否 1-是) */ + @Schema(description = "是否为客户家属(0-否 1-是)") + private Integer isCustFamily; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private Date createTime; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新时间") + private Date updateTime; + + /** 创建人 */ + @Schema(description = "创建人") + private String createdBy; + + /** 更新人 */ + @Schema(description = "更新人") + private String updatedBy; +} +``` + +### 4.3 新增DTO CcdiCustEnterpriseRelationAddDTO.java + +```java +package com.ruoyi.ccdi.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息新增DTO + */ +@Data +@Schema(description = "信贷客户实体关联信息新增") +public class CcdiCustEnterpriseRelationAddDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 身份证号 */ + @NotBlank(message = "身份证号不能为空") + @Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$", message = "身份证号格式不正确") + @Schema(description = "身份证号") + private String personId; + + /** 关联人在企业的职务 */ + @Size(max = 100, message = "关联人在企业的职务长度不能超过100个字符") + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 统一社会信用代码 */ + @NotBlank(message = "统一社会信用代码不能为空") + @Pattern(regexp = "^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$", message = "统一社会信用代码格式不正确") + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @NotBlank(message = "企业名称不能为空") + @Size(max = 200, message = "企业名称长度不能超过200个字符") + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; + + /** 补充说明 */ + @Schema(description = "补充说明") + private String remark; +} +``` + +### 4.4 编辑DTO CcdiCustEnterpriseRelationEditDTO.java + +```java +package com.ruoyi.ccdi.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息编辑DTO + */ +@Data +@Schema(description = "信贷客户实体关联信息编辑") +public class CcdiCustEnterpriseRelationEditDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @NotNull(message = "主键ID不能为空") + @Schema(description = "主键ID") + private Long id; + + /** 身份证号(不可修改) */ + @Schema(description = "身份证号(不可修改)") + private String personId; + + /** 关联人在企业的职务 */ + @Size(max = 100, message = "关联人在企业的职务长度不能超过100个字符") + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 统一社会信用代码(不可修改) */ + @Schema(description = "统一社会信用代码(不可修改)") + private String socialCreditCode; + + /** 企业名称 */ + @NotBlank(message = "企业名称不能为空") + @Size(max = 200, message = "企业名称长度不能超过200个字符") + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; + + /** 补充说明 */ + @Schema(description = "补充说明") + private String remark; +} +``` + +### 4.5 查询DTO CcdiCustEnterpriseRelationQueryDTO.java + +```java +package com.ruoyi.ccdi.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息查询DTO + */ +@Data +@Schema(description = "信贷客户实体关联信息查询条件") +public class CcdiCustEnterpriseRelationQueryDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 身份证号 */ + @Schema(description = "身份证号") + private String personId; + + /** 统一社会信用代码 */ + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; +} +``` + +### 4.6 Excel类 CcdiCustEnterpriseRelationExcel.java + +```java +package com.ruoyi.ccdi.domain.excel; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import com.ruoyi.common.annotation.Required; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息Excel导入导出对象 + */ +@Data +@Schema(description = "信贷客户实体关联信息Excel导入导出对象") +public class CcdiCustEnterpriseRelationExcel implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 身份证号 */ + @ExcelProperty(value = "身份证号", index = 0) + @ColumnWidth(20) + @Required + @Schema(description = "身份证号") + private String personId; + + /** 统一社会信用代码 */ + @ExcelProperty(value = "统一社会信用代码", index = 1) + @ColumnWidth(25) + @Required + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @ExcelProperty(value = "企业名称", index = 2) + @ColumnWidth(30) + @Required + @Schema(description = "企业名称") + private String enterpriseName; + + /** 关联人在企业的职务 */ + @ExcelProperty(value = "关联人在企业的职务", index = 3) + @ColumnWidth(25) + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 补充说明 */ + @ExcelProperty(value = "补充说明", index = 4) + @ColumnWidth(40) + @Schema(description = "补充说明") + private String remark; +} +``` + +### 4.7 导入失败VO CustEnterpriseRelationImportFailureVO.java + +```java +package com.ruoyi.ccdi.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息导入失败记录VO + */ +@Data +@Schema(description = "信贷客户实体关联信息导入失败记录") +public class CustEnterpriseRelationImportFailureVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 身份证号 */ + @Schema(description = "身份证号") + private String personId; + + /** 统一社会信用代码 */ + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @Schema(description = "企业名称") + private String enterpriseName; + + /** 错误信息 */ + @Schema(description = "错误信息") + private String errorMessage; +} +``` + +--- + +## 五、Mapper层实现 + +### 5.1 Mapper接口 CcdiCustEnterpriseRelationMapper.java + +```java +package com.ruoyi.ccdi.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.ccdi.domain.CcdiCustEnterpriseRelation; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationQueryDTO; +import com.ruoyi.ccdi.domain.vo.CcdiCustEnterpriseRelationVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Set; + +/** + * 信贷客户实体关联信息 数据层 + */ +@Mapper +public interface CcdiCustEnterpriseRelationMapper extends BaseMapper { + + /** + * 分页查询信贷客户实体关联列表 + */ + Page selectRelationPage(@Param("page") Page page, + @Param("query") CcdiCustEnterpriseRelationQueryDTO queryDTO); + + /** + * 查询信贷客户实体关联详情 + */ + CcdiCustEnterpriseRelationVO selectRelationById(@Param("id") Long id); + + /** + * 判断身份证号和统一社会信用代码的组合是否已存在 + */ + boolean existsByPersonIdAndSocialCreditCode(@Param("personId") String personId, + @Param("socialCreditCode") String socialCreditCode); + + /** + * 批量查询已存在的person_id + social_credit_code组合 + */ + Set batchExistsByCombinations(@Param("combinations") List combinations); + + /** + * 批量插入信贷客户实体关联数据 + */ + int insertBatch(@Param("list") List list); +} +``` + +### 5.2 Mapper XML CcdiCustEnterpriseRelationMapper.xml + +**与员工实体关系Mapper XML的关键差异**: +- 无 `personName` 字段查询 +- 无 JOIN 员工表 +- 表名不同 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO ccdi_cust_enterprise_relation + (person_id, relation_person_post, social_credit_code, enterprise_name, + status, remark, data_source, is_employee, is_emp_family, is_customer, is_cust_family, + created_by, create_time, updated_by, update_time) + VALUES + + (#{item.personId}, #{item.relationPersonPost}, #{item.socialCreditCode}, #{item.enterpriseName}, + #{item.status}, #{item.remark}, #{item.dataSource}, #{item.isEmployee}, #{item.isEmpFamily}, #{item.isCustomer}, #{item.isCustFamily}, + #{item.createdBy}, NOW(), #{item.updatedBy}, NOW()) + + + + +``` + +--- + +## 六、Service层实现 + +### 6.1 服务接口 ICcdiCustEnterpriseRelationService.java + +```java +package com.ruoyi.ccdi.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationAddDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationEditDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationQueryDTO; +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CcdiCustEnterpriseRelationVO; + +import java.util.List; + +/** + * 信贷客户实体关联信息 服务层 + */ +public interface ICcdiCustEnterpriseRelationService { + + /** + * 查询信贷客户实体关联列表 + */ + List selectRelationList(CcdiCustEnterpriseRelationQueryDTO queryDTO); + + /** + * 分页查询信贷客户实体关联列表 + */ + Page selectRelationPage(Page page, CcdiCustEnterpriseRelationQueryDTO queryDTO); + + /** + * 查询信贷客户实体关联列表(用于导出) + */ + List selectRelationListForExport(CcdiCustEnterpriseRelationQueryDTO queryDTO); + + /** + * 查询信贷客户实体关联详情 + */ + CcdiCustEnterpriseRelationVO selectRelationById(Long id); + + /** + * 新增信贷客户实体关联 + */ + int insertRelation(CcdiCustEnterpriseRelationAddDTO addDTO); + + /** + * 修改信贷客户实体关联 + */ + int updateRelation(CcdiCustEnterpriseRelationEditDTO editDTO); + + /** + * 批量删除信贷客户实体关联 + */ + int deleteRelationByIds(Long[] ids); + + /** + * 导入信贷客户实体关联数据(异步) + */ + String importRelation(List excelList); +} +``` + +### 6.2 服务接口 ICcdiCustEnterpriseRelationImportService.java + +```java +package com.ruoyi.ccdi.service; + +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CustEnterpriseRelationImportFailureVO; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; + +import java.util.List; + +/** + * 信贷客户实体关联信息异步导入服务层 + */ +public interface ICcdiCustEnterpriseRelationImportService { + + /** + * 异步导入信贷客户实体关联数据 + */ + void importRelationAsync(List excelList, String taskId, String userName); + + /** + * 获取导入失败记录 + */ + List getImportFailures(String taskId); + + /** + * 查询导入状态 + */ + ImportStatusVO getImportStatus(String taskId); +} +``` + +### 6.3 服务实现 CcdiCustEnterpriseRelationServiceImpl.java + +**关键差异点**: +- 身份标识默认值:`is_cust_family = 1`(其他为0) +- 无员工身份证号验证 + +```java +package com.ruoyi.ccdi.service.impl; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.ccdi.domain.CcdiCustEnterpriseRelation; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationAddDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationEditDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationQueryDTO; +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CcdiCustEnterpriseRelationVO; +import com.ruoyi.ccdi.mapper.CcdiCustEnterpriseRelationMapper; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationImportService; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationService; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import jakarta.annotation.Resource; +import org.springframework.beans.BeanUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 信贷客户实体关联信息 服务层处理 + */ +@Service +public class CcdiCustEnterpriseRelationServiceImpl implements ICcdiCustEnterpriseRelationService { + + @Resource + private CcdiCustEnterpriseRelationMapper relationMapper; + + @Resource + private ICcdiCustEnterpriseRelationImportService relationImportService; + + @Resource + private RedisTemplate redisTemplate; + + @Override + public java.util.List selectRelationList(CcdiCustEnterpriseRelationQueryDTO queryDTO) { + Page page = new Page<>(1, Integer.MAX_VALUE); + Page resultPage = relationMapper.selectRelationPage(page, queryDTO); + return resultPage.getRecords(); + } + + @Override + public Page selectRelationPage(Page page, CcdiCustEnterpriseRelationQueryDTO queryDTO) { + return relationMapper.selectRelationPage(page, queryDTO); + } + + @Override + public java.util.List selectRelationListForExport(CcdiCustEnterpriseRelationQueryDTO queryDTO) { + Page page = new Page<>(1, Integer.MAX_VALUE); + Page resultPage = relationMapper.selectRelationPage(page, queryDTO); + + return resultPage.getRecords().stream().map(vo -> { + CcdiCustEnterpriseRelationExcel excel = new CcdiCustEnterpriseRelationExcel(); + BeanUtils.copyProperties(vo, excel); + return excel; + }).collect(Collectors.toList()); + } + + @Override + public CcdiCustEnterpriseRelationVO selectRelationById(Long id) { + return relationMapper.selectRelationById(id); + } + + @Override + @Transactional + public int insertRelation(CcdiCustEnterpriseRelationAddDTO addDTO) { + // 检查身份证号+统一社会信用代码唯一性 + if (relationMapper.existsByPersonIdAndSocialCreditCode(addDTO.getPersonId(), addDTO.getSocialCreditCode())) { + throw new RuntimeException("该身份证号和统一社会信用代码组合已存在"); + } + + CcdiCustEnterpriseRelation relation = new CcdiCustEnterpriseRelation(); + BeanUtils.copyProperties(addDTO, relation); + + // 设置默认值 + // 新增时强制设置状态为有效 + relation.setStatus(1); + + // 【关键差异】信贷客户实体关联的身份标识默认值 + if (relation.getIsEmployee() == null) { + relation.setIsEmployee(0); + } + if (relation.getIsEmpFamily() == null) { + relation.setIsEmpFamily(0); + } + if (relation.getIsCustomer() == null) { + relation.setIsCustomer(0); + } + if (relation.getIsCustFamily() == null) { + relation.setIsCustFamily(1); // 信贷客户关联人标识为1 + } + if (StringUtils.isEmpty(relation.getDataSource())) { + relation.setDataSource("MANUAL"); + } + + int result = relationMapper.insert(relation); + + return result; + } + + @Override + @Transactional + public int updateRelation(CcdiCustEnterpriseRelationEditDTO editDTO) { + // 使用LambdaUpdateWrapper只更新非null字段,保护系统字段不被覆盖 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(CcdiCustEnterpriseRelation::getId, editDTO.getId()); + + // 只更新前端可编辑的字段 + updateWrapper.set(editDTO.getRelationPersonPost() != null, CcdiCustEnterpriseRelation::getRelationPersonPost, editDTO.getRelationPersonPost()); + updateWrapper.set(editDTO.getEnterpriseName() != null, CcdiCustEnterpriseRelation::getEnterpriseName, editDTO.getEnterpriseName()); + updateWrapper.set(editDTO.getStatus() != null, CcdiCustEnterpriseRelation::getStatus, editDTO.getStatus()); + updateWrapper.set(editDTO.getRemark() != null, CcdiCustEnterpriseRelation::getRemark, editDTO.getRemark()); + + // 注意:以下字段不可修改 + // - personId(身份证号,业务主键) + // - socialCreditCode(统一社会信用代码,业务主键) + // - dataSource(数据来源,系统字段) + // - isEmployee(是否为员工,系统字段) + // - isEmpFamily(是否为员工家属,系统字段) + // - isCustomer(是否为客户,系统字段) + // - isCustFamily(是否为客户家属,系统字段) + + int result = relationMapper.update(null, updateWrapper); + + return result; + } + + @Override + @Transactional + public int deleteRelationByIds(Long[] ids) { + return relationMapper.deleteBatchIds(java.util.List.of(ids)); + } + + @Override + @Transactional + public String importRelation(java.util.List excelList) { + if (StringUtils.isNull(excelList) || excelList.isEmpty()) { + throw new RuntimeException("至少需要一条数据"); + } + + // 生成任务ID + String taskId = UUID.randomUUID().toString(); + long startTime = System.currentTimeMillis(); + + // 获取当前用户名 + String userName = SecurityUtils.getUsername(); + + // 初始化Redis状态 + String statusKey = "import:custEnterpriseRelation:" + taskId; + Map statusData = new HashMap<>(); + statusData.put("taskId", taskId); + statusData.put("status", "PROCESSING"); + statusData.put("totalCount", excelList.size()); + statusData.put("successCount", 0); + statusData.put("failureCount", 0); + statusData.put("progress", 0); + statusData.put("startTime", startTime); + statusData.put("message", "正在处理..."); + + redisTemplate.opsForHash().putAll(statusKey, statusData); + redisTemplate.expire(statusKey, 7, TimeUnit.DAYS); + + // 调用异步导入服务 + relationImportService.importRelationAsync(excelList, taskId, userName); + + return taskId; + } +} +``` + +### 6.4 异步导入服务实现 CcdiCustEnterpriseRelationImportServiceImpl.java + +**【关键实现】异步导入核心逻辑** + +**与员工实体关系导入的关键差异**: +- **不验证身份证号是否存在**(移除员工表验证逻辑) +- 身份标识默认值:`is_cust_family = 1` +- Redis key前缀:`import:custEnterpriseRelation:` + +```java +package com.ruoyi.ccdi.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.ccdi.domain.CcdiCustEnterpriseRelation; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationAddDTO; +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CustEnterpriseRelationImportFailureVO; +import com.ruoyi.ccdi.domain.vo.ImportResult; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; +import com.ruoyi.ccdi.mapper.CcdiCustEnterpriseRelationMapper; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationImportService; +import com.ruoyi.ccdi.utils.ImportLogUtils; +import com.ruoyi.common.utils.StringUtils; +import jakarta.annotation.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +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.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 信贷客户实体关联信息异步导入服务层处理 + */ +@Service +@EnableAsync +public class CcdiCustEnterpriseRelationImportServiceImpl implements ICcdiCustEnterpriseRelationImportService { + + private static final Logger log = LoggerFactory.getLogger(CcdiCustEnterpriseRelationImportServiceImpl.class); + + @Resource + private CcdiCustEnterpriseRelationMapper relationMapper; + + @Resource + private RedisTemplate redisTemplate; + + @Override + @Async + @Transactional + public void importRelationAsync(List excelList, String taskId, String userName) { + long startTime = System.currentTimeMillis(); + + // 记录导入开始 + ImportLogUtils.logImportStart(log, taskId, "信贷客户实体关联", excelList.size(), userName); + + List newRecords = new ArrayList<>(); + List failures = new ArrayList<>(); + + // 【关键差异】不需要验证身份证号是否存在 + // 员工实体关系导入会验证身份证号是否存在于员工表,信贷客户实体关联不需要此验证 + + // 批量查询已存在的person_id + social_credit_code组合 + ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的客户企业关系组合", excelList.size()); + Set existingCombinations = getExistingCombinations(excelList); + ImportLogUtils.logBatchQueryComplete(log, taskId, "客户企业关系组合", existingCombinations.size()); + + // 用于跟踪Excel文件内已处理的组合 + Set processedCombinations = new HashSet<>(); + + // 分类数据 + for (int i = 0; i < excelList.size(); i++) { + CcdiCustEnterpriseRelationExcel excel = excelList.get(i); + + try { + // 转换为AddDTO进行验证 + CcdiCustEnterpriseRelationAddDTO addDTO = new CcdiCustEnterpriseRelationAddDTO(); + BeanUtils.copyProperties(excel, addDTO); + + // 验证数据(不验证身份证号是否存在) + validateRelationData(addDTO); + + String combination = excel.getPersonId() + "|" + excel.getSocialCreditCode(); + + CcdiCustEnterpriseRelation relation = new CcdiCustEnterpriseRelation(); + BeanUtils.copyProperties(excel, relation); + + if (existingCombinations.contains(combination)) { + // 组合已存在,直接报错 + throw new RuntimeException(String.format("身份证号[%s]和统一社会信用代码[%s]的组合已存在,请勿重复导入", + excel.getPersonId(), excel.getSocialCreditCode())); + } else if (processedCombinations.contains(combination)) { + // Excel文件内部重复 + throw new RuntimeException(String.format("身份证号[%s]和统一社会信用代码[%s]的组合在导入文件中重复,已跳过此条记录", + excel.getPersonId(), excel.getSocialCreditCode())); + } else { + relation.setCreatedBy(userName); + relation.setUpdatedBy(userName); + + // 设置默认值 + relation.setStatus(1); + // 【关键差异】信贷客户实体关联的身份标识 + relation.setIsEmployee(0); + relation.setIsEmpFamily(0); + relation.setIsCustomer(0); + relation.setIsCustFamily(1); // 信贷客户关联人标识为1 + relation.setDataSource("IMPORT"); + + newRecords.add(relation); + processedCombinations.add(combination); // 标记为已处理 + } + + // 记录进度 + ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(), + newRecords.size(), failures.size()); + + } catch (Exception e) { + CustEnterpriseRelationImportFailureVO failure = new CustEnterpriseRelationImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(e.getMessage()); + failures.add(failure); + + // 记录验证失败日志 + String keyData = String.format("身份证号=%s, 统一社会信用代码=%s, 企业名称=%s", + excel.getPersonId(), excel.getSocialCreditCode(), excel.getEnterpriseName()); + ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData); + } + } + + // 批量插入新数据 + if (!newRecords.isEmpty()) { + ImportLogUtils.logBatchOperationStart(log, taskId, "插入", + (newRecords.size() + 499) / 500, 500); + saveBatch(newRecords, 500); + } + + // 保存失败记录到Redis + if (!failures.isEmpty()) { + try { + String failuresKey = "import:custEnterpriseRelation:" + taskId + ":failures"; + redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS); + ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size()); + } catch (Exception e) { + ImportLogUtils.logRedisError(log, taskId, "保存失败记录", e); + } + } + + ImportResult result = new ImportResult(); + result.setTotalCount(excelList.size()); + result.setSuccessCount(newRecords.size()); + result.setFailureCount(failures.size()); + + // 更新最终状态 + String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS"; + updateImportStatus(taskId, finalStatus, result); + + // 记录导入完成 + long duration = System.currentTimeMillis() - startTime; + ImportLogUtils.logImportComplete(log, taskId, "信贷客户实体关联", + excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration); + } + + @Override + public List getImportFailures(String taskId) { + String key = "import:custEnterpriseRelation:" + taskId + ":failures"; + Object failuresObj = redisTemplate.opsForValue().get(key); + + if (failuresObj == null) { + return Collections.emptyList(); + } + + return JSON.parseArray(JSON.toJSONString(failuresObj), CustEnterpriseRelationImportFailureVO.class); + } + + @Override + public ImportStatusVO getImportStatus(String taskId) { + String key = "import:custEnterpriseRelation:" + taskId; + Boolean hasKey = redisTemplate.hasKey(key); + + if (Boolean.FALSE.equals(hasKey)) { + throw new RuntimeException("任务不存在或已过期"); + } + + Map statusMap = redisTemplate.opsForHash().entries(key); + + ImportStatusVO statusVO = new ImportStatusVO(); + statusVO.setTaskId((String) statusMap.get("taskId")); + statusVO.setStatus((String) statusMap.get("status")); + statusVO.setTotalCount((Integer) statusMap.get("totalCount")); + statusVO.setSuccessCount((Integer) statusMap.get("successCount")); + statusVO.setFailureCount((Integer) statusMap.get("failureCount")); + statusVO.setProgress((Integer) statusMap.get("progress")); + statusVO.setStartTime((Long) statusMap.get("startTime")); + statusVO.setEndTime((Long) statusMap.get("endTime")); + statusVO.setMessage((String) statusMap.get("message")); + + return statusVO; + } + + /** + * 更新导入状态 + */ + private void updateImportStatus(String taskId, String status, ImportResult result) { + String key = "import:custEnterpriseRelation:" + taskId; + Map statusData = new HashMap<>(); + statusData.put("status", status); + statusData.put("successCount", result.getSuccessCount()); + statusData.put("failureCount", result.getFailureCount()); + statusData.put("progress", 100); + statusData.put("endTime", System.currentTimeMillis()); + + if ("SUCCESS".equals(status)) { + statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据"); + } else { + statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "条"); + } + + redisTemplate.opsForHash().putAll(key, statusData); + } + + /** + * 批量查询已存在的person_id + social_credit_code组合 + */ + private Set getExistingCombinations(List excelList) { + List combinations = excelList.stream() + .map(excel -> excel.getPersonId() + "|" + excel.getSocialCreditCode()) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + if (combinations.isEmpty()) { + return Collections.emptySet(); + } + + return new HashSet<>(relationMapper.batchExistsByCombinations(combinations)); + } + + /** + * 批量保存 + */ + private void saveBatch(List list, int batchSize) { + for (int i = 0; i < list.size(); i += batchSize) { + int end = Math.min(i + batchSize, list.size()); + List subList = list.subList(i, end); + relationMapper.insertBatch(subList); + } + } + + /** + * 验证信贷客户实体关联数据 + * 【关键差异】不验证身份证号是否存在于员工表 + */ + private void validateRelationData(CcdiCustEnterpriseRelationAddDTO addDTO) { + // 验证必填字段 + if (StringUtils.isEmpty(addDTO.getPersonId())) { + throw new RuntimeException("身份证号不能为空"); + } + if (StringUtils.isEmpty(addDTO.getSocialCreditCode())) { + throw new RuntimeException("统一社会信用代码不能为空"); + } + if (StringUtils.isEmpty(addDTO.getEnterpriseName())) { + throw new RuntimeException("企业名称不能为空"); + } + + // 验证身份证号格式(18位) + if (!addDTO.getPersonId().matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$")) { + throw new RuntimeException("身份证号格式不正确,必须为18位有效身份证号"); + } + + // 验证统一社会信用代码格式(18位) + if (!addDTO.getSocialCreditCode().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) { + throw new RuntimeException("统一社会信用代码格式不正确,必须为18位有效统一社会信用代码"); + } + + // 验证字段长度 + if (StringUtils.isNotEmpty(addDTO.getRelationPersonPost()) && addDTO.getRelationPersonPost().length() > 100) { + throw new RuntimeException("关联人在企业的职务长度不能超过100个字符"); + } + if (addDTO.getEnterpriseName().length() > 200) { + throw new RuntimeException("企业名称长度不能超过200个字符"); + } + + // 【注意】不验证身份证号是否存在于员工表 + } +} +``` + +--- + +## 七、Controller层实现 + +### 7.1 CcdiCustEnterpriseRelationController.java + +```java +package com.ruoyi.ccdi.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationAddDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationEditDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationQueryDTO; +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CcdiCustEnterpriseRelationVO; +import com.ruoyi.ccdi.domain.vo.CustEnterpriseRelationImportFailureVO; +import com.ruoyi.ccdi.domain.vo.ImportResultVO; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationImportService; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationService; +import com.ruoyi.ccdi.utils.EasyExcelUtil; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDomain; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.page.TableSupport; +import com.ruoyi.common.enums.BusinessType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.List; + +/** + * 信贷客户实体关联信息Controller + */ +@Tag(name = "信贷客户实体关联信息管理") +@RestController +@RequestMapping("/ccdi/custEnterpriseRelation") +public class CcdiCustEnterpriseRelationController extends BaseController { + + @Resource + private ICcdiCustEnterpriseRelationService relationService; + + @Resource + private ICcdiCustEnterpriseRelationImportService relationImportService; + + /** + * 查询信贷客户实体关联列表 + */ + @Operation(summary = "查询信贷客户实体关联列表") + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:list')") + @GetMapping("/list") + public TableDataInfo list(CcdiCustEnterpriseRelationQueryDTO queryDTO) { + PageDomain pageDomain = TableSupport.buildPageRequest(); + Page page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize()); + Page result = relationService.selectRelationPage(page, queryDTO); + return getDataTable(result.getRecords(), result.getTotal()); + } + + /** + * 导出信贷客户实体关联列表 + */ + @Operation(summary = "导出信贷客户实体关联列表") + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:export')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, CcdiCustEnterpriseRelationQueryDTO queryDTO) { + List list = relationService.selectRelationListForExport(queryDTO); + EasyExcelUtil.exportExcel(response, list, CcdiCustEnterpriseRelationExcel.class, "信贷客户实体关联信息"); + } + + /** + * 获取信贷客户实体关联详细信息 + */ + @Operation(summary = "获取信贷客户实体关联详细信息") + @Parameter(name = "id", description = "主键ID", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable Long id) { + return success(relationService.selectRelationById(id)); + } + + /** + * 新增信贷客户实体关联 + */ + @Operation(summary = "新增信贷客户实体关联") + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:add')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody CcdiCustEnterpriseRelationAddDTO addDTO) { + return toAjax(relationService.insertRelation(addDTO)); + } + + /** + * 修改信贷客户实体关联 + */ + @Operation(summary = "修改信贷客户实体关联") + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:edit')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody CcdiCustEnterpriseRelationEditDTO editDTO) { + return toAjax(relationService.updateRelation(editDTO)); + } + + /** + * 删除信贷客户实体关联 + */ + @Operation(summary = "删除信贷客户实体关联") + @Parameter(name = "ids", description = "主键ID数组", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:remove')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(relationService.deleteRelationByIds(ids)); + } + + /** + * 下载导入模板 + */ + @Operation(summary = "下载导入模板") + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) { + EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiCustEnterpriseRelationExcel.class, "信贷客户实体关联信息"); + } + + /** + * 异步导入信贷客户实体关联 + */ + @Operation(summary = "异步导入信贷客户实体关联") + @Parameter(name = "file", description = "导入文件", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:import')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.IMPORT) + @PostMapping("/importData") + public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception { + List list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiCustEnterpriseRelationExcel.class); + + if (list == null || list.isEmpty()) { + return error("至少需要一条数据"); + } + + // 提交异步任务 + String taskId = relationService.importRelation(list); + + // 立即返回 + ImportResultVO result = new ImportResultVO(); + result.setTaskId(taskId); + result.setStatus("PROCESSING"); + result.setMessage("导入任务已提交,正在后台处理"); + + return AjaxResult.success("导入任务已提交,正在后台处理", result); + } + + /** + * 查询导入状态 + */ + @Operation(summary = "查询导入状态") + @Parameter(name = "taskId", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:import')") + @GetMapping("/importStatus/{taskId}") + public AjaxResult getImportStatus(@PathVariable String taskId) { + ImportStatusVO statusVO = relationImportService.getImportStatus(taskId); + return success(statusVO); + } + + /** + * 查询导入失败记录 + */ + @Operation(summary = "查询导入失败记录") + @Parameter(name = "taskId", description = "任务ID", required = true) + @Parameter(name = "pageNum", description = "页码", required = false) + @Parameter(name = "pageSize", description = "每页条数", required = false) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:import')") + @GetMapping("/importFailures/{taskId}") + public TableDataInfo getImportFailures( + @PathVariable String taskId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + + List failures = relationImportService.getImportFailures(taskId); + + // 手动分页 + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, failures.size()); + + if (fromIndex >= failures.size()) { + return getDataTable(new ArrayList<>(), failures.size()); + } + + List pageData = failures.subList(fromIndex, toIndex); + + return getDataTable(pageData, failures.size()); + } +} +``` + +--- + +## 八、与员工实体关系代码对比 + +### 8.1 关键差异总结 + +| 对比项 | 员工实体关系 | 信贷客户实体关联 | +|--------|-------------|-----------------| +| 表名 | ccdi_staff_enterprise_relation | ccdi_cust_enterprise_relation | +| VO中是否有personName | 有(JOIN员工表) | 无(不JOIN) | +| 身份证号验证 | 验证存在于员工表 | 不验证 | +| 员工搜索功能 | 有 | 无 | +| 身份标识默认值 | is_emp_family=1 | is_cust_family=1 | +| Redis key前缀 | import:staffEnterpriseRelation: | import:custEnterpriseRelation: | +| 权限标识 | ccdi:staffEnterpriseRelation:* | cdi:custEnterpriseRelation:* | +| API路径 | /ccdi/staffEnterpriseRelation/* | /ccdi/custEnterpriseRelation/* | + +### 8.2 导入逻辑对比 + +| 步骤 | 员工实体关系 | 信贷客户实体关联 | +|------|-------------|-----------------| +| 1. 验证必填字段 | 相同 | 相同 | +| 2. 验证格式 | 相同 | 相同 | +| 3. 验证身份证号存在 | **验证** | **不验证** | +| 4. 检查组合唯一性 | 相同 | 相同 | +| 5. 设置身份标识 | is_emp_family=1 | is_cust_family=1 | +| 6. 批量插入 | 相同 | 相同 | + +--- + +## 九、实施步骤 + +1. 执行数据库建表SQL +2. 创建Domain层文件(Entity、VO、DTO、Excel) +3. 创建Mapper层文件(Mapper接口、Mapper XML) +4. 创建Service层文件(Service接口、Service实现) +5. 创建Controller层文件 +6. 配置菜单权限(需执行菜单SQL) +7. 编译测试 diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiCustEnterpriseRelationController.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiCustEnterpriseRelationController.java new file mode 100644 index 0000000..8c630d9 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiCustEnterpriseRelationController.java @@ -0,0 +1,200 @@ +package com.ruoyi.ccdi.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationAddDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationEditDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationQueryDTO; +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CcdiCustEnterpriseRelationVO; +import com.ruoyi.ccdi.domain.vo.CustEnterpriseRelationImportFailureVO; +import com.ruoyi.ccdi.domain.vo.ImportResultVO; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationImportService; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationService; +import com.ruoyi.ccdi.utils.EasyExcelUtil; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDomain; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.page.TableSupport; +import com.ruoyi.common.enums.BusinessType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.List; + +/** + * 信贷客户实体关联信息Controller + * + * @author ruoyi + * @date 2026-02-12 + */ +@Tag(name = "信贷客户实体关联信息管理") +@RestController +@RequestMapping("/ccdi/custEnterpriseRelation") +public class CcdiCustEnterpriseRelationController extends BaseController { + + @Resource + private ICcdiCustEnterpriseRelationService relationService; + + @Resource + private ICcdiCustEnterpriseRelationImportService relationImportService; + + /** + * 查询信贷客户实体关联列表 + */ + @Operation(summary = "查询信贷客户实体关联列表") + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:list')") + @GetMapping("/list") + public TableDataInfo list(CcdiCustEnterpriseRelationQueryDTO queryDTO) { + // 使用MyBatis Plus分页 + PageDomain pageDomain = TableSupport.buildPageRequest(); + Page page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize()); + Page result = relationService.selectRelationPage(page, queryDTO); + return getDataTable(result.getRecords(), result.getTotal()); + } + + /** + * 导出信贷客户实体关联列表 + */ + @Operation(summary = "导出信贷客户实体关联列表") + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:export')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, CcdiCustEnterpriseRelationQueryDTO queryDTO) { + List list = relationService.selectRelationListForExport(queryDTO); + EasyExcelUtil.exportExcel(response, list, CcdiCustEnterpriseRelationExcel.class, "信贷客户实体关联信息"); + } + + /** + * 获取信贷客户实体关联详细信息 + */ + @Operation(summary = "获取信贷客户实体关联详细信息") + @Parameter(name = "id", description = "主键ID", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable Long id) { + return success(relationService.selectRelationById(id)); + } + + /** + * 新增信贷客户实体关联 + */ + @Operation(summary = "新增信贷客户实体关联") + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:add')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody CcdiCustEnterpriseRelationAddDTO addDTO) { + return toAjax(relationService.insertRelation(addDTO)); + } + + /** + * 修改信贷客户实体关联 + */ + @Operation(summary = "修改信贷客户实体关联") + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:edit')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody CcdiCustEnterpriseRelationEditDTO editDTO) { + return toAjax(relationService.updateRelation(editDTO)); + } + + /** + * 删除信贷客户实体关联 + */ + @Operation(summary = "删除信贷客户实体关联") + @Parameter(name = "ids", description = "主键ID数组", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:remove')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(relationService.deleteRelationByIds(ids)); + } + + /** + * 下载导入模板 + */ + @Operation(summary = "下载导入模板") + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) { + EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiCustEnterpriseRelationExcel.class, "信贷客户实体关联信息"); + } + + /** + * 异步导入信贷客户实体关联 + */ + @Operation(summary = "异步导入信贷客户实体关联") + @Parameter(name = "file", description = "导入文件", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:import')") + @Log(title = "信贷客户实体关联信息", businessType = BusinessType.IMPORT) + @PostMapping("/importData") + public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception { + List list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiCustEnterpriseRelationExcel.class); + + if (list == null || list.isEmpty()) { + return error("至少需要一条数据"); + } + + // 提交异步任务 + String taskId = relationService.importRelation(list); + + // 立即返回 + ImportResultVO result = new ImportResultVO(); + result.setTaskId(taskId); + result.setStatus("PROCESSING"); + result.setMessage("导入任务已提交,正在后台处理"); + + return AjaxResult.success("导入任务已提交,正在后台处理", result); + } + + /** + * 查询导入状态 + */ + @Operation(summary = "查询导入状态") + @Parameter(name = "taskId", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:import')") + @GetMapping("/importStatus/{taskId}") + public AjaxResult getImportStatus(@PathVariable String taskId) { + ImportStatusVO statusVO = relationImportService.getImportStatus(taskId); + return success(statusVO); + } + + /** + * 查询导入失败记录 + */ + @Operation(summary = "查询导入失败记录") + @Parameter(name = "taskId", description = "任务ID", required = true) + @Parameter(name = "pageNum", description = "页码", required = false) + @Parameter(name = "pageSize", description = "每页条数", required = false) + @PreAuthorize("@ss.hasPermi('ccdi:custEnterpriseRelation:import')") + @GetMapping("/importFailures/{taskId}") + public TableDataInfo getImportFailures( + @PathVariable String taskId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + + List failures = relationImportService.getImportFailures(taskId); + + // 手动分页 + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, failures.size()); + + // 检查 fromIndex 是否超出范围 + if (fromIndex >= failures.size()) { + return getDataTable(new ArrayList<>(), failures.size()); + } + + List pageData = failures.subList(fromIndex, toIndex); + + return getDataTable(pageData, failures.size()); + } +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiCustEnterpriseRelation.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiCustEnterpriseRelation.java new file mode 100644 index 0000000..9dd5d71 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiCustEnterpriseRelation.java @@ -0,0 +1,93 @@ +package com.ruoyi.ccdi.domain; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 信贷客户实体关联信息对象 ccdi_cust_enterprise_relation + * + * @author ruoyi + * @date 2026-02-12 + */ +@Data +@TableName("ccdi_cust_enterprise_relation") +@Schema(description = "信贷客户实体关联信息") +public class CcdiCustEnterpriseRelation implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @TableId(type = IdType.AUTO) + @Schema(description = "主键ID") + private Long id; + + /** 身份证号 */ + @Schema(description = "身份证号") + private String personId; + + /** 关联人在企业的职务 */ + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 统一社会信用代码 */ + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; + + /** 补充说明 */ + @Schema(description = "补充说明") + private String remark; + + /** 数据来源 */ + @Schema(description = "数据来源") + private String dataSource; + + /** 是否为员工(0-否 1-是) */ + @Schema(description = "是否为员工(0-否 1-是)") + private Integer isEmployee; + + /** 是否为员工家属(0-否 1-是) */ + @Schema(description = "是否为员工家属(0-否 1-是)") + private Integer isEmpFamily; + + /** 是否为客户(0-否 1-是) */ + @Schema(description = "是否为客户(0-否 1-是)") + private Integer isCustomer; + + /** 是否为客户家属(0-否 1-是) */ + @Schema(description = "是否为客户家属(0-否 1-是)") + private Integer isCustFamily; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + @Schema(description = "创建时间") + private Date createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(description = "更新时间") + private Date updateTime; + + /** 创建人 */ + @TableField(fill = FieldFill.INSERT) + @Schema(description = "创建人") + private String createdBy; + + /** 更新人 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(description = "更新人") + private String updatedBy; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationAddDTO.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationAddDTO.java new file mode 100644 index 0000000..89c0a67 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationAddDTO.java @@ -0,0 +1,55 @@ +package com.ruoyi.ccdi.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息新增DTO + * + * @author ruoyi + * @date 2026-02-12 + */ +@Data +@Schema(description = "信贷客户实体关联信息新增") +public class CcdiCustEnterpriseRelationAddDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 身份证号 */ + @NotBlank(message = "身份证号不能为空") + @Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$", message = "身份证号格式不正确") + @Schema(description = "身份证号") + private String personId; + + /** 关联人在企业的职务 */ + @Size(max = 100, message = "关联人在企业的职务长度不能超过100个字符") + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 统一社会信用代码 */ + @NotBlank(message = "统一社会信用代码不能为空") + @Pattern(regexp = "^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$", message = "统一社会信用代码格式不正确") + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @NotBlank(message = "企业名称不能为空") + @Size(max = 200, message = "企业名称长度不能超过200个字符") + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; + + /** 补充说明 */ + @Schema(description = "补充说明") + private String remark; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationEditDTO.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationEditDTO.java new file mode 100644 index 0000000..f2add41 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationEditDTO.java @@ -0,0 +1,56 @@ +package com.ruoyi.ccdi.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息编辑DTO + * + * @author ruoyi + * @date 2026-02-12 + */ +@Data +@Schema(description = "信贷客户实体关联信息编辑") +public class CcdiCustEnterpriseRelationEditDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @NotNull(message = "主键ID不能为空") + @Schema(description = "主键ID") + private Long id; + + /** 身份证号(不可修改) */ + @Schema(description = "身份证号(不可修改)") + private String personId; + + /** 关联人在企业的职务 */ + @Size(max = 100, message = "关联人在企业的职务长度不能超过100个字符") + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 统一社会信用代码(不可修改) */ + @Schema(description = "统一社会信用代码(不可修改)") + private String socialCreditCode; + + /** 企业名称 */ + @NotBlank(message = "企业名称不能为空") + @Size(max = 200, message = "企业名称长度不能超过200个字符") + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; + + /** 补充说明 */ + @Schema(description = "补充说明") + private String remark; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationQueryDTO.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationQueryDTO.java new file mode 100644 index 0000000..c8b46cd --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustEnterpriseRelationQueryDTO.java @@ -0,0 +1,37 @@ +package com.ruoyi.ccdi.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息查询DTO + * + * @author ruoyi + * @date 2026-02-12 + */ +@Data +@Schema(description = "信贷客户实体关联信息查询条件") +public class CcdiCustEnterpriseRelationQueryDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 身份证号 */ + @Schema(description = "身份证号") + private String personId; + + /** 统一社会信用代码 */ + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiCustEnterpriseRelationExcel.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiCustEnterpriseRelationExcel.java new file mode 100644 index 0000000..d70cfea --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiCustEnterpriseRelationExcel.java @@ -0,0 +1,57 @@ +package com.ruoyi.ccdi.domain.excel; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import com.ruoyi.common.annotation.Required; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息Excel导入导出对象 + * + * @author ruoyi + * @date 2026-02-12 + */ +@Data +@Schema(description = "信贷客户实体关联信息Excel导入导出对象") +public class CcdiCustEnterpriseRelationExcel implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 身份证号 */ + @ExcelProperty(value = "身份证号", index = 0) + @ColumnWidth(20) + @Required + @Schema(description = "身份证号") + private String personId; + + /** 统一社会信用代码 */ + @ExcelProperty(value = "统一社会信用代码", index = 1) + @ColumnWidth(25) + @Required + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @ExcelProperty(value = "企业名称", index = 2) + @ColumnWidth(30) + @Required + @Schema(description = "企业名称") + private String enterpriseName; + + /** 关联人在企业的职务 */ + @ExcelProperty(value = "关联人在企业的职务", index = 3) + @ColumnWidth(25) + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 补充说明 */ + @ExcelProperty(value = "补充说明", index = 4) + @ColumnWidth(40) + @Schema(description = "补充说明") + private String remark; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiCustEnterpriseRelationVO.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiCustEnterpriseRelationVO.java new file mode 100644 index 0000000..5f468a7 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiCustEnterpriseRelationVO.java @@ -0,0 +1,89 @@ +package com.ruoyi.ccdi.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 信贷客户实体关联信息VO + * + * @author ruoyi + * @date 2026-02-12 + */ +@Data +@Schema(description = "信贷客户实体关联信息") +public class CcdiCustEnterpriseRelationVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @Schema(description = "主键ID") + private Long id; + + /** 身份证号 */ + @Schema(description = "身份证号") + private String personId; + + /** 关联人在企业的职务 */ + @Schema(description = "关联人在企业的职务") + private String relationPersonPost; + + /** 统一社会信用代码 */ + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @Schema(description = "企业名称") + private String enterpriseName; + + /** 状态(0-无效 1-有效) */ + @Schema(description = "状态(0-无效 1-有效)") + private Integer status; + + /** 补充说明 */ + @Schema(description = "补充说明") + private String remark; + + /** 数据来源 */ + @Schema(description = "数据来源") + private String dataSource; + + /** 是否为员工(0-否 1-是) */ + @Schema(description = "是否为员工(0-否 1-是)") + private Integer isEmployee; + + /** 是否为员工家属(0-否 1-是) */ + @Schema(description = "是否为员工家属(0-否 1-是)") + private Integer isEmpFamily; + + /** 是否为客户(0-否 1-是) */ + @Schema(description = "是否为客户(0-否 1-是)") + private Integer isCustomer; + + /** 是否为客户家属(0-否 1-是) */ + @Schema(description = "是否为客户家属(0-否 1-是)") + private Integer isCustFamily; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private Date createTime; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新时间") + private Date updateTime; + + /** 创建人 */ + @Schema(description = "创建人") + private String createdBy; + + /** 更新人 */ + @Schema(description = "更新人") + private String updatedBy; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CustEnterpriseRelationImportFailureVO.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CustEnterpriseRelationImportFailureVO.java new file mode 100644 index 0000000..98357d3 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CustEnterpriseRelationImportFailureVO.java @@ -0,0 +1,37 @@ +package com.ruoyi.ccdi.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 信贷客户实体关联信息导入失败记录VO + * + * @author ruoyi + * @date 2026-02-12 + */ +@Data +@Schema(description = "信贷客户实体关联信息导入失败记录") +public class CustEnterpriseRelationImportFailureVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 身份证号 */ + @Schema(description = "身份证号") + private String personId; + + /** 统一社会信用代码 */ + @Schema(description = "统一社会信用代码") + private String socialCreditCode; + + /** 企业名称 */ + @Schema(description = "企业名称") + private String enterpriseName; + + /** 错误信息 */ + @Schema(description = "错误信息") + private String errorMessage; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiCustEnterpriseRelationMapper.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiCustEnterpriseRelationMapper.java new file mode 100644 index 0000000..42954f4 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiCustEnterpriseRelationMapper.java @@ -0,0 +1,67 @@ +package com.ruoyi.ccdi.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.ccdi.domain.CcdiCustEnterpriseRelation; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationQueryDTO; +import com.ruoyi.ccdi.domain.vo.CcdiCustEnterpriseRelationVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Set; + +/** + * 信贷客户实体关联信息 数据层 + * + * @author ruoyi + * @date 2026-02-12 + */ +@Mapper +public interface CcdiCustEnterpriseRelationMapper extends BaseMapper { + + /** + * 分页查询信贷客户实体关联列表 + * + * @param page 分页对象 + * @param queryDTO 查询条件 + * @return 信贷客户实体关联VO分页结果 + */ + Page selectRelationPage(@Param("page") Page page, + @Param("query") CcdiCustEnterpriseRelationQueryDTO queryDTO); + + /** + * 查询信贷客户实体关联详情 + * + * @param id 主键ID + * @return 信贷客户实体关联VO + */ + CcdiCustEnterpriseRelationVO selectRelationById(@Param("id") Long id); + + /** + * 判断身份证号和统一社会信用代码的组合是否已存在 + * + * @param personId 身份证号 + * @param socialCreditCode 统一社会信用代码 + * @return 存在返回true,否则返回false + */ + boolean existsByPersonIdAndSocialCreditCode(@Param("personId") String personId, + @Param("socialCreditCode") String socialCreditCode); + + /** + * 批量查询已存在的person_id + social_credit_code组合 + * 优化导入性能,一次性查询所有组合 + * + * @param combinations 组合列表,格式为 ["personId1|socialCreditCode1", "personId2|socialCreditCode2", ...] + * @return 已存在的组合集合 + */ + Set batchExistsByCombinations(@Param("combinations") List combinations); + + /** + * 批量插入信贷客户实体关联数据 + * + * @param list 信贷客户实体关联列表 + * @return 插入行数 + */ + int insertBatch(@Param("list") List list); +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustEnterpriseRelationImportService.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustEnterpriseRelationImportService.java new file mode 100644 index 0000000..5aba198 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustEnterpriseRelationImportService.java @@ -0,0 +1,41 @@ +package com.ruoyi.ccdi.service; + +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CustEnterpriseRelationImportFailureVO; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; + +import java.util.List; + +/** + * 信贷客户实体关联信息异步导入服务层 + * + * @author ruoyi + * @date 2026-02-12 + */ +public interface ICcdiCustEnterpriseRelationImportService { + + /** + * 异步导入信贷客户实体关联数据 + * + * @param excelList Excel数据列表 + * @param taskId 任务ID + * @param userName 用户名 + */ + void importRelationAsync(List excelList, String taskId, String userName); + + /** + * 获取导入失败记录 + * + * @param taskId 任务ID + * @return 失败记录列表 + */ + List getImportFailures(String taskId); + + /** + * 查询导入状态 + * + * @param taskId 任务ID + * @return 导入状态信息 + */ + ImportStatusVO getImportStatus(String taskId); +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustEnterpriseRelationService.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustEnterpriseRelationService.java new file mode 100644 index 0000000..0a7b7b2 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustEnterpriseRelationService.java @@ -0,0 +1,84 @@ +package com.ruoyi.ccdi.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationAddDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationEditDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationQueryDTO; +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CcdiCustEnterpriseRelationVO; + +import java.util.List; + +/** + * 信贷客户实体关联信息 服务层 + * + * @author ruoyi + * @date 2026-02-12 + */ +public interface ICcdiCustEnterpriseRelationService { + + /** + * 查询信贷客户实体关联列表 + * + * @param queryDTO 查询条件 + * @return 信贷客户实体关联VO集合 + */ + List selectRelationList(CcdiCustEnterpriseRelationQueryDTO queryDTO); + + /** + * 分页查询信贷客户实体关联列表 + * + * @param page 分页对象 + * @param queryDTO 查询条件 + * @return 信贷客户实体关联VO分页结果 + */ + Page selectRelationPage(Page page, CcdiCustEnterpriseRelationQueryDTO queryDTO); + + /** + * 查询信贷客户实体关联列表(用于导出) + * + * @param queryDTO 查询条件 + * @return 信贷客户实体关联Excel实体集合 + */ + List selectRelationListForExport(CcdiCustEnterpriseRelationQueryDTO queryDTO); + + /** + * 查询信贷客户实体关联详情 + * + * @param id 主键ID + * @return 信贷客户实体关联VO + */ + CcdiCustEnterpriseRelationVO selectRelationById(Long id); + + /** + * 新增信贷客户实体关联 + * + * @param addDTO 新增DTO + * @return 结果 + */ + int insertRelation(CcdiCustEnterpriseRelationAddDTO addDTO); + + /** + * 修改信贷客户实体关联 + * + * @param editDTO 编辑DTO + * @return 结果 + */ + int updateRelation(CcdiCustEnterpriseRelationEditDTO editDTO); + + /** + * 批量删除信贷客户实体关联 + * + * @param ids 需要删除的主键ID + * @return 结果 + */ + int deleteRelationByIds(Long[] ids); + + /** + * 导入信贷客户实体关联数据(异步) + * + * @param excelList Excel实体列表 + * @return 任务ID + */ + String importRelation(List excelList); +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustEnterpriseRelationImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustEnterpriseRelationImportServiceImpl.java new file mode 100644 index 0000000..e03cfdd --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustEnterpriseRelationImportServiceImpl.java @@ -0,0 +1,306 @@ +package com.ruoyi.ccdi.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.ccdi.domain.CcdiCustEnterpriseRelation; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationAddDTO; +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CustEnterpriseRelationImportFailureVO; +import com.ruoyi.ccdi.domain.vo.ImportResult; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; +import com.ruoyi.ccdi.mapper.CcdiCustEnterpriseRelationMapper; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationImportService; +import com.ruoyi.ccdi.utils.ImportLogUtils; +import com.ruoyi.common.utils.StringUtils; +import jakarta.annotation.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +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.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 信贷客户实体关联信息异步导入服务层处理 + * + * @author ruoyi + * @date 2026-02-12 + */ +@Service +@EnableAsync +public class CcdiCustEnterpriseRelationImportServiceImpl implements ICcdiCustEnterpriseRelationImportService { + + private static final Logger log = LoggerFactory.getLogger(CcdiCustEnterpriseRelationImportServiceImpl.class); + + @Resource + private CcdiCustEnterpriseRelationMapper relationMapper; + + @Resource + private RedisTemplate redisTemplate; + + @Override + @Async + @Transactional + public void importRelationAsync(List excelList, String taskId, String userName) { + long startTime = System.currentTimeMillis(); + + // 记录导入开始 + ImportLogUtils.logImportStart(log, taskId, "信贷客户实体关联", excelList.size(), userName); + + List newRecords = new ArrayList<>(); + List failures = new ArrayList<>(); + + // 【关键差异】不需要验证身份证号是否存在 + // 员工实体关系导入会验证身份证号是否存在于员工表,信贷客户实体关联不需要此验证 + + // 批量查询已存在的person_id + social_credit_code组合 + ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的客户企业关系组合", excelList.size()); + Set existingCombinations = getExistingCombinations(excelList); + ImportLogUtils.logBatchQueryComplete(log, taskId, "客户企业关系组合", existingCombinations.size()); + + // 用于跟踪Excel文件内已处理的组合 + Set processedCombinations = new HashSet<>(); + + // 分类数据 + for (int i = 0; i < excelList.size(); i++) { + CcdiCustEnterpriseRelationExcel excel = excelList.get(i); + + try { + // 转换为AddDTO进行验证 + CcdiCustEnterpriseRelationAddDTO addDTO = new CcdiCustEnterpriseRelationAddDTO(); + BeanUtils.copyProperties(excel, addDTO); + + // 验证数据(不验证身份证号是否存在) + validateRelationData(addDTO); + + String combination = excel.getPersonId() + "|" + excel.getSocialCreditCode(); + + CcdiCustEnterpriseRelation relation = new CcdiCustEnterpriseRelation(); + BeanUtils.copyProperties(excel, relation); + + if (existingCombinations.contains(combination)) { + // 组合已存在,直接报错 + throw new RuntimeException(String.format("身份证号[%s]和统一社会信用代码[%s]的组合已存在,请勿重复导入", + excel.getPersonId(), excel.getSocialCreditCode())); + } else if (processedCombinations.contains(combination)) { + // Excel文件内部重复 + throw new RuntimeException(String.format("身份证号[%s]和统一社会信用代码[%s]的组合在导入文件中重复,已跳过此条记录", + excel.getPersonId(), excel.getSocialCreditCode())); + } else { + relation.setCreatedBy(userName); + relation.setUpdatedBy(userName); + + // 设置默认值 + relation.setStatus(1); + // 信贷客户实体关联的身份标识 + relation.setIsEmployee(0); + relation.setIsEmpFamily(0); + relation.setIsCustomer(0); + relation.setIsCustFamily(1); // 信贷客户关联人标识为1 + relation.setDataSource("IMPORT"); + + newRecords.add(relation); + processedCombinations.add(combination); // 标记为已处理 + } + + // 记录进度 + ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(), + newRecords.size(), failures.size()); + + } catch (Exception e) { + CustEnterpriseRelationImportFailureVO failure = new CustEnterpriseRelationImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(e.getMessage()); + failures.add(failure); + + // 记录验证失败日志 + String keyData = String.format("身份证号=%s, 统一社会信用代码=%s, 企业名称=%s", + excel.getPersonId(), excel.getSocialCreditCode(), excel.getEnterpriseName()); + ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData); + } + } + + // 批量插入新数据 + if (!newRecords.isEmpty()) { + ImportLogUtils.logBatchOperationStart(log, taskId, "插入", + (newRecords.size() + 499) / 500, 500); + saveBatch(newRecords, 500); + } + + // 保存失败记录到Redis + if (!failures.isEmpty()) { + try { + String failuresKey = "import:custEnterpriseRelation:" + taskId + ":failures"; + redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS); + ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size()); + } catch (Exception e) { + ImportLogUtils.logRedisError(log, taskId, "保存失败记录", e); + } + } + + ImportResult result = new ImportResult(); + result.setTotalCount(excelList.size()); + result.setSuccessCount(newRecords.size()); + result.setFailureCount(failures.size()); + + // 更新最终状态 + String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS"; + updateImportStatus(taskId, finalStatus, result); + + // 记录导入完成 + long duration = System.currentTimeMillis() - startTime; + ImportLogUtils.logImportComplete(log, taskId, "信贷客户实体关联", + excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration); + } + + /** + * 获取导入失败记录 + * + * @param taskId 任务ID + * @return 失败记录列表 + */ + @Override + public List getImportFailures(String taskId) { + String key = "import:custEnterpriseRelation:" + taskId + ":failures"; + Object failuresObj = redisTemplate.opsForValue().get(key); + + if (failuresObj == null) { + return Collections.emptyList(); + } + + return JSON.parseArray(JSON.toJSONString(failuresObj), CustEnterpriseRelationImportFailureVO.class); + } + + /** + * 查询导入状态 + * + * @param taskId 任务ID + * @return 导入状态信息 + */ + @Override + public ImportStatusVO getImportStatus(String taskId) { + String key = "import:custEnterpriseRelation:" + taskId; + Boolean hasKey = redisTemplate.hasKey(key); + + if (Boolean.FALSE.equals(hasKey)) { + throw new RuntimeException("任务不存在或已过期"); + } + + Map statusMap = redisTemplate.opsForHash().entries(key); + + ImportStatusVO statusVO = new ImportStatusVO(); + statusVO.setTaskId((String) statusMap.get("taskId")); + statusVO.setStatus((String) statusMap.get("status")); + statusVO.setTotalCount((Integer) statusMap.get("totalCount")); + statusVO.setSuccessCount((Integer) statusMap.get("successCount")); + statusVO.setFailureCount((Integer) statusMap.get("failureCount")); + statusVO.setProgress((Integer) statusMap.get("progress")); + statusVO.setStartTime((Long) statusMap.get("startTime")); + statusVO.setEndTime((Long) statusMap.get("endTime")); + statusVO.setMessage((String) statusMap.get("message")); + + return statusVO; + } + + /** + * 更新导入状态 + */ + private void updateImportStatus(String taskId, String status, ImportResult result) { + String key = "import:custEnterpriseRelation:" + taskId; + Map statusData = new HashMap<>(); + statusData.put("status", status); + statusData.put("successCount", result.getSuccessCount()); + statusData.put("failureCount", result.getFailureCount()); + statusData.put("progress", 100); + statusData.put("endTime", System.currentTimeMillis()); + + if ("SUCCESS".equals(status)) { + statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据"); + } else { + statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "条"); + } + + redisTemplate.opsForHash().putAll(key, statusData); + } + + /** + * 批量查询已存在的person_id + social_credit_code组合 + * 性能优化:一次性查询所有组合,避免N+1查询问题 + * + * @param excelList Excel导入数据列表 + * @return 已存在的组合集合 + */ + private Set getExistingCombinations(List excelList) { + // 提取所有的person_id和social_credit_code组合 + List combinations = excelList.stream() + .map(excel -> excel.getPersonId() + "|" + excel.getSocialCreditCode()) + .filter(Objects::nonNull) + .distinct() // 去重 + .collect(Collectors.toList()); + + if (combinations.isEmpty()) { + return Collections.emptySet(); + } + + // 一次性查询所有已存在的组合 + // 优化前:循环调用existsByPersonIdAndSocialCreditCode,N次数据库查询 + // 优化后:批量查询,1次数据库查询 + return new HashSet<>(relationMapper.batchExistsByCombinations(combinations)); + } + + /** + * 批量保存 + */ + private void saveBatch(List list, int batchSize) { + // 使用真正的批量插入,分批次执行以提高性能 + for (int i = 0; i < list.size(); i += batchSize) { + int end = Math.min(i + batchSize, list.size()); + List subList = list.subList(i, end); + relationMapper.insertBatch(subList); + } + } + + /** + * 验证信贷客户实体关联数据 + * 【关键差异】不验证身份证号是否存在于员工表 + * + * @param addDTO 新增DTO + */ + private void validateRelationData(CcdiCustEnterpriseRelationAddDTO addDTO) { + // 验证必填字段 + if (StringUtils.isEmpty(addDTO.getPersonId())) { + throw new RuntimeException("身份证号不能为空"); + } + if (StringUtils.isEmpty(addDTO.getSocialCreditCode())) { + throw new RuntimeException("统一社会信用代码不能为空"); + } + if (StringUtils.isEmpty(addDTO.getEnterpriseName())) { + throw new RuntimeException("企业名称不能为空"); + } + + // 验证身份证号格式(18位) + if (!addDTO.getPersonId().matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$")) { + throw new RuntimeException("身份证号格式不正确,必须为18位有效身份证号"); + } + + // 验证统一社会信用代码格式(18位) + if (!addDTO.getSocialCreditCode().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) { + throw new RuntimeException("统一社会信用代码格式不正确,必须为18位有效统一社会信用代码"); + } + + // 验证字段长度 + if (StringUtils.isNotEmpty(addDTO.getRelationPersonPost()) && addDTO.getRelationPersonPost().length() > 100) { + throw new RuntimeException("关联人在企业的职务长度不能超过100个字符"); + } + if (addDTO.getEnterpriseName().length() > 200) { + throw new RuntimeException("企业名称长度不能超过200个字符"); + } + + // 【注意】不验证身份证号是否存在于员工表 + } +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustEnterpriseRelationServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustEnterpriseRelationServiceImpl.java new file mode 100644 index 0000000..41f2203 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustEnterpriseRelationServiceImpl.java @@ -0,0 +1,228 @@ +package com.ruoyi.ccdi.service.impl; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.ccdi.domain.CcdiCustEnterpriseRelation; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationAddDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationEditDTO; +import com.ruoyi.ccdi.domain.dto.CcdiCustEnterpriseRelationQueryDTO; +import com.ruoyi.ccdi.domain.excel.CcdiCustEnterpriseRelationExcel; +import com.ruoyi.ccdi.domain.vo.CcdiCustEnterpriseRelationVO; +import com.ruoyi.ccdi.mapper.CcdiCustEnterpriseRelationMapper; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationImportService; +import com.ruoyi.ccdi.service.ICcdiCustEnterpriseRelationService; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import jakarta.annotation.Resource; +import org.springframework.beans.BeanUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 信贷客户实体关联信息 服务层处理 + * + * @author ruoyi + * @date 2026-02-12 + */ +@Service +public class CcdiCustEnterpriseRelationServiceImpl implements ICcdiCustEnterpriseRelationService { + + @Resource + private CcdiCustEnterpriseRelationMapper relationMapper; + + @Resource + private ICcdiCustEnterpriseRelationImportService relationImportService; + + @Resource + private RedisTemplate redisTemplate; + + /** + * 查询信贷客户实体关联列表 + * + * @param queryDTO 查询条件 + * @return 信贷客户实体关联VO集合 + */ + @Override + public java.util.List selectRelationList(CcdiCustEnterpriseRelationQueryDTO queryDTO) { + Page page = new Page<>(1, Integer.MAX_VALUE); + Page resultPage = relationMapper.selectRelationPage(page, queryDTO); + return resultPage.getRecords(); + } + + /** + * 分页查询信贷客户实体关联列表 + * + * @param page 分页对象 + * @param queryDTO 查询条件 + * @return 信贷客户实体关联VO分页结果 + */ + @Override + public Page selectRelationPage(Page page, CcdiCustEnterpriseRelationQueryDTO queryDTO) { + return relationMapper.selectRelationPage(page, queryDTO); + } + + /** + * 查询信贷客户实体关联列表(用于导出) + * + * @param queryDTO 查询条件 + * @return 信贷客户实体关联Excel实体集合 + */ + @Override + public java.util.List selectRelationListForExport(CcdiCustEnterpriseRelationQueryDTO queryDTO) { + Page page = new Page<>(1, Integer.MAX_VALUE); + Page resultPage = relationMapper.selectRelationPage(page, queryDTO); + + return resultPage.getRecords().stream().map(vo -> { + CcdiCustEnterpriseRelationExcel excel = new CcdiCustEnterpriseRelationExcel(); + BeanUtils.copyProperties(vo, excel); + return excel; + }).collect(Collectors.toList()); + } + + /** + * 查询信贷客户实体关联详情 + * + * @param id 主键ID + * @return 信贷客户实体关联VO + */ + @Override + public CcdiCustEnterpriseRelationVO selectRelationById(Long id) { + return relationMapper.selectRelationById(id); + } + + /** + * 新增信贷客户实体关联 + * + * @param addDTO 新增DTO + * @return 结果 + */ + @Override + @Transactional + public int insertRelation(CcdiCustEnterpriseRelationAddDTO addDTO) { + // 检查身份证号+统一社会信用代码唯一性 + if (relationMapper.existsByPersonIdAndSocialCreditCode(addDTO.getPersonId(), addDTO.getSocialCreditCode())) { + throw new RuntimeException("该身份证号和统一社会信用代码组合已存在"); + } + + CcdiCustEnterpriseRelation relation = new CcdiCustEnterpriseRelation(); + BeanUtils.copyProperties(addDTO, relation); + + // 设置默认值 + // 新增时强制设置状态为有效 + relation.setStatus(1); + + // 信贷客户实体关联的身份标识默认值 + if (relation.getIsEmployee() == null) { + relation.setIsEmployee(0); + } + if (relation.getIsEmpFamily() == null) { + relation.setIsEmpFamily(0); + } + if (relation.getIsCustomer() == null) { + relation.setIsCustomer(0); + } + if (relation.getIsCustFamily() == null) { + relation.setIsCustFamily(1); // 信贷客户关联人标识为1 + } + if (StringUtils.isEmpty(relation.getDataSource())) { + relation.setDataSource("MANUAL"); + } + + int result = relationMapper.insert(relation); + + return result; + } + + /** + * 修改信贷客户实体关联 + * + * @param editDTO 编辑DTO + * @return 结果 + */ + @Override + @Transactional + public int updateRelation(CcdiCustEnterpriseRelationEditDTO editDTO) { + // 使用LambdaUpdateWrapper只更新非null字段,保护系统字段不被覆盖 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(CcdiCustEnterpriseRelation::getId, editDTO.getId()); + + // 只更新前端可编辑的字段 + updateWrapper.set(editDTO.getRelationPersonPost() != null, CcdiCustEnterpriseRelation::getRelationPersonPost, editDTO.getRelationPersonPost()); + updateWrapper.set(editDTO.getEnterpriseName() != null, CcdiCustEnterpriseRelation::getEnterpriseName, editDTO.getEnterpriseName()); + updateWrapper.set(editDTO.getStatus() != null, CcdiCustEnterpriseRelation::getStatus, editDTO.getStatus()); + updateWrapper.set(editDTO.getRemark() != null, CcdiCustEnterpriseRelation::getRemark, editDTO.getRemark()); + + // 注意:以下字段不可修改 + // - personId(身份证号,业务主键) + // - socialCreditCode(统一社会信用代码,业务主键) + // - dataSource(数据来源,系统字段) + // - isEmployee(是否为员工,系统字段) + // - isEmpFamily(是否为员工家属,系统字段) + // - isCustomer(是否为客户,系统字段) + // - isCustFamily(是否为客户家属,系统字段) + + int result = relationMapper.update(null, updateWrapper); + + return result; + } + + /** + * 批量删除信贷客户实体关联 + * + * @param ids 需要删除的主键ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRelationByIds(Long[] ids) { + return relationMapper.deleteBatchIds(java.util.List.of(ids)); + } + + /** + * 导入信贷客户实体关联数据(异步) + * + * @param excelList Excel实体列表 + * @return 任务ID + */ + @Override + @Transactional + public String importRelation(java.util.List excelList) { + if (StringUtils.isNull(excelList) || excelList.isEmpty()) { + throw new RuntimeException("至少需要一条数据"); + } + + // 生成任务ID + String taskId = UUID.randomUUID().toString(); + long startTime = System.currentTimeMillis(); + + // 获取当前用户名 + String userName = SecurityUtils.getUsername(); + + // 初始化Redis状态 + String statusKey = "import:custEnterpriseRelation:" + taskId; + Map statusData = new HashMap<>(); + statusData.put("taskId", taskId); + statusData.put("status", "PROCESSING"); + statusData.put("totalCount", excelList.size()); + statusData.put("successCount", 0); + statusData.put("failureCount", 0); + statusData.put("progress", 0); + statusData.put("startTime", startTime); + statusData.put("message", "正在处理..."); + + redisTemplate.opsForHash().putAll(statusKey, statusData); + redisTemplate.expire(statusKey, 7, TimeUnit.DAYS); + + // 调用异步导入服务 + relationImportService.importRelationAsync(excelList, taskId, userName); + + return taskId; + } +} diff --git a/ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiCustEnterpriseRelationMapper.xml b/ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiCustEnterpriseRelationMapper.xml new file mode 100644 index 0000000..ac5d42c --- /dev/null +++ b/ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiCustEnterpriseRelationMapper.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO ccdi_cust_enterprise_relation + (person_id, relation_person_post, social_credit_code, enterprise_name, + status, remark, data_source, is_employee, is_emp_family, is_customer, is_cust_family, + created_by, create_time, updated_by, update_time) + VALUES + + (#{item.personId}, #{item.relationPersonPost}, #{item.socialCreditCode}, #{item.enterpriseName}, + #{item.status}, #{item.remark}, #{item.dataSource}, #{item.isEmployee}, #{item.isEmpFamily}, #{item.isCustomer}, #{item.isCustFamily}, + #{item.createdBy}, NOW(), #{item.updatedBy}, NOW()) + + + +