Compare commits
8 Commits
55f6eb9129
...
fc6af5234d
| Author | SHA1 | Date | |
|---|---|---|---|
| fc6af5234d | |||
| 624b51292f | |||
| 6385778e4c | |||
| 60a7906eb3 | |||
| 49118a4418 | |||
| d2d36d75a7 | |||
| bc2a885abf | |||
| 018b085447 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -87,4 +87,6 @@ ruoyi-ui/vue.config.js
|
|||||||
|
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
||||||
tests/
|
tests/
|
||||||
|
|
||||||
|
tongweb_62318.properties
|
||||||
74
AGENTS.md
74
AGENTS.md
@@ -15,21 +15,58 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 协作约定
|
## 高优先级规则
|
||||||
|
|
||||||
- 使用简体中文进行思考和对话
|
- 使用简体中文进行思考和对话
|
||||||
- Git 提交说明使用中文
|
- Git 提交说明必须使用中文
|
||||||
- Git 提交前必须检查暂存区,仅允许包含本次任务相关文件
|
- 忽略 `.DS_Store` 文件,不将其视为本次任务需要处理或提交的有效变更
|
||||||
- 若暂存区存在无关文件,必须先移出暂存或与用户确认,禁止顺带提交
|
- 仅当用户明确声明调用 `using-superpowers` 时才允许启用;未明确声明时按普通流程直接处理需求
|
||||||
- 根据设计文档产出实施计划时,默认输出两份文档:
|
- Git 提交前必须检查暂存区,仅允许包含本次任务相关文件;若存在无关文件,必须先移出暂存或与用户确认
|
||||||
- 后端实施计划放 `docs/plans/backend/`
|
- 每一次改动都需要留下实施文档,记录修改内容、影响范围与验证情况
|
||||||
- 前端实施计划放 `docs/plans/frontend/`
|
- 功能设计同时涉及前端和后端改动时,必须分别输出后端与前端两份实施计划;若仅涉及单侧,则只输出对应实施计划
|
||||||
|
- 新增或修改设计文档、实施计划、实施记录前,必须先确认保存路径是否正确
|
||||||
|
- 前端相关安装、构建、调试、测试命令执行前,必须先通过 `nvm` 切换并确认 Node 版本
|
||||||
|
- 测试结束后,自动关闭测试过程中启动的前后端进程
|
||||||
|
- 重启后端时,必须优先使用 `bin/restart_java_backend.sh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 协作约定
|
||||||
|
|
||||||
|
### 基础协作
|
||||||
|
|
||||||
- 前端开发直接在当前分支进行,不需要额外创建 git worktree
|
- 前端开发直接在当前分支进行,不需要额外创建 git worktree
|
||||||
|
- 给出方案时,必须保持最短路径实现,不允许提供兼容性、补丁性或过度设计的方案
|
||||||
|
- 不允许自行扩展出用户需求之外的兜底、降级或变体方案,避免业务逻辑偏移
|
||||||
|
- 输出方案前必须完成全链路逻辑校验,确保方案逻辑正确、链路闭环
|
||||||
|
|
||||||
|
### Git 与变更管理
|
||||||
|
|
||||||
|
- Git 提交前必须检查暂存区,仅保留本次任务相关文件
|
||||||
|
- 若暂存区存在无关文件,必须先移出暂存或与用户确认,禁止顺带提交
|
||||||
|
- `.DS_Store` 默认忽略,不纳入任务变更范围
|
||||||
|
|
||||||
|
### 文档产出
|
||||||
|
|
||||||
|
- 若需求来自设计文档,默认同时沉淀后端与前端两份实施计划
|
||||||
|
- 功能设计同时涉及前端和后端改动时,实施计划分别放在 `docs/plans/backend/` 与 `docs/plans/frontend/`
|
||||||
|
- 功能修改只涉及前端或只涉及后端时,只输出对应的实施计划
|
||||||
|
- 非前后端架构项目不强制拆分两份实施计划
|
||||||
|
- 每一次改动都需要留下实施文档,实施记录优先放在 `docs/reports/implementation/`
|
||||||
|
- 每次新增或修改设计文档、实施计划、实施记录前,都要先确认保存路径是否正确
|
||||||
|
|
||||||
|
### 测试与运行
|
||||||
|
|
||||||
- 测试结束后,自动关闭测试过程中启动的前后端进程
|
- 测试结束后,自动关闭测试过程中启动的前后端进程
|
||||||
- 重启后端时,必须优先使用 `bin/restart_java_backend.sh`,不要直接手工执行 `java -jar` 替代正式重启流程
|
- 重启后端时,必须优先使用 `bin/restart_java_backend.sh`,不要直接手工执行 `java -jar` 替代正式重启流程
|
||||||
|
- 前端相关安装、构建、调试、测试命令执行前,必须先通过 `nvm` 切换并确认 Node 版本
|
||||||
|
|
||||||
|
### 数据库与编码
|
||||||
|
|
||||||
- 遇到 MCP 数据库操作时,使用项目配置文件中的数据库连接信息
|
- 遇到 MCP 数据库操作时,使用项目配置文件中的数据库连接信息
|
||||||
- 执行包含中文内容的 MySQL SQL 脚本或数据库导入时,禁止直接手写 `mysql -e` 或普通重定向执行;必须优先使用 `bin/mysql_utf8_exec.sh <sql-file>`,确保会话字符集为 `utf8mb4`,避免导入或写入乱码
|
- 执行包含中文内容的 MySQL SQL 脚本或数据库导入时,禁止直接手写 `mysql -e` 或普通重定向执行;必须优先使用 `bin/mysql_utf8_exec.sh <sql-file>`,确保会话字符集为 `utf8mb4`
|
||||||
- 数据库字符集与排序规则统一要求:所有业务表、系统表新增或修改时,必须显式使用 `utf8mb4` 字符集与 `utf8mb4_general_ci` 排序规则;禁止引入 `utf8mb4_0900_ai_ci`、`utf8mb4_unicode_ci` 或其他混用排序规则
|
- 所有业务表、系统表新增或修改时,必须显式使用 `utf8mb4` 字符集与 `utf8mb4_general_ci` 排序规则
|
||||||
|
- 禁止引入 `utf8mb4_0900_ai_ci`、`utf8mb4_unicode_ci` 或其他混用排序规则
|
||||||
- 银行流水打标相关规则与参数编码需要统一使用全大写;新增或修改 `rule_code`、`indicator_code`、`param_code` 时,禁止混用大小写风格
|
- 银行流水打标相关规则与参数编码需要统一使用全大写;新增或修改 `rule_code`、`indicator_code`、`param_code` 时,禁止混用大小写风格
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -63,6 +100,9 @@ mvn clean package -DskipTests
|
|||||||
```bash
|
```bash
|
||||||
cd ruoyi-ui
|
cd ruoyi-ui
|
||||||
|
|
||||||
|
# 使用 nvm 切换到项目所需 Node 版本
|
||||||
|
nvm use
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
npm install --registry=https://registry.npmmirror.com
|
npm install --registry=https://registry.npmmirror.com
|
||||||
|
|
||||||
@@ -166,8 +206,10 @@ return AjaxResult.success(result);
|
|||||||
- 非业务字段如 `create_by`、`create_time` 由后端自动维护
|
- 非业务字段如 `create_by`、`create_time` 由后端自动维护
|
||||||
- 前端表单不要暴露通用审计字段
|
- 前端表单不要暴露通用审计字段
|
||||||
- 新增菜单、字典、初始化数据时,同步补充 SQL 脚本
|
- 新增菜单、字典、初始化数据时,同步补充 SQL 脚本
|
||||||
- 执行数据库脚本或导入数据库前,需确认客户端会话字符集为 `utf8mb4`;涉及中文插入、更新、导入时默认使用 `bin/mysql_utf8_exec.sh`
|
- 执行数据库脚本或导入数据库前,需确认客户端会话字符集为 `utf8mb4`
|
||||||
- 所有系统表和业务表的表级、字符字段级排序规则统一为 `utf8mb4_general_ci`;新增建表 SQL、字段追加 SQL、表结构修复 SQL 必须显式声明,避免因默认排序规则漂移导致联表或条件查询报错
|
- 涉及中文插入、更新、导入时默认使用 `bin/mysql_utf8_exec.sh`
|
||||||
|
- 所有系统表和业务表的表级、字符字段级排序规则统一为 `utf8mb4_general_ci`
|
||||||
|
- 新增建表 SQL、字段追加 SQL、表结构修复 SQL 必须显式声明字符集与排序规则,避免因默认排序规则漂移导致联表或条件查询报错
|
||||||
|
|
||||||
### 前端规范
|
### 前端规范
|
||||||
|
|
||||||
@@ -228,15 +270,10 @@ ccdi/
|
|||||||
### 主要业务代码分布
|
### 主要业务代码分布
|
||||||
|
|
||||||
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/`
|
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/`
|
||||||
- 含 `controller`、`domain`、`mapper`、`service`、`annotation`、`validation` 等目录
|
|
||||||
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/`
|
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/`
|
||||||
- 含 `config`、`controller`、`domain`、`mapper`、`service`
|
|
||||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/`
|
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/`
|
||||||
- 含 `client`、`config`、`constants`、`controller`、`domain/request`、`domain/response`
|
|
||||||
- `ruoyi-ui/src/views/`
|
- `ruoyi-ui/src/views/`
|
||||||
- 当前包含 `ccdi`、`ccdiBaseStaff`、`ccdiProject`、`ccdiPurchaseTransaction`、`ccdiIntermediary`、亲属关系、员工调动、招聘等业务页面
|
|
||||||
- `ruoyi-ui/src/api/ccdi/`
|
- `ruoyi-ui/src/api/ccdi/`
|
||||||
- 放置纪检初核业务 API 封装
|
|
||||||
|
|
||||||
### 添加新后端模块时
|
### 添加新后端模块时
|
||||||
|
|
||||||
@@ -297,6 +334,9 @@ ccdi/
|
|||||||
- 只有历史资料或外部原始材料才放入 `assets/`
|
- 只有历史资料或外部原始材料才放入 `assets/`
|
||||||
- 如果移动了文档,需同步修正文档内引用路径
|
- 如果移动了文档,需同步修正文档内引用路径
|
||||||
- 若需求来自设计文档,默认同时沉淀后端与前端两份实施计划
|
- 若需求来自设计文档,默认同时沉淀后端与前端两份实施计划
|
||||||
|
- 功能设计同时涉及前端和后端改动时,必须分别输出后端与前端两份实施计划;若仅涉及前端或仅涉及后端,则只输出对应实施计划;非前后端架构项目不强制拆分双文档
|
||||||
|
- 每一次改动都需要留下实施文档,记录本次修改内容、影响范围与验证情况,实施记录优先放在 `docs/reports/implementation/`
|
||||||
|
- 每次新增或修改设计文档、实施计划、实施记录前,都要先确认保存路径是否正确
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -307,3 +347,5 @@ ccdi/
|
|||||||
- `docker/backend`、`docker/frontend`、`docker/mock` 分别对应三类运行时镜像
|
- `docker/backend`、`docker/frontend`、`docker/mock` 分别对应三类运行时镜像
|
||||||
- `sql/migration/` 用于增量迁移脚本,新增修复脚本优先按日期或功能命名
|
- `sql/migration/` 用于增量迁移脚本,新增修复脚本优先按日期或功能命名
|
||||||
- 启动前后端或 Mock 服务做验证后,结束测试时要主动停止进程,避免残留占用端口
|
- 启动前后端或 Mock 服务做验证后,结束测试时要主动停止进程,避免残留占用端口
|
||||||
|
- 前端相关安装、构建、调试、测试命令执行前,必须先通过 `nvm` 切换并确认 Node 版本
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package com.ruoyi.info.collection.controller;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.ruoyi.info.collection.domain.dto.*;
|
import com.ruoyi.info.collection.domain.dto.*;
|
||||||
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEntityExcel;
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
|
||||||
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel;
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel;
|
||||||
import com.ruoyi.info.collection.domain.vo.*;
|
import com.ruoyi.info.collection.domain.vo.*;
|
||||||
import com.ruoyi.info.collection.service.ICcdiIntermediaryEntityImportService;
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryEnterpriseRelationImportService;
|
||||||
import com.ruoyi.info.collection.service.ICcdiIntermediaryPersonImportService;
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryPersonImportService;
|
||||||
import com.ruoyi.info.collection.service.ICcdiIntermediaryService;
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryService;
|
||||||
import com.ruoyi.info.collection.utils.EasyExcelUtil;
|
import com.ruoyi.info.collection.utils.EasyExcelUtil;
|
||||||
@@ -46,7 +46,7 @@ public class CcdiIntermediaryController extends BaseController {
|
|||||||
private ICcdiIntermediaryPersonImportService personImportService;
|
private ICcdiIntermediaryPersonImportService personImportService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ICcdiIntermediaryEntityImportService entityImportService;
|
private ICcdiIntermediaryEnterpriseRelationImportService enterpriseRelationImportService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询中介列表
|
* 查询中介列表
|
||||||
@@ -277,10 +277,10 @@ public class CcdiIntermediaryController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* 下载实体中介导入模板
|
* 下载实体中介导入模板
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "下载实体中介导入模板")
|
@Operation(summary = "下载中介实体关联关系导入模板")
|
||||||
@PostMapping("/importEntityTemplate")
|
@PostMapping("/importEnterpriseRelationTemplate")
|
||||||
public void importEntityTemplate(HttpServletResponse response) {
|
public void importEnterpriseRelationTemplate(HttpServletResponse response) {
|
||||||
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiIntermediaryEntityExcel.class, "实体中介信息");
|
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiIntermediaryEnterpriseRelationExcel.class, "中介实体关联关系信息");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -313,20 +313,19 @@ public class CcdiIntermediaryController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* 导入实体中介数据(异步)
|
* 导入实体中介数据(异步)
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "导入实体中介数据")
|
@Operation(summary = "导入中介实体关联关系数据")
|
||||||
@PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')")
|
@PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')")
|
||||||
@Log(title = "实体中介", businessType = BusinessType.IMPORT)
|
@Log(title = "中介实体关联关系", businessType = BusinessType.IMPORT)
|
||||||
@PostMapping("/importEntityData")
|
@PostMapping("/importEnterpriseRelationData")
|
||||||
public AjaxResult importEntityData(MultipartFile file) throws Exception {
|
public AjaxResult importEnterpriseRelationData(MultipartFile file) throws Exception {
|
||||||
List<CcdiIntermediaryEntityExcel> list = EasyExcelUtil.importExcel(
|
List<CcdiIntermediaryEnterpriseRelationExcel> list = EasyExcelUtil.importExcel(
|
||||||
file.getInputStream(), CcdiIntermediaryEntityExcel.class);
|
file.getInputStream(), CcdiIntermediaryEnterpriseRelationExcel.class);
|
||||||
|
|
||||||
if (list == null || list.isEmpty()) {
|
if (list == null || list.isEmpty()) {
|
||||||
return error("至少需要一条数据");
|
return error("至少需要一条数据");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交异步任务
|
String taskId = intermediaryService.importIntermediaryEnterpriseRelation(list);
|
||||||
String taskId = intermediaryService.importIntermediaryEntity(list);
|
|
||||||
|
|
||||||
// 立即返回,不等待后台任务完成
|
// 立即返回,不等待后台任务完成
|
||||||
ImportResultVO result = new ImportResultVO();
|
ImportResultVO result = new ImportResultVO();
|
||||||
@@ -383,12 +382,12 @@ public class CcdiIntermediaryController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* 查询实体中介导入状态
|
* 查询实体中介导入状态
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "查询实体中介导入状态")
|
@Operation(summary = "查询中介实体关联关系导入状态")
|
||||||
@PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')")
|
@PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')")
|
||||||
@GetMapping("/importEntityStatus/{taskId}")
|
@GetMapping("/importEnterpriseRelationStatus/{taskId}")
|
||||||
public AjaxResult getEntityImportStatus(@PathVariable String taskId) {
|
public AjaxResult getEnterpriseRelationImportStatus(@PathVariable String taskId) {
|
||||||
try {
|
try {
|
||||||
ImportStatusVO status = entityImportService.getImportStatus(taskId);
|
ImportStatusVO status = enterpriseRelationImportService.getImportStatus(taskId);
|
||||||
return success(status);
|
return success(status);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return error(e.getMessage());
|
return error(e.getMessage());
|
||||||
@@ -396,18 +395,18 @@ public class CcdiIntermediaryController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询实体中介导入失败记录
|
* 查询中介实体关联关系导入失败记录
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "查询实体中介导入失败记录")
|
@Operation(summary = "查询中介实体关联关系导入失败记录")
|
||||||
@PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')")
|
@PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')")
|
||||||
@GetMapping("/importEntityFailures/{taskId}")
|
@GetMapping("/importEnterpriseRelationFailures/{taskId}")
|
||||||
public TableDataInfo getEntityImportFailures(
|
public TableDataInfo getEnterpriseRelationImportFailures(
|
||||||
@PathVariable String taskId,
|
@PathVariable String taskId,
|
||||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||||
|
|
||||||
List<IntermediaryEntityImportFailureVO> failures =
|
List<IntermediaryEnterpriseRelationImportFailureVO> failures =
|
||||||
entityImportService.getImportFailures(taskId);
|
enterpriseRelationImportService.getImportFailures(taskId);
|
||||||
|
|
||||||
// 手动分页
|
// 手动分页
|
||||||
int fromIndex = (pageNum - 1) * pageSize;
|
int fromIndex = (pageNum - 1) * pageSize;
|
||||||
@@ -418,7 +417,7 @@ public class CcdiIntermediaryController extends BaseController {
|
|||||||
return getDataTable(new ArrayList<>(), failures.size());
|
return getDataTable(new ArrayList<>(), failures.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<IntermediaryEntityImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
|
List<IntermediaryEnterpriseRelationImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
|
||||||
|
|
||||||
return getDataTable(pageData, failures.size());
|
return getDataTable(pageData, failures.size());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public class CcdiBizIntermediary implements Serializable {
|
|||||||
/** 职位 */
|
/** 职位 */
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
/** 关联人员ID */
|
/** 关联中介本人证件号码 */
|
||||||
private String relatedNumId;
|
private String relatedNumId;
|
||||||
|
|
||||||
/** 数据来源,MANUAL:手动录入, SYSTEM:系统同步, IMPORT:批量导入, API:接口获取 */
|
/** 数据来源,MANUAL:手动录入, SYSTEM:系统同步, IMPORT:批量导入, API:接口获取 */
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ public class CcdiIntermediaryPersonAddDTO implements Serializable {
|
|||||||
@Size(max = 100, message = "职位长度不能超过100个字符")
|
@Size(max = 100, message = "职位长度不能超过100个字符")
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
@Schema(description = "关联人员ID")
|
@Schema(description = "关联中介本人证件号码")
|
||||||
@Size(max = 50, message = "关联人员ID长度不能超过50个字符")
|
@Size(max = 50, message = "关联中介本人证件号码长度不能超过50个字符")
|
||||||
private String relatedNumId;
|
private String relatedNumId;
|
||||||
|
|
||||||
@Schema(description = "关联关系")
|
@Schema(description = "关联关系")
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ public class CcdiIntermediaryPersonEditDTO implements Serializable {
|
|||||||
@Size(max = 100, message = "职位长度不能超过100个字符")
|
@Size(max = 100, message = "职位长度不能超过100个字符")
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
@Schema(description = "关联人员ID")
|
@Schema(description = "关联中介本人证件号码")
|
||||||
@Size(max = 50, message = "关联人员ID长度不能超过50个字符")
|
@Size(max = 50, message = "关联中介本人证件号码长度不能超过50个字符")
|
||||||
private String relatedNumId;
|
private String relatedNumId;
|
||||||
|
|
||||||
@Schema(description = "关联关系")
|
@Schema(description = "关联关系")
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ruoyi.info.collection.domain.excel;
|
||||||
|
|
||||||
|
import com.alibaba.excel.annotation.ExcelProperty;
|
||||||
|
import com.alibaba.excel.annotation.write.style.ColumnWidth;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中介实体关联关系导入对象
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CcdiIntermediaryEnterpriseRelationExcel implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 中介本人证件号码 */
|
||||||
|
@ExcelProperty(value = "中介本人证件号码*", index = 0)
|
||||||
|
@ColumnWidth(24)
|
||||||
|
private String ownerPersonId;
|
||||||
|
|
||||||
|
/** 统一社会信用代码 */
|
||||||
|
@ExcelProperty(value = "统一社会信用代码*", index = 1)
|
||||||
|
@ColumnWidth(24)
|
||||||
|
private String socialCreditCode;
|
||||||
|
|
||||||
|
/** 关联人职务 */
|
||||||
|
@ExcelProperty(value = "关联人职务", index = 2)
|
||||||
|
@ColumnWidth(20)
|
||||||
|
private String relationPersonPost;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
@ExcelProperty(value = "备注", index = 3)
|
||||||
|
@ColumnWidth(30)
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ public class CcdiIntermediaryPersonExcel implements Serializable {
|
|||||||
/** 人员子类型 */
|
/** 人员子类型 */
|
||||||
@ExcelProperty(value = "人员子类型", index = 2)
|
@ExcelProperty(value = "人员子类型", index = 2)
|
||||||
@ColumnWidth(15)
|
@ColumnWidth(15)
|
||||||
|
@DictDropdown(dictType = "ccdi_person_sub_type")
|
||||||
private String personSubType;
|
private String personSubType;
|
||||||
|
|
||||||
/** 性别 */
|
/** 性别 */
|
||||||
@@ -83,19 +84,13 @@ public class CcdiIntermediaryPersonExcel implements Serializable {
|
|||||||
@ColumnWidth(15)
|
@ColumnWidth(15)
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
/** 关联人员ID */
|
/** 关联中介本人证件号码 */
|
||||||
@ExcelProperty(value = "关联人员ID", index = 12)
|
@ExcelProperty(value = "关联中介本人证件号码", index = 12)
|
||||||
@ColumnWidth(15)
|
@ColumnWidth(24)
|
||||||
private String relatedNumId;
|
private String relatedNumId;
|
||||||
|
|
||||||
/** 关系类型 */
|
|
||||||
@ExcelProperty(value = "关系类型", index = 13)
|
|
||||||
@ColumnWidth(15)
|
|
||||||
@DictDropdown(dictType = "ccdi_relation_type")
|
|
||||||
private String relationType;
|
|
||||||
|
|
||||||
/** 备注 */
|
/** 备注 */
|
||||||
@ExcelProperty(value = "备注", index = 14)
|
@ExcelProperty(value = "备注", index = 13)
|
||||||
@ColumnWidth(30)
|
@ColumnWidth(30)
|
||||||
private String remark;
|
private String remark;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public class CcdiIntermediaryPersonDetailVO implements Serializable {
|
|||||||
@Schema(description = "职位")
|
@Schema(description = "职位")
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
@Schema(description = "关联人员ID")
|
@Schema(description = "关联中介本人证件号码")
|
||||||
private String relatedNumId;
|
private String relatedNumId;
|
||||||
|
|
||||||
@Schema(description = "关联关系")
|
@Schema(description = "关联关系")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class CcdiIntermediaryRelativeVO implements Serializable {
|
|||||||
@Schema(description = "人员ID")
|
@Schema(description = "人员ID")
|
||||||
private String bizId;
|
private String bizId;
|
||||||
|
|
||||||
@Schema(description = "所属中介ID")
|
@Schema(description = "关联中介本人证件号码")
|
||||||
private String relatedNumId;
|
private String relatedNumId;
|
||||||
|
|
||||||
@Schema(description = "姓名")
|
@Schema(description = "姓名")
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ruoyi.info.collection.domain.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中介实体关联关系导入失败记录
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "中介实体关联关系导入失败记录")
|
||||||
|
public class IntermediaryEnterpriseRelationImportFailureVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "中介本人证件号码")
|
||||||
|
private String ownerPersonId;
|
||||||
|
|
||||||
|
@Schema(description = "统一社会信用代码")
|
||||||
|
private String socialCreditCode;
|
||||||
|
|
||||||
|
@Schema(description = "关联人职务")
|
||||||
|
private String relationPersonPost;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "错误信息")
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
@@ -22,21 +22,45 @@ public class IntermediaryPersonImportFailureVO implements Serializable {
|
|||||||
@Schema(description = "姓名")
|
@Schema(description = "姓名")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@Schema(description = "证件号码")
|
|
||||||
private String personId;
|
|
||||||
|
|
||||||
@Schema(description = "人员类型")
|
@Schema(description = "人员类型")
|
||||||
private String personType;
|
private String personType;
|
||||||
|
|
||||||
|
@Schema(description = "人员子类型")
|
||||||
|
private String personSubType;
|
||||||
|
|
||||||
@Schema(description = "性别")
|
@Schema(description = "性别")
|
||||||
private String gender;
|
private String gender;
|
||||||
|
|
||||||
|
@Schema(description = "证件类型")
|
||||||
|
private String idType;
|
||||||
|
|
||||||
|
@Schema(description = "证件号码")
|
||||||
|
private String personId;
|
||||||
|
|
||||||
@Schema(description = "手机号码")
|
@Schema(description = "手机号码")
|
||||||
private String mobile;
|
private String mobile;
|
||||||
|
|
||||||
|
@Schema(description = "微信号")
|
||||||
|
private String wechatNo;
|
||||||
|
|
||||||
|
@Schema(description = "联系地址")
|
||||||
|
private String contactAddress;
|
||||||
|
|
||||||
@Schema(description = "所在公司")
|
@Schema(description = "所在公司")
|
||||||
private String company;
|
private String company;
|
||||||
|
|
||||||
|
@Schema(description = "企业统一信用码")
|
||||||
|
private String socialCreditCode;
|
||||||
|
|
||||||
|
@Schema(description = "职位")
|
||||||
|
private String position;
|
||||||
|
|
||||||
|
@Schema(description = "关联中介本人证件号码")
|
||||||
|
private String relatedNumId;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
@Schema(description = "错误信息")
|
@Schema(description = "错误信息")
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ import java.util.List;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface CcdiIntermediaryEnterpriseRelationMapper extends BaseMapper<CcdiIntermediaryEnterpriseRelation> {
|
public interface CcdiIntermediaryEnterpriseRelationMapper extends BaseMapper<CcdiIntermediaryEnterpriseRelation> {
|
||||||
|
|
||||||
|
int insertBatch(@Param("list") List<CcdiIntermediaryEnterpriseRelation> list);
|
||||||
|
|
||||||
List<CcdiIntermediaryEnterpriseRelationVO> selectByIntermediaryBizId(@Param("bizId") String bizId);
|
List<CcdiIntermediaryEnterpriseRelationVO> selectByIntermediaryBizId(@Param("bizId") String bizId);
|
||||||
|
|
||||||
CcdiIntermediaryEnterpriseRelationVO selectDetailById(@Param("id") Long id);
|
CcdiIntermediaryEnterpriseRelationVO selectDetailById(@Param("id") Long id);
|
||||||
|
|
||||||
boolean existsByIntermediaryBizIdAndSocialCreditCode(@Param("bizId") String bizId,
|
boolean existsByIntermediaryBizIdAndSocialCreditCode(@Param("bizId") String bizId,
|
||||||
@Param("socialCreditCode") String socialCreditCode);
|
@Param("socialCreditCode") String socialCreditCode);
|
||||||
|
|
||||||
|
List<String> batchExistsByCombinations(@Param("combinations") List<String> combinations);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ruoyi.info.collection.service;
|
||||||
|
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.IntermediaryEnterpriseRelationImportFailureVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中介实体关联关系异步导入服务接口
|
||||||
|
*/
|
||||||
|
public interface ICcdiIntermediaryEnterpriseRelationImportService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步导入中介实体关联关系
|
||||||
|
*
|
||||||
|
* @param excelList Excel数据
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param userName 当前用户名
|
||||||
|
*/
|
||||||
|
void importAsync(List<CcdiIntermediaryEnterpriseRelationExcel> excelList, String taskId, String userName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询导入状态
|
||||||
|
*
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @return 导入状态
|
||||||
|
*/
|
||||||
|
ImportStatusVO getImportStatus(String taskId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询导入失败记录
|
||||||
|
*
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @return 失败记录
|
||||||
|
*/
|
||||||
|
List<IntermediaryEnterpriseRelationImportFailureVO> getImportFailures(String taskId);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import com.ruoyi.info.collection.domain.vo.IntermediaryPersonImportFailureVO;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 个人中介异步导入Service接口
|
* 中介信息异步导入Service接口
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
* @date 2026-02-06
|
* @date 2026-02-06
|
||||||
@@ -15,7 +15,7 @@ import java.util.List;
|
|||||||
public interface ICcdiIntermediaryPersonImportService {
|
public interface ICcdiIntermediaryPersonImportService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步导入个人中介数据
|
* 异步导入中介信息
|
||||||
*
|
*
|
||||||
* @param excelList Excel数据列表
|
* @param excelList Excel数据列表
|
||||||
* @param taskId 任务ID
|
* @param taskId 任务ID
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.ruoyi.info.collection.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.ruoyi.info.collection.domain.dto.*;
|
import com.ruoyi.info.collection.domain.dto.*;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
|
||||||
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEntityExcel;
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEntityExcel;
|
||||||
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel;
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel;
|
||||||
import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEnterpriseRelationVO;
|
import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEnterpriseRelationVO;
|
||||||
@@ -168,7 +169,7 @@ public interface ICcdiIntermediaryService {
|
|||||||
int deleteIntermediaryByIds(String[] ids);
|
int deleteIntermediaryByIds(String[] ids);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验人员ID唯一性
|
* 校验中介本人证件号码唯一性
|
||||||
*
|
*
|
||||||
* @param personId 人员ID
|
* @param personId 人员ID
|
||||||
* @param bizId 排除的人员ID
|
* @param bizId 排除的人员ID
|
||||||
@@ -193,6 +194,14 @@ public interface ICcdiIntermediaryService {
|
|||||||
*/
|
*/
|
||||||
String importIntermediaryPerson(List<CcdiIntermediaryPersonExcel> list);
|
String importIntermediaryPerson(List<CcdiIntermediaryPersonExcel> list);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入中介实体关联关系
|
||||||
|
*
|
||||||
|
* @param list Excel实体列表
|
||||||
|
* @return 任务ID
|
||||||
|
*/
|
||||||
|
String importIntermediaryEnterpriseRelation(List<CcdiIntermediaryEnterpriseRelationExcel> list);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入实体中介数据
|
* 导入实体中介数据
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiCustEnterpriseRelation;
|
||||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation;
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiStaffEnterpriseRelation;
|
||||||
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoAddDTO;
|
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoAddDTO;
|
||||||
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
|
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
|
||||||
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoQueryDTO;
|
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoQueryDTO;
|
||||||
@@ -14,6 +17,9 @@ import com.ruoyi.info.collection.enums.DataSource;
|
|||||||
import com.ruoyi.info.collection.enums.EnterpriseRiskLevel;
|
import com.ruoyi.info.collection.enums.EnterpriseRiskLevel;
|
||||||
import com.ruoyi.info.collection.enums.EnterpriseSource;
|
import com.ruoyi.info.collection.enums.EnterpriseSource;
|
||||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiCustEnterpriseRelationMapper;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
|
||||||
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoImportService;
|
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoImportService;
|
||||||
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoService;
|
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -25,6 +31,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.StringJoiner;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@@ -40,6 +47,15 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
|||||||
@Resource
|
@Resource
|
||||||
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
|
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiStaffEnterpriseRelationMapper staffEnterpriseRelationMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiCustEnterpriseRelationMapper custEnterpriseRelationMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiIntermediaryEnterpriseRelationMapper intermediaryEnterpriseRelationMapper;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ICcdiEnterpriseBaseInfoImportService enterpriseBaseInfoImportService;
|
private ICcdiEnterpriseBaseInfoImportService enterpriseBaseInfoImportService;
|
||||||
|
|
||||||
@@ -96,6 +112,9 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
|||||||
if (socialCreditCodes == null || socialCreditCodes.length == 0) {
|
if (socialCreditCodes == null || socialCreditCodes.length == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
for (String socialCreditCode : socialCreditCodes) {
|
||||||
|
validateDeleteRelations(socialCreditCode);
|
||||||
|
}
|
||||||
return enterpriseBaseInfoMapper.deleteBatchIds(List.of(socialCreditCodes));
|
return enterpriseBaseInfoMapper.deleteBatchIds(List.of(socialCreditCodes));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,4 +198,23 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateDeleteRelations(String socialCreditCode) {
|
||||||
|
StringJoiner relationTypes = new StringJoiner("、");
|
||||||
|
if (staffEnterpriseRelationMapper.selectCount(new LambdaQueryWrapper<CcdiStaffEnterpriseRelation>()
|
||||||
|
.eq(CcdiStaffEnterpriseRelation::getSocialCreditCode, socialCreditCode)) > 0) {
|
||||||
|
relationTypes.add("员工");
|
||||||
|
}
|
||||||
|
if (custEnterpriseRelationMapper.selectCount(new LambdaQueryWrapper<CcdiCustEnterpriseRelation>()
|
||||||
|
.eq(CcdiCustEnterpriseRelation::getSocialCreditCode, socialCreditCode)) > 0) {
|
||||||
|
relationTypes.add("信贷客户");
|
||||||
|
}
|
||||||
|
if (intermediaryEnterpriseRelationMapper.selectCount(new LambdaQueryWrapper<CcdiIntermediaryEnterpriseRelation>()
|
||||||
|
.eq(CcdiIntermediaryEnterpriseRelation::getSocialCreditCode, socialCreditCode)) > 0) {
|
||||||
|
relationTypes.add("中介");
|
||||||
|
}
|
||||||
|
if (relationTypes.length() > 0) {
|
||||||
|
throw new RuntimeException("统一社会信用代码[" + socialCreditCode + "]已关联" + relationTypes + ",删除失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
package com.ruoyi.info.collection.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiBizIntermediary;
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.ImportResult;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.IntermediaryEnterpriseRelationImportFailureVO;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
|
||||||
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryEnterpriseRelationImportService;
|
||||||
|
import com.ruoyi.info.collection.utils.ImportLogUtils;
|
||||||
|
import com.ruoyi.common.utils.IdCardUtil;
|
||||||
|
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.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中介实体关联关系异步导入实现
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@EnableAsync
|
||||||
|
public class CcdiIntermediaryEnterpriseRelationImportServiceImpl implements ICcdiIntermediaryEnterpriseRelationImportService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CcdiIntermediaryEnterpriseRelationImportServiceImpl.class);
|
||||||
|
|
||||||
|
private static final String STATUS_KEY_PREFIX = "import:intermediary-enterprise-relation:";
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiIntermediaryEnterpriseRelationMapper relationMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiBizIntermediaryMapper intermediaryMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Async
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void importAsync(List<CcdiIntermediaryEnterpriseRelationExcel> excelList, String taskId, String userName) {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
ImportLogUtils.logImportStart(log, taskId, "中介实体关联关系", excelList.size(), userName);
|
||||||
|
|
||||||
|
Map<String, String> ownerBizIdByPersonId = getOwnerBizIdByPersonId(excelList);
|
||||||
|
Set<String> existingEnterpriseCodes = getExistingEnterpriseCodes(excelList);
|
||||||
|
Set<String> existingCombinations = getExistingRelationCombinations(ownerBizIdByPersonId, excelList);
|
||||||
|
|
||||||
|
List<CcdiIntermediaryEnterpriseRelation> successRecords = new ArrayList<>();
|
||||||
|
List<IntermediaryEnterpriseRelationImportFailureVO> failures = new ArrayList<>();
|
||||||
|
Set<String> processedCombinations = new HashSet<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < excelList.size(); i++) {
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel excel = excelList.get(i);
|
||||||
|
try {
|
||||||
|
validateExcel(excel);
|
||||||
|
|
||||||
|
String ownerBizId = ownerBizIdByPersonId.get(excel.getOwnerPersonId());
|
||||||
|
if (StringUtils.isEmpty(ownerBizId)) {
|
||||||
|
throw new RuntimeException("中介本人不存在,请先导入或维护中介本人信息");
|
||||||
|
}
|
||||||
|
if (!existingEnterpriseCodes.contains(excel.getSocialCreditCode())) {
|
||||||
|
throw new RuntimeException("统一社会信用代码不存在于系统机构表");
|
||||||
|
}
|
||||||
|
|
||||||
|
String combination = ownerBizId + "|" + excel.getSocialCreditCode();
|
||||||
|
if (existingCombinations.contains(combination)) {
|
||||||
|
throw new RuntimeException("中介实体关联关系已存在,请勿重复导入");
|
||||||
|
}
|
||||||
|
if (!processedCombinations.add(combination)) {
|
||||||
|
throw new RuntimeException("同一中介本人与统一社会信用代码组合在导入文件中重复");
|
||||||
|
}
|
||||||
|
|
||||||
|
CcdiIntermediaryEnterpriseRelation relation = new CcdiIntermediaryEnterpriseRelation();
|
||||||
|
BeanUtils.copyProperties(excel, relation);
|
||||||
|
relation.setIntermediaryBizId(ownerBizId);
|
||||||
|
relation.setCreatedBy(userName);
|
||||||
|
relation.setUpdatedBy(userName);
|
||||||
|
successRecords.add(relation);
|
||||||
|
} catch (Exception e) {
|
||||||
|
failures.add(createFailureVO(excel, e.getMessage()));
|
||||||
|
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(),
|
||||||
|
String.format("中介本人证件号码=%s, 统一社会信用代码=%s", excel.getOwnerPersonId(), excel.getSocialCreditCode()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!successRecords.isEmpty()) {
|
||||||
|
saveBatch(successRecords, 500);
|
||||||
|
}
|
||||||
|
if (!failures.isEmpty()) {
|
||||||
|
redisTemplate.opsForValue().set(failureKey(taskId), failures, 7, TimeUnit.DAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportResult result = new ImportResult();
|
||||||
|
result.setTotalCount(excelList.size());
|
||||||
|
result.setSuccessCount(successRecords.size());
|
||||||
|
result.setFailureCount(failures.size());
|
||||||
|
updateImportStatus(taskId, result);
|
||||||
|
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
ImportLogUtils.logImportComplete(log, taskId, "中介实体关联关系",
|
||||||
|
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImportStatusVO getImportStatus(String taskId) {
|
||||||
|
String key = statusKey(taskId);
|
||||||
|
if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
|
||||||
|
throw new RuntimeException("任务不存在或已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Object, Object> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IntermediaryEnterpriseRelationImportFailureVO> getImportFailures(String taskId) {
|
||||||
|
Object failuresObj = redisTemplate.opsForValue().get(failureKey(taskId));
|
||||||
|
if (failuresObj == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return JSON.parseArray(JSON.toJSONString(failuresObj), IntermediaryEnterpriseRelationImportFailureVO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getOwnerBizIdByPersonId(List<CcdiIntermediaryEnterpriseRelationExcel> excelList) {
|
||||||
|
List<String> ownerPersonIds = excelList.stream()
|
||||||
|
.map(CcdiIntermediaryEnterpriseRelationExcel::getOwnerPersonId)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (ownerPersonIds.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(CcdiBizIntermediary::getPersonSubType, "本人")
|
||||||
|
.in(CcdiBizIntermediary::getPersonId, ownerPersonIds);
|
||||||
|
return intermediaryMapper.selectList(wrapper).stream()
|
||||||
|
.collect(Collectors.toMap(CcdiBizIntermediary::getPersonId, CcdiBizIntermediary::getBizId, (left, right) -> left));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getExistingEnterpriseCodes(List<CcdiIntermediaryEnterpriseRelationExcel> excelList) {
|
||||||
|
List<String> socialCreditCodes = excelList.stream()
|
||||||
|
.map(CcdiIntermediaryEnterpriseRelationExcel::getSocialCreditCode)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (socialCreditCodes.isEmpty()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, socialCreditCodes);
|
||||||
|
return enterpriseBaseInfoMapper.selectList(wrapper).stream()
|
||||||
|
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getExistingRelationCombinations(Map<String, String> ownerBizIdByPersonId,
|
||||||
|
List<CcdiIntermediaryEnterpriseRelationExcel> excelList) {
|
||||||
|
List<String> combinations = excelList.stream()
|
||||||
|
.map(excel -> {
|
||||||
|
String ownerBizId = ownerBizIdByPersonId.get(excel.getOwnerPersonId());
|
||||||
|
if (StringUtils.isEmpty(ownerBizId) || StringUtils.isEmpty(excel.getSocialCreditCode())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ownerBizId + "|" + excel.getSocialCreditCode();
|
||||||
|
})
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (combinations.isEmpty()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
return new HashSet<>(relationMapper.batchExistsByCombinations(combinations));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateExcel(CcdiIntermediaryEnterpriseRelationExcel excel) {
|
||||||
|
if (StringUtils.isEmpty(excel.getOwnerPersonId())) {
|
||||||
|
throw new RuntimeException("中介本人证件号码不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(excel.getSocialCreditCode())) {
|
||||||
|
throw new RuntimeException("统一社会信用代码不能为空");
|
||||||
|
}
|
||||||
|
String ownerPersonIdError = IdCardUtil.getErrorMessage(excel.getOwnerPersonId());
|
||||||
|
if (ownerPersonIdError != null) {
|
||||||
|
throw new RuntimeException("中介本人证件号码" + ownerPersonIdError);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(excel.getRelationPersonPost()) && excel.getRelationPersonPost().length() > 100) {
|
||||||
|
throw new RuntimeException("关联人职务长度不能超过100个字符");
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(excel.getRemark()) && excel.getRemark().length() > 500) {
|
||||||
|
throw new RuntimeException("备注长度不能超过500个字符");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntermediaryEnterpriseRelationImportFailureVO createFailureVO(CcdiIntermediaryEnterpriseRelationExcel excel,
|
||||||
|
String errorMessage) {
|
||||||
|
IntermediaryEnterpriseRelationImportFailureVO failure = new IntermediaryEnterpriseRelationImportFailureVO();
|
||||||
|
BeanUtils.copyProperties(excel, failure);
|
||||||
|
failure.setErrorMessage(errorMessage);
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveBatch(List<CcdiIntermediaryEnterpriseRelation> list, int batchSize) {
|
||||||
|
for (int i = 0; i < list.size(); i += batchSize) {
|
||||||
|
int end = Math.min(i + batchSize, list.size());
|
||||||
|
relationMapper.insertBatch(list.subList(i, end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateImportStatus(String taskId, ImportResult result) {
|
||||||
|
Map<String, Object> statusData = new HashMap<>();
|
||||||
|
statusData.put("status", result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS");
|
||||||
|
statusData.put("successCount", result.getSuccessCount());
|
||||||
|
statusData.put("failureCount", result.getFailureCount());
|
||||||
|
statusData.put("progress", 100);
|
||||||
|
statusData.put("endTime", System.currentTimeMillis());
|
||||||
|
statusData.put("message", result.getFailureCount() == 0
|
||||||
|
? "全部成功!共导入" + result.getTotalCount() + "条数据"
|
||||||
|
: "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "条");
|
||||||
|
redisTemplate.opsForHash().putAll(statusKey(taskId), statusData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String statusKey(String taskId) {
|
||||||
|
return STATUS_KEY_PREFIX + taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String failureKey(String taskId) {
|
||||||
|
return statusKey(taskId) + ":failures";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,15 +22,18 @@ import org.springframework.scheduling.annotation.EnableAsync;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 个人中介异步导入Service实现
|
* 中介信息异步导入实现
|
||||||
*
|
|
||||||
* @author ruoyi
|
|
||||||
* @date 2026-02-06
|
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
@@ -38,6 +41,8 @@ public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediar
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(CcdiIntermediaryPersonImportServiceImpl.class);
|
private static final Logger log = LoggerFactory.getLogger(CcdiIntermediaryPersonImportServiceImpl.class);
|
||||||
|
|
||||||
|
private static final String STATUS_KEY_PREFIX = "import:intermediary:";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private CcdiBizIntermediaryMapper intermediaryMapper;
|
private CcdiBizIntermediaryMapper intermediaryMapper;
|
||||||
|
|
||||||
@@ -47,110 +52,104 @@ public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediar
|
|||||||
@Override
|
@Override
|
||||||
@Async
|
@Async
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
|
public void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList, String taskId, String userName) {
|
||||||
String taskId,
|
|
||||||
String userName) {
|
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
ImportLogUtils.logImportStart(log, taskId, "中介信息", excelList.size(), userName);
|
||||||
|
|
||||||
// 记录导入开始
|
List<CcdiIntermediaryPersonExcel> ownerRows = new ArrayList<>();
|
||||||
ImportLogUtils.logImportStart(log, taskId, "个人中介", excelList.size(), userName);
|
List<CcdiIntermediaryPersonExcel> relativeRows = new ArrayList<>();
|
||||||
|
|
||||||
List<CcdiBizIntermediary> newRecords = new ArrayList<>();
|
|
||||||
List<IntermediaryPersonImportFailureVO> failures = new ArrayList<>();
|
List<IntermediaryPersonImportFailureVO> failures = new ArrayList<>();
|
||||||
|
|
||||||
// 批量查询已存在的证件号
|
|
||||||
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的证件号", excelList.size());
|
|
||||||
Set<String> existingPersonIds = getExistingPersonIds(excelList);
|
|
||||||
ImportLogUtils.logBatchQueryComplete(log, taskId, "证件号", existingPersonIds.size());
|
|
||||||
|
|
||||||
// 用于检测Excel内部的重复ID
|
|
||||||
Set<String> excelProcessedIds = new HashSet<>();
|
|
||||||
|
|
||||||
// 分类数据
|
|
||||||
for (int i = 0; i < excelList.size(); i++) {
|
for (int i = 0; i < excelList.size(); i++) {
|
||||||
CcdiIntermediaryPersonExcel excel = excelList.get(i);
|
CcdiIntermediaryPersonExcel excel = excelList.get(i);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 验证数据
|
validateCommonRow(excel);
|
||||||
validatePersonData(excel, existingPersonIds);
|
if (isOwnerRow(excel)) {
|
||||||
|
validateOwnerRow(excel);
|
||||||
CcdiBizIntermediary intermediary = new CcdiBizIntermediary();
|
ownerRows.add(excel);
|
||||||
BeanUtils.copyProperties(excel, intermediary);
|
|
||||||
|
|
||||||
// 设置数据来源和审计字段
|
|
||||||
intermediary.setDataSource("IMPORT");
|
|
||||||
intermediary.setCreatedBy(userName);
|
|
||||||
intermediary.setUpdatedBy(userName);
|
|
||||||
|
|
||||||
if (existingPersonIds.contains(excel.getPersonId())) {
|
|
||||||
// 证件号码在数据库中已存在,直接报错
|
|
||||||
throw new RuntimeException(String.format("证件号码[%s]已存在,请勿重复导入", excel.getPersonId()));
|
|
||||||
} else if (excelProcessedIds.contains(excel.getPersonId())) {
|
|
||||||
// 证件号码在Excel文件内部重复
|
|
||||||
throw new RuntimeException(String.format("证件号码[%s]在导入文件中重复,已跳过此条记录", excel.getPersonId()));
|
|
||||||
} else {
|
} else {
|
||||||
newRecords.add(intermediary);
|
validateRelativeRow(excel);
|
||||||
excelProcessedIds.add(excel.getPersonId()); // 标记为已处理
|
relativeRows.add(excel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录进度
|
|
||||||
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
|
|
||||||
newRecords.size(), failures.size());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
failures.add(createFailureVO(excel, e.getMessage()));
|
failures.add(createFailureVO(excel, e.getMessage()));
|
||||||
|
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(),
|
||||||
// 记录验证失败日志
|
String.format("姓名=%s, 证件号码=%s", excel.getName(), excel.getPersonId()));
|
||||||
String keyData = String.format("姓名=%s, 证件号码=%s",
|
|
||||||
excel.getName(), excel.getPersonId());
|
|
||||||
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量插入新数据
|
Set<String> existingOwnerPersonIds = getExistingOwnerPersonIds(ownerRows);
|
||||||
if (!newRecords.isEmpty()) {
|
Set<String> existingOwnerRefs = getExistingOwnerRefs(relativeRows);
|
||||||
ImportLogUtils.logBatchOperationStart(log, taskId, "插入",
|
Set<String> existingRelativeCombinations = getExistingRelativeCombinations(relativeRows);
|
||||||
(newRecords.size() + 499) / 500, 500);
|
|
||||||
saveBatch(newRecords, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存失败记录到Redis
|
List<CcdiBizIntermediary> successRecords = new ArrayList<>();
|
||||||
if (!failures.isEmpty()) {
|
Set<String> importedOwnerPersonIds = new HashSet<>();
|
||||||
|
|
||||||
|
for (CcdiIntermediaryPersonExcel ownerExcel : ownerRows) {
|
||||||
try {
|
try {
|
||||||
String failuresKey = "import:intermediary:" + taskId + ":failures";
|
String ownerPersonId = ownerExcel.getPersonId();
|
||||||
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
|
if (existingOwnerPersonIds.contains(ownerPersonId)) {
|
||||||
ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size());
|
throw new RuntimeException(String.format("中介本人证件号码[%s]已存在,请勿重复导入", ownerPersonId));
|
||||||
|
}
|
||||||
|
if (!importedOwnerPersonIds.add(ownerPersonId)) {
|
||||||
|
throw new RuntimeException(String.format("中介本人证件号码[%s]在导入文件中重复", ownerPersonId));
|
||||||
|
}
|
||||||
|
successRecords.add(buildRecord(ownerExcel, userName, null));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ImportLogUtils.logRedisError(log, taskId, "保存失败记录", e);
|
failures.add(createFailureVO(ownerExcel, e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<String> validOwnerRefs = new HashSet<>(existingOwnerRefs);
|
||||||
|
validOwnerRefs.addAll(importedOwnerPersonIds);
|
||||||
|
Set<String> processedRelativeCombinations = new HashSet<>();
|
||||||
|
|
||||||
|
for (CcdiIntermediaryPersonExcel relativeExcel : relativeRows) {
|
||||||
|
try {
|
||||||
|
String ownerPersonId = relativeExcel.getRelatedNumId();
|
||||||
|
String combination = ownerPersonId + "|" + relativeExcel.getPersonId();
|
||||||
|
if (!validOwnerRefs.contains(ownerPersonId)) {
|
||||||
|
throw new RuntimeException(String.format("关联中介本人证件号码[%s]不存在", ownerPersonId));
|
||||||
|
}
|
||||||
|
if (existingRelativeCombinations.contains(combination)) {
|
||||||
|
throw new RuntimeException(String.format("同一中介本人名下证件号码[%s]的亲属已存在,请勿重复导入", relativeExcel.getPersonId()));
|
||||||
|
}
|
||||||
|
if (!processedRelativeCombinations.add(combination)) {
|
||||||
|
throw new RuntimeException(String.format("同一中介本人名下证件号码[%s]的亲属在导入文件中重复", relativeExcel.getPersonId()));
|
||||||
|
}
|
||||||
|
successRecords.add(buildRecord(relativeExcel, userName, ownerPersonId));
|
||||||
|
} catch (Exception e) {
|
||||||
|
failures.add(createFailureVO(relativeExcel, e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!successRecords.isEmpty()) {
|
||||||
|
saveBatch(successRecords, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!failures.isEmpty()) {
|
||||||
|
redisTemplate.opsForValue().set(failureKey(taskId), failures, 7, TimeUnit.DAYS);
|
||||||
|
}
|
||||||
|
|
||||||
ImportResult result = new ImportResult();
|
ImportResult result = new ImportResult();
|
||||||
result.setTotalCount(excelList.size());
|
result.setTotalCount(excelList.size());
|
||||||
result.setSuccessCount(newRecords.size());
|
result.setSuccessCount(successRecords.size());
|
||||||
result.setFailureCount(failures.size());
|
result.setFailureCount(failures.size());
|
||||||
|
updateImportStatus(taskId, result);
|
||||||
|
|
||||||
// 更新最终状态
|
|
||||||
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
|
|
||||||
updateImportStatus(taskId, finalStatus, result);
|
|
||||||
|
|
||||||
// 记录导入完成
|
|
||||||
long duration = System.currentTimeMillis() - startTime;
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
ImportLogUtils.logImportComplete(log, taskId, "个人中介",
|
ImportLogUtils.logImportComplete(log, taskId, "中介信息",
|
||||||
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
|
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ImportStatusVO getImportStatus(String taskId) {
|
public ImportStatusVO getImportStatus(String taskId) {
|
||||||
String key = "import:intermediary:" + taskId;
|
String key = statusKey(taskId);
|
||||||
Boolean hasKey = redisTemplate.hasKey(key);
|
if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
|
||||||
|
|
||||||
if (Boolean.FALSE.equals(hasKey)) {
|
|
||||||
throw new RuntimeException("任务不存在或已过期");
|
throw new RuntimeException("任务不存在或已过期");
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Object, Object> statusMap = redisTemplate.opsForHash().entries(key);
|
Map<Object, Object> statusMap = redisTemplate.opsForHash().entries(key);
|
||||||
|
|
||||||
ImportStatusVO statusVO = new ImportStatusVO();
|
ImportStatusVO statusVO = new ImportStatusVO();
|
||||||
statusVO.setTaskId((String) statusMap.get("taskId"));
|
statusVO.setTaskId((String) statusMap.get("taskId"));
|
||||||
statusVO.setStatus((String) statusMap.get("status"));
|
statusVO.setStatus((String) statusMap.get("status"));
|
||||||
@@ -161,83 +160,120 @@ public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediar
|
|||||||
statusVO.setStartTime((Long) statusMap.get("startTime"));
|
statusVO.setStartTime((Long) statusMap.get("startTime"));
|
||||||
statusVO.setEndTime((Long) statusMap.get("endTime"));
|
statusVO.setEndTime((Long) statusMap.get("endTime"));
|
||||||
statusVO.setMessage((String) statusMap.get("message"));
|
statusVO.setMessage((String) statusMap.get("message"));
|
||||||
|
|
||||||
return statusVO;
|
return statusVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<IntermediaryPersonImportFailureVO> getImportFailures(String taskId) {
|
public List<IntermediaryPersonImportFailureVO> getImportFailures(String taskId) {
|
||||||
String key = "import:intermediary:" + taskId + ":failures";
|
Object failuresObj = redisTemplate.opsForValue().get(failureKey(taskId));
|
||||||
Object failuresObj = redisTemplate.opsForValue().get(key);
|
|
||||||
|
|
||||||
if (failuresObj == null) {
|
if (failuresObj == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parseArray(JSON.toJSONString(failuresObj), IntermediaryPersonImportFailureVO.class);
|
return JSON.parseArray(JSON.toJSONString(failuresObj), IntermediaryPersonImportFailureVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private boolean isOwnerRow(CcdiIntermediaryPersonExcel excel) {
|
||||||
* 批量查询已存在的证件号
|
return "本人".equals(excel.getPersonSubType());
|
||||||
*/
|
}
|
||||||
private Set<String> getExistingPersonIds(List<CcdiIntermediaryPersonExcel> excelList) {
|
|
||||||
List<String> personIds = excelList.stream()
|
|
||||||
.map(CcdiIntermediaryPersonExcel::getPersonId)
|
|
||||||
.filter(StringUtils::isNotEmpty)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (personIds.isEmpty()) {
|
private void validateCommonRow(CcdiIntermediaryPersonExcel excel) {
|
||||||
|
if (StringUtils.isEmpty(excel.getName())) {
|
||||||
|
throw new RuntimeException("姓名不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(excel.getPersonSubType())) {
|
||||||
|
throw new RuntimeException("人员子类型不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(excel.getPersonId())) {
|
||||||
|
throw new RuntimeException("证件号码不能为空");
|
||||||
|
}
|
||||||
|
String idCardError = IdCardUtil.getErrorMessage(excel.getPersonId());
|
||||||
|
if (idCardError != null) {
|
||||||
|
throw new RuntimeException("证件号码" + idCardError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOwnerRow(CcdiIntermediaryPersonExcel excel) {
|
||||||
|
if (StringUtils.isNotEmpty(excel.getRelatedNumId())) {
|
||||||
|
throw new RuntimeException("本人行关联中介本人证件号码必须为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRelativeRow(CcdiIntermediaryPersonExcel excel) {
|
||||||
|
if (StringUtils.isEmpty(excel.getRelatedNumId())) {
|
||||||
|
throw new RuntimeException("亲属行必须填写关联中介本人证件号码");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getExistingOwnerPersonIds(List<CcdiIntermediaryPersonExcel> ownerRows) {
|
||||||
|
List<String> ownerPersonIds = ownerRows.stream()
|
||||||
|
.map(CcdiIntermediaryPersonExcel::getPersonId)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (ownerPersonIds.isEmpty()) {
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.in(CcdiBizIntermediary::getPersonId, personIds);
|
wrapper.eq(CcdiBizIntermediary::getPersonSubType, "本人")
|
||||||
List<CcdiBizIntermediary> existingIntermediaries = intermediaryMapper.selectList(wrapper);
|
.in(CcdiBizIntermediary::getPersonId, ownerPersonIds);
|
||||||
|
return intermediaryMapper.selectList(wrapper).stream()
|
||||||
return existingIntermediaries.stream()
|
.map(CcdiBizIntermediary::getPersonId)
|
||||||
.map(CcdiBizIntermediary::getPersonId)
|
.collect(Collectors.toSet());
|
||||||
.collect(Collectors.toSet());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private Set<String> getExistingOwnerRefs(List<CcdiIntermediaryPersonExcel> relativeRows) {
|
||||||
* 批量保存(使用ON DUPLICATE KEY UPDATE)
|
List<String> ownerRefs = relativeRows.stream()
|
||||||
*/
|
.map(CcdiIntermediaryPersonExcel::getRelatedNumId)
|
||||||
private int saveBatchWithUpsert(List<CcdiBizIntermediary> list, int batchSize) {
|
.filter(StringUtils::isNotEmpty)
|
||||||
int totalCount = 0;
|
.distinct()
|
||||||
for (int i = 0; i < list.size(); i += batchSize) {
|
.collect(Collectors.toList());
|
||||||
int end = Math.min(i + batchSize, list.size());
|
if (ownerRefs.isEmpty()) {
|
||||||
List<CcdiBizIntermediary> subList = list.subList(i, end);
|
|
||||||
int count = intermediaryMapper.importPersonBatch(subList);
|
|
||||||
totalCount += count;
|
|
||||||
}
|
|
||||||
return totalCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从数据库获取已存在的证件号
|
|
||||||
*/
|
|
||||||
private Set<String> getExistingPersonIdsFromDb(List<CcdiBizIntermediary> records) {
|
|
||||||
List<String> personIds = records.stream()
|
|
||||||
.map(CcdiBizIntermediary::getPersonId)
|
|
||||||
.filter(StringUtils::isNotEmpty)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (personIds.isEmpty()) {
|
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.in(CcdiBizIntermediary::getPersonId, personIds);
|
wrapper.eq(CcdiBizIntermediary::getPersonSubType, "本人")
|
||||||
List<CcdiBizIntermediary> existing = intermediaryMapper.selectList(wrapper);
|
.in(CcdiBizIntermediary::getPersonId, ownerRefs);
|
||||||
|
return intermediaryMapper.selectList(wrapper).stream()
|
||||||
return existing.stream()
|
.map(CcdiBizIntermediary::getPersonId)
|
||||||
.map(CcdiBizIntermediary::getPersonId)
|
.collect(Collectors.toSet());
|
||||||
.collect(Collectors.toSet());
|
}
|
||||||
|
|
||||||
|
private Set<String> getExistingRelativeCombinations(List<CcdiIntermediaryPersonExcel> relativeRows) {
|
||||||
|
List<String> ownerRefs = relativeRows.stream()
|
||||||
|
.map(CcdiIntermediaryPersonExcel::getRelatedNumId)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<String> relativePersonIds = relativeRows.stream()
|
||||||
|
.map(CcdiIntermediaryPersonExcel::getPersonId)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (ownerRefs.isEmpty() || relativePersonIds.isEmpty()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.ne(CcdiBizIntermediary::getPersonSubType, "本人")
|
||||||
|
.in(CcdiBizIntermediary::getRelatedNumId, ownerRefs)
|
||||||
|
.in(CcdiBizIntermediary::getPersonId, relativePersonIds);
|
||||||
|
return intermediaryMapper.selectList(wrapper).stream()
|
||||||
|
.map(item -> item.getRelatedNumId() + "|" + item.getPersonId())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiBizIntermediary buildRecord(CcdiIntermediaryPersonExcel excel, String userName, String ownerPersonId) {
|
||||||
|
CcdiBizIntermediary intermediary = new CcdiBizIntermediary();
|
||||||
|
BeanUtils.copyProperties(excel, intermediary);
|
||||||
|
intermediary.setRelatedNumId(ownerPersonId);
|
||||||
|
intermediary.setDataSource("IMPORT");
|
||||||
|
intermediary.setCreatedBy(userName);
|
||||||
|
intermediary.setUpdatedBy(userName);
|
||||||
|
return intermediary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建失败记录VO
|
|
||||||
*/
|
|
||||||
private IntermediaryPersonImportFailureVO createFailureVO(CcdiIntermediaryPersonExcel excel, String errorMsg) {
|
private IntermediaryPersonImportFailureVO createFailureVO(CcdiIntermediaryPersonExcel excel, String errorMsg) {
|
||||||
IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO();
|
IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO();
|
||||||
BeanUtils.copyProperties(excel, failure);
|
BeanUtils.copyProperties(excel, failure);
|
||||||
@@ -245,73 +281,31 @@ public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediar
|
|||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void saveBatch(List<CcdiBizIntermediary> list, int batchSize) {
|
||||||
* 创建失败记录VO(重载方法)
|
|
||||||
*/
|
|
||||||
private IntermediaryPersonImportFailureVO createFailureVO(CcdiBizIntermediary record, String errorMsg) {
|
|
||||||
CcdiIntermediaryPersonExcel excel = new CcdiIntermediaryPersonExcel();
|
|
||||||
BeanUtils.copyProperties(record, excel);
|
|
||||||
return createFailureVO(excel, errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量保存
|
|
||||||
*/
|
|
||||||
private int saveBatch(List<CcdiBizIntermediary> list, int batchSize) {
|
|
||||||
// 使用真正的批量插入,分批次执行以提高性能
|
|
||||||
int totalCount = 0;
|
|
||||||
for (int i = 0; i < list.size(); i += batchSize) {
|
for (int i = 0; i < list.size(); i += batchSize) {
|
||||||
int end = Math.min(i + batchSize, list.size());
|
int end = Math.min(i + batchSize, list.size());
|
||||||
List<CcdiBizIntermediary> subList = list.subList(i, end);
|
intermediaryMapper.insertBatch(list.subList(i, end));
|
||||||
int count = intermediaryMapper.insertBatch(subList);
|
|
||||||
totalCount += count;
|
|
||||||
}
|
}
|
||||||
return totalCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void updateImportStatus(String taskId, ImportResult result) {
|
||||||
* 更新导入状态
|
|
||||||
*/
|
|
||||||
private void updateImportStatus(String taskId, String status, ImportResult result) {
|
|
||||||
String key = "import:intermediary:" + taskId;
|
|
||||||
Map<String, Object> statusData = new HashMap<>();
|
Map<String, Object> statusData = new HashMap<>();
|
||||||
statusData.put("status", status);
|
statusData.put("status", result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS");
|
||||||
statusData.put("successCount", result.getSuccessCount());
|
statusData.put("successCount", result.getSuccessCount());
|
||||||
statusData.put("failureCount", result.getFailureCount());
|
statusData.put("failureCount", result.getFailureCount());
|
||||||
statusData.put("progress", 100);
|
statusData.put("progress", 100);
|
||||||
statusData.put("endTime", System.currentTimeMillis());
|
statusData.put("endTime", System.currentTimeMillis());
|
||||||
|
statusData.put("message", result.getFailureCount() == 0
|
||||||
if ("SUCCESS".equals(status)) {
|
? "全部成功!共导入" + result.getTotalCount() + "条数据"
|
||||||
statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据");
|
: "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "条");
|
||||||
} else {
|
redisTemplate.opsForHash().putAll(statusKey(taskId), statusData);
|
||||||
statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "条");
|
|
||||||
}
|
|
||||||
|
|
||||||
redisTemplate.opsForHash().putAll(key, statusData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String statusKey(String taskId) {
|
||||||
* 验证个人中介数据
|
return STATUS_KEY_PREFIX + taskId;
|
||||||
*
|
}
|
||||||
* @param excel Excel数据
|
|
||||||
* @param existingPersonIds 已存在的证件号集合
|
|
||||||
*/
|
|
||||||
private void validatePersonData(CcdiIntermediaryPersonExcel excel,
|
|
||||||
Set<String> existingPersonIds) {
|
|
||||||
// 验证必填字段:姓名
|
|
||||||
if (StringUtils.isEmpty(excel.getName())) {
|
|
||||||
throw new RuntimeException("姓名不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证必填字段:证件号码
|
private String failureKey(String taskId) {
|
||||||
if (StringUtils.isEmpty(excel.getPersonId())) {
|
return statusKey(taskId) + ":failures";
|
||||||
throw new RuntimeException("证件号码不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证证件号码格式
|
|
||||||
String idCardError = IdCardUtil.getErrorMessage(excel.getPersonId());
|
|
||||||
if (idCardError != null) {
|
|
||||||
throw new RuntimeException("证件号码" + idCardError);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.ruoyi.info.collection.domain.CcdiBizIntermediary;
|
|||||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
||||||
import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation;
|
import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation;
|
||||||
import com.ruoyi.info.collection.domain.dto.*;
|
import com.ruoyi.info.collection.domain.dto.*;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
|
||||||
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEntityExcel;
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEntityExcel;
|
||||||
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel;
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel;
|
||||||
import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEnterpriseRelationVO;
|
import com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEnterpriseRelationVO;
|
||||||
@@ -17,6 +18,7 @@ import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper;
|
|||||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||||
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
|
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
|
||||||
import com.ruoyi.info.collection.mapper.CcdiIntermediaryMapper;
|
import com.ruoyi.info.collection.mapper.CcdiIntermediaryMapper;
|
||||||
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryEnterpriseRelationImportService;
|
||||||
import com.ruoyi.info.collection.service.ICcdiIntermediaryEntityImportService;
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryEntityImportService;
|
||||||
import com.ruoyi.info.collection.service.ICcdiIntermediaryPersonImportService;
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryPersonImportService;
|
||||||
import com.ruoyi.info.collection.service.ICcdiIntermediaryService;
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryService;
|
||||||
@@ -61,6 +63,9 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ICcdiIntermediaryEntityImportService entityImportService;
|
private ICcdiIntermediaryEntityImportService entityImportService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ICcdiIntermediaryEnterpriseRelationImportService enterpriseRelationImportService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private RedisTemplate<String, Object> redisTemplate;
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
@@ -101,8 +106,9 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<CcdiIntermediaryRelativeVO> selectIntermediaryRelativeList(String bizId) {
|
public List<CcdiIntermediaryRelativeVO> selectIntermediaryRelativeList(String bizId) {
|
||||||
|
CcdiBizIntermediary owner = requireIntermediaryPerson(bizId);
|
||||||
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(CcdiBizIntermediary::getRelatedNumId, bizId)
|
wrapper.eq(CcdiBizIntermediary::getRelatedNumId, owner.getPersonId())
|
||||||
.ne(CcdiBizIntermediary::getPersonSubType, "本人")
|
.ne(CcdiBizIntermediary::getPersonSubType, "本人")
|
||||||
.orderByDesc(CcdiBizIntermediary::getCreateTime);
|
.orderByDesc(CcdiBizIntermediary::getCreateTime);
|
||||||
return bizIntermediaryMapper.selectList(wrapper).stream().map(this::buildRelativeVo).toList();
|
return bizIntermediaryMapper.selectList(wrapper).stream().map(this::buildRelativeVo).toList();
|
||||||
@@ -187,8 +193,9 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
BeanUtils.copyProperties(editDTO, person);
|
BeanUtils.copyProperties(editDTO, person);
|
||||||
person.setPersonSubType("本人");
|
person.setPersonSubType("本人");
|
||||||
person.setRelatedNumId(null);
|
person.setRelatedNumId(null);
|
||||||
|
int updated = bizIntermediaryMapper.updateById(person);
|
||||||
return bizIntermediaryMapper.updateById(person);
|
syncRelativeOwnerPersonId(existing.getPersonId(), editDTO.getPersonId());
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -196,13 +203,13 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
public int insertIntermediaryRelative(String bizId, CcdiIntermediaryRelativeAddDTO addDTO) {
|
public int insertIntermediaryRelative(String bizId, CcdiIntermediaryRelativeAddDTO addDTO) {
|
||||||
CcdiBizIntermediary owner = requireIntermediaryPerson(bizId);
|
CcdiBizIntermediary owner = requireIntermediaryPerson(bizId);
|
||||||
validateRelativePersonSubType(addDTO.getPersonSubType());
|
validateRelativePersonSubType(addDTO.getPersonSubType());
|
||||||
if (!checkPersonIdUnique(addDTO.getPersonId(), null)) {
|
if (!checkRelativePersonUnique(owner.getPersonId(), addDTO.getPersonId(), null)) {
|
||||||
throw new RuntimeException("该证件号已存在");
|
throw new RuntimeException("该中介本人下已存在相同证件号亲属");
|
||||||
}
|
}
|
||||||
|
|
||||||
CcdiBizIntermediary relative = new CcdiBizIntermediary();
|
CcdiBizIntermediary relative = new CcdiBizIntermediary();
|
||||||
BeanUtils.copyProperties(addDTO, relative);
|
BeanUtils.copyProperties(addDTO, relative);
|
||||||
relative.setRelatedNumId(owner.getBizId());
|
relative.setRelatedNumId(owner.getPersonId());
|
||||||
relative.setDataSource("MANUAL");
|
relative.setDataSource("MANUAL");
|
||||||
return bizIntermediaryMapper.insert(relative);
|
return bizIntermediaryMapper.insert(relative);
|
||||||
}
|
}
|
||||||
@@ -216,8 +223,8 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
}
|
}
|
||||||
validateRelativePersonSubType(editDTO.getPersonSubType());
|
validateRelativePersonSubType(editDTO.getPersonSubType());
|
||||||
if (StringUtils.isNotEmpty(editDTO.getPersonId())
|
if (StringUtils.isNotEmpty(editDTO.getPersonId())
|
||||||
&& !checkPersonIdUnique(editDTO.getPersonId(), editDTO.getBizId())) {
|
&& !checkRelativePersonUnique(existing.getRelatedNumId(), editDTO.getPersonId(), editDTO.getBizId())) {
|
||||||
throw new RuntimeException("该证件号已存在");
|
throw new RuntimeException("该中介本人下已存在相同证件号亲属");
|
||||||
}
|
}
|
||||||
|
|
||||||
CcdiBizIntermediary relative = new CcdiBizIntermediary();
|
CcdiBizIntermediary relative = new CcdiBizIntermediary();
|
||||||
@@ -334,7 +341,8 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
if (intermediary != null) {
|
if (intermediary != null) {
|
||||||
if (isIntermediaryPerson(intermediary)) {
|
if (isIntermediaryPerson(intermediary)) {
|
||||||
bizIntermediaryMapper.delete(new LambdaQueryWrapper<CcdiBizIntermediary>()
|
bizIntermediaryMapper.delete(new LambdaQueryWrapper<CcdiBizIntermediary>()
|
||||||
.eq(CcdiBizIntermediary::getRelatedNumId, id));
|
.eq(CcdiBizIntermediary::getRelatedNumId, intermediary.getPersonId())
|
||||||
|
.ne(CcdiBizIntermediary::getPersonSubType, "本人"));
|
||||||
enterpriseRelationMapper.delete(new LambdaQueryWrapper<CcdiIntermediaryEnterpriseRelation>()
|
enterpriseRelationMapper.delete(new LambdaQueryWrapper<CcdiIntermediaryEnterpriseRelation>()
|
||||||
.eq(CcdiIntermediaryEnterpriseRelation::getIntermediaryBizId, id));
|
.eq(CcdiIntermediaryEnterpriseRelation::getIntermediaryBizId, id));
|
||||||
}
|
}
|
||||||
@@ -359,7 +367,8 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
@Override
|
@Override
|
||||||
public boolean checkPersonIdUnique(String personId, String bizId) {
|
public boolean checkPersonIdUnique(String personId, String bizId) {
|
||||||
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(CcdiBizIntermediary::getPersonId, personId);
|
wrapper.eq(CcdiBizIntermediary::getPersonId, personId)
|
||||||
|
.eq(CcdiBizIntermediary::getPersonSubType, "本人");
|
||||||
if (StringUtils.isNotEmpty(bizId)) {
|
if (StringUtils.isNotEmpty(bizId)) {
|
||||||
wrapper.ne(CcdiBizIntermediary::getBizId, bizId);
|
wrapper.ne(CcdiBizIntermediary::getBizId, bizId);
|
||||||
}
|
}
|
||||||
@@ -419,6 +428,31 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
return taskId;
|
return taskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public String importIntermediaryEnterpriseRelation(List<CcdiIntermediaryEnterpriseRelationExcel> list) {
|
||||||
|
String taskId = UUID.randomUUID().toString();
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
String statusKey = "import:intermediary-enterprise-relation:" + taskId;
|
||||||
|
Map<String, Object> statusData = new HashMap<>();
|
||||||
|
statusData.put("taskId", taskId);
|
||||||
|
statusData.put("status", "PROCESSING");
|
||||||
|
statusData.put("totalCount", list.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);
|
||||||
|
|
||||||
|
String userName = SecurityUtils.getUsername();
|
||||||
|
enterpriseRelationImportService.importAsync(list, taskId, userName);
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入实体中介数据(异步)
|
* 导入实体中介数据(异步)
|
||||||
*
|
*
|
||||||
@@ -473,6 +507,17 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean checkRelativePersonUnique(String ownerPersonId, String personId, String excludeBizId) {
|
||||||
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(CcdiBizIntermediary::getRelatedNumId, ownerPersonId)
|
||||||
|
.eq(CcdiBizIntermediary::getPersonId, personId)
|
||||||
|
.ne(CcdiBizIntermediary::getPersonSubType, "本人");
|
||||||
|
if (StringUtils.isNotEmpty(excludeBizId)) {
|
||||||
|
wrapper.ne(CcdiBizIntermediary::getBizId, excludeBizId);
|
||||||
|
}
|
||||||
|
return bizIntermediaryMapper.selectCount(wrapper) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
private void validateEnterpriseRelation(String bizId, String socialCreditCode, Long excludeId) {
|
private void validateEnterpriseRelation(String bizId, String socialCreditCode, Long excludeId) {
|
||||||
requireIntermediaryPerson(bizId);
|
requireIntermediaryPerson(bizId);
|
||||||
if (enterpriseBaseInfoMapper.selectById(socialCreditCode) == null) {
|
if (enterpriseBaseInfoMapper.selectById(socialCreditCode) == null) {
|
||||||
@@ -490,6 +535,20 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void syncRelativeOwnerPersonId(String oldOwnerPersonId, String newOwnerPersonId) {
|
||||||
|
if (StringUtils.isEmpty(oldOwnerPersonId)
|
||||||
|
|| StringUtils.isEmpty(newOwnerPersonId)
|
||||||
|
|| oldOwnerPersonId.equals(newOwnerPersonId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CcdiBizIntermediary relative = new CcdiBizIntermediary();
|
||||||
|
relative.setRelatedNumId(newOwnerPersonId);
|
||||||
|
bizIntermediaryMapper.update(relative, new LambdaQueryWrapper<CcdiBizIntermediary>()
|
||||||
|
.eq(CcdiBizIntermediary::getRelatedNumId, oldOwnerPersonId)
|
||||||
|
.ne(CcdiBizIntermediary::getPersonSubType, "本人"));
|
||||||
|
}
|
||||||
|
|
||||||
private CcdiIntermediaryRelativeVO buildRelativeVo(CcdiBizIntermediary relative) {
|
private CcdiIntermediaryRelativeVO buildRelativeVo(CcdiBizIntermediary relative) {
|
||||||
CcdiIntermediaryRelativeVO vo = new CcdiIntermediaryRelativeVO();
|
CcdiIntermediaryRelativeVO vo = new CcdiIntermediaryRelativeVO();
|
||||||
BeanUtils.copyProperties(relative, vo);
|
BeanUtils.copyProperties(relative, vo);
|
||||||
|
|||||||
@@ -4,6 +4,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper">
|
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper">
|
||||||
|
|
||||||
|
<insert id="insertBatch" parameterType="java.util.List">
|
||||||
|
INSERT INTO ccdi_intermediary_enterprise_relation (
|
||||||
|
intermediary_biz_id, social_credit_code, relation_person_post, remark,
|
||||||
|
created_by, updated_by, create_time, update_time
|
||||||
|
) VALUES
|
||||||
|
<foreach collection="list" item="item" separator=",">
|
||||||
|
(
|
||||||
|
#{item.intermediaryBizId}, #{item.socialCreditCode}, #{item.relationPersonPost}, #{item.remark},
|
||||||
|
#{item.createdBy}, #{item.updatedBy}, NOW(), NOW()
|
||||||
|
)
|
||||||
|
</foreach>
|
||||||
|
</insert>
|
||||||
|
|
||||||
<resultMap id="CcdiIntermediaryEnterpriseRelationVOResult"
|
<resultMap id="CcdiIntermediaryEnterpriseRelationVOResult"
|
||||||
type="com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEnterpriseRelationVO">
|
type="com.ruoyi.info.collection.domain.vo.CcdiIntermediaryEnterpriseRelationVO">
|
||||||
<id property="id" column="id"/>
|
<id property="id" column="id"/>
|
||||||
@@ -63,4 +76,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
AND social_credit_code = #{socialCreditCode}
|
AND social_credit_code = #{socialCreditCode}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="batchExistsByCombinations" resultType="string">
|
||||||
|
SELECT CONCAT(intermediary_biz_id, '|', social_credit_code)
|
||||||
|
FROM ccdi_intermediary_enterprise_relation
|
||||||
|
WHERE CONCAT(intermediary_biz_id, '|', social_credit_code) IN
|
||||||
|
<foreach collection="combinations" item="item" open="(" separator="," close=")">
|
||||||
|
#{item}
|
||||||
|
</foreach>
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
child.create_time
|
child.create_time
|
||||||
FROM ccdi_biz_intermediary child
|
FROM ccdi_biz_intermediary child
|
||||||
INNER JOIN ccdi_biz_intermediary parent
|
INNER JOIN ccdi_biz_intermediary parent
|
||||||
ON child.related_num_id COLLATE utf8mb4_general_ci = parent.biz_id COLLATE utf8mb4_general_ci
|
ON child.related_num_id COLLATE utf8mb4_general_ci = parent.person_id COLLATE utf8mb4_general_ci
|
||||||
AND parent.person_sub_type COLLATE utf8mb4_general_ci = '本人' COLLATE utf8mb4_general_ci
|
AND parent.person_sub_type COLLATE utf8mb4_general_ci = '本人' COLLATE utf8mb4_general_ci
|
||||||
WHERE child.person_sub_type IS NOT NULL
|
WHERE child.person_sub_type IS NOT NULL
|
||||||
AND child.person_sub_type COLLATE utf8mb4_general_ci != '本人' COLLATE utf8mb4_general_ci
|
AND child.person_sub_type COLLATE utf8mb4_general_ci != '本人' COLLATE utf8mb4_general_ci
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.ruoyi.info.collection.controller;
|
||||||
|
|
||||||
|
import com.ruoyi.common.constant.HttpStatus;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.ImportResultVO;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.IntermediaryEnterpriseRelationImportFailureVO;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.IntermediaryPersonImportFailureVO;
|
||||||
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryEnterpriseRelationImportService;
|
||||||
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryPersonImportService;
|
||||||
|
import com.ruoyi.info.collection.service.ICcdiIntermediaryService;
|
||||||
|
import com.ruoyi.info.collection.utils.EasyExcelUtil;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CcdiIntermediaryControllerTest {
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CcdiIntermediaryController controller;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ICcdiIntermediaryService intermediaryService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ICcdiIntermediaryPersonImportService personImportService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ICcdiIntermediaryEnterpriseRelationImportService enterpriseRelationImportService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importPersonData_shouldReturnWarnWhenExcelHasNoRows() throws Exception {
|
||||||
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"intermediary-empty.xlsx",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"empty".getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
|
||||||
|
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||||
|
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiIntermediaryPersonExcel.class)))
|
||||||
|
.thenReturn(List.of());
|
||||||
|
|
||||||
|
AjaxResult result = controller.importPersonData(file);
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
|
||||||
|
assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importPersonData_shouldReturnSuccessWhenTaskCreated() throws Exception {
|
||||||
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"intermediary-person.xlsx",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"person".getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
CcdiIntermediaryPersonExcel excel = new CcdiIntermediaryPersonExcel();
|
||||||
|
excel.setPersonId("320101199001010011");
|
||||||
|
when(intermediaryService.importIntermediaryPerson(List.of(excel))).thenReturn("task-person");
|
||||||
|
|
||||||
|
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||||
|
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiIntermediaryPersonExcel.class)))
|
||||||
|
.thenReturn(List.of(excel));
|
||||||
|
|
||||||
|
AjaxResult result = controller.importPersonData(file);
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
|
||||||
|
ImportResultVO data = (ImportResultVO) result.get(AjaxResult.DATA_TAG);
|
||||||
|
assertEquals("task-person", data.getTaskId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importEnterpriseRelationData_shouldReturnSuccessWhenTaskCreated() throws Exception {
|
||||||
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"intermediary-relation.xlsx",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"relation".getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel excel = new CcdiIntermediaryEnterpriseRelationExcel();
|
||||||
|
excel.setOwnerPersonId("320101199001010011");
|
||||||
|
excel.setSocialCreditCode("91330100MA27X12345");
|
||||||
|
when(intermediaryService.importIntermediaryEnterpriseRelation(List.of(excel))).thenReturn("task-relation");
|
||||||
|
|
||||||
|
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||||
|
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiIntermediaryEnterpriseRelationExcel.class)))
|
||||||
|
.thenReturn(List.of(excel));
|
||||||
|
|
||||||
|
AjaxResult result = controller.importEnterpriseRelationData(file);
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
|
||||||
|
ImportResultVO data = (ImportResultVO) result.get(AjaxResult.DATA_TAG);
|
||||||
|
assertEquals("task-relation", data.getTaskId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEnterpriseRelationImportStatus_shouldDelegateToRelationImportService() {
|
||||||
|
ImportStatusVO statusVO = new ImportStatusVO();
|
||||||
|
statusVO.setTaskId("task-status");
|
||||||
|
when(enterpriseRelationImportService.getImportStatus("task-status")).thenReturn(statusVO);
|
||||||
|
|
||||||
|
AjaxResult result = controller.getEnterpriseRelationImportStatus("task-status");
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
|
||||||
|
assertEquals(statusVO, result.get(AjaxResult.DATA_TAG));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEnterpriseRelationImportFailures_shouldReturnPagedRows() {
|
||||||
|
IntermediaryEnterpriseRelationImportFailureVO failure1 = new IntermediaryEnterpriseRelationImportFailureVO();
|
||||||
|
failure1.setOwnerPersonId("A1");
|
||||||
|
IntermediaryEnterpriseRelationImportFailureVO failure2 = new IntermediaryEnterpriseRelationImportFailureVO();
|
||||||
|
failure2.setOwnerPersonId("A2");
|
||||||
|
when(enterpriseRelationImportService.getImportFailures("task-failures")).thenReturn(List.of(failure1, failure2));
|
||||||
|
|
||||||
|
TableDataInfo result = controller.getEnterpriseRelationImportFailures("task-failures", 2, 1);
|
||||||
|
|
||||||
|
assertEquals(2, result.getTotal());
|
||||||
|
assertEquals(1, result.getRows().size());
|
||||||
|
assertEquals("A2", ((IntermediaryEnterpriseRelationImportFailureVO) result.getRows().get(0)).getOwnerPersonId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getPersonImportFailures_shouldReturnPagedRows() {
|
||||||
|
IntermediaryPersonImportFailureVO failure1 = new IntermediaryPersonImportFailureVO();
|
||||||
|
failure1.setPersonId("A1");
|
||||||
|
IntermediaryPersonImportFailureVO failure2 = new IntermediaryPersonImportFailureVO();
|
||||||
|
failure2.setPersonId("A2");
|
||||||
|
when(personImportService.getImportFailures("task-person-failures")).thenReturn(List.of(failure1, failure2));
|
||||||
|
|
||||||
|
TableDataInfo result = controller.getPersonImportFailures("task-person-failures", 2, 1);
|
||||||
|
|
||||||
|
assertEquals(2, result.getTotal());
|
||||||
|
assertEquals(1, result.getRows().size());
|
||||||
|
assertEquals("A2", ((IntermediaryPersonImportFailureVO) result.getRows().get(0)).getPersonId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importEnterpriseRelationTemplate_shouldUseRelationTemplateName() {
|
||||||
|
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||||
|
controller.importEnterpriseRelationTemplate(null);
|
||||||
|
|
||||||
|
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(
|
||||||
|
null,
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel.class,
|
||||||
|
"中介实体关联关系信息"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.ruoyi.info.collection.service;
|
||||||
|
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiBizIntermediary;
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryEnterpriseRelationExcel;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.IntermediaryEnterpriseRelationImportFailureVO;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
|
||||||
|
import com.ruoyi.info.collection.service.impl.CcdiIntermediaryEnterpriseRelationImportServiceImpl;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.data.redis.core.HashOperations;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.ValueOperations;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CcdiIntermediaryEnterpriseRelationImportServiceImplTest {
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CcdiIntermediaryEnterpriseRelationImportServiceImpl service;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CcdiIntermediaryEnterpriseRelationMapper relationMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CcdiBizIntermediaryMapper intermediaryMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HashOperations<String, Object, Object> hashOperations;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ValueOperations<String, Object> valueOperations;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importEnterpriseRelationAsync_shouldFailWhenOwnerPersonIdDoesNotExist() {
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel excel = buildExcel("320101199001010014", "91330100MA27X12345");
|
||||||
|
prepareFailureRedisMocks();
|
||||||
|
when(intermediaryMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
when(enterpriseBaseInfoMapper.selectList(any())).thenReturn(List.of(enterprise("91330100MA27X12345")));
|
||||||
|
|
||||||
|
service.importAsync(List.of(excel), "task-owner-miss", "tester");
|
||||||
|
|
||||||
|
verify(relationMapper, never()).insertBatch(any());
|
||||||
|
IntermediaryEnterpriseRelationImportFailureVO failure =
|
||||||
|
firstFailure("import:intermediary-enterprise-relation:task-owner-miss:failures");
|
||||||
|
assertEquals("320101199001010014", failure.getOwnerPersonId());
|
||||||
|
assertTrue(failure.getErrorMessage().contains("中介本人不存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importEnterpriseRelationAsync_shouldFailWhenEnterpriseDoesNotExist() {
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel excel = buildExcel("320101199001010014", "91330100MA27X12345");
|
||||||
|
prepareFailureRedisMocks();
|
||||||
|
when(intermediaryMapper.selectList(any())).thenReturn(List.of(owner("owner-biz", "320101199001010014")));
|
||||||
|
when(enterpriseBaseInfoMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
when(relationMapper.batchExistsByCombinations(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
service.importAsync(List.of(excel), "task-ent-miss", "tester");
|
||||||
|
|
||||||
|
verify(relationMapper, never()).insertBatch(any());
|
||||||
|
IntermediaryEnterpriseRelationImportFailureVO failure =
|
||||||
|
firstFailure("import:intermediary-enterprise-relation:task-ent-miss:failures");
|
||||||
|
assertEquals("91330100MA27X12345", failure.getSocialCreditCode());
|
||||||
|
assertTrue(failure.getErrorMessage().contains("机构表"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importEnterpriseRelationAsync_shouldRejectFileDuplicateAndDbDuplicate() {
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel duplicateInDb = buildExcel("320101199001010014", "91330100MA27X12345");
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel duplicateInFile1 = buildExcel("320101199003030035", "91330100MA27X12346");
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel duplicateInFile2 = buildExcel("320101199003030035", "91330100MA27X12346");
|
||||||
|
prepareFailureRedisMocks();
|
||||||
|
when(intermediaryMapper.selectList(any())).thenReturn(List.of(
|
||||||
|
owner("owner-biz-1", "320101199001010014"),
|
||||||
|
owner("owner-biz-2", "320101199003030035")
|
||||||
|
));
|
||||||
|
when(enterpriseBaseInfoMapper.selectList(any())).thenReturn(List.of(
|
||||||
|
enterprise("91330100MA27X12345"),
|
||||||
|
enterprise("91330100MA27X12346")
|
||||||
|
));
|
||||||
|
when(relationMapper.batchExistsByCombinations(any())).thenReturn(List.of("owner-biz-1|91330100MA27X12345"));
|
||||||
|
|
||||||
|
service.importAsync(List.of(duplicateInDb, duplicateInFile1, duplicateInFile2), "task-duplicate", "tester");
|
||||||
|
|
||||||
|
ArgumentCaptor<List<CcdiIntermediaryEnterpriseRelation>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(relationMapper).insertBatch(captor.capture());
|
||||||
|
assertEquals(1, captor.getValue().size());
|
||||||
|
assertEquals("owner-biz-2", captor.getValue().get(0).getIntermediaryBizId());
|
||||||
|
IntermediaryEnterpriseRelationImportFailureVO failure =
|
||||||
|
firstFailure("import:intermediary-enterprise-relation:task-duplicate:failures");
|
||||||
|
assertTrue(failure.getErrorMessage().contains("重复") || failure.getErrorMessage().contains("已存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importEnterpriseRelationAsync_shouldImportSuccessWhenOwnerAndEnterpriseExist() {
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel excel = buildExcel("320101199001010014", "91330100MA27X12345");
|
||||||
|
prepareStatusRedisMock();
|
||||||
|
when(intermediaryMapper.selectList(any())).thenReturn(List.of(owner("owner-biz", "320101199001010014")));
|
||||||
|
when(enterpriseBaseInfoMapper.selectList(any())).thenReturn(List.of(enterprise("91330100MA27X12345")));
|
||||||
|
when(relationMapper.batchExistsByCombinations(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
service.importAsync(List.of(excel), "task-success", "tester");
|
||||||
|
|
||||||
|
ArgumentCaptor<List<CcdiIntermediaryEnterpriseRelation>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(relationMapper).insertBatch(captor.capture());
|
||||||
|
assertEquals(1, captor.getValue().size());
|
||||||
|
assertEquals("owner-biz", captor.getValue().get(0).getIntermediaryBizId());
|
||||||
|
verify(valueOperations, never()).set(any(), any(), any(Long.class), any(TimeUnit.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareStatusRedisMock() {
|
||||||
|
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareFailureRedisMocks() {
|
||||||
|
prepareStatusRedisMock();
|
||||||
|
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntermediaryEnterpriseRelationImportFailureVO firstFailure(String key) {
|
||||||
|
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
|
||||||
|
verify(valueOperations).set(org.mockito.ArgumentMatchers.eq(key), failureCaptor.capture(),
|
||||||
|
org.mockito.ArgumentMatchers.eq(7L), org.mockito.ArgumentMatchers.eq(TimeUnit.DAYS));
|
||||||
|
return (IntermediaryEnterpriseRelationImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiIntermediaryEnterpriseRelationExcel buildExcel(String ownerPersonId, String socialCreditCode) {
|
||||||
|
CcdiIntermediaryEnterpriseRelationExcel excel = new CcdiIntermediaryEnterpriseRelationExcel();
|
||||||
|
excel.setOwnerPersonId(ownerPersonId);
|
||||||
|
excel.setSocialCreditCode(socialCreditCode);
|
||||||
|
excel.setRelationPersonPost("董事");
|
||||||
|
excel.setRemark("备注");
|
||||||
|
return excel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiBizIntermediary owner(String bizId, String personId) {
|
||||||
|
CcdiBizIntermediary owner = new CcdiBizIntermediary();
|
||||||
|
owner.setBizId(bizId);
|
||||||
|
owner.setPersonId(personId);
|
||||||
|
owner.setPersonSubType("本人");
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiEnterpriseBaseInfo enterprise(String socialCreditCode) {
|
||||||
|
CcdiEnterpriseBaseInfo enterprise = new CcdiEnterpriseBaseInfo();
|
||||||
|
enterprise.setSocialCreditCode(socialCreditCode);
|
||||||
|
enterprise.setEnterpriseName("机构" + socialCreditCode.substring(socialCreditCode.length() - 2));
|
||||||
|
return enterprise;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package com.ruoyi.info.collection.service;
|
||||||
|
|
||||||
|
import com.ruoyi.common.annotation.DictDropdown;
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiBizIntermediary;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiIntermediaryPersonExcel;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.IntermediaryPersonImportFailureVO;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper;
|
||||||
|
import com.ruoyi.info.collection.service.impl.CcdiIntermediaryPersonImportServiceImpl;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.data.redis.core.HashOperations;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.ValueOperations;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CcdiIntermediaryPersonImportServiceImplTest {
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CcdiIntermediaryPersonImportServiceImpl service;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CcdiBizIntermediaryMapper intermediaryMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HashOperations<String, Object, Object> hashOperations;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ValueOperations<String, Object> valueOperations;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void intermediaryPersonExcel_shouldUsePersonSubTypeDropdownAndDropRelationType() throws Exception {
|
||||||
|
Field personSubTypeField = CcdiIntermediaryPersonExcel.class.getDeclaredField("personSubType");
|
||||||
|
DictDropdown dictDropdown = personSubTypeField.getAnnotation(DictDropdown.class);
|
||||||
|
|
||||||
|
assertNotNull(dictDropdown);
|
||||||
|
assertEquals("ccdi_person_sub_type", dictDropdown.dictType());
|
||||||
|
assertThrows(NoSuchFieldException.class, () -> CcdiIntermediaryPersonExcel.class.getDeclaredField("relationType"));
|
||||||
|
assertThrows(NoSuchFieldException.class, () -> IntermediaryPersonImportFailureVO.class.getDeclaredField("relationType"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importPersonAsync_shouldFailWhenOwnerRowContainsOwnerPersonIdReference() {
|
||||||
|
CcdiIntermediaryPersonExcel owner = buildOwnerExcel("320101199001010014");
|
||||||
|
owner.setRelatedNumId("320101199105050053");
|
||||||
|
prepareFailureRedisMocks();
|
||||||
|
|
||||||
|
service.importPersonAsync(List.of(owner), "task-owner", "tester");
|
||||||
|
|
||||||
|
verify(intermediaryMapper, never()).insertBatch(any());
|
||||||
|
IntermediaryPersonImportFailureVO failure = firstFailure("import:intermediary:task-owner:failures");
|
||||||
|
assertEquals("320101199105050053", failure.getRelatedNumId());
|
||||||
|
assertTrue(failure.getErrorMessage().contains("本人"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importPersonAsync_shouldFailWhenRelativeRowMissesOwnerPersonId() {
|
||||||
|
CcdiIntermediaryPersonExcel relative = buildRelativeExcel("320101199201010027", "配偶", null);
|
||||||
|
prepareFailureRedisMocks();
|
||||||
|
|
||||||
|
service.importPersonAsync(List.of(relative), "task-relative", "tester");
|
||||||
|
|
||||||
|
verify(intermediaryMapper, never()).insertBatch(any());
|
||||||
|
IntermediaryPersonImportFailureVO failure = firstFailure("import:intermediary:task-relative:failures");
|
||||||
|
assertEquals("320101199201010027", failure.getPersonId());
|
||||||
|
assertTrue(failure.getErrorMessage().contains("关联中介本人证件号码"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importPersonAsync_shouldAllowRelativeReferencingSuccessfulOwnerInSameFile() {
|
||||||
|
CcdiIntermediaryPersonExcel owner = buildOwnerExcel("320101199001010014");
|
||||||
|
CcdiIntermediaryPersonExcel relative = buildRelativeExcel("320101199201010027", "配偶", "320101199001010014");
|
||||||
|
prepareStatusRedisMock();
|
||||||
|
when(intermediaryMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
service.importPersonAsync(List.of(owner, relative), "task-mixed", "tester");
|
||||||
|
|
||||||
|
ArgumentCaptor<List<CcdiBizIntermediary>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(intermediaryMapper).insertBatch(captor.capture());
|
||||||
|
assertEquals(2, captor.getValue().size());
|
||||||
|
assertEquals("本人", captor.getValue().get(0).getPersonSubType());
|
||||||
|
assertEquals("320101199001010014", captor.getValue().get(1).getRelatedNumId());
|
||||||
|
verify(valueOperations, never()).set(any(), any(), any(Long.class), any(TimeUnit.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importPersonAsync_shouldAllowSameRelativePersonIdUnderDifferentOwners() {
|
||||||
|
CcdiIntermediaryPersonExcel owner1 = buildOwnerExcel("320101199001010014");
|
||||||
|
CcdiIntermediaryPersonExcel owner2 = buildOwnerExcel("320101199003030035");
|
||||||
|
CcdiIntermediaryPersonExcel relative1 = buildRelativeExcel("320101199201010027", "配偶", "320101199001010014");
|
||||||
|
CcdiIntermediaryPersonExcel relative2 = buildRelativeExcel("320101199201010027", "配偶", "320101199003030035");
|
||||||
|
prepareStatusRedisMock();
|
||||||
|
when(intermediaryMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
service.importPersonAsync(List.of(owner1, owner2, relative1, relative2), "task-owner-scope", "tester");
|
||||||
|
|
||||||
|
ArgumentCaptor<List<CcdiBizIntermediary>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(intermediaryMapper).insertBatch(captor.capture());
|
||||||
|
assertEquals(4, captor.getValue().size());
|
||||||
|
verify(valueOperations, never()).set(any(), any(), any(Long.class), any(TimeUnit.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importPersonAsync_shouldRejectDuplicateRelativeUnderSameOwner() {
|
||||||
|
CcdiIntermediaryPersonExcel owner = buildOwnerExcel("320101199001010014");
|
||||||
|
CcdiIntermediaryPersonExcel relative1 = buildRelativeExcel("320101199201010027", "配偶", "320101199001010014");
|
||||||
|
CcdiIntermediaryPersonExcel relative2 = buildRelativeExcel("320101199201010027", "配偶", "320101199001010014");
|
||||||
|
prepareFailureRedisMocks();
|
||||||
|
when(intermediaryMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
service.importPersonAsync(List.of(owner, relative1, relative2), "task-duplicate", "tester");
|
||||||
|
|
||||||
|
ArgumentCaptor<List<CcdiBizIntermediary>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(intermediaryMapper).insertBatch(captor.capture());
|
||||||
|
assertEquals(2, captor.getValue().size());
|
||||||
|
IntermediaryPersonImportFailureVO failure = firstFailure("import:intermediary:task-duplicate:failures");
|
||||||
|
assertEquals("320101199201010027", failure.getPersonId());
|
||||||
|
assertTrue(failure.getErrorMessage().contains("重复"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareStatusRedisMock() {
|
||||||
|
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareFailureRedisMocks() {
|
||||||
|
prepareStatusRedisMock();
|
||||||
|
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntermediaryPersonImportFailureVO firstFailure(String key) {
|
||||||
|
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
|
||||||
|
verify(valueOperations).set(org.mockito.ArgumentMatchers.eq(key), failureCaptor.capture(),
|
||||||
|
org.mockito.ArgumentMatchers.eq(7L), org.mockito.ArgumentMatchers.eq(TimeUnit.DAYS));
|
||||||
|
return (IntermediaryPersonImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiIntermediaryPersonExcel buildOwnerExcel(String personId) {
|
||||||
|
CcdiIntermediaryPersonExcel excel = new CcdiIntermediaryPersonExcel();
|
||||||
|
excel.setName("中介本人" + personId.substring(personId.length() - 2));
|
||||||
|
excel.setPersonType("中介");
|
||||||
|
excel.setPersonSubType("本人");
|
||||||
|
excel.setIdType("身份证");
|
||||||
|
excel.setPersonId(personId);
|
||||||
|
return excel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiIntermediaryPersonExcel buildRelativeExcel(String personId, String personSubType, String ownerPersonId) {
|
||||||
|
CcdiIntermediaryPersonExcel excel = new CcdiIntermediaryPersonExcel();
|
||||||
|
excel.setName("中介亲属" + personId.substring(personId.length() - 2));
|
||||||
|
excel.setPersonType("中介");
|
||||||
|
excel.setPersonSubType(personSubType);
|
||||||
|
excel.setIdType("身份证");
|
||||||
|
excel.setPersonId(personId);
|
||||||
|
excel.setRelatedNumId(ownerPersonId);
|
||||||
|
return excel;
|
||||||
|
}
|
||||||
|
}
|
||||||
573
docs/design/2026-04-20-intermediary-import-refactor-design.md
Normal file
573
docs/design/2026-04-20-intermediary-import-refactor-design.md
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
# 中介库导入改造设计文档
|
||||||
|
|
||||||
|
**模块**: 中介库管理
|
||||||
|
**日期**: 2026-04-20
|
||||||
|
**作者**: Codex
|
||||||
|
**状态**: 已实现(SQL 已执行,历史脏数据待清洗)
|
||||||
|
|
||||||
|
## 一、背景
|
||||||
|
|
||||||
|
当前中介库模块已经完成“中介本人 / 中介亲属 / 中介关联机构”三类记录的统一展示与维护,但导入能力仍停留在旧语义:
|
||||||
|
|
||||||
|
1. 页面导入入口与当前主从维护模式不一致
|
||||||
|
2. `related_num_id` 仍按“关联中介本人 `biz_id`”理解
|
||||||
|
3. 中介人员导入与中介实体关系导入边界不清晰
|
||||||
|
4. 导入交互未完全对齐系统内“员工数据导入”的标准模式
|
||||||
|
|
||||||
|
本次需求要求在中介库管理中补齐导入能力,并统一中介模块内 `relatedNumId` 的业务语义。
|
||||||
|
|
||||||
|
## 二、目标
|
||||||
|
|
||||||
|
本次设计目标如下:
|
||||||
|
|
||||||
|
1. 中介库顶部保留 2 个导入按钮:
|
||||||
|
- 导入中介信息
|
||||||
|
- 导入中介实体关联关系
|
||||||
|
2. “导入中介信息”统一处理中介本人和中介亲属,依据 `personSubType` 判断身份
|
||||||
|
3. “导入中介实体关联关系”单独写入 `ccdi_intermediary_enterprise_relation`
|
||||||
|
4. `ccdi_biz_intermediary.related_num_id` 在整个中介模块内统一改为保存“关联中介本人证件号码”
|
||||||
|
5. 全部导入采用纯新增模式,不支持覆盖更新
|
||||||
|
6. 导入交互、异步任务状态、失败记录展示与“员工数据导入”保持一致
|
||||||
|
7. 对历史数据执行一次性迁移,不保留双语义兼容逻辑
|
||||||
|
8. 中介模块现有外部接口继续保持 `bizId` 作为主资源标识,不改成证件号码路由
|
||||||
|
9. `relationType` 在中介模块中废弃,不再参与导入模板、校验与维护流程
|
||||||
|
|
||||||
|
## 三、范围
|
||||||
|
|
||||||
|
### 3.1 本次范围
|
||||||
|
|
||||||
|
- 中介库导入入口改造
|
||||||
|
- 中介信息导入与中介实体关联关系导入设计
|
||||||
|
- `related_num_id` 语义切换
|
||||||
|
- 中介模块内受影响代码调整
|
||||||
|
- 历史数据迁移设计
|
||||||
|
- 失败记录、去重、异步任务状态设计
|
||||||
|
- 设计文档、后端实施计划、前端实施计划沉淀
|
||||||
|
|
||||||
|
### 3.2 不在本次范围
|
||||||
|
|
||||||
|
- 不新增独立的“中介亲属关系导入”按钮
|
||||||
|
- 不新增机构主档导入能力
|
||||||
|
- 不支持导入覆盖更新
|
||||||
|
- 不做“同时兼容旧 `biz_id` 与新证件号码语义”的补丁方案
|
||||||
|
- 不抽象通用关系平台
|
||||||
|
|
||||||
|
## 四、现状分析
|
||||||
|
|
||||||
|
### 4.1 中介模块现状
|
||||||
|
|
||||||
|
当前中介库页面位于:
|
||||||
|
|
||||||
|
- `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
|
||||||
|
|
||||||
|
后端核心位于:
|
||||||
|
|
||||||
|
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java`
|
||||||
|
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java`
|
||||||
|
|
||||||
|
现有中介模块特点:
|
||||||
|
|
||||||
|
1. `ccdi_biz_intermediary` 同时承载中介本人和中介亲属
|
||||||
|
2. `ccdi_intermediary_enterprise_relation` 维护中介与机构关系
|
||||||
|
3. 中介统一列表通过联合查询展示本人、亲属、机构关系三类记录
|
||||||
|
4. 中介亲属当前通过 `related_num_id = 本人 biz_id` 建立归属关系
|
||||||
|
|
||||||
|
### 4.2 导入能力现状
|
||||||
|
|
||||||
|
当前仓库内已存在:
|
||||||
|
|
||||||
|
1. 个人中介导入异步链路
|
||||||
|
2. 实体中介导入异步链路
|
||||||
|
3. 员工数据导入标准交互
|
||||||
|
|
||||||
|
其中存在的偏差:
|
||||||
|
|
||||||
|
1. 现有中介导入仍沿用“个人 / 机构”旧语义
|
||||||
|
2. 顶部入口与当前业务结构不一致
|
||||||
|
3. 个人导入没有支持“本人 + 亲属”混合导入
|
||||||
|
4. 中介实体关联关系没有对应独立导入能力
|
||||||
|
5. `related_num_id` 新旧语义未统一
|
||||||
|
|
||||||
|
### 4.3 参考实现
|
||||||
|
|
||||||
|
本次导入交互和异步链路主要参考:
|
||||||
|
|
||||||
|
- `ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
|
||||||
|
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java`
|
||||||
|
|
||||||
|
本次关系导入与失败记录模式主要参考:
|
||||||
|
|
||||||
|
- 员工实体关系导入
|
||||||
|
- 员工亲属关系导入
|
||||||
|
|
||||||
|
## 五、方案对比
|
||||||
|
|
||||||
|
### 5.1 方案 A:两个导入按钮,人员与机构关系分开导入
|
||||||
|
|
||||||
|
做法:
|
||||||
|
|
||||||
|
1. 顶部仅保留“导入中介信息”和“导入中介实体关联关系”
|
||||||
|
2. 中介本人和中介亲属共用一个人员导入模板
|
||||||
|
3. 中介实体关联关系单独导入关系表
|
||||||
|
4. `related_num_id` 整模块统一改为“关联中介本人证件号码”
|
||||||
|
5. 执行一次性历史数据迁移
|
||||||
|
|
||||||
|
优点:
|
||||||
|
|
||||||
|
1. 与当前业务边界一致
|
||||||
|
2. 符合最短路径原则
|
||||||
|
3. 导入入口数量最少,业务理解成本低
|
||||||
|
4. 不引入补丁兼容逻辑
|
||||||
|
|
||||||
|
缺点:
|
||||||
|
|
||||||
|
1. 需要同步改造中介模块内既有主从关系代码
|
||||||
|
2. 需要补历史数据迁移脚本
|
||||||
|
|
||||||
|
### 5.2 方案 B:三个导入按钮,亲属关系独立导入
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
1. 与用户最终确认的交互不一致
|
||||||
|
2. 与“本人 / 亲属字段完全一致”的业务口径不一致
|
||||||
|
3. 增加额外入口和模板维护成本
|
||||||
|
|
||||||
|
### 5.3 方案 C:保留 `related_num_id` 旧语义,仅导入层转换
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
1. 形成代码与导入口径分裂
|
||||||
|
2. 后续维护成本高
|
||||||
|
3. 属于兼容型补丁方案,不符合本次约束
|
||||||
|
|
||||||
|
### 5.4 结论
|
||||||
|
|
||||||
|
采用 **方案 A:两个导入按钮,人员与机构关系分开导入,并整体切换 `related_num_id` 语义**。
|
||||||
|
|
||||||
|
## 六、总体设计
|
||||||
|
|
||||||
|
### 6.1 设计原则
|
||||||
|
|
||||||
|
1. 保持中介模块现有主从结构,不新增平行模块
|
||||||
|
2. 所有“亲属归属中介本人”的关系统一按证件号码表达
|
||||||
|
3. 导入全部为纯新增,失败逐条返回
|
||||||
|
4. 文件内去重与库内去重同时存在
|
||||||
|
5. 不引入补丁兼容逻辑
|
||||||
|
6. 现有外部接口对前端继续保持 `bizId` 契约,内部再转换为本人证件号码处理
|
||||||
|
|
||||||
|
### 6.2 顶部入口设计
|
||||||
|
|
||||||
|
中介库列表页顶部工具栏最终包含:
|
||||||
|
|
||||||
|
1. 新增
|
||||||
|
2. 导入中介信息
|
||||||
|
3. 导入中介实体关联关系
|
||||||
|
4. 查看中介信息导入失败记录
|
||||||
|
5. 查看中介实体关联关系导入失败记录
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
1. 不再保留独立“中介亲属关系导入”按钮
|
||||||
|
2. 两类导入任务各自维护状态和失败记录
|
||||||
|
|
||||||
|
## 七、数据模型与语义调整
|
||||||
|
|
||||||
|
### 7.1 `ccdi_biz_intermediary` 使用方式
|
||||||
|
|
||||||
|
`ccdi_biz_intermediary` 继续承载中介本人与中介亲属:
|
||||||
|
|
||||||
|
1. 中介本人
|
||||||
|
- `person_sub_type = 本人`
|
||||||
|
- `related_num_id = null`
|
||||||
|
2. 中介亲属
|
||||||
|
- `person_sub_type != 本人`
|
||||||
|
- `related_num_id = 关联中介本人证件号码`
|
||||||
|
|
||||||
|
唯一性口径调整为:
|
||||||
|
|
||||||
|
1. 中介本人 `person_id` 全表唯一
|
||||||
|
2. 中介亲属允许相同 `person_id` 关联多个不同中介本人
|
||||||
|
3. 同一中介本人名下的亲属以 `related_num_id + person_id` 唯一
|
||||||
|
|
||||||
|
### 7.2 `related_num_id` 语义切换
|
||||||
|
|
||||||
|
本次统一调整为:
|
||||||
|
|
||||||
|
- **旧语义**:关联中介本人 `biz_id`
|
||||||
|
- **新语义**:关联中介本人 `person_id`
|
||||||
|
|
||||||
|
影响原则:
|
||||||
|
|
||||||
|
1. 手工新增亲属时写入本人证件号码
|
||||||
|
2. 查询亲属列表时按本人证件号码关联
|
||||||
|
3. 首页联合查询亲属记录时按本人证件号码关联
|
||||||
|
4. 删除本人时按本人证件号码删除其亲属
|
||||||
|
5. 导入亲属时按本人证件号码回填 `related_num_id`
|
||||||
|
6. 外部接口继续传本人 `bizId`,服务层内部先查本人证件号码后再执行业务逻辑
|
||||||
|
|
||||||
|
### 7.3 外部接口契约
|
||||||
|
|
||||||
|
本次仅调整中介模块内部关联语义,不调整现有外部接口的路径参数契约:
|
||||||
|
|
||||||
|
1. 查询中介详情继续使用 `bizId`
|
||||||
|
2. 查询亲属列表继续使用 `bizId`
|
||||||
|
3. 新增中介亲属继续使用 `bizId`
|
||||||
|
4. 查询中介关联机构列表继续使用 `bizId`
|
||||||
|
|
||||||
|
服务层处理方式统一为:
|
||||||
|
|
||||||
|
1. 先根据外部传入的 `bizId` 查询中介本人
|
||||||
|
2. 再使用本人 `personId` 处理亲属链路
|
||||||
|
3. 机构关系链路继续使用本人 `bizId`
|
||||||
|
|
||||||
|
### 7.4 `ccdi_intermediary_enterprise_relation`
|
||||||
|
|
||||||
|
中介实体关联关系继续存于:
|
||||||
|
|
||||||
|
- `ccdi_intermediary_enterprise_relation`
|
||||||
|
|
||||||
|
该表语义保持不变:
|
||||||
|
|
||||||
|
1. `intermediary_biz_id` 仍保存中介本人 `biz_id`
|
||||||
|
2. 导入时先通过“中介本人证件号码”定位本人,再回填 `intermediary_biz_id`
|
||||||
|
3. 不在该链路中修改关系表结构
|
||||||
|
|
||||||
|
## 八、导入能力设计
|
||||||
|
|
||||||
|
### 8.1 导入中介信息
|
||||||
|
|
||||||
|
#### 8.1.1 目标
|
||||||
|
|
||||||
|
统一导入中介本人与中介亲属,落表:
|
||||||
|
|
||||||
|
- `ccdi_biz_intermediary`
|
||||||
|
|
||||||
|
#### 8.1.2 接口
|
||||||
|
|
||||||
|
建议保留或调整为:
|
||||||
|
|
||||||
|
1. `POST /ccdi/intermediary/importPersonTemplate`
|
||||||
|
2. `POST /ccdi/intermediary/importPersonData`
|
||||||
|
3. `GET /ccdi/intermediary/importPersonStatus/{taskId}`
|
||||||
|
4. `GET /ccdi/intermediary/importPersonFailures/{taskId}`
|
||||||
|
|
||||||
|
#### 8.1.3 模板字段
|
||||||
|
|
||||||
|
模板字段如下:
|
||||||
|
|
||||||
|
1. 姓名
|
||||||
|
2. 人员类型
|
||||||
|
3. 人员子类型
|
||||||
|
4. 性别
|
||||||
|
5. 证件类型
|
||||||
|
6. 证件号码
|
||||||
|
7. 手机号码
|
||||||
|
8. 微信号
|
||||||
|
9. 联系地址
|
||||||
|
10. 所在公司
|
||||||
|
11. 企业统一信用码
|
||||||
|
12. 职位
|
||||||
|
13. 关联中介本人证件号码
|
||||||
|
14. 备注
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
1. `personSubType` 在导入模板中必须使用下拉框,不允许自由文本输入
|
||||||
|
2. `personSubType` 下拉来源使用系统字典 `ccdi_person_sub_type`
|
||||||
|
3. 模板生成方式与系统现有 Excel 字典下拉保持一致
|
||||||
|
4. `personSubType = 本人` 时,`relatedNumId` 必须为空
|
||||||
|
5. `personSubType != 本人` 时,`relatedNumId` 必须填写“关联中介本人证件号码”
|
||||||
|
6. `relationType` 字段在本次导入中废弃,不出现在模板中,也不参与导入校验与落库
|
||||||
|
|
||||||
|
#### 8.1.4 导入处理规则
|
||||||
|
|
||||||
|
导入分两阶段处理:
|
||||||
|
|
||||||
|
第一阶段:处理中介本人
|
||||||
|
|
||||||
|
1. `personSubType = 本人` 时,`relatedNumId` 必须为空
|
||||||
|
2. 提取所有 `personSubType = 本人` 的行
|
||||||
|
3. 校验必填、证件号格式、文件内重复、库内重复
|
||||||
|
4. 导入成功后建立“本人证件号集合”
|
||||||
|
|
||||||
|
第二阶段:处理中介亲属
|
||||||
|
|
||||||
|
1. 提取所有 `personSubType != 本人` 的行
|
||||||
|
2. 校验 `relatedNumId` 必填
|
||||||
|
3. 校验 `relatedNumId` 对应的中介本人是否存在于:
|
||||||
|
- 数据库已存在记录
|
||||||
|
- 本次第一阶段已导入成功的本人记录
|
||||||
|
4. 允许相同亲属证件号码关联多个不同中介本人
|
||||||
|
5. 校验同一中介本人名下的亲属关系唯一性
|
||||||
|
6. 落库时 `related_num_id = 本人证件号码`
|
||||||
|
|
||||||
|
#### 8.1.5 去重逻辑
|
||||||
|
|
||||||
|
双层去重:
|
||||||
|
|
||||||
|
1. 对中介本人记录,文件内按 `personId` 去重
|
||||||
|
2. 对中介本人记录,数据库内按 `personId` 去重
|
||||||
|
3. 对中介亲属记录,文件内按 `relatedNumId + personId` 去重
|
||||||
|
4. 对中介亲属记录,数据库内按 `relatedNumId + personId` 去重
|
||||||
|
|
||||||
|
失败判定口径:
|
||||||
|
|
||||||
|
1. 中介本人证件号在同一文件内重复,记失败
|
||||||
|
2. 中介本人证件号在数据库中已存在,记失败
|
||||||
|
3. 同一文件内相同“本人证件号 + 亲属证件号”重复,记失败
|
||||||
|
4. 数据库中相同“本人证件号 + 亲属证件号”已存在,记失败
|
||||||
|
5. 相同亲属证件号若关联的是不同中介本人,允许导入成功
|
||||||
|
|
||||||
|
### 8.2 导入中介实体关联关系
|
||||||
|
|
||||||
|
#### 8.2.1 目标
|
||||||
|
|
||||||
|
导入中介与机构的关系,落表:
|
||||||
|
|
||||||
|
- `ccdi_intermediary_enterprise_relation`
|
||||||
|
|
||||||
|
#### 8.2.2 接口
|
||||||
|
|
||||||
|
新增独立接口:
|
||||||
|
|
||||||
|
1. `POST /ccdi/intermediary/importEnterpriseRelationTemplate`
|
||||||
|
2. `POST /ccdi/intermediary/importEnterpriseRelationData`
|
||||||
|
3. `GET /ccdi/intermediary/importEnterpriseRelationStatus/{taskId}`
|
||||||
|
4. `GET /ccdi/intermediary/importEnterpriseRelationFailures/{taskId}`
|
||||||
|
|
||||||
|
#### 8.2.3 模板字段
|
||||||
|
|
||||||
|
模板字段如下:
|
||||||
|
|
||||||
|
1. 中介本人证件号码
|
||||||
|
2. 统一社会信用代码
|
||||||
|
3. 关联角色/职务
|
||||||
|
4. 备注
|
||||||
|
|
||||||
|
#### 8.2.4 导入处理规则
|
||||||
|
|
||||||
|
1. 根据“中介本人证件号码”查找 `person_sub_type = 本人` 的中介本人
|
||||||
|
2. 根据统一社会信用代码校验机构已存在于 `ccdi_enterprise_base_info`
|
||||||
|
3. 将本人 `biz_id` 回填为 `intermediary_biz_id`
|
||||||
|
4. 写入 `ccdi_intermediary_enterprise_relation`
|
||||||
|
|
||||||
|
#### 8.2.5 去重逻辑
|
||||||
|
|
||||||
|
双层去重:
|
||||||
|
|
||||||
|
1. 文件内按 `中介本人证件号码 + 统一社会信用代码` 去重
|
||||||
|
2. 数据库内按 `intermediary_biz_id + social_credit_code` 去重
|
||||||
|
|
||||||
|
失败判定口径:
|
||||||
|
|
||||||
|
1. 中介本人证件号码不存在,记失败
|
||||||
|
2. 统一社会信用代码不存在于系统机构表,记失败
|
||||||
|
3. 文件内重复关系,记失败
|
||||||
|
4. 数据库内已存在关系,记失败
|
||||||
|
|
||||||
|
## 九、异步任务与失败记录设计
|
||||||
|
|
||||||
|
### 9.1 异步链路
|
||||||
|
|
||||||
|
两类导入都采用与员工数据导入一致的异步模式:
|
||||||
|
|
||||||
|
1. Controller 解析 Excel
|
||||||
|
2. 主 Service 生成 `taskId`
|
||||||
|
3. Redis 初始化状态为 `PROCESSING`
|
||||||
|
4. 异步 ImportService 后台处理
|
||||||
|
5. 前端轮询任务状态
|
||||||
|
6. 完成后展示结果并支持查看失败记录
|
||||||
|
|
||||||
|
### 9.2 Redis Key 设计
|
||||||
|
|
||||||
|
中介信息导入:
|
||||||
|
|
||||||
|
1. `import:intermediary-person:{taskId}`
|
||||||
|
2. `import:intermediary-person:{taskId}:failures`
|
||||||
|
|
||||||
|
中介实体关联关系导入:
|
||||||
|
|
||||||
|
1. `import:intermediary-enterprise-relation:{taskId}`
|
||||||
|
2. `import:intermediary-enterprise-relation:{taskId}:failures`
|
||||||
|
|
||||||
|
### 9.3 状态字段
|
||||||
|
|
||||||
|
状态字段统一为:
|
||||||
|
|
||||||
|
1. `taskId`
|
||||||
|
2. `status`
|
||||||
|
3. `totalCount`
|
||||||
|
4. `successCount`
|
||||||
|
5. `failureCount`
|
||||||
|
6. `progress`
|
||||||
|
7. `startTime`
|
||||||
|
8. `endTime`
|
||||||
|
9. `message`
|
||||||
|
|
||||||
|
### 9.4 失败记录
|
||||||
|
|
||||||
|
失败记录按模板字段原样返回,并增加:
|
||||||
|
|
||||||
|
- `errorMessage`
|
||||||
|
|
||||||
|
前端可直接复用员工数据导入的失败记录弹窗模式。
|
||||||
|
|
||||||
|
## 十、`relatedNumId` 语义切换影响清单
|
||||||
|
|
||||||
|
### 10.1 直接受影响代码
|
||||||
|
|
||||||
|
1. `CcdiIntermediaryServiceImpl`
|
||||||
|
- 亲属新增、修改、查询、删除、级联删除
|
||||||
|
2. `CcdiIntermediaryMapper.xml`
|
||||||
|
- 统一列表中亲属联表条件
|
||||||
|
3. `CcdiIntermediaryPersonImportServiceImpl`
|
||||||
|
- 本人/亲属混合导入逻辑
|
||||||
|
4. `CcdiIntermediaryController`
|
||||||
|
- 导入接口重组
|
||||||
|
5. `CcdiBizIntermediary`
|
||||||
|
- 字段语义注释
|
||||||
|
6. `CcdiIntermediaryPersonDetailVO`
|
||||||
|
7. `CcdiIntermediaryRelativeVO`
|
||||||
|
8. `CcdiIntermediaryPersonAddDTO`
|
||||||
|
9. `CcdiIntermediaryPersonEditDTO`
|
||||||
|
10. `CcdiIntermediaryRelativeAddDTO`
|
||||||
|
11. `CcdiIntermediaryRelativeEditDTO`
|
||||||
|
12. `CcdiIntermediaryPersonExcel`
|
||||||
|
|
||||||
|
### 10.2 前端受影响代码
|
||||||
|
|
||||||
|
1. `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
|
||||||
|
2. `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
|
||||||
|
3. `ruoyi-ui/src/api/ccdiIntermediary.js`
|
||||||
|
4. 失败记录弹窗相关状态管理
|
||||||
|
5. 导入模板说明文案
|
||||||
|
6. 现有依赖 `bizId` 的亲属维护入口保持不变
|
||||||
|
|
||||||
|
### 10.3 基本不受影响代码
|
||||||
|
|
||||||
|
1. `CcdiIntermediaryEnterpriseRelationMapper.xml`
|
||||||
|
- 仍通过 `intermediary_biz_id -> biz_id` 关联
|
||||||
|
2. 账户模块中 `INTERMEDIARY` 所属人校验逻辑
|
||||||
|
- 当前仅以中介证件号作为 ownerId,不依赖 `relatedNumId`
|
||||||
|
|
||||||
|
### 10.4 文档与资料
|
||||||
|
|
||||||
|
以下文档需要同步修正语义:
|
||||||
|
|
||||||
|
1. 旧中介库设计文档
|
||||||
|
2. 中介 API 文档
|
||||||
|
3. 数据库字段说明
|
||||||
|
4. 相关实施计划与测试文档
|
||||||
|
|
||||||
|
## 十一、历史数据迁移设计
|
||||||
|
|
||||||
|
### 11.1 迁移目标
|
||||||
|
|
||||||
|
将历史数据中 `related_num_id = 中介本人 biz_id` 的记录一次性迁移为:
|
||||||
|
|
||||||
|
- `related_num_id = 中介本人 person_id`
|
||||||
|
|
||||||
|
### 11.2 迁移前校验
|
||||||
|
|
||||||
|
迁移前必须检查:
|
||||||
|
|
||||||
|
1. `related_num_id` 有值,但找不到对应本人 `biz_id`
|
||||||
|
2. 找到本人后,本人 `person_id` 为空
|
||||||
|
3. 同一历史关系迁移后是否会与现有新语义数据冲突
|
||||||
|
4. 现有亲属记录在迁移后是否出现同一中介本人名下 `related_num_id + person_id` 冲突
|
||||||
|
|
||||||
|
### 11.3 迁移原则
|
||||||
|
|
||||||
|
1. 按用户要求,迁移先于功能改造执行
|
||||||
|
2. 先清洗异常数据,再执行正式迁移
|
||||||
|
3. 迁移完成后,全部代码统一按新语义运行
|
||||||
|
4. 不保留同时兼容 `biz_id` 与证件号码的查询逻辑
|
||||||
|
5. 为避免旧逻辑读取失败,迁移、代码发布、回归验证必须放在同一实施窗口内连续完成
|
||||||
|
|
||||||
|
## 十二、异常处理口径
|
||||||
|
|
||||||
|
### 12.1 导入中介信息
|
||||||
|
|
||||||
|
常见失败原因包括:
|
||||||
|
|
||||||
|
1. 证件号码不能为空
|
||||||
|
2. 人员子类型不能为空
|
||||||
|
3. `personSubType = 本人` 但 `relatedNumId` 非空
|
||||||
|
4. `personSubType != 本人` 但 `relatedNumId` 为空
|
||||||
|
5. 关联中介本人证件号码不存在
|
||||||
|
6. 中介本人证件号在导入文件内重复
|
||||||
|
7. 中介本人证件号在数据库中已存在
|
||||||
|
8. 导入文件内亲属关系重复
|
||||||
|
9. 数据库中亲属关系已存在
|
||||||
|
|
||||||
|
### 12.2 导入中介实体关联关系
|
||||||
|
|
||||||
|
常见失败原因包括:
|
||||||
|
|
||||||
|
1. 中介本人证件号码不能为空
|
||||||
|
2. 统一社会信用代码不能为空
|
||||||
|
3. 中介本人不存在
|
||||||
|
4. 统一社会信用代码不存在于系统机构表
|
||||||
|
5. 导入文件内关系重复
|
||||||
|
6. 数据库中关系已存在
|
||||||
|
|
||||||
|
## 十三、测试边界
|
||||||
|
|
||||||
|
### 13.1 后端测试
|
||||||
|
|
||||||
|
至少覆盖:
|
||||||
|
|
||||||
|
1. 历史数据迁移后亲属列表查询正确
|
||||||
|
2. 手工新增亲属后 `related_num_id` 保存本人证件号码
|
||||||
|
3. 删除中介本人时级联删除其亲属和关联机构关系
|
||||||
|
4. 本人和亲属混合导入成功
|
||||||
|
5. 亲属引用“本次文件内先导入成功的本人”成功
|
||||||
|
6. 相同亲属证件号关联不同中介本人成功
|
||||||
|
7. 文件内重复、库内重复、无效关联均能正确记失败
|
||||||
|
8. 中介实体关联关系导入成功与失败路径正确
|
||||||
|
|
||||||
|
### 13.2 前端测试
|
||||||
|
|
||||||
|
至少覆盖:
|
||||||
|
|
||||||
|
1. 顶部两个导入按钮显示正确
|
||||||
|
2. 模板下载地址正确
|
||||||
|
3. 上传后异步轮询正常
|
||||||
|
4. 完成后结果提示正确
|
||||||
|
5. 失败记录弹窗、分页、清除历史记录可用
|
||||||
|
6. 页面刷新后可恢复最近一次导入任务状态
|
||||||
|
|
||||||
|
## 十四、实施建议
|
||||||
|
|
||||||
|
建议实施顺序如下:
|
||||||
|
|
||||||
|
1. 先完成 `related_num_id` 历史数据迁移脚本与迁移校验 SQL
|
||||||
|
2. 在同一实施窗口内先执行迁移
|
||||||
|
3. 紧接着发布中介模块语义切换代码,完成亲属查询、手工维护、删除级联改造
|
||||||
|
4. 再接入两类导入后端接口与异步处理
|
||||||
|
5. 最后改造前端入口、弹窗、失败记录与状态恢复
|
||||||
|
|
||||||
|
## 十五、设计结论
|
||||||
|
|
||||||
|
本次中介库导入改造最终确认如下:
|
||||||
|
|
||||||
|
1. 顶部仅保留 2 个导入按钮
|
||||||
|
2. 中介本人和中介亲属合并为“导入中介信息”
|
||||||
|
3. 中介实体关联关系单独导入,落关系表
|
||||||
|
4. `related_num_id` 整个中介模块统一改为“关联中介本人证件号码”
|
||||||
|
5. 所有导入采用纯新增模式
|
||||||
|
6. 中介本人按证件号全局唯一,亲属允许关联多个不同中介本人
|
||||||
|
7. 去重采用“文件内去重 + 库内去重”
|
||||||
|
8. 外部接口继续保持 `bizId` 契约
|
||||||
|
9. `relationType` 废弃,`personSubType` 使用字典下拉
|
||||||
|
10. 通过一次性历史数据迁移完成语义统一
|
||||||
|
|
||||||
|
该方案满足当前需求边界,且符合最短路径实现原则,不引入兼容性补丁方案。
|
||||||
|
|
||||||
|
## 十六、实施回写
|
||||||
|
|
||||||
|
- 2026-04-20 已完成中介模块 `related_num_id` 语义切换,手工新增亲属、统一列表查询、本人证件号变更同步和级联删除逻辑均按“关联中介本人证件号码”改造。
|
||||||
|
- 2026-04-20 已完成“导入中介信息”和“导入中介实体关联关系”两条异步导入链路,并同步完成前端双按钮、双任务状态、双失败记录模式改造。
|
||||||
|
- 2026-04-20 已完成后端目标测试回归、信息采集模块编译、前端静态单测和 `build:prod` 构建验证。
|
||||||
|
- 2026-04-20 已执行 `bin/mysql_utf8_exec.sh sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql` 与 `bin/mysql_utf8_exec.sh sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql`。
|
||||||
|
- 迁移后核查结果:`legacy_biz_id_reference = 0`,说明旧 `biz_id` 语义残留已清零;`post_migration_missing_parent = 1025`、`owner_person_id_empty_after_migration = 754`,说明历史数据中仍存在无法关联到本人证件号的脏数据,需后续专项清洗。
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
# 中介库导入改造后端实施计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
> **执行约束:** 仓库约定不开 subagent,执行时使用 `superpowers:executing-plans`。
|
||||||
|
|
||||||
|
**Goal:** 完成中介库后端导入改造,统一 `related_num_id` 为“关联中介本人证件号码”,新增“导入中介信息”和“导入中介实体关联关系”两条异步导入链路,并保证手工维护、统一查询和级联删除与新语义一致。
|
||||||
|
|
||||||
|
**Architecture:** 后端保持 `CcdiIntermediaryController + CcdiIntermediaryServiceImpl + ccdi_biz_intermediary/ccdi_intermediary_enterprise_relation` 的现有主线,不新增平行模块。外部 API 继续保持 `bizId` 契约,服务层内部把亲属链路统一转换为“本人证件号码”处理;导入链路对齐员工数据导入,拆分为“中介信息导入”和“中介实体关联关系导入”两套独立任务与失败记录。
|
||||||
|
|
||||||
|
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MySQL, Redis, EasyExcel, JUnit 5, Maven, Markdown
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构与职责
|
||||||
|
|
||||||
|
**设计与计划基线**
|
||||||
|
|
||||||
|
- `docs/design/2026-04-20-intermediary-import-refactor-design.md`
|
||||||
|
当前需求和边界的唯一设计基线,实施不得偏离其中已确认口径。
|
||||||
|
|
||||||
|
**SQL 与迁移**
|
||||||
|
|
||||||
|
- Create: `sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql`
|
||||||
|
负责把历史 `related_num_id = 本人 biz_id` 迁移为 `related_num_id = 本人 person_id`。
|
||||||
|
- Create: `sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql`
|
||||||
|
负责补齐或校正 `ccdi_person_sub_type` 字典项,供导入模板下拉使用。
|
||||||
|
|
||||||
|
**中介主链路**
|
||||||
|
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiBizIntermediary.java`
|
||||||
|
调整 `relatedNumId` 字段注释语义。
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java`
|
||||||
|
暴露新的校验与导入能力方法。
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java`
|
||||||
|
完成亲属主从语义切换、手工维护逻辑修正、关系唯一性判断和级联删除。
|
||||||
|
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml`
|
||||||
|
把统一列表中的亲属关联条件从 `parent.biz_id` 改为 `parent.person_id`。
|
||||||
|
|
||||||
|
**导入链路**
|
||||||
|
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java`
|
||||||
|
重组导入接口,保留中介信息导入,新增实体关联关系导入。
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryPersonExcel.java`
|
||||||
|
让 `personSubType` 使用字典下拉,移除废弃的 `relationType` 列,重命名 `relatedNumId` 模板文案。
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryPersonImportFailureVO.java`
|
||||||
|
对齐新模板字段,移除废弃字段。
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryPersonImportService.java`
|
||||||
|
调整中介信息导入接口定义。
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryPersonImportServiceImpl.java`
|
||||||
|
重写为“本人阶段 + 亲属阶段”的混合导入实现。
|
||||||
|
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryEnterpriseRelationExcel.java`
|
||||||
|
中介实体关联关系导入模板实体。
|
||||||
|
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryEnterpriseRelationImportFailureVO.java`
|
||||||
|
实体关联关系失败记录 VO。
|
||||||
|
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryEnterpriseRelationImportService.java`
|
||||||
|
实体关联关系导入服务接口。
|
||||||
|
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java`
|
||||||
|
实体关联关系异步导入实现。
|
||||||
|
|
||||||
|
**既有 DTO / VO / Mapper 补充**
|
||||||
|
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryPersonAddDTO.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryPersonEditDTO.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeAddDTO.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeEditDTO.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiIntermediaryPersonDetailVO.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiIntermediaryRelativeVO.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryEnterpriseRelationMapper.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryEnterpriseRelationMapper.xml`
|
||||||
|
|
||||||
|
**测试**
|
||||||
|
|
||||||
|
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryMapperTest.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java`
|
||||||
|
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryPersonImportServiceImplTest.java`
|
||||||
|
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryEnterpriseRelationImportServiceImplTest.java`
|
||||||
|
|
||||||
|
## 实施任务
|
||||||
|
|
||||||
|
### Task 1: 编写迁移脚本并锁定实施窗口
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql`
|
||||||
|
- Create: `sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql`
|
||||||
|
- Reference: `sql/migration/2026-04-17-fix-intermediary-person-sub-type-dict.sql`
|
||||||
|
- Reference: `sql/dpc_intermediary_dict_data_20260129.sql`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写迁移前校验 SQL**
|
||||||
|
|
||||||
|
在迁移脚本头部先写校验查询,明确输出:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 1. 找不到对应本人 biz_id 的亲属
|
||||||
|
-- 2. 本人 person_id 为空的记录
|
||||||
|
-- 3. 迁移后同一中介本人下 related_num_id + person_id 冲突的记录
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 写正式迁移 SQL**
|
||||||
|
|
||||||
|
将旧语义统一改为新语义:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE ccdi_biz_intermediary child
|
||||||
|
JOIN ccdi_biz_intermediary parent
|
||||||
|
ON child.related_num_id = parent.biz_id
|
||||||
|
SET child.related_num_id = parent.person_id
|
||||||
|
WHERE child.person_sub_type <> '本人';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 写 `ccdi_person_sub_type` 字典修正脚本**
|
||||||
|
|
||||||
|
至少补齐:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
本人 / 配偶 / 子女 / 父母 / 兄弟姐妹 / 其他
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 模板下拉和后端校验使用同一字典源。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 用 UTF-8 脚本执行 SQL 验证**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/mysql_utf8_exec.sh sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql
|
||||||
|
bin/mysql_utf8_exec.sh sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: SQL 正常执行,无乱码,无唯一性冲突。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交迁移脚本**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql
|
||||||
|
git commit -m "新增中介导入迁移脚本"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: 切换中介模块内部主从语义
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiBizIntermediary.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeAddDTO.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeEditDTO.java`
|
||||||
|
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java`
|
||||||
|
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryMapperTest.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写失败测试,覆盖亲属新语义**
|
||||||
|
|
||||||
|
补测试场景:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 1. 手工新增亲属时 related_num_id 写本人 person_id
|
||||||
|
// 2. 查询亲属列表按 parent.person_id 关联
|
||||||
|
// 3. 删除本人时按本人 person_id 级联删除亲属
|
||||||
|
// 4. 相同亲属 person_id 挂到不同本人时允许成功
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 调整服务层内部转换逻辑**
|
||||||
|
|
||||||
|
关键改造点:
|
||||||
|
|
||||||
|
```java
|
||||||
|
CcdiBizIntermediary owner = requireIntermediaryPerson(bizId);
|
||||||
|
relative.setRelatedNumId(owner.getPersonId());
|
||||||
|
```
|
||||||
|
|
||||||
|
以及:
|
||||||
|
|
||||||
|
```java
|
||||||
|
wrapper.eq(CcdiBizIntermediary::getRelatedNumId, owner.getPersonId())
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 收敛唯一性校验**
|
||||||
|
|
||||||
|
新增两个明确规则:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 本人:personId 全表唯一
|
||||||
|
// 亲属:同一 relatedNumId + personId 唯一
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 不再把亲属 `personId` 误当成全表唯一。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 修改联合查询 SQL**
|
||||||
|
|
||||||
|
把亲属联表条件改成:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
ON child.related_num_id = parent.person_id
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 统一列表在迁移后仍能查出亲属记录。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行服务层与 Mapper 测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 目标测试通过,新的主从语义断言成立。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交语义切换代码**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiBizIntermediary.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeAddDTO.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeEditDTO.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryMapperTest.java
|
||||||
|
git commit -m "调整中介亲属关联语义"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: 重构中介信息导入为“本人 + 亲属”混合导入
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryPersonExcel.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryPersonImportFailureVO.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryPersonImportService.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryPersonImportServiceImpl.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java`
|
||||||
|
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryPersonImportServiceImplTest.java`
|
||||||
|
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写导入失败测试**
|
||||||
|
|
||||||
|
覆盖:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 1. personSubType 使用字典下拉字段
|
||||||
|
// 2. personSubType=本人 且 relatedNumId 非空 -> 失败
|
||||||
|
// 3. personSubType!=本人 且 relatedNumId 为空 -> 失败
|
||||||
|
// 4. 亲属引用本次导入前面成功的本人 -> 成功
|
||||||
|
// 5. 同一亲属挂不同本人 -> 成功
|
||||||
|
// 6. 同一本人名下重复亲属 -> 失败
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 调整 Excel 模板实体**
|
||||||
|
|
||||||
|
最小改动:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@DictDropdown(dictType = "ccdi_person_sub_type")
|
||||||
|
private String personSubType;
|
||||||
|
|
||||||
|
@ExcelProperty("关联中介本人证件号码")
|
||||||
|
private String relatedNumId;
|
||||||
|
```
|
||||||
|
|
||||||
|
同时删除:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private String relationType;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 按“两阶段”重写导入实现**
|
||||||
|
|
||||||
|
核心流程:
|
||||||
|
|
||||||
|
```java
|
||||||
|
List<CcdiIntermediaryPersonExcel> owners = ...
|
||||||
|
List<CcdiIntermediaryPersonExcel> relatives = ...
|
||||||
|
|
||||||
|
// owners: personId 全表唯一
|
||||||
|
// relatives: relatedNumId + personId 唯一
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 中介本人和亲属在一个文件中可混合导入。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 统一失败记录字段**
|
||||||
|
|
||||||
|
`IntermediaryPersonImportFailureVO` 只保留当前模板字段,不再暴露废弃的 `relationType`。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 调整 Controller 返回文案与模板下载**
|
||||||
|
|
||||||
|
Expected: 下载模板时 `personSubType` 下拉来源于 `ccdi_person_sub_type`,返回的失败记录字段与模板一致。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 运行导入链路测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryControllerTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 混合导入与失败口径通过。
|
||||||
|
|
||||||
|
- [ ] **Step 7: 提交中介信息导入改造**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryPersonExcel.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryPersonImportFailureVO.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryPersonImportService.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryPersonImportServiceImpl.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryPersonImportServiceImplTest.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java
|
||||||
|
git commit -m "改造中介信息导入链路"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: 新增中介实体关联关系导入链路
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryEnterpriseRelationExcel.java`
|
||||||
|
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryEnterpriseRelationImportFailureVO.java`
|
||||||
|
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryEnterpriseRelationImportService.java`
|
||||||
|
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java`
|
||||||
|
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryEnterpriseRelationMapper.xml`
|
||||||
|
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryEnterpriseRelationImportServiceImplTest.java`
|
||||||
|
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写关系导入测试**
|
||||||
|
|
||||||
|
覆盖:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 1. 中介本人证件号码不存在 -> 失败
|
||||||
|
// 2. socialCreditCode 不存在于机构表 -> 失败
|
||||||
|
// 3. 同一文件内相同 personId + socialCreditCode -> 失败
|
||||||
|
// 4. 数据库内已存在相同 intermediary_biz_id + socialCredit_code -> 失败
|
||||||
|
// 5. 正常导入成功并写入关系表
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 新建 Excel 与失败记录对象**
|
||||||
|
|
||||||
|
字段固定为:
|
||||||
|
|
||||||
|
```java
|
||||||
|
ownerPersonId
|
||||||
|
socialCreditCode
|
||||||
|
relationPersonPost
|
||||||
|
remark
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 新建异步导入服务**
|
||||||
|
|
||||||
|
服务逻辑:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 证件号码 -> 查本人 bizId
|
||||||
|
// 社会信用代码 -> 校验机构存在
|
||||||
|
// intermediaryBizId + socialCreditCode 唯一
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 补 Controller 接口**
|
||||||
|
|
||||||
|
新增:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/importEnterpriseRelationTemplate
|
||||||
|
/importEnterpriseRelationData
|
||||||
|
/importEnterpriseRelationStatus/{taskId}
|
||||||
|
/importEnterpriseRelationFailures/{taskId}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行关系导入测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiIntermediaryControllerTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 实体关联关系导入链路完整通过。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交关系导入代码**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryEnterpriseRelationExcel.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryEnterpriseRelationImportFailureVO.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryEnterpriseRelationImportService.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryEnterpriseRelationMapper.xml ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryEnterpriseRelationImportServiceImplTest.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java
|
||||||
|
git commit -m "新增中介实体关联关系导入"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: 做完整回归验证并更新文档
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `docs/design/2026-04-20-intermediary-import-refactor-design.md`
|
||||||
|
- Modify: `docs/plans/backend/2026-04-20-intermediary-import-backend-implementation.md`
|
||||||
|
- Reference: `bin/mysql_utf8_exec.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 运行后端完整相关测试集合**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest,CcdiIntermediaryControllerTest,CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 全部 PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 编译信息采集模块,确认没有遗漏编译错误**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl ccdi-info-collection -am clean compile
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `BUILD SUCCESS`。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 记录 SQL 执行与测试结果**
|
||||||
|
|
||||||
|
在计划文档底部补:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- 实际执行的 SQL 脚本
|
||||||
|
- 实际执行的 Maven 命令
|
||||||
|
- PASS / FAIL 结果
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交文档回写**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/design/2026-04-20-intermediary-import-refactor-design.md docs/plans/backend/2026-04-20-intermediary-import-backend-implementation.md
|
||||||
|
git commit -m "补充中介导入后端实施记录"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/mysql_utf8_exec.sh sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql
|
||||||
|
bin/mysql_utf8_exec.sh sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql
|
||||||
|
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest,CcdiIntermediaryControllerTest,CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest test
|
||||||
|
mvn -pl ccdi-info-collection -am clean compile
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完成标准
|
||||||
|
|
||||||
|
- `related_num_id` 已按“关联中介本人证件号码”完成迁移和代码切换
|
||||||
|
- 手工新增亲属、查询亲属、删除本人级联删除全部与新语义一致
|
||||||
|
- 中介信息导入支持本人和亲属混合导入
|
||||||
|
- 同一亲属证件号允许关联多个不同中介本人
|
||||||
|
- 中介实体关联关系导入独立可用,且只写关系表
|
||||||
|
- `relationType` 已从导入链路中移除
|
||||||
|
- `personSubType` 模板下拉已切换为 `ccdi_person_sub_type`
|
||||||
|
- 后端测试与编译验证通过
|
||||||
|
|
||||||
|
## 执行结果
|
||||||
|
|
||||||
|
- SQL 脚本:
|
||||||
|
`sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql`
|
||||||
|
`sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql`
|
||||||
|
结果:已执行 `bin/mysql_utf8_exec.sh`。
|
||||||
|
- SQL 核查:
|
||||||
|
`post_migration_missing_parent = 1025`
|
||||||
|
`legacy_biz_id_reference = 0`
|
||||||
|
`owner_person_id_empty_after_migration = 754`
|
||||||
|
结果:可迁移数据已切换完成,旧 `biz_id` 语义残留清零,但历史脏数据仍需后续清洗。
|
||||||
|
- Maven 命令:
|
||||||
|
`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest test`
|
||||||
|
结果:PASS
|
||||||
|
- Maven 命令:
|
||||||
|
`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiIntermediaryControllerTest test`
|
||||||
|
结果:PASS
|
||||||
|
- Maven 命令:
|
||||||
|
`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest,CcdiIntermediaryControllerTest,CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest test`
|
||||||
|
结果:PASS
|
||||||
|
- Maven 命令:
|
||||||
|
`mvn -pl ccdi-info-collection -am clean compile`
|
||||||
|
结果:PASS(BUILD SUCCESS)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 实体库删除关联校验后端实施计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在实体库管理删除接口中增加删除前校验,确保待删除实体与中介、员工、信贷客户不存在关联关系;一旦存在任一关联,当前删除操作直接失败并返回明确提示。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiEnterpriseBaseInfoServiceImpl.java`
|
||||||
|
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiEnterpriseBaseInfoServiceImplTest.java`
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
1. 在 `CcdiEnterpriseBaseInfoServiceImpl` 的批量删除入口增加逐条删除前校验。
|
||||||
|
2. 使用现有三张关系表进行计数判断:
|
||||||
|
`ccdi_staff_enterprise_relation`
|
||||||
|
`ccdi_cust_enterprise_relation`
|
||||||
|
`ccdi_intermediary_enterprise_relation`
|
||||||
|
3. 若任一关系存在,拼装“员工/信贷客户/中介”中文提示并抛出运行时异常,阻断整批删除。
|
||||||
|
4. 保持所有待删实体均通过校验后,再执行原有批量删除。
|
||||||
|
5. 补充服务层单元测试,覆盖无关联可删除、单一关联拦截、多关联拦截三类场景。
|
||||||
|
|
||||||
|
## 验证命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiEnterpriseBaseInfoServiceImplTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完成标准
|
||||||
|
|
||||||
|
- 实体库删除前会校验员工、信贷客户、中介三类关联
|
||||||
|
- 存在关联时接口返回明确失败原因,不执行删除
|
||||||
|
- 无关联时保留原有批量删除行为
|
||||||
|
- 定向单元测试通过
|
||||||
53
docs/plans/backend/2026-04-21-redis断连自动重连修复实施计划.md
Normal file
53
docs/plans/backend/2026-04-21-redis断连自动重连修复实施计划.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Redis 断连自动重连修复实施计划
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
后端当前通过 Spring Boot 自动配置的 Lettuce `RedisConnectionFactory` 提供 Redis 连接。现场反馈 Redis 短暂断连或重启后,后端不会恢复可用连接,后续 Redis 访问持续失败。
|
||||||
|
|
||||||
|
## 2. 问题定位
|
||||||
|
|
||||||
|
- 项目未自定义 `LettuceConnectionFactory`,使用的是 Spring Boot 默认装配。
|
||||||
|
- 默认共享连接在未开启连接校验时,存在继续复用失效连接的风险。
|
||||||
|
- 当前 `ruoyi-framework` 仅配置了 `RedisTemplate` 序列化,未对 Lettuce 连接工厂做任何重连相关定制。
|
||||||
|
|
||||||
|
## 3. 实施方案
|
||||||
|
|
||||||
|
### 3.1 最短路径修复
|
||||||
|
|
||||||
|
在 `ruoyi-framework` 的 Redis 配置中增加一个 `BeanPostProcessor`,对 Spring Boot 自动创建的 `LettuceConnectionFactory` 统一开启 `validateConnection`。
|
||||||
|
|
||||||
|
### 3.2 预期效果
|
||||||
|
|
||||||
|
- Redis 恢复可用后,连接工厂在获取共享连接时会先校验连接有效性。
|
||||||
|
- 避免继续复用已经失效的 Lettuce 共享连接。
|
||||||
|
- 不调整现有 `RedisTemplate`、业务缓存调用方式和 YAML 配置结构。
|
||||||
|
|
||||||
|
## 4. 代码改动点
|
||||||
|
|
||||||
|
- `ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java`
|
||||||
|
- 增加 Lettuce 连接工厂后处理器。
|
||||||
|
- 在 Bean 初始化阶段统一开启 `validateConnection`。
|
||||||
|
- `ruoyi-framework/src/test/java/com/ruoyi/framework/config/RedisConfigTest.java`
|
||||||
|
- 新增回归测试,校验 Lettuce 连接工厂会被强制打开连接校验。
|
||||||
|
- `ruoyi-framework/pom.xml`
|
||||||
|
- 增加 `spring-boot-starter-test` 测试依赖。
|
||||||
|
|
||||||
|
## 5. 验证计划
|
||||||
|
|
||||||
|
执行以下命令验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl ruoyi-framework -am test -Dtest=RedisConfigTest -Dsurefire.failIfNoSpecifiedTests=false
|
||||||
|
```
|
||||||
|
|
||||||
|
验证通过标准:
|
||||||
|
|
||||||
|
- `RedisConfigTest` 通过。
|
||||||
|
- `ruoyi-framework` 模块构建成功。
|
||||||
|
- 不影响 `ruoyi-common`、`ruoyi-system` 作为依赖模块的测试阶段执行。
|
||||||
|
|
||||||
|
## 6. 影响范围
|
||||||
|
|
||||||
|
- 影响模块:`ruoyi-framework`
|
||||||
|
- 影响对象:所有通过 Spring 容器注入并复用 `RedisConnectionFactory` / `RedisTemplate` 的后端 Redis 调用
|
||||||
|
- 不涉及数据库结构、前端页面、接口入参与返回结构变更
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
# 中介库导入改造前端实施计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
> **执行约束:** 仓库约定不开 subagent,执行时使用 `superpowers:executing-plans`;前端 Node 版本必须通过 `nvm` 切换到仓库已验证版本。
|
||||||
|
|
||||||
|
**Goal:** 将中介库前端导入入口改造为“导入中介信息 + 导入中介实体关联关系”两按钮模式,去掉旧的“个人/机构”切换导入,打通异步轮询、失败记录查看、历史任务恢复,并保持现有手工维护链路继续使用 `bizId` 契约。
|
||||||
|
|
||||||
|
**Architecture:** 保留 `ruoyi-ui/src/views/ccdiIntermediary/` 作为唯一页面入口,不新增平行页面。导入弹窗继续复用现有 `ImportDialog.vue` 外壳,但改为由父页面通过 `scene`/`importType` 驱动,而不是弹窗内部单选切换;`index.vue` 负责管理两类导入任务状态、失败记录和按钮展示,交互整体对齐员工数据导入页面。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 2, Element UI, JavaScript, npm, nvm, Markdown
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构与职责
|
||||||
|
|
||||||
|
**设计与计划基线**
|
||||||
|
|
||||||
|
- `docs/design/2026-04-20-intermediary-import-refactor-design.md`
|
||||||
|
已确认的导入边界、字段口径和前端交互基线。
|
||||||
|
|
||||||
|
**前端源码**
|
||||||
|
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
|
||||||
|
负责顶部按钮、两类导入弹窗打开逻辑、异步任务状态恢复、失败记录弹窗和表格刷新。
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
|
||||||
|
从“内部切换 person/entity”改为“父组件传入导入场景”,支持中介信息导入和实体关联关系导入。
|
||||||
|
- Modify: `ruoyi-ui/src/api/ccdiIntermediary.js`
|
||||||
|
补两类导入的模板下载、上传、状态查询、失败记录 API。
|
||||||
|
|
||||||
|
**测试**
|
||||||
|
|
||||||
|
- Create: `ruoyi-ui/tests/unit/intermediary-import-toolbar.test.js`
|
||||||
|
- Create: `ruoyi-ui/tests/unit/intermediary-import-dialog.test.js`
|
||||||
|
- Create: `ruoyi-ui/tests/unit/intermediary-import-state.test.js`
|
||||||
|
- Create: `ruoyi-ui/tests/unit/intermediary-import-api.test.js`
|
||||||
|
- Modify: `ruoyi-ui/tests/unit/intermediary-person-edit-ui.test.js`
|
||||||
|
|
||||||
|
**依赖参考**
|
||||||
|
|
||||||
|
- `ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
|
||||||
|
参考顶部导入按钮、轮询、失败记录、历史任务恢复。
|
||||||
|
- `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
|
||||||
|
继续作为导入弹窗壳子,不重复造一个全新组件。
|
||||||
|
- `ruoyi-ui/tests/unit/employee-asset-import-ui.test.js`
|
||||||
|
参考导入类单测结构和断言风格。
|
||||||
|
|
||||||
|
## 实施任务
|
||||||
|
|
||||||
|
### Task 1: 改造 API 文件为两类导入链路
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `ruoyi-ui/src/api/ccdiIntermediary.js`
|
||||||
|
- Test: `ruoyi-ui/tests/unit/intermediary-import-api.test.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写失败测试,固定 API 方法名与路径**
|
||||||
|
|
||||||
|
覆盖:
|
||||||
|
|
||||||
|
```js
|
||||||
|
importPersonTemplate
|
||||||
|
importPersonData
|
||||||
|
getPersonImportStatus
|
||||||
|
getPersonImportFailures
|
||||||
|
importEnterpriseRelationTemplate
|
||||||
|
importEnterpriseRelationData
|
||||||
|
getEnterpriseRelationImportStatus
|
||||||
|
getEnterpriseRelationImportFailures
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 保留中介信息导入 API**
|
||||||
|
|
||||||
|
中介信息导入继续使用:
|
||||||
|
|
||||||
|
```js
|
||||||
|
/ccdi/intermediary/importPersonTemplate
|
||||||
|
/ccdi/intermediary/importPersonData
|
||||||
|
/ccdi/intermediary/importPersonStatus/{taskId}
|
||||||
|
/ccdi/intermediary/importPersonFailures/{taskId}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 新增实体关联关系导入 API**
|
||||||
|
|
||||||
|
新增:
|
||||||
|
|
||||||
|
```js
|
||||||
|
/ccdi/intermediary/importEnterpriseRelationTemplate
|
||||||
|
/ccdi/intermediary/importEnterpriseRelationData
|
||||||
|
/ccdi/intermediary/importEnterpriseRelationStatus/{taskId}
|
||||||
|
/ccdi/intermediary/importEnterpriseRelationFailures/{taskId}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 删除前端对旧 `importEntity*` 导入接口的依赖**
|
||||||
|
|
||||||
|
Expected: 页面不再调用旧“机构中介导入”接口。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行 API 测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui
|
||||||
|
node tests/unit/intermediary-import-api.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS,方法名和 URL 与设计文档一致。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交 API 调整**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/api/ccdiIntermediary.js ruoyi-ui/tests/unit/intermediary-import-api.test.js
|
||||||
|
git commit -m "调整中介导入前端接口"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: 将导入弹窗改为场景驱动
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
|
||||||
|
- Test: `ruoyi-ui/tests/unit/intermediary-import-dialog.test.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写弹窗行为测试**
|
||||||
|
|
||||||
|
覆盖:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 1. 不再渲染“个人中介/机构中介”单选
|
||||||
|
// 2. scene=person 时调用中介信息导入地址
|
||||||
|
// 3. scene=enterpriseRelation 时调用实体关联关系导入地址
|
||||||
|
// 4. 模板文案随场景切换
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 把内部单选切换改成父组件传参**
|
||||||
|
|
||||||
|
新增 props:
|
||||||
|
|
||||||
|
```js
|
||||||
|
scene: {
|
||||||
|
type: String,
|
||||||
|
default: 'person'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 弹窗自身不再维护 `formData.importType`。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 根据 `scene` 计算上传地址和模板地址**
|
||||||
|
|
||||||
|
```js
|
||||||
|
scene === 'person'
|
||||||
|
scene === 'enterpriseRelation'
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 按场景返回不同提示文案**
|
||||||
|
|
||||||
|
中介信息导入提示需要包含:
|
||||||
|
|
||||||
|
```text
|
||||||
|
personSubType 为字典下拉;
|
||||||
|
本人行 relatedNumId 为空;
|
||||||
|
亲属行 relatedNumId 填关联中介本人证件号码。
|
||||||
|
```
|
||||||
|
|
||||||
|
实体关联关系导入提示需要包含:
|
||||||
|
|
||||||
|
```text
|
||||||
|
只导入中介与机构关系;
|
||||||
|
统一社会信用代码必须已存在于系统机构表。
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行弹窗测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui
|
||||||
|
node tests/unit/intermediary-import-dialog.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS,弹窗只根据外部场景工作。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交弹窗改造**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue ruoyi-ui/tests/unit/intermediary-import-dialog.test.js
|
||||||
|
git commit -m "改造中介导入弹窗场景切换"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: 改造页面顶部按钮与任务状态
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
|
||||||
|
- Test: `ruoyi-ui/tests/unit/intermediary-import-toolbar.test.js`
|
||||||
|
- Test: `ruoyi-ui/tests/unit/intermediary-import-state.test.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写按钮与状态测试**
|
||||||
|
|
||||||
|
覆盖:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 1. 顶部有两个导入按钮
|
||||||
|
// 2. 按钮文案正确
|
||||||
|
// 3. 两类失败记录按钮独立显示
|
||||||
|
// 4. localStorage 使用两套独立 key
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在工具栏增加两个导入按钮**
|
||||||
|
|
||||||
|
按钮文案固定为:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
导入中介信息
|
||||||
|
导入中介实体关联关系
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在页面中维护两套独立任务状态**
|
||||||
|
|
||||||
|
建议状态结构:
|
||||||
|
|
||||||
|
```js
|
||||||
|
personImportTask
|
||||||
|
enterpriseRelationImportTask
|
||||||
|
```
|
||||||
|
|
||||||
|
以及两套本地缓存 key:
|
||||||
|
|
||||||
|
```js
|
||||||
|
ccdi_intermediary_person_import_task
|
||||||
|
ccdi_intermediary_enterprise_relation_import_task
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 保持页面原有手工维护链路不变**
|
||||||
|
|
||||||
|
Expected: 打开详情、新增亲属、查询关系列表等既有逻辑继续走 `bizId`,不因导入改造改变页面其它交互。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行按钮与状态测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui
|
||||||
|
node tests/unit/intermediary-import-toolbar.test.js
|
||||||
|
node tests/unit/intermediary-import-state.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS,按钮与任务缓存隔离正确。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交页面状态改造**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/views/ccdiIntermediary/index.vue ruoyi-ui/tests/unit/intermediary-import-toolbar.test.js ruoyi-ui/tests/unit/intermediary-import-state.test.js
|
||||||
|
git commit -m "调整中介导入页面入口状态"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: 接入失败记录弹窗与完成态刷新
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
|
||||||
|
- Modify: `ruoyi-ui/tests/unit/intermediary-person-edit-ui.test.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写失败记录行为断言**
|
||||||
|
|
||||||
|
在现有中介页面测试里补充:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 1. 导入完成且 failureCount > 0 时显示对应失败记录按钮
|
||||||
|
// 2. 查看失败记录时调用对应接口
|
||||||
|
// 3. 清除历史记录后按钮消失
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 `index.vue` 中落两套失败记录弹窗状态**
|
||||||
|
|
||||||
|
建议状态:
|
||||||
|
|
||||||
|
```js
|
||||||
|
personFailureDialogVisible
|
||||||
|
enterpriseRelationFailureDialogVisible
|
||||||
|
personFailureList
|
||||||
|
enterpriseRelationFailureList
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 对齐员工导入的完成态逻辑**
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
```js
|
||||||
|
导入完成 -> 刷新列表 -> 刷新当前详情(如有) -> 更新失败按钮状态
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 对齐员工导入的历史记录恢复逻辑**
|
||||||
|
|
||||||
|
页面刷新后如果任务仍在缓存中:
|
||||||
|
|
||||||
|
```js
|
||||||
|
恢复失败记录按钮
|
||||||
|
恢复上次导入摘要
|
||||||
|
必要时继续轮询
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行页面行为测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui
|
||||||
|
node tests/unit/intermediary-person-edit-ui.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS,没有把导入状态逻辑打坏现有详情维护链路。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交失败记录与刷新逻辑**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/views/ccdiIntermediary/index.vue ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue ruoyi-ui/tests/unit/intermediary-person-edit-ui.test.js
|
||||||
|
git commit -m "补齐中介导入失败记录交互"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: 做前端验证并回写计划结果
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `docs/plans/frontend/2026-04-20-intermediary-import-frontend-implementation.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 使用 nvm 切换到仓库已验证版本**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui
|
||||||
|
source ~/.nvm/nvm.sh && nvm use 14.21.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 输出 `Now using node v14.21.3`。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行前端相关单测脚本**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui
|
||||||
|
node tests/unit/intermediary-import-api.test.js
|
||||||
|
node tests/unit/intermediary-import-dialog.test.js
|
||||||
|
node tests/unit/intermediary-import-toolbar.test.js
|
||||||
|
node tests/unit/intermediary-import-state.test.js
|
||||||
|
node tests/unit/intermediary-person-edit-ui.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 全部 PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 执行生产构建验证**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui
|
||||||
|
source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `BUILD SUCCESS`,没有新增语法或模块解析错误。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 回写执行结果**
|
||||||
|
|
||||||
|
在计划文档末尾补:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- 实际执行的 node / build 命令
|
||||||
|
- 各测试脚本 PASS / FAIL 结果
|
||||||
|
- 构建结果
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交验证结果**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/plans/frontend/2026-04-20-intermediary-import-frontend-implementation.md
|
||||||
|
git commit -m "补充中介导入前端实施记录"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui
|
||||||
|
source ~/.nvm/nvm.sh && nvm use 14.21.3
|
||||||
|
node tests/unit/intermediary-import-api.test.js
|
||||||
|
node tests/unit/intermediary-import-dialog.test.js
|
||||||
|
node tests/unit/intermediary-import-toolbar.test.js
|
||||||
|
node tests/unit/intermediary-import-state.test.js
|
||||||
|
node tests/unit/intermediary-person-edit-ui.test.js
|
||||||
|
source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完成标准
|
||||||
|
|
||||||
|
- 顶部只有“导入中介信息”和“导入中介实体关联关系”两个按钮
|
||||||
|
- 导入弹窗不再内部切换“个人/机构”,而是由页面场景驱动
|
||||||
|
- 两类导入的模板下载、上传、轮询、失败记录全部独立
|
||||||
|
- 现有手工维护链路继续保持 `bizId` 契约
|
||||||
|
- 页面支持恢复两类最近一次导入任务状态
|
||||||
|
- 导入提示文案已经明确 `personSubType` 字典下拉、`relationType` 废弃、`relatedNumId` 新语义
|
||||||
|
- 已使用 `nvm use 14.21.3` 完成前端测试脚本和生产构建验证
|
||||||
|
|
||||||
|
## 执行结果
|
||||||
|
|
||||||
|
- Node 命令:
|
||||||
|
`source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use 14.21.3`
|
||||||
|
结果:PASS(Now using node v14.21.3)
|
||||||
|
- Node 命令:
|
||||||
|
`source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use 14.21.3 >/dev/null && node tests/unit/intermediary-import-api.test.js && node tests/unit/intermediary-import-dialog.test.js && node tests/unit/intermediary-import-toolbar.test.js && node tests/unit/intermediary-import-state.test.js && node tests/unit/intermediary-person-edit-ui.test.js`
|
||||||
|
结果:PASS
|
||||||
|
- Node 命令:
|
||||||
|
`source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use 14.21.3 >/dev/null && npm run build:prod`
|
||||||
|
结果:PASS(构建成功,仅保留原有 bundle size warning)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AGENTS 全局规则同步实施记录
|
||||||
|
|
||||||
|
## 本次改动
|
||||||
|
|
||||||
|
- 更新根目录 `AGENTS.md` 的协作约定,补充 `.DS_Store` 忽略要求,以及 `using-superpowers` 只能在用户明确声明时启用的限制
|
||||||
|
- 更新根目录 `AGENTS.md` 的前端命令与开发测试提醒,明确前端相关命令执行前必须先通过 `nvm` 切换并确认 Node 版本
|
||||||
|
- 更新根目录 `AGENTS.md` 的文档维护要求,补充“每次改动均需留下实施文档”“前后端联动设计需拆分前后端实施计划”“写文档前先确认保存路径正确”等规则
|
||||||
|
- 在根目录 `AGENTS.md` 新增“方案规范”章节,约束方案输出必须走最短实现路径,不得擅自扩展兼容、补丁、兜底或降级方案,并要求完成全链路逻辑校验
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 影响文件仅限仓库根目录 `AGENTS.md`
|
||||||
|
- 本次为项目协作规范更新,不涉及业务代码、数据库脚本、前端页面或后端接口变更
|
||||||
|
|
||||||
|
## 验证说明
|
||||||
|
|
||||||
|
- 已确认实施记录保存路径为 `docs/reports/implementation/`
|
||||||
|
- 已人工核对新增规则与本次提供的全局 `AGENTS.md` 要求一致,且未覆盖或破坏仓库原有约束
|
||||||
|
- 本次仅修改文档,无需运行代码测试
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AGENTS 文档结构优化实施记录
|
||||||
|
|
||||||
|
## 本次改动
|
||||||
|
|
||||||
|
- 重构根目录 `AGENTS.md` 的章节结构,新增“高优先级规则”章节,将高频且强约束的协作规则前置
|
||||||
|
- 将原有分散在多个章节中的规则重新归类到“基础协作”“Git 与变更管理”“文档产出”“测试与运行”“数据库与编码”等小节
|
||||||
|
- 保留原有核心约束不变,仅优化文档层次、阅读顺序与检索效率
|
||||||
|
- 精简重复表达,例如将前后端实施计划拆分、实施记录留痕、`nvm` 使用、测试后关闭进程等规则统一收口到更明确的章节
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 影响文件仅限仓库根目录 `AGENTS.md`
|
||||||
|
- 本次为项目协作规范文档优化,不涉及业务代码、数据库脚本、前端页面或后端接口变更
|
||||||
|
|
||||||
|
## 验证说明
|
||||||
|
|
||||||
|
- 已确认实施记录保存路径位于 `docs/reports/implementation/`
|
||||||
|
- 已人工检查优化后的 `AGENTS.md`,确认原有关键规则仍然保留,且文档结构更清晰
|
||||||
|
- 本次仅修改文档,无需运行代码测试
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 中介库导入改造实施记录
|
||||||
|
|
||||||
|
## 基本信息
|
||||||
|
|
||||||
|
- 日期:2026-04-20
|
||||||
|
- 范围:中介库后端导入改造 + 前端导入入口与状态改造
|
||||||
|
- 关联设计:`docs/design/2026-04-20-intermediary-import-refactor-design.md`
|
||||||
|
- 关联计划:
|
||||||
|
`docs/plans/backend/2026-04-20-intermediary-import-backend-implementation.md`
|
||||||
|
`docs/plans/frontend/2026-04-20-intermediary-import-frontend-implementation.md`
|
||||||
|
|
||||||
|
## 实施内容
|
||||||
|
|
||||||
|
- 后端完成 `related_num_id` 语义切换,统一为“关联中介本人证件号码”,并补齐本人证件号变更同步、亲属唯一性收敛、统一列表联表条件切换。
|
||||||
|
- 后端完成“导入中介信息”链路重构,支持本人与亲属混合导入、同文件内引用先成功导入的本人、同亲属证件号挂到不同本人。
|
||||||
|
- 后端新增“导入中介实体关联关系”链路,按“本人证件号码 -> 本人 bizId -> 关系表”写入,并支持文件内去重、库内去重、失败记录回看。
|
||||||
|
- 前端完成中介导入入口改造,页面顶部改为“导入中介信息”“导入中介实体关联关系”两个按钮,导入弹窗改为 `scene` 驱动。
|
||||||
|
- 前端完成两类导入任务状态、本地缓存键、失败记录弹窗、历史任务恢复和完成态刷新逻辑,保留现有详情维护、亲属维护、关联机构维护的 `bizId` 契约。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 后端测试:
|
||||||
|
`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest,CcdiIntermediaryControllerTest,CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest test`
|
||||||
|
结果:PASS
|
||||||
|
- 后端编译:
|
||||||
|
`mvn -pl ccdi-info-collection -am clean compile`
|
||||||
|
结果:PASS
|
||||||
|
- 前端静态测试:
|
||||||
|
`source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use 14.21.3 >/dev/null && node tests/unit/intermediary-import-api.test.js && node tests/unit/intermediary-import-dialog.test.js && node tests/unit/intermediary-import-toolbar.test.js && node tests/unit/intermediary-import-state.test.js && node tests/unit/intermediary-person-edit-ui.test.js`
|
||||||
|
结果:PASS
|
||||||
|
- 前端构建:
|
||||||
|
`source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use 14.21.3 >/dev/null && npm run build:prod`
|
||||||
|
结果:PASS(仅有原有 bundle size warning)
|
||||||
|
|
||||||
|
## SQL 执行结果
|
||||||
|
|
||||||
|
- 已执行:
|
||||||
|
`bin/mysql_utf8_exec.sh sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql`
|
||||||
|
`bin/mysql_utf8_exec.sh sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql`
|
||||||
|
- 迁移后核查:
|
||||||
|
`post_migration_missing_parent = 1025`
|
||||||
|
`legacy_biz_id_reference = 0`
|
||||||
|
`owner_person_id_empty_after_migration = 754`
|
||||||
|
- 结论:可迁移数据已经完成语义切换,旧 `biz_id` 语义残留已清零;历史中仍有缺失本人映射或 `related_num_id` 为空的脏数据,需要后续专项清洗。
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# 实体库管理导入浏览器测试记录
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
|
||||||
|
- 在真实页面中下载“实体库管理”导入模板
|
||||||
|
- 基于下载模板生成覆盖全部后端显式校验分支的测试数据
|
||||||
|
- 通过页面上传测试文件并核对导入结果、失败记录与列表落库情况
|
||||||
|
|
||||||
|
## 测试环境
|
||||||
|
|
||||||
|
- 测试日期:2026-04-21
|
||||||
|
- 前端地址:`http://127.0.0.1:8080`
|
||||||
|
- 后端地址:`http://127.0.0.1:62318`
|
||||||
|
- 登录账号:`admin`
|
||||||
|
- 登录方式:浏览器页面登录
|
||||||
|
|
||||||
|
## 测试文件
|
||||||
|
|
||||||
|
- 页面下载模板:`/Users/wkc/Desktop/ccdi/ccdi/.playwright-cli/实体库管理模板-1776753846277.xlsx`
|
||||||
|
- 生成测试文件:`/Users/wkc/Desktop/ccdi/ccdi/output/spreadsheet/enterprise-base-info-import-browser-test.xlsx`
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. 登录系统后进入“信息维护 -> 实体库管理”页面。
|
||||||
|
2. 打开导入弹窗并点击“下载模板”。
|
||||||
|
3. 基于下载模板填写 11 条测试数据。
|
||||||
|
4. 在导入弹窗上传测试文件并点击“确定”。
|
||||||
|
5. 等待异步导入完成。
|
||||||
|
6. 核对列表新增数据、失败记录条数与失败原因。
|
||||||
|
|
||||||
|
## 测试数据覆盖范围
|
||||||
|
|
||||||
|
- 成功导入完整数据 1 条
|
||||||
|
- 成功导入最简数据 1 条
|
||||||
|
- 与库内已存在统一社会信用代码重复 1 条
|
||||||
|
- 文件内统一社会信用代码重复 1 条
|
||||||
|
- 企业名称为空 1 条
|
||||||
|
- 统一社会信用代码为空 1 条
|
||||||
|
- 统一社会信用代码格式错误 1 条
|
||||||
|
- 经营状态为空 1 条
|
||||||
|
- 风险等级非法 1 条
|
||||||
|
- 企业来源非法 1 条
|
||||||
|
- 数据来源非法 1 条
|
||||||
|
|
||||||
|
## 页面验证结果
|
||||||
|
|
||||||
|
- 模板下载成功。
|
||||||
|
- 上传请求成功发送到 `/dev-api/ccdi/enterpriseBaseInfo/importData`。
|
||||||
|
- 导入任务状态写入浏览器本地存储:
|
||||||
|
- `status=PARTIAL_SUCCESS`
|
||||||
|
- `totalCount=11`
|
||||||
|
- `successCount=2`
|
||||||
|
- `failureCount=9`
|
||||||
|
- 列表总数由 `2001` 增加到 `2003`,与成功导入 2 条一致。
|
||||||
|
- 新增记录已出现在列表顶部:
|
||||||
|
- `浏览器测试实体企业A / 992604210000000001`
|
||||||
|
- `浏览器测试实体企业B / 992604210000000002`
|
||||||
|
- “查看导入失败记录”弹窗成功展示 9 条失败数据。
|
||||||
|
|
||||||
|
## 失败原因核对
|
||||||
|
|
||||||
|
- `111333432959145585`:统一社会信用代码已存在
|
||||||
|
- `992604210000000002`:统一社会信用代码在导入文件中重复
|
||||||
|
- `992604210000000003`:企业名称不能为空
|
||||||
|
- 空统一社会信用代码:统一社会信用代码不能为空
|
||||||
|
- `ABC123`:统一社会信用代码格式不正确
|
||||||
|
- `992604210000000004`:经营状态不能为空
|
||||||
|
- `992604210000000005`:风险等级不在允许范围内
|
||||||
|
- `992604210000000006`:企业来源不在允许范围内
|
||||||
|
- `992604210000000007`:数据来源不在允许范围内
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
- 实体库管理导入功能在真实浏览器场景下可正常完成模板下载、文件上传、异步导入、成功入库和失败记录展示。
|
||||||
|
- 本次基于页面和后端实际行为验证,后端当前显式校验分支均已命中且返回结果符合预期。
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 实体库删除关联校验实施记录
|
||||||
|
|
||||||
|
## 基本信息
|
||||||
|
|
||||||
|
- 日期:2026-04-21
|
||||||
|
- 范围:实体库管理后端删除校验
|
||||||
|
- 关联计划:`docs/plans/backend/2026-04-21-enterprise-delete-relation-check-backend-implementation.md`
|
||||||
|
|
||||||
|
## 实施内容
|
||||||
|
|
||||||
|
- 在 `CcdiEnterpriseBaseInfoServiceImpl` 的 `deleteEnterpriseBaseInfoByIds` 中增加删除前校验逻辑。
|
||||||
|
- 删除前分别查询员工实体关联、信贷客户实体关联、中介关联机构三张关系表,只要任一表存在当前统一社会信用代码的关联记录,即终止删除。
|
||||||
|
- 失败提示按实际命中的关联类型拼装中文文案,例如“已关联员工”“已关联员工、信贷客户、中介”,便于页面直接回显原因。
|
||||||
|
- 保持“先全部校验、后统一删除”的处理方式,避免批量删除时出现部分删除成功、部分失败的不一致情况。
|
||||||
|
- 补充 `CcdiEnterpriseBaseInfoServiceImplTest`,覆盖删除成功、员工关联拦截、多关联拦截场景。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 执行命令:
|
||||||
|
`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiEnterpriseBaseInfoServiceImplTest test`
|
||||||
|
- 结果:PASS
|
||||||
|
- 备注:Maven 构建过程中仍存在项目原有 `ccdi-lsfx` 重复依赖声明 warning,本次改动未触碰该问题,不影响本次测试通过。
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# 中介库管理导入功能浏览器测试实施记录
|
||||||
|
|
||||||
|
## 本次操作
|
||||||
|
|
||||||
|
- 使用真实浏览器进入 `中介库管理` 页面。
|
||||||
|
- 分别下载两类导入模板:
|
||||||
|
- 中介和亲属信息导入模板
|
||||||
|
- 中介实体关联关系导入模板
|
||||||
|
- 基于下载模板生成浏览器回传测试文件,并在页面上传验证。
|
||||||
|
|
||||||
|
## 测试文件
|
||||||
|
|
||||||
|
- `output/spreadsheet/intermediary_person_import_browser_phase1.xlsx`
|
||||||
|
- `output/spreadsheet/intermediary_person_import_browser_phase2_existing_db_cases.xlsx`
|
||||||
|
- `output/spreadsheet/intermediary_enterprise_relation_import_browser_phase1.xlsx`
|
||||||
|
- `output/spreadsheet/intermediary_enterprise_relation_import_browser_phase2_db_duplicate.xlsx`
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
### 中介和亲属信息导入
|
||||||
|
|
||||||
|
- 第一轮导入结果:总数 `13`,成功 `4`,失败 `9`
|
||||||
|
- 第二轮导入结果:总数 `2`,成功 `0`,失败 `2`
|
||||||
|
- 成功数据已在页面列表可见:
|
||||||
|
- `自动化中介本人A`
|
||||||
|
- `自动化中介A配偶`
|
||||||
|
- `文件内重复本人1`
|
||||||
|
- `文件内重复亲属1`
|
||||||
|
- 页面失败记录已确认命中:
|
||||||
|
- 本人行关联字段错误
|
||||||
|
- 亲属缺少关联本人
|
||||||
|
- 姓名为空
|
||||||
|
- 人员子类型为空
|
||||||
|
- 证件号非法
|
||||||
|
- 文件内本人重复
|
||||||
|
- 关联本人不存在
|
||||||
|
- 文件内亲属重复
|
||||||
|
- 库内本人重复
|
||||||
|
- 库内亲属重复
|
||||||
|
|
||||||
|
### 中介实体关联关系导入
|
||||||
|
|
||||||
|
- 第一轮导入结果:总数 `11`,成功 `3`,失败 `8`
|
||||||
|
- 第二轮导入结果:总数 `1`,成功 `0`,失败 `1`
|
||||||
|
- 成功数据已在页面列表可见:
|
||||||
|
- `成都市资产企业 / 自动化中介本人A / 董事`
|
||||||
|
- `上海市资产企业 / 自动化中介本人A / 监事`
|
||||||
|
- `杭州市不动产合伙企业 / 自动化中介本人A / 法人`
|
||||||
|
- 页面失败记录已确认命中:
|
||||||
|
- 中介本人为空
|
||||||
|
- 中介本人证件号非法
|
||||||
|
- 中介本人不存在
|
||||||
|
- 统一社会信用代码为空
|
||||||
|
- 统一社会信用代码不存在
|
||||||
|
- 关联人职务超长
|
||||||
|
- 备注超长
|
||||||
|
- 文件内关系重复
|
||||||
|
- 库内关系重复
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 页面:`ruoyi-ui/src/views/ccdiIntermediary/`
|
||||||
|
- 后端接口:
|
||||||
|
- `/ccdi/intermediary/importPersonTemplate`
|
||||||
|
- `/ccdi/intermediary/importPersonData`
|
||||||
|
- `/ccdi/intermediary/importPersonStatus/{taskId}`
|
||||||
|
- `/ccdi/intermediary/importPersonFailures/{taskId}`
|
||||||
|
- `/ccdi/intermediary/importEnterpriseRelationTemplate`
|
||||||
|
- `/ccdi/intermediary/importEnterpriseRelationData`
|
||||||
|
- `/ccdi/intermediary/importEnterpriseRelationStatus/{taskId}`
|
||||||
|
- `/ccdi/intermediary/importEnterpriseRelationFailures/{taskId}`
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- `docs/tests/records/2026-04-21-intermediary-import-browser-test-record.md` 也已生成更完整测试记录,但该目录受当前仓库 `.gitignore` 规则影响,不进入版本管理。
|
||||||
|
- 测试结束后,已关闭本次启动的前端开发进程和 Playwright 浏览器;后端沿用原有 `62318` 进程,未做重启或停止。
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# 中介导入文案调整实施记录
|
||||||
|
|
||||||
|
## 基本信息
|
||||||
|
|
||||||
|
- 日期:2026-04-21
|
||||||
|
- 范围:中介库前端导入入口文案调整
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
- 将页面主按钮文案从“导入中介信息”改为“导入中介和亲属信息”。
|
||||||
|
- 将失败记录入口和失败记录弹窗标题同步改为“中介和亲属信息”表述。
|
||||||
|
- 将导入弹窗下载模板名称同步改为“中介和亲属信息导入模板”。
|
||||||
|
- 同步更新前端静态测试断言,确保页面文案与测试基线一致。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 已执行:
|
||||||
|
`source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use 14.21.3 >/dev/null && node tests/unit/intermediary-import-toolbar.test.js && node tests/unit/intermediary-import-dialog.test.js && node tests/unit/intermediary-import-state.test.js && node tests/unit/intermediary-person-edit-ui.test.js`
|
||||||
|
- 结果:PASS
|
||||||
41
docs/reports/implementation/2026-04-21-redis断连自动重连修复实施记录.md
Normal file
41
docs/reports/implementation/2026-04-21-redis断连自动重连修复实施记录.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Redis 断连自动重连修复实施记录
|
||||||
|
|
||||||
|
## 1. 本次修改内容
|
||||||
|
|
||||||
|
- 在 `ruoyi-framework` 的 Redis 配置中新增 Lettuce 连接工厂后处理器。
|
||||||
|
- 对 Spring Boot 自动装配的 `LettuceConnectionFactory` 统一开启 `validateConnection`。
|
||||||
|
- 新增回归测试,校验连接工厂初始化时已开启连接校验。
|
||||||
|
- 为 `ruoyi-framework` 补充测试依赖。
|
||||||
|
|
||||||
|
## 2. 修改原因
|
||||||
|
|
||||||
|
后端 Redis 连接在发生断连后,存在持续复用失效连接的风险,导致 Redis 恢复后应用侧仍无法正常访问缓存。此次修复通过在连接工厂层开启连接校验,缩短恢复路径,避免业务代码层面持续拿到不可用连接。
|
||||||
|
|
||||||
|
## 3. 实际变更文件
|
||||||
|
|
||||||
|
- `ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java`
|
||||||
|
- `ruoyi-framework/src/test/java/com/ruoyi/framework/config/RedisConfigTest.java`
|
||||||
|
- `ruoyi-framework/pom.xml`
|
||||||
|
|
||||||
|
## 4. 验证结果
|
||||||
|
|
||||||
|
已执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -pl ruoyi-framework -am test -Dtest=RedisConfigTest -Dsurefire.failIfNoSpecifiedTests=false
|
||||||
|
```
|
||||||
|
|
||||||
|
执行结果:
|
||||||
|
|
||||||
|
- `BUILD SUCCESS`
|
||||||
|
- `RedisConfigTest` 通过
|
||||||
|
|
||||||
|
## 5. 影响范围
|
||||||
|
|
||||||
|
- 后端 Redis 连接恢复行为
|
||||||
|
- 所有依赖 `RedisTemplate` 与 `RedisCache` 的缓存、登录态、验证码、限流等功能
|
||||||
|
|
||||||
|
## 6. 备注
|
||||||
|
|
||||||
|
- 本次未改动 Redis 地址、密码、库索引、连接池大小等运行参数。
|
||||||
|
- 本次未引入新的 Redis 客户端,仍保持现有 Spring Data Redis + Lettuce 方案。
|
||||||
@@ -89,11 +89,11 @@ spring:
|
|||||||
# 地址
|
# 地址
|
||||||
host: 116.62.17.81
|
host: 116.62.17.81
|
||||||
# 端口,默认为6379
|
# 端口,默认为6379
|
||||||
port: 6379
|
port: 56379
|
||||||
# 数据库索引
|
# 数据库索引
|
||||||
database: 0
|
database: 9
|
||||||
# 密码
|
# 密码
|
||||||
password: Kfcx@1234
|
password: N0f3d12c4a927eee1+
|
||||||
# 连接超时时间
|
# 连接超时时间
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
lettuce:
|
lettuce:
|
||||||
|
|||||||
@@ -59,6 +59,13 @@
|
|||||||
<artifactId>ruoyi-system</artifactId>
|
<artifactId>ruoyi-system</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 测试依赖 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package com.ruoyi.framework.config;
|
|||||||
|
|
||||||
import org.springframework.cache.annotation.CachingConfigurerSupport;
|
import org.springframework.cache.annotation.CachingConfigurerSupport;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
@@ -19,6 +21,23 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
|
|||||||
@EnableCaching
|
@EnableCaching
|
||||||
public class RedisConfig extends CachingConfigurerSupport
|
public class RedisConfig extends CachingConfigurerSupport
|
||||||
{
|
{
|
||||||
|
@Bean
|
||||||
|
public static BeanPostProcessor lettuceConnectionFactoryBeanPostProcessor()
|
||||||
|
{
|
||||||
|
return new BeanPostProcessor()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Object postProcessBeforeInitialization(Object bean, String beanName)
|
||||||
|
{
|
||||||
|
if (bean instanceof LettuceConnectionFactory lettuceConnectionFactory)
|
||||||
|
{
|
||||||
|
lettuceConnectionFactory.setValidateConnection(true);
|
||||||
|
}
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
||||||
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
|
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
|
||||||
|
|||||||
@@ -178,14 +178,6 @@ export function importPersonTemplate() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载机构中介导入模板
|
|
||||||
export function importEntityTemplate() {
|
|
||||||
return request({
|
|
||||||
url: '/ccdi/intermediary/importEntityTemplate',
|
|
||||||
method: 'post'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入个人中介黑名单
|
// 导入个人中介黑名单
|
||||||
export function importPersonData(data, updateSupport) {
|
export function importPersonData(data, updateSupport) {
|
||||||
return request({
|
return request({
|
||||||
@@ -195,12 +187,11 @@ export function importPersonData(data, updateSupport) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导入机构中介黑名单
|
// 下载中介实体关联关系导入模板
|
||||||
export function importEntityData(data, updateSupport) {
|
export function importEnterpriseRelationTemplate() {
|
||||||
return request({
|
return request({
|
||||||
url: '/ccdi/intermediary/importEntityData?updateSupport=' + updateSupport,
|
url: '/ccdi/intermediary/importEnterpriseRelationTemplate',
|
||||||
method: 'post',
|
method: 'post'
|
||||||
data: data
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,18 +212,27 @@ export function getPersonImportFailures(taskId, pageNum, pageSize) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询实体中介导入状态
|
// 导入中介实体关联关系
|
||||||
export function getEntityImportStatus(taskId) {
|
export function importEnterpriseRelationData(data, updateSupport) {
|
||||||
return request({
|
return request({
|
||||||
url: `/ccdi/intermediary/importEntityStatus/${taskId}`,
|
url: '/ccdi/intermediary/importEnterpriseRelationData?updateSupport=' + updateSupport,
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询中介实体关联关系导入状态
|
||||||
|
export function getEnterpriseRelationImportStatus(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/ccdi/intermediary/importEnterpriseRelationStatus/${taskId}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询实体中介导入失败记录
|
// 查询中介实体关联关系导入失败记录
|
||||||
export function getEntityImportFailures(taskId, pageNum, pageSize) {
|
export function getEnterpriseRelationImportFailures(taskId, pageNum, pageSize) {
|
||||||
return request({
|
return request({
|
||||||
url: `/ccdi/intermediary/importEntityFailures/${taskId}`,
|
url: `/ccdi/intermediary/importEnterpriseRelationFailures/${taskId}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { pageNum, pageSize }
|
params: { pageNum, pageSize }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<el-descriptions-item label="姓名">{{ detailData.name || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="姓名">{{ detailData.name || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="证件号">{{ detailData.personId || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="证件号">{{ detailData.personId || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="人员类型">{{ detailData.personType || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="人员类型">{{ detailData.personType || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="中介子类型">{{ detailData.personSubType || detailData.relationType || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="性别">{{ formatGender(detailData.gender) }}</el-descriptions-item>
|
<el-descriptions-item label="性别">{{ formatGender(detailData.gender) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="证件类型">{{ detailData.idType || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="证件类型">{{ detailData.idType || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="职位">{{ detailData.position || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="职位">{{ detailData.position || '-' }}</el-descriptions-item>
|
||||||
|
|||||||
@@ -26,6 +26,27 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<!-- v-model="form.personSubType" -->
|
||||||
|
<el-form-item label="中介子类型">
|
||||||
|
<el-select
|
||||||
|
v-model="localForm.personSubType"
|
||||||
|
placeholder="请选择中介子类型"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handlePersonSubTypeChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in personSubTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="证件类型">
|
<el-form-item label="证件类型">
|
||||||
<el-select v-model="localForm.idType" placeholder="请选择证件类型" clearable style="width: 100%">
|
<el-select v-model="localForm.idType" placeholder="请选择证件类型" clearable style="width: 100%">
|
||||||
@@ -131,6 +152,10 @@ export default {
|
|||||||
certTypeOptions: {
|
certTypeOptions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
personSubTypeOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -162,6 +187,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePersonSubTypeChange(value) {
|
||||||
|
const typeMappings = [
|
||||||
|
{ label: '个人', value: '本人' }
|
||||||
|
];
|
||||||
|
return typeMappings.find(item => item.value === value) || null;
|
||||||
|
},
|
||||||
handleSubmit() {
|
handleSubmit() {
|
||||||
this.$refs.formRef.validate(valid => {
|
this.$refs.formRef.validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- 导入对话框 -->
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:title="title"
|
:title="title"
|
||||||
:visible.sync="visible"
|
:visible.sync="visible"
|
||||||
@@ -13,22 +12,18 @@
|
|||||||
:close-on-press-escape="false"
|
:close-on-press-escape="false"
|
||||||
custom-class="import-dialog-wrapper"
|
custom-class="import-dialog-wrapper"
|
||||||
>
|
>
|
||||||
<!-- 全屏Loading遮罩层 -->
|
|
||||||
<div v-show="isUploading" class="import-loading-overlay">
|
<div v-show="isUploading" class="import-loading-overlay">
|
||||||
<i class="el-icon-loading"></i>
|
<i class="el-icon-loading"></i>
|
||||||
<p>正在导入中,请稍候...</p>
|
<p>正在导入中,请稍候...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form :model="formData" label-position="top" size="medium">
|
<el-form label-position="top" size="medium">
|
||||||
<!-- 导入类型 -->
|
<el-form-item label="导入说明">
|
||||||
<el-form-item label="导入类型">
|
<div class="scene-tips">
|
||||||
<el-radio-group v-model="formData.importType" @change="handleImportTypeChange" style="width: 100%">
|
<p v-for="item in sceneTips" :key="item">{{ item }}</p>
|
||||||
<el-radio label="person" border>个人中介</el-radio>
|
</div>
|
||||||
<el-radio label="entity" border>机构中介</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 文件上传 -->
|
|
||||||
<el-form-item label="选择文件">
|
<el-form-item label="选择文件">
|
||||||
<el-upload
|
<el-upload
|
||||||
ref="upload"
|
ref="upload"
|
||||||
@@ -53,7 +48,6 @@
|
|||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 下载模板 -->
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-link type="primary" :underline="false" @click="handleDownloadTemplate">
|
<el-link type="primary" :underline="false" @click="handleDownloadTemplate">
|
||||||
<i class="el-icon-download"></i>
|
<i class="el-icon-download"></i>
|
||||||
@@ -62,7 +56,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<!-- 底部按钮 -->
|
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -79,7 +72,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 导入结果对话框 -->
|
|
||||||
<import-result-dialog
|
<import-result-dialog
|
||||||
:visible.sync="importResultVisible"
|
:visible.sync="importResultVisible"
|
||||||
:content="importResultContent"
|
:content="importResultContent"
|
||||||
@@ -90,10 +82,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {getToken} from "@/utils/auth";
|
import { getToken } from "@/utils/auth";
|
||||||
import {getEntityImportStatus, getPersonImportStatus} from "@/api/ccdiIntermediary";
|
import {
|
||||||
|
getEnterpriseRelationImportStatus,
|
||||||
|
getPersonImportStatus
|
||||||
|
} from "@/api/ccdiIntermediary";
|
||||||
import ImportResultDialog from "@/components/ImportResultDialog.vue";
|
import ImportResultDialog from "@/components/ImportResultDialog.vue";
|
||||||
|
|
||||||
|
const PERSON_SCENE = "person";
|
||||||
|
const ENTERPRISE_RELATION_SCENE = "enterpriseRelation";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ImportDialog",
|
name: "ImportDialog",
|
||||||
components: { ImportResultDialog },
|
components: { ImportResultDialog },
|
||||||
@@ -105,32 +103,55 @@ export default {
|
|||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "数据导入"
|
default: "数据导入"
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
type: String,
|
||||||
|
default: PERSON_SCENE
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formData: {
|
|
||||||
importType: "person"
|
|
||||||
},
|
|
||||||
headers: { Authorization: "Bearer " + getToken() },
|
headers: { Authorization: "Bearer " + getToken() },
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
isFileSelected: false,
|
isFileSelected: false,
|
||||||
// 导入结果弹窗
|
|
||||||
importResultVisible: false,
|
importResultVisible: false,
|
||||||
importResultContent: "",
|
importResultContent: "",
|
||||||
// 轮询状态
|
|
||||||
pollingTimer: null,
|
pollingTimer: null,
|
||||||
currentTaskId: null
|
currentTaskId: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
sceneConfig() {
|
||||||
|
if (this.scene === ENTERPRISE_RELATION_SCENE) {
|
||||||
|
return {
|
||||||
|
uploadPath: "/ccdi/intermediary/importEnterpriseRelationData",
|
||||||
|
templatePath: "ccdi/intermediary/importEnterpriseRelationTemplate",
|
||||||
|
templateName: "中介实体关联关系导入模板",
|
||||||
|
statusApi: getEnterpriseRelationImportStatus,
|
||||||
|
tips: [
|
||||||
|
"只导入中介与机构关系;",
|
||||||
|
"统一社会信用代码必须已存在于系统机构表。"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
uploadPath: "/ccdi/intermediary/importPersonData",
|
||||||
|
templatePath: "ccdi/intermediary/importPersonTemplate",
|
||||||
|
templateName: "中介和亲属信息导入模板",
|
||||||
|
statusApi: getPersonImportStatus,
|
||||||
|
tips: [
|
||||||
|
"personSubType 为字典下拉;",
|
||||||
|
"本人行 relatedNumId 为空;",
|
||||||
|
"亲属行 relatedNumId 填关联中介本人证件号码。"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
},
|
||||||
uploadUrl() {
|
uploadUrl() {
|
||||||
const baseUrl = process.env.VUE_APP_BASE_API;
|
const baseUrl = process.env.VUE_APP_BASE_API;
|
||||||
if (this.formData.importType === 'person') {
|
return `${baseUrl}${this.sceneConfig.uploadPath}`;
|
||||||
return `${baseUrl}/ccdi/intermediary/importPersonData`;
|
},
|
||||||
} else {
|
sceneTips() {
|
||||||
return `${baseUrl}/ccdi/intermediary/importEntityData`;
|
return this.sceneConfig.tips;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -145,21 +166,14 @@ export default {
|
|||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
},
|
},
|
||||||
handleCancel() {
|
handleCancel() {
|
||||||
// 通过 $emit 通知父组件更新 visible 状态,而不是直接修改 prop
|
this.$emit("update:visible", false);
|
||||||
this.$emit('update:visible', false);
|
|
||||||
},
|
|
||||||
handleImportTypeChange() {
|
|
||||||
if (this.$refs.upload) {
|
|
||||||
this.$refs.upload.clearFiles();
|
|
||||||
}
|
|
||||||
this.isFileSelected = false;
|
|
||||||
},
|
},
|
||||||
handleDownloadTemplate() {
|
handleDownloadTemplate() {
|
||||||
if (this.formData.importType === 'person') {
|
this.download(
|
||||||
this.download('ccdi/intermediary/importPersonTemplate', {}, `个人中介黑名单模板_${new Date().getTime()}.xlsx`);
|
this.sceneConfig.templatePath,
|
||||||
} else {
|
{},
|
||||||
this.download('ccdi/intermediary/importEntityTemplate', {}, `机构中介黑名单模板_${new Date().getTime()}.xlsx`);
|
`${this.sceneConfig.templateName}_${new Date().getTime()}.xlsx`
|
||||||
}
|
);
|
||||||
},
|
},
|
||||||
handleFileUploadProgress() {
|
handleFileUploadProgress() {
|
||||||
this.isUploading = true;
|
this.isUploading = true;
|
||||||
@@ -177,28 +191,30 @@ export default {
|
|||||||
const taskId = response.data.taskId;
|
const taskId = response.data.taskId;
|
||||||
this.currentTaskId = taskId;
|
this.currentTaskId = taskId;
|
||||||
|
|
||||||
// 显示通知
|
|
||||||
this.$notify({
|
this.$notify({
|
||||||
title: '导入任务已提交',
|
title: "导入任务已提交",
|
||||||
message: '正在后台处理中,处理完成后将通知您',
|
message: "正在后台处理中,处理完成后将通知您",
|
||||||
type: 'info',
|
type: "info",
|
||||||
duration: 3000
|
duration: 3000
|
||||||
});
|
});
|
||||||
|
|
||||||
// 关闭对话框 - 使用$emit更新父组件的visible
|
this.$emit("task-created", {
|
||||||
this.$emit('update:visible', false);
|
scene: this.scene,
|
||||||
this.$refs.upload.clearFiles();
|
taskId,
|
||||||
|
status: "PROCESSING"
|
||||||
|
});
|
||||||
|
this.$emit("update:visible", false);
|
||||||
|
this.$emit("success", { scene: this.scene, taskId });
|
||||||
|
|
||||||
// 通知父组件刷新列表
|
if (this.$refs.upload) {
|
||||||
this.$emit("success");
|
this.$refs.upload.clearFiles();
|
||||||
|
}
|
||||||
|
|
||||||
// 开始轮询
|
|
||||||
this.startImportStatusPolling(taskId);
|
this.startImportStatusPolling(taskId);
|
||||||
} else {
|
} else {
|
||||||
this.$modal.msgError(response.msg || '导入失败');
|
this.$modal.msgError(response.msg || "导入失败");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 导入结果弹窗关闭
|
|
||||||
handleImportResultClose() {
|
handleImportResultClose() {
|
||||||
this.importResultVisible = false;
|
this.importResultVisible = false;
|
||||||
this.importResultContent = "";
|
this.importResultContent = "";
|
||||||
@@ -206,77 +222,65 @@ export default {
|
|||||||
handleFileError() {
|
handleFileError() {
|
||||||
this.isUploading = false;
|
this.isUploading = false;
|
||||||
this.$modal.msgError("导入失败,请检查文件格式是否正确");
|
this.$modal.msgError("导入失败,请检查文件格式是否正确");
|
||||||
this.$refs.upload.clearFiles();
|
if (this.$refs.upload) {
|
||||||
|
this.$refs.upload.clearFiles();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleSubmit() {
|
handleSubmit() {
|
||||||
// 触发清除历史记录事件
|
this.$emit("clear-import-history", this.scene);
|
||||||
this.$emit('clear-import-history', this.formData.importType);
|
|
||||||
|
|
||||||
// 提交文件上传
|
|
||||||
this.$refs.upload.submit();
|
this.$refs.upload.submit();
|
||||||
},
|
},
|
||||||
/** 开始轮询导入状态 */
|
|
||||||
startImportStatusPolling(taskId) {
|
startImportStatusPolling(taskId) {
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
const maxPolls = 150; // 最多5分钟
|
const maxPolls = 150;
|
||||||
|
|
||||||
this.pollingTimer = setInterval(async () => {
|
this.pollingTimer = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
pollCount++;
|
pollCount++;
|
||||||
|
|
||||||
if (pollCount > maxPolls) {
|
if (pollCount > maxPolls) {
|
||||||
clearInterval(this.pollingTimer);
|
clearInterval(this.pollingTimer);
|
||||||
this.$modal.msgWarning('导入任务处理超时,请联系管理员');
|
this.$modal.msgWarning("导入任务处理超时,请联系管理员");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据导入类型调用不同的API
|
const response = await this.sceneConfig.statusApi(taskId);
|
||||||
const apiMethod = this.formData.importType === 'person'
|
if (response.data && response.data.status !== "PROCESSING") {
|
||||||
? getPersonImportStatus
|
|
||||||
: getEntityImportStatus;
|
|
||||||
|
|
||||||
const response = await apiMethod(taskId);
|
|
||||||
|
|
||||||
if (response.data && response.data.status !== 'PROCESSING') {
|
|
||||||
clearInterval(this.pollingTimer);
|
clearInterval(this.pollingTimer);
|
||||||
this.handleImportComplete(response.data);
|
this.handleImportComplete(response.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearInterval(this.pollingTimer);
|
clearInterval(this.pollingTimer);
|
||||||
this.$modal.msgError('查询导入状态失败: ' + error.message);
|
this.$modal.msgError("查询导入状态失败: " + error.message);
|
||||||
}
|
}
|
||||||
}, 2000); // 每2秒轮询一次
|
}, 2000);
|
||||||
},
|
},
|
||||||
/** 处理导入完成 */
|
|
||||||
handleImportComplete(statusResult) {
|
handleImportComplete(statusResult) {
|
||||||
if (statusResult.status === 'SUCCESS') {
|
if (statusResult.status === "SUCCESS") {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
title: '导入完成',
|
title: "导入完成",
|
||||||
message: `全部成功!共导入${statusResult.totalCount}条数据`,
|
message: `全部成功!共导入${statusResult.totalCount}条数据`,
|
||||||
type: 'success',
|
type: "success",
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
} else if (statusResult.failureCount > 0) {
|
} else if (statusResult.failureCount > 0) {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
title: '导入完成',
|
title: "导入完成",
|
||||||
message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`,
|
message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`,
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知父组件更新失败记录状态
|
|
||||||
this.$emit("import-complete", {
|
this.$emit("import-complete", {
|
||||||
|
scene: this.scene,
|
||||||
taskId: statusResult.taskId,
|
taskId: statusResult.taskId,
|
||||||
hasFailures: statusResult.failureCount > 0,
|
hasFailures: statusResult.failureCount > 0,
|
||||||
importType: this.formData.importType,
|
|
||||||
totalCount: statusResult.totalCount,
|
totalCount: statusResult.totalCount,
|
||||||
successCount: statusResult.successCount,
|
successCount: statusResult.successCount,
|
||||||
failureCount: statusResult.failureCount
|
failureCount: statusResult.failureCount
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/** 组件销毁时清除定时器 */
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.pollingTimer) {
|
if (this.pollingTimer) {
|
||||||
clearInterval(this.pollingTimer);
|
clearInterval(this.pollingTimer);
|
||||||
@@ -292,35 +296,6 @@ export default {
|
|||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-radio-group {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.el-radio {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
margin-right: 0;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
.el-radio__label {
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
.el-radio__label {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-bordered {
|
|
||||||
padding: 10px 0;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-upload {
|
.el-upload {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -355,6 +330,18 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scene-tips {
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.7;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 5px 0 0;
|
padding: 5px 0 0;
|
||||||
|
|||||||
@@ -17,6 +17,44 @@
|
|||||||
v-hasPermi="['ccdi:intermediary:add']"
|
v-hasPermi="['ccdi:intermediary:add']"
|
||||||
>新增</el-button>
|
>新增</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
icon="el-icon-upload2"
|
||||||
|
size="mini"
|
||||||
|
@click="handleOpenPersonImport"
|
||||||
|
v-hasPermi="['ccdi:intermediary:import']"
|
||||||
|
>导入中介和亲属信息</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
icon="el-icon-connection"
|
||||||
|
size="mini"
|
||||||
|
@click="handleOpenEnterpriseRelationImport"
|
||||||
|
v-hasPermi="['ccdi:intermediary:import']"
|
||||||
|
>导入中介实体关联关系</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5" v-if="personImportTask && personImportTask.failureCount > 0">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-warning"
|
||||||
|
size="mini"
|
||||||
|
@click="viewPersonImportFailures"
|
||||||
|
>查看中介和亲属信息导入失败记录</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5" v-if="enterpriseRelationImportTask && enterpriseRelationImportTask.failureCount > 0">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-warning-outline"
|
||||||
|
size="mini"
|
||||||
|
@click="viewEnterpriseRelationImportFailures"
|
||||||
|
>查看中介实体关联关系导入失败记录</el-button>
|
||||||
|
</el-col>
|
||||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
@@ -36,6 +74,7 @@
|
|||||||
:visible.sync="personDialogVisible"
|
:visible.sync="personDialogVisible"
|
||||||
:title="personDialogTitle"
|
:title="personDialogTitle"
|
||||||
:form="personForm"
|
:form="personForm"
|
||||||
|
:person-sub-type-options="relationTypeOptions"
|
||||||
:indiv-type-options="indivTypeOptions"
|
:indiv-type-options="indivTypeOptions"
|
||||||
:gender-options="genderOptions"
|
:gender-options="genderOptions"
|
||||||
:cert-type-options="certTypeOptions"
|
:cert-type-options="certTypeOptions"
|
||||||
@@ -80,6 +119,87 @@
|
|||||||
@edit-enterprise-relation="handleEditEnterpriseRelationFromDetail"
|
@edit-enterprise-relation="handleEditEnterpriseRelationFromDetail"
|
||||||
@delete-enterprise-relation="handleDelete"
|
@delete-enterprise-relation="handleDelete"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<import-dialog
|
||||||
|
:visible.sync="importDialogVisible"
|
||||||
|
:title="importDialogTitle"
|
||||||
|
:scene="importScene"
|
||||||
|
@task-created="handleImportTaskCreated"
|
||||||
|
@success="handleImportDialogSuccess"
|
||||||
|
@import-complete="handleImportComplete"
|
||||||
|
@clear-import-history="handleClearImportHistory"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="中介和亲属信息导入失败记录"
|
||||||
|
:visible.sync="personFailureDialogVisible"
|
||||||
|
width="1200px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
v-if="personLastImportInfo"
|
||||||
|
:title="personLastImportInfo"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 15px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table :data="personFailureList" v-loading="personFailureLoading">
|
||||||
|
<el-table-column label="姓名" prop="name" align="center" />
|
||||||
|
<el-table-column label="人员子类型" prop="personSubType" align="center" />
|
||||||
|
<el-table-column label="证件号码" prop="personId" align="center" min-width="180" />
|
||||||
|
<el-table-column label="关联中介本人证件号码" prop="relatedNumId" align="center" min-width="180" />
|
||||||
|
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="220" :show-overflow-tooltip="true" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="personFailureTotal > 0"
|
||||||
|
:total="personFailureTotal"
|
||||||
|
:page.sync="personFailureQueryParams.pageNum"
|
||||||
|
:limit.sync="personFailureQueryParams.pageSize"
|
||||||
|
@pagination="getPersonFailureList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="personFailureDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button type="danger" plain @click="clearPersonImportHistory">清除历史记录</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="中介实体关联关系导入失败记录"
|
||||||
|
:visible.sync="enterpriseRelationFailureDialogVisible"
|
||||||
|
width="1200px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
v-if="enterpriseRelationLastImportInfo"
|
||||||
|
:title="enterpriseRelationLastImportInfo"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 15px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table :data="enterpriseRelationFailureList" v-loading="enterpriseRelationFailureLoading">
|
||||||
|
<el-table-column label="中介本人证件号码" prop="ownerPersonId" align="center" min-width="180" />
|
||||||
|
<el-table-column label="统一社会信用代码" prop="socialCreditCode" align="center" min-width="180" />
|
||||||
|
<el-table-column label="关联人职务" prop="relationPersonPost" align="center" />
|
||||||
|
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="220" :show-overflow-tooltip="true" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="enterpriseRelationFailureTotal > 0"
|
||||||
|
:total="enterpriseRelationFailureTotal"
|
||||||
|
:page.sync="enterpriseRelationFailureQueryParams.pageNum"
|
||||||
|
:limit.sync="enterpriseRelationFailureQueryParams.pageSize"
|
||||||
|
@pagination="getEnterpriseRelationFailureList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="enterpriseRelationFailureDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button type="danger" plain @click="clearEnterpriseRelationImportHistory">清除历史记录</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -91,8 +211,12 @@ import {
|
|||||||
delIntermediary,
|
delIntermediary,
|
||||||
delIntermediaryEnterpriseRelation,
|
delIntermediaryEnterpriseRelation,
|
||||||
delIntermediaryRelative,
|
delIntermediaryRelative,
|
||||||
|
getEnterpriseRelationImportFailures,
|
||||||
|
getEnterpriseRelationImportStatus,
|
||||||
getIntermediaryEnterpriseRelation,
|
getIntermediaryEnterpriseRelation,
|
||||||
getIntermediaryRelative,
|
getIntermediaryRelative,
|
||||||
|
getPersonImportFailures,
|
||||||
|
getPersonImportStatus,
|
||||||
getPersonIntermediary,
|
getPersonIntermediary,
|
||||||
listIntermediary,
|
listIntermediary,
|
||||||
listIntermediaryEnterpriseRelations,
|
listIntermediaryEnterpriseRelations,
|
||||||
@@ -113,6 +237,10 @@ import EditDialog from "./components/EditDialog";
|
|||||||
import DetailDialog from "./components/DetailDialog";
|
import DetailDialog from "./components/DetailDialog";
|
||||||
import RelativeEditDialog from "./components/RelativeEditDialog";
|
import RelativeEditDialog from "./components/RelativeEditDialog";
|
||||||
import EnterpriseRelationEditDialog from "./components/EnterpriseRelationEditDialog";
|
import EnterpriseRelationEditDialog from "./components/EnterpriseRelationEditDialog";
|
||||||
|
import ImportDialog from "./components/ImportDialog";
|
||||||
|
|
||||||
|
const PERSON_SCENE = "person";
|
||||||
|
const ENTERPRISE_RELATION_SCENE = "enterpriseRelation";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Intermediary",
|
name: "Intermediary",
|
||||||
@@ -122,7 +250,8 @@ export default {
|
|||||||
EditDialog,
|
EditDialog,
|
||||||
DetailDialog,
|
DetailDialog,
|
||||||
RelativeEditDialog,
|
RelativeEditDialog,
|
||||||
EnterpriseRelationEditDialog
|
EnterpriseRelationEditDialog,
|
||||||
|
ImportDialog
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -159,7 +288,32 @@ export default {
|
|||||||
relativeList: [],
|
relativeList: [],
|
||||||
enterpriseRelationList: [],
|
enterpriseRelationList: [],
|
||||||
currentIntermediaryId: null,
|
currentIntermediaryId: null,
|
||||||
currentOwnerName: ""
|
currentOwnerName: "",
|
||||||
|
importDialogVisible: false,
|
||||||
|
importDialogTitle: "",
|
||||||
|
importScene: PERSON_SCENE,
|
||||||
|
personImportTask: null,
|
||||||
|
enterpriseRelationImportTask: null,
|
||||||
|
personImportPollingTimer: null,
|
||||||
|
enterpriseRelationImportPollingTimer: null,
|
||||||
|
personFailureDialogVisible: false,
|
||||||
|
personFailureList: [],
|
||||||
|
personFailureLoading: false,
|
||||||
|
personFailureTotal: 0,
|
||||||
|
personFailureQueryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10
|
||||||
|
},
|
||||||
|
personLastImportInfo: "",
|
||||||
|
enterpriseRelationFailureDialogVisible: false,
|
||||||
|
enterpriseRelationFailureList: [],
|
||||||
|
enterpriseRelationFailureLoading: false,
|
||||||
|
enterpriseRelationFailureTotal: 0,
|
||||||
|
enterpriseRelationFailureQueryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10
|
||||||
|
},
|
||||||
|
enterpriseRelationLastImportInfo: ""
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -168,6 +322,11 @@ export default {
|
|||||||
this.resetEnterpriseRelationForm();
|
this.resetEnterpriseRelationForm();
|
||||||
this.getList();
|
this.getList();
|
||||||
this.loadEnumOptions();
|
this.loadEnumOptions();
|
||||||
|
this.restoreImportTasks();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.clearImportPolling(PERSON_SCENE);
|
||||||
|
this.clearImportPolling(ENTERPRISE_RELATION_SCENE);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadEnumOptions() {
|
loadEnumOptions() {
|
||||||
@@ -252,6 +411,206 @@ export default {
|
|||||||
this.personDialogTitle = "新增中介本人";
|
this.personDialogTitle = "新增中介本人";
|
||||||
this.personDialogVisible = true;
|
this.personDialogVisible = true;
|
||||||
},
|
},
|
||||||
|
handleOpenPersonImport() {
|
||||||
|
this.importScene = PERSON_SCENE;
|
||||||
|
this.importDialogTitle = "导入中介和亲属信息";
|
||||||
|
this.importDialogVisible = true;
|
||||||
|
},
|
||||||
|
handleOpenEnterpriseRelationImport() {
|
||||||
|
this.importScene = ENTERPRISE_RELATION_SCENE;
|
||||||
|
this.importDialogTitle = "导入中介实体关联关系";
|
||||||
|
this.importDialogVisible = true;
|
||||||
|
},
|
||||||
|
handleImportDialogSuccess() {
|
||||||
|
// 导入任务创建后由任务轮询负责状态更新
|
||||||
|
},
|
||||||
|
handleImportTaskCreated(payload) {
|
||||||
|
const task = {
|
||||||
|
...payload,
|
||||||
|
failureCount: 0,
|
||||||
|
successCount: 0,
|
||||||
|
totalCount: 0,
|
||||||
|
saveTime: Date.now()
|
||||||
|
};
|
||||||
|
this.setImportTask(payload.scene, task);
|
||||||
|
this.saveImportTaskToStorage(payload.scene, task);
|
||||||
|
},
|
||||||
|
handleImportComplete(payload) {
|
||||||
|
const task = {
|
||||||
|
...payload,
|
||||||
|
status: payload.hasFailures ? "PARTIAL_SUCCESS" : "SUCCESS",
|
||||||
|
saveTime: Date.now()
|
||||||
|
};
|
||||||
|
this.setImportTask(payload.scene, task);
|
||||||
|
this.saveImportTaskToStorage(payload.scene, task);
|
||||||
|
if (payload.scene === PERSON_SCENE) {
|
||||||
|
this.personLastImportInfo = this.buildImportSummary(task);
|
||||||
|
} else {
|
||||||
|
this.enterpriseRelationLastImportInfo = this.buildImportSummary(task);
|
||||||
|
}
|
||||||
|
this.getList();
|
||||||
|
this.refreshCurrentDetail();
|
||||||
|
},
|
||||||
|
handleClearImportHistory(scene) {
|
||||||
|
this.clearImportTaskFromStorage(scene);
|
||||||
|
this.setImportTask(scene, null);
|
||||||
|
if (scene === PERSON_SCENE) {
|
||||||
|
this.personFailureList = [];
|
||||||
|
this.personFailureTotal = 0;
|
||||||
|
this.personLastImportInfo = "";
|
||||||
|
} else {
|
||||||
|
this.enterpriseRelationFailureList = [];
|
||||||
|
this.enterpriseRelationFailureTotal = 0;
|
||||||
|
this.enterpriseRelationLastImportInfo = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setImportTask(scene, task) {
|
||||||
|
if (scene === PERSON_SCENE) {
|
||||||
|
this.personImportTask = task;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.enterpriseRelationImportTask = task;
|
||||||
|
},
|
||||||
|
getImportTask(scene) {
|
||||||
|
return scene === PERSON_SCENE ? this.personImportTask : this.enterpriseRelationImportTask;
|
||||||
|
},
|
||||||
|
getImportStorageKey(scene) {
|
||||||
|
if (scene === PERSON_SCENE) {
|
||||||
|
return "ccdi_intermediary_person_import_task";
|
||||||
|
}
|
||||||
|
return "ccdi_intermediary_enterprise_relation_import_task";
|
||||||
|
},
|
||||||
|
saveImportTaskToStorage(scene, task) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(this.getImportStorageKey(scene), JSON.stringify(task));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("保存中介导入任务状态失败:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getImportTaskFromStorage(scene) {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(this.getImportStorageKey(scene));
|
||||||
|
return raw ? JSON.parse(raw) : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("读取中介导入任务状态失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearImportTaskFromStorage(scene) {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(this.getImportStorageKey(scene));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("清除中介导入任务状态失败:", error);
|
||||||
|
}
|
||||||
|
this.clearImportPolling(scene);
|
||||||
|
},
|
||||||
|
restoreImportTasks() {
|
||||||
|
[PERSON_SCENE, ENTERPRISE_RELATION_SCENE].forEach((scene) => {
|
||||||
|
const task = this.getImportTaskFromStorage(scene);
|
||||||
|
if (!task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setImportTask(scene, task);
|
||||||
|
if (scene === PERSON_SCENE) {
|
||||||
|
this.personLastImportInfo = this.buildImportSummary(task);
|
||||||
|
} else {
|
||||||
|
this.enterpriseRelationLastImportInfo = this.buildImportSummary(task);
|
||||||
|
}
|
||||||
|
if (task.status === "PROCESSING") {
|
||||||
|
this.resumeImportPolling(scene, task.taskId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resumeImportPolling(scene, taskId) {
|
||||||
|
this.clearImportPolling(scene);
|
||||||
|
const timerKey = scene === PERSON_SCENE ? "personImportPollingTimer" : "enterpriseRelationImportPollingTimer";
|
||||||
|
const getStatus = scene === PERSON_SCENE ? getPersonImportStatus : getEnterpriseRelationImportStatus;
|
||||||
|
let pollCount = 0;
|
||||||
|
this[timerKey] = setInterval(async () => {
|
||||||
|
pollCount++;
|
||||||
|
if (pollCount > 150) {
|
||||||
|
this.clearImportPolling(scene);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await getStatus(taskId);
|
||||||
|
if (response.data && response.data.status !== "PROCESSING") {
|
||||||
|
this.clearImportPolling(scene);
|
||||||
|
this.handleImportComplete({
|
||||||
|
scene,
|
||||||
|
taskId: response.data.taskId,
|
||||||
|
hasFailures: response.data.failureCount > 0,
|
||||||
|
totalCount: response.data.totalCount,
|
||||||
|
successCount: response.data.successCount,
|
||||||
|
failureCount: response.data.failureCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearImportPolling(scene) {
|
||||||
|
const timerKey = scene === PERSON_SCENE ? "personImportPollingTimer" : "enterpriseRelationImportPollingTimer";
|
||||||
|
if (this[timerKey]) {
|
||||||
|
clearInterval(this[timerKey]);
|
||||||
|
this[timerKey] = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildImportSummary(task) {
|
||||||
|
if (!task) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `任务ID: ${task.taskId},总数 ${task.totalCount || 0} 条,成功 ${task.successCount || 0} 条,失败 ${task.failureCount || 0} 条`;
|
||||||
|
},
|
||||||
|
viewPersonImportFailures() {
|
||||||
|
this.personFailureDialogVisible = true;
|
||||||
|
this.personFailureQueryParams.pageNum = 1;
|
||||||
|
this.personLastImportInfo = this.buildImportSummary(this.personImportTask);
|
||||||
|
this.getPersonFailureList();
|
||||||
|
},
|
||||||
|
getPersonFailureList() {
|
||||||
|
if (!this.personImportTask) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.personFailureLoading = true;
|
||||||
|
getPersonImportFailures(
|
||||||
|
this.personImportTask.taskId,
|
||||||
|
this.personFailureQueryParams.pageNum,
|
||||||
|
this.personFailureQueryParams.pageSize
|
||||||
|
).then((response) => {
|
||||||
|
this.personFailureList = response.rows || [];
|
||||||
|
this.personFailureTotal = response.total || 0;
|
||||||
|
}).finally(() => {
|
||||||
|
this.personFailureLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearPersonImportHistory() {
|
||||||
|
this.personFailureDialogVisible = false;
|
||||||
|
this.handleClearImportHistory(PERSON_SCENE);
|
||||||
|
},
|
||||||
|
viewEnterpriseRelationImportFailures() {
|
||||||
|
this.enterpriseRelationFailureDialogVisible = true;
|
||||||
|
this.enterpriseRelationFailureQueryParams.pageNum = 1;
|
||||||
|
this.enterpriseRelationLastImportInfo = this.buildImportSummary(this.enterpriseRelationImportTask);
|
||||||
|
this.getEnterpriseRelationFailureList();
|
||||||
|
},
|
||||||
|
getEnterpriseRelationFailureList() {
|
||||||
|
if (!this.enterpriseRelationImportTask) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.enterpriseRelationFailureLoading = true;
|
||||||
|
getEnterpriseRelationImportFailures(
|
||||||
|
this.enterpriseRelationImportTask.taskId,
|
||||||
|
this.enterpriseRelationFailureQueryParams.pageNum,
|
||||||
|
this.enterpriseRelationFailureQueryParams.pageSize
|
||||||
|
).then((response) => {
|
||||||
|
this.enterpriseRelationFailureList = response.rows || [];
|
||||||
|
this.enterpriseRelationFailureTotal = response.total || 0;
|
||||||
|
}).finally(() => {
|
||||||
|
this.enterpriseRelationFailureLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearEnterpriseRelationImportHistory() {
|
||||||
|
this.enterpriseRelationFailureDialogVisible = false;
|
||||||
|
this.handleClearImportHistory(ENTERPRISE_RELATION_SCENE);
|
||||||
|
},
|
||||||
handleDetail(row) {
|
handleDetail(row) {
|
||||||
if (row.recordType === "INTERMEDIARY") {
|
if (row.recordType === "INTERMEDIARY") {
|
||||||
this.openIntermediaryDetail(row.recordId);
|
this.openIntermediaryDetail(row.recordId);
|
||||||
|
|||||||
45
ruoyi-ui/tests/unit/intermediary-import-api.test.js
Normal file
45
ruoyi-ui/tests/unit/intermediary-import-api.test.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const apiPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/api/ccdiIntermediary.js"
|
||||||
|
);
|
||||||
|
const source = fs.readFileSync(apiPath, "utf8");
|
||||||
|
|
||||||
|
[
|
||||||
|
"export function importPersonTemplate()",
|
||||||
|
"export function importPersonData(data, updateSupport)",
|
||||||
|
"export function getPersonImportStatus(taskId)",
|
||||||
|
"export function getPersonImportFailures(taskId, pageNum, pageSize)",
|
||||||
|
"export function importEnterpriseRelationTemplate()",
|
||||||
|
"export function importEnterpriseRelationData(data, updateSupport)",
|
||||||
|
"export function getEnterpriseRelationImportStatus(taskId)",
|
||||||
|
"export function getEnterpriseRelationImportFailures(taskId, pageNum, pageSize)",
|
||||||
|
"/ccdi/intermediary/importPersonTemplate",
|
||||||
|
"/ccdi/intermediary/importPersonData",
|
||||||
|
"/ccdi/intermediary/importPersonStatus/",
|
||||||
|
"/ccdi/intermediary/importPersonFailures/",
|
||||||
|
"/ccdi/intermediary/importEnterpriseRelationTemplate",
|
||||||
|
"/ccdi/intermediary/importEnterpriseRelationData",
|
||||||
|
"/ccdi/intermediary/importEnterpriseRelationStatus/",
|
||||||
|
"/ccdi/intermediary/importEnterpriseRelationFailures/",
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `中介导入 API 缺少关键方法或路径: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
"export function importEntityTemplate()",
|
||||||
|
"export function importEntityData(data, updateSupport)",
|
||||||
|
"export function getEntityImportStatus(taskId)",
|
||||||
|
"export function getEntityImportFailures(taskId, pageNum, pageSize)",
|
||||||
|
"/ccdi/intermediary/importEntityTemplate",
|
||||||
|
"/ccdi/intermediary/importEntityData",
|
||||||
|
"/ccdi/intermediary/importEntityStatus/",
|
||||||
|
"/ccdi/intermediary/importEntityFailures/",
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(!source.includes(token), `中介导入 API 不应继续保留旧机构导入接口: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("intermediary-import-api test passed");
|
||||||
40
ruoyi-ui/tests/unit/intermediary-import-dialog.test.js
Normal file
40
ruoyi-ui/tests/unit/intermediary-import-dialog.test.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const componentPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/views/ccdiIntermediary/components/ImportDialog.vue"
|
||||||
|
);
|
||||||
|
const source = fs.readFileSync(componentPath, "utf8");
|
||||||
|
|
||||||
|
[
|
||||||
|
"scene: {",
|
||||||
|
"default: PERSON_SCENE",
|
||||||
|
"this.scene === ENTERPRISE_RELATION_SCENE",
|
||||||
|
"/ccdi/intermediary/importPersonData",
|
||||||
|
"/ccdi/intermediary/importEnterpriseRelationData",
|
||||||
|
"ccdi/intermediary/importPersonTemplate",
|
||||||
|
"ccdi/intermediary/importEnterpriseRelationTemplate",
|
||||||
|
"getPersonImportStatus",
|
||||||
|
"getEnterpriseRelationImportStatus",
|
||||||
|
"personSubType 为字典下拉;",
|
||||||
|
"本人行 relatedNumId 为空;",
|
||||||
|
"亲属行 relatedNumId 填关联中介本人证件号码。",
|
||||||
|
"只导入中介与机构关系;",
|
||||||
|
"统一社会信用代码必须已存在于系统机构表。"
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `中介导入弹窗缺少场景驱动能力: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
'label="导入类型"',
|
||||||
|
"个人中介",
|
||||||
|
"机构中介",
|
||||||
|
"formData.importType",
|
||||||
|
"handleImportTypeChange"
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(!source.includes(token), `中介导入弹窗不应继续保留内部类型切换: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("intermediary-import-dialog test passed");
|
||||||
28
ruoyi-ui/tests/unit/intermediary-import-state.test.js
Normal file
28
ruoyi-ui/tests/unit/intermediary-import-state.test.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const pagePath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/views/ccdiIntermediary/index.vue"
|
||||||
|
);
|
||||||
|
const source = fs.readFileSync(pagePath, "utf8");
|
||||||
|
|
||||||
|
[
|
||||||
|
"ccdi_intermediary_person_import_task",
|
||||||
|
"ccdi_intermediary_enterprise_relation_import_task",
|
||||||
|
"personFailureDialogVisible",
|
||||||
|
"enterpriseRelationFailureDialogVisible",
|
||||||
|
"personFailureList",
|
||||||
|
"enterpriseRelationFailureList",
|
||||||
|
"getPersonImportFailures",
|
||||||
|
"getEnterpriseRelationImportFailures",
|
||||||
|
"getPersonImportStatus",
|
||||||
|
"getEnterpriseRelationImportStatus",
|
||||||
|
"refreshCurrentDetail()",
|
||||||
|
"restoreImportTasks()"
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `中介导入页面缺少任务恢复或失败记录状态: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("intermediary-import-state test passed");
|
||||||
22
ruoyi-ui/tests/unit/intermediary-import-toolbar.test.js
Normal file
22
ruoyi-ui/tests/unit/intermediary-import-toolbar.test.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const pagePath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/views/ccdiIntermediary/index.vue"
|
||||||
|
);
|
||||||
|
const source = fs.readFileSync(pagePath, "utf8");
|
||||||
|
|
||||||
|
[
|
||||||
|
"导入中介和亲属信息",
|
||||||
|
"导入中介实体关联关系",
|
||||||
|
"查看中介和亲属信息导入失败记录",
|
||||||
|
"查看中介实体关联关系导入失败记录",
|
||||||
|
"personImportTask",
|
||||||
|
"enterpriseRelationImportTask"
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `中介导入页面缺少按钮或任务状态: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("intermediary-import-toolbar test passed");
|
||||||
@@ -10,9 +10,14 @@ const detailDialogPath = path.resolve(
|
|||||||
__dirname,
|
__dirname,
|
||||||
"../../src/views/ccdiIntermediary/components/DetailDialog.vue"
|
"../../src/views/ccdiIntermediary/components/DetailDialog.vue"
|
||||||
);
|
);
|
||||||
|
const pagePath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/views/ccdiIntermediary/index.vue"
|
||||||
|
);
|
||||||
|
|
||||||
const editDialogSource = fs.readFileSync(editDialogPath, "utf8");
|
const editDialogSource = fs.readFileSync(editDialogPath, "utf8");
|
||||||
const detailDialogSource = fs.readFileSync(detailDialogPath, "utf8");
|
const detailDialogSource = fs.readFileSync(detailDialogPath, "utf8");
|
||||||
|
const pageSource = fs.readFileSync(pagePath, "utf8");
|
||||||
|
|
||||||
[
|
[
|
||||||
'label="中介子类型"',
|
'label="中介子类型"',
|
||||||
@@ -52,4 +57,19 @@ assert(
|
|||||||
"个人中介详情不应继续展示单独的关系类型字段"
|
"个人中介详情不应继续展示单独的关系类型字段"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
[
|
||||||
|
"personFailureDialogVisible",
|
||||||
|
"enterpriseRelationFailureDialogVisible",
|
||||||
|
"viewPersonImportFailures",
|
||||||
|
"viewEnterpriseRelationImportFailures",
|
||||||
|
"clearPersonImportHistory",
|
||||||
|
"clearEnterpriseRelationImportHistory",
|
||||||
|
"refreshCurrentDetail()",
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(
|
||||||
|
pageSource.includes(token),
|
||||||
|
`中介页面缺少导入失败记录或详情刷新逻辑: ${token}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
console.log("intermediary-person-edit-ui test passed");
|
console.log("intermediary-person-edit-ui test passed");
|
||||||
|
|||||||
153
sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql
Normal file
153
sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET CHARACTER SET utf8mb4;
|
||||||
|
SET character_set_client = utf8mb4;
|
||||||
|
SET character_set_connection = utf8mb4;
|
||||||
|
SET character_set_results = utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||||
|
SELECT '人员子类型', 'ccdi_person_sub_type', '0', 'admin', NOW(), '中介黑名单-人员子类型'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_dict_type
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_dict_type
|
||||||
|
SET dict_name = '人员子类型',
|
||||||
|
status = '0',
|
||||||
|
remark = '中介黑名单-人员子类型',
|
||||||
|
update_by = 'admin',
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type';
|
||||||
|
|
||||||
|
UPDATE sys_dict_data
|
||||||
|
SET dict_sort = 1,
|
||||||
|
dict_label = '本人',
|
||||||
|
css_class = '',
|
||||||
|
list_class = 'default',
|
||||||
|
is_default = 'N',
|
||||||
|
status = '0',
|
||||||
|
update_by = 'admin',
|
||||||
|
update_time = NOW(),
|
||||||
|
remark = '中介黑名单-人员子类型'
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '本人';
|
||||||
|
|
||||||
|
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||||
|
SELECT 1, '本人', '本人', 'ccdi_person_sub_type', '', 'default', 'N', '0', 'admin', NOW(), '中介黑名单-人员子类型'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_dict_data
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '本人'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_dict_data
|
||||||
|
SET dict_sort = 2,
|
||||||
|
dict_label = '配偶',
|
||||||
|
css_class = '',
|
||||||
|
list_class = 'default',
|
||||||
|
is_default = 'N',
|
||||||
|
status = '0',
|
||||||
|
update_by = 'admin',
|
||||||
|
update_time = NOW(),
|
||||||
|
remark = '中介黑名单-人员子类型'
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '配偶';
|
||||||
|
|
||||||
|
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||||
|
SELECT 2, '配偶', '配偶', 'ccdi_person_sub_type', '', 'default', 'N', '0', 'admin', NOW(), '中介黑名单-人员子类型'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_dict_data
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '配偶'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_dict_data
|
||||||
|
SET dict_sort = 3,
|
||||||
|
dict_label = '子女',
|
||||||
|
css_class = '',
|
||||||
|
list_class = 'default',
|
||||||
|
is_default = 'N',
|
||||||
|
status = '0',
|
||||||
|
update_by = 'admin',
|
||||||
|
update_time = NOW(),
|
||||||
|
remark = '中介黑名单-人员子类型'
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '子女';
|
||||||
|
|
||||||
|
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||||
|
SELECT 3, '子女', '子女', 'ccdi_person_sub_type', '', 'default', 'N', '0', 'admin', NOW(), '中介黑名单-人员子类型'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_dict_data
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '子女'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_dict_data
|
||||||
|
SET dict_sort = 4,
|
||||||
|
dict_label = '父母',
|
||||||
|
css_class = '',
|
||||||
|
list_class = 'default',
|
||||||
|
is_default = 'N',
|
||||||
|
status = '0',
|
||||||
|
update_by = 'admin',
|
||||||
|
update_time = NOW(),
|
||||||
|
remark = '中介黑名单-人员子类型'
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '父母';
|
||||||
|
|
||||||
|
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||||
|
SELECT 4, '父母', '父母', 'ccdi_person_sub_type', '', 'default', 'N', '0', 'admin', NOW(), '中介黑名单-人员子类型'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_dict_data
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '父母'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_dict_data
|
||||||
|
SET dict_sort = 5,
|
||||||
|
dict_label = '兄弟姐妹',
|
||||||
|
css_class = '',
|
||||||
|
list_class = 'default',
|
||||||
|
is_default = 'N',
|
||||||
|
status = '0',
|
||||||
|
update_by = 'admin',
|
||||||
|
update_time = NOW(),
|
||||||
|
remark = '中介黑名单-人员子类型'
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '兄弟姐妹';
|
||||||
|
|
||||||
|
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||||
|
SELECT 5, '兄弟姐妹', '兄弟姐妹', 'ccdi_person_sub_type', '', 'default', 'N', '0', 'admin', NOW(), '中介黑名单-人员子类型'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_dict_data
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '兄弟姐妹'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_dict_data
|
||||||
|
SET dict_sort = 6,
|
||||||
|
dict_label = '其他',
|
||||||
|
css_class = '',
|
||||||
|
list_class = 'default',
|
||||||
|
is_default = 'N',
|
||||||
|
status = '0',
|
||||||
|
update_by = 'admin',
|
||||||
|
update_time = NOW(),
|
||||||
|
remark = '中介黑名单-人员子类型'
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '其他';
|
||||||
|
|
||||||
|
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||||
|
SELECT 6, '其他', '其他', 'ccdi_person_sub_type', '', 'default', 'N', '0', 'admin', NOW(), '中介黑名单-人员子类型'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_dict_data
|
||||||
|
WHERE dict_type = 'ccdi_person_sub_type'
|
||||||
|
AND dict_value = '其他'
|
||||||
|
);
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET CHARACTER SET utf8mb4;
|
||||||
|
SET character_set_client = utf8mb4;
|
||||||
|
SET character_set_connection = utf8mb4;
|
||||||
|
SET character_set_results = utf8mb4;
|
||||||
|
|
||||||
|
-- 迁移前检查 1:找不到对应本人 biz_id 的亲属记录
|
||||||
|
SELECT
|
||||||
|
child.biz_id AS child_biz_id,
|
||||||
|
child.name AS child_name,
|
||||||
|
child.person_id AS child_person_id,
|
||||||
|
child.related_num_id AS legacy_owner_biz_id
|
||||||
|
FROM ccdi_biz_intermediary child
|
||||||
|
LEFT JOIN ccdi_biz_intermediary parent
|
||||||
|
ON child.related_num_id = parent.biz_id
|
||||||
|
AND parent.person_sub_type = '本人'
|
||||||
|
WHERE child.person_sub_type <> '本人'
|
||||||
|
AND (child.related_num_id IS NULL OR parent.biz_id IS NULL);
|
||||||
|
|
||||||
|
-- 迁移前检查 2:本人 person_id 为空的记录
|
||||||
|
SELECT
|
||||||
|
child.biz_id AS child_biz_id,
|
||||||
|
child.name AS child_name,
|
||||||
|
child.person_id AS child_person_id,
|
||||||
|
child.related_num_id AS legacy_owner_biz_id,
|
||||||
|
parent.biz_id AS parent_biz_id,
|
||||||
|
parent.name AS parent_name,
|
||||||
|
parent.person_id AS parent_person_id
|
||||||
|
FROM ccdi_biz_intermediary child
|
||||||
|
JOIN ccdi_biz_intermediary parent
|
||||||
|
ON child.related_num_id = parent.biz_id
|
||||||
|
AND parent.person_sub_type = '本人'
|
||||||
|
WHERE child.person_sub_type <> '本人'
|
||||||
|
AND (parent.person_id IS NULL OR TRIM(parent.person_id) = '');
|
||||||
|
|
||||||
|
-- 迁移前检查 3:迁移后同一中介本人下 related_num_id + person_id 冲突的记录
|
||||||
|
SELECT
|
||||||
|
parent.person_id AS owner_person_id,
|
||||||
|
child.person_id AS relative_person_id,
|
||||||
|
COUNT(*) AS conflict_count,
|
||||||
|
GROUP_CONCAT(child.biz_id ORDER BY child.biz_id SEPARATOR ',') AS child_biz_ids
|
||||||
|
FROM ccdi_biz_intermediary child
|
||||||
|
JOIN ccdi_biz_intermediary parent
|
||||||
|
ON child.related_num_id = parent.biz_id
|
||||||
|
AND parent.person_sub_type = '本人'
|
||||||
|
WHERE child.person_sub_type <> '本人'
|
||||||
|
AND parent.person_id IS NOT NULL
|
||||||
|
AND TRIM(parent.person_id) <> ''
|
||||||
|
GROUP BY parent.person_id, child.person_id
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
UPDATE ccdi_biz_intermediary child
|
||||||
|
JOIN ccdi_biz_intermediary parent
|
||||||
|
ON child.related_num_id = parent.biz_id
|
||||||
|
SET child.related_num_id = parent.person_id
|
||||||
|
WHERE child.person_sub_type <> '本人';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- 迁移后检查:亲属记录应统一按本人证件号码关联
|
||||||
|
SELECT
|
||||||
|
child.biz_id AS child_biz_id,
|
||||||
|
child.related_num_id AS owner_person_id,
|
||||||
|
parent.biz_id AS parent_biz_id,
|
||||||
|
parent.person_id AS parent_person_id
|
||||||
|
FROM ccdi_biz_intermediary child
|
||||||
|
LEFT JOIN ccdi_biz_intermediary parent
|
||||||
|
ON child.related_num_id = parent.person_id
|
||||||
|
AND parent.person_sub_type = '本人'
|
||||||
|
WHERE child.person_sub_type <> '本人'
|
||||||
|
AND (child.related_num_id IS NULL OR parent.biz_id IS NULL);
|
||||||
Reference in New Issue
Block a user