115 Commits

Author SHA1 Message Date
wjj
75d094dad4 支持拉取行内和金综流水 2026-07-20 13:08:33 +08:00
wjj
1561df924f 修复流水明细导出权限校验 2026-07-20 11:26:58 +08:00
wjj
657977dfb7 完善异常账户预警口径和账户查询 2026-07-20 11:13:54 +08:00
wjj
4a75d68bf6 修复流水上传身份证号回填 2026-07-20 11:03:05 +08:00
wjj
b70f967153 调整涉疑交易默认每页十条 2026-07-16 19:05:34 +08:00
wjj
6492fca2dd 设计涉疑交易默认分页调整 2026-07-16 18:52:47 +08:00
wjj
e0188b5e5b 修复涉疑交易筛选与标签错乱 2026-07-16 18:46:51 +08:00
wjj
c3b56bdf2a 设计涉疑交易筛选一致性优化 2026-07-16 18:01:14 +08:00
wjj
6d90bb4e58 优化无外部人员涉疑交易查询 2026-07-16 15:26:14 +08:00
wjj
4a7cfde4af 调整专项排查资产收入匹配口径 2026-07-16 14:47:04 +08:00
wjj
15aa9117c3 调整员工信息维护列表和个人资料校验 2026-07-16 11:28:55 +08:00
wjj
ded07a2c33 优化前端页面布局细节 2026-07-13 16:46:37 +08:00
wjj
7a317204f7 调整流水打标规则口径 2026-07-13 13:05:35 +08:00
wjj
2c6c982ed3 账户库中介风险等级固定为高 2026-07-10 17:46:51 +08:00
wjj
486a499af7 优化流水账号入库和明细筛选 2026-07-10 17:33:16 +08:00
wjj
c999497693 优化外部人员风险明细模型筛选 2026-07-09 16:19:08 +08:00
wjj
3f3e9268f2 实现结果总览排除可疑预警 2026-07-09 14:49:02 +08:00
wjj
9c02812675 合并dev最新代码到dev-ui 2026-07-06 09:05:10 +08:00
wkc
b7e9a8da03 校验上传流水文件名身份证信息 2026-07-03 17:38:46 +08:00
wjj
bb41fd7e89 新增新增贷款查询和导出 2026-07-03 10:01:17 +08:00
wjj
8b9226643c 新增涉疑交易明细筛选 2026-07-03 09:15:14 +08:00
wjj
6d2f40843a 调整项目列表入口和操作按钮 2026-07-02 17:48:08 +08:00
wkc
21c4e91f41 修复专项核查图谱项目ID传递 2026-07-02 17:04:22 +08:00
wkc
87fb6443e6 test 2026-07-02 16:48:17 +08:00
wkc
979ed9669f 调整测试目录忽略规则 2026-07-02 16:46:57 +08:00
wkc
57a33098c9 实现流程项目逻辑删除与恢复 2026-07-02 10:54:36 +08:00
wkc
2f53fc4d1e 用户角色选择改为下拉单选 2026-07-02 10:53:01 +08:00
wkc
64ddc362e3 新增项目管理员角色权限控制 2026-07-01 17:02:17 +08:00
wkc
6c70149e0c 项目上传删除暂不调用流水分析平台 2026-07-01 17:02:17 +08:00
wkc
2298f9e589 移除风险模型导出按钮 2026-07-01 17:02:17 +08:00
wkc
e08f437acc 为风险明细列表添加分页选择器 2026-07-01 17:00:11 +08:00
wjj
6134ec06af 放开涉疑交易流水级模型命中 2026-07-01 16:58:16 +08:00
wjj
dfa35ebd85 优化涉疑交易模型口径和报告展示 2026-07-01 16:58:16 +08:00
wkc
1f82c7e16e 优化资金图谱主题节点检索 2026-07-01 16:57:31 +08:00
wkc
6c206deb5f 实现结果总览详情资产和征信页签 2026-07-01 16:57:31 +08:00
wkc
2999d7cf7d 调整项目人数按流水证件号统计 2026-07-01 16:55:09 +08:00
wjj
f42981f153 放开涉疑交易流水级模型命中 2026-07-01 16:53:44 +08:00
wkc
67f206be1b Refactor project pages and update related docs 2026-07-01 16:47:26 +08:00
wjj
66d34c7ceb 优化涉疑交易模型口径和报告展示 2026-07-01 16:46:23 +08:00
wkc
2e30b2a877 修正双员工夫妻家庭专项核查口径 2026-07-01 16:37:00 +08:00
wkc
d2cdb0f3c9 修复风险总览无风险人员负数问题 2026-07-01 16:37:00 +08:00
wkc
aaec746181 优化资产估值万元展示 2026-07-01 16:37:00 +08:00
wkc
dcfd869d84 Implement credit parse result polling and sentinel handling 2026-07-01 16:37:00 +08:00
wkc
d284923c59 调整征信解析返回解析和日志 2026-07-01 16:37:00 +08:00
wkc
84b04e31ed Refactor credit parse to use remote HTML paths 2026-07-01 16:37:00 +08:00
wkc
507bc1c2d7 Remove obsolete code and documentation 2026-07-01 16:37:00 +08:00
wkc
64891a621d 移除前端默认登录凭据 2026-07-01 16:37:00 +08:00
wkc
606b4f836b 生产配置 2026-07-01 16:37:00 +08:00
wkc
33c1f9df90 新增生产统一部署脚本 2026-07-01 16:37:00 +08:00
wkc
bee1e800ce Fix PDF font loading for project overview reports 2026-07-01 16:37:00 +08:00
wkc
a191cc7ca3 新增专项排查图谱展示 2026-07-01 16:33:59 +08:00
wkc
7826645a90 新增专项排查图谱前端实施计划 2026-07-01 16:31:35 +08:00
wkc
9b11426617 新增专项排查图谱嵌入设计 2026-07-01 16:31:35 +08:00
wkc
1494491d89 展示员工亲属实体关联统信码 2026-07-01 16:31:35 +08:00
wkc
5509ece708 调整招聘信息毕业年月选择控件 2026-07-01 16:31:35 +08:00
wkc
02e45dadcc 完善招聘信息主键关联与工作经历维护 2026-07-01 16:31:35 +08:00
wkc
caeac0f115 招聘导入模板增加招聘类型下拉框 2026-07-01 16:31:35 +08:00
wkc
a4005d3e9f 修复导入模板格式和必填标记 2026-07-01 16:31:35 +08:00
wkc
f9e0a079f5 Update import templates and relation query fields 2026-07-01 16:31:35 +08:00
wkc
e00d60f5c1 回测四类导入自动补入实体库 2026-07-01 16:31:35 +08:00
wkc
87da17bccb 修复双Sheet资产单独导入任务ID 2026-07-01 16:31:35 +08:00
wkc
4a9c6ba64b 实现关联业务自动补入实体库 2026-07-01 16:31:35 +08:00
wkc
17eb11b3b8 uat配置更新 2026-07-01 16:31:35 +08:00
wkc
b740d4c100 移除.DS_Store跟踪 2026-07-01 16:31:35 +08:00
wkc
f5a98bbd7a 修订: 中介实体补入无需机构名称 2026-07-01 16:31:35 +08:00
wkc
987afc3863 调整信息维护页面并修复项目概览统计 2026-07-01 16:31:35 +08:00
wkc
051650370e 计划: 员工资产导入与实体库自动补入修复 2026-07-01 16:28:16 +08:00
wkc
c211413bd2 修订: 明确资产Sheet单独导入规则 2026-07-01 16:28:16 +08:00
wkc
bb9994a339 修订: 完善员工资产导入与实体库补入设计 2026-07-01 16:28:16 +08:00
wkc
daf463bbb4 设计: 员工资产导入与实体库自动补入修复 2026-07-01 16:28:16 +08:00
wkc
6f6f2d264c 统一项目分析弹窗圆角样式 2026-07-01 16:28:16 +08:00
wkc
266108ac06 修复流水上传原始文件名保持 2026-07-01 16:16:48 +08:00
wkc
5ff67442bb 修复流水分析上传文件名传递 2026-07-01 16:16:48 +08:00
wkc
b7bb96b43b 设计: 保持上传流水原始文件名 2026-07-01 16:16:48 +08:00
wkc
9902430350 Add import dropdown validation 2026-07-01 16:16:48 +08:00
wkc
89773caa7b 新增导入下拉框校验实施计划 2026-07-01 16:16:48 +08:00
wkc
def656d57b 完善导入下拉框校验设计 2026-07-01 16:16:48 +08:00
wkc
6b6fae4b92 新增导入下拉框校验设计文档 2026-07-01 16:16:48 +08:00
wkc
0dc3b6ee2f uat配置文件 2026-07-01 16:16:47 +08:00
wjj
04ede64767 优化项目分析详情与异常明细展示 2026-07-01 11:26:01 +08:00
wjj
066d850389 清洗流水本方证件号格式 2026-06-30 13:40:24 +08:00
wjj
5e4bfca05b 完善外部人员预警与项目分析上线内容 2026-06-30 13:26:29 +08:00
wkc
ce66dc3ba8 项目上传删除暂不调用流水分析平台 2026-06-26 15:19:23 +08:00
wkc
f3c1e2ea93 移除风险模型导出按钮 2026-06-26 15:18:46 +08:00
wkc
87b2352001 为风险明细列表添加分页选择器 2026-06-26 15:16:30 +08:00
wjj
999350265b 优化项目详情资金流向图谱展示 2026-06-26 10:05:46 +08:00
wjj
f8ee1ecf1c 调整流水模型理财剔除与提现规则 2026-06-26 10:05:46 +08:00
wjj
35467fd361 放开涉疑交易流水级模型命中 2026-06-26 10:05:46 +08:00
wjj
64cb847db3 优化涉疑交易模型口径和报告展示 2026-06-26 10:03:33 +08:00
wjj
bf290c509c 补充项目分析个人详情页正式化样式 2026-06-26 10:00:09 +08:00
wjj
4e90e22ee2 优化项目详情资金流向图谱展示 2026-06-04 18:01:55 +08:00
wjj
a5eba12ed5 调整流水模型理财剔除与提现规则 2026-06-04 15:55:25 +08:00
wkc
c5b2033a3d 优化资金图谱主题节点检索 2026-06-03 17:11:09 +08:00
wkc
d45e9410ef 实现结果总览详情资产和征信页签 2026-06-02 17:17:49 +08:00
wkc
457e6c1d27 调整项目人数按流水证件号统计 2026-06-02 10:38:43 +08:00
wkc
850f97ea22 调整专项核查图谱展示 2026-06-01 17:37:59 +08:00
wjj
de6e6bd628 放开涉疑交易流水级模型命中 2026-06-01 17:22:51 +08:00
wjj
3a867e5857 修正短时间多次存现本人口径 2026-06-01 17:22:51 +08:00
wjj
19a60c987e 新增图谱功能及验收清单 2026-06-01 17:22:51 +08:00
wjj
aef6c43181 放开涉疑交易流水级模型命中 2026-06-01 16:44:51 +08:00
wkc
7ce721ef93 Refactor project pages and update related docs 2026-06-01 15:52:50 +08:00
wjj
000e8698a5 优化涉疑交易模型口径和报告展示 2026-06-01 15:52:50 +08:00
wjj
9d3e8beceb 补充项目分析个人详情页正式化样式 2026-06-01 15:50:26 +08:00
wjj
4c2ea9d8e6 修正短时间多次存现本人口径 2026-06-01 15:50:25 +08:00
wjj
26cd049991 新增图谱功能及验收清单 2026-05-29 18:33:26 +08:00
wkc
0ea504f6b3 修正双员工夫妻家庭专项核查口径 2026-05-26 17:18:26 +08:00
wkc
a39594faf8 修复风险总览无风险人员负数问题 2026-05-26 16:55:53 +08:00
wkc
1b45296df3 优化资产估值万元展示 2026-05-26 16:53:45 +08:00
wjj
180a892275 优化涉疑交易模型口径和报告展示 2026-05-26 16:43:39 +08:00
wkc
1fadb38d99 Implement credit parse result polling and sentinel handling 2026-05-18 10:56:25 +08:00
wkc
9917d10e59 调整征信解析返回解析和日志 2026-05-13 16:28:57 +08:00
wkc
be443d1b31 Refactor credit parse to use remote HTML paths 2026-05-13 14:20:42 +08:00
wkc
b822cc202e Remove obsolete code and documentation 2026-05-12 17:53:02 +08:00
wkc
598f5dec1c 移除前端默认登录凭据 2026-05-11 16:32:20 +08:00
wjj
ef8147892e 补充项目分析个人详情页正式化样式 2026-05-06 16:18:03 +08:00
437 changed files with 29652 additions and 2559 deletions

8
.gitignore vendored
View File

@@ -86,14 +86,14 @@ ruoyi-ui/vue.config.js
ruoyi-ui/dist.zip
*/src/test/
.pytest_cache/
tests/
tongweb_62318.properties
.superpowers/
tmp/
.codegraph/
.claude/

View File

@@ -67,6 +67,7 @@
- 前端相关安装、构建、调试、测试命令执行前,必须先通过 `nvm` 切换并确认 Node 版本
- 测试结束后,自动关闭测试过程中启动的前后端进程
- 重启后端时,必须优先使用 `bin/restart_java_backend.sh`
- 禁止在前端源码、配置、示例数据或页面默认值中硬编码或预填真实账号密码;登录页不得将密码保存到 Cookie、localStorage 或 sessionStorage
---
@@ -257,6 +258,7 @@ return AjaxResult.success(result);
- 请求统一使用 `@/utils/request`
- 新增页面或功能入口时,同步检查 `sys_menu`、路由、权限标识
- 优先延续现有 `ccdi*` 业务目录与命名方式,不随意新造平行目录
- 登录页只能在用户主动选择时保存用户名,不允许保存密码或预填默认密码
### 导入功能规范

669
CLAUDE.md
View File

@@ -1,669 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 快速参考
**启动项目:**
- 后端: `mvn spring-boot:run` 或运行 `ry.bat`
- 前端: `cd ruoyi-ui && npm run dev`
**访问地址:**
- 前端: http://localhost:80
- 后端: http://localhost:8080
- Swagger: http://localhost:8080/swagger-ui/index.html
- Druid 监控: http://localhost:8080/druid/ (ruoyi/123456)
**测试账号:**
- 用户名: `admin`
- 密码: `admin123`
**获取 Token:**
```bash
POST http://localhost:8080/login/test?username=admin&password=admin123
```
---
## 项目概述
**纪检初核系统** - 基于 **若依管理系统 v3.9.1** 构建的企业级前后端分离管理系统,用于员工异常行为风险识别。
### 技术栈版本
| 后端技术 | 版本 | 前端技术 | 版本 |
|-----------------------------|--------|------------|---------|
| Spring Boot | 3.5.8 | Vue.js | 2.6.12 |
| Java | 21 | Element UI | 2.15.14 |
| MyBatis Spring Boot Starter | 3.0.5 | Vuex | 3.6.0 |
| MySQL Connector | 8.2.0 | Vue Router | 3.4.9 |
| SpringDoc OpenAPI | 2.8.14 | Axios | 0.28.1 |
| EasyExcel | 3.3.4 | ECharts | 5.4.0 |
| Quartz | 2.5.2 | Sass | 1.32.13 |
---
## 常用命令
### 后端 (Maven)
```bash
# 编译项目
mvn clean compile
# 运行应用 (开发环境)
mvn spring-boot:run
# 打包部署
mvn clean package
# Windows 启动
ry.bat
# Linux/Mac 启动
./ry.sh start
```
### 前端 (npm)
```bash
cd ruoyi-ui
# 安装依赖 (推荐使用国内镜像)
npm install --registry=https://registry.npmmirror.com
# 开发服务器 (端口 80)
npm run dev
# 生产构建
npm run build:prod
# 预览生产构建
npm run preview
```
### 数据库初始化
```bash
# 初始化若依框架基础表
mysql -u root -p < sql/ry_20250522.sql
# 初始化定时任务表
mysql -u root -p < sql/quartz.sql
# 导入业务表(根据需要执行)
mysql -u root -p ccdi < sql/dpc_employee.sql
mysql -u root -p ccdi < sql/dpc_intermediary_blacklist.sql
# ... 其他业务表脚本
```
**注意:**
- 业务表脚本文件名以 `ccdi_``dpc_` 开头
- 部分脚本包含菜单数据,需要按顺序执行
- 数据库需要先创建(数据库名: `ccdi`
---
## 模块架构
```
ccdi/
├── ruoyi-admin/ # 主应用入口 (Spring Boot 启动类)
├── ruoyi-framework/ # 核心框架 (Security, Config, Filters)
├── ruoyi-system/ # 系统管理 (Users, Roles, Menus, Depts)
├── ruoyi-common/ # 通用工具 (annotations, utils, constants)
├── ruoyi-quartz/ # 定时任务
├── ruoyi-generator/ # 代码生成器
├── ccdi-info-collection/ # 【核心业务模块】信息采集
├── ccdi-project/ # 【核心业务模块】项目管理
├── ccdi-lsfx/ # 【核心业务模块】流水分析对接
├── lsfx-mock-server/ # 流水分析模拟服务器 (Python)
├── ruoyi-ui/ # 前端 Vue 应用
├── sql/ # 数据库脚本
├── bin/ # 启动脚本
└── doc/ # 项目文档
```
### 模块依赖关系
```
ruoyi-admin (启动模块)
├── ruoyi-framework (核心安全配置)
├── ruoyi-system (系统核心业务)
├── ruoyi-common (共享工具)
├── ruoyi-quartz (定时任务)
├── ruoyi-generator (代码生成)
├── ccdi-info-collection (信息采集模块)
│ └── 依赖 ruoyi-common
├── ccdi-project (项目管理模块)
│ └── 依赖 ruoyi-common
└── ccdi-lsfx (流水分析对接模块)
└── 依赖 ruoyi-common
```
**添加新业务模块:**
1. 在根目录 `pom.xml``<modules>` 中添加新模块
2. 在新模块的 `pom.xml` 中添加对 `ruoyi-common` 的依赖
3.`ruoyi-admin/pom.xml` 中添加对新模块的依赖
4. 在新模块中按照分层规范创建 controller/service/mapper/domain 包
### ccdi-info-collection 业务模块 (核心)
自定义业务模块,包含以下核心功能:
| 功能 | Controller | 实体类 |
|----------|---------------------------------------|-----------------------------|
| 员工基础信息 | CcdiBaseStaffController | CcdiBaseStaff |
| 中介黑名单 | CcdiIntermediaryController | CcdiBizIntermediary |
| 员工家庭关系 | CcdiStaffFmyRelationController | CcdiStaffFmyRelation |
| 员工企业关系 | CcdiStaffEnterpriseRelationController | CcdiStaffEnterpriseRelation |
| 信贷客户家庭关系 | CcdiCustFmyRelationController | CcdiCustFmyRelation |
| 信贷客户企业关系 | CcdiCustEnterpriseRelationController | CcdiCustEnterpriseRelation |
| 员工调动记录 | CcdiStaffTransferController | CcdiStaffTransfer |
| 员工招聘记录 | CcdiStaffRecruitmentController | CcdiStaffRecruitment |
| 采购交易 | CcdiPurchaseTransactionController | CcdiPurchaseTransaction |
**分层结构:**
- Controller: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/`
- Service: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/`
- Mapper: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/`
- Domain: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/`
- dto/: 数据传输对象
- vo/: 视图对象
- excel/: Excel导入导出实体
- XML映射: `ccdi-info-collection/src/main/resources/mapper/info/collection/`
### ccdi-project 业务模块 (核心)
项目管理模块,用于管理纪检初核项目的全生命周期:
**核心功能:**
- 项目创建、更新、删除、查询
- 项目状态管理 (进行中、已完成、已归档)
- 项目统计(按状态统计数量)
- 模型参数配置管理
**主要 Controller:**
- CcdiProjectController: 项目管理
- CcdiModelParamController: 模型参数配置
**分层结构:**
- Controller: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/`
- Service: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/`
- Mapper: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/`
- Domain: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/`
- XML映射: `ccdi-project/src/main/resources/mapper/ccdi/project/`
### ccdi-lsfx 业务模块 (核心)
流水分析平台对接模块,用于与外部流水分析系统交互:
**核心功能:**
- 获取访问令牌 (Token)
- 上传流水文件并解析
- 拉取行内流水数据
- 查询解析状态和结果
- 获取银行流水明细
**主要组件:**
- LsfxAnalysisClient: 流水分析平台客户端
- LsfxTestController: 测试接口
**配置项 (application-dev.yml):**
```yaml
lsfx:
api:
base-url: http://localhost:8000 # 流水分析平台地址
app-id: your-app-id
app-secret: your-app-secret
client-id: your-client-id
endpoints:
get-token: /api/auth/token
upload-file: /api/files/upload
fetch-inner-flow: /api/flow/inner
```
**分层结构:**
- Client: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/`
- Controller: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/`
- Domain: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/`
- request/: 请求对象
- response/: 响应对象
- Config: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/config/`
### lsfx-mock-server (开发测试工具)
Python 实现的流水分析平台模拟服务器,用于本地开发和测试:
**用途:**
- 模拟流水分析平台的 API 接口
- 提供测试数据和模拟响应
- 支持错误场景模拟
**启动方式:**
```bash
cd lsfx-mock-server
python app.py # 默认监听 http://localhost:8000
```
---
## 后端开发规范
### 通用规范
- **新模块命名**: 项目英文名首字母集合 + 主要功能 (如 `ruoyi-info-collection`)
- **代码分离**: 新功能代码与若依框架自带代码分离Controller 放在新模块中
- **审计字段**: 实体类不继承 BaseEntity单独添加审计字段通过注释实现自动插入
### Java 代码风格
```java
// 使用 @Data 注解
@Data
public class CcdiBaseStaff {
// 审计字段通过注释实现自动插入
/** 创建者 */
private String createBy;
/** 创建时间 */
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
private Date updateTime;
}
// 服务层使用 @Resource 注入
@Resource
private ICcdiBaseStaffService baseStaffService;
```
### 分层规范
- **Controller**: 所有接口添加 Swagger 注释,分页使用 MyBatis Plus Page
- **Service**: 简单 CRUD 用 MyBatis Plus 方法,复杂操作在 XML 写 SQL
- **DTO/VO**: 接口传参使用独立 DTO返回使用独立 VO不与 entity 混用
- **Mapper**: 简单操作继承 BaseMapper复杂操作在 XML 中定义
### 禁止事项
- **禁止使用全限定类名**: 必须使用 `import` 语句导入类,不要在代码中使用 `java.util.List` 这样的全限定名
- **禁止使用 `extends ServiceImpl<>`**: Service 接口和实现类分离定义
- **禁止 Entity 混用**: DTO、VO、Excel 类必须独立,不与 Entity 混用
- **禁止缺少 `@Resource`**: Service 注入必须使用 `@Resource` 注解
### API 响应格式
```java
// 成功
AjaxResult.success("操作成功", data);
// 错误
AjaxResult.error("操作失败");
// 分页
Page<CcdiBaseStaff> page = new Page<>(pageNum, pageSize);
IPage<CcdiBaseStaff> result = baseStaffMapper.selectPage(page, queryWrapper);
return AjaxResult.success(result);
```
---
## 前端开发规范
### 目录结构
```
ruoyi-ui/src/
├── api/ # API 请求定义 (与后端 Controller 对应)
├── views/ # 页面组件 (按功能模块组织)
│ ├── ccdiBaseStaff/
│ ├── ccdiIntermediary/
│ └── ...
├── components/ # 可复用组件 (复杂组件需拆分)
├── router/ # 路由配置
└── store/ # Vuex 状态管理
```
### API 调用示例
```javascript
import request from '@/utils/request'
export function listStaff(query) {
return request({
url: '/ccdi/baseStaff/list',
method: 'get',
params: query
})
}
```
### 菜单联动
添加页面和组件后,需要同步修改数据库中的菜单表 (`sys_menu`)。
---
## 特殊功能
### 异步导入
支持大数据量异步 Excel 导入,通过 taskId 查询导入状态:
```java
@PostMapping("/import")
public AjaxResult asyncImport(@RequestParam("file") MultipartFile file) {
String taskId = asyncImportService.startImport(file);
return AjaxResult.success("导入任务已启动", taskId);
}
@GetMapping("/import/status/{taskId}")
public AjaxResult getImportStatus(@PathVariable String taskId) {
return AjaxResult.success(asyncImportService.getStatus(taskId));
}
```
**导入流程:**
1. 前端上传 Excel 文件
2. 后端异步处理,返回 taskId
3. 前端轮询 `/import/status/{taskId}` 获取导入进度
4. 导入完成后,可获取成功/失败数据统计
**导入结果处理:**
- 只返回导入失败的数据(含失败原因)
- 成功数据不返回,减少响应体积
- 支持批量插入,提高性能
### EasyExcel 字典下拉框
导入模板支持字典下拉框配置,提升数据录入准确性。使用 `DictDropdownWriteHandler` 实现。
### 权限控制
基于 Spring Security + JWT 的角色菜单权限系统:
- 权限格式: `system:user:edit`, `ccdi:staff:list`
- 数据权限: 支持全部、自定义、部门等范围
---
## 测试与验证
### 测试账号
- **用户名**: `admin`
- **密码**: `admin123`
### 登录获取 Token
```bash
# 登录接口
POST /login/test?username=admin&password=admin123
```
### API 文档
- **Swagger UI**: `/swagger-ui/index.html`
- **API Docs**: `/v3/api-docs`
### 测试规范
- 不在命令行启动后端进行测试
- 生成可执行的测试脚本进行验证
- 测试完成后保存接口输出并生成测试用例报告
### 开发调试技巧
**使用 Swagger 测试接口:**
1. 访问 `/swagger-ui/index.html`
2. 点击接口展开详情
3. 点击 "Try it out" 进行测试
4. 填写参数后点击 "Execute" 执行
**查看 SQL 执行日志:**
-`application.yml` 中设置日志级别: `com.ruoyi: debug`
- 使用 Druid 监控台查看慢 SQL
**前端代理配置:**
前端开发服务器通过代理转发请求到后端:
- 前端地址: `http://localhost:80`
- 后端地址: `http://localhost:8080`
- 代理配置文件: `ruoyi-ui/vue.config.js`
---
## 配置说明
| 配置项 | 值 |
|---------|-------------------|
| 后端端口 | 8080 |
| 前端开发端口 | 80 |
| 默认管理员 | admin/admin123 |
| JWT 有效期 | 30 分钟 |
| 文件上传限制 | 单文件 10MB, 总计 20MB |
### 配置文件位置
| 配置 | 路径 |
|----------|------------------------------------------------------|
| 主配置 | `ruoyi-admin/src/main/resources/application.yml` |
| 开发环境 | `ruoyi-admin/src/main/resources/application-dev.yml` |
| 数据库连接 | `application-dev.yml` |
| Redis 配置 | `application-dev.yml` |
### 数据源配置
项目使用 Druid 连接池,支持主从分离(默认关闭从库):
- **数据库连接**: `jdbc:mysql://host:3306/ccdi`
- **初始连接数**: 5
- **最小连接数**: 10
- **最大连接数**: 20
- **慢 SQL 记录**: 超过 1000ms 的 SQL 会被记录
### Redis 配置
- **默认端口**: 6379
- **数据库索引**: 0
- **连接超时**: 10s
### 流水分析平台配置
项目集成了外部流水分析平台,配置项位于 `application-dev.yml`:
```yaml
lsfx:
api:
base-url: http://localhost:8000 # 流水分析平台基础地址
app-id: ccdi-app # 应用ID
app-secret: ccdi-secret-2024 # 应用密钥
client-id: ccdi-client # 客户端ID
endpoints:
get-token: /api/auth/token # 获取令牌接口
upload-file: /api/files/upload # 文件上传接口
fetch-inner-flow: /api/flow/inner # 拉取行内流水接口
```
**开发环境使用 Mock 服务器:**
- 本地开发时,将 `base-url` 设置为 `http://localhost:8000`
- 启动 `lsfx-mock-server` 提供模拟接口
- 生产环境替换为真实的流水分析平台地址
### MCP 配置
项目使用 MCP (Model Context Protocol) 连接数据库,配置文件: `.mcp.json`
```json
{
"mcpServers": {
"mysql": {
"command": "npx",
"args": ["-y", "@fhuang/mcp-mysql-server"],
"env": {
"MYSQL_HOST": "116.62.17.81",
"MYSQL_PORT": "3306",
"MYSQL_USER": "root",
"MYSQL_PASSWORD": "Kfcx@1234",
"MYSQL_DATABASE": "ccdi"
}
}
}
}
```
**使用场景:**
- 通过 MCP 工具直接查询和操作数据库
- 在开发过程中快速验证数据
- 生成测试数据和调试 SQL
### Druid 监控台
访问地址: `http://localhost:8080/druid/`
- 用户名: `ruoyi`
- 密码: `123456`
用于监控 SQL 执行情况、连接池状态等。
---
## 重要文件路径
| 用途 | 路径 |
|---------------|--------------------------------------------------------------------------------|
| 应用入口 | `ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java` |
| 安全配置 | `ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java` |
| 信息采集 Controller | `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/` |
| 信息采集 Mapper XML | `ccdi-info-collection/src/main/resources/mapper/info/collection/` |
| 项目管理 Controller | `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/` |
| 项目管理 Mapper XML | `ccdi-project/src/main/resources/mapper/ccdi/project/` |
| 流水分析 Client | `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java` |
| Vue 路由 | `ruoyi-ui/src/router/index.js` |
| Vuex Store | `ruoyi-ui/src/store/` |
| 前端 API | `ruoyi-ui/src/api/` |
---
## 数据库规范
- **新建表名**: 需要加上项目英文名首字母集合前缀 `ccdi_` (如 `ccdi_base_staff`)
---
## 文档管理
- **文档语言**: 使用简体中文编写 .md 文档
- **文档目录**: 所有生成的文档放在 `doc/` 目录下,按类型分类
- **需求分析**: 在 `doc/` 目录下新建文件夹,以需求内容命名
### doc 目录结构
```
doc/
├── api-docs/ # API 文档
├── database/ # 数据库相关
├── design/ # 设计文档
├── implementation/ # 实施文档
├── requirements/ # 需求文档
└── test-scripts/ # 测试脚本
```
---
## OpenSpec 工作流
项目使用 OpenSpec 进行规范驱动开发,参考 `openspec/AGENTS.md`
### 何时创建 Proposal
**需要创建:**
- 新功能或能力
- 破坏性变更 (API, 数据库结构)
- 架构变更
- 改变行为的性能优化
**无需创建:**
- Bug 修复 (恢复预期行为)
- 拼写错误、格式、注释
- 非破坏性依赖更新
- 配置变更
---
## 沟通规范
- 永远使用简体中文进行思考和对话
---
## 常见问题排查
### 数据库连接失败
**检查项:**
1. 确认 MySQL 服务已启动
2. 检查 `application-dev.yml` 中的数据库连接配置
3. 确认数据库用户名和密码正确
4. 检查数据库是否已创建(数据库名: `ccdi`
### Redis 连接失败
**检查项:**
1. 确认 Redis 服务已启动
2. 检查 `application-dev.yml` 中的 Redis 配置
3. 如果 Redis 不需要密码,将 `password` 配置注释掉
### 前端无法访问后端接口
**检查项:**
1. 确认后端已启动(端口 8080
2. 检查前端代理配置(`ruoyi-ui/vue.config.js`
3. 确认后端接口路径正确(查看 Controller 的 `@RequestMapping`
### 导入功能无响应
**检查项:**
1. 检查文件大小是否超过限制(默认 10MB
2. 查看后端日志是否有异常
3. 确认 Excel 模板格式正确
4. 检查必填字段是否为空
### 流水分析平台连接失败
**检查项:**
1. 确认 `lsfx-mock-server` 已启动(开发环境)
2. 检查 `application-dev.yml` 中的 `lsfx.api.base-url` 配置
3. 验证 app-id、app-secret、client-id 是否正确
4. 检查网络连接和防火墙设置
5. 查看后端日志中的 HTTP 请求错误信息
---
## MyBatis Plus 分页使用
```java
// Controller 层
@GetMapping("/list")
public TableDataInfo list(QueryDTO queryDTO) {
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<VO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<VO> result = service.selectPage(page, queryDTO);
return getDataTable(result.getRecords(), result.getTotal());
}
// Service 层
Page<VO> selectPage(Page<VO> page, QueryDTO queryDTO);
// Mapper 层 (使用 XML)
<select id="selectPage" resultType="VO">
SELECT * FROM table_name
<where>
<if test="queryDTO.name != null">
AND name LIKE CONCAT('%', #{queryDTO.name}, '%')
</if>
</where>
</select>
```

View File

@@ -1,92 +0,0 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
DATE_STAMP=$(date "+%Y%m%d")
RELEASE_ZIP="$ROOT_DIR/ccdi_${DATE_STAMP}.zip"
STAGE_DIR="$ROOT_DIR/.deploy/ccdi-release-package"
WORK_DIR="$STAGE_DIR/files"
BACKEND_JAR_SOURCE="$ROOT_DIR/ruoyi-admin/target/ruoyi-admin.jar"
FRONTEND_DIR="$ROOT_DIR/ruoyi-ui"
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
FRONTEND_DIST_ZIP="$WORK_DIR/dist.zip"
log_info() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
}
log_error() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
reset_stage_dir() {
rm -rf "$STAGE_DIR"
mkdir -p "$WORK_DIR"
}
build_backend() {
log_info "开始构建后端生产 jar"
(
cd "$ROOT_DIR"
mvn -pl ruoyi-admin -am clean package -DskipTests
)
if [ ! -f "$BACKEND_JAR_SOURCE" ]; then
log_error "未生成后端 jar: $BACKEND_JAR_SOURCE"
exit 1
fi
}
build_frontend() {
log_info "开始构建前端生产 dist"
FRONTEND_DIR="$FRONTEND_DIR" zsh -lic 'cd "$FRONTEND_DIR" && nvm use >/dev/null && npm run build:prod'
if [ ! -f "$FRONTEND_DIST_DIR/index.html" ]; then
log_error "前端生产构建失败,未找到: $FRONTEND_DIST_DIR/index.html"
exit 1
fi
(
cd "$FRONTEND_DIR"
zip -qr "$FRONTEND_DIST_ZIP" dist
)
if [ ! -f "$FRONTEND_DIST_ZIP" ]; then
log_error "未生成前端压缩包: $FRONTEND_DIST_ZIP"
exit 1
fi
}
package_release() {
cp "$BACKEND_JAR_SOURCE" "$WORK_DIR/ruoyi-admin.jar"
rm -f "$RELEASE_ZIP"
(
cd "$WORK_DIR"
zip -qr "$RELEASE_ZIP" ruoyi-admin.jar dist.zip
)
log_info "上线压缩包已生成: $RELEASE_ZIP"
log_info "压缩包根层内容: ruoyi-admin.jar, dist.zip"
}
main() {
require_command mvn
require_command zsh
require_command zip
reset_stage_dir
build_backend
build_frontend
package_release
}
main "$@"

View File

@@ -39,6 +39,10 @@ public class CcdiAccountInfoQueryDTO implements Serializable {
@Schema(description = "账户姓名")
private String accountName;
/** 账户号码 */
@Schema(description = "账户号码")
private String accountNo;
/** 账户类型 */
@Schema(description = "账户类型")
private String accountType;

View File

@@ -28,6 +28,14 @@ public interface CcdiBaseStaffMapper extends BaseMapper<CcdiBaseStaff> {
Page<CcdiBaseStaffVO> selectBaseStaffPageWithDept(@Param("page") Page<CcdiBaseStaffVO> page,
@Param("query") CcdiBaseStaffQueryDTO queryDTO);
/**
* 查询员工详情(包含部门名称)
*
* @param staffId 员工ID
* @return 员工详情
*/
CcdiBaseStaffVO selectBaseStaffByIdWithDept(@Param("staffId") Long staffId);
int insertOrUpdateBatch(@Param("list") List<CcdiBaseStaff> list);
/**

View File

@@ -49,6 +49,10 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
private static final Set<String> ACCOUNT_TYPES = Set.of("BANK", "SECURITIES", "PAYMENT", "OTHER");
private static final Set<String> BANK_SCOPES = Set.of("INTERNAL", "EXTERNAL");
private static final Set<String> LEVELS = Set.of("LOW", "MEDIUM", "HIGH");
private static final String OWNER_TYPE_INTERMEDIARY = "INTERMEDIARY";
private static final String BANK_SCOPE_EXTERNAL = "EXTERNAL";
private static final String RISK_LEVEL_LOW = "LOW";
private static final String RISK_LEVEL_HIGH = "HIGH";
private final List<AccountInfoImportFailureVO> latestImportFailures = new CopyOnWriteArrayList<>();
@Resource
@@ -195,12 +199,12 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
private void validateOwner(String ownerType, String ownerId) {
if (StringUtils.isEmpty(ownerId)) {
if ("EXTERNAL".equals(ownerType) || "INTERMEDIARY".equals(ownerType)) {
if ("EXTERNAL".equals(ownerType) || OWNER_TYPE_INTERMEDIARY.equals(ownerType)) {
throw new RuntimeException("证件号不能为空");
}
throw new RuntimeException("所属人不能为空");
}
if ("EXTERNAL".equals(ownerType) || "INTERMEDIARY".equals(ownerType)) {
if ("EXTERNAL".equals(ownerType) || OWNER_TYPE_INTERMEDIARY.equals(ownerType)) {
return;
}
if ("EMPLOYEE".equals(ownerType)) {
@@ -232,8 +236,9 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
}
private void prepareAnalysisFields(CcdiAccountInfo accountInfo) {
if (!"EXTERNAL".equals(accountInfo.getBankScope())) {
if (!BANK_SCOPE_EXTERNAL.equals(accountInfo.getBankScope())) {
clearAnalysisFields(accountInfo);
applyIntermediaryRiskLevel(accountInfo);
return;
}
if (accountInfo.getIsActualControl() == null) {
@@ -249,8 +254,9 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
accountInfo.setTxnFrequencyLevel("MEDIUM");
}
if (StringUtils.isEmpty(accountInfo.getTxnRiskLevel())) {
accountInfo.setTxnRiskLevel("LOW");
accountInfo.setTxnRiskLevel(RISK_LEVEL_LOW);
}
applyIntermediaryRiskLevel(accountInfo);
}
private void clearAnalysisFields(CcdiAccountInfo accountInfo) {
@@ -265,6 +271,12 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
accountInfo.setTxnRiskLevel(null);
}
private void applyIntermediaryRiskLevel(CcdiAccountInfo accountInfo) {
if (OWNER_TYPE_INTERMEDIARY.equals(accountInfo.getOwnerType())) {
accountInfo.setTxnRiskLevel(RISK_LEVEL_HIGH);
}
}
private void validateAmount(BigDecimal amount, String fieldLabel) {
if (amount == null) {
return;
@@ -285,6 +297,7 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
addDTO.setTxnFrequencyLevel(toUpper(addDTO.getTxnFrequencyLevel()));
addDTO.setTxnRiskLevel(toUpper(addDTO.getTxnRiskLevel()));
addDTO.setOwnerId(normalizeOwnerId(addDTO.getOwnerId()));
applyIntermediaryRiskLevel(addDTO);
}
private void normalizeEditDto(CcdiAccountInfoEditDTO editDTO) {
@@ -295,6 +308,19 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
editDTO.setTxnFrequencyLevel(toUpper(editDTO.getTxnFrequencyLevel()));
editDTO.setTxnRiskLevel(toUpper(editDTO.getTxnRiskLevel()));
editDTO.setOwnerId(normalizeOwnerId(editDTO.getOwnerId()));
applyIntermediaryRiskLevel(editDTO);
}
private void applyIntermediaryRiskLevel(CcdiAccountInfoAddDTO addDTO) {
if (OWNER_TYPE_INTERMEDIARY.equals(addDTO.getOwnerType())) {
addDTO.setTxnRiskLevel(RISK_LEVEL_HIGH);
}
}
private void applyIntermediaryRiskLevel(CcdiAccountInfoEditDTO editDTO) {
if (OWNER_TYPE_INTERMEDIARY.equals(editDTO.getOwnerType())) {
editDTO.setTxnRiskLevel(RISK_LEVEL_HIGH);
}
}
private String normalizeCurrency(String currency) {
@@ -362,7 +388,8 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
private CcdiAccountInfoAddDTO toAddDto(CcdiAccountInfoExcel excel) {
CcdiAccountInfoAddDTO dto = new CcdiAccountInfoAddDTO();
dto.setOwnerType(parseOwnerType(excel.getOwnerType()));
String ownerType = parseOwnerType(excel.getOwnerType());
dto.setOwnerType(ownerType);
dto.setOwnerId(normalizeOwnerId(excel.getOwnerId()));
dto.setAccountName(trimToNull(excel.getAccountName()));
dto.setAccountNo(trimToNull(excel.getAccountNo()));
@@ -382,7 +409,9 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
dto.setCreditSingleMaxAmount(parseDecimal(excel.getCreditSingleMaxAmount()));
dto.setDebitDailyMaxAmount(parseDecimal(excel.getDebitDailyMaxAmount()));
dto.setCreditDailyMaxAmount(parseDecimal(excel.getCreditDailyMaxAmount()));
dto.setTxnRiskLevel(parseLevel(excel.getTxnRiskLevel(), "风险等级"));
dto.setTxnRiskLevel(OWNER_TYPE_INTERMEDIARY.equals(ownerType)
? RISK_LEVEL_HIGH
: parseLevel(excel.getTxnRiskLevel(), "风险等级"));
return dto;
}
@@ -418,7 +447,7 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
return "RELATION";
}
if ("中介".equals(value)) {
return "INTERMEDIARY";
return OWNER_TYPE_INTERMEDIARY;
}
if ("外部人员".equals(value)) {
return "EXTERNAL";

View File

@@ -119,10 +119,10 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
*/
@Override
public CcdiBaseStaffVO selectBaseStaffById(Long staffId) {
CcdiBaseStaff staff = baseStaffMapper.selectById(staffId);
CcdiBaseStaffVO vo = convertToVO(staff);
if (staff != null) {
vo.setAssetInfoList(assetInfoService.selectByFamilyIdAndPersonId(staff.getIdCard(), staff.getIdCard()).stream().map(asset -> {
CcdiBaseStaffVO vo = baseStaffMapper.selectBaseStaffByIdWithDept(staffId);
if (vo != null) {
vo.setStatusDesc(EmployeeStatus.getDescByCode(vo.getStatus()));
vo.setAssetInfoList(assetInfoService.selectByFamilyIdAndPersonId(vo.getIdCard(), vo.getIdCard()).stream().map(asset -> {
CcdiAssetInfoVO assetInfoVO = new CcdiAssetInfoVO();
BeanUtils.copyProperties(asset, assetInfoVO);
return assetInfoVO;

View File

@@ -14,8 +14,10 @@ import com.ruoyi.info.collection.mapper.CcdiCreditInfoQueryMapper;
import com.ruoyi.info.collection.mapper.CcdiCreditNegativeInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiDebtsInfoMapper;
import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
import com.ruoyi.info.collection.service.support.CreditHtmlStorageService;
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
import com.ruoyi.lsfx.client.CreditParseClient;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
import jakarta.annotation.Resource;
@@ -23,8 +25,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@@ -36,9 +36,16 @@ import java.util.Map;
@Service
public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
private static final int CREDIT_PARSE_SUCCESS_CODE = 10000;
private static final int CREDIT_PARSE_SUCCESS_STATUS = 1;
private static final int CREDIT_PARSE_SUCCESS_REASON_CODE = 200;
@Resource
private CreditParseClient creditParseClient;
@Resource
private CreditHtmlStorageService creditHtmlStorageService;
@Resource
private CreditInfoPayloadAssembler assembler;
@@ -141,37 +148,20 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
}
}
private void handleSingleFile(MultipartFile multipartFile, String userName) throws IOException {
File tempFile = createTempFile(multipartFile);
try {
CreditParseResponse response = creditParseClient.parse("LXCUSTALL", "PERSON", tempFile);
CreditParsePayload payload = requireResponse(response).getPayload();
Map<String, Object> header = requireHeader(payload);
String personId = stringValue(header.get("query_cert_no"));
String personName = stringValue(header.get("query_cust_name"));
LocalDate queryDate = parseQueryDate(stringValue(header.get("report_time")));
ensurePersonIdPresent(personId);
ensureLatestQueryDate(personId, queryDate);
private void handleSingleFile(MultipartFile multipartFile, String userName) throws Exception {
CreditHtmlStorageService.StoredCreditHtml storedHtml = creditHtmlStorageService.save(multipartFile);
CreditParseInvokeResponse response = creditParseClient.parse(storedHtml.remotePath());
CreditParsePayload payload = requireResponse(response).getPayload();
Map<String, Object> header = requireHeader(payload);
String personId = stringValue(header.get("query_cert_no"));
String personName = stringValue(header.get("query_cust_name"));
LocalDate queryDate = parseQueryDate(stringValue(header.get("report_time")));
ensurePersonIdPresent(personId);
ensureLatestQueryDate(personId, queryDate);
List<CcdiDebtsInfo> debts = assembler.buildDebts(personId, personName, queryDate, payload);
CcdiCreditNegativeInfo negative = assembler.buildNegative(personId, personName, queryDate, payload);
replaceEmployeeCredit(personId, debts, negative, userName);
} finally {
if (tempFile.exists()) {
tempFile.delete();
}
}
}
private File createTempFile(MultipartFile multipartFile) throws IOException {
String originalFilename = multipartFile.getOriginalFilename();
String suffix = ".html";
if (originalFilename != null && originalFilename.contains(".")) {
suffix = originalFilename.substring(originalFilename.lastIndexOf('.'));
}
File tempFile = File.createTempFile("credit-info-", suffix);
multipartFile.transferTo(tempFile);
return tempFile;
List<CcdiDebtsInfo> debts = assembler.buildDebts(personId, personName, queryDate, payload);
CcdiCreditNegativeInfo negative = assembler.buildNegative(personId, personName, queryDate, payload);
replaceEmployeeCredit(personId, debts, negative, userName);
}
private void validateHtmlFile(MultipartFile file) {
@@ -185,14 +175,41 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
}
}
private CreditParseResponse requireResponse(CreditParseResponse response) {
if (response == null || response.getPayload() == null) {
private CreditParseResponse requireResponse(CreditParseInvokeResponse response) {
if (response == null || response.getData() == null || response.getData().getMappingOutputFields() == null) {
throw new RuntimeException("征信解析结果为空");
}
if (!"0".equals(response.getStatusCode())) {
throw new RuntimeException(stringValue(response.getMessage(), "征信解析失败"));
CreditParseResponse mappingOutputFields = response.getData().getMappingOutputFields();
if (!Boolean.TRUE.equals(response.getSuccess())) {
throw new RuntimeException(stringValue(mappingOutputFields.getMessage(), "征信解析平台调用失败"));
}
return response;
if (!Integer.valueOf(CREDIT_PARSE_SUCCESS_CODE).equals(response.getCode())) {
throw new RuntimeException("征信解析平台状态码异常: " + response.getCode());
}
if (!Integer.valueOf(CREDIT_PARSE_SUCCESS_STATUS).equals(response.getData().getStatus())) {
throw new RuntimeException(parseErrorMessage(response, "征信解析状态异常: " + response.getData().getStatus()));
}
if (!Integer.valueOf(CREDIT_PARSE_SUCCESS_REASON_CODE).equals(response.getData().getReasonCode())) {
throw new RuntimeException(parseErrorMessage(response, "征信解析原因码异常: " + response.getData().getReasonCode()));
}
if (mappingOutputFields.getPayload() == null) {
throw new RuntimeException("征信解析结果为空");
}
return mappingOutputFields;
}
private String parseErrorMessage(CreditParseInvokeResponse response, String defaultValue) {
if (response == null || response.getData() == null) {
return defaultValue;
}
String reasonMessage = stringValue(response.getData().getReasonMessage());
if (!isBlank(reasonMessage)) {
return reasonMessage;
}
if (response.getData().getMappingOutputFields() != null) {
return stringValue(response.getData().getMappingOutputFields().getMessage(), defaultValue);
}
return defaultValue;
}
private Map<String, Object> requireHeader(CreditParsePayload payload) {

View File

@@ -0,0 +1,44 @@
package com.ruoyi.info.collection.service.support;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
/**
* 征信 HTML 服务器落盘与远程访问地址生成。
*/
@Component
public class CreditHtmlStorageService {
private static final String CREDIT_HTML_DIR = "credit-html";
private static final String[] HTML_EXTENSIONS = {"html", "htm"};
@Value("${credit-parse.api.file-public-base-url}")
private String filePublicBaseUrl;
public StoredCreditHtml save(MultipartFile file) throws Exception {
String profilePath = FileUploadUtils.upload(getCreditHtmlBaseDir(), file, HTML_EXTENSIONS);
return new StoredCreditHtml(profilePath, buildRemotePath(profilePath));
}
private String getCreditHtmlBaseDir() {
return RuoYiConfig.getProfile() + File.separator + CREDIT_HTML_DIR;
}
private String buildRemotePath(String profilePath) {
if (StringUtils.isBlank(filePublicBaseUrl)) {
throw new IllegalStateException("征信HTML公开访问地址未配置");
}
String normalizedBaseUrl = StringUtils.stripEnd(filePublicBaseUrl.trim(), "/");
String normalizedProfilePath = profilePath.startsWith("/") ? profilePath : "/" + profilePath;
return normalizedBaseUrl + normalizedProfilePath;
}
public record StoredCreditHtml(String profilePath, String remotePath) {
}
}

View File

@@ -19,6 +19,8 @@ import java.util.Objects;
@Component
public class CreditInfoPayloadAssembler {
private static final BigDecimal MISSING_SENTINEL = new BigDecimal("-9999");
private static final List<DebtMapping> DEBT_MAPPINGS = List.of(
new DebtMapping("uncle_bank_house", "银行", "住房贷款", "银行", "未结清银行住房贷款"),
new DebtMapping("uncle_bank_car", "银行", "汽车贷款", "银行", "未结清银行汽车贷款"),
@@ -26,7 +28,7 @@ public class CreditInfoPayloadAssembler {
new DebtMapping("uncle_bank_consume", "银行", "消费贷款", "银行", "未结清银行消费贷款"),
new DebtMapping("uncle_bank_other", "银行", "其他贷款", "银行", "未结清银行其他贷款"),
new DebtMapping("uncle_not_bank", "非银", "非银行贷款", "非银", "未结清非银行贷款"),
new DebtMapping("uncle_credit_cart", "银行", "信用卡", "银行", "未结清信用卡")
new DebtMapping("uncle_credit_card", "银行", "信用卡", "银行", "未结清信用卡")
);
public List<CcdiDebtsInfo> buildDebts(String personId, String personName, LocalDate queryDate, CreditParsePayload payload) {
@@ -61,9 +63,13 @@ public class CreditInfoPayloadAssembler {
private CcdiDebtsInfo buildDebtRow(String personId, String personName, LocalDate queryDate,
Map<String, Object> source, DebtMapping mapping) {
Object stateValue = source.get(mapping.prefix() + "_state");
if (isMissingSentinel(stateValue)) {
return null;
}
BigDecimal principalBalance = toBigDecimal(source.get(mapping.prefix() + "_bal"));
BigDecimal debtTotalAmount = toBigDecimal(source.get(mapping.prefix() + "_lmt"));
String debtStatus = toStringValue(source.get(mapping.prefix() + "_state"));
String debtStatus = toStringValue(stateValue);
if (isEmptyMetrics(principalBalance, debtTotalAmount, debtStatus)) {
return null;
}
@@ -97,6 +103,9 @@ public class CreditInfoPayloadAssembler {
if (value == null) {
return null;
}
if (isMissingSentinel(value)) {
return null;
}
if (value instanceof BigDecimal decimal) {
return decimal;
}
@@ -111,10 +120,28 @@ public class CreditInfoPayloadAssembler {
if (value == null) {
return null;
}
if (isMissingSentinel(value)) {
return null;
}
String text = Objects.toString(value, "").trim();
return text.isEmpty() ? null : text;
}
private boolean isMissingSentinel(Object value) {
if (value == null) {
return false;
}
String text = Objects.toString(value, "").trim();
if (text.isEmpty()) {
return false;
}
try {
return new BigDecimal(text).compareTo(MISSING_SENTINEL) == 0;
} catch (NumberFormatException e) {
return false;
}
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}

View File

@@ -84,6 +84,7 @@
<sql id="AccountInfoWhereClause">
WHERE 1 = 1
AND ai.owner_type &lt;&gt; 'CREDIT_CUSTOMER'
<if test="query.staffName != null and query.staffName != ''">
AND (
(ai.owner_type = 'EMPLOYEE' AND bs.name LIKE CONCAT('%', #{query.staffName}, '%'))
@@ -103,6 +104,9 @@
<if test="query.accountName != null and query.accountName != ''">
AND ai.account_name LIKE CONCAT('%', #{query.accountName}, '%')
</if>
<if test="query.accountNo != null and query.accountNo != ''">
AND ai.account_no LIKE CONCAT('%', #{query.accountNo}, '%')
</if>
<if test="query.accountType != null and query.accountType != ''">
AND ai.account_type = #{query.accountType}
</if>

View File

@@ -42,7 +42,16 @@
AND e.status = #{query.status}
</if>
</where>
ORDER BY e.create_time DESC, e.staff_id DESC
ORDER BY e.hire_date DESC, e.staff_id DESC
</select>
<select id="selectBaseStaffByIdWithDept" resultMap="CcdiBaseStaffVOResult">
SELECT
e.staff_id, e.name, e.dept_id, e.id_card, e.phone, e.annual_income, e.hire_date, e.is_party_member, e.status, e.create_time,
d.dept_name
FROM ccdi_base_staff e
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id
WHERE e.staff_id = #{staffId}
</select>
<!-- 批量插入或更新员工信息只更新非null字段 -->

View File

@@ -0,0 +1,147 @@
package com.ruoyi.info.collection.controller;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.BaseStaffImportSubmitResultVO;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffService;
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.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiBaseStaffControllerTest {
@InjectMocks
private CcdiBaseStaffController controller;
@Mock
private ICcdiBaseStaffService baseStaffService;
@Mock
private ICcdiBaseStaffImportService importAsyncService;
@Mock
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
@Test
void importTemplate_shouldDownloadDualSheetTemplate() {
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
controller.importTemplate(null);
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(
null,
CcdiBaseStaffExcel.class,
"员工信息",
CcdiBaseStaffAssetInfoExcel.class,
"员工资产信息",
"员工信息维护导入模板"
));
}
}
@Test
void importData_shouldWarnWhenBothSheetsAreEmpty() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"base-staff-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(CcdiBaseStaffExcel.class), eq("员工信息")))
.thenReturn(List.of());
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息")))
.thenReturn(List.of());
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG));
verifyNoInteractions(baseStaffService);
}
}
@Test
void importData_shouldSubmitOnlyStaffTaskWhenOnlyStaffSheetHasRows() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"base-staff.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"staff".getBytes(StandardCharsets.UTF_8)
);
CcdiBaseStaffExcel staffExcel = new CcdiBaseStaffExcel();
staffExcel.setStaffId(1001L);
BaseStaffImportSubmitResultVO submitResult = new BaseStaffImportSubmitResultVO();
submitResult.setStaffTaskId("staff-task-1");
when(baseStaffService.importBaseStaffWithAssets(List.of(staffExcel), List.of())).thenReturn(submitResult);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息")))
.thenReturn(List.of(staffExcel));
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息")))
.thenReturn(List.of());
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
assertEquals("导入任务已提交,正在后台处理", result.get(AjaxResult.MSG_TAG));
Object data = result.get(AjaxResult.DATA_TAG);
assertEquals("staff-task-1", data.getClass().getMethod("getStaffTaskId").invoke(data));
assertNull(data.getClass().getMethod("getAssetTaskId").invoke(data));
}
}
@Test
void importData_shouldSubmitTwoTasksWhenBothSheetsHaveRows() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"base-staff-both.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"both".getBytes(StandardCharsets.UTF_8)
);
CcdiBaseStaffExcel staffExcel = new CcdiBaseStaffExcel();
staffExcel.setStaffId(1002L);
CcdiBaseStaffAssetInfoExcel assetExcel = new CcdiBaseStaffAssetInfoExcel();
assetExcel.setPersonId("320101199001010011");
BaseStaffImportSubmitResultVO submitResult = new BaseStaffImportSubmitResultVO();
submitResult.setStaffTaskId("staff-task-2");
submitResult.setAssetTaskId("asset-task-2");
when(baseStaffService.importBaseStaffWithAssets(List.of(staffExcel), List.of(assetExcel))).thenReturn(submitResult);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息")))
.thenReturn(List.of(staffExcel));
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息")))
.thenReturn(List.of(assetExcel));
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
Object data = result.get(AjaxResult.DATA_TAG);
assertEquals("staff-task-2", data.getClass().getMethod("getStaffTaskId").invoke(data));
assertEquals("asset-task-2", data.getClass().getMethod("getAssetTaskId").invoke(data));
}
}
}

View File

@@ -0,0 +1,37 @@
package com.ruoyi.info.collection.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.info.collection.domain.vo.EnumOptionVO;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class CcdiEnumControllerTest {
private final CcdiEnumController controller = new CcdiEnumController();
@Test
void getEnterpriseRiskLevelOptions_shouldReturnConfiguredOptions() {
AjaxResult result = controller.getEnterpriseRiskLevelOptions();
List<?> data = (List<?>) result.get("data");
assertEquals(3, data.size());
EnumOptionVO first = (EnumOptionVO) data.get(0);
assertEquals("1", first.getValue());
assertEquals("高风险", first.getLabel());
}
@Test
void getEnterpriseSourceOptions_shouldReturnConfiguredOptions() {
AjaxResult result = controller.getEnterpriseSourceOptions();
List<?> data = (List<?>) result.get("data");
assertFalse(data.isEmpty());
EnumOptionVO first = (EnumOptionVO) data.get(0);
assertEquals("GENERAL", first.getValue());
assertEquals("一般企业", first.getLabel());
}
}

View File

@@ -0,0 +1,173 @@
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.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportFailureVO;
import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
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.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiStaffFmyRelationControllerTest {
@InjectMocks
private CcdiStaffFmyRelationController controller;
@Mock
private ICcdiStaffFmyRelationService relationService;
@Mock
private ICcdiStaffFmyRelationImportService relationImportService;
@Mock
private ICcdiAssetInfoImportService assetInfoImportService;
@Test
void importTemplate_shouldDownloadDualSheetTemplate() {
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
controller.importTemplate(null);
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(
null,
CcdiStaffFmyRelationExcel.class,
"员工亲属关系信息",
CcdiAssetInfoExcel.class,
"亲属资产信息",
"员工亲属关系维护导入模板"
));
}
}
@Test
void importData_shouldErrorWhenBothSheetsAreEmpty() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"staff-family-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(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息")))
.thenReturn(List.of());
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息")))
.thenReturn(List.of());
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG));
verifyNoInteractions(relationService, assetInfoImportService);
}
}
@Test
void importData_shouldSubmitOnlyRelationTaskWhenOnlyRelationSheetHasRows() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"staff-family-relation.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"relation".getBytes(StandardCharsets.UTF_8)
);
CcdiStaffFmyRelationExcel relationExcel = new CcdiStaffFmyRelationExcel();
relationExcel.setPersonId("320101199001010011");
StaffFmyRelationImportSubmitResultVO submitResult = new StaffFmyRelationImportSubmitResultVO();
submitResult.setRelationTaskId("relation-task-1");
when(relationService.importRelationWithAssets(List.of(relationExcel), List.of())).thenReturn(submitResult);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息")))
.thenReturn(List.of(relationExcel));
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息")))
.thenReturn(List.of());
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
assertEquals("导入任务已提交,正在后台处理", result.get(AjaxResult.MSG_TAG));
Object data = result.get(AjaxResult.DATA_TAG);
assertEquals("relation-task-1", data.getClass().getMethod("getRelationTaskId").invoke(data));
assertNull(data.getClass().getMethod("getAssetTaskId").invoke(data));
}
}
@Test
void importData_shouldSubmitTwoTasksWhenBothSheetsHaveRows() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"staff-family-both.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"both".getBytes(StandardCharsets.UTF_8)
);
CcdiStaffFmyRelationExcel relationExcel = new CcdiStaffFmyRelationExcel();
relationExcel.setPersonId("320101199001010012");
CcdiAssetInfoExcel assetExcel = new CcdiAssetInfoExcel();
assetExcel.setPersonId("320101199001010099");
StaffFmyRelationImportSubmitResultVO submitResult = new StaffFmyRelationImportSubmitResultVO();
submitResult.setRelationTaskId("relation-task-2");
submitResult.setAssetTaskId("asset-task-2");
when(relationService.importRelationWithAssets(List.of(relationExcel), List.of(assetExcel))).thenReturn(submitResult);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息")))
.thenReturn(List.of(relationExcel));
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息")))
.thenReturn(List.of(assetExcel));
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
Object data = result.get(AjaxResult.DATA_TAG);
assertEquals("relation-task-2", data.getClass().getMethod("getRelationTaskId").invoke(data));
assertEquals("asset-task-2", data.getClass().getMethod("getAssetTaskId").invoke(data));
}
}
@Test
void getImportFailures_shouldReturnPagedRowsWithSheetAndRowInfo() {
StaffFmyRelationImportFailureVO failure1 = new StaffFmyRelationImportFailureVO();
failure1.setSheetName("员工亲属关系信息");
failure1.setRowNum(2);
failure1.setPersonId("A1");
StaffFmyRelationImportFailureVO failure2 = new StaffFmyRelationImportFailureVO();
failure2.setSheetName("员工亲属关系信息");
failure2.setRowNum(3);
failure2.setPersonId("A2");
when(relationImportService.getImportFailures("task-1")).thenReturn(List.of(failure1, failure2));
TableDataInfo result = controller.getImportFailures("task-1", 2, 1);
assertEquals(2, result.getTotal());
assertEquals(1, result.getRows().size());
StaffFmyRelationImportFailureVO row = (StaffFmyRelationImportFailureVO) result.getRows().get(0);
assertEquals("员工亲属关系信息", row.getSheetName());
assertEquals(3, row.getRowNum());
assertEquals("A2", row.getPersonId());
}
}

View File

@@ -35,6 +35,19 @@ class CcdiAccountInfoMapperTest {
assertTrue(sql.contains("ai.is_self_account as isactualcontrol"), sql);
assertTrue(sql.contains("ai.monthly_avg_trans_count as avgmonthtxncount"), sql);
assertTrue(sql.contains("ai.trans_risk_level as txnrisklevel"), sql);
assertTrue(sql.contains("ai.owner_type <> 'credit_customer'"), sql);
}
@Test
void selectAccountInfoPage_shouldFilterByAccountNo() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper.selectAccountInfoPage");
CcdiAccountInfoQueryDTO queryDTO = new CcdiAccountInfoQueryDTO();
queryDTO.setAccountNo("6222");
String sql = renderSql(mappedStatement, Map.of("query", queryDTO)).toLowerCase();
assertTrue(sql.contains("ai.account_no like concat('%', ?, '%')"), sql);
}
private MappedStatement loadMappedStatement(String statementId) throws Exception {

View File

@@ -0,0 +1,25 @@
package com.ruoyi.info.collection.mapper;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiEnterpriseBaseInfoMapperTest {
@Test
void mapperXml_shouldContainPageQueryAndImportColumns() throws Exception {
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("mapper/info/collection/CcdiEnterpriseBaseInfoMapper.xml")) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("selectEnterpriseBaseInfoPage"), xml);
assertTrue(xml.contains("risk_level"), xml);
assertTrue(xml.contains("ent_source"), xml);
assertTrue(xml.contains("data_source"), xml);
assertTrue(xml.contains("ORDER BY create_time DESC"), xml);
}
}
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.info.collection.mapper;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiIntermediaryMapperTest {
@Test
void mapperXml_shouldContainThreeRecordTypesAndRelatedKeywordQuery() throws Exception {
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("mapper/info/collection/CcdiIntermediaryMapper.xml")) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("INTERMEDIARY"), xml);
assertTrue(xml.contains("RELATIVE"), xml);
assertTrue(xml.contains("ENTERPRISE_RELATION"), xml);
assertTrue(xml.contains("relatedIntermediaryKeyword"), xml);
assertTrue(xml.contains("related_intermediary_name"), xml);
assertTrue(xml.contains("relation_text"), xml);
assertTrue(xml.contains("CAST('实体'"), xml);
}
}
}

View File

@@ -3,6 +3,7 @@ package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiAccountInfo;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoAddDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAccountInfoExcel;
import com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
@@ -16,6 +17,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.BeanWrapperImpl;
import java.math.BigDecimal;
import java.util.List;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -109,6 +111,64 @@ class CcdiAccountInfoServiceImplTest {
assertNull(wrapper.getPropertyValue("txnRiskLevel"));
}
@Test
void insertInternalIntermediaryAccount_shouldForceHighRiskLevelOnAccountInfo() {
CcdiAccountInfoAddDTO dto = buildBaseAddDto();
dto.setOwnerType("INTERMEDIARY");
dto.setOwnerId("91330100MA00000001");
dto.setBankScope("INTERNAL");
dto.setIsActualControl(1);
dto.setAvgMonthTxnCount(8);
dto.setAvgMonthTxnAmount(new BigDecimal("9988.66"));
dto.setTxnFrequencyLevel("HIGH");
dto.setDebitSingleMaxAmount(new BigDecimal("111.11"));
dto.setCreditSingleMaxAmount(new BigDecimal("222.22"));
dto.setDebitDailyMaxAmount(new BigDecimal("333.33"));
dto.setCreditDailyMaxAmount(new BigDecimal("444.44"));
dto.setTxnRiskLevel("LOW");
when(accountInfoMapper.selectCount(any())).thenReturn(0L);
when(accountInfoMapper.insert(any(CcdiAccountInfo.class))).thenReturn(1);
service.insertAccountInfo(dto);
ArgumentCaptor<CcdiAccountInfo> captor = ArgumentCaptor.forClass(CcdiAccountInfo.class);
verify(accountInfoMapper).insert(captor.capture());
BeanWrapperImpl wrapper = new BeanWrapperImpl(captor.getValue());
assertNull(wrapper.getPropertyValue("isActualControl"));
assertNull(wrapper.getPropertyValue("avgMonthTxnCount"));
assertNull(wrapper.getPropertyValue("avgMonthTxnAmount"));
assertNull(wrapper.getPropertyValue("txnFrequencyLevel"));
assertEquals("HIGH", wrapper.getPropertyValue("txnRiskLevel"));
}
@Test
void importExternalIntermediaryAccount_shouldForceHighRiskLevelOnAccountInfo() {
CcdiAccountInfoExcel excel = new CcdiAccountInfoExcel();
excel.setOwnerType("中介");
excel.setOwnerId("91330100MA00000002");
excel.setAccountName("测试中介");
excel.setAccountNo("6222024000000099");
excel.setAccountType("银行账户");
excel.setBankScope("行外");
excel.setOpenBank("中国银行");
excel.setBankCode("BOC");
excel.setCurrency("CNY");
excel.setStatus("正常");
excel.setEffectiveDate("2026-07-10");
excel.setTxnRiskLevel("LOW");
when(accountInfoMapper.selectOne(any())).thenReturn(null);
when(accountInfoMapper.selectCount(any())).thenReturn(0L);
when(accountInfoMapper.insert(any(CcdiAccountInfo.class))).thenReturn(1);
service.importAccountInfo(List.of(excel), false);
ArgumentCaptor<CcdiAccountInfo> captor = ArgumentCaptor.forClass(CcdiAccountInfo.class);
verify(accountInfoMapper).insert(captor.capture());
assertEquals("HIGH", captor.getValue().getTxnRiskLevel());
}
private CcdiAccountInfoAddDTO buildBaseAddDto() {
CcdiAccountInfoAddDTO dto = new CcdiAccountInfoAddDTO();
dto.setAccountNo("6222024000000001");

View File

@@ -0,0 +1,163 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.ImportFailureVO;
import com.ruoyi.info.collection.service.impl.CcdiBaseStaffImportServiceImpl;
import com.ruoyi.system.mapper.SysDeptMapper;
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.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
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.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiBaseStaffDualImportServiceTest {
@InjectMocks
private CcdiBaseStaffImportServiceImpl service;
@Mock
private com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper baseStaffMapper;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Mock
private HashOperations<String, Object, Object> hashOperations;
@Mock
private ValueOperations<String, Object> valueOperations;
@Mock
private SysDeptMapper deptMapper;
@Test
void importBaseStaffAsync_shouldTreatExistingEmployeeAsFailureInsteadOfUpdate() {
CcdiBaseStaffExcel excel = new CcdiBaseStaffExcel();
excel.setStaffId(1001L);
excel.setName("张三");
excel.setDeptId(10L);
excel.setIdCard("11010519491231002X");
excel.setPhone("13812345678");
excel.setStatus("0");
excel.setPartyMember(1);
CcdiBaseStaff existing = new CcdiBaseStaff();
existing.setStaffId(1001L);
existing.setIdCard("11010519491231002X");
when(baseStaffMapper.selectBatchIds(List.of(1001L))).thenReturn(List.of(existing));
when(baseStaffMapper.selectList(any())).thenReturn(List.of(existing));
lenient().when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0"));
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
service.importBaseStaffAsync(List.of(excel), "task-existing");
verify(baseStaffMapper, never()).insertBatch(any());
verify(baseStaffMapper, never()).insertOrUpdateBatch(any());
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:baseStaff:task-existing:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
ImportFailureVO failure = (ImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertEquals("员工信息", failure.getSheetName());
assertEquals(2, failure.getRowNum());
assertEquals(1001L, failure.getStaffId());
assertEquals("该员工ID已存在", failure.getErrorMessage());
}
@Test
void validateStaffData_shouldRejectExistingIdCardWhenStaffIdDoesNotExist() {
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0"));
RuntimeException exception = org.junit.jupiter.api.Assertions.assertThrows(
RuntimeException.class,
() -> service.validateStaffData(buildExcelDto(), Set.of(), Set.of("11010519491231002X"))
);
assertEquals("该身份证号已存在", exception.getMessage());
}
@Test
void importBaseStaffAsync_shouldSaveFailureWhenDeptIsInvalid() {
CcdiBaseStaffExcel validExcel = buildExcel(1001L, 10L, "11010519491231002X");
CcdiBaseStaffExcel invalidExcel = buildExcel(1002L, 99L, "320101199001010014");
when(baseStaffMapper.selectBatchIds(List.of(1001L, 1002L))).thenReturn(List.of());
when(baseStaffMapper.selectList(any())).thenReturn(List.of());
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0"));
when(deptMapper.selectDeptById(99L)).thenReturn(null);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
service.importBaseStaffAsync(List.of(validExcel, invalidExcel), "task-invalid-dept");
verify(baseStaffMapper).insertBatch(any());
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:baseStaff:task-invalid-dept:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
ImportFailureVO failure = (ImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertEquals("员工信息", failure.getSheetName());
assertEquals(3, failure.getRowNum());
assertEquals(1002L, failure.getStaffId());
assertEquals("所属部门ID[99]不存在或已停用/删除,请检查机构号", failure.getErrorMessage());
ArgumentCaptor<Map<String, Object>> statusCaptor = ArgumentCaptor.forClass(Map.class);
verify(hashOperations).putAll(eq("import:baseStaff:task-invalid-dept"), statusCaptor.capture());
assertEquals("PARTIAL_SUCCESS", statusCaptor.getValue().get("status"));
assertEquals(1, statusCaptor.getValue().get("successCount"));
assertEquals(1, statusCaptor.getValue().get("failureCount"));
}
private com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO buildExcelDto() {
com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO dto = new com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO();
dto.setName("李四");
dto.setStaffId(2001L);
dto.setDeptId(10L);
dto.setIdCard("11010519491231002X");
dto.setPhone("13812345678");
dto.setStatus("0");
dto.setPartyMember(1);
return dto;
}
private CcdiBaseStaffExcel buildExcel(Long staffId, Long deptId, String idCard) {
CcdiBaseStaffExcel excel = new CcdiBaseStaffExcel();
excel.setStaffId(staffId);
excel.setName("张三");
excel.setDeptId(deptId);
excel.setIdCard(idCard);
excel.setPhone("13812345678");
excel.setStatus("0");
excel.setPartyMember(1);
return excel;
}
private SysDept buildDept(Long deptId, String status, String delFlag) {
SysDept dept = new SysDept();
dept.setDeptId(deptId);
dept.setDeptName("测试部门");
dept.setStatus(status);
dept.setDelFlag(delFlag);
return dept;
}
}

View File

@@ -1,5 +1,6 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
import com.ruoyi.info.collection.domain.vo.CreditInfoUploadResultVO;
@@ -7,26 +8,33 @@ import com.ruoyi.info.collection.mapper.CcdiCreditInfoQueryMapper;
import com.ruoyi.info.collection.mapper.CcdiCreditNegativeInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiDebtsInfoMapper;
import com.ruoyi.info.collection.service.impl.CcdiCreditInfoServiceImpl;
import com.ruoyi.info.collection.service.support.CreditHtmlStorageService;
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
import com.ruoyi.lsfx.client.CreditParseClient;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeData;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.File;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
@@ -41,6 +49,9 @@ class CcdiCreditInfoServiceImplTest {
@Mock
private CreditParseClient creditParseClient;
@Mock
private CreditHtmlStorageService creditHtmlStorageService;
@Mock
private CreditInfoPayloadAssembler assembler;
@@ -54,11 +65,15 @@ class CcdiCreditInfoServiceImplTest {
private CcdiCreditInfoQueryMapper queryMapper;
@Test
void uploadHtmlFiles_shouldStoreCreditObjectWithoutStaffBinding() {
void uploadHtmlFiles_shouldStoreCreditObjectWithoutStaffBinding() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"files", "family.html", "text/html", "<html>ok</html>".getBytes(StandardCharsets.UTF_8));
when(creditParseClient.parse(anyString(), anyString(), any(File.class)))
when(creditHtmlStorageService.save(any()))
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
"/profile/credit-html/2026/05/12/family_1.html",
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html"));
when(creditParseClient.parse(anyString()))
.thenReturn(successResponse("330101199202020022", "李四", "2026-03-24"));
when(assembler.buildDebts(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
.thenReturn(List.of(buildDebt("330101199202020022")));
@@ -69,15 +84,20 @@ class CcdiCreditInfoServiceImplTest {
assertEquals(1, result.getSuccessCount());
assertEquals(0, result.getFailureCount());
verify(creditParseClient).parse("http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html");
verify(debtsInfoMapper).deleteByPersonId("330101199202020022");
verify(negativeInfoMapper).deleteByPersonId("330101199202020022");
}
@Test
void uploadHtmlFiles_shouldRejectOlderReportDate() {
void uploadHtmlFiles_shouldRejectOlderReportDate() throws Exception {
MockMultipartFile file = new MockMultipartFile("files", "a.html", "text/html", "<html>a</html>".getBytes(StandardCharsets.UTF_8));
when(creditParseClient.parse(anyString(), anyString(), any(File.class)))
when(creditHtmlStorageService.save(any()))
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
"/profile/credit-html/2026/05/12/a_1.html",
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/a_1.html"));
when(creditParseClient.parse(anyString()))
.thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"));
when(queryMapper.selectLatestQueryDate("330101199001010011"))
.thenReturn(LocalDate.parse("2026-03-05"));
@@ -88,7 +108,68 @@ class CcdiCreditInfoServiceImplTest {
assertEquals("上传征信日期早于当前已维护最新记录", result.getFailures().get(0).getReason());
}
private CreditParseResponse successResponse(String personId, String personName, String reportTime) {
@Test
void uploadHtmlFiles_shouldRejectInvalidPlatformCode() throws Exception {
MockMultipartFile file = new MockMultipartFile("files", "a.html", "text/html", "<html>a</html>".getBytes(StandardCharsets.UTF_8));
when(creditHtmlStorageService.save(any()))
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
"/profile/credit-html/2026/05/12/a_1.html",
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/a_1.html"));
CreditParseInvokeResponse response = successResponse("330101199001010011", "张三", "2026-03-03");
response.setCode(99999);
when(creditParseClient.parse(anyString())).thenReturn(response);
CreditInfoUploadResultVO result = service.upload(List.of(file));
assertEquals(0, result.getSuccessCount());
assertEquals("征信解析平台状态码异常: 99999", result.getFailures().get(0).getReason());
}
@Test
void uploadHtmlFiles_shouldRejectInvalidResultStatus() throws Exception {
MockMultipartFile file = new MockMultipartFile("files", "a.html", "text/html", "<html>a</html>".getBytes(StandardCharsets.UTF_8));
when(creditHtmlStorageService.save(any()))
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
"/profile/credit-html/2026/05/12/a_1.html",
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/a_1.html"));
CreditParseInvokeResponse response = successResponse("330101199001010011", "张三", "2026-03-03");
response.getData().setStatus(0);
response.getData().setReasonCode(500);
response.getData().setReasonMessage("结果解析失败");
when(creditParseClient.parse(anyString())).thenReturn(response);
CreditInfoUploadResultVO result = service.upload(List.of(file));
assertEquals(0, result.getSuccessCount());
assertEquals("结果解析失败", result.getFailures().get(0).getReason());
}
@Test
void creditHtmlStorage_shouldStoreHtmlUnderProfileAndBuildRemotePath(@TempDir Path profileDir) throws Exception {
String oldProfile = RuoYiConfig.getProfile();
new RuoYiConfig().setProfile(profileDir.toString());
try {
CreditHtmlStorageService storageService = new CreditHtmlStorageService();
ReflectionTestUtils.setField(storageService, "filePublicBaseUrl", "http://127.0.0.1:62318/");
MockMultipartFile file = new MockMultipartFile(
"files", "credit.html", "text/html", "<html>ok</html>".getBytes(StandardCharsets.UTF_8));
CreditHtmlStorageService.StoredCreditHtml storedHtml = storageService.save(file);
assertTrue(storedHtml.profilePath().startsWith("/profile/credit-html/"));
assertTrue(storedHtml.profilePath().endsWith(".html"));
assertEquals("http://127.0.0.1:62318" + storedHtml.profilePath(), storedHtml.remotePath());
Path savedFile = profileDir.resolve(storedHtml.profilePath().substring("/profile/".length()));
assertTrue(Files.exists(savedFile));
} finally {
new RuoYiConfig().setProfile(oldProfile);
}
}
private CreditParseInvokeResponse successResponse(String personId, String personName, String reportTime) {
CreditParsePayload payload = new CreditParsePayload();
Map<String, Object> header = new HashMap<>();
header.put("query_cert_no", personId);
@@ -99,9 +180,20 @@ class CcdiCreditInfoServiceImplTest {
payload.setLxPublictype(Map.of("civil_cnt", 1));
CreditParseResponse response = new CreditParseResponse();
response.setStatusCode("0");
response.setMessage("成功");
response.setStatusCode("ERR_SHOULD_IGNORE");
response.setPayload(payload);
return response;
CreditParseInvokeData data = new CreditParseInvokeData();
data.setMappingOutputFields(response);
data.setStatus(1);
data.setReasonCode(200);
CreditParseInvokeResponse invokeResponse = new CreditParseInvokeResponse();
invokeResponse.setSuccess(true);
invokeResponse.setCode(10000);
invokeResponse.setData(data);
return invokeResponse;
}
private CcdiDebtsInfo buildDebt(String personId) {

View File

@@ -0,0 +1,113 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiBizIntermediary;
import com.ruoyi.info.collection.domain.dto.CcdiIntermediaryRelativeAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiIntermediaryPersonAddDTO;
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.mapper.CcdiIntermediaryMapper;
import com.ruoyi.info.collection.service.impl.CcdiIntermediaryServiceImpl;
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.RedisTemplate;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
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 CcdiIntermediaryServiceImplTest {
@InjectMocks
private CcdiIntermediaryServiceImpl service;
@Mock
private CcdiBizIntermediaryMapper bizIntermediaryMapper;
@Mock
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
@Mock
private CcdiIntermediaryMapper intermediaryMapper;
@Mock
private CcdiIntermediaryEnterpriseRelationMapper enterpriseRelationMapper;
@Mock
private ICcdiIntermediaryPersonImportService personImportService;
@Mock
private ICcdiIntermediaryEntityImportService entityImportService;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Test
void insertIntermediaryPerson_shouldForceBenrenAndClearRelatedNumId() {
CcdiIntermediaryPersonAddDTO addDTO = new CcdiIntermediaryPersonAddDTO();
addDTO.setName("测试中介");
addDTO.setPersonId("320101199001010011");
addDTO.setPersonSubType("配偶");
addDTO.setRelatedNumId("parent-id");
when(bizIntermediaryMapper.selectCount(any())).thenReturn(0L);
when(bizIntermediaryMapper.insert(any(CcdiBizIntermediary.class))).thenReturn(1);
int result = service.insertIntermediaryPerson(addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiBizIntermediary> captor = ArgumentCaptor.forClass(CcdiBizIntermediary.class);
verify(bizIntermediaryMapper).insert(captor.capture());
assertEquals("本人", captor.getValue().getPersonSubType());
assertNull(captor.getValue().getRelatedNumId());
assertEquals("MANUAL", captor.getValue().getDataSource());
}
@Test
void insertIntermediaryRelative_shouldRejectBenrenSubType() {
CcdiBizIntermediary owner = new CcdiBizIntermediary();
owner.setBizId("biz-1");
owner.setPersonSubType("本人");
CcdiIntermediaryRelativeAddDTO addDTO = new CcdiIntermediaryRelativeAddDTO();
addDTO.setName("测试亲属");
addDTO.setPersonId("320101199001010022");
addDTO.setPersonSubType("本人");
when(bizIntermediaryMapper.selectById("biz-1")).thenReturn(owner);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.insertIntermediaryRelative("biz-1", addDTO));
assertEquals("亲属关系不能为本人", exception.getMessage());
verify(bizIntermediaryMapper, never()).insert(any(CcdiBizIntermediary.class));
}
@Test
void deleteIntermediaryByIds_shouldDeleteRelativesAndEnterpriseRelationsWhenRemovingOwner() {
CcdiBizIntermediary owner = new CcdiBizIntermediary();
owner.setBizId("biz-1");
owner.setPersonSubType("本人");
when(bizIntermediaryMapper.selectById("biz-1")).thenReturn(owner);
when(bizIntermediaryMapper.delete(any())).thenReturn(2);
when(enterpriseRelationMapper.delete(any())).thenReturn(1);
when(bizIntermediaryMapper.deleteById("biz-1")).thenReturn(1);
int result = service.deleteIntermediaryByIds(new String[]{"biz-1"});
assertEquals(1, result);
verify(bizIntermediaryMapper).delete(any());
verify(enterpriseRelationMapper).delete(any());
verify(bizIntermediaryMapper).deleteById("biz-1");
}
}

View File

@@ -0,0 +1,78 @@
package com.ruoyi.info.collection.service;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiPurchaseTransactionFeatureContractTest {
@Test
void shouldExposeSupplierListContractsAcrossPurchaseTransactionModels() throws Exception {
assertHasField(
"com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO",
"supplierList"
);
assertHasField(
"com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO",
"supplierList"
);
assertHasField(
"com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO",
"supplierList"
);
assertHasField(
"com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO",
"supplierCount"
);
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier"));
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionSupplierDTO"));
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionSupplierVO"));
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel"));
}
@Test
void shouldDefineSupplierSubTableAndBiddingMigrationScripts() throws Exception {
String initSql = Files.readString(repoPath("sql/ccdi_purchase_transaction.sql"));
assertTrue(initSql.contains("CREATE TABLE `ccdi_purchase_transaction_supplier`"));
assertTrue(initSql.contains("`is_bid_winner`"));
assertTrue(initSql.contains("`sort_order`"));
assertTrue(initSql.contains("utf8mb4_general_ci"));
String menuSql = Files.readString(repoPath("sql/ccdi_purchase_transaction_menu.sql"));
assertTrue(menuSql.contains("招投标信息维护"));
Path migrationPath = repoPath("sql/migration/2026-04-22-bidding-info-maintenance-supplier-detail.sql");
assertTrue(Files.exists(migrationPath), "应提供招投标供应商明细迁移脚本");
String migrationSql = Files.readString(migrationPath);
assertTrue(migrationSql.contains("ccdi_purchase_transaction_supplier"));
assertTrue(migrationSql.contains("INSERT INTO ccdi_purchase_transaction_supplier"));
assertTrue(migrationSql.contains("UPDATE sys_menu"));
assertTrue(migrationSql.contains("招投标信息维护"));
}
@Test
void shouldUseTwoSheetTemplateForBiddingImport() throws Exception {
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel"));
String controller = Files.readString(
Path.of("src/main/java/com/ruoyi/info/collection/controller/CcdiPurchaseTransactionController.java")
);
assertTrue(controller.contains("招投标主信息"));
assertTrue(controller.contains("供应商明细"));
}
private void assertHasField(String className, String fieldName) throws Exception {
Class<?> clazz = Class.forName(className);
Field field = clazz.getDeclaredField(fieldName);
assertNotNull(field);
}
private Path repoPath(String relativePath) {
return Path.of("..", relativePath);
}
}

View File

@@ -12,6 +12,7 @@ import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CreditInfoPayloadAssemblerTest {
@@ -28,13 +29,18 @@ class CreditInfoPayloadAssemblerTest {
debt.put("uncle_not_bank_bal", "2000");
debt.put("uncle_not_bank_lmt", "3000");
debt.put("uncle_not_bank_state", "逾期");
debt.put("uncle_credit_card_bal", "100");
debt.put("uncle_credit_card_lmt", "500");
debt.put("uncle_credit_card_state", "正常");
payload.setLxDebt(debt);
List<CcdiDebtsInfo> rows = assembler.buildDebts("330101199001010011", "张三", LocalDate.parse("2026-03-01"), payload);
assertEquals(2, rows.size());
assertEquals(3, rows.size());
assertEquals("住房贷款", rows.get(0).getDebtSubType());
assertEquals("非银", rows.get(1).getCreditorType());
assertEquals("信用卡", rows.get(2).getDebtSubType());
assertEquals(new BigDecimal("500"), rows.get(2).getDebtTotalAmount());
}
@Test
@@ -51,6 +57,42 @@ class CreditInfoPayloadAssemblerTest {
assertEquals(new BigDecimal("9800"), info.getCivilLmt());
}
@Test
void shouldSkipDebtTypeWhenStateIsMissingSentinel() {
CreditParsePayload payload = new CreditParsePayload();
Map<String, Object> debt = new HashMap<>();
debt.put("uncle_bank_house_bal", "50000");
debt.put("uncle_bank_house_lmt", "100000");
debt.put("uncle_bank_house_state", "-9999");
debt.put("uncle_not_bank_bal", "2000");
debt.put("uncle_not_bank_lmt", "3000");
debt.put("uncle_not_bank_state", "正常");
payload.setLxDebt(debt);
List<CcdiDebtsInfo> rows = assembler.buildDebts("330101199001010011", "张三", LocalDate.parse("2026-03-01"), payload);
assertEquals(1, rows.size());
assertEquals("非银行贷款", rows.get(0).getDebtSubType());
}
@Test
void shouldTreatNegativeRiskMissingSentinelAsEmptyValue() {
CreditParsePayload payload = new CreditParsePayload();
Map<String, Object> publictype = new HashMap<>();
publictype.put("civil_cnt", "-9999");
publictype.put("civil_lmt", "-9999.0");
publictype.put("enforce_cnt", 1);
publictype.put("enforce_lmt", "1200");
payload.setLxPublictype(publictype);
CcdiCreditNegativeInfo info = assembler.buildNegative("330101199001010011", "张三", LocalDate.parse("2026-03-01"), payload);
assertEquals(0, info.getCivilCnt());
assertNull(info.getCivilLmt());
assertEquals(1, info.getEnforceCnt());
assertEquals(new BigDecimal("1200"), info.getEnforceLmt());
}
@Test
void shouldSkipDebtRowWhenAllMetricsAreEmpty() {
CreditParsePayload payload = new CreditParsePayload();

View File

@@ -0,0 +1,248 @@
package com.ruoyi.info.collection.utils;
import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.DataValidationHelper;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class EasyExcelUtilImportDropdownValidationTest {
@Test
void importExcel_shouldPassWhenAllDictDropdownColumnsKeepListValidation() throws Exception {
byte[] bytes = baseStaffWorkbook(true, true, true, 2);
List<CcdiBaseStaffExcel> rows = EasyExcelUtil.importExcel(
new ByteArrayInputStream(bytes),
CcdiBaseStaffExcel.class,
"员工信息"
);
assertEquals(2, rows.size());
}
@Test
void importExcel_shouldFailWhenPartyMemberDropdownIsMissing() throws Exception {
byte[] bytes = baseStaffWorkbook(false, true, true, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertTrue(exception.getMessage().contains("是否党员 列缺少下拉框"));
}
@Test
void importExcel_shouldFailWhenStatusDropdownIsMissing() throws Exception {
byte[] bytes = baseStaffWorkbook(true, false, true, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertEquals("员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
}
@Test
void importExcel_shouldReportAllMissingDropdownColumnsInSameSheet() throws Exception {
byte[] bytes = baseStaffWorkbook(false, false, true, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertEquals("员工信息 Sheet 的 是否党员、状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
}
@Test
void importExcel_shouldFailWhenValidationIsNotListType() throws Exception {
byte[] bytes = baseStaffWorkbook(true, true, false, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertTrue(exception.getMessage().contains("状态 列缺少下拉框"));
}
@Test
void importExcel_shouldFailWhenListValidationDoesNotCoverEveryActualDataRow() throws Exception {
byte[] bytes = baseStaffWorkbook(true, true, true, 1);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertTrue(exception.getMessage().contains("状态 列缺少下拉框"));
}
@Test
void importExcel_shouldFailWhenSecondSheetDropdownIsMissing() throws Exception {
byte[] bytes = baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown();
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffAssetInfoExcel.class, "员工资产信息")
);
assertEquals("员工资产信息 Sheet 的 资产状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
}
@Test
void importExcel_shouldSkipDropdownStructureValidationWhenClassHasNoDictDropdownFields() throws Exception {
byte[] bytes = plainWorkbookWithoutDropdown();
List<PlainExcel> rows = EasyExcelUtil.importExcel(
new ByteArrayInputStream(bytes),
PlainExcel.class,
"普通信息"
);
assertEquals(1, rows.size());
}
private byte[] baseStaffWorkbook(boolean partyDropdown, boolean statusDropdown, boolean statusAsList,
int statusLastRow) throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("员工信息");
Row header = sheet.createRow(0);
String[] headers = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)",
"入职时间", "是否党员", "状态"};
for (int i = 0; i < headers.length; i++) {
header.createCell(i).setCellValue(headers[i]);
}
createBaseStaffRow(sheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1");
createBaseStaffRow(sheet, 2, "李四", 9020002L, "330106198603031022", "1", "1");
if (partyDropdown) {
addListValidation(sheet, 7, 1, 2, "0", "1");
}
if (statusDropdown) {
if (statusAsList) {
addListValidation(sheet, 8, 1, statusLastRow, "0", "1");
} else {
addIntegerValidation(sheet, 8, 1, 2);
}
}
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private byte[] baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown() throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Sheet staffSheet = workbook.createSheet("员工信息");
Row staffHeader = staffSheet.createRow(0);
String[] staffHeaders = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)",
"入职时间", "是否党员", "状态"};
for (int i = 0; i < staffHeaders.length; i++) {
staffHeader.createCell(i).setCellValue(staffHeaders[i]);
}
createBaseStaffRow(staffSheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1");
addListValidation(staffSheet, 7, 1, 1, "0", "1");
addListValidation(staffSheet, 8, 1, 1, "0", "1");
Sheet assetSheet = workbook.createSheet("员工资产信息");
Row assetHeader = assetSheet.createRow(0);
String[] assetHeaders = {"员工身份证号*", "资产大类*", "资产小类*", "资产名称*", "产权占比",
"购买/评估日期", "资产原值", "当前估值*", "估值截止日期", "资产状态*", "备注"};
for (int i = 0; i < assetHeaders.length; i++) {
assetHeader.createCell(i).setCellValue(assetHeaders[i]);
}
Row assetRow = assetSheet.createRow(1);
assetRow.createCell(0).setCellValue("33010619850202101X");
assetRow.createCell(1).setCellValue("房产");
assetRow.createCell(2).setCellValue("住宅");
assetRow.createCell(3).setCellValue("测试住宅");
assetRow.createCell(7).setCellValue(1000000D);
assetRow.createCell(9).setCellValue("正常");
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private void createBaseStaffRow(Sheet sheet, int rowIndex, String name, long staffId, String idCard,
String partyMember, String status) {
Row row = sheet.createRow(rowIndex);
row.createCell(0).setCellValue(name);
row.createCell(1).setCellValue(staffId);
row.createCell(2).setCellValue(103L);
row.createCell(3, CellType.STRING).setCellValue(idCard);
row.createCell(4, CellType.STRING).setCellValue("13370000001");
row.createCell(5).setCellValue(new BigDecimal("180000").doubleValue());
row.createCell(6).setCellValue("2026-04-30");
row.createCell(7, CellType.STRING).setCellValue(partyMember);
row.createCell(8, CellType.STRING).setCellValue(status);
}
private void addListValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow, String... options) {
DataValidationHelper helper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = helper.createExplicitListConstraint(options);
DataValidation validation = helper.createValidation(
constraint,
new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex)
);
sheet.addValidationData(validation);
}
private void addIntegerValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow) {
DataValidationHelper helper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = helper.createIntegerConstraint(
DataValidationConstraint.OperatorType.BETWEEN,
"0",
"1"
);
DataValidation validation = helper.createValidation(
constraint,
new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex)
);
sheet.addValidationData(validation);
}
private byte[] plainWorkbookWithoutDropdown() throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("普通信息");
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("名称");
Row row = sheet.createRow(1);
row.createCell(0).setCellValue("张三");
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
public static class PlainExcel {
@ExcelProperty(value = "名称", index = 0)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -1,6 +1,9 @@
package com.ruoyi.lsfx.client;
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.exception.LsfxApiException;
import com.ruoyi.lsfx.util.HttpUtil;
import jakarta.annotation.Resource;
@@ -8,7 +11,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
@@ -16,32 +18,191 @@ import java.util.Map;
@Component
public class CreditParseClient {
private static final int PLATFORM_SUCCESS_CODE = 10000;
private static final int INITIATE_SUCCESS_STATUS = 1;
private static final int INITIATE_SUCCESS_REASON_CODE = 200;
private static final int RESULT_QUERY_MAX_ATTEMPTS = 5;
private static final long RESULT_QUERY_INTERVAL_MILLIS = 2000L;
@Resource
private HttpUtil httpUtil;
@Resource
private ObjectMapper objectMapper;
@Value("${credit-parse.api.url}")
private String creditParseUrl;
public CreditParseResponse parse(String model, String hType, File file) {
@Value("${credit-parse.api.result-url}")
private String creditParseResultUrl;
@Value("${credit-parse.api.org-code:999000}")
private String orgCode;
@Value("${credit-parse.api.run-type:1}")
private String runType;
@Value("${credit-parse.api.model:LXCUSTALL}")
private String defaultModel;
public CreditParseInvokeResponse parse(String remotePath) {
return parse(defaultModel, remotePath);
}
public CreditParseInvokeResponse parse(String model, String remotePath) {
long startTime = System.currentTimeMillis();
log.info("【征信解析】开始调用: fileName={}, model={}, hType={}", file.getName(), model, hType);
String actualModel = StringUtils.isBlank(model) ? defaultModel : model;
String serialNum = buildSerialNum();
try {
Map<String, Object> params = new HashMap<>();
params.put("model", model);
params.put("hType", hType);
params.put("file", file);
Map<String, Object> initiateParams = buildInitiateParams(serialNum, actualModel, remotePath);
CreditParseInvokeResponse initiateResponse = request(creditParseUrl, initiateParams, "发起接口");
requireSuccessfulInitiateResponse(initiateResponse, "征信解析发起接口");
CreditParseResponse response = httpUtil.uploadFile(creditParseUrl, params, null, CreditParseResponse.class);
CreditParseInvokeResponse response = queryResult(serialNum);
long elapsed = System.currentTimeMillis() - startTime;
log.info("【征信解析】调用完成: statusCode={}, cost={}ms",
response != null ? response.getStatusCode() : null, elapsed);
log.info("【征信解析】调用完成: success={}, code={}, businessStatusCode={}, cost={}ms",
response != null ? response.getSuccess() : null,
response != null ? response.getCode() : null,
response != null && response.getData() != null && response.getData().getMappingOutputFields() != null
? response.getData().getMappingOutputFields().getStatusCode() : null,
elapsed);
return response;
} catch (LsfxApiException e) {
log.error("【征信解析】调用失败: serialNum={}, model={}, remotePath={}, error={}",
serialNum, actualModel, remotePath, e.getMessage(), e);
throw e;
} catch (Exception e) {
log.error("【征信解析】调用失败: fileName={}, model={}, hType={}, error={}",
file.getName(), model, hType, e.getMessage(), e);
log.error("【征信解析】调用失败: serialNum={}, model={}, remotePath={}, error={}",
serialNum, actualModel, remotePath, e.getMessage(), e);
throw new LsfxApiException("征信解析调用失败: " + e.getMessage(), e);
}
}
private Map<String, Object> buildInitiateParams(String serialNum, String model, String remotePath) {
Map<String, Object> params = buildBaseParams(serialNum);
params.put("remotePath", remotePath);
params.put("model", model);
return params;
}
private Map<String, Object> buildBaseParams(String serialNum) {
Map<String, Object> params = new HashMap<>();
params.put("serialNum", serialNum);
params.put("orgCode", orgCode);
params.put("runType", runType);
return params;
}
private CreditParseInvokeResponse queryResult(String serialNum) {
Map<String, Object> params = buildBaseParams(serialNum);
for (int attempt = 1; attempt <= RESULT_QUERY_MAX_ATTEMPTS; attempt++) {
CreditParseInvokeResponse response = request(creditParseResultUrl, params,
"结果接口第" + attempt + "次查询");
requireSuccessfulServiceResponse(response, "征信解析结果接口");
if (response.getData() == null || response.getData().getMappingOutputFields() == null) {
waitForNextResult(serialNum, attempt);
continue;
}
if (response.getData().getMappingOutputFields().getPayload() != null) {
return response;
}
waitForNextResult(serialNum, attempt);
}
throw new LsfxApiException("征信解析结果未返回");
}
private void waitForNextResult(String serialNum, int attempt) {
if (attempt >= RESULT_QUERY_MAX_ATTEMPTS) {
return;
}
log.info("【征信解析】结果未返回: serialNum={}, attempt={}/{}, {}ms后重试",
serialNum, attempt, RESULT_QUERY_MAX_ATTEMPTS, RESULT_QUERY_INTERVAL_MILLIS);
try {
sleepBeforeNextResultQuery(RESULT_QUERY_INTERVAL_MILLIS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LsfxApiException("征信解析结果查询被中断", e);
}
}
protected void sleepBeforeNextResultQuery(long intervalMillis) throws InterruptedException {
Thread.sleep(intervalMillis);
}
private CreditParseInvokeResponse request(String url, Map<String, Object> params, String stage) {
try {
log.info("【征信解析】{}请求: url={}, params={}", stage, url, toJson(params));
String responseJson = httpUtil.postUrlEncodedFormForString(url, params, null);
log.info("【征信解析】{}返回JSON: {}", stage, responseJson);
return objectMapper.readValue(responseJson, CreditParseInvokeResponse.class);
} catch (LsfxApiException e) {
throw e;
} catch (Exception e) {
throw new LsfxApiException("征信解析" + stage + "调用失败: " + e.getMessage(), e);
}
}
private void requireSuccessfulInitiateResponse(CreditParseInvokeResponse response, String stage) {
requireSuccessfulServiceResponse(response, stage);
}
private void requireSuccessfulServiceResponse(CreditParseInvokeResponse response, String stage) {
requireSuccessfulPlatformResponse(response, stage);
if (response.getData() == null) {
throw new LsfxApiException(stage + "返回结果为空");
}
if (!Integer.valueOf(INITIATE_SUCCESS_STATUS).equals(response.getData().getStatus())) {
throw new LsfxApiException(serviceErrorMessage(response, stage + "状态异常: " + response.getData().getStatus()));
}
if (!Integer.valueOf(INITIATE_SUCCESS_REASON_CODE).equals(response.getData().getReasonCode())) {
throw new LsfxApiException(serviceErrorMessage(response, stage + "原因码异常: " + response.getData().getReasonCode()));
}
}
private void requireSuccessfulPlatformResponse(CreditParseInvokeResponse response, String stage) {
if (response == null) {
throw new LsfxApiException(stage + "返回结果为空");
}
if (!Boolean.TRUE.equals(response.getSuccess())) {
throw new LsfxApiException(stage + "平台调用失败");
}
if (!Integer.valueOf(PLATFORM_SUCCESS_CODE).equals(response.getCode())) {
throw new LsfxApiException(stage + "平台状态码异常: " + response.getCode());
}
}
private String buildSerialNum() {
return "CCDI_CREDIT_" + System.currentTimeMillis() + "_" + IdUtils.fastSimpleUUID();
}
private String stringValue(Object value, String defaultValue) {
if (value == null) {
return defaultValue;
}
String text = value.toString().trim();
return text.isEmpty() ? defaultValue : text;
}
private String serviceErrorMessage(CreditParseInvokeResponse response, String defaultValue) {
if (response == null || response.getData() == null) {
return defaultValue;
}
String reasonMessage = stringValue(response.getData().getReasonMessage(), null);
if (reasonMessage != null) {
return reasonMessage;
}
if (response.getData().getMappingOutputFields() != null) {
return stringValue(response.getData().getMappingOutputFields().getMessage(), defaultValue);
}
return defaultValue;
}
private String toJson(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (Exception e) {
return String.valueOf(value);
}
}
}

View File

@@ -17,6 +17,7 @@ public class LsfxConstants {
/** 数据渠道编码 */
public static final String DATA_CHANNEL_ZJRCU = "ZJRCU";
public static final String DATA_CHANNEL_JZL = "JZL";
/** 分析类型 */
public static final String ANALYSIS_TYPE = "-1";

View File

@@ -4,7 +4,7 @@ import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lsfx.client.CreditParseClient;
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.exception.LsfxApiException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -14,13 +14,6 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@Tag(name = "征信解析接口测试", description = "用于测试征信解析接口")
@Anonymous
@@ -29,56 +22,27 @@ import java.nio.file.StandardCopyOption;
public class CreditParseController {
private static final String DEFAULT_MODEL = "LXCUSTALL";
private static final String DEFAULT_HTYPE = "PERSON";
@Resource
private CreditParseClient creditParseClient;
@Operation(summary = "解析征信HTML", description = "传征信HTML文件并调用外部解析服务")
@Operation(summary = "解析征信HTML", description = "征信HTML远程地址并调用外部解析服务")
@PostMapping("/parse")
public AjaxResult parse(@Parameter(description = "征信HTML文件") @RequestParam("file") MultipartFile file,
@Parameter(description = "解析模型默认LXCUSTALL") @RequestParam(required = false) String model,
@Parameter(description = "主体类型默认PERSON") @RequestParam(required = false) String hType) {
if (file == null || file.isEmpty()) {
return AjaxResult.error("征信HTML文件不能为空");
}
String originalFilename = file.getOriginalFilename();
if (StringUtils.isBlank(originalFilename)) {
return AjaxResult.error("文件名不能为空");
}
String lowerCaseName = originalFilename.toLowerCase();
if (!lowerCaseName.endsWith(".html") && !lowerCaseName.endsWith(".htm")) {
return AjaxResult.error("仅支持 HTML 格式文件");
public AjaxResult parse(@Parameter(description = "征信HTML远程访问地址") @RequestParam("remotePath") String remotePath,
@Parameter(description = "模型编码默认LXCUSTALL") @RequestParam(required = false) String model) {
if (StringUtils.isBlank(remotePath)) {
return AjaxResult.error("征信HTML远程地址不能为空");
}
String actualModel = StringUtils.isBlank(model) ? DEFAULT_MODEL : model;
String actualHType = StringUtils.isBlank(hType) ? DEFAULT_HTYPE : hType;
Path tempFile = null;
try {
String suffix = lowerCaseName.endsWith(".htm") ? ".htm" : ".html";
tempFile = Files.createTempFile("credit_parse_", suffix);
Files.copy(file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);
File convertedFile = tempFile.toFile();
CreditParseResponse response = creditParseClient.parse(actualModel, actualHType, convertedFile);
CreditParseInvokeResponse response = creditParseClient.parse(actualModel, remotePath);
return AjaxResult.success(response);
} catch (LsfxApiException e) {
return AjaxResult.error(e.getMessage());
} catch (IOException e) {
return AjaxResult.error("文件转换失败:" + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("征信解析失败:" + e.getMessage());
} finally {
if (tempFile != null) {
try {
Files.deleteIfExists(tempFile);
} catch (IOException ignored) {
// 忽略临时文件删除失败,避免影响主流程返回
}
}
}
}
}

View File

@@ -14,7 +14,7 @@ public class FetchInnerFlowRequest {
/** 客户身份证号 */
private String customerNo;
/** 数据渠道编码(固定值:ZJRCU) */
/** 数据渠道编码(ZJRCU-行内JZL-金综) */
private String dataChannelCode;
/** 发起请求的时间(格式:yyyyMMdd) */

View File

@@ -0,0 +1,17 @@
package com.ruoyi.lsfx.domain.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CreditParseInvokeData {
private CreditParseResponse mappingOutputFields;
private Integer status;
private Integer reasonCode;
private String reasonMessage;
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.lsfx.domain.response;
import lombok.Data;
@Data
public class CreditParseInvokeResponse {
private Boolean success;
private Integer code;
private CreditParseInvokeData data;
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.lsfx.domain.response;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import java.io.IOException;
public class CreditParsePayloadDeserializer extends JsonDeserializer<CreditParsePayload> {
@Override
public CreditParsePayload deserialize(JsonParser parser, DeserializationContext context) throws IOException {
ObjectCodec codec = parser.getCodec();
JsonNode node = codec.readTree(parser);
if (node == null || node.isNull()) {
return null;
}
if (node.isTextual()) {
String payloadText = node.asText();
if (payloadText == null || payloadText.trim().isEmpty()) {
return null;
}
node = codec.readTree(codec.getFactory().createParser(payloadText));
}
if (!node.isObject()) {
throw JsonMappingException.from(parser, "征信解析payload格式不支持");
}
return codec.treeToValue(node, CreditParsePayload.class);
}
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.lsfx.domain.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Data;
@Data
@@ -11,5 +12,6 @@ public class CreditParseResponse {
@JsonProperty("status_code")
private String statusCode;
@JsonDeserialize(using = CreditParsePayloadDeserializer.class)
private CreditParsePayload payload;
}

View File

@@ -51,7 +51,7 @@ public class GetBankStatementResponse {
private String leName;
/** 企业银行账号 */
private String accountMaskNo;
private String accountNo;
/** 账号日期ID */
private Integer accountingDateId;
@@ -102,7 +102,7 @@ public class GetBankStatementResponse {
private String customerName;
/** 对手方账号 */
private String customerAccountMaskNo;
private String customerAccountNo;
/** 对手方银行 */
private String customerBank;

View File

@@ -206,6 +206,86 @@ public class HttpUtil {
}
}
/**
* 发送POST请求application/x-www-form-urlencoded格式带请求头
* @param url 请求URL
* @param params 表单参数
* @param headers 请求头
* @param responseType 响应类型
* @return 响应对象
*/
public <T> T postUrlEncodedForm(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
try {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach((key, value) -> {
if (value != null) {
body.add(key, value.toString());
}
});
}
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new LsfxApiException("API调用失败HTTP状态码: " + response.getStatusCode());
}
T responseBody = response.getBody();
if (responseBody == null) {
throw new LsfxApiException("API返回数据为空");
}
return responseBody;
} catch (RestClientException e) {
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
}
}
/**
* 发送POST请求application/x-www-form-urlencoded格式并返回原始JSON字符串
* @param url 请求URL
* @param params 表单参数
* @param headers 请求头
* @return 原始响应内容
*/
public String postUrlEncodedFormForString(String url, Map<String, Object> params, Map<String, String> headers) {
try {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach((key, value) -> {
if (value != null) {
body.add(key, value.toString());
}
});
}
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new LsfxApiException("API调用失败HTTP状态码: " + response.getStatusCode());
}
String responseBody = response.getBody();
if (responseBody == null) {
throw new LsfxApiException("API返回数据为空");
}
return responseBody;
} catch (RestClientException e) {
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
}
}
/**
* 上传文件Multipart格式
* @param url 请求URL

View File

@@ -0,0 +1,61 @@
package com.ruoyi.lsfx.client;
import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.response.UploadFileResponse;
import com.ruoyi.lsfx.util.HttpUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
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.core.io.Resource;
import org.springframework.test.util.ReflectionTestUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class LsfxAnalysisClientTest {
@Mock
private HttpUtil httpUtil;
@InjectMocks
private LsfxAnalysisClient client;
@TempDir
Path tempDir;
@Test
void uploadFile_shouldPassOriginalFilenameToMultipartResource() throws Exception {
ReflectionTestUtils.setField(client, "baseUrl", "http://lsfx");
ReflectionTestUtils.setField(client, "uploadFileEndpoint", "/upload");
ReflectionTestUtils.setField(client, "clientId", "client-1");
Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
Files.writeString(tempFile, "content");
UploadFileResponse response = new UploadFileResponse();
response.setData(new UploadFileResponse.UploadData());
ArgumentCaptor<Map<String, Object>> paramsCaptor = ArgumentCaptor.forClass(Map.class);
ArgumentCaptor<Map<String, String>> headersCaptor = ArgumentCaptor.forClass(Map.class);
when(httpUtil.uploadFile(eq("http://lsfx/upload"), paramsCaptor.capture(), headersCaptor.capture(), eq(UploadFileResponse.class)))
.thenReturn(response);
client.uploadFile(200, tempFile.toFile(), "银行流水A.xlsx");
assertEquals(200, paramsCaptor.getValue().get("groupId"));
Resource filePart = assertInstanceOf(Resource.class, paramsCaptor.getValue().get("files"));
assertEquals("银行流水A.xlsx", filePart.getFilename());
assertEquals("client-1", headersCaptor.getValue().get(LsfxConstants.HEADER_CLIENT_ID));
}
}

View File

@@ -1,24 +1,34 @@
package com.ruoyi.lsfx.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.lsfx.client.CreditParseClient;
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.exception.LsfxApiException;
import com.ruoyi.lsfx.util.HttpUtil;
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.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -31,31 +41,21 @@ class CreditParseControllerTest {
private CreditParseController controller;
@Test
void parse_shouldRejectEmptyFile() {
AjaxResult result = controller.parse(null, null, null);
void parse_shouldRejectBlankRemotePath() {
AjaxResult result = controller.parse(null, null);
assertEquals(500, result.get("code"));
}
@Test
void parse_shouldRejectNonHtmlFile() {
MockMultipartFile file = new MockMultipartFile(
"file", "credit.pdf", "application/pdf", "x".getBytes(StandardCharsets.UTF_8)
);
AjaxResult result = controller.parse(file, null, null);
assertEquals(500, result.get("code"));
}
void shouldUseDefaultModelWhenMissing() {
CreditParseInvokeResponse response = new CreditParseInvokeResponse();
response.setSuccess(true);
response.setCode(10000);
@Test
void shouldUseDefaultModelAndTypeWhenMissing() {
MockMultipartFile file = new MockMultipartFile(
"file", "credit.html", "text/html", "<html/>".getBytes(StandardCharsets.UTF_8)
);
CreditParseResponse response = new CreditParseResponse();
response.setStatusCode("0");
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
when(client.parse(eq("LXCUSTALL"), eq(remotePath))).thenReturn(response);
when(client.parse(eq("LXCUSTALL"), eq("PERSON"), any(File.class))).thenReturn(response);
AjaxResult result = controller.parse(file, null, null);
AjaxResult result = controller.parse(remotePath, null);
assertEquals(200, result.get("code"));
assertSame(response, result.get("data"));
@@ -63,14 +63,230 @@ class CreditParseControllerTest {
@Test
void shouldReturnAjaxErrorWhenClientThrows() {
MockMultipartFile file = new MockMultipartFile(
"file", "credit.html", "text/html", "<html/>".getBytes(StandardCharsets.UTF_8)
);
when(client.parse(anyString(), anyString(), any(File.class)))
when(client.parse(anyString(), anyString()))
.thenThrow(new LsfxApiException("超时"));
AjaxResult result = controller.parse(file, null, null);
AjaxResult result = controller.parse("http://127.0.0.1:62318/profile/credit-html/a.html", null);
assertEquals(500, result.get("code"));
}
@Test
@SuppressWarnings({"unchecked", "rawtypes"})
void creditParseClient_shouldInitiateAndQueryResultWithSameSerialNum() throws Exception {
HttpUtil httpUtil = mock(HttpUtil.class);
CreditParseClient parseClient = new CreditParseClient();
ObjectMapper objectMapper = new ObjectMapper();
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
ReflectionTestUtils.setField(parseClient, "runType", "1");
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
String payload = "{\"lx_header\":{\"query_cert_no\":\"330101199001010011\",\"query_cust_name\":\"张三\",\"report_time\":\"2026-03-24\"},\"lx_debt\":{\"uncle_bank_house_bal\":\"1\"},\"lx_publictype\":{\"civil_cnt\":1}}";
when(httpUtil.postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(initiateSuccessResponse());
when(httpUtil.postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(resultSuccessResponse(objectMapper, payload, "ERR_SHOULD_IGNORE"));
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
CreditParseInvokeResponse actual = parseClient.parse(remotePath);
assertEquals(true, actual.getSuccess());
assertEquals(10000, actual.getCode());
assertEquals("330101199001010011", actual.getData().getMappingOutputFields()
.getPayload().getLxHeader().get("query_cert_no"));
ArgumentCaptor<Map<String, Object>> initiateParamsCaptor = ArgumentCaptor.forClass((Class) Map.class);
verify(httpUtil).postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeature"),
initiateParamsCaptor.capture(),
isNull()
);
ArgumentCaptor<Map<String, Object>> resultParamsCaptor = ArgumentCaptor.forClass((Class) Map.class);
verify(httpUtil).postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
resultParamsCaptor.capture(),
isNull()
);
Map<String, Object> initiateParams = initiateParamsCaptor.getValue();
Map<String, Object> resultParams = resultParamsCaptor.getValue();
assertNotNull(initiateParams.get("serialNum"));
assertTrue(initiateParams.get("serialNum").toString().startsWith("CCDI_CREDIT_"));
assertEquals(initiateParams.get("serialNum"), resultParams.get("serialNum"));
assertEquals("999000", initiateParams.get("orgCode"));
assertEquals("999000", resultParams.get("orgCode"));
assertEquals("1", initiateParams.get("runType"));
assertEquals("1", resultParams.get("runType"));
assertEquals(remotePath, initiateParams.get("remotePath"));
assertEquals("LXCUSTALL", initiateParams.get("model"));
assertEquals(false, resultParams.containsKey("remotePath"));
assertEquals(false, resultParams.containsKey("model"));
}
@Test
@SuppressWarnings({"unchecked", "rawtypes"})
void creditParseClient_shouldRetryEmptyPayloadFiveTimesWithTwoSecondInterval() throws Exception {
HttpUtil httpUtil = mock(HttpUtil.class);
TestableCreditParseClient parseClient = new TestableCreditParseClient();
ObjectMapper objectMapper = new ObjectMapper();
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
ReflectionTestUtils.setField(parseClient, "runType", "1");
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
String emptyPayloadResponse = "{\"success\":true,\"code\":10000,\"data\":{\"status\":1,\"reasonCode\":200,\"mappingOutputFields\":{\"message\":\"\",\"status_code\":\"ERR_SHOULD_IGNORE\"}}}";
String payload = "{\"lx_header\":{\"query_cert_no\":\"330101199001010011\",\"query_cust_name\":\"张三\",\"report_time\":\"2026-03-24\"}}";
String resultResponse = resultSuccessResponse(objectMapper, payload, "ERR_SHOULD_IGNORE");
when(httpUtil.postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(initiateSuccessResponse());
when(httpUtil.postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(emptyPayloadResponse, emptyPayloadResponse, emptyPayloadResponse, emptyPayloadResponse, resultResponse);
CreditParseInvokeResponse actual = parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html");
assertEquals("330101199001010011", actual.getData().getMappingOutputFields()
.getPayload().getLxHeader().get("query_cert_no"));
verify(httpUtil, times(5)).postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
);
assertEquals(List.of(2000L, 2000L, 2000L, 2000L), parseClient.getSleepIntervals());
}
@Test
void creditParseClient_shouldRejectInvalidOuterCode() throws Exception {
HttpUtil httpUtil = mock(HttpUtil.class);
TestableCreditParseClient parseClient = new TestableCreditParseClient();
ObjectMapper objectMapper = new ObjectMapper();
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
ReflectionTestUtils.setField(parseClient, "runType", "1");
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
when(httpUtil.postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(initiateSuccessResponse());
when(httpUtil.postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn("{\"success\":true,\"code\":99999,\"data\":{\"mappingOutputFields\":{\"message\":\"\",\"status_code\":\"0\"}}}");
LsfxApiException exception = assertThrows(LsfxApiException.class,
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
assertTrue(exception.getMessage().contains("平台状态码异常"));
}
@Test
void creditParseClient_shouldRejectFailedResultStatus() throws Exception {
HttpUtil httpUtil = mock(HttpUtil.class);
TestableCreditParseClient parseClient = new TestableCreditParseClient();
ObjectMapper objectMapper = new ObjectMapper();
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
ReflectionTestUtils.setField(parseClient, "runType", "1");
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
when(httpUtil.postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(initiateSuccessResponse());
when(httpUtil.postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn("{\"success\":true,\"code\":10000,\"data\":{\"reasonMessage\":\"解析失败\",\"reasonCode\":500,\"status\":0,\"mappingOutputFields\":{\"message\":\"结果异常\"}}}");
LsfxApiException exception = assertThrows(LsfxApiException.class,
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
assertTrue(exception.getMessage().contains("解析失败"));
}
@Test
void creditParseClient_shouldRejectFailedInitiateStatus() throws Exception {
HttpUtil httpUtil = mock(HttpUtil.class);
TestableCreditParseClient parseClient = new TestableCreditParseClient();
ObjectMapper objectMapper = new ObjectMapper();
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
ReflectionTestUtils.setField(parseClient, "runType", "1");
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
when(httpUtil.postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn("{\"success\":true,\"code\":10000,\"data\":{\"mappingOutputFields\":{\"message\":\"文件写入失败\"},\"reasonMessage\":\"文件写入失败\",\"reasonCode\":500,\"status\":0}}");
LsfxApiException exception = assertThrows(LsfxApiException.class,
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
assertTrue(exception.getMessage().contains("文件写入失败"));
verify(httpUtil, times(0)).postUrlEncodedFormForString(
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
);
}
private static String initiateSuccessResponse() {
return "{\"success\":true,\"code\":10000,\"data\":{\"traceId\":\"TRACE-001\","
+ "\"mappingOutputFields\":{\"message\":\"文件写入成功,流水号为: CCDI_CREDIT_TEST\"},"
+ "\"reasonMessage\":\"Running successfully\",\"procCode\":\"999000\","
+ "\"reasonCode\":200,\"status\":1}}";
}
private static String resultSuccessResponse(ObjectMapper objectMapper, String payload, String statusCode) throws Exception {
return "{\"success\":true,\"code\":10000,\"data\":{\"status\":1,\"reasonCode\":200,"
+ "\"mappingOutputFields\":{\"message\":\"\",\"status_code\":\"" + statusCode + "\",\"payload\":"
+ objectMapper.writeValueAsString(payload) + "}}}";
}
private static class TestableCreditParseClient extends CreditParseClient {
private final List<Long> sleepIntervals = new ArrayList<>();
@Override
protected void sleepBeforeNextResultQuery(long intervalMillis) {
sleepIntervals.add(intervalMillis);
}
private List<Long> getSleepIntervals() {
return sleepIntervals;
}
}
}

View File

@@ -0,0 +1,59 @@
package com.ruoyi.lsfx.util;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HttpUtilTest {
@Mock
private RestTemplate restTemplate;
@TempDir
Path tempDir;
@Test
void uploadFile_shouldUseExplicitResourceFilename() throws Exception {
HttpUtil httpUtil = new HttpUtil();
ReflectionTestUtils.setField(httpUtil, "restTemplate", restTemplate);
Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
Files.writeString(tempFile, "content");
ArgumentCaptor<HttpEntity> captor = ArgumentCaptor.forClass(HttpEntity.class);
when(restTemplate.postForEntity(eq("http://lsfx/upload"), captor.capture(), eq(String.class)))
.thenReturn(ResponseEntity.ok("ok"));
Map<String, Object> params = new HashMap<>();
params.put("groupId", 200);
params.put("files", HttpUtil.namedFileResource(tempFile.toFile(), "银行流水A.xlsx"));
String result = httpUtil.uploadFile("http://lsfx/upload", params, null, String.class);
assertEquals("ok", result);
MultiValueMap<String, Object> body = (MultiValueMap<String, Object>) captor.getValue().getBody();
Object filePart = body.getFirst("files");
Resource resource = assertInstanceOf(Resource.class, filePart);
assertEquals("银行流水A.xlsx", resource.getFilename());
}
}

View File

@@ -0,0 +1 @@
mock-maker-subclass

View File

@@ -9,6 +9,8 @@ public final class CcdiProjectStatusConstants {
public static final String COMPLETED = "1";
public static final String ARCHIVED = "2";
public static final String TAGGING = "3";
public static final String TAG_FAILED = "4";
public static final String DELETED = "5";
private CcdiProjectStatusConstants() {
}

View File

@@ -6,6 +6,7 @@ import com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiBankStatementService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -17,7 +18,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -39,13 +39,16 @@ public class CcdiBankStatementController extends BaseController {
@Resource
private ICcdiBankStatementService bankStatementService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 分页查询流水明细
*/
@GetMapping("/list")
@Operation(summary = "分页查询流水明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public TableDataInfo list(CcdiBankStatementQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiBankStatementListVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiBankStatementListVO> result = bankStatementService.selectStatementPage(page, queryDTO);
@@ -57,8 +60,8 @@ public class CcdiBankStatementController extends BaseController {
*/
@GetMapping("/options")
@Operation(summary = "查询项目级筛选项")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getOptions(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiBankStatementFilterOptionsVO options = bankStatementService.getFilterOptions(projectId);
return AjaxResult.success(options);
}
@@ -68,9 +71,13 @@ public class CcdiBankStatementController extends BaseController {
*/
@GetMapping("/detail/{bankStatementId}")
@Operation(summary = "查询流水详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getDetail(@PathVariable Long bankStatementId) {
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(bankStatementId);
public AjaxResult getDetail(@PathVariable Long bankStatementId, String modelCode, String suspiciousType) {
projectAccessService.assertCanReadByBankStatementId(bankStatementId);
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(
bankStatementId,
modelCode,
suspiciousType
);
return AjaxResult.success(detail);
}
@@ -79,8 +86,8 @@ public class CcdiBankStatementController extends BaseController {
*/
@PostMapping("/export")
@Operation(summary = "导出流水明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:export')")
public void export(HttpServletResponse response, CcdiBankStatementQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiBankStatementExcel> list = bankStatementService.selectStatementListForExport(queryDTO);
ExcelUtil<CcdiBankStatementExcel> util = new ExcelUtil<>(CcdiBankStatementExcel.class);
util.exportExcel(response, list, "流水明细");

View File

@@ -1,6 +1,7 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiBankTagRebuildDTO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -9,6 +10,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -27,12 +29,17 @@ public class CcdiBankTagController extends BaseController {
@Resource
private ICcdiBankTagService bankTagService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 手动提交流水标签重算任务
*/
@Operation(summary = "手动重算项目流水标签")
@PostMapping("/rebuild")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult rebuild(@Validated @RequestBody CcdiBankTagRebuildDTO dto) {
projectAccessService.assertCanOperate(dto.getProjectId());
String operator = SecurityUtils.getUsername();
log.info("【流水标签】收到手动重算请求: projectId={}, modelCode={}, operator={}",
dto.getProjectId(), dto.getModelCode(), operator);

View File

@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceSaveDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiEvidenceVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiEvidenceService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -34,13 +35,17 @@ public class CcdiEvidenceController extends BaseController {
@Resource
private ICcdiEvidenceService evidenceService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 保存证据
*/
@PostMapping
@Operation(summary = "保存证据")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult saveEvidence(@Validated @RequestBody CcdiEvidenceSaveDTO dto) {
projectAccessService.assertCanOperate(dto.getProjectId());
CcdiEvidenceVO vo = evidenceService.saveEvidence(dto, SecurityUtils.getUsername());
return AjaxResult.success("证据入库成功", vo);
}
@@ -50,8 +55,8 @@ public class CcdiEvidenceController extends BaseController {
*/
@GetMapping("/list")
@Operation(summary = "查询项目证据列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult listEvidence(CcdiEvidenceQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiEvidenceVO> list = evidenceService.listEvidence(queryDTO);
return AjaxResult.success(list);
}
@@ -61,8 +66,8 @@ public class CcdiEvidenceController extends BaseController {
*/
@GetMapping("/{evidenceId}")
@Operation(summary = "查询证据详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getEvidence(@PathVariable Long evidenceId) {
projectAccessService.assertCanReadByEvidenceId(evidenceId);
CcdiEvidenceVO vo = evidenceService.getEvidence(evidenceId);
return AjaxResult.success(vo);
}

View File

@@ -6,6 +6,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiPullBankInfoSubmitDTO;
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiIdCardParseVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -13,10 +14,12 @@ import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.lsfx.constants.LsfxConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@@ -40,17 +43,22 @@ public class CcdiFileUploadController extends BaseController {
@Resource
private ICcdiFileUploadService fileUploadService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 批量上传文件(异步)
*/
@PostMapping("/batch")
@Operation(summary = "批量上传文件", description = "异步批量上传流水文件")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult batchUpload(@RequestParam Long projectId,
@RequestParam MultipartFile[] files) {
// 参数校验
if (projectId == null) {
return AjaxResult.error("项目ID不能为空");
}
projectAccessService.assertCanOperate(projectId);
if (files == null || files.length == 0) {
return AjaxResult.error("请选择要上传的文件");
}
@@ -58,7 +66,7 @@ public class CcdiFileUploadController extends BaseController {
return AjaxResult.error("单次最多上传100个文件");
}
// 校验文件大小和格式
// 校验文件数量、空文件和大小,文件名业务规则统一在 Service 层处理
for (MultipartFile file : files) {
if (file.isEmpty()) {
return AjaxResult.error("文件不能为空");
@@ -66,15 +74,6 @@ public class CcdiFileUploadController extends BaseController {
if (file.getSize() > 50 * 1024 * 1024) {
return AjaxResult.error("文件 " + file.getOriginalFilename() + " 超过50MB限制");
}
String fileName = file.getOriginalFilename();
if (fileName == null || fileName.trim().isEmpty()) {
return AjaxResult.error("文件名不能为空");
}
String lowerFileName = fileName.toLowerCase();
if (!lowerFileName.endsWith(".xlsx") && !lowerFileName.endsWith(".xls")
&& !lowerFileName.endsWith(".csv") && !lowerFileName.endsWith(".pdf")) {
return AjaxResult.error("文件 " + fileName + " 格式不支持, 仅支持 PDF, CSV, Excel 文件");
}
}
try {
@@ -95,6 +94,7 @@ public class CcdiFileUploadController extends BaseController {
*/
@PostMapping("/parse-id-card-file")
@Operation(summary = "解析身份证文件", description = "解析首个sheet第一列的身份证号")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult parseIdCardFile(@RequestParam MultipartFile file) {
if (file == null || file.isEmpty()) {
return AjaxResult.error("身份证文件不能为空");
@@ -104,18 +104,29 @@ public class CcdiFileUploadController extends BaseController {
}
/**
* 提交拉取本行信息任务
* 提交拉取行内/金综流水任务
*/
@PostMapping("/pull-bank-info")
@Operation(summary = "拉取本行信息", description = "身份证号批量提交拉取本行信息任务")
@Operation(summary = "拉取行内/金综流水", description = "证件号码批量提交拉取行内/金综流水任务")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult pullBankInfo(@RequestBody CcdiPullBankInfoSubmitDTO dto) {
if (dto == null || dto.getProjectId() == null) {
return AjaxResult.error("项目ID不能为空");
}
projectAccessService.assertCanOperate(dto.getProjectId());
if (CollectionUtils.isEmpty(dto.getIdCards())) {
return AjaxResult.error("身份证号不能为空");
}
if (!StringUtils.hasText(dto.getStartDate()) || !StringUtils.hasText(dto.getEndDate())) {
if (!StringUtils.hasText(dto.getDataChannelCode())) {
return AjaxResult.error("流水来源不能为空");
}
String dataChannelCode = dto.getDataChannelCode().trim().toUpperCase();
if (!LsfxConstants.DATA_CHANNEL_ZJRCU.equals(dataChannelCode)
&& !LsfxConstants.DATA_CHANNEL_JZL.equals(dataChannelCode)) {
return AjaxResult.error("流水来源不支持");
}
if (LsfxConstants.DATA_CHANNEL_ZJRCU.equals(dataChannelCode)
&& (!StringUtils.hasText(dto.getStartDate()) || !StringUtils.hasText(dto.getEndDate()))) {
return AjaxResult.error("开始日期和结束日期不能为空");
}
@@ -124,6 +135,7 @@ public class CcdiFileUploadController extends BaseController {
String batchId = fileUploadService.submitPullBankInfo(
dto.getProjectId(),
dto.getIdCards(),
dataChannelCode,
dto.getStartDate(),
dto.getEndDate(),
userId,
@@ -138,6 +150,7 @@ public class CcdiFileUploadController extends BaseController {
@GetMapping("/list")
@Operation(summary = "查询上传记录列表", description = "分页查询文件上传记录")
public TableDataInfo list(CcdiFileUploadQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiFileUploadRecord> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiFileUploadRecord> result = fileUploadService.selectPage(page, queryDTO);
@@ -150,6 +163,7 @@ public class CcdiFileUploadController extends BaseController {
@GetMapping("/statistics/{projectId}")
@Operation(summary = "查询上传统计", description = "统计各状态的文件数量")
public AjaxResult getStatistics(@PathVariable Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiFileUploadStatisticsVO statistics = fileUploadService.countByStatus(projectId);
return AjaxResult.success(statistics);
}
@@ -160,6 +174,7 @@ public class CcdiFileUploadController extends BaseController {
@GetMapping("/detail/{id}")
@Operation(summary = "查询记录详情", description = "根据ID查询文件上传记录详情")
public AjaxResult getDetail(@PathVariable Long id) {
projectAccessService.assertCanReadByFileRecordId(id);
CcdiFileUploadRecord record = fileUploadService.getById(id);
return AjaxResult.success(record);
}
@@ -169,7 +184,9 @@ public class CcdiFileUploadController extends BaseController {
*/
@DeleteMapping("/{id}")
@Operation(summary = "删除上传文件", description = "按上传记录ID删除文件并清理流水")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult deleteFile(@PathVariable Long id) {
projectAccessService.assertCanOperateByFileRecordId(id);
Long userId = SecurityUtils.getUserId();
String message = fileUploadService.deleteFileUploadRecord(id, userId);
return AjaxResult.success(message);

View File

@@ -0,0 +1,86 @@
package com.ruoyi.ccdi.project.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.dto.CcdiFundGraphEdgeDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiFundGraphManualEdgeSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiFundGraphQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphEdgeVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphNodeVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphStatementVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiFundGraphService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.SecurityUtils;
import java.util.List;
/**
* 资金流图谱Controller
*/
@RestController
@RequestMapping("/ccdi/project/fund-graph")
@Tag(name = "资金流图谱")
public class CcdiFundGraphController extends BaseController {
@Resource
private ICcdiFundGraphService fundGraphService;
@Resource
private CcdiProjectAccessService projectAccessService;
@GetMapping("/search")
@Operation(summary = "查询资金流图谱主体")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult searchSubjects(CcdiFundGraphQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiFundGraphNodeVO> subjects = fundGraphService.searchSubjects(queryDTO);
return AjaxResult.success(subjects);
}
@GetMapping("/graph")
@Operation(summary = "查询一层资金流图谱")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getGraph(CcdiFundGraphQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiFundGraphVO graph = fundGraphService.getFundGraph(queryDTO);
return AjaxResult.success(graph);
}
@GetMapping("/edge-detail")
@Operation(summary = "查询资金边流水明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public TableDataInfo getEdgeDetail(CcdiFundGraphEdgeDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiFundGraphStatementVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiFundGraphStatementVO> result = fundGraphService.getEdgeDetails(page, queryDTO);
return getDataTable(result.getRecords(), result.getTotal());
}
@PostMapping("/manual-edge")
@Operation(summary = "新增手工资金流向")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult saveManualEdge(@RequestBody CcdiFundGraphManualEdgeSaveDTO saveDTO) {
try {
projectAccessService.assertCanOperate(saveDTO == null ? null : saveDTO.getProjectId());
CcdiFundGraphEdgeVO edge = fundGraphService.saveManualEdge(saveDTO, SecurityUtils.getUsername());
return AjaxResult.success(edge);
} catch (IllegalArgumentException e) {
return AjaxResult.error(e.getMessage());
}
}
}

View File

@@ -11,9 +11,11 @@ import com.ruoyi.ccdi.project.domain.dto.ModelParamSaveAllDTO;
import com.ruoyi.ccdi.project.domain.vo.ModelListVO;
import com.ruoyi.ccdi.project.domain.vo.ModelParamVO;
import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiModelParamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -31,12 +33,17 @@ public class CcdiModelParamController extends BaseController {
@Resource
private ICcdiModelParamService modelParamService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 查询模型列表
*/
@Operation(summary = "查询模型列表")
@GetMapping("/modelList")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult listModels(@RequestParam(required = false) Long projectId) {
assertCanReadProjectParam(projectId);
List<ModelListVO> list = modelParamService.selectModelList(projectId);
return success(list);
}
@@ -46,7 +53,9 @@ public class CcdiModelParamController extends BaseController {
*/
@Operation(summary = "查询模型参数列表")
@GetMapping("/list")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult list(@Validated ModelParamQueryDTO queryDTO) {
assertCanReadProjectParam(queryDTO.getProjectId());
List<ModelParamVO> list = modelParamService.selectParamList(queryDTO);
return success(list);
}
@@ -57,7 +66,9 @@ public class CcdiModelParamController extends BaseController {
@Operation(summary = "保存模型参数")
@Log(title = "模型参数配置", businessType = BusinessType.UPDATE)
@PostMapping("/save")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult save(@Validated @RequestBody ModelParamSaveDTO saveDTO) {
assertCanOperateProjectParam(saveDTO.getProjectId());
modelParamService.saveParams(saveDTO);
return success("保存成功");
}
@@ -67,7 +78,9 @@ public class CcdiModelParamController extends BaseController {
*/
@Operation(summary = "查询所有模型及其参数")
@GetMapping("/listAll")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult listAll(@Validated ModelParamAllQueryDTO queryDTO) {
assertCanReadProjectParam(queryDTO.getProjectId());
ModelParamAllVO result = modelParamService.selectAllParams(queryDTO.getProjectId());
return success(result);
}
@@ -78,8 +91,24 @@ public class CcdiModelParamController extends BaseController {
@Operation(summary = "批量保存所有模型参数")
@Log(title = "模型参数配置", businessType = BusinessType.UPDATE)
@PostMapping("/saveAll")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult saveAll(@Validated @RequestBody ModelParamSaveAllDTO saveAllDTO) {
assertCanOperateProjectParam(saveAllDTO.getProjectId());
modelParamService.saveAllParams(saveAllDTO);
return success("保存成功");
}
private void assertCanReadProjectParam(Long projectId) {
if (projectId == null || projectId <= 0) {
return;
}
projectAccessService.assertCanRead(projectId);
}
private void assertCanOperateProjectParam(Long projectId) {
if (projectId == null || projectId <= 0) {
return;
}
projectAccessService.assertCanOperate(projectId);
}
}

View File

@@ -74,18 +74,28 @@ public class CcdiProjectController extends BaseController {
*/
@DeleteMapping("/{projectId}")
@Operation(summary = "删除项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:remove')")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult deleteProject(@PathVariable Long projectId) {
boolean success = projectService.deleteProject(projectId);
boolean success = projectService.deleteProject(projectId, SecurityUtils.getUsername());
return success ? AjaxResult.success("项目删除成功") : AjaxResult.error("项目删除失败");
}
/**
* 恢复项目
*/
@PostMapping("/{projectId}/restore")
@Operation(summary = "恢复项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult restoreProject(@PathVariable Long projectId) {
projectService.restoreProject(projectId, SecurityUtils.getUsername());
return AjaxResult.success("项目恢复成功");
}
/**
* 查询项目详情
*/
@GetMapping("/{projectId}")
@Operation(summary = "查询项目详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getProject(@PathVariable Long projectId) {
CcdiProjectVO vo = projectService.getProjectById(projectId);
return AjaxResult.success(vo);
@@ -96,7 +106,6 @@ public class CcdiProjectController extends BaseController {
*/
@GetMapping("/list")
@Operation(summary = "查询项目列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public TableDataInfo listProject(CcdiProjectQueryDTO queryDTO) {
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiProjectVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
@@ -109,7 +118,6 @@ public class CcdiProjectController extends BaseController {
*/
@GetMapping("/history")
@Operation(summary = "查询历史项目列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult listHistoryProjects(CcdiProjectQueryDTO queryDTO) {
List<CcdiProjectHistoryListItemVO> result = projectService.listHistoryProjects(queryDTO);
return AjaxResult.success(result);
@@ -131,7 +139,6 @@ public class CcdiProjectController extends BaseController {
*/
@GetMapping("/statusCounts")
@Operation(summary = "查询项目状态统计")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult getStatusCounts() {
CcdiProjectStatusCountsVO counts = projectService.getStatusCounts();
return AjaxResult.success(counts);

View File

@@ -2,14 +2,21 @@ package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalPersonQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
@@ -17,6 +24,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.controller.BaseController;
@@ -26,8 +34,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@@ -45,6 +55,9 @@ public class CcdiProjectOverviewController extends BaseController {
@Resource
private ICcdiProjectOverviewService overviewService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 查询风险仪表盘
*/
@@ -52,10 +65,23 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询风险仪表盘")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getDashboard(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectOverviewDashboardVO dashboard = overviewService.getDashboard(projectId);
return AjaxResult.success(dashboard);
}
/**
* 排除单条可疑预警
*/
@PostMapping("/risk-exclusions")
@Operation(summary = "排除单条可疑预警")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult excludeRisk(@Validated @RequestBody CcdiProjectRiskExclusionSaveDTO dto) {
projectAccessService.assertCanOperate(dto.getProjectId());
overviewService.excludeRisk(dto);
return AjaxResult.success("排除成功");
}
/**
* 查询风险人员总览
*/
@@ -63,10 +89,35 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询风险人员总览")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getRiskPeople(CcdiProjectRiskPeopleQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectRiskPeopleOverviewVO overview = overviewService.getRiskPeopleOverview(queryDTO);
return AjaxResult.success(overview);
}
/**
* 查询外部人员预警
*/
@GetMapping("/external-persons")
@Operation(summary = "查询外部人员预警")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExternalPersons(CcdiProjectExternalPersonQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExternalPersonWarningVO warnings = overviewService.getExternalPersonWarnings(queryDTO);
return AjaxResult.success(warnings);
}
/**
* 查询外部人员风险汇总
*/
@GetMapping("/external-persons/summary")
@Operation(summary = "查询外部人员风险汇总")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExternalRiskSummary(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectExternalRiskSummaryVO summary = overviewService.getExternalRiskSummary(projectId);
return AjaxResult.success(summary);
}
/**
* 查询中高风险人员TOP10
*/
@@ -74,6 +125,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询中高风险人员TOP10")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getTopRiskPeople(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectTopRiskPeopleVO topRiskPeople = overviewService.getTopRiskPeople(projectId);
return AjaxResult.success(topRiskPeople);
}
@@ -85,6 +137,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询风险模型卡片")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getRiskModelCards(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectRiskModelCardsVO cards = overviewService.getRiskModelCards(projectId);
return AjaxResult.success(cards);
}
@@ -96,10 +149,35 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询风险模型命中人员")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getRiskModelPeople(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectRiskModelPeopleVO people = overviewService.getRiskModelPeople(queryDTO);
return AjaxResult.success(people);
}
/**
* 查询外部人员风险模型卡片
*/
@GetMapping("/external-risk-models/cards")
@Operation(summary = "查询外部人员风险模型卡片")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExternalRiskModelCards(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectRiskModelCardsVO cards = overviewService.getExternalRiskModelCards(projectId);
return AjaxResult.success(cards);
}
/**
* 查询外部人员风险模型命中人员
*/
@GetMapping("/external-risk-models/people")
@Operation(summary = "查询外部人员风险模型命中人员")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExternalRiskModelPeople(CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectRiskModelPeopleVO people = overviewService.getExternalRiskModelPeople(queryDTO);
return AjaxResult.success(people);
}
/**
* 查询项目分析详情
*/
@@ -107,6 +185,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询项目分析详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getPersonAnalysisDetail(CcdiProjectPersonAnalysisDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectPersonAnalysisDetailVO detail = overviewService.getPersonAnalysisDetail(queryDTO);
return AjaxResult.success(detail);
}
@@ -118,6 +197,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询涉疑交易明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getSuspiciousTransactions(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectSuspiciousTransactionPageVO pageVO = overviewService.getSuspiciousTransactions(queryDTO);
return AjaxResult.success(pageVO);
}
@@ -129,6 +209,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询项目员工负面征信")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getEmployeeCreditNegative(CcdiProjectEmployeeCreditNegativeQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectEmployeeCreditNegativePageVO pageVO = overviewService.getEmployeeCreditNegative(queryDTO);
return AjaxResult.success(pageVO);
}
@@ -140,6 +221,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询异常账户人员信息")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getAbnormalAccountPeople(CcdiProjectAbnormalAccountQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectAbnormalAccountPageVO pageVO = overviewService.getAbnormalAccountPeople(queryDTO);
return AjaxResult.success(pageVO);
}
@@ -154,6 +236,7 @@ public class CcdiProjectOverviewController extends BaseController {
HttpServletResponse response,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectSuspiciousTransactionExcel> rows = overviewService.exportSuspiciousTransactions(queryDTO);
ExcelUtil<CcdiProjectSuspiciousTransactionExcel> util =
new ExcelUtil<>(CcdiProjectSuspiciousTransactionExcel.class);
@@ -167,29 +250,80 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "导出风险人员总览")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskPeople(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
List<CcdiProjectRiskPeopleOverviewExcel> rows = overviewService.exportRiskPeopleOverview(projectId);
ExcelUtil<CcdiProjectRiskPeopleOverviewExcel> util =
new ExcelUtil<>(CcdiProjectRiskPeopleOverviewExcel.class);
util.exportExcel(response, rows, "风险人员总览");
}
/**
* 导出外部人员预警
*/
@PostMapping("/external-persons/export")
@Operation(summary = "导出外部人员预警")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportExternalPersons(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
List<CcdiProjectExternalPersonWarningExcel> rows = overviewService.exportExternalPersonWarnings(projectId);
ExcelUtil<CcdiProjectExternalPersonWarningExcel> util =
new ExcelUtil<>(CcdiProjectExternalPersonWarningExcel.class);
util.exportExcel(response, rows, "外部人员预警");
}
/**
* 导出风险模型命中人员
*/
@PostMapping("/risk-models/people/export")
@Operation(summary = "导出风险模型命中人员")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskModelPeople(HttpServletResponse response, CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectRiskModelPeopleExcel> rows = overviewService.exportRiskModelPeople(queryDTO);
ExcelUtil<CcdiProjectRiskModelPeopleExcel> util =
new ExcelUtil<>(CcdiProjectRiskModelPeopleExcel.class);
util.exportExcel(response, rows, "风险模型命中人员");
}
/**
* 导出外部人员风险模型命中人员
*/
@PostMapping("/external-risk-models/people/export")
@Operation(summary = "导出外部人员风险模型命中人员")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportExternalRiskModelPeople(
HttpServletResponse response,
CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO
) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectRiskModelPeopleExcel> rows = overviewService.exportExternalRiskModelPeople(queryDTO);
ExcelUtil<CcdiProjectRiskModelPeopleExcel> util =
new ExcelUtil<>(CcdiProjectRiskModelPeopleExcel.class);
util.exportExcel(response, rows, "外部人员风险模型命中人员");
}
/**
* 导出风险明细
*/
@PostMapping("/risk-details/export")
@Operation(summary = "导出风险明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
overviewService.exportRiskDetails(response, projectId);
public void exportRiskDetails(
HttpServletResponse response,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
overviewService.exportRiskDetails(response, queryDTO);
}
/**
* 一键导出结果总览报告
* 导出结果总览报告
*/
@RequestMapping(value = "/report/export", method = { RequestMethod.GET, RequestMethod.POST })
@Operation(summary = "一键导出结果总览报告")
@Operation(summary = "导出结果总览报告")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportOverviewReport(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
overviewService.exportOverviewReport(response, projectId);
}
}

View File

@@ -8,6 +8,8 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryD
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectIncreaseLendingQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
@@ -16,15 +18,21 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.poi.ExcelUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -39,6 +47,9 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Resource
private ICcdiProjectSpecialCheckService specialCheckService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 查询员工家庭资产负债列表
*/
@@ -46,6 +57,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询员工家庭资产负债列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getFamilyAssetLiabilityList(@Validated CcdiProjectFamilyAssetLiabilityListQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectFamilyAssetLiabilityListVO result = specialCheckService.getFamilyAssetLiabilityList(queryDTO);
return AjaxResult.success(result);
}
@@ -57,6 +69,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询员工家庭资产负债详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getFamilyAssetLiabilityDetail(@Validated CcdiProjectFamilyAssetLiabilityDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectFamilyAssetLiabilityDetailVO result = specialCheckService.getFamilyAssetLiabilityDetail(queryDTO);
return AjaxResult.success(result);
}
@@ -68,6 +81,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询采购拓展列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedPurchaseList(@Validated CcdiProjectExtendedPurchaseQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedPurchaseListVO result = specialCheckService.getExtendedPurchaseList(queryDTO);
return AjaxResult.success(result);
}
@@ -79,6 +93,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询采购拓展详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedPurchaseDetail(@Validated CcdiProjectExtendedPurchaseDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedPurchaseDetailVO result = specialCheckService.getExtendedPurchaseDetail(queryDTO);
return AjaxResult.success(result);
}
@@ -90,6 +105,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询招聘拓展列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedRecruitmentList(@Validated CcdiProjectExtendedRecruitmentQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedRecruitmentListVO result = specialCheckService.getExtendedRecruitmentList(queryDTO);
return AjaxResult.success(result);
}
@@ -101,6 +117,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询招聘拓展详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedRecruitmentDetail(@Validated CcdiProjectExtendedRecruitmentDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedRecruitmentDetailVO result = specialCheckService.getExtendedRecruitmentDetail(queryDTO);
return AjaxResult.success(result);
}
@@ -112,6 +129,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询调动拓展列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedTransferList(@Validated CcdiProjectExtendedTransferQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedTransferListVO result = specialCheckService.getExtendedTransferList(queryDTO);
return AjaxResult.success(result);
}
@@ -123,7 +141,33 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询调动拓展详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedTransferDetail(@Validated CcdiProjectExtendedTransferDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedTransferDetailVO result = specialCheckService.getExtendedTransferDetail(queryDTO);
return AjaxResult.success(result);
}
/**
* 查询新增贷款列表
*/
@GetMapping("/increase-lending/list")
@Operation(summary = "查询新增贷款列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getIncreaseLendingList(@Validated CcdiProjectIncreaseLendingQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectIncreaseLendingListVO result = specialCheckService.getIncreaseLendingList(queryDTO);
return AjaxResult.success(result);
}
/**
* 导出新增贷款列表
*/
@PostMapping("/increase-lending/export")
@Operation(summary = "导出新增贷款列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportIncreaseLending(HttpServletResponse response, @Validated CcdiProjectIncreaseLendingQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectIncreaseLendingExcel> rows = specialCheckService.exportIncreaseLendingList(queryDTO);
ExcelUtil<CcdiProjectIncreaseLendingExcel> util = new ExcelUtil<>(CcdiProjectIncreaseLendingExcel.class);
util.exportExcel(response, rows, "新增贷款查询");
}
}

View File

@@ -0,0 +1,62 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiRelationGraphQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiRelationGraphSuspectedEnterpriseQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphNodeVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphSuspectedEnterpriseVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiRelationGraphService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 关系图谱Controller
*/
@RestController
@RequestMapping("/ccdi/project/relation-graph")
@Tag(name = "关系图谱")
public class CcdiRelationGraphController extends BaseController {
@Resource
private ICcdiRelationGraphService relationGraphService;
@Resource
private CcdiProjectAccessService projectAccessService;
@GetMapping("/search")
@Operation(summary = "查询关系图谱主体")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult searchSubjects(CcdiRelationGraphQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiRelationGraphNodeVO> subjects = relationGraphService.searchSubjects(queryDTO);
return AjaxResult.success(subjects);
}
@GetMapping("/graph")
@Operation(summary = "查询一层关系图谱")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getGraph(CcdiRelationGraphQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiRelationGraphVO graph = relationGraphService.getRelationGraph(queryDTO);
return AjaxResult.success(graph);
}
@GetMapping("/suspected-enterprises")
@Operation(summary = "查询关系图谱疑似同名企业")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getSuspectedEnterprises(CcdiRelationGraphSuspectedEnterpriseQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiRelationGraphSuspectedEnterpriseVO result = relationGraphService.getSuspectedEnterprises(queryDTO);
return AjaxResult.success(result);
}
}

View File

@@ -32,7 +32,7 @@ public class CcdiProject implements Serializable {
/** 配置方式default-全局默认custom-自定义 */
private String configType;
/** 项目状态0-进行中1-已完成2-已归档3-打标中 */
/** 项目状态0-进行中1-已完成2-已归档3-打标中4-打标失败5-已删除 */
private String status;
/** 是否归档0-未归档1-已归档 */
@@ -54,7 +54,7 @@ public class CcdiProject implements Serializable {
private Integer lsfxProjectId;
/** 删除标志0-存在2-删除 */
@TableLogic
@TableLogic(value = "0", delval = "2")
private String delFlag;
/** 创建者 */

View File

@@ -0,0 +1,24 @@
package com.ruoyi.ccdi.project.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 当前登录用户的项目访问范围。
*/
@Data
@AllArgsConstructor
public class ProjectAccessScope {
/** 当前用户名 */
private String username;
/** 是否可查看全部项目 */
private boolean viewAllProjects;
/** 是否超级管理员 */
private boolean superAdmin;
/** 是否项目管理员 */
private boolean projectManager;
}

View File

@@ -40,6 +40,9 @@ public class CcdiBankStatementQueryDTO {
/** 本方主体 */
private List<String> ourSubjects;
/** 本方证件号 */
private List<String> ourCertNos;
/** 本方银行 */
private List<String> ourBanks;

View File

@@ -0,0 +1,42 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
* 资金流图谱边明细查询条件
*/
@Data
public class CcdiFundGraphEdgeDetailQueryDTO {
/** 项目ID历史字段资金流图谱不按项目过滤 */
private Long projectId;
/** 身份证号、员工姓名或本方户名 */
private String keyword;
/** 主体节点object_key复用图谱公共SQL片段时兼容条件判断 */
private String objectKey;
/** 边起点 */
private String fromKey;
/** 边终点 */
private String toKey;
/** 方向1支出2收入 */
private String direction;
/** 交易开始时间 */
private String transactionStartTime;
/** 交易结束时间 */
private String transactionEndTime;
/** 最小金额 */
private BigDecimal amountMin;
/** 最大金额 */
private BigDecimal amountMax;
}

View File

@@ -0,0 +1,48 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
* 手工资金流向保存参数。
*/
@Data
public class CcdiFundGraphManualEdgeSaveDTO {
/** 当前项目ID仅用于写权限校验不参与手工资金流归属过滤 */
private Long projectId;
/** 起点主体object_key为空时默认使用当前查询中心 */
private String fromObjectKey;
/** 起点主体名称 */
private String fromName;
/** 终点主体object_key已有节点时传入 */
private String toObjectKey;
/** 终点主体名称;新建主体时必填 */
private String toName;
/** 终点主体身份证号/证件号有值时按md5(trim(idNo))复用主体 */
private String toIdNo;
/** 手工录入汇总金额 */
private BigDecimal amount;
/** 手工录入笔数 */
private Integer transactionCount;
/** 方向1支出2收入 */
private String direction;
/** 资金流向关系说明 */
private String relationDesc;
/** 来源说明 */
private String sourceDesc;
/** 分析备注 */
private String remark;
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
* 资金流图谱查询条件
*/
@Data
public class CcdiFundGraphQueryDTO {
/** 项目ID历史字段资金流图谱不按项目过滤 */
private Long projectId;
/** 身份证号、员工姓名或本方户名 */
private String keyword;
/** 主体节点object_key节点穿透时直接使用 */
private String objectKey;
/** 交易开始时间 */
private String transactionStartTime;
/** 交易结束时间 */
private String transactionEndTime;
/** 最小金额 */
private BigDecimal amountMin;
/** 最大金额 */
private BigDecimal amountMax;
/** 最小汇总金额默认1000 */
private BigDecimal minTotalAmount;
/** 方向1支出2收入 */
private String direction;
/** 返回边数量上限 */
private Integer limit;
/** 预留追溯层级,一期固定按一层处理 */
private Integer depth;
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 外部人员预警查询DTO
*/
@Data
public class CcdiProjectExternalPersonQueryDTO {
/** 项目ID */
private Long projectId;
/** 页码 */
private Integer pageNum;
/** 每页数量 */
private Integer pageSize;
}

View File

@@ -0,0 +1,41 @@
package com.ruoyi.ccdi.project.domain.dto;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Data;
/**
* 外部人员模型命中人员查询DTO
*/
@Data
public class CcdiProjectExternalRiskModelPeopleQueryDTO {
/** 项目ID */
private Long projectId;
/** 模型编码 */
private List<String> modelCodes;
/** 匹配方式 */
private String matchMode;
/** 关键字 */
private String keyword;
/** 页码 */
private Integer pageNum;
/** 每页数量 */
private Integer pageSize;
public String getModelCodesCsv() {
if (modelCodes == null || modelCodes.isEmpty()) {
return null;
}
return modelCodes.stream()
.filter(item -> item != null && !item.isBlank())
.map(String::trim)
.distinct()
.collect(Collectors.joining(","));
}
}

View File

@@ -0,0 +1,36 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 项目新增贷款查询入参
*/
@Data
public class CcdiProjectIncreaseLendingQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 柜员号 */
private String staffId;
/** 员工身份证号 */
private String staffIdCard;
/** 审核人柜员号 */
private String approver;
/** 发放日期起 */
private String loanStartDate;
/** 发放日期止 */
private String loanEndDate;
/** 页码 */
private Integer pageNum = 1;
/** 每页条数 */
private Integer pageSize = 10;
}

View File

@@ -14,4 +14,7 @@ public class CcdiProjectQueryDTO {
/** 项目状态 */
private String status;
/** 是否查询已删除项目列表 */
private Boolean includeDeleted;
}

View File

@@ -0,0 +1,36 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 项目结果页排除可疑保存DTO
*/
@Data
public class CcdiProjectRiskExclusionSaveDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 人员证件号 */
private String staffIdCard;
/** 规则编码 */
@NotBlank(message = "规则编码不能为空")
private String ruleCode;
/** 排除类型STATEMENT/OBJECT */
@NotBlank(message = "排除类型不能为空")
private String exclusionType;
/** 流水ID */
private Long bankStatementId;
/** 排除原因 */
@NotBlank(message = "排除原因不能为空")
@Size(max = 1000, message = "排除原因不能超过1000个字符")
private String excludeReason;
}

View File

@@ -11,6 +11,9 @@ public class CcdiProjectSuspiciousTransactionQueryDTO {
/** 项目ID */
private Long projectId;
/** 模型编码 */
private String modelCode;
/** 涉疑类型 */
private String suspiciousType;
@@ -19,4 +22,7 @@ public class CcdiProjectSuspiciousTransactionQueryDTO {
/** 每页数量 */
private Integer pageSize;
/** 是否包含外部人员预警分支 */
private Boolean includeExternalPerson;
}

View File

@@ -5,7 +5,7 @@ import lombok.Data;
import java.util.List;
/**
* 拉取本行信息提交参数
* 拉取行内/金综流水提交参数
*/
@Data
public class CcdiPullBankInfoSubmitDTO {
@@ -16,6 +16,9 @@ public class CcdiPullBankInfoSubmitDTO {
/** 身份证号列表 */
private List<String> idCards;
/** 数据渠道编码ZJRCU-行内JZL-金综 */
private String dataChannelCode;
/** 开始日期 */
private String startDate;

View File

@@ -0,0 +1,25 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 关系图谱查询条件
*/
@Data
public class CcdiRelationGraphQueryDTO {
/** 项目ID历史字段关系图谱不按项目过滤 */
private Long projectId;
/** 身份证号、姓名、统一社会信用代码或节点object_key */
private String keyword;
/** 节点object_key节点穿透时直接使用 */
private String objectKey;
/** 返回边数量上限 */
private Integer limit;
/** 预留追溯层级,一期固定按一层处理 */
private Integer depth;
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 关系图谱疑似企业查询条件
*/
@Data
public class CcdiRelationGraphSuspectedEnterpriseQueryDTO {
/** 项目ID */
private Long projectId;
/** 姓名 */
private String personName;
/** 证件号 */
private String certNo;
/** 出生日期yyyy-MM-dd */
private String birthDate;
/** 返回数量上限 */
private Integer limit;
}

View File

@@ -194,8 +194,8 @@ public class CcdiBankStatement implements Serializable {
BeanUtils.copyProperties(item, entity);
// 4. 手动映射字段名不一致的情况
entity.setLeAccountNo(item.getAccountMaskNo());
entity.setCustomerAccountNo(item.getCustomerAccountMaskNo());
entity.setLeAccountNo(item.getAccountNo());
entity.setCustomerAccountNo(item.getCustomerAccountNo());
entity.setLeAccountName(item.getLeName());
entity.setAmountDr(item.getDrAmount());
entity.setAmountCr(item.getCrAmount());
@@ -207,6 +207,7 @@ public class CcdiBankStatement implements Serializable {
entity.setBatchSequence(item.getUploadSequnceNumber());
entity.setCustomerCertNo(item.getCustomerCertNo());
entity.setCustomerSocialCreditCode(item.getCustomerSocialCreditCode());
entity.setCretNo(normalizeCertNo(item.getCretNo()));
// 5. 特殊字段处理
entity.setMetaJson(null); // 根据文档要求强制设为 null
@@ -219,4 +220,19 @@ public class CcdiBankStatement implements Serializable {
throw new RuntimeException("流水数据转换失败", e);
}
}
private static String normalizeCertNo(String value) {
if (value == null || value.isBlank()) {
return null;
}
String normalized = value.trim()
.replace('', '-')
.replace('—', '-')
.replace('', '-');
int separatorIndex = normalized.indexOf('-');
if (separatorIndex >= 0) {
normalized = normalized.substring(0, separatorIndex).trim();
}
return normalized.isEmpty() ? null : normalized;
}
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.ccdi.project.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 项目结果页排除可疑记录
*/
@Data
@TableName("ccdi_project_risk_exclusion")
public class CcdiProjectRiskExclusion implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 项目ID */
private Long projectId;
/** 人员证件号 */
private String staffIdCard;
/** 规则编码 */
private String ruleCode;
/** 排除类型STATEMENT/OBJECT */
private String exclusionType;
/** 流水ID */
private Long bankStatementId;
/** 排除原因 */
private String excludeReason;
/** 创建者 */
private String createBy;
/** 创建时间 */
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
private Date updateTime;
/** 备注 */
private String remark;
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import java.math.BigDecimal;
import lombok.Data;
/**
@@ -26,4 +27,10 @@ public class CcdiProjectAbnormalAccountExcel {
@Excel(name = "状态")
private String status;
@Excel(name = "命中原因")
private String reasonDetail;
@Excel(name = "涉及金额")
private BigDecimal involvedAmount;
}

View File

@@ -0,0 +1,35 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
/**
* 外部人员预警导出对象
*/
@Data
public class CcdiProjectExternalPersonWarningExcel {
@Excel(name = "姓名")
private String name;
@Excel(name = "证件号")
private String idNo;
@Excel(name = "主体类型")
private String subjectType;
@Excel(name = "风险等级")
private String riskLevel;
@Excel(name = "命中模型数")
private Integer modelCount;
@Excel(name = "核心异常点")
private String riskPoint;
@Excel(name = "涉及对象")
private String relatedObject;
@Excel(name = "最近交易时间")
private String latestTradeTime;
}

View File

@@ -0,0 +1,84 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import java.math.BigDecimal;
import lombok.Data;
/**
* 新增贷款查询导出对象
*/
@Data
public class CcdiProjectIncreaseLendingExcel {
/** 柜员号 */
@Excel(name = "柜员号")
private String staffId;
/** 员工姓名 */
@Excel(name = "员工姓名")
private String staffName;
/** 员工身份证 */
@Excel(name = "员工身份证")
private String staffIdCard;
/** 部门 */
@Excel(name = "部门")
private String deptName;
/** 合同编号 */
@Excel(name = "合同编号")
private String contractNo;
/** 放款机构号 */
@Excel(name = "放款机构号")
private String lendingOrgNo;
/** 借款人 */
@Excel(name = "借款人")
private String borrowerName;
/** 借款人证件号 */
@Excel(name = "借款人证件号")
private String borrowerCertNo;
/** 贷款产品 */
@Excel(name = "贷款产品")
private String loanProduct;
/** 合同金额 */
@Excel(name = "合同金额")
private BigDecimal contractAmount;
/** 当前余额 */
@Excel(name = "当前余额")
private BigDecimal loanBalance;
/** 发放日期 */
@Excel(name = "发放日期")
private String loanStartDate;
/** 到期日期 */
@Excel(name = "到期日期")
private String loanEndDate;
/** 合同状态 */
@Excel(name = "合同状态")
private String status;
/** 五级分类 */
@Excel(name = "五级分类")
private String fiveClassification;
/** 客户经理ID */
@Excel(name = "客户经理ID")
private String customerManagerId;
/** 客户经理 */
@Excel(name = "客户经理")
private String customerManagerName;
/** 审批人 */
@Excel(name = "审批人")
private String approver;
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
/**
* 风险模型命中人员导出对象
*/
@Data
public class CcdiProjectRiskModelPeopleExcel {
@Excel(name = "风险主体")
private String personName;
@Excel(name = "主体类型")
private String subjectType;
@Excel(name = "证件号")
private String idNo;
@Excel(name = "部门/涉及对象")
private String scopeName;
@Excel(name = "命中模型")
private String modelNames;
@Excel(name = "异常标签")
private String hitTags;
}

View File

@@ -14,20 +14,29 @@ public class CcdiProjectSuspiciousTransactionExcel {
@Excel(name = "交易时间")
private String trxDate;
@Excel(name = "可疑人员")
private String suspiciousPersonName;
@Excel(name = "本方账户")
private String leAccountNo;
@Excel(name = "关联人")
private String relatedPersonName;
@Excel(name = "本方主体")
private String leAccountName;
@Excel(name = "对方名称")
private String customerAccountName;
@Excel(name = "对方账户")
private String customerAccountNo;
@Excel(name = "关联员工")
private String relatedStaffDisplay;
@Excel(name = "关系")
private String relationType;
@Excel(name = "摘要")
private String userMemo;
@Excel(name = "摘要/交易类型")
private String summaryAndCashType;
@Excel(name = "交易类型")
private String cashType;
@Excel(name = "异常标签")
private String hitTags;
@Excel(name = "交易金额")
private BigDecimal displayAmount;

View File

@@ -8,6 +8,9 @@ import lombok.Data;
@Data
public class CcdiBankStatementHitTagVO {
/** 模型编码 */
private String modelCode;
/** 规则编码 */
private String ruleCode;

View File

@@ -0,0 +1,50 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 资金流图谱汇总边
*/
@Data
public class CcdiFundGraphEdgeVO {
private String edgeKey;
private String fromKey;
private String toKey;
private String fromObjectKey;
private String toObjectKey;
private String fromName;
private String toName;
private BigDecimal totalAmount;
private Long transactionCount;
private String firstTrxDate;
private String lastTrxDate;
private String direction;
private String familyRelationType;
private String sourceType;
private String relationDesc;
private String sourceDesc;
private String remark;
private Integer depth;
private Boolean canTrace;
}

View File

@@ -0,0 +1,48 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 资金流图谱节点
*/
@Data
public class CcdiFundGraphNodeVO {
private String nodeKey;
private String objectKey;
private String nodeName;
private String idNo;
private String cinocsno;
private String idnoType;
private String staffId;
private String sourceType;
private String nodeType;
private String identityType;
private String relationType;
private Long accountCount;
private String createdTime;
private String updatedTime;
private Boolean canExpand;
private Integer depth;
private BigDecimal totalAmount;
private Long transactionCount;
}

View File

@@ -0,0 +1,34 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 资金流图谱边对应流水明细
*/
@Data
public class CcdiFundGraphStatementVO {
private Long bankStatementId;
private String trxDate;
private String leAccountNo;
private String leAccountName;
private String customerAccountName;
private String customerAccountNo;
private String cashType;
private String userMemo;
private BigDecimal amount;
private String direction;
private String familyRelationType;
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 资金流图谱结果
*/
@Data
public class CcdiFundGraphVO {
private CcdiFundGraphNodeVO centerNode;
private List<CcdiFundGraphNodeVO> nodes = new ArrayList<>();
private List<CcdiFundGraphEdgeVO> edges = new ArrayList<>();
private BigDecimal totalAmount = BigDecimal.ZERO;
private Long transactionCount = 0L;
private Integer maxDepth = 1;
private Boolean traceReserved = true;
}

View File

@@ -1,5 +1,6 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
@@ -19,4 +20,8 @@ public class CcdiProjectAbnormalAccountItemVO {
private String abnormalTime;
private String status;
private String reasonDetail;
private BigDecimal involvedAmount;
}

View File

@@ -0,0 +1,35 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 外部人员预警项
*/
@Data
public class CcdiProjectExternalPersonWarningItemVO {
private String name;
private String idNo;
private String subjectType;
private String riskLevel;
private String riskLevelType;
private Integer riskCount;
private Integer modelCount;
private String riskPoint;
private String relatedObject;
private String latestTradeTime;
private List<CcdiProjectRiskHitTagVO> riskPointTagList;
private String actionLabel;
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 外部人员预警分页
*/
@Data
public class CcdiProjectExternalPersonWarningVO {
private List<CcdiProjectExternalPersonWarningItemVO> rows;
private Long total;
private Long pageNum;
private Long pageSize;
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 外部人员风险等级汇总
*/
@Data
public class CcdiProjectExternalRiskSummaryVO {
private Integer total;
private Integer high;
private Integer medium;
private Integer low;
private Integer noRisk;
}

View File

@@ -24,6 +24,15 @@ public class CcdiProjectFamilyAssetLiabilityListItemVO {
/** 家庭总年收入 */
private BigDecimal totalIncome;
/** 本人年收入 */
private BigDecimal selfIncome;
/** 配偶年收入 */
private BigDecimal spouseIncome;
/** 本人入职年限 */
private Integer employmentYears;
/** 家庭总资产 */
private BigDecimal totalAsset;
@@ -33,6 +42,12 @@ public class CcdiProjectFamilyAssetLiabilityListItemVO {
/** 收入负债对比金额 */
private BigDecimal comparisonAmount;
/** 可解释收入 */
private BigDecimal explainableIncome;
/** 资产收入倍数 */
private BigDecimal assetIncomeRatio;
/** 风险等级编码 */
private String riskLevelCode;

View File

@@ -15,6 +15,12 @@ public class CcdiProjectFamilyIncomeDetailVO {
/** 配偶年收入 */
private BigDecimal spouseIncome;
/** 本人入职年限 */
private Integer employmentYears;
/** 家庭总年收入 */
private BigDecimal totalIncome;
/** 可解释收入 */
private BigDecimal explainableIncome;
}

View File

@@ -0,0 +1,65 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
* 项目新增贷款查询列表项
*/
@Data
public class CcdiProjectIncreaseLendingListItemVO {
/** 员工ID/柜员号 */
private String staffId;
/** 员工姓名 */
private String staffName;
/** 员工身份证号 */
private String staffIdCard;
/** 所属部门 */
private String deptName;
/** 合同编号 */
private String contractNo;
/** 放款机构号 */
private String lendingOrgNo;
/** 借款人名称 */
private String borrowerName;
/** 借款人证件号码 */
private String borrowerCertNo;
/** 贷款产品 */
private String loanProduct;
/** 合同金额 */
private BigDecimal contractAmount;
/** 当前贷款余额 */
private BigDecimal loanBalance;
/** 贷款发放日期 */
private String loanStartDate;
/** 贷款到期日期 */
private String loanEndDate;
/** 合同状态 */
private String status;
/** 五级分类 */
private String fiveClassification;
/** 客户经理ID */
private String customerManagerId;
/** 客户经理姓名 */
private String customerManagerName;
/** 审批人 */
private String approver;
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 项目新增贷款查询列表结果
*/
@Data
public class CcdiProjectIncreaseLendingListVO {
/** 列表数据 */
private List<CcdiProjectIncreaseLendingListItemVO> rows;
/** 总数 */
private Long total;
}

View File

@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.project.domain.vo;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@@ -21,10 +22,16 @@ public class CcdiProjectOverviewReportVO {
private CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO();
private CcdiProjectExternalRiskSummaryVO externalRiskSummary = new CcdiProjectExternalRiskSummaryVO();
private List<CcdiProjectOverviewReportModelSummaryVO> modelSummaries = new ArrayList<>();
private List<CcdiProjectOverviewReportModelSummaryVO> externalModelSummaries = new ArrayList<>();
private List<CcdiProjectRiskModelPeopleItemVO> riskPeople = new ArrayList<>();
private List<CcdiProjectExternalPersonWarningExcel> externalPersonWarnings = new ArrayList<>();
private List<CcdiProjectOverviewReportSuspiciousTransactionVO> suspiciousTransactions = new ArrayList<>();
private List<CcdiProjectEmployeeCreditNegativeExcel> illegalPeople = new ArrayList<>();

View File

@@ -13,4 +13,8 @@ public class CcdiProjectOverviewStatVO {
private String label;
private Integer value;
private Integer employeeValue;
private Integer externalValue;
}

View File

@@ -12,6 +12,8 @@ public class CcdiProjectPersonAnalysisObjectRecordVO {
private String modelCode;
private String ruleCode;
private String title;
private String subtitle;

View File

@@ -17,4 +17,6 @@ public class CcdiProjectRiskHitTagVO {
private String ruleName;
private String riskLevel;
private String reasonDetail;
}

View File

@@ -23,4 +23,10 @@ public class CcdiProjectStatusCountsVO {
/** 打标中项目数(状态3) */
private Long status3;
/** 打标失败项目数(状态4) */
private Long status4;
/** 已删除项目数(状态5) */
private Long status5;
}

View File

@@ -3,6 +3,8 @@ package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 涉疑交易明细行
@@ -14,6 +16,14 @@ public class CcdiProjectSuspiciousTransactionItemVO {
private String trxDate;
private String leAccountNo;
private String leAccountName;
private String customerAccountName;
private String customerAccountNo;
private String suspiciousPersonName;
private String relatedPersonName;
@@ -33,4 +43,8 @@ public class CcdiProjectSuspiciousTransactionItemVO {
private Boolean hasModelRuleHit;
private Boolean hasNameListHit;
private String nameListHitType;
private List<CcdiBankStatementHitTagVO> hitTags = new ArrayList<>();
}

View File

@@ -50,9 +50,24 @@ public class CcdiProjectVO {
/** 更新时间 */
private Date updateTime;
/** 最近一次打标失败原因 */
private String latestTagTaskErrorMessage;
/** 最近一次打标失败结束时间 */
private Date latestTagTaskEndTime;
/** 创建者(用户名) */
private String createBy;
/** 创建者姓名(真实姓名) */
private String createByName;
/** 是否当前用户创建 */
private Boolean ownedByCurrentUser;
/** 当前用户是否可操作 */
private Boolean canOperate;
/** 当前用户是否可删除 */
private Boolean canDelete;
}

View File

@@ -0,0 +1,90 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 关系图谱边
*/
@Data
public class CcdiRelationGraphEdgeVO {
private String objectKey;
private String fromKey;
private String toKey;
private String fromObjectKey;
private String toObjectKey;
private String fromName;
private String toName;
private String edgeTable;
private String relationType;
private String companyName;
private String stockName;
private String stockType;
private String stockPercent;
private String shouldCapi;
private String shouldCapiValue;
private String shouldCapiUnit;
private String shoudDate;
private String pKeyNo;
private String operName;
private String operKeyNo;
private String personId;
private String relationName;
private String relationCertNo;
private String gender;
private String birthDate;
private String relationCertType;
private String mobilePhone1;
private String mobilePhone2;
private String wechatNo1;
private String wechatNo2;
private String wechatNo3;
private String contactAddress;
private BigDecimal annualIncome;
private String relationDesc;
private String status;
private String effectiveDate;
private String invalidDate;
private String remark;
private String dataSource;
}

View File

@@ -0,0 +1,34 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 关系图谱节点
*/
@Data
public class CcdiRelationGraphNodeVO {
private String objectKey;
private String nodeKey;
private String nodeName;
private String idNumber;
private String subjectType;
private String sourceType;
private String detailRefType;
private String detailRefKey;
private String createdTime;
private String updatedTime;
private Boolean canExpand;
private Integer depth;
}

View File

@@ -0,0 +1,35 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 关系图谱疑似企业明细
*/
@Data
public class CcdiRelationGraphSuspectedEnterpriseItemVO {
private String candidateKeyNo;
private String personName;
private String companyId;
private String companyName;
private String creditCode;
private String enterpriseStatus;
private String industryName;
private String relationType;
private String stockPercent;
/** 企业成立日期或当前可用的工商关系日期 */
private String establishDate;
private Integer ageAtEstablish;
private String matchReason;
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 关系图谱疑似企业结果
*/
@Data
public class CcdiRelationGraphSuspectedEnterpriseVO {
/** 是否因同名候选过多被拦截 */
private Boolean blocked = false;
/** 拦截或空结果说明 */
private String message;
/** 同名工商keyno数量 */
private Integer sameNameKeyNoCount = 0;
/** 表格明细 */
private List<CcdiRelationGraphSuspectedEnterpriseItemVO> rows = new ArrayList<>();
}

View File

@@ -0,0 +1,23 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 关系图谱结果
*/
@Data
public class CcdiRelationGraphVO {
private CcdiRelationGraphNodeVO centerNode;
private List<CcdiRelationGraphNodeVO> nodes = new ArrayList<>();
private List<CcdiRelationGraphEdgeVO> edges = new ArrayList<>();
private Long edgeCount = 0L;
private Integer maxDepth = 1;
}

View File

@@ -90,6 +90,36 @@ public interface CcdiBankTagAnalysisMapper {
List<BankTagStatementHitVO> selectLargeTransferStatements(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 外部人员单笔大额交易
*
* @param projectId 项目ID
* @param threshold 单笔大额阈值
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectExternalSingleLargeAmountStatements(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 外部人员累计交易超限
*
* @param projectId 项目ID
* @param threshold 累计交易阈值
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectExternalCumulativeTransactionAmountObjects(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 外部人员年流水超限
*
* @param projectId 项目ID
* @param threshold 年流水阈值
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectExternalAnnualTurnoverObjects(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 与客户之间非正常资金往来
*
@@ -126,6 +156,26 @@ public interface CcdiBankTagAnalysisMapper {
*/
List<BankTagStatementHitVO> selectGamblingSensitiveKeywordStatements(@Param("projectId") Long projectId);
/**
* 外部人员疑似赌博摘要
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectExternalGamblingMemoStatements(@Param("projectId") Long projectId);
/**
* 外部人员同日多对手方疑似赌博交易
*
* @param projectId 项目ID
* @param amountMinThreshold 可疑金额下限
* @param amountMaxThreshold 可疑金额上限
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectExternalMultiPartyGamblingTransferObjects(@Param("projectId") Long projectId,
@Param("amountMinThreshold") BigDecimal amountMinThreshold,
@Param("amountMaxThreshold") BigDecimal amountMaxThreshold);
/**
* 特殊金额交易
*
@@ -134,6 +184,22 @@ public interface CcdiBankTagAnalysisMapper {
*/
List<BankTagStatementHitVO> selectSpecialAmountTransactionStatements(@Param("projectId") Long projectId);
/**
* 外部人员与员工或员工亲属交易
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectExternalToStaffOrFamilyTransactionStatements(@Param("projectId") Long projectId);
/**
* 外部人员夜间交易
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectExternalNightTransactionStatements(@Param("projectId") Long projectId);
/**
* 月度固定收入疑似兼职
*
@@ -272,9 +338,11 @@ public interface CcdiBankTagAnalysisMapper {
* 微信支付宝提现超额
*
* @param projectId 项目ID
* @param amountThreshold 提现金额阈值
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectWithdrawAmtObjects(@Param("projectId") Long projectId);
List<BankTagObjectHitVO> selectWithdrawAmtObjects(@Param("projectId") Long projectId,
@Param("amountThreshold") BigDecimal amountThreshold);
/**
* 工资快速转出

View File

@@ -26,11 +26,15 @@ public interface CcdiBankTagResultMapper extends BaseMapper<CcdiBankTagResult> {
*
* @param projectId 项目ID
* @param bankStatementIds 流水ID列表
* @param modelCode 模型编码
* @param suspiciousType 预警类型
* @return 命中的异常标签列表
*/
List<CcdiBankStatementHitTagVO> selectStatementTagsByProjectAndStatementIds(
@Param("projectId") Long projectId,
@Param("bankStatementIds") List<Long> bankStatementIds
@Param("bankStatementIds") List<Long> bankStatementIds,
@Param("modelCode") String modelCode,
@Param("suspiciousType") String suspiciousType
);
/**

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