Compare commits
62 Commits
0bf73a923f
...
dev-ui
| Author | SHA1 | Date | |
|---|---|---|---|
| 64ddc362e3 | |||
| 6c70149e0c | |||
| 2298f9e589 | |||
| e08f437acc | |||
| 6134ec06af | |||
| dfa35ebd85 | |||
| 1f82c7e16e | |||
| 6c206deb5f | |||
| 2999d7cf7d | |||
| f42981f153 | |||
| 67f206be1b | |||
| 66d34c7ceb | |||
| 2e30b2a877 | |||
| d2cdb0f3c9 | |||
| aaec746181 | |||
| dcfd869d84 | |||
| d284923c59 | |||
| 84b04e31ed | |||
| 507bc1c2d7 | |||
| 64891a621d | |||
| 606b4f836b | |||
| 33c1f9df90 | |||
| bee1e800ce | |||
| a191cc7ca3 | |||
| 7826645a90 | |||
| 9b11426617 | |||
| 1494491d89 | |||
| 5509ece708 | |||
| 02e45dadcc | |||
| caeac0f115 | |||
| a4005d3e9f | |||
| f9e0a079f5 | |||
| e00d60f5c1 | |||
| 87da17bccb | |||
| 4a9c6ba64b | |||
| 17eb11b3b8 | |||
| b740d4c100 | |||
| f5a98bbd7a | |||
| 987afc3863 | |||
| 051650370e | |||
| c211413bd2 | |||
| bb9994a339 | |||
| daf463bbb4 | |||
| 6f6f2d264c | |||
| 266108ac06 | |||
| 5ff67442bb | |||
| b7bb96b43b | |||
| 9902430350 | |||
| 89773caa7b | |||
| def656d57b | |||
| 6b6fae4b92 | |||
| 0dc3b6ee2f | |||
| 04ede64767 | |||
| 066d850389 | |||
| 5e4bfca05b | |||
| 4e90e22ee2 | |||
| a5eba12ed5 | |||
| aef6c43181 | |||
| 4c2ea9d8e6 | |||
| 26cd049991 | |||
| 180a892275 | |||
| ef8147892e |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -97,3 +97,7 @@ tongweb_62318.properties
|
||||
.superpowers/
|
||||
|
||||
tmp/
|
||||
|
||||
.codegraph/
|
||||
|
||||
.claude/
|
||||
@@ -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
669
CLAUDE.md
@@ -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>
|
||||
```
|
||||
@@ -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 "$@"
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
|
||||
<sql id="AccountInfoWhereClause">
|
||||
WHERE 1 = 1
|
||||
AND ai.owner_type <> 'CREDIT_CUSTOMER'
|
||||
<if test="query.staffName != null and query.staffName != ''">
|
||||
AND (
|
||||
(ai.owner_type = 'EMPLOYEE' AND bs.name LIKE CONCAT('%', #{query.staffName}, '%'))
|
||||
|
||||
@@ -35,6 +35,7 @@ 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);
|
||||
}
|
||||
|
||||
private MappedStatement loadMappedStatement(String statementId) throws Exception {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
// 忽略临时文件删除失败,避免影响主流程返回
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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";
|
||||
|
||||
private CcdiProjectStatusConstants() {
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -39,13 +40,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 +61,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,8 +72,8 @@ public class CcdiBankStatementController extends BaseController {
|
||||
*/
|
||||
@GetMapping("/detail/{bankStatementId}")
|
||||
@Operation(summary = "查询流水详情")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public AjaxResult getDetail(@PathVariable Long bankStatementId) {
|
||||
projectAccessService.assertCanReadByBankStatementId(bankStatementId);
|
||||
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(bankStatementId);
|
||||
return AjaxResult.success(detail);
|
||||
}
|
||||
@@ -81,6 +85,7 @@ public class CcdiBankStatementController extends BaseController {
|
||||
@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, "流水明细");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -17,6 +18,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.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -40,17 +42,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("请选择要上传的文件");
|
||||
}
|
||||
@@ -71,9 +78,9 @@ public class CcdiFileUploadController extends BaseController {
|
||||
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 文件");
|
||||
if (!lowerFileName.endsWith(".xlsx") && !lowerFileName.endsWith(".csv")
|
||||
&& !lowerFileName.endsWith(".pdf")) {
|
||||
return AjaxResult.error("文件 " + fileName + " 格式不支持, 仅支持 PDF, CSV, XLSX 文件");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +102,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("身份证文件不能为空");
|
||||
@@ -108,10 +116,12 @@ public class CcdiFileUploadController extends BaseController {
|
||||
*/
|
||||
@PostMapping("/pull-bank-info")
|
||||
@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("身份证号不能为空");
|
||||
}
|
||||
@@ -138,6 +148,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 +161,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 +172,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 +182,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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ public class CcdiProjectController extends BaseController {
|
||||
*/
|
||||
@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 +95,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 +107,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 +128,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);
|
||||
|
||||
@@ -2,14 +2,20 @@ 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.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 +23,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;
|
||||
@@ -45,6 +52,9 @@ public class CcdiProjectOverviewController extends BaseController {
|
||||
@Resource
|
||||
private ICcdiProjectOverviewService overviewService;
|
||||
|
||||
@Resource
|
||||
private CcdiProjectAccessService projectAccessService;
|
||||
|
||||
/**
|
||||
* 查询风险仪表盘
|
||||
*/
|
||||
@@ -52,6 +62,7 @@ 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);
|
||||
}
|
||||
@@ -63,10 +74,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 +110,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 +122,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 +134,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 +170,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 +182,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 +194,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 +206,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 +221,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,12 +235,58 @@ 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, "外部人员风险模型命中人员");
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出风险明细
|
||||
*/
|
||||
@@ -180,16 +294,18 @@ public class CcdiProjectOverviewController extends BaseController {
|
||||
@Operation(summary = "导出风险明细")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
|
||||
projectAccessService.assertCanRead(projectId);
|
||||
overviewService.exportRiskDetails(response, projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键导出结果总览报告
|
||||
* 导出结果总览报告
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ 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.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
@@ -39,6 +40,9 @@ public class CcdiProjectSpecialCheckController extends BaseController {
|
||||
@Resource
|
||||
private ICcdiProjectSpecialCheckService specialCheckService;
|
||||
|
||||
@Resource
|
||||
private CcdiProjectAccessService projectAccessService;
|
||||
|
||||
/**
|
||||
* 查询员工家庭资产负债列表
|
||||
*/
|
||||
@@ -46,6 +50,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 +62,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 +74,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 +86,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 +98,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 +110,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 +122,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,6 +134,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class CcdiProject implements Serializable {
|
||||
/** 配置方式:default-全局默认,custom-自定义 */
|
||||
private String configType;
|
||||
|
||||
/** 项目状态:0-进行中,1-已完成,2-已归档,3-打标中 */
|
||||
/** 项目状态:0-进行中,1-已完成,2-已归档,3-打标中,4-打标失败 */
|
||||
private String status;
|
||||
|
||||
/** 是否归档:0-未归档,1-已归档 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -40,6 +40,9 @@ public class CcdiBankStatementQueryDTO {
|
||||
/** 本方主体 */
|
||||
private List<String> ourSubjects;
|
||||
|
||||
/** 本方证件号 */
|
||||
private List<String> ourCertNos;
|
||||
|
||||
/** 本方银行 */
|
||||
private List<String> ourBanks;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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(","));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<>();
|
||||
|
||||
@@ -13,4 +13,8 @@ public class CcdiProjectOverviewStatVO {
|
||||
private String label;
|
||||
|
||||
private Integer value;
|
||||
|
||||
private Integer employeeValue;
|
||||
|
||||
private Integer externalValue;
|
||||
}
|
||||
|
||||
@@ -17,4 +17,6 @@ public class CcdiProjectRiskHitTagVO {
|
||||
private String ruleName;
|
||||
|
||||
private String riskLevel;
|
||||
|
||||
private String reasonDetail;
|
||||
}
|
||||
|
||||
@@ -23,4 +23,7 @@ public class CcdiProjectStatusCountsVO {
|
||||
|
||||
/** 打标中项目数(状态3) */
|
||||
private Long status3;
|
||||
|
||||
/** 打标失败项目数(状态4) */
|
||||
private Long status4;
|
||||
}
|
||||
|
||||
@@ -33,4 +33,6 @@ public class CcdiProjectSuspiciousTransactionItemVO {
|
||||
private Boolean hasModelRuleHit;
|
||||
|
||||
private Boolean hasNameListHit;
|
||||
|
||||
private String nameListHitType;
|
||||
}
|
||||
|
||||
@@ -50,9 +50,21 @@ public class CcdiProjectVO {
|
||||
/** 更新时间 */
|
||||
private Date updateTime;
|
||||
|
||||
/** 最近一次打标失败原因 */
|
||||
private String latestTagTaskErrorMessage;
|
||||
|
||||
/** 最近一次打标失败结束时间 */
|
||||
private Date latestTagTaskEndTime;
|
||||
|
||||
/** 创建者(用户名) */
|
||||
private String createBy;
|
||||
|
||||
/** 创建者姓名(真实姓名) */
|
||||
private String createByName;
|
||||
|
||||
/** 是否当前用户创建 */
|
||||
private Boolean ownedByCurrentUser;
|
||||
|
||||
/** 当前用户是否可操作 */
|
||||
private Boolean canOperate;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 工资快速转出
|
||||
|
||||
@@ -32,4 +32,12 @@ public interface CcdiBankTagTaskMapper extends BaseMapper<CcdiBankTagTask> {
|
||||
* @return 任务实体
|
||||
*/
|
||||
CcdiBankTagTask selectRunningTaskByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 查询项目最近一次失败任务
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 任务实体
|
||||
*/
|
||||
CcdiBankTagTask selectLatestFailedTaskByProjectId(@Param("projectId") Long projectId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
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 org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 资金流图谱Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface CcdiFundGraphMapper {
|
||||
|
||||
List<CcdiFundGraphNodeVO> selectFundGraphSubjects(@Param("query") CcdiFundGraphQueryDTO query);
|
||||
|
||||
List<CcdiFundGraphNodeVO> selectFundGraphSubjectsByExactKeyword(@Param("query") CcdiFundGraphQueryDTO query);
|
||||
|
||||
List<CcdiFundGraphNodeVO> selectFundGraphSubjectsByName(@Param("query") CcdiFundGraphQueryDTO query);
|
||||
|
||||
List<CcdiFundGraphEdgeVO> selectFundGraphEdges(@Param("query") CcdiFundGraphQueryDTO query);
|
||||
|
||||
List<CcdiFundGraphEdgeVO> selectFundGraphManualEdges(@Param("query") CcdiFundGraphQueryDTO query);
|
||||
|
||||
int countSubjectByObjectKey(@Param("objectKey") String objectKey);
|
||||
|
||||
int insertManualSubject(
|
||||
@Param("objectKey") String objectKey,
|
||||
@Param("idNo") String idNo,
|
||||
@Param("name") String name
|
||||
);
|
||||
|
||||
int insertManualEdge(
|
||||
@Param("objectKey") String objectKey,
|
||||
@Param("dto") CcdiFundGraphManualEdgeSaveDTO dto,
|
||||
@Param("operator") String operator
|
||||
);
|
||||
|
||||
Page<CcdiFundGraphStatementVO> selectFundGraphEdgeDetails(
|
||||
Page<CcdiFundGraphStatementVO> page,
|
||||
@Param("query") CcdiFundGraphEdgeDetailQueryDTO query
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.project.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
|
||||
@@ -25,7 +26,9 @@ public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
|
||||
* @param queryDTO 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, @Param("queryDTO") CcdiProjectQueryDTO queryDTO);
|
||||
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page,
|
||||
@Param("queryDTO") CcdiProjectQueryDTO queryDTO,
|
||||
@Param("scope") ProjectAccessScope scope);
|
||||
|
||||
/**
|
||||
* 查询历史项目列表
|
||||
@@ -33,12 +36,14 @@ public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
|
||||
* @param queryDTO 查询条件
|
||||
* @return 历史项目列表
|
||||
*/
|
||||
List<CcdiProjectHistoryListItemVO> selectHistoryProjects(@Param("queryDTO") CcdiProjectQueryDTO queryDTO);
|
||||
List<CcdiProjectHistoryListItemVO> selectHistoryProjects(@Param("queryDTO") CcdiProjectQueryDTO queryDTO,
|
||||
@Param("scope") ProjectAccessScope scope);
|
||||
|
||||
/**
|
||||
* 更新项目风险人数
|
||||
* 更新项目总人数与风险人数
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param targetCount 总人数
|
||||
* @param highRiskCount 高风险人数
|
||||
* @param mediumRiskCount 中风险人数
|
||||
* @param lowRiskCount 低风险人数
|
||||
@@ -46,6 +51,7 @@ public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
|
||||
* @return 更新行数
|
||||
*/
|
||||
int updateRiskCountsByProjectId(@Param("projectId") Long projectId,
|
||||
@Param("targetCount") Integer targetCount,
|
||||
@Param("highRiskCount") Integer highRiskCount,
|
||||
@Param("mediumRiskCount") Integer mediumRiskCount,
|
||||
@Param("lowRiskCount") Integer lowRiskCount,
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
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.CcdiProjectRiskModelPeopleQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
|
||||
@@ -11,6 +13,8 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO;
|
||||
@@ -121,6 +125,88 @@ public interface CcdiProjectOverviewMapper {
|
||||
@Param("query") CcdiProjectRiskModelPeopleQueryDTO query
|
||||
);
|
||||
|
||||
/**
|
||||
* 查询风险模型命中人员导出列表
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 命中人员列表
|
||||
*/
|
||||
List<CcdiProjectRiskModelPeopleItemVO> selectRiskModelPeopleList(
|
||||
@Param("query") CcdiProjectRiskModelPeopleQueryDTO query
|
||||
);
|
||||
|
||||
/**
|
||||
* 分页查询外部人员预警
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param query 查询条件
|
||||
* @return 外部人员预警分页
|
||||
*/
|
||||
Page<CcdiProjectExternalPersonWarningItemVO> selectExternalPersonWarningPage(
|
||||
Page<CcdiProjectExternalPersonWarningItemVO> page,
|
||||
@Param("query") CcdiProjectExternalPersonQueryDTO query
|
||||
);
|
||||
|
||||
/**
|
||||
* 查询外部人员预警导出列表
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 外部人员预警列表
|
||||
*/
|
||||
List<CcdiProjectExternalPersonWarningItemVO> selectExternalPersonWarningList(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 查询外部人员风险等级汇总
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 外部人员风险等级汇总
|
||||
*/
|
||||
CcdiProjectExternalRiskSummaryVO selectExternalRiskSummaryByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 查询外部人员预警模型卡片
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 模型卡片
|
||||
*/
|
||||
List<CcdiProjectRiskModelCardVO> selectExternalRiskModelCardsByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 分页查询外部人员模型命中人员
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param query 查询条件
|
||||
* @return 命中人员分页
|
||||
*/
|
||||
Page<CcdiProjectRiskModelPeopleItemVO> selectExternalRiskModelPeoplePage(
|
||||
Page<CcdiProjectRiskModelPeopleItemVO> page,
|
||||
@Param("query") CcdiProjectExternalRiskModelPeopleQueryDTO query
|
||||
);
|
||||
|
||||
/**
|
||||
* 查询外部人员模型命中人员导出列表
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 命中人员列表
|
||||
*/
|
||||
List<CcdiProjectRiskModelPeopleItemVO> selectExternalRiskModelPeopleList(
|
||||
@Param("query") CcdiProjectExternalRiskModelPeopleQueryDTO query
|
||||
);
|
||||
|
||||
/**
|
||||
* 查询外部人员命中标签
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param certNo 外部人员证件号
|
||||
* @param selectedModelCodes 已选模型编码CSV,可为空
|
||||
* @return 命中标签列表
|
||||
*/
|
||||
List<CcdiProjectRiskHitTagVO> selectExternalRiskHitTagsByScope(
|
||||
@Param("projectId") Long projectId,
|
||||
@Param("certNo") String certNo,
|
||||
@Param("selectedModelCodes") String selectedModelCodes
|
||||
);
|
||||
|
||||
/**
|
||||
* 分页查询涉疑交易明细
|
||||
*
|
||||
@@ -240,4 +326,5 @@ public interface CcdiProjectOverviewMapper {
|
||||
* @return 风险人数汇总
|
||||
*/
|
||||
Map<String, Object> selectRiskCountSummaryByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiRelationGraphQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphEdgeVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphNodeVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphSuspectedEnterpriseItemVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 关系图谱Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface CcdiRelationGraphMapper {
|
||||
|
||||
List<CcdiRelationGraphNodeVO> selectRelationGraphSubjects(@Param("query") CcdiRelationGraphQueryDTO query);
|
||||
|
||||
List<CcdiRelationGraphNodeVO> selectRelationGraphNodesByKeys(@Param("objectKeys") List<String> objectKeys);
|
||||
|
||||
List<CcdiRelationGraphEdgeVO> selectRelationGraphEdges(@Param("query") CcdiRelationGraphQueryDTO query);
|
||||
|
||||
int countSuspectedEnterpriseKeyNos(@Param("personName") String personName);
|
||||
|
||||
List<CcdiRelationGraphSuspectedEnterpriseItemVO> selectSuspectedEnterprises(@Param("personName") String personName,
|
||||
@Param("limit") Integer limit);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.ruoyi.ccdi.project.service;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiEvidence;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiEvidenceMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.common.core.domain.entity.SysRole;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 项目访问控制。
|
||||
*/
|
||||
@Service
|
||||
public class CcdiProjectAccessService {
|
||||
|
||||
private static final String ROLE_ADMIN = "admin";
|
||||
|
||||
private static final String ROLE_MANAGER = "manager";
|
||||
|
||||
@Resource
|
||||
private CcdiProjectMapper projectMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiBankStatementMapper bankStatementMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiFileUploadRecordMapper fileUploadRecordMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiEvidenceMapper evidenceMapper;
|
||||
|
||||
public ProjectAccessScope buildCurrentScope() {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
String username = SecurityUtils.getUsername();
|
||||
boolean superAdmin = isSuperAdmin(loginUser);
|
||||
boolean projectManager = hasRole(loginUser, ROLE_MANAGER);
|
||||
return new ProjectAccessScope(username, superAdmin || projectManager, superAdmin, projectManager);
|
||||
}
|
||||
|
||||
public boolean canOperate(CcdiProject project) {
|
||||
if (project == null) {
|
||||
return false;
|
||||
}
|
||||
ProjectAccessScope scope = buildCurrentScope();
|
||||
return scope.isSuperAdmin() || Objects.equals(scope.getUsername(), project.getCreateBy());
|
||||
}
|
||||
|
||||
public void assertCanRead(Long projectId) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
ProjectAccessScope scope = buildCurrentScope();
|
||||
if (scope.isViewAllProjects() || Objects.equals(scope.getUsername(), project.getCreateBy())) {
|
||||
return;
|
||||
}
|
||||
throw new ServiceException("无权查看该项目");
|
||||
}
|
||||
|
||||
public void assertCanOperate(Long projectId) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
if (canOperate(project)) {
|
||||
return;
|
||||
}
|
||||
throw new ServiceException("无权操作该项目");
|
||||
}
|
||||
|
||||
public void assertCanReadByBankStatementId(Long bankStatementId) {
|
||||
CcdiBankStatement statement = getRequiredBankStatement(bankStatementId);
|
||||
assertCanRead(statement.getProjectId());
|
||||
}
|
||||
|
||||
public void assertCanReadByFileRecordId(Long fileRecordId) {
|
||||
CcdiFileUploadRecord record = getRequiredFileRecord(fileRecordId);
|
||||
assertCanRead(record.getProjectId());
|
||||
}
|
||||
|
||||
public void assertCanOperateByFileRecordId(Long fileRecordId) {
|
||||
CcdiFileUploadRecord record = getRequiredFileRecord(fileRecordId);
|
||||
assertCanOperate(record.getProjectId());
|
||||
}
|
||||
|
||||
public void assertCanReadByEvidenceId(Long evidenceId) {
|
||||
CcdiEvidence evidence = getRequiredEvidence(evidenceId);
|
||||
assertCanRead(evidence.getProjectId());
|
||||
}
|
||||
|
||||
public void assertSourceProjectsReadable(List<Long> sourceProjectIds) {
|
||||
if (CollectionUtils.isEmpty(sourceProjectIds)) {
|
||||
return;
|
||||
}
|
||||
for (Long sourceProjectId : sourceProjectIds) {
|
||||
assertCanRead(sourceProjectId);
|
||||
}
|
||||
}
|
||||
|
||||
private CcdiProject getRequiredProject(Long projectId) {
|
||||
if (projectId == null) {
|
||||
throw new ServiceException("项目ID不能为空");
|
||||
}
|
||||
CcdiProject project = projectMapper.selectById(projectId);
|
||||
if (project == null) {
|
||||
throw new ServiceException("项目不存在");
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
private CcdiBankStatement getRequiredBankStatement(Long bankStatementId) {
|
||||
if (bankStatementId == null) {
|
||||
throw new ServiceException("流水ID不能为空");
|
||||
}
|
||||
CcdiBankStatement statement = bankStatementMapper.selectById(bankStatementId);
|
||||
if (statement == null) {
|
||||
throw new ServiceException("流水记录不存在");
|
||||
}
|
||||
return statement;
|
||||
}
|
||||
|
||||
private CcdiFileUploadRecord getRequiredFileRecord(Long fileRecordId) {
|
||||
if (fileRecordId == null) {
|
||||
throw new ServiceException("文件记录ID不能为空");
|
||||
}
|
||||
CcdiFileUploadRecord record = fileUploadRecordMapper.selectById(fileRecordId);
|
||||
if (record == null) {
|
||||
throw new ServiceException("文件记录不存在");
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
private CcdiEvidence getRequiredEvidence(Long evidenceId) {
|
||||
if (evidenceId == null) {
|
||||
throw new ServiceException("证据ID不能为空");
|
||||
}
|
||||
CcdiEvidence evidence = evidenceMapper.selectById(evidenceId);
|
||||
if (evidence == null) {
|
||||
throw new ServiceException("证据不存在");
|
||||
}
|
||||
return evidence;
|
||||
}
|
||||
|
||||
private boolean isSuperAdmin(LoginUser loginUser) {
|
||||
if (loginUser == null) {
|
||||
return false;
|
||||
}
|
||||
if (SecurityUtils.isAdmin(loginUser.getUserId())) {
|
||||
return true;
|
||||
}
|
||||
return hasRole(loginUser, ROLE_ADMIN);
|
||||
}
|
||||
|
||||
private boolean hasRole(LoginUser loginUser, String roleKey) {
|
||||
if (loginUser == null || loginUser.getUser() == null
|
||||
|| CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {
|
||||
return false;
|
||||
}
|
||||
for (SysRole role : loginUser.getUser().getRoles()) {
|
||||
if (roleKey.equals(role.getRoleKey())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ruoyi.ccdi.project.service;
|
||||
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* 资金流图谱Service接口
|
||||
*/
|
||||
public interface ICcdiFundGraphService {
|
||||
|
||||
List<CcdiFundGraphNodeVO> searchSubjects(CcdiFundGraphQueryDTO queryDTO);
|
||||
|
||||
CcdiFundGraphVO getFundGraph(CcdiFundGraphQueryDTO queryDTO);
|
||||
|
||||
Page<CcdiFundGraphStatementVO> getEdgeDetails(
|
||||
Page<CcdiFundGraphStatementVO> page,
|
||||
CcdiFundGraphEdgeDetailQueryDTO queryDTO
|
||||
);
|
||||
|
||||
CcdiFundGraphEdgeVO saveManualEdge(CcdiFundGraphManualEdgeSaveDTO saveDTO, String operator);
|
||||
}
|
||||
@@ -2,16 +2,22 @@ package com.ruoyi.ccdi.project.service;
|
||||
|
||||
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.CcdiProjectSuspiciousTransactionQueryDTO;
|
||||
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 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;
|
||||
@@ -82,6 +88,80 @@ public interface ICcdiProjectOverviewService {
|
||||
return new CcdiProjectRiskModelPeopleVO();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出风险模型命中人员
|
||||
*
|
||||
* @param queryDTO 查询条件
|
||||
* @return 导出列表
|
||||
*/
|
||||
default List<CcdiProjectRiskModelPeopleExcel> exportRiskModelPeople(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询外部人员预警
|
||||
*
|
||||
* @param queryDTO 查询条件
|
||||
* @return 外部人员预警
|
||||
*/
|
||||
default CcdiProjectExternalPersonWarningVO getExternalPersonWarnings(CcdiProjectExternalPersonQueryDTO queryDTO) {
|
||||
return new CcdiProjectExternalPersonWarningVO();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询外部人员风险等级汇总
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 外部人员风险等级汇总
|
||||
*/
|
||||
default CcdiProjectExternalRiskSummaryVO getExternalRiskSummary(Long projectId) {
|
||||
return new CcdiProjectExternalRiskSummaryVO();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出外部人员预警
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 导出列表
|
||||
*/
|
||||
default List<CcdiProjectExternalPersonWarningExcel> exportExternalPersonWarnings(Long projectId) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询外部人员风险模型卡片
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 风险模型卡片
|
||||
*/
|
||||
default CcdiProjectRiskModelCardsVO getExternalRiskModelCards(Long projectId) {
|
||||
return new CcdiProjectRiskModelCardsVO();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询外部人员风险模型命中人员
|
||||
*
|
||||
* @param queryDTO 查询条件
|
||||
* @return 命中人员
|
||||
*/
|
||||
default CcdiProjectRiskModelPeopleVO getExternalRiskModelPeople(
|
||||
CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO
|
||||
) {
|
||||
return new CcdiProjectRiskModelPeopleVO();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出外部人员风险模型命中人员
|
||||
*
|
||||
* @param queryDTO 查询条件
|
||||
* @return 导出列表
|
||||
*/
|
||||
default List<CcdiProjectRiskModelPeopleExcel> exportExternalRiskModelPeople(
|
||||
CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO
|
||||
) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询涉疑交易明细
|
||||
*
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.ccdi.project.service;
|
||||
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* 关系图谱Service接口
|
||||
*/
|
||||
public interface ICcdiRelationGraphService {
|
||||
|
||||
List<CcdiRelationGraphNodeVO> searchSubjects(CcdiRelationGraphQueryDTO queryDTO);
|
||||
|
||||
CcdiRelationGraphVO getRelationGraph(CcdiRelationGraphQueryDTO queryDTO);
|
||||
|
||||
CcdiRelationGraphSuspectedEnterpriseVO getSuspectedEnterprises(CcdiRelationGraphSuspectedEnterpriseQueryDTO queryDTO);
|
||||
}
|
||||
@@ -27,20 +27,32 @@ public class BankTagRuleConfigResolver {
|
||||
private static final Map<String, Set<String>> RULE_PARAM_MAPPING = Map.ofEntries(
|
||||
Map.entry("SINGLE_LARGE_INCOME", Set.of("SINGLE_TRANSACTION_AMOUNT")),
|
||||
Map.entry("CUMULATIVE_INCOME", Set.of("CUMULATIVE_TRANSACTION_AMOUNT")),
|
||||
Map.entry("EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT", Set.of("CUMULATIVE_TRANSACTION_AMOUNT")),
|
||||
Map.entry("ANNUAL_TURNOVER", Set.of("ANNUAL_TURNOVER")),
|
||||
Map.entry("EXTERNAL_ANNUAL_TURNOVER", Set.of("ANNUAL_TURNOVER")),
|
||||
Map.entry("LARGE_CASH_DEPOSIT", Set.of("LARGE_CASH_DEPOSIT")),
|
||||
Map.entry("FREQUENT_CASH_DEPOSIT", Set.of("LARGE_CASH_DEPOSIT", "FREQUENT_CASH_DEPOSIT")),
|
||||
Map.entry("LARGE_TRANSFER", Set.of("FREQUENT_TRANSFER")),
|
||||
Map.entry("EXTERNAL_SINGLE_LARGE_AMOUNT", Set.of("FREQUENT_TRANSFER")),
|
||||
Map.entry("FOREX_BUY_AMT", Set.of("SINGLE_PURCHASE_AMOUNT")),
|
||||
Map.entry("FOREX_SELL_AMT", Set.of("SINGLE_SETTLEMENT_AMOUNT")),
|
||||
Map.entry("WITHDRAW_CNT", Set.of("WITHDRAW_CNT")),
|
||||
Map.entry("WITHDRAW_AMT", Set.of("WITHDRAW_AMT")),
|
||||
Map.entry("STOCK_TFR_LARGE", Set.of("STOCK_TFR_LARGE")),
|
||||
Map.entry("LARGE_STOCK_TRADING", Set.of("STOCK_TFR_LARGE")),
|
||||
Map.entry("MULTI_PARTY_GAMBLING_TRANSFER", Set.of("MULTI_PARTY_AMT_MIN", "MULTI_PARTY_AMT_MAX")),
|
||||
Map.entry("EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER", Set.of("MULTI_PARTY_AMT_MIN", "MULTI_PARTY_AMT_MAX")),
|
||||
Map.entry("MONTHLY_FIXED_INCOME", Set.of("MONTHLY_FIXED_INCOME")),
|
||||
Map.entry("FIXED_COUNTERPARTY_TRANSFER", Set.of("FIXED_COUNTERPARTY_TRANSFER_MIN", "FIXED_COUNTERPARTY_TRANSFER_MAX"))
|
||||
);
|
||||
|
||||
private static final Map<String, String> RULE_PARAM_MODEL_MAPPING = Map.of(
|
||||
"EXTERNAL_SINGLE_LARGE_AMOUNT", "LARGE_TRANSACTION",
|
||||
"EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT", "LARGE_TRANSACTION",
|
||||
"EXTERNAL_ANNUAL_TURNOVER", "LARGE_TRANSACTION",
|
||||
"EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER", "SUSPICIOUS_GAMBLING"
|
||||
);
|
||||
|
||||
@Resource
|
||||
private CcdiProjectMapper projectMapper;
|
||||
|
||||
@@ -68,12 +80,13 @@ public class BankTagRuleConfigResolver {
|
||||
}
|
||||
|
||||
Long effectiveProjectId = "default".equals(project.getConfigType()) ? 0L : projectId;
|
||||
List<CcdiModelParam> params = modelParamMapper.selectByProjectAndModel(effectiveProjectId, ruleMeta.getModelCode());
|
||||
String paramModelCode = RULE_PARAM_MODEL_MAPPING.getOrDefault(ruleMeta.getRuleCode(), ruleMeta.getModelCode());
|
||||
List<CcdiModelParam> params = modelParamMapper.selectByProjectAndModel(effectiveProjectId, paramModelCode);
|
||||
|
||||
Map<String, String> thresholdValues = new LinkedHashMap<>();
|
||||
Set<String> requiredParamCodes = RULE_PARAM_MAPPING.getOrDefault(ruleMeta.getRuleCode(), Set.of());
|
||||
log.info("【流水标签】解析规则参数: projectId={}, effectiveProjectId={}, ruleCode={}, requiredParams={}",
|
||||
projectId, effectiveProjectId, ruleMeta.getRuleCode(), requiredParamCodes);
|
||||
log.info("【流水标签】解析规则参数: projectId={}, effectiveProjectId={}, ruleCode={}, paramModelCode={}, requiredParams={}",
|
||||
projectId, effectiveProjectId, ruleMeta.getRuleCode(), paramModelCode, requiredParamCodes);
|
||||
for (CcdiModelParam param : params) {
|
||||
if (requiredParamCodes.contains(param.getParamCode())) {
|
||||
thresholdValues.put(param.getParamCode(), param.getParamValue());
|
||||
|
||||
@@ -153,7 +153,7 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
|
||||
task.setUpdateBy(operator);
|
||||
task.setUpdateTime(new Date());
|
||||
updateFailedTaskSafely(task, ex);
|
||||
projectService.updateProjectStatus(projectId, CcdiProjectStatusConstants.PROCESSING, operator);
|
||||
projectService.updateProjectStatus(projectId, CcdiProjectStatusConstants.TAG_FAILED, operator);
|
||||
log.error("【流水标签】任务执行失败: taskId={}, projectId={}, modelCode={}, triggerType={}, error={}",
|
||||
task.getId(), projectId, modelCode, triggerType, ex.getMessage(), ex);
|
||||
throw ex;
|
||||
@@ -225,9 +225,15 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
|
||||
case "LARGE_TRANSFER" -> analysisMapper.selectLargeTransferStatements(
|
||||
projectId, toBigDecimal(config.getThresholdValue("FREQUENT_TRANSFER"))
|
||||
);
|
||||
case "EXTERNAL_SINGLE_LARGE_AMOUNT" -> analysisMapper.selectExternalSingleLargeAmountStatements(
|
||||
projectId, toBigDecimal(config.getThresholdValue("FREQUENT_TRANSFER"))
|
||||
);
|
||||
case "ABNORMAL_CUSTOMER_TRANSACTION" -> analysisMapper.selectAbnormalCustomerTransactionStatements(projectId);
|
||||
case "EXTERNAL_NIGHT_TRANSACTION" -> analysisMapper.selectExternalNightTransactionStatements(projectId);
|
||||
case "GAMBLING_SENSITIVE_KEYWORD" -> analysisMapper.selectGamblingSensitiveKeywordStatements(projectId);
|
||||
case "EXTERNAL_GAMBLING_MEMO" -> analysisMapper.selectExternalGamblingMemoStatements(projectId);
|
||||
case "SPECIAL_AMOUNT_TRANSACTION" -> analysisMapper.selectSpecialAmountTransactionStatements(projectId);
|
||||
case "EXTERNAL_TO_STAFF_FAMILY_TRANSACTION" -> analysisMapper.selectExternalToStaffOrFamilyTransactionStatements(projectId);
|
||||
case "SUSPICIOUS_INCOME_KEYWORD" -> analysisMapper.selectSuspiciousIncomeKeywordStatements(projectId);
|
||||
case "HOUSE_REGISTRATION_MISMATCH" -> analysisMapper.selectHouseRegistrationMismatchStatements(projectId);
|
||||
case "PROPERTY_FEE_REGISTRATION_MISMATCH" -> analysisMapper.selectPropertyFeeRegistrationMismatchStatements(projectId);
|
||||
@@ -258,9 +264,15 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
|
||||
case "CUMULATIVE_INCOME" -> analysisMapper.selectCumulativeIncomeObjects(
|
||||
projectId, toBigDecimal(config.getThresholdValue("CUMULATIVE_TRANSACTION_AMOUNT"))
|
||||
);
|
||||
case "EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT" -> analysisMapper.selectExternalCumulativeTransactionAmountObjects(
|
||||
projectId, toBigDecimal(config.getThresholdValue("CUMULATIVE_TRANSACTION_AMOUNT"))
|
||||
);
|
||||
case "ANNUAL_TURNOVER" -> analysisMapper.selectAnnualTurnoverObjects(
|
||||
projectId, toBigDecimal(config.getThresholdValue("ANNUAL_TURNOVER"))
|
||||
);
|
||||
case "EXTERNAL_ANNUAL_TURNOVER" -> analysisMapper.selectExternalAnnualTurnoverObjects(
|
||||
projectId, toBigDecimal(config.getThresholdValue("ANNUAL_TURNOVER"))
|
||||
);
|
||||
case "FREQUENT_CASH_DEPOSIT" -> analysisMapper.selectFrequentCashDepositObjects(
|
||||
projectId,
|
||||
toBigDecimal(config.getThresholdValue("LARGE_CASH_DEPOSIT")),
|
||||
@@ -272,6 +284,11 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
|
||||
toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MIN")),
|
||||
toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MAX"))
|
||||
);
|
||||
case "EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER" -> analysisMapper.selectExternalMultiPartyGamblingTransferObjects(
|
||||
projectId,
|
||||
toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MIN")),
|
||||
toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MAX"))
|
||||
);
|
||||
case "MONTHLY_FIXED_INCOME" -> analysisMapper.selectMonthlyFixedIncomeObjects(
|
||||
projectId, toBigDecimal(config.getThresholdValue("MONTHLY_FIXED_INCOME"))
|
||||
);
|
||||
@@ -285,7 +302,9 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
|
||||
case "WITHDRAW_CNT" -> analysisMapper.selectWithdrawCntObjects(
|
||||
projectId, toInteger(config.getThresholdValue("WITHDRAW_CNT"))
|
||||
);
|
||||
case "WITHDRAW_AMT" -> analysisMapper.selectWithdrawAmtObjects(projectId);
|
||||
case "WITHDRAW_AMT" -> analysisMapper.selectWithdrawAmtObjects(
|
||||
projectId, toBigDecimal(config.getThresholdValue("WITHDRAW_AMT"))
|
||||
);
|
||||
case "SALARY_QUICK_TRANSFER" -> analysisMapper.selectSalaryQuickTransferObjects(projectId);
|
||||
case "SALARY_UNUSED" -> analysisMapper.selectSalaryUnusedObjects(projectId);
|
||||
case "SUDDEN_ACCOUNT_CLOSURE" -> analysisMapper.selectSuddenAccountClosureObjects(projectId);
|
||||
|
||||
@@ -226,15 +226,18 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
CcdiFileUploadRecord record = recordMapper.selectById(id);
|
||||
validateDeleteRecord(record);
|
||||
|
||||
DeleteFilesRequest request = new DeleteFilesRequest();
|
||||
request.setGroupId(record.getLsfxProjectId());
|
||||
request.setLogIds(new Integer[]{record.getLogId()});
|
||||
request.setUserId(toUploadUserId(operatorUserId));
|
||||
|
||||
DeleteFilesResponse response = lsfxClient.deleteFiles(request);
|
||||
if (response == null || Boolean.FALSE.equals(response.getSuccessResponse())) {
|
||||
throw new RuntimeException("流水分析平台删除文件失败");
|
||||
}
|
||||
/*
|
||||
* 按当前要求,项目管理-上传数据页面删除时先不调用流水分析平台删除接口。
|
||||
* DeleteFilesRequest request = new DeleteFilesRequest();
|
||||
* request.setGroupId(record.getLsfxProjectId());
|
||||
* request.setLogIds(new Integer[]{record.getLogId()});
|
||||
* request.setUserId(toUploadUserId(operatorUserId));
|
||||
*
|
||||
* DeleteFilesResponse response = lsfxClient.deleteFiles(request);
|
||||
* if (response == null || Boolean.FALSE.equals(response.getSuccessResponse())) {
|
||||
* throw new RuntimeException("流水分析平台删除文件失败");
|
||||
* }
|
||||
*/
|
||||
|
||||
bankStatementMapper.deleteByProjectIdAndBatchId(record.getProjectId(), record.getLogId());
|
||||
refreshProjectTargetCount(record.getProjectId());
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
package com.ruoyi.ccdi.project.service.impl;
|
||||
|
||||
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.mapper.CcdiFundGraphMapper;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiFundGraphService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 资金流图谱Service实现
|
||||
*/
|
||||
@Service
|
||||
public class CcdiFundGraphServiceImpl implements ICcdiFundGraphService {
|
||||
|
||||
private static final int DEFAULT_LIMIT = 20;
|
||||
private static final int MAX_LIMIT = 100;
|
||||
private static final BigDecimal DEFAULT_MIN_TOTAL_AMOUNT = new BigDecimal("1000");
|
||||
private static final Comparator<CcdiFundGraphEdgeVO> EDGE_COMPARATOR = Comparator
|
||||
.comparing(CcdiFundGraphServiceImpl::safeAmount, Comparator.reverseOrder())
|
||||
.thenComparing(CcdiFundGraphServiceImpl::safeTransactionCount, Comparator.reverseOrder())
|
||||
.thenComparing(CcdiFundGraphServiceImpl::safeDateText, Comparator.reverseOrder())
|
||||
.thenComparing(edge -> normalizeSortText(edge == null ? null : edge.getEdgeKey()));
|
||||
|
||||
@Resource
|
||||
private CcdiFundGraphMapper fundGraphMapper;
|
||||
|
||||
@Override
|
||||
public List<CcdiFundGraphNodeVO> searchSubjects(CcdiFundGraphQueryDTO queryDTO) {
|
||||
CcdiFundGraphQueryDTO query = normalizeGraphQuery(queryDTO);
|
||||
if (isBlank(query.getKeyword()) && isBlank(query.getObjectKey())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return selectSubjects(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiFundGraphVO getFundGraph(CcdiFundGraphQueryDTO queryDTO) {
|
||||
CcdiFundGraphQueryDTO query = normalizeGraphQuery(queryDTO);
|
||||
CcdiFundGraphNodeVO centerNode = resolveCenterNode(query);
|
||||
if (centerNode == null || isBlank(centerNode.getObjectKey())) {
|
||||
return new CcdiFundGraphVO();
|
||||
}
|
||||
|
||||
query.setObjectKey(centerNode.getObjectKey());
|
||||
List<CcdiFundGraphEdgeVO> edges = new ArrayList<>();
|
||||
List<CcdiFundGraphEdgeVO> realEdges = fundGraphMapper.selectFundGraphEdges(query);
|
||||
if (realEdges != null) {
|
||||
edges.addAll(realEdges);
|
||||
}
|
||||
List<CcdiFundGraphEdgeVO> manualEdges = fundGraphMapper.selectFundGraphManualEdges(query);
|
||||
if (manualEdges != null) {
|
||||
edges.addAll(manualEdges);
|
||||
}
|
||||
edges = sortAndLimitEdges(edges, query.getLimit());
|
||||
CcdiFundGraphVO graph = new CcdiFundGraphVO();
|
||||
graph.setCenterNode(centerNode);
|
||||
graph.setEdges(edges);
|
||||
graph.setNodes(buildNodes(centerNode, edges));
|
||||
graph.setTransactionCount(edges.stream()
|
||||
.map(CcdiFundGraphEdgeVO::getTransactionCount)
|
||||
.filter(item -> item != null)
|
||||
.reduce(0L, Long::sum));
|
||||
graph.setTotalAmount(edges.stream()
|
||||
.map(CcdiFundGraphEdgeVO::getTotalAmount)
|
||||
.filter(item -> item != null)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
graph.setMaxDepth(1);
|
||||
graph.setTraceReserved(true);
|
||||
return graph;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<CcdiFundGraphStatementVO> getEdgeDetails(
|
||||
Page<CcdiFundGraphStatementVO> page,
|
||||
CcdiFundGraphEdgeDetailQueryDTO queryDTO
|
||||
) {
|
||||
CcdiFundGraphEdgeDetailQueryDTO query = normalizeDetailQuery(queryDTO);
|
||||
if (isBlank(query.getFromKey()) || isBlank(query.getToKey())) {
|
||||
return page;
|
||||
}
|
||||
return fundGraphMapper.selectFundGraphEdgeDetails(page, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiFundGraphEdgeVO saveManualEdge(CcdiFundGraphManualEdgeSaveDTO saveDTO, String operator) {
|
||||
CcdiFundGraphManualEdgeSaveDTO dto = normalizeManualEdge(saveDTO);
|
||||
String fromObjectKey = dto.getFromObjectKey();
|
||||
String toObjectKey = resolveManualToObjectKey(dto);
|
||||
dto.setToObjectKey(toObjectKey);
|
||||
|
||||
String edgeObjectKey = md5("MANUAL_EDGE|" + fromObjectKey + "|" + toObjectKey + "|" + dto.getDirection()
|
||||
+ "|" + UUID.randomUUID());
|
||||
fundGraphMapper.insertManualEdge(edgeObjectKey, dto, normalizeText(operator));
|
||||
|
||||
CcdiFundGraphEdgeVO edge = new CcdiFundGraphEdgeVO();
|
||||
edge.setEdgeKey(edgeObjectKey);
|
||||
edge.setFromKey(toSubjectKey(fromObjectKey));
|
||||
edge.setToKey(toSubjectKey(toObjectKey));
|
||||
edge.setFromObjectKey(fromObjectKey);
|
||||
edge.setToObjectKey(toObjectKey);
|
||||
edge.setFromName(dto.getFromName());
|
||||
edge.setToName(dto.getToName());
|
||||
edge.setTotalAmount(dto.getAmount());
|
||||
edge.setTransactionCount(dto.getTransactionCount() == null ? 1L : dto.getTransactionCount().longValue());
|
||||
edge.setDirection(dto.getDirection());
|
||||
edge.setRelationDesc(dto.getRelationDesc());
|
||||
edge.setSourceDesc(dto.getSourceDesc());
|
||||
edge.setRemark(dto.getRemark());
|
||||
edge.setSourceType("MANUAL");
|
||||
edge.setDepth(1);
|
||||
edge.setCanTrace(false);
|
||||
return edge;
|
||||
}
|
||||
|
||||
private CcdiFundGraphNodeVO resolveCenterNode(CcdiFundGraphQueryDTO query) {
|
||||
if (isBlank(query.getObjectKey()) && isBlank(query.getKeyword())) {
|
||||
return null;
|
||||
}
|
||||
List<CcdiFundGraphNodeVO> subjects = selectSubjects(query);
|
||||
if (subjects == null || subjects.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return subjects.get(0);
|
||||
}
|
||||
|
||||
private List<CcdiFundGraphNodeVO> selectSubjects(CcdiFundGraphQueryDTO query) {
|
||||
if (query == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (!isBlank(query.getObjectKey())) {
|
||||
return fundGraphMapper.selectFundGraphSubjects(query);
|
||||
}
|
||||
if (isBlank(query.getKeyword())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<CcdiFundGraphNodeVO> exactSubjects = fundGraphMapper.selectFundGraphSubjectsByExactKeyword(query);
|
||||
if (exactSubjects != null && !exactSubjects.isEmpty()) {
|
||||
return exactSubjects;
|
||||
}
|
||||
return fundGraphMapper.selectFundGraphSubjectsByName(query);
|
||||
}
|
||||
|
||||
private List<CcdiFundGraphNodeVO> buildNodes(CcdiFundGraphNodeVO centerNode, List<CcdiFundGraphEdgeVO> edges) {
|
||||
Map<String, CcdiFundGraphNodeVO> nodeMap = new LinkedHashMap<>();
|
||||
Map<String, CcdiFundGraphNodeVO> subjectCache = new LinkedHashMap<>();
|
||||
subjectCache.put(centerNode.getObjectKey(), centerNode);
|
||||
addNode(nodeMap, centerNode, centerNode.getNodeKey(), centerNode.getObjectKey(), centerNode.getNodeName(),
|
||||
centerNode.getRelationType(), centerNode.getCanExpand(), BigDecimal.ZERO, 0L);
|
||||
|
||||
for (CcdiFundGraphEdgeVO edge : edges) {
|
||||
String centerObjectKey = centerNode.getObjectKey();
|
||||
String fromRelationType = centerObjectKey != null && centerObjectKey.equals(edge.getFromObjectKey())
|
||||
? null
|
||||
: edge.getFamilyRelationType();
|
||||
String toRelationType = centerObjectKey != null && centerObjectKey.equals(edge.getToObjectKey())
|
||||
? null
|
||||
: edge.getFamilyRelationType();
|
||||
addNode(nodeMap, lookupSubject(edge.getFromObjectKey(), subjectCache), edge.getFromKey(),
|
||||
edge.getFromObjectKey(), edge.getFromName(), fromRelationType, true, edge.getTotalAmount(),
|
||||
edge.getTransactionCount());
|
||||
addNode(nodeMap, lookupSubject(edge.getToObjectKey(), subjectCache), edge.getToKey(),
|
||||
edge.getToObjectKey(), edge.getToName(), toRelationType, edge.getCanTrace(),
|
||||
edge.getTotalAmount(), edge.getTransactionCount());
|
||||
}
|
||||
return List.copyOf(nodeMap.values());
|
||||
}
|
||||
|
||||
private CcdiFundGraphNodeVO lookupSubject(String objectKey, Map<String, CcdiFundGraphNodeVO> subjectCache) {
|
||||
if (isBlank(objectKey)) {
|
||||
return null;
|
||||
}
|
||||
if (subjectCache.containsKey(objectKey)) {
|
||||
return subjectCache.get(objectKey);
|
||||
}
|
||||
CcdiFundGraphQueryDTO query = new CcdiFundGraphQueryDTO();
|
||||
query.setObjectKey(objectKey);
|
||||
query.setLimit(DEFAULT_LIMIT);
|
||||
List<CcdiFundGraphNodeVO> subjects = fundGraphMapper.selectFundGraphSubjects(query);
|
||||
CcdiFundGraphNodeVO subject = subjects == null || subjects.isEmpty() ? null : subjects.get(0);
|
||||
subjectCache.put(objectKey, subject);
|
||||
return subject;
|
||||
}
|
||||
|
||||
private String resolveManualToObjectKey(CcdiFundGraphManualEdgeSaveDTO dto) {
|
||||
if (!isBlank(dto.getToObjectKey())) {
|
||||
ensureManualSubject(dto.getToObjectKey(), dto.getToIdNo(), dto.getToName());
|
||||
return dto.getToObjectKey();
|
||||
}
|
||||
String objectKey = !isBlank(dto.getToIdNo())
|
||||
? md5(dto.getToIdNo())
|
||||
: md5("MANUAL_NODE|" + dto.getToName() + "|" + UUID.randomUUID());
|
||||
dto.setToObjectKey(objectKey);
|
||||
ensureManualSubject(objectKey, dto.getToIdNo(), dto.getToName());
|
||||
return objectKey;
|
||||
}
|
||||
|
||||
private void ensureManualSubject(String objectKey, String idNo, String name) {
|
||||
if (fundGraphMapper.countSubjectByObjectKey(objectKey) > 0) {
|
||||
return;
|
||||
}
|
||||
fundGraphMapper.insertManualSubject(objectKey, normalizeText(idNo), normalizeText(name));
|
||||
}
|
||||
|
||||
private CcdiFundGraphManualEdgeSaveDTO normalizeManualEdge(CcdiFundGraphManualEdgeSaveDTO saveDTO) {
|
||||
CcdiFundGraphManualEdgeSaveDTO dto = saveDTO == null ? new CcdiFundGraphManualEdgeSaveDTO() : saveDTO;
|
||||
dto.setFromObjectKey(normalizeText(dto.getFromObjectKey()));
|
||||
dto.setFromName(normalizeText(dto.getFromName()));
|
||||
dto.setToObjectKey(normalizeText(dto.getToObjectKey()));
|
||||
dto.setToName(normalizeText(dto.getToName()));
|
||||
dto.setToIdNo(normalizeText(dto.getToIdNo()));
|
||||
dto.setDirection(normalizeText(dto.getDirection()));
|
||||
dto.setRelationDesc(normalizeText(dto.getRelationDesc()));
|
||||
dto.setSourceDesc(normalizeText(dto.getSourceDesc()));
|
||||
dto.setRemark(normalizeText(dto.getRemark()));
|
||||
if (isBlank(dto.getFromObjectKey())) {
|
||||
throw new IllegalArgumentException("起点主体不能为空");
|
||||
}
|
||||
if (isBlank(dto.getToObjectKey()) && isBlank(dto.getToName())) {
|
||||
throw new IllegalArgumentException("终点主体不能为空");
|
||||
}
|
||||
if (isBlank(dto.getDirection())) {
|
||||
dto.setDirection("1");
|
||||
}
|
||||
if (dto.getAmount() == null) {
|
||||
dto.setAmount(BigDecimal.ZERO);
|
||||
}
|
||||
if (dto.getTransactionCount() == null || dto.getTransactionCount() <= 0) {
|
||||
dto.setTransactionCount(1);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void addNode(
|
||||
Map<String, CcdiFundGraphNodeVO> nodeMap,
|
||||
CcdiFundGraphNodeVO subject,
|
||||
String nodeKey,
|
||||
String objectKey,
|
||||
String nodeName,
|
||||
String relationType,
|
||||
Boolean canExpand,
|
||||
BigDecimal edgeAmount,
|
||||
Long edgeCount
|
||||
) {
|
||||
if (isBlank(nodeKey)) {
|
||||
return;
|
||||
}
|
||||
CcdiFundGraphNodeVO node = nodeMap.computeIfAbsent(nodeKey, key -> {
|
||||
CcdiFundGraphNodeVO item = new CcdiFundGraphNodeVO();
|
||||
item.setNodeKey(key);
|
||||
item.setObjectKey(objectKey);
|
||||
item.setNodeName(subject != null && !isBlank(subject.getNodeName())
|
||||
? subject.getNodeName()
|
||||
: (isBlank(nodeName) ? "未知主体" : nodeName));
|
||||
item.setIdNo(subject == null ? null : subject.getIdNo());
|
||||
item.setCinocsno(subject == null ? null : subject.getCinocsno());
|
||||
item.setIdnoType(subject == null ? null : subject.getIdnoType());
|
||||
item.setStaffId(subject == null ? null : subject.getStaffId());
|
||||
item.setSourceType(subject == null ? null : subject.getSourceType());
|
||||
item.setNodeType(subject != null && !isBlank(subject.getNodeType()) ? subject.getNodeType() : "PERSON");
|
||||
item.setIdentityType(subject != null && !isBlank(subject.getIdentityType()) ? subject.getIdentityType() : "IDNO");
|
||||
item.setRelationType(relationType);
|
||||
item.setAccountCount(subject == null ? 0L : subject.getAccountCount());
|
||||
item.setCreatedTime(subject == null ? null : subject.getCreatedTime());
|
||||
item.setUpdatedTime(subject == null ? null : subject.getUpdatedTime());
|
||||
item.setCanExpand(Boolean.TRUE.equals(canExpand));
|
||||
item.setDepth(1);
|
||||
item.setTotalAmount(BigDecimal.ZERO);
|
||||
item.setTransactionCount(0L);
|
||||
return item;
|
||||
});
|
||||
if (isBlank(node.getObjectKey())) {
|
||||
node.setObjectKey(objectKey);
|
||||
}
|
||||
if (isBlank(node.getRelationType())) {
|
||||
node.setRelationType(relationType);
|
||||
}
|
||||
if (Boolean.TRUE.equals(canExpand)) {
|
||||
node.setCanExpand(true);
|
||||
}
|
||||
node.setTotalAmount(node.getTotalAmount().add(edgeAmount == null ? BigDecimal.ZERO : edgeAmount));
|
||||
node.setTransactionCount(node.getTransactionCount() + (edgeCount == null ? 0L : edgeCount));
|
||||
}
|
||||
|
||||
private CcdiFundGraphQueryDTO normalizeGraphQuery(CcdiFundGraphQueryDTO queryDTO) {
|
||||
CcdiFundGraphQueryDTO query = queryDTO == null ? new CcdiFundGraphQueryDTO() : queryDTO;
|
||||
query.setKeyword(normalizeText(query.getKeyword()));
|
||||
query.setObjectKey(normalizeText(query.getObjectKey()));
|
||||
query.setTransactionStartTime(normalizeText(query.getTransactionStartTime()));
|
||||
query.setTransactionEndTime(normalizeText(query.getTransactionEndTime()));
|
||||
query.setDirection(normalizeText(query.getDirection()));
|
||||
if (query.getMinTotalAmount() == null) {
|
||||
query.setMinTotalAmount(DEFAULT_MIN_TOTAL_AMOUNT);
|
||||
}
|
||||
query.setLimit(normalizeLimit(query.getLimit()));
|
||||
query.setDepth(1);
|
||||
return query;
|
||||
}
|
||||
|
||||
private CcdiFundGraphEdgeDetailQueryDTO normalizeDetailQuery(CcdiFundGraphEdgeDetailQueryDTO queryDTO) {
|
||||
CcdiFundGraphEdgeDetailQueryDTO query = queryDTO == null ? new CcdiFundGraphEdgeDetailQueryDTO() : queryDTO;
|
||||
query.setKeyword(normalizeText(query.getKeyword()));
|
||||
query.setFromKey(normalizeText(query.getFromKey()));
|
||||
query.setToKey(normalizeText(query.getToKey()));
|
||||
query.setDirection(normalizeText(query.getDirection()));
|
||||
query.setTransactionStartTime(normalizeText(query.getTransactionStartTime()));
|
||||
query.setTransactionEndTime(normalizeText(query.getTransactionEndTime()));
|
||||
return query;
|
||||
}
|
||||
|
||||
private Integer normalizeLimit(Integer limit) {
|
||||
if (limit == null || limit <= 0) {
|
||||
return DEFAULT_LIMIT;
|
||||
}
|
||||
return Math.min(limit, MAX_LIMIT);
|
||||
}
|
||||
|
||||
private List<CcdiFundGraphEdgeVO> sortAndLimitEdges(List<CcdiFundGraphEdgeVO> edges, Integer limit) {
|
||||
if (edges == null || edges.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<CcdiFundGraphEdgeVO> sorted = new ArrayList<>(edges);
|
||||
sorted.sort(EDGE_COMPARATOR);
|
||||
int finalLimit = normalizeLimit(limit);
|
||||
if (sorted.size() > finalLimit) {
|
||||
return List.copyOf(sorted.subList(0, finalLimit));
|
||||
}
|
||||
return List.copyOf(sorted);
|
||||
}
|
||||
|
||||
private String normalizeText(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private String toSubjectKey(String objectKey) {
|
||||
return "idno_node/" + objectKey;
|
||||
}
|
||||
|
||||
private String md5(String value) {
|
||||
return DigestUtils.md5DigestAsHex(value.trim().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static BigDecimal safeAmount(CcdiFundGraphEdgeVO edge) {
|
||||
return edge == null || edge.getTotalAmount() == null ? BigDecimal.ZERO : edge.getTotalAmount();
|
||||
}
|
||||
|
||||
private static Long safeTransactionCount(CcdiFundGraphEdgeVO edge) {
|
||||
return edge == null || edge.getTransactionCount() == null ? 0L : edge.getTransactionCount();
|
||||
}
|
||||
|
||||
private static String safeDateText(CcdiFundGraphEdgeVO edge) {
|
||||
return normalizeSortText(edge == null ? null : edge.getLastTrxDate());
|
||||
}
|
||||
|
||||
private static String normalizeSortText(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -53,6 +54,7 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService {
|
||||
private ICcdiProjectService projectService;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
private ICcdiBankTagService bankTagService;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ruoyi.ccdi.project.service.impl;
|
||||
|
||||
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 com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportParamVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
|
||||
@@ -65,7 +66,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
writeCover(writer, report);
|
||||
writeUploadSubjects(writer, report.getUploadSubjects());
|
||||
writeParams(writer, report.getParams());
|
||||
writeRiskModels(writer, report);
|
||||
writeRiskOverview(writer, report);
|
||||
writeRiskDetails(writer, report);
|
||||
writer.close();
|
||||
document.save(response.getOutputStream());
|
||||
@@ -118,9 +119,9 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
);
|
||||
}
|
||||
|
||||
private void writeRiskModels(PdfPageWriter writer, CcdiProjectOverviewReportVO report) throws IOException {
|
||||
writer.section("三、风险模型");
|
||||
writer.metrics(report.getDashboard().getStats());
|
||||
private void writeRiskOverview(PdfPageWriter writer, CcdiProjectOverviewReportVO report) throws IOException {
|
||||
writer.section("三、风险总览");
|
||||
writer.metrics(buildOverallRiskMetrics(report));
|
||||
writer.subsection("风险模型汇总");
|
||||
writer.table(
|
||||
List.of("模型名称", "预警数量", "涉及人员"),
|
||||
@@ -151,6 +152,40 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
new float[] { 0.1F, 0.11F, 0.16F, 0.14F, 0.24F, 0.25F },
|
||||
"暂无风险人员与异常点数据"
|
||||
);
|
||||
|
||||
if (hasExternalRisk(report)) {
|
||||
writer.subsection("外部人员预警");
|
||||
writer.metrics(buildExternalMetrics(report));
|
||||
writer.table(
|
||||
List.of("外部模型", "预警数量", "涉及人数"),
|
||||
report.getExternalModelSummaries().stream()
|
||||
.map(item -> List.of(
|
||||
safeText(item.getModelName()),
|
||||
String.valueOf(defaultZero(item.getWarningCount())),
|
||||
formatCount(item.getPeopleCount(), "人")
|
||||
))
|
||||
.collect(Collectors.toList()),
|
||||
new float[] { 0.5F, 0.2F, 0.3F },
|
||||
"暂无外部人员模型汇总数据"
|
||||
);
|
||||
writer.table(
|
||||
List.of("姓名", "证件号", "主体类型", "风险等级", "命中模型数", "核心异常点", "涉及对象", "最近交易时间"),
|
||||
report.getExternalPersonWarnings().stream()
|
||||
.map(item -> List.of(
|
||||
safeText(item.getName()),
|
||||
maskIdCard(item.getIdNo()),
|
||||
safeText(item.getSubjectType()),
|
||||
safeText(item.getRiskLevel()),
|
||||
String.valueOf(defaultZero(item.getModelCount())),
|
||||
safeText(item.getRiskPoint()),
|
||||
safeText(item.getRelatedObject()),
|
||||
safeText(item.getLatestTradeTime())
|
||||
))
|
||||
.collect(Collectors.toList()),
|
||||
new float[] { 0.09F, 0.15F, 0.1F, 0.09F, 0.1F, 0.25F, 0.12F, 0.1F },
|
||||
"暂无外部人员预警数据"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeRiskDetails(PdfPageWriter writer, CcdiProjectOverviewReportVO report) throws IOException {
|
||||
@@ -328,6 +363,69 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<CcdiProjectOverviewStatVO> buildOverallRiskMetrics(CcdiProjectOverviewReportVO report) {
|
||||
List<CcdiProjectOverviewStatVO> employeeStats = report.getDashboard().getStats();
|
||||
if (employeeStats == null || employeeStats.isEmpty()) {
|
||||
return List.of(
|
||||
buildMetric("总人数", report.getExternalRiskSummary().getTotal()),
|
||||
buildMetric("高风险", report.getExternalRiskSummary().getHigh()),
|
||||
buildMetric("中风险", report.getExternalRiskSummary().getMedium()),
|
||||
buildMetric("低风险", report.getExternalRiskSummary().getLow()),
|
||||
buildMetric("无风险", report.getExternalRiskSummary().getNoRisk())
|
||||
);
|
||||
}
|
||||
int employeeTotal = metricValue(employeeStats, "people");
|
||||
int high = metricValue(employeeStats, "riskPeople");
|
||||
int medium = metricValue(employeeStats, "medium");
|
||||
int low = metricValue(employeeStats, "low");
|
||||
int noRisk = metricValue(employeeStats, "count");
|
||||
int externalTotal = defaultZero(report.getExternalRiskSummary().getTotal());
|
||||
int externalHigh = defaultZero(report.getExternalRiskSummary().getHigh());
|
||||
int externalMedium = defaultZero(report.getExternalRiskSummary().getMedium());
|
||||
int externalLow = defaultZero(report.getExternalRiskSummary().getLow());
|
||||
int externalNoRisk = defaultZero(report.getExternalRiskSummary().getNoRisk());
|
||||
return List.of(
|
||||
buildMetric("总人数", employeeTotal + externalTotal),
|
||||
buildMetric("高风险", high + externalHigh),
|
||||
buildMetric("中风险", medium + externalMedium),
|
||||
buildMetric("低风险", low + externalLow),
|
||||
buildMetric("无风险", noRisk + externalNoRisk)
|
||||
);
|
||||
}
|
||||
|
||||
private Integer metricValue(List<CcdiProjectOverviewStatVO> stats, String key) {
|
||||
return defaultZero(stats.stream()
|
||||
.filter(stat -> key.equals(stat.getKey()))
|
||||
.findFirst()
|
||||
.map(CcdiProjectOverviewStatVO::getValue)
|
||||
.orElse(0));
|
||||
}
|
||||
|
||||
private boolean hasExternalRisk(CcdiProjectOverviewReportVO report) {
|
||||
return defaultZero(report.getExternalRiskSummary().getHigh()) > 0
|
||||
|| defaultZero(report.getExternalRiskSummary().getMedium()) > 0
|
||||
|| defaultZero(report.getExternalRiskSummary().getLow()) > 0
|
||||
|| !report.getExternalModelSummaries().isEmpty()
|
||||
|| !report.getExternalPersonWarnings().isEmpty();
|
||||
}
|
||||
|
||||
private List<CcdiProjectOverviewStatVO> buildExternalMetrics(CcdiProjectOverviewReportVO report) {
|
||||
return List.of(
|
||||
buildMetric("外部人员", report.getExternalRiskSummary().getTotal()),
|
||||
buildMetric("高风险", report.getExternalRiskSummary().getHigh()),
|
||||
buildMetric("中风险", report.getExternalRiskSummary().getMedium()),
|
||||
buildMetric("低风险", report.getExternalRiskSummary().getLow()),
|
||||
buildMetric("无风险人员", report.getExternalRiskSummary().getNoRisk())
|
||||
);
|
||||
}
|
||||
|
||||
private CcdiProjectOverviewStatVO buildMetric(String label, Integer value) {
|
||||
CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
|
||||
stat.setLabel(label);
|
||||
stat.setValue(defaultZero(value));
|
||||
return stat;
|
||||
}
|
||||
|
||||
private String formatPeopleSummary(CcdiProjectOverviewReportModelSummaryVO item) {
|
||||
String names = safeText(item.getPeopleNames());
|
||||
if ("-".equals(names)) {
|
||||
@@ -469,6 +567,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
private static final float SUBSECTION_FONT_SIZE = 12F;
|
||||
private static final float LINE_HEIGHT = 12F;
|
||||
private static final float CELL_PADDING = 5F;
|
||||
private static final float TABLE_AFTER_GAP = 32F;
|
||||
|
||||
private final PDDocument document;
|
||||
private final PDType0Font font;
|
||||
@@ -496,7 +595,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
}
|
||||
|
||||
void title(String text) throws IOException {
|
||||
writeLine(text, TITLE_FONT_SIZE, new Color(18, 56, 93), 0F, 28F);
|
||||
writeLine(text, TITLE_FONT_SIZE, new Color(18, 56, 93), 0F, 28F, true);
|
||||
}
|
||||
|
||||
void text(String text, float fontSize, Color color) throws IOException {
|
||||
@@ -505,12 +604,12 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
|
||||
void section(String text) throws IOException {
|
||||
ensureSpace(32F);
|
||||
writeLine(text, SECTION_FONT_SIZE, new Color(18, 56, 93), 0F, 26F);
|
||||
writeLine(text, SECTION_FONT_SIZE, new Color(18, 56, 93), 0F, 26F, true);
|
||||
}
|
||||
|
||||
void subsection(String text) throws IOException {
|
||||
ensureSpace(26F);
|
||||
writeLine(text, SUBSECTION_FONT_SIZE, new Color(51, 65, 85), 0F, 22F);
|
||||
writeLine(text, SUBSECTION_FONT_SIZE, new Color(51, 65, 85), 0F, 22F, true);
|
||||
}
|
||||
|
||||
void separator() throws IOException {
|
||||
@@ -557,7 +656,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
for (List<String> row : safeRows) {
|
||||
drawRow(row, widths, false);
|
||||
}
|
||||
y -= 8F;
|
||||
y -= TABLE_AFTER_GAP;
|
||||
}
|
||||
|
||||
private float[] calculateWidths(float[] ratios) {
|
||||
@@ -632,6 +731,17 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
}
|
||||
|
||||
private void writeLine(String text, float fontSize, Color color, float indent, float advance) throws IOException {
|
||||
writeLine(text, fontSize, color, indent, advance, false);
|
||||
}
|
||||
|
||||
private void writeLine(
|
||||
String text,
|
||||
float fontSize,
|
||||
Color color,
|
||||
float indent,
|
||||
float advance,
|
||||
boolean bold
|
||||
) throws IOException {
|
||||
ensureSpace(advance);
|
||||
content.beginText();
|
||||
content.setNonStrokingColor(color);
|
||||
@@ -639,6 +749,14 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
content.newLineAtOffset(MARGIN + indent, y);
|
||||
content.showText(text);
|
||||
content.endText();
|
||||
if (bold) {
|
||||
content.beginText();
|
||||
content.setNonStrokingColor(color);
|
||||
content.setFont(font, fontSize);
|
||||
content.newLineAtOffset(MARGIN + indent + 0.25F, y);
|
||||
content.showText(text);
|
||||
content.endText();
|
||||
}
|
||||
y -= advance;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,16 @@ import com.ruoyi.ccdi.project.domain.CcdiModelParam;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
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.CcdiProjectSuspiciousTransactionQueryDTO;
|
||||
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 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.entity.CcdiProjectOverviewEmployeeResult;
|
||||
@@ -26,14 +30,21 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisAbnormalGroupVO
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportParamVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
|
||||
@@ -41,6 +52,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
|
||||
@@ -74,6 +86,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
@Resource
|
||||
private CcdiProjectMapper projectMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiBankStatementMapper bankStatementMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiModelParamMapper modelParamMapper;
|
||||
|
||||
@@ -212,6 +227,108 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
return people;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CcdiProjectRiskModelPeopleExcel> exportRiskModelPeople(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
normalizeRiskModelPeopleQuery(queryDTO);
|
||||
|
||||
return defaultList(overviewMapper.selectRiskModelPeopleList(queryDTO)).stream()
|
||||
.map(item -> buildRiskModelPeopleExcelRow(item, "员工"))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiProjectExternalPersonWarningVO getExternalPersonWarnings(CcdiProjectExternalPersonQueryDTO queryDTO) {
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
|
||||
Page<CcdiProjectExternalPersonWarningItemVO> page = new Page<>(
|
||||
defaultRiskPeoplePageNum(queryDTO.getPageNum()),
|
||||
defaultRiskPeoplePageSize(queryDTO.getPageSize())
|
||||
);
|
||||
Page<CcdiProjectExternalPersonWarningItemVO> resultPage =
|
||||
overviewMapper.selectExternalPersonWarningPage(page, queryDTO);
|
||||
|
||||
List<CcdiProjectExternalPersonWarningItemVO> rows =
|
||||
defaultList(resultPage == null ? null : resultPage.getRecords()).stream()
|
||||
.peek(item -> item.setActionLabel(ACTION_LABEL))
|
||||
.toList();
|
||||
|
||||
CcdiProjectExternalPersonWarningVO warnings = new CcdiProjectExternalPersonWarningVO();
|
||||
warnings.setRows(rows);
|
||||
warnings.setTotal(resultPage == null ? 0L : resultPage.getTotal());
|
||||
warnings.setPageNum(page.getCurrent());
|
||||
warnings.setPageSize(page.getSize());
|
||||
return warnings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiProjectExternalRiskSummaryVO getExternalRiskSummary(Long projectId) {
|
||||
ensureProjectExists(projectId);
|
||||
CcdiProjectExternalRiskSummaryVO summary = overviewMapper.selectExternalRiskSummaryByProjectId(projectId);
|
||||
if (summary == null) {
|
||||
return new CcdiProjectExternalRiskSummaryVO();
|
||||
}
|
||||
summary.setTotal(defaultZero(summary.getTotal()));
|
||||
summary.setHigh(defaultZero(summary.getHigh()));
|
||||
summary.setMedium(defaultZero(summary.getMedium()));
|
||||
summary.setLow(defaultZero(summary.getLow()));
|
||||
summary.setNoRisk(defaultZero(summary.getNoRisk()));
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CcdiProjectExternalPersonWarningExcel> exportExternalPersonWarnings(Long projectId) {
|
||||
ensureProjectExists(projectId);
|
||||
|
||||
return defaultList(overviewMapper.selectExternalPersonWarningList(projectId)).stream()
|
||||
.map(this::buildExternalPersonWarningExcelRow)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiProjectRiskModelCardsVO getExternalRiskModelCards(Long projectId) {
|
||||
ensureProjectExists(projectId);
|
||||
|
||||
CcdiProjectRiskModelCardsVO cards = new CcdiProjectRiskModelCardsVO();
|
||||
cards.setCardList(defaultList(overviewMapper.selectExternalRiskModelCardsByProjectId(projectId)));
|
||||
return cards;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiProjectRiskModelPeopleVO getExternalRiskModelPeople(CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO) {
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
normalizeExternalRiskModelPeopleQuery(queryDTO);
|
||||
|
||||
Page<CcdiProjectRiskModelPeopleItemVO> page = new Page<>(
|
||||
defaultPageNum(queryDTO.getPageNum()),
|
||||
defaultPageSize(queryDTO.getPageSize())
|
||||
);
|
||||
Page<CcdiProjectRiskModelPeopleItemVO> resultPage =
|
||||
overviewMapper.selectExternalRiskModelPeoplePage(page, queryDTO);
|
||||
|
||||
List<CcdiProjectRiskModelPeopleItemVO> rows = defaultList(resultPage == null ? null : resultPage.getRecords())
|
||||
.stream()
|
||||
.peek(item -> item.setActionLabel(ACTION_LABEL))
|
||||
.toList();
|
||||
|
||||
CcdiProjectRiskModelPeopleVO people = new CcdiProjectRiskModelPeopleVO();
|
||||
people.setRows(rows);
|
||||
people.setTotal(resultPage == null ? 0L : resultPage.getTotal());
|
||||
return people;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CcdiProjectRiskModelPeopleExcel> exportExternalRiskModelPeople(
|
||||
CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO
|
||||
) {
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
normalizeExternalRiskModelPeopleQuery(queryDTO);
|
||||
|
||||
return defaultList(overviewMapper.selectExternalRiskModelPeopleList(queryDTO)).stream()
|
||||
.map(item -> buildRiskModelPeopleExcelRow(item, item.getStaffCode()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiProjectSuspiciousTransactionPageVO getSuspiciousTransactions(
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
|
||||
@@ -239,7 +356,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
normalizeSuspiciousTransactionQuery(queryDTO);
|
||||
|
||||
return defaultList(overviewMapper.selectSuspiciousTransactionList(queryDTO)).stream()
|
||||
return defaultList(overviewMapper.selectReportSuspiciousTransactionList(queryDTO)).stream()
|
||||
.map(this::buildSuspiciousTransactionExcelRow)
|
||||
.toList();
|
||||
}
|
||||
@@ -330,9 +447,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
.toList());
|
||||
report.setParams(buildReportParams(project));
|
||||
report.setModelSummaries(defaultList(overviewMapper.selectReportRiskModelSummaries(projectId)));
|
||||
report.setExternalRiskSummary(getExternalRiskSummary(projectId));
|
||||
report.setExternalModelSummaries(buildExternalReportModelSummaries(projectId));
|
||||
report.setRiskPeople(defaultList(overviewMapper.selectReportRiskPeople(projectId)).stream()
|
||||
.peek(item -> item.setActionLabel(ACTION_LABEL))
|
||||
.toList());
|
||||
report.setExternalPersonWarnings(exportExternalPersonWarnings(projectId));
|
||||
report.setSuspiciousTransactions(defaultList(
|
||||
overviewMapper.selectReportSuspiciousTransactionList(suspiciousQuery)
|
||||
));
|
||||
@@ -371,6 +491,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
|
||||
projectMapper.updateRiskCountsByProjectId(
|
||||
projectId,
|
||||
countProjectScopeStaff(projectId),
|
||||
countRiskLevel(results, "HIGH"),
|
||||
countRiskLevel(results, "MEDIUM"),
|
||||
countRiskLevel(results, "LOW"),
|
||||
@@ -385,6 +506,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
Map<String, Object> summary = overviewMapper.selectRiskCountSummaryByProjectId(projectId);
|
||||
projectMapper.updateRiskCountsByProjectId(
|
||||
projectId,
|
||||
countProjectScopeStaff(projectId),
|
||||
readCount(summary, "highRiskCount"),
|
||||
readCount(summary, "mediumRiskCount"),
|
||||
readCount(summary, "lowRiskCount"),
|
||||
@@ -392,6 +514,10 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
);
|
||||
}
|
||||
|
||||
private int countProjectScopeStaff(Long projectId) {
|
||||
return defaultZero(bankStatementMapper.countMatchedStaffCountByProjectId(projectId));
|
||||
}
|
||||
|
||||
private CcdiProjectRiskPeopleOverviewItemVO buildRiskPeopleItem(Long projectId, CcdiProjectEmployeeRiskAggregateVO aggregate) {
|
||||
CcdiProjectRiskPeopleOverviewItemVO item = new CcdiProjectRiskPeopleOverviewItemVO();
|
||||
item.setName(aggregate.getStaffName());
|
||||
@@ -433,6 +559,51 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
return row;
|
||||
}
|
||||
|
||||
private CcdiProjectExternalPersonWarningExcel buildExternalPersonWarningExcelRow(
|
||||
CcdiProjectExternalPersonWarningItemVO item
|
||||
) {
|
||||
CcdiProjectExternalPersonWarningExcel row = new CcdiProjectExternalPersonWarningExcel();
|
||||
row.setName(item.getName());
|
||||
row.setIdNo(item.getIdNo());
|
||||
row.setSubjectType(item.getSubjectType());
|
||||
row.setRiskLevel(item.getRiskLevel());
|
||||
row.setModelCount(item.getModelCount());
|
||||
row.setRiskPoint(item.getRiskPoint());
|
||||
row.setRelatedObject(item.getRelatedObject());
|
||||
row.setLatestTradeTime(item.getLatestTradeTime());
|
||||
return row;
|
||||
}
|
||||
|
||||
private List<CcdiProjectOverviewReportModelSummaryVO> buildExternalReportModelSummaries(Long projectId) {
|
||||
return defaultList(overviewMapper.selectExternalRiskModelCardsByProjectId(projectId)).stream()
|
||||
.map(this::buildExternalReportModelSummary)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private CcdiProjectOverviewReportModelSummaryVO buildExternalReportModelSummary(CcdiProjectRiskModelCardVO card) {
|
||||
CcdiProjectOverviewReportModelSummaryVO row = new CcdiProjectOverviewReportModelSummaryVO();
|
||||
row.setModelCode(card.getModelCode());
|
||||
row.setModelName(card.getModelName());
|
||||
row.setWarningCount(card.getWarningCount());
|
||||
row.setPeopleCount(card.getPeopleCount());
|
||||
row.setPeopleNames("-");
|
||||
return row;
|
||||
}
|
||||
|
||||
private CcdiProjectRiskModelPeopleExcel buildRiskModelPeopleExcelRow(
|
||||
CcdiProjectRiskModelPeopleItemVO item,
|
||||
String subjectType
|
||||
) {
|
||||
CcdiProjectRiskModelPeopleExcel row = new CcdiProjectRiskModelPeopleExcel();
|
||||
row.setPersonName(item.getStaffName());
|
||||
row.setSubjectType(subjectType);
|
||||
row.setIdNo(item.getIdNo());
|
||||
row.setScopeName(item.getDepartment());
|
||||
row.setModelNames(joinModelNames(item.getModelNames()));
|
||||
row.setHitTags(joinHitTagNames(item.getHitTagList()));
|
||||
return row;
|
||||
}
|
||||
|
||||
private void ensureProjectExists(Long projectId) {
|
||||
getRequiredProject(projectId);
|
||||
}
|
||||
@@ -445,6 +616,14 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
queryDTO.setMatchMode(queryDTO.getMatchMode().trim().toUpperCase());
|
||||
}
|
||||
|
||||
private void normalizeExternalRiskModelPeopleQuery(CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO) {
|
||||
if (queryDTO.getMatchMode() == null || queryDTO.getMatchMode().isBlank()) {
|
||||
queryDTO.setMatchMode("ANY");
|
||||
return;
|
||||
}
|
||||
queryDTO.setMatchMode(queryDTO.getMatchMode().trim().toUpperCase());
|
||||
}
|
||||
|
||||
private void normalizeSuspiciousTransactionQuery(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
|
||||
if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) {
|
||||
queryDTO.setSuspiciousType("ALL");
|
||||
@@ -514,15 +693,18 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
}
|
||||
|
||||
private CcdiProjectSuspiciousTransactionExcel buildSuspiciousTransactionExcelRow(
|
||||
CcdiProjectSuspiciousTransactionItemVO item
|
||||
CcdiProjectOverviewReportSuspiciousTransactionVO item
|
||||
) {
|
||||
CcdiProjectSuspiciousTransactionExcel row = new CcdiProjectSuspiciousTransactionExcel();
|
||||
row.setTrxDate(item.getTrxDate());
|
||||
row.setSuspiciousPersonName(item.getSuspiciousPersonName());
|
||||
row.setRelatedPersonName(item.getRelatedPersonName());
|
||||
row.setLeAccountNo(item.getLeAccountNo());
|
||||
row.setLeAccountName(item.getLeAccountName());
|
||||
row.setCustomerAccountName(item.getCustomerAccountName());
|
||||
row.setCustomerAccountNo(item.getCustomerAccountNo());
|
||||
row.setRelatedStaffDisplay(formatRelatedStaff(item.getRelatedStaffName(), item.getRelatedStaffCode()));
|
||||
row.setRelationType(item.getRelationType());
|
||||
row.setSummaryAndCashType(formatSummaryAndCashType(item.getUserMemo(), item.getCashType()));
|
||||
row.setUserMemo(item.getUserMemo());
|
||||
row.setCashType(item.getCashType());
|
||||
row.setHitTags(item.getHitTags());
|
||||
row.setDisplayAmount(item.getDisplayAmount());
|
||||
return row;
|
||||
}
|
||||
@@ -597,6 +779,21 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
return safeMemo + "/" + safeCashType;
|
||||
}
|
||||
|
||||
private String joinModelNames(List<String> modelNames) {
|
||||
return defaultList(modelNames).stream()
|
||||
.filter(item -> item != null && !item.isBlank())
|
||||
.distinct()
|
||||
.collect(Collectors.joining("、"));
|
||||
}
|
||||
|
||||
private String joinHitTagNames(List<CcdiProjectRiskHitTagVO> hitTags) {
|
||||
return defaultList(hitTags).stream()
|
||||
.map(CcdiProjectRiskHitTagVO::getRuleName)
|
||||
.filter(item -> item != null && !item.isBlank())
|
||||
.distinct()
|
||||
.collect(Collectors.joining("、"));
|
||||
}
|
||||
|
||||
private CcdiProjectPersonAnalysisAbnormalDetailVO buildAbnormalDetail(
|
||||
List<CcdiBankStatementListVO> statementRows,
|
||||
List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows
|
||||
|
||||
@@ -44,19 +44,33 @@ public class CcdiProjectRiskDetailWorkbookExporter {
|
||||
|
||||
private void writeSuspiciousSheet(Sheet sheet, List<CcdiProjectSuspiciousTransactionExcel> rows) {
|
||||
Row header = sheet.createRow(0);
|
||||
String[] headers = { "交易时间", "可疑人员", "关联人", "关联员工", "关系", "摘要/交易类型", "交易金额" };
|
||||
String[] headers = {
|
||||
"交易时间",
|
||||
"本方账户",
|
||||
"本方主体",
|
||||
"对方名称",
|
||||
"对方账户",
|
||||
"关联员工",
|
||||
"摘要",
|
||||
"交易类型",
|
||||
"异常标签",
|
||||
"交易金额"
|
||||
};
|
||||
writeHeader(header, headers);
|
||||
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
CcdiProjectSuspiciousTransactionExcel item = rows.get(i);
|
||||
Row row = sheet.createRow(i + 1);
|
||||
row.createCell(0).setCellValue(safeText(item.getTrxDate()));
|
||||
row.createCell(1).setCellValue(safeText(item.getSuspiciousPersonName()));
|
||||
row.createCell(2).setCellValue(safeText(item.getRelatedPersonName()));
|
||||
row.createCell(3).setCellValue(safeText(item.getRelatedStaffDisplay()));
|
||||
row.createCell(4).setCellValue(safeText(item.getRelationType()));
|
||||
row.createCell(5).setCellValue(safeText(item.getSummaryAndCashType()));
|
||||
row.createCell(6).setCellValue(safeNumber(item.getDisplayAmount()));
|
||||
row.createCell(1).setCellValue(safeText(item.getLeAccountNo()));
|
||||
row.createCell(2).setCellValue(safeText(item.getLeAccountName()));
|
||||
row.createCell(3).setCellValue(safeText(item.getCustomerAccountName()));
|
||||
row.createCell(4).setCellValue(safeText(item.getCustomerAccountNo()));
|
||||
row.createCell(5).setCellValue(safeText(item.getRelatedStaffDisplay()));
|
||||
row.createCell(6).setCellValue(safeText(item.getUserMemo()));
|
||||
row.createCell(7).setCellValue(safeText(item.getCashType()));
|
||||
row.createCell(8).setCellValue(safeText(item.getHitTags()));
|
||||
row.createCell(9).setCellValue(safeNumber(item.getDisplayAmount()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,18 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagTask;
|
||||
import com.ruoyi.ccdi.project.domain.event.CcdiProjectHistoryImportSubmittedEvent;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
@@ -43,12 +47,18 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
@Resource
|
||||
private CcdiProjectMapper projectMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiBankTagTaskMapper bankTagTaskMapper;
|
||||
|
||||
@Resource
|
||||
private LsfxAnalysisClient lsfxAnalysisClient;
|
||||
|
||||
@Resource
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@Resource
|
||||
private CcdiProjectAccessService projectAccessService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
|
||||
@@ -77,6 +87,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
// 5. 返回VO
|
||||
CcdiProjectVO vo = new CcdiProjectVO();
|
||||
BeanUtils.copyProperties(project, vo);
|
||||
fillProjectExtraFields(project, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -90,6 +101,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
if (existingProject == null) {
|
||||
throw new ServiceException("项目不存在");
|
||||
}
|
||||
projectAccessService.assertCanOperate(dto.getProjectId());
|
||||
|
||||
// 只更新允许修改的字段
|
||||
existingProject.setProjectName(dto.getProjectName());
|
||||
@@ -100,38 +112,46 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
|
||||
CcdiProjectVO vo = new CcdiProjectVO();
|
||||
BeanUtils.copyProperties(existingProject, vo);
|
||||
fillProjectExtraFields(existingProject, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteProject(Long projectId) {
|
||||
projectAccessService.assertCanOperate(projectId);
|
||||
return projectMapper.deleteById(projectId) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiProjectVO getProjectById(Long projectId) {
|
||||
projectAccessService.assertCanRead(projectId);
|
||||
CcdiProject project = projectMapper.selectById(projectId);
|
||||
if (project == null) {
|
||||
return null;
|
||||
}
|
||||
CcdiProjectVO vo = new CcdiProjectVO();
|
||||
BeanUtils.copyProperties(project, vo);
|
||||
fillProjectExtraFields(project, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, CcdiProjectQueryDTO queryDTO) {
|
||||
return projectMapper.selectProjectPage(page, queryDTO);
|
||||
ProjectAccessScope scope = projectAccessService.buildCurrentScope();
|
||||
Page<CcdiProjectVO> result = projectMapper.selectProjectPage(page, queryDTO, scope);
|
||||
fillProjectExtraFields(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CcdiProjectHistoryListItemVO> listHistoryProjects(CcdiProjectQueryDTO queryDTO) {
|
||||
return projectMapper.selectHistoryProjects(queryDTO);
|
||||
return projectMapper.selectHistoryProjects(queryDTO, projectAccessService.buildCurrentScope());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator) {
|
||||
projectAccessService.assertSourceProjectsReadable(dto.getSourceProjectIds());
|
||||
CcdiProjectSaveDTO saveDTO = new CcdiProjectSaveDTO();
|
||||
saveDTO.setProjectName(dto.getProjectName());
|
||||
saveDTO.setDescription(dto.getDescription());
|
||||
@@ -151,44 +171,43 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
@Override
|
||||
public CcdiProjectStatusCountsVO getStatusCounts() {
|
||||
CcdiProjectStatusCountsVO vo = new CcdiProjectStatusCountsVO();
|
||||
ProjectAccessScope scope = projectAccessService.buildCurrentScope();
|
||||
|
||||
// 统计全部项目
|
||||
Long totalCount = projectMapper.selectCount(null);
|
||||
LambdaQueryWrapper<CcdiProject> baseWrapper = buildScopeWrapper(scope);
|
||||
Long totalCount = projectMapper.selectCount(baseWrapper);
|
||||
vo.setAll(totalCount);
|
||||
|
||||
// 统计进行中项目(状态0)
|
||||
Long status0Count = projectMapper.selectCount(
|
||||
new LambdaQueryWrapper<CcdiProject>()
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.PROCESSING)
|
||||
);
|
||||
Long status0Count = projectMapper.selectCount(buildScopeWrapper(scope)
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.PROCESSING));
|
||||
vo.setStatus0(status0Count);
|
||||
|
||||
// 统计已完成项目(状态1)
|
||||
Long status1Count = projectMapper.selectCount(
|
||||
new LambdaQueryWrapper<CcdiProject>()
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.COMPLETED)
|
||||
);
|
||||
Long status1Count = projectMapper.selectCount(buildScopeWrapper(scope)
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.COMPLETED));
|
||||
vo.setStatus1(status1Count);
|
||||
|
||||
// 统计已归档项目(状态2)
|
||||
Long status2Count = projectMapper.selectCount(
|
||||
new LambdaQueryWrapper<CcdiProject>()
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.ARCHIVED)
|
||||
);
|
||||
Long status2Count = projectMapper.selectCount(buildScopeWrapper(scope)
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.ARCHIVED));
|
||||
vo.setStatus2(status2Count);
|
||||
|
||||
Long status3Count = projectMapper.selectCount(
|
||||
new LambdaQueryWrapper<CcdiProject>()
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAGGING)
|
||||
);
|
||||
Long status3Count = projectMapper.selectCount(buildScopeWrapper(scope)
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAGGING));
|
||||
vo.setStatus3(status3Count);
|
||||
|
||||
Long status4Count = projectMapper.selectCount(buildScopeWrapper(scope)
|
||||
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAG_FAILED));
|
||||
vo.setStatus4(status4Count);
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void archiveProject(Long projectId, String operator) {
|
||||
CcdiProject project = getRequiredProject(projectId);
|
||||
projectAccessService.assertCanOperate(projectId);
|
||||
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) {
|
||||
throw new ServiceException("项目已归档,无需重复操作");
|
||||
}
|
||||
@@ -263,10 +282,55 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
case CcdiProjectStatusConstants.COMPLETED -> "已完成";
|
||||
case CcdiProjectStatusConstants.ARCHIVED -> "已归档";
|
||||
case CcdiProjectStatusConstants.TAGGING -> "打标中";
|
||||
case CcdiProjectStatusConstants.TAG_FAILED -> "打标失败";
|
||||
default -> "未知";
|
||||
};
|
||||
}
|
||||
|
||||
private void fillLatestTagFailure(CcdiProject project, CcdiProjectVO vo) {
|
||||
if (!CcdiProjectStatusConstants.TAG_FAILED.equals(project.getStatus())) {
|
||||
return;
|
||||
}
|
||||
CcdiBankTagTask latestFailedTask = bankTagTaskMapper.selectLatestFailedTaskByProjectId(project.getProjectId());
|
||||
if (latestFailedTask == null) {
|
||||
return;
|
||||
}
|
||||
vo.setLatestTagTaskErrorMessage(latestFailedTask.getErrorMessage());
|
||||
vo.setLatestTagTaskEndTime(latestFailedTask.getEndTime());
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<CcdiProject> buildScopeWrapper(ProjectAccessScope scope) {
|
||||
LambdaQueryWrapper<CcdiProject> wrapper = new LambdaQueryWrapper<>();
|
||||
if (scope != null && !scope.isViewAllProjects()) {
|
||||
wrapper.eq(CcdiProject::getCreateBy, scope.getUsername());
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private void fillProjectExtraFields(List<CcdiProjectVO> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ProjectAccessScope scope = projectAccessService.buildCurrentScope();
|
||||
for (CcdiProjectVO vo : records) {
|
||||
fillProjectAccessFields(vo, scope);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillProjectExtraFields(CcdiProject project, CcdiProjectVO vo) {
|
||||
fillLatestTagFailure(project, vo);
|
||||
fillProjectAccessFields(vo, projectAccessService.buildCurrentScope());
|
||||
}
|
||||
|
||||
private void fillProjectAccessFields(CcdiProjectVO vo, ProjectAccessScope scope) {
|
||||
if (vo == null || scope == null) {
|
||||
return;
|
||||
}
|
||||
boolean ownedByCurrentUser = Objects.equals(scope.getUsername(), vo.getCreateBy());
|
||||
vo.setOwnedByCurrentUser(ownedByCurrentUser);
|
||||
vo.setCanOperate(scope.isSuperAdmin() || ownedByCurrentUser);
|
||||
}
|
||||
|
||||
private String resolveOperator(String operator) {
|
||||
return StringUtils.hasText(operator) ? operator : "system";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.ruoyi.ccdi.project.service.impl;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiRelationGraphQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiRelationGraphSuspectedEnterpriseQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphEdgeVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphNodeVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphSuspectedEnterpriseItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphSuspectedEnterpriseVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiRelationGraphMapper;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiRelationGraphService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 关系图谱Service实现
|
||||
*/
|
||||
@Service
|
||||
public class CcdiRelationGraphServiceImpl implements ICcdiRelationGraphService {
|
||||
|
||||
private static final int DEFAULT_LIMIT = 80;
|
||||
private static final int MAX_LIMIT = 200;
|
||||
private static final int DEFAULT_SUSPECTED_LIMIT = 10;
|
||||
private static final int MAX_SUSPECTED_LIMIT = 20;
|
||||
private static final int SAME_NAME_BLOCK_THRESHOLD = 20;
|
||||
private static final String NODE_PREFIX = "rel_node/";
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
@Resource
|
||||
private CcdiRelationGraphMapper relationGraphMapper;
|
||||
|
||||
@Override
|
||||
public List<CcdiRelationGraphNodeVO> searchSubjects(CcdiRelationGraphQueryDTO queryDTO) {
|
||||
CcdiRelationGraphQueryDTO query = normalizeGraphQuery(queryDTO);
|
||||
if (isBlank(query.getKeyword()) && isBlank(query.getObjectKey())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return relationGraphMapper.selectRelationGraphSubjects(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiRelationGraphVO getRelationGraph(CcdiRelationGraphQueryDTO queryDTO) {
|
||||
CcdiRelationGraphQueryDTO query = normalizeGraphQuery(queryDTO);
|
||||
CcdiRelationGraphNodeVO centerNode = resolveCenterNode(query);
|
||||
if (centerNode == null || isBlank(centerNode.getObjectKey())) {
|
||||
return new CcdiRelationGraphVO();
|
||||
}
|
||||
|
||||
query.setObjectKey(centerNode.getObjectKey());
|
||||
List<CcdiRelationGraphEdgeVO> edges = relationGraphMapper.selectRelationGraphEdges(query);
|
||||
if (edges == null) {
|
||||
edges = Collections.emptyList();
|
||||
}
|
||||
|
||||
List<CcdiRelationGraphNodeVO> nodes = buildNodes(centerNode, edges);
|
||||
CcdiRelationGraphVO graph = new CcdiRelationGraphVO();
|
||||
graph.setCenterNode(centerNode);
|
||||
graph.setNodes(nodes);
|
||||
graph.setEdges(edges);
|
||||
graph.setEdgeCount((long) edges.size());
|
||||
graph.setMaxDepth(1);
|
||||
return graph;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiRelationGraphSuspectedEnterpriseVO getSuspectedEnterprises(CcdiRelationGraphSuspectedEnterpriseQueryDTO queryDTO) {
|
||||
CcdiRelationGraphSuspectedEnterpriseVO result = new CcdiRelationGraphSuspectedEnterpriseVO();
|
||||
CcdiRelationGraphSuspectedEnterpriseQueryDTO query = normalizeSuspectedEnterpriseQuery(queryDTO);
|
||||
if (isBlank(query.getPersonName())) {
|
||||
result.setMessage("缺少人员姓名,无法按工商同名主体召回");
|
||||
return result;
|
||||
}
|
||||
|
||||
int sameNameKeyNoCount = relationGraphMapper.countSuspectedEnterpriseKeyNos(query.getPersonName());
|
||||
result.setSameNameKeyNoCount(sameNameKeyNoCount);
|
||||
if (sameNameKeyNoCount > SAME_NAME_BLOCK_THRESHOLD) {
|
||||
result.setBlocked(true);
|
||||
result.setMessage("同名工商主体过多,请结合交易对手企业名称或其他线索进一步筛选");
|
||||
return result;
|
||||
}
|
||||
|
||||
LocalDate birthDate = resolveBirthDate(query);
|
||||
List<CcdiRelationGraphSuspectedEnterpriseItemVO> rows =
|
||||
relationGraphMapper.selectSuspectedEnterprises(query.getPersonName(), MAX_SUSPECTED_LIMIT);
|
||||
List<CcdiRelationGraphSuspectedEnterpriseItemVO> filteredRows = new ArrayList<>();
|
||||
if (rows != null) {
|
||||
for (CcdiRelationGraphSuspectedEnterpriseItemVO row : rows) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
row.setPersonName(query.getPersonName());
|
||||
if (applyAgeRule(row, birthDate)) {
|
||||
filteredRows.add(row);
|
||||
if (filteredRows.size() >= query.getLimit()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.setRows(filteredRows);
|
||||
if (filteredRows.isEmpty()) {
|
||||
result.setMessage("未发现可展示的疑似同名企业");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private CcdiRelationGraphNodeVO resolveCenterNode(CcdiRelationGraphQueryDTO query) {
|
||||
List<CcdiRelationGraphNodeVO> subjects = relationGraphMapper.selectRelationGraphSubjects(query);
|
||||
if (subjects == null || subjects.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return subjects.get(0);
|
||||
}
|
||||
|
||||
private List<CcdiRelationGraphNodeVO> buildNodes(CcdiRelationGraphNodeVO centerNode, List<CcdiRelationGraphEdgeVO> edges) {
|
||||
Set<String> objectKeys = new LinkedHashSet<>();
|
||||
objectKeys.add(centerNode.getObjectKey());
|
||||
for (CcdiRelationGraphEdgeVO edge : edges) {
|
||||
edge.setFromObjectKey(toObjectKey(edge.getFromKey()));
|
||||
edge.setToObjectKey(toObjectKey(edge.getToKey()));
|
||||
if (!isBlank(edge.getFromObjectKey())) {
|
||||
objectKeys.add(edge.getFromObjectKey());
|
||||
}
|
||||
if (!isBlank(edge.getToObjectKey())) {
|
||||
objectKeys.add(edge.getToObjectKey());
|
||||
}
|
||||
}
|
||||
|
||||
List<CcdiRelationGraphNodeVO> rawNodes = relationGraphMapper.selectRelationGraphNodesByKeys(new ArrayList<>(objectKeys));
|
||||
Map<String, CcdiRelationGraphNodeVO> nodeMap = new LinkedHashMap<>();
|
||||
if (rawNodes != null) {
|
||||
for (CcdiRelationGraphNodeVO node : rawNodes) {
|
||||
enrichNode(node, centerNode);
|
||||
nodeMap.put(node.getObjectKey(), node);
|
||||
}
|
||||
}
|
||||
if (!nodeMap.containsKey(centerNode.getObjectKey())) {
|
||||
enrichNode(centerNode, centerNode);
|
||||
nodeMap.put(centerNode.getObjectKey(), centerNode);
|
||||
}
|
||||
|
||||
for (CcdiRelationGraphEdgeVO edge : edges) {
|
||||
CcdiRelationGraphNodeVO fromNode = nodeMap.get(edge.getFromObjectKey());
|
||||
CcdiRelationGraphNodeVO toNode = nodeMap.get(edge.getToObjectKey());
|
||||
if (fromNode != null) {
|
||||
edge.setFromName(fromNode.getNodeName());
|
||||
}
|
||||
if (toNode != null) {
|
||||
edge.setToName(toNode.getNodeName());
|
||||
}
|
||||
}
|
||||
return List.copyOf(nodeMap.values());
|
||||
}
|
||||
|
||||
private void enrichNode(CcdiRelationGraphNodeVO node, CcdiRelationGraphNodeVO centerNode) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
node.setNodeKey(NODE_PREFIX + node.getObjectKey());
|
||||
node.setCanExpand(true);
|
||||
node.setDepth(node.getObjectKey() != null && node.getObjectKey().equals(centerNode.getObjectKey()) ? 0 : 1);
|
||||
}
|
||||
|
||||
private CcdiRelationGraphQueryDTO normalizeGraphQuery(CcdiRelationGraphQueryDTO queryDTO) {
|
||||
CcdiRelationGraphQueryDTO query = queryDTO == null ? new CcdiRelationGraphQueryDTO() : queryDTO;
|
||||
query.setKeyword(normalizeText(query.getKeyword()));
|
||||
query.setObjectKey(normalizeText(query.getObjectKey()));
|
||||
query.setLimit(normalizeLimit(query.getLimit()));
|
||||
query.setDepth(1);
|
||||
return query;
|
||||
}
|
||||
|
||||
private CcdiRelationGraphSuspectedEnterpriseQueryDTO normalizeSuspectedEnterpriseQuery(CcdiRelationGraphSuspectedEnterpriseQueryDTO queryDTO) {
|
||||
CcdiRelationGraphSuspectedEnterpriseQueryDTO query =
|
||||
queryDTO == null ? new CcdiRelationGraphSuspectedEnterpriseQueryDTO() : queryDTO;
|
||||
query.setPersonName(normalizeText(query.getPersonName()));
|
||||
query.setCertNo(normalizeText(query.getCertNo()));
|
||||
query.setBirthDate(normalizeText(query.getBirthDate()));
|
||||
query.setLimit(normalizeSuspectedLimit(query.getLimit()));
|
||||
return query;
|
||||
}
|
||||
|
||||
private Integer normalizeSuspectedLimit(Integer limit) {
|
||||
if (limit == null || limit <= 0) {
|
||||
return DEFAULT_SUSPECTED_LIMIT;
|
||||
}
|
||||
return Math.min(limit, MAX_SUSPECTED_LIMIT);
|
||||
}
|
||||
|
||||
private LocalDate resolveBirthDate(CcdiRelationGraphSuspectedEnterpriseQueryDTO query) {
|
||||
LocalDate explicitBirthDate = parseDate(query.getBirthDate());
|
||||
if (explicitBirthDate != null) {
|
||||
return explicitBirthDate;
|
||||
}
|
||||
return parseBirthDateFromCertNo(query.getCertNo());
|
||||
}
|
||||
|
||||
private boolean applyAgeRule(CcdiRelationGraphSuspectedEnterpriseItemVO row, LocalDate birthDate) {
|
||||
LocalDate establishDate = parseDate(row.getEstablishDate());
|
||||
if (birthDate == null || establishDate == null) {
|
||||
row.setMatchReason("姓名一致;企业成立日期或出生日期缺失,年龄无法判断");
|
||||
return true;
|
||||
}
|
||||
int age = Period.between(birthDate, establishDate).getYears();
|
||||
row.setAgeAtEstablish(age);
|
||||
if (age < 18) {
|
||||
return false;
|
||||
}
|
||||
row.setMatchReason("姓名一致;成立时年龄" + age + "岁");
|
||||
return true;
|
||||
}
|
||||
|
||||
private LocalDate parseBirthDateFromCertNo(String certNo) {
|
||||
if (isBlank(certNo)) {
|
||||
return null;
|
||||
}
|
||||
String value = certNo.trim();
|
||||
if (value.matches("^\\d{17}[0-9Xx]$")) {
|
||||
return parseCompactDate(value.substring(6, 14));
|
||||
}
|
||||
if (value.matches("^\\d{15}$")) {
|
||||
return parseCompactDate("19" + value.substring(6, 12));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private LocalDate parseCompactDate(String value) {
|
||||
try {
|
||||
return LocalDate.parse(value, DateTimeFormatter.BASIC_ISO_DATE);
|
||||
} catch (DateTimeParseException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate parseDate(String value) {
|
||||
if (isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(value.trim(), DATE_FORMATTER);
|
||||
} catch (DateTimeParseException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer normalizeLimit(Integer limit) {
|
||||
if (limit == null || limit <= 0) {
|
||||
return DEFAULT_LIMIT;
|
||||
}
|
||||
return Math.min(limit, MAX_LIMIT);
|
||||
}
|
||||
|
||||
private String toObjectKey(String nodeKey) {
|
||||
if (isBlank(nodeKey)) {
|
||||
return null;
|
||||
}
|
||||
return nodeKey.startsWith(NODE_PREFIX) ? nodeKey.substring(NODE_PREFIX.length()) : nodeKey;
|
||||
}
|
||||
|
||||
private String normalizeText(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<select id="countMatchedStaffCountByProjectId" resultType="java.lang.Integer">
|
||||
select count(distinct trim(bs.cret_no))
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
|
||||
inner join ccdi_base_staff staff on staff.id_card = trim(bs.cret_no)
|
||||
where bs.project_id = #{projectId}
|
||||
and bs.cret_no is not null
|
||||
and trim(bs.cret_no) != ''
|
||||
@@ -215,6 +215,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
#{item}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="query.ourCertNos != null and query.ourCertNos.size() > 0">
|
||||
AND bs.cret_no IN
|
||||
<foreach collection="query.ourCertNos" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="query.ourBanks != null and query.ourBanks.size() > 0">
|
||||
AND bs.BANK IN
|
||||
<foreach collection="query.ourBanks" item="item" open="(" separator="," close=")">
|
||||
|
||||
@@ -41,6 +41,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and trim(bs.cret_no) != ''
|
||||
</sql>
|
||||
|
||||
<sql id="externalPersonPredicateSql">
|
||||
bs.cret_no is not null
|
||||
and trim(bs.cret_no) != ''
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_base_staff staff
|
||||
where staff.id_card = bs.cret_no
|
||||
)
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_staff_fmy_relation relation
|
||||
where relation.status = 1
|
||||
and relation.relation_cert_no = bs.cret_no
|
||||
)
|
||||
and trim(IFNULL(bs.LE_ACCOUNT_NAME, '')) <> trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''))
|
||||
</sql>
|
||||
|
||||
<sql id="cashDepositPredicate">
|
||||
(
|
||||
(
|
||||
@@ -105,36 +122,123 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<sql id="salaryExclusionPredicate">
|
||||
not (
|
||||
bs.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司'
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') LIKE '%代发%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%工资%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%奖金%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%薪酬%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%薪金%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%补贴%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%薪%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%年终奖%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%年金%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%加班费%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%劳务费%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%劳务外包%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%提成%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%劳务派遣%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%绩效%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%酬劳%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%PAYROLL%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%SALA%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%CPF%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%directors%fees%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%批量代付%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%代发%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%工资%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%劳务费%'
|
||||
(
|
||||
bs.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司'
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') LIKE '%代发%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%工资%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%奖金%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%薪酬%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%薪金%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%补贴%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%薪%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%年终奖%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%年金%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%加班费%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%劳务费%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%劳务外包%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%提成%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%劳务派遣%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%绩效%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%酬劳%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%PAYROLL%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%SALA%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%CPF%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%directors%fees%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%批量代付%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%代发%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%工资%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%劳务费%'
|
||||
)
|
||||
)
|
||||
or (
|
||||
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%公积金中心%'
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') LIKE '%公积金%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%批量代付%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%公积金%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%批量代付%'
|
||||
)
|
||||
)
|
||||
)
|
||||
</sql>
|
||||
|
||||
<sql id="financialProductExclusionPredicate">
|
||||
not (
|
||||
(
|
||||
(
|
||||
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '理财|理财产品|结构.*存款|结构性存款|理财.*托管|余额宝|朝朝宝|朝朝盈|现金宝|金添利|定存宝'
|
||||
or IFNULL(bs.USER_MEMO, '') REGEXP '理财|理财产品|结构.*存款|结构性存款|本金划出|本金返还|余额宝|朝朝宝|朝朝盈|现金宝|金添利|定存宝|整存整取|智能存款|通知存款'
|
||||
or IFNULL(bs.CASH_TYPE, '') REGEXP '受托理财|表内理财|购买理财|理财购买|理财扣款|理财申购|理财认购|结构性存款|存款产品|朝朝宝'
|
||||
or (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '申购|认购|赎回'
|
||||
and IFNULL(bs.USER_MEMO, '') REGEXP '理财|产品|存款|本金|余额宝|朝朝宝|朝朝盈'
|
||||
)
|
||||
)
|
||||
and IFNULL(bs.USER_MEMO, '') NOT REGEXP '财务|经理|代理财税'
|
||||
and IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') NOT LIKE '%代理财政%'
|
||||
)
|
||||
)
|
||||
</sql>
|
||||
|
||||
<sql id="thirdPartyWithdrawIncomePredicate">
|
||||
(
|
||||
(
|
||||
bs.BANK in ('ALIPAY', 'WECHAT')
|
||||
and (
|
||||
IFNULL(bs.CASH_TYPE, '') LIKE '%提现%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%提现%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%转出到%银行%'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%提现%'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%转出到%银行%'
|
||||
)
|
||||
)
|
||||
or (
|
||||
(
|
||||
bs.BANK is null
|
||||
or bs.BANK = ''
|
||||
or bs.BANK not in ('ALIPAY', 'WECHAT')
|
||||
)
|
||||
and (
|
||||
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '支付宝|Alipay|财付通|Tenpay|微信|wechat|WeChat|微信零钱'
|
||||
or IFNULL(bs.USER_MEMO, '') REGEXP '支付宝|Alipay|财付通|Tenpay|微信|wechat|WeChat|微信零钱'
|
||||
or IFNULL(bs.CASH_TYPE, '') REGEXP '支付宝|Alipay|财付通|Tenpay|微信|wechat|WeChat|微信零钱'
|
||||
)
|
||||
and (
|
||||
IFNULL(bs.CASH_TYPE, '') LIKE '%提现%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%提现%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%转出到%银行%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%提现到账%'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%提现%'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%转出到%银行%'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%提现到账%'
|
||||
)
|
||||
)
|
||||
)
|
||||
</sql>
|
||||
|
||||
<sql id="abnormalCustomerTransactionSubjectSql">
|
||||
select
|
||||
staff.id_card as subjectCertNo,
|
||||
staff.name as subjectName,
|
||||
'本人' as subjectType
|
||||
from ccdi_base_staff staff
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
relation.relation_cert_no as subjectCertNo,
|
||||
relation.relation_name as subjectName,
|
||||
case
|
||||
when relation.relation_type is not null and trim(relation.relation_type) != '' then relation.relation_type
|
||||
else '关系人'
|
||||
end as subjectType
|
||||
from ccdi_staff_fmy_relation relation
|
||||
where relation.status = 1
|
||||
and relation.relation_cert_no is not null
|
||||
and trim(relation.relation_cert_no) != ''
|
||||
</sql>
|
||||
|
||||
<sql id="salaryIncomePredicate">
|
||||
bs.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司'
|
||||
and (
|
||||
@@ -167,6 +271,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '(购|买).*房|(购|买).*车|车款|房款|首付|(房|车).*贷'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '汽车销售|汽车金融|4S店|汽贸|车行|房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局'
|
||||
)
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
and (
|
||||
exists (
|
||||
select 1
|
||||
@@ -198,6 +303,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '税务|缴税|税款'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '税务|税务局|国库|国家金库|财政'
|
||||
)
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
and (
|
||||
exists (
|
||||
select 1
|
||||
@@ -235,6 +341,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
|
||||
and relation.person_id is null
|
||||
and <include refid="salaryExclusionPredicate"/>
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
</select>
|
||||
|
||||
<select id="selectCumulativeIncomeObjects" resultMap="BankTagObjectHitResultMap">
|
||||
@@ -262,6 +369,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
|
||||
and relation.person_id is null
|
||||
and <include refid="salaryExclusionPredicate"/>
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
group by staff.id_card, bs.CUSTOMER_ACCOUNT_NAME
|
||||
having SUM(IFNULL(bs.AMOUNT_CR, 0)) > #{threshold}
|
||||
) t
|
||||
@@ -283,6 +391,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
and STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
||||
group by staff.id_card
|
||||
having SUM(IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0)) > #{threshold}
|
||||
@@ -302,18 +411,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.AMOUNT_CR, 0) > #{threshold}
|
||||
and <include refid="cashDepositPredicate"/>
|
||||
and (
|
||||
exists (
|
||||
select 1
|
||||
from ccdi_base_staff staff
|
||||
where staff.id_card = bs.cret_no
|
||||
)
|
||||
or exists (
|
||||
select 1
|
||||
from ccdi_staff_fmy_relation relation
|
||||
where relation.relation_cert_no = bs.cret_no
|
||||
and relation.status = 1
|
||||
)
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
and exists (
|
||||
select 1
|
||||
from ccdi_base_staff staff
|
||||
where staff.id_card = bs.cret_no
|
||||
)
|
||||
</select>
|
||||
|
||||
@@ -340,16 +442,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.AMOUNT_CR, 0) > #{amountThreshold}
|
||||
and <include refid="cashDepositPredicate"/>
|
||||
union all
|
||||
select
|
||||
relation.person_id AS object_key,
|
||||
LEFT(TRIM(bs.TRX_DATE), 10) AS cash_date
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_staff_fmy_relation relation on relation.relation_cert_no = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and relation.status = 1
|
||||
and IFNULL(bs.AMOUNT_CR, 0) > #{amountThreshold}
|
||||
and <include refid="cashDepositPredicate"/>
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
) source
|
||||
group by source.object_key, source.cash_date
|
||||
having COUNT(1) > #{frequencyThreshold}
|
||||
@@ -373,8 +466,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
or IFNULL(bs.USER_MEMO, '') REGEXP '转帐|转账|汇入|转存|红包|汇款|网转|转入'
|
||||
or IFNULL(bs.CASH_TYPE, '') REGEXP '转帐|转账|汇入|转存|红包|汇款|网转|转入'
|
||||
)
|
||||
and IFNULL(bs.USER_MEMO, '') NOT LIKE '%款%'
|
||||
and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
and (
|
||||
exists (
|
||||
select 1
|
||||
@@ -390,14 +483,200 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
)
|
||||
</select>
|
||||
|
||||
<select id="selectAbnormalCustomerTransactionStatements" resultMap="BankTagStatementHitResultMap">
|
||||
<select id="selectExternalSingleLargeAmountStatements" resultMap="BankTagStatementHitResultMap">
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
'占位SQL,待补充真实规则' AS reasonDetail
|
||||
CONCAT(
|
||||
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
|
||||
'”单笔交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
|
||||
' 元,超过阈值 ', CAST(#{threshold} AS CHAR),
|
||||
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
where 1 = 0
|
||||
where bs.project_id = #{projectId}
|
||||
and <include refid="externalPersonPredicateSql"/>
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > #{threshold}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
</select>
|
||||
|
||||
<select id="selectExternalCumulativeTransactionAmountObjects" resultMap="BankTagObjectHitResultMap">
|
||||
select
|
||||
'EXTERNAL_CERT_NO' AS objectType,
|
||||
t.certNo AS objectKey,
|
||||
CONCAT(
|
||||
'外部人员“', IFNULL(t.personName, ''),
|
||||
'”累计交易金额 ', CAST(t.totalAmount AS CHAR),
|
||||
' 元,超过阈值 ', CAST(#{threshold} AS CHAR), ' 元'
|
||||
) AS reasonDetail
|
||||
from (
|
||||
select
|
||||
bs.cret_no AS certNo,
|
||||
max(IFNULL(bs.LE_ACCOUNT_NAME, '')) AS personName,
|
||||
ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) AS totalAmount
|
||||
from ccdi_bank_statement bs
|
||||
where bs.project_id = #{projectId}
|
||||
and <include refid="externalPersonPredicateSql"/>
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
group by bs.cret_no
|
||||
having ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) > #{threshold}
|
||||
) t
|
||||
</select>
|
||||
|
||||
<select id="selectExternalAnnualTurnoverObjects" resultMap="BankTagObjectHitResultMap">
|
||||
select
|
||||
'EXTERNAL_CERT_NO' AS objectType,
|
||||
t.certNo AS objectKey,
|
||||
CONCAT(
|
||||
'外部人员“', IFNULL(t.personName, ''),
|
||||
'”近一年流水交易额 ', CAST(t.annualAmount AS CHAR),
|
||||
' 元,超过阈值 ', CAST(#{threshold} AS CHAR), ' 元'
|
||||
) AS reasonDetail
|
||||
from (
|
||||
select
|
||||
bs.cret_no AS certNo,
|
||||
max(IFNULL(bs.LE_ACCOUNT_NAME, '')) AS personName,
|
||||
ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) AS annualAmount
|
||||
from ccdi_bank_statement bs
|
||||
where bs.project_id = #{projectId}
|
||||
and <include refid="externalPersonPredicateSql"/>
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
and STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
||||
group by bs.cret_no
|
||||
having ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) > #{threshold}
|
||||
) t
|
||||
</select>
|
||||
|
||||
<select id="selectAbnormalCustomerTransactionStatements" resultMap="BankTagStatementHitResultMap">
|
||||
select
|
||||
hit.bankStatementId AS bankStatementId,
|
||||
max(hit.groupId) AS groupId,
|
||||
max(hit.logId) AS logId,
|
||||
substring_index(
|
||||
min(concat(lpad(hit.matchPriority, 2, '0'), '|', hit.reasonDetail)),
|
||||
'|',
|
||||
-1
|
||||
) AS reasonDetail
|
||||
from (
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
1 AS matchPriority,
|
||||
CONCAT(
|
||||
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与信贷客户账号发生交易,',
|
||||
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
|
||||
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
inner join (
|
||||
<include refid="abnormalCustomerTransactionSubjectSql"/>
|
||||
) subject on subject.subjectCertNo = bs.cret_no
|
||||
inner join ccdi_account_info account
|
||||
on trim(IFNULL(bs.customer_account_no, '')) != ''
|
||||
and account.owner_type = 'CREDIT_CUSTOMER'
|
||||
and account.account_no = bs.customer_account_no
|
||||
where bs.project_id = #{projectId}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
2 AS matchPriority,
|
||||
CONCAT(
|
||||
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与中介账号发生交易,',
|
||||
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
|
||||
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
inner join (
|
||||
<include refid="abnormalCustomerTransactionSubjectSql"/>
|
||||
) subject on subject.subjectCertNo = bs.cret_no
|
||||
inner join ccdi_account_info account
|
||||
on trim(IFNULL(bs.customer_account_no, '')) != ''
|
||||
and account.owner_type = 'INTERMEDIARY'
|
||||
and account.account_no = bs.customer_account_no
|
||||
where bs.project_id = #{projectId}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
3 AS matchPriority,
|
||||
CONCAT(
|
||||
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与中介关联企业发生交易,',
|
||||
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
|
||||
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
inner join (
|
||||
<include refid="abnormalCustomerTransactionSubjectSql"/>
|
||||
) subject on subject.subjectCertNo = bs.cret_no
|
||||
inner join ccdi_enterprise_base_info enterprise
|
||||
on trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')) != ''
|
||||
and enterprise.enterprise_name = bs.CUSTOMER_ACCOUNT_NAME
|
||||
and enterprise.ent_source = 'INTERMEDIARY'
|
||||
where bs.project_id = #{projectId}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
4 AS matchPriority,
|
||||
CONCAT(
|
||||
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与中介库人员发生微信/支付宝交易,',
|
||||
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
|
||||
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
inner join (
|
||||
<include refid="abnormalCustomerTransactionSubjectSql"/>
|
||||
) subject on subject.subjectCertNo = bs.cret_no
|
||||
inner join ccdi_biz_intermediary intermediary
|
||||
on trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')) != ''
|
||||
and trim(IFNULL(intermediary.name, '')) != ''
|
||||
and bs.CUSTOMER_ACCOUNT_NAME like concat('%', intermediary.name, '%')
|
||||
where bs.project_id = #{projectId}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
|
||||
and bs.bank in ('ALIPAY', 'WECHAT')
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
5 AS matchPriority,
|
||||
CONCAT(
|
||||
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与中介库人员发生名称精确匹配交易,',
|
||||
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
|
||||
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
inner join (
|
||||
<include refid="abnormalCustomerTransactionSubjectSql"/>
|
||||
) subject on subject.subjectCertNo = bs.cret_no
|
||||
inner join ccdi_biz_intermediary intermediary
|
||||
on trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')) != ''
|
||||
and intermediary.name = bs.CUSTOMER_ACCOUNT_NAME
|
||||
where bs.project_id = #{projectId}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
|
||||
|
||||
) hit
|
||||
group by hit.bankStatementId
|
||||
</select>
|
||||
|
||||
<select id="selectLowIncomeRelativeLargeTransactionObjects" resultMap="BankTagObjectHitResultMap">
|
||||
@@ -416,9 +695,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from ccdi_staff_fmy_relation relation
|
||||
inner join ccdi_bank_statement bs on relation.relation_cert_no = bs.cret_no
|
||||
where relation.status = 1
|
||||
and relation.annual_income is not null
|
||||
and (
|
||||
relation.annual_income is null
|
||||
or relation.annual_income = 0
|
||||
relation.annual_income = 0
|
||||
or relation.annual_income / 12 < 3000
|
||||
)
|
||||
and bs.project_id = #{projectId}
|
||||
@@ -503,11 +782,77 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.AMOUNT_DR, 0) > 0
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌|球|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|投注'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌|球|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|投注'
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|VIP666|USDT下注'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|VIP666|USDT下注'
|
||||
)
|
||||
</select>
|
||||
|
||||
<select id="selectExternalGamblingMemoStatements" resultMap="BankTagStatementHitResultMap">
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
CONCAT(
|
||||
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
|
||||
'”摘要/对手方命中疑似赌博关键词,摘要“', IFNULL(bs.USER_MEMO, ''),
|
||||
'”,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
|
||||
'”,交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR), ' 元'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
where bs.project_id = #{projectId}
|
||||
and <include refid="externalPersonPredicateSql"/>
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
|
||||
or IFNULL(bs.CASH_TYPE, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
|
||||
)
|
||||
</select>
|
||||
|
||||
<select id="selectExternalMultiPartyGamblingTransferObjects" resultMap="BankTagObjectHitResultMap">
|
||||
select
|
||||
'EXTERNAL_CERT_NO' AS objectType,
|
||||
t.certNo AS objectKey,
|
||||
CONCAT(
|
||||
'外部人员“', IFNULL(MAX(t.personName), ''),
|
||||
'”交易日 ', MAX(t.tradeDate),
|
||||
' 发生 ', CAST(MAX(t.hitCount) AS CHAR),
|
||||
' 笔疑似赌博交易,涉及 ', CAST(MAX(t.partyCount) AS CHAR),
|
||||
' 个对手方,金额合计 ', CAST(MAX(t.totalAmount) AS CHAR), ' 元'
|
||||
) AS reasonDetail
|
||||
from (
|
||||
select
|
||||
source.certNo AS certNo,
|
||||
max(source.personName) AS personName,
|
||||
source.tradeDate AS tradeDate,
|
||||
COUNT(1) AS hitCount,
|
||||
COUNT(DISTINCT source.customerAccountName) AS partyCount,
|
||||
ROUND(SUM(source.tradeAmount), 2) AS totalAmount
|
||||
from (
|
||||
select
|
||||
bs.cret_no AS certNo,
|
||||
IFNULL(bs.LE_ACCOUNT_NAME, '') AS personName,
|
||||
LEFT(TRIM(bs.TRX_DATE), 10) AS tradeDate,
|
||||
bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName,
|
||||
GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS tradeAmount
|
||||
from ccdi_bank_statement bs
|
||||
where bs.project_id = #{projectId}
|
||||
and <include refid="externalPersonPredicateSql"/>
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) between #{amountMinThreshold} and #{amountMaxThreshold}
|
||||
and IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') <> ''
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '微信|wechat|WeChat|财付通|Tenpay|支付宝|Alipay|转账|红包|牌局|赌'
|
||||
or IFNULL(bs.CASH_TYPE, '') REGEXP '微信|wechat|WeChat|财付通|Tenpay|支付宝|Alipay|转账|红包|牌局|赌'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '微信|wechat|WeChat|财付通|Tenpay|支付宝|Alipay'
|
||||
)
|
||||
) source
|
||||
group by source.certNo, source.tradeDate
|
||||
having COUNT(1) > 2
|
||||
and COUNT(DISTINCT source.customerAccountName) >= 2
|
||||
) t
|
||||
group by t.certNo
|
||||
</select>
|
||||
|
||||
<select id="selectSpecialAmountTransactionStatements" resultMap="BankTagStatementHitResultMap">
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
@@ -534,6 +879,74 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
)
|
||||
</select>
|
||||
|
||||
<select id="selectExternalToStaffOrFamilyTransactionStatements" resultMap="BankTagStatementHitResultMap">
|
||||
select distinct
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
CONCAT(
|
||||
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
|
||||
'”与', CASE
|
||||
WHEN counter_account.owner_type = 'EMPLOYEE' THEN '员工'
|
||||
WHEN counter_account.owner_type = 'RELATION' THEN '员工亲属'
|
||||
WHEN counter_staff.id_card is not null THEN '员工'
|
||||
WHEN counter_relation.relation_cert_no is not null THEN '员工亲属'
|
||||
ELSE '员工/员工亲属'
|
||||
END,
|
||||
'“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
|
||||
'”发生资金往来,交易金额 ',
|
||||
CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR), ' 元'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
left join ccdi_account_info counter_account
|
||||
on trim(bs.CUSTOMER_ACCOUNT_NO) != ''
|
||||
and counter_account.account_no = trim(bs.CUSTOMER_ACCOUNT_NO)
|
||||
and counter_account.owner_type in ('EMPLOYEE', 'RELATION', 'INTERMEDIARY', 'CREDIT_CUSTOMER')
|
||||
left join ccdi_base_staff counter_staff
|
||||
on counter_account.account_no is null
|
||||
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
|
||||
and counter_staff.name = trim(bs.CUSTOMER_ACCOUNT_NAME)
|
||||
left join ccdi_staff_fmy_relation counter_relation
|
||||
on counter_account.account_no is null
|
||||
and counter_relation.status = 1
|
||||
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
|
||||
and counter_relation.relation_name = trim(bs.CUSTOMER_ACCOUNT_NAME)
|
||||
where bs.project_id = #{projectId}
|
||||
and <include refid="externalPersonPredicateSql"/>
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
|
||||
and (
|
||||
counter_account.owner_type in ('EMPLOYEE', 'RELATION')
|
||||
or (
|
||||
counter_account.account_no is null
|
||||
and (
|
||||
counter_staff.id_card is not null
|
||||
or counter_relation.relation_cert_no is not null
|
||||
)
|
||||
)
|
||||
)
|
||||
</select>
|
||||
|
||||
<select id="selectExternalNightTransactionStatements" resultMap="BankTagStatementHitResultMap">
|
||||
select
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
CONCAT(
|
||||
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
|
||||
'”夜间交易,交易时间 ', IFNULL(bs.TRX_DATE, ''),
|
||||
',对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
|
||||
'”,交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR), ' 元'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
where bs.project_id = #{projectId}
|
||||
and <include refid="externalPersonPredicateSql"/>
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
|
||||
and (
|
||||
HOUR(STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s')) >= 22
|
||||
or HOUR(STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s')) < 6
|
||||
)
|
||||
</select>
|
||||
|
||||
<select id="selectMonthlyFixedIncomeObjects" resultMap="BankTagObjectHitResultMap">
|
||||
select
|
||||
'STAFF_ID_CARD' AS objectType,
|
||||
@@ -658,6 +1071,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.AMOUNT_CR, 0) > 0
|
||||
and IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') <> '浙江兰溪农村商业银行股份有限公司'
|
||||
and not (
|
||||
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%公积金中心%'
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') LIKE '%公积金%'
|
||||
or IFNULL(bs.USER_MEMO, '') LIKE '%批量代付%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%公积金%'
|
||||
or IFNULL(bs.CASH_TYPE, '') LIKE '%批量代付%'
|
||||
)
|
||||
)
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees'
|
||||
or IFNULL(bs.CASH_TYPE, '') REGEXP '代发|工资|劳务费'
|
||||
@@ -1055,6 +1477,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
or IFNULL(bs.USER_MEMO, '') REGEXP '证券|国泰君安|中信建投|中金|基金|期货|信托|同花顺|资金存管|第三方存管|银证转账|银证|证转银|银转证'
|
||||
or IFNULL(bs.CASH_TYPE, '') REGEXP '证券|国泰君安|中信建投|中金|基金|期货|信托|同花顺|资金存管|第三方存管|银证转账|银证|证转银|银转证'
|
||||
)
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
</select>
|
||||
|
||||
<select id="selectWithdrawCntObjects" resultMap="BankTagObjectHitResultMap">
|
||||
@@ -1074,11 +1497,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.AMOUNT_CR, 0) >= 0
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '财付通|微信零钱|微信|wechat|WeChat|Tenpay|支付宝|Alipay|提现'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '财付通|微信零钱|微信|wechat|WeChat|Tenpay|支付宝|Alipay|提现'
|
||||
)
|
||||
and IFNULL(bs.AMOUNT_CR, 0) > 0
|
||||
and <include refid="thirdPartyWithdrawIncomePredicate"/>
|
||||
group by staff.id_card, LEFT(TRIM(bs.TRX_DATE), 10)
|
||||
having COUNT(1) > #{frequencyThreshold}
|
||||
) t
|
||||
@@ -1087,10 +1507,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<select id="selectWithdrawAmtObjects" resultMap="BankTagObjectHitResultMap">
|
||||
select
|
||||
'STAFF_ID_CARD' AS objectType,
|
||||
'' AS objectKey,
|
||||
'占位SQL,待补充真实规则' AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
where 1 = 0
|
||||
t.objectKey AS objectKey,
|
||||
CONCAT(
|
||||
'单日微信/支付宝提现到账金额 ', CAST(t.withdrawAmount AS CHAR),
|
||||
' 元,超过阈值 ', CAST(#{amountThreshold} AS CHAR),
|
||||
' 元,交易日:', t.transDate
|
||||
) AS reasonDetail
|
||||
from (
|
||||
select
|
||||
staff.id_card AS objectKey,
|
||||
LEFT(TRIM(bs.TRX_DATE), 10) AS transDate,
|
||||
ROUND(SUM(IFNULL(bs.AMOUNT_CR, 0)), 2) AS withdrawAmount
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.AMOUNT_CR, 0) > 0
|
||||
and <include refid="thirdPartyWithdrawIncomePredicate"/>
|
||||
group by staff.id_card, LEFT(TRIM(bs.TRX_DATE), 10)
|
||||
having SUM(IFNULL(bs.AMOUNT_CR, 0)) > #{amountThreshold}
|
||||
) t
|
||||
</select>
|
||||
|
||||
<select id="selectSalaryQuickTransferObjects" resultMap="BankTagObjectHitResultMap">
|
||||
@@ -1322,10 +1757,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.AMOUNT_DR, 0) > #{threshold}
|
||||
and (
|
||||
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '证券|国泰君安|中信建投|中金|基金|期货|信托|同花顺|理财|资金存管|第三方存管'
|
||||
or IFNULL(bs.USER_MEMO, '') REGEXP '证券|国泰君安|中信建投|中金|基金|期货|信托|同花顺|理财|资金存管|第三方存管'
|
||||
or IFNULL(bs.CASH_TYPE, '') REGEXP '证券|国泰君安|中信建投|中金|基金|期货|信托|同花顺|理财|资金存管|第三方存管'
|
||||
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '证券|国泰君安|中信建投|中金|基金|期货|信托|同花顺|资金存管|第三方存管'
|
||||
or IFNULL(bs.USER_MEMO, '') REGEXP '证券|国泰君安|中信建投|中金|基金|期货|信托|同花顺|资金存管|第三方存管|银证转账|银证|证转银|银转证'
|
||||
or IFNULL(bs.CASH_TYPE, '') REGEXP '证券|国泰君安|中信建投|中金|基金|期货|信托|同花顺|资金存管|第三方存管|银证转账|银证|证转银|银转证'
|
||||
)
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
</select>
|
||||
|
||||
<select id="selectProxyAccountOperationObjects" resultMap="BankTagObjectHitResultMap">
|
||||
|
||||
@@ -65,4 +65,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectLatestFailedTaskByProjectId" resultMap="CcdiBankTagTaskResultMap">
|
||||
select id, project_id, trigger_type, model_code, status, need_rerun, success_rule_count,
|
||||
failed_rule_count, hit_count, error_message, start_time, end_time,
|
||||
create_by, create_time, update_by, update_time
|
||||
from ccdi_bank_tag_task
|
||||
where project_id = #{projectId}
|
||||
and status = 'FAILED'
|
||||
order by id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiFundGraphMapper">
|
||||
|
||||
<resultMap id="FundGraphNodeResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphNodeVO">
|
||||
<id property="objectKey" column="objectKey"/>
|
||||
<result property="nodeKey" column="nodeKey"/>
|
||||
<result property="nodeName" column="nodeName"/>
|
||||
<result property="idNo" column="idNo"/>
|
||||
<result property="cinocsno" column="cinocsno"/>
|
||||
<result property="idnoType" column="idnoType"/>
|
||||
<result property="staffId" column="staffId"/>
|
||||
<result property="sourceType" column="sourceType"/>
|
||||
<result property="nodeType" column="nodeType"/>
|
||||
<result property="identityType" column="identityType"/>
|
||||
<result property="relationType" column="relationType"/>
|
||||
<result property="accountCount" column="accountCount"/>
|
||||
<result property="createdTime" column="createdTime"/>
|
||||
<result property="updatedTime" column="updatedTime"/>
|
||||
<result property="canExpand" column="canExpand"/>
|
||||
<result property="depth" column="depth"/>
|
||||
<result property="totalAmount" column="totalAmount"/>
|
||||
<result property="transactionCount" column="transactionCount"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="FundGraphEdgeResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphEdgeVO">
|
||||
<id property="edgeKey" column="edgeKey"/>
|
||||
<result property="fromKey" column="fromKey"/>
|
||||
<result property="toKey" column="toKey"/>
|
||||
<result property="fromObjectKey" column="fromObjectKey"/>
|
||||
<result property="toObjectKey" column="toObjectKey"/>
|
||||
<result property="fromName" column="fromName"/>
|
||||
<result property="toName" column="toName"/>
|
||||
<result property="totalAmount" column="totalAmount"/>
|
||||
<result property="transactionCount" column="transactionCount"/>
|
||||
<result property="firstTrxDate" column="firstTrxDate"/>
|
||||
<result property="lastTrxDate" column="lastTrxDate"/>
|
||||
<result property="direction" column="direction"/>
|
||||
<result property="familyRelationType" column="familyRelationType"/>
|
||||
<result property="sourceType" column="sourceType"/>
|
||||
<result property="relationDesc" column="relationDesc"/>
|
||||
<result property="sourceDesc" column="sourceDesc"/>
|
||||
<result property="remark" column="remark"/>
|
||||
<result property="depth" column="depth"/>
|
||||
<result property="canTrace" column="canTrace"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="FundGraphStatementResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphStatementVO">
|
||||
<id property="bankStatementId" column="bankStatementId"/>
|
||||
<result property="trxDate" column="trxDate"/>
|
||||
<result property="leAccountNo" column="leAccountNo"/>
|
||||
<result property="leAccountName" column="leAccountName"/>
|
||||
<result property="customerAccountName" column="customerAccountName"/>
|
||||
<result property="customerAccountNo" column="customerAccountNo"/>
|
||||
<result property="cashType" column="cashType"/>
|
||||
<result property="userMemo" column="userMemo"/>
|
||||
<result property="amount" column="amount"/>
|
||||
<result property="direction" column="direction"/>
|
||||
<result property="familyRelationType" column="familyRelationType"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="detailFilter">
|
||||
<if test="query.transactionStartTime != null and query.transactionStartTime != ''">
|
||||
AND d.trx_date <![CDATA[ >= ]]> (#{query.transactionStartTime} COLLATE utf8mb4_general_ci)
|
||||
</if>
|
||||
<if test="query.transactionEndTime != null and query.transactionEndTime != ''">
|
||||
AND d.trx_date <![CDATA[ <= ]]>
|
||||
(CASE
|
||||
WHEN LENGTH(TRIM(#{query.transactionEndTime})) = 10
|
||||
THEN CONCAT(TRIM(#{query.transactionEndTime}), ' 23:59:59')
|
||||
ELSE TRIM(#{query.transactionEndTime})
|
||||
END COLLATE utf8mb4_general_ci)
|
||||
</if>
|
||||
<if test="query.amountMin != null">
|
||||
AND d.amount <![CDATA[ >= ]]> #{query.amountMin}
|
||||
</if>
|
||||
<if test="query.amountMax != null">
|
||||
AND d.amount <![CDATA[ <= ]]> #{query.amountMax}
|
||||
</if>
|
||||
<if test="query.direction != null and query.direction != ''">
|
||||
AND d.flag = (#{query.direction} COLLATE utf8mb4_general_ci)
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<sql id="subjectJoinRows">
|
||||
SELECT
|
||||
d.object_key AS detailObjectKey,
|
||||
d.bank_statement_id AS bankStatementId,
|
||||
d.trx_date AS trxDate,
|
||||
d.le_account_no AS leAccountNo,
|
||||
d.le_account_name AS leAccountName,
|
||||
d.customer_account_name AS customerAccountName,
|
||||
d.customer_account_no AS customerAccountNo,
|
||||
d.cash_type AS cashType,
|
||||
d.user_memo AS userMemo,
|
||||
d.amount,
|
||||
d.flag AS direction,
|
||||
d.family_relation_type AS familyRelationType,
|
||||
from_subject.object_key AS fromObjectKey,
|
||||
to_subject.object_key AS toObjectKey,
|
||||
CONCAT('idno_node/', from_subject.object_key) AS fromKey,
|
||||
CONCAT('idno_node/', to_subject.object_key) AS toKey,
|
||||
from_subject.name AS fromName,
|
||||
to_subject.name AS toName,
|
||||
from_subject.idnocfno AS fromIdNo,
|
||||
to_subject.idnocfno AS toIdNo
|
||||
FROM lx_fund_flow_detail_edge d
|
||||
INNER JOIN lx_fund_flow_own_account_edge from_own
|
||||
ON from_own.to_key = d.from_key
|
||||
INNER JOIN lx_fund_flow_subject_node from_subject
|
||||
ON CONCAT('idno_node/', from_subject.object_key) = from_own.from_key
|
||||
INNER JOIN lx_fund_flow_own_account_edge to_own
|
||||
ON to_own.to_key = d.to_key
|
||||
INNER JOIN lx_fund_flow_subject_node to_subject
|
||||
ON CONCAT('idno_node/', to_subject.object_key) = to_own.from_key
|
||||
WHERE 1 = 1
|
||||
<include refid="detailFilter"/>
|
||||
</sql>
|
||||
|
||||
<sql id="subjectJoinRowsByCenter">
|
||||
SELECT
|
||||
d.object_key AS detailObjectKey,
|
||||
d.bank_statement_id AS bankStatementId,
|
||||
d.trx_date AS trxDate,
|
||||
d.le_account_no AS leAccountNo,
|
||||
d.le_account_name AS leAccountName,
|
||||
d.customer_account_name AS customerAccountName,
|
||||
d.customer_account_no AS customerAccountNo,
|
||||
d.cash_type AS cashType,
|
||||
d.user_memo AS userMemo,
|
||||
d.amount,
|
||||
d.flag AS direction,
|
||||
d.family_relation_type AS familyRelationType,
|
||||
center_subject.object_key AS fromObjectKey,
|
||||
to_subject.object_key AS toObjectKey,
|
||||
CONCAT('idno_node/', center_subject.object_key) AS fromKey,
|
||||
CONCAT('idno_node/', to_subject.object_key) AS toKey,
|
||||
center_subject.name AS fromName,
|
||||
to_subject.name AS toName,
|
||||
center_subject.idnocfno AS fromIdNo,
|
||||
to_subject.idnocfno AS toIdNo
|
||||
FROM lx_fund_flow_subject_node center_subject
|
||||
INNER JOIN lx_fund_flow_own_account_edge from_own
|
||||
ON from_own.from_key = CONCAT('idno_node/', center_subject.object_key)
|
||||
INNER JOIN lx_fund_flow_detail_edge d
|
||||
ON d.from_key = from_own.to_key
|
||||
INNER JOIN lx_fund_flow_own_account_edge to_own
|
||||
ON to_own.to_key = d.to_key
|
||||
INNER JOIN lx_fund_flow_subject_node to_subject
|
||||
ON to_subject.object_key = SUBSTRING(to_own.from_key, 11)
|
||||
WHERE center_subject.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
|
||||
<include refid="detailFilter"/>
|
||||
UNION ALL
|
||||
SELECT
|
||||
d.object_key AS detailObjectKey,
|
||||
d.bank_statement_id AS bankStatementId,
|
||||
d.trx_date AS trxDate,
|
||||
d.le_account_no AS leAccountNo,
|
||||
d.le_account_name AS leAccountName,
|
||||
d.customer_account_name AS customerAccountName,
|
||||
d.customer_account_no AS customerAccountNo,
|
||||
d.cash_type AS cashType,
|
||||
d.user_memo AS userMemo,
|
||||
d.amount,
|
||||
d.flag AS direction,
|
||||
d.family_relation_type AS familyRelationType,
|
||||
from_subject.object_key AS fromObjectKey,
|
||||
center_subject.object_key AS toObjectKey,
|
||||
CONCAT('idno_node/', from_subject.object_key) AS fromKey,
|
||||
CONCAT('idno_node/', center_subject.object_key) AS toKey,
|
||||
from_subject.name AS fromName,
|
||||
center_subject.name AS toName,
|
||||
from_subject.idnocfno AS fromIdNo,
|
||||
center_subject.idnocfno AS toIdNo
|
||||
FROM lx_fund_flow_subject_node center_subject
|
||||
INNER JOIN lx_fund_flow_own_account_edge to_own
|
||||
ON to_own.from_key = CONCAT('idno_node/', center_subject.object_key)
|
||||
INNER JOIN lx_fund_flow_detail_edge d
|
||||
ON d.to_key = to_own.to_key
|
||||
INNER JOIN lx_fund_flow_own_account_edge from_own
|
||||
ON from_own.to_key = d.from_key
|
||||
INNER JOIN lx_fund_flow_subject_node from_subject
|
||||
ON from_subject.object_key = SUBSTRING(from_own.from_key, 11)
|
||||
WHERE center_subject.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
|
||||
AND from_subject.object_key != center_subject.object_key
|
||||
<include refid="detailFilter"/>
|
||||
</sql>
|
||||
|
||||
<sql id="fundGraphSubjectColumns">
|
||||
n.object_key AS objectKey,
|
||||
CONCAT('idno_node/', n.object_key) AS nodeKey,
|
||||
n.name AS nodeName,
|
||||
n.idnocfno AS idNo,
|
||||
n.cinocsno AS cinocsno,
|
||||
n.idno_type AS idnoType,
|
||||
n.staff_id AS staffId,
|
||||
n.source_type AS sourceType,
|
||||
CASE
|
||||
WHEN n.idno_type = 'NAME_PROXY' OR n.source_type LIKE '%COUNTERPARTY%' THEN 'PROXY'
|
||||
ELSE 'PERSON'
|
||||
END AS nodeType,
|
||||
CASE
|
||||
WHEN n.idnocfno IS NOT NULL AND TRIM(n.idnocfno) != '' THEN 'IDNO'
|
||||
ELSE 'NAME'
|
||||
END AS identityType,
|
||||
CASE
|
||||
WHEN n.source_type LIKE 'GRAPH_TEST_FAMILY_%' THEN REPLACE(n.source_type, 'GRAPH_TEST_FAMILY_', '')
|
||||
ELSE NULL
|
||||
END AS relationType,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM lx_fund_flow_own_account_edge own
|
||||
WHERE own.from_key = CONCAT('idno_node/', n.object_key)
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END AS canExpand,
|
||||
(
|
||||
SELECT COUNT(1)
|
||||
FROM lx_fund_flow_own_account_edge own_count
|
||||
WHERE own_count.from_key = CONCAT('idno_node/', n.object_key)
|
||||
) AS accountCount,
|
||||
DATE_FORMAT(n.created_time, '%Y-%m-%d %H:%i:%s') AS createdTime,
|
||||
DATE_FORMAT(n.updated_time, '%Y-%m-%d %H:%i:%s') AS updatedTime,
|
||||
0 AS depth,
|
||||
0 AS totalAmount,
|
||||
0 AS transactionCount
|
||||
</sql>
|
||||
|
||||
<select id="selectFundGraphSubjects" resultMap="FundGraphNodeResultMap">
|
||||
SELECT
|
||||
<include refid="fundGraphSubjectColumns"/>
|
||||
FROM lx_fund_flow_subject_node n
|
||||
WHERE n.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
|
||||
</select>
|
||||
|
||||
<select id="selectFundGraphSubjectsByExactKeyword" resultMap="FundGraphNodeResultMap">
|
||||
SELECT exact_rows.*
|
||||
FROM (
|
||||
SELECT
|
||||
<include refid="fundGraphSubjectColumns"/>,
|
||||
0 AS matchOrder
|
||||
FROM lx_fund_flow_subject_node n
|
||||
WHERE n.idnocfno = (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
|
||||
UNION ALL
|
||||
SELECT
|
||||
<include refid="fundGraphSubjectColumns"/>,
|
||||
1 AS matchOrder
|
||||
FROM lx_fund_flow_subject_node n
|
||||
WHERE n.object_key = (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
|
||||
AND (
|
||||
n.idnocfno IS NULL
|
||||
OR n.idnocfno != (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
|
||||
)
|
||||
) exact_rows
|
||||
ORDER BY exact_rows.matchOrder, exact_rows.nodeName
|
||||
LIMIT
|
||||
<choose>
|
||||
<when test="query.limit != null and query.limit > 0">
|
||||
#{query.limit}
|
||||
</when>
|
||||
<otherwise>
|
||||
20
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
|
||||
<select id="selectFundGraphSubjectsByName" resultMap="FundGraphNodeResultMap">
|
||||
SELECT
|
||||
<include refid="fundGraphSubjectColumns"/>
|
||||
FROM lx_fund_flow_subject_node n
|
||||
WHERE n.name LIKE (CONCAT('%', TRIM(#{query.keyword}), '%') COLLATE utf8mb4_general_ci)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN n.staff_id IS NOT NULL AND TRIM(n.staff_id) != '' THEN 0
|
||||
WHEN UPPER(IFNULL(n.source_type, '')) LIKE '%EMPLOYEE%' THEN 0
|
||||
WHEN n.source_type LIKE '%员工%' THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
n.name
|
||||
LIMIT
|
||||
<choose>
|
||||
<when test="query.limit != null and query.limit > 0">
|
||||
#{query.limit}
|
||||
</when>
|
||||
<otherwise>
|
||||
20
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
|
||||
<select id="selectFundGraphEdges" resultMap="FundGraphEdgeResultMap">
|
||||
SELECT
|
||||
MD5(CONCAT(graph_rows.fromKey, '|', graph_rows.toKey, '|', graph_rows.direction, '|', IFNULL(graph_rows.familyRelationType, ''))) AS edgeKey,
|
||||
graph_rows.fromKey,
|
||||
graph_rows.toKey,
|
||||
graph_rows.fromObjectKey,
|
||||
graph_rows.toObjectKey,
|
||||
MAX(graph_rows.fromName) AS fromName,
|
||||
MAX(graph_rows.toName) AS toName,
|
||||
SUM(graph_rows.amount) AS totalAmount,
|
||||
COUNT(1) AS transactionCount,
|
||||
MIN(graph_rows.trxDate) AS firstTrxDate,
|
||||
MAX(graph_rows.trxDate) AS lastTrxDate,
|
||||
graph_rows.direction,
|
||||
graph_rows.familyRelationType,
|
||||
'BANK' AS sourceType,
|
||||
NULL AS relationDesc,
|
||||
NULL AS sourceDesc,
|
||||
NULL AS remark,
|
||||
1 AS depth,
|
||||
CASE
|
||||
WHEN MAX(
|
||||
CASE
|
||||
WHEN graph_rows.fromObjectKey = #{query.objectKey} THEN
|
||||
CASE
|
||||
WHEN graph_rows.toIdNo IS NOT NULL AND TRIM(graph_rows.toIdNo) != '' THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
ELSE
|
||||
CASE
|
||||
WHEN graph_rows.fromIdNo IS NOT NULL AND TRIM(graph_rows.fromIdNo) != '' THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
END
|
||||
) = 1 THEN 1
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM lx_fund_flow_own_account_edge own
|
||||
WHERE own.from_key = CASE
|
||||
WHEN graph_rows.fromObjectKey = #{query.objectKey}
|
||||
THEN graph_rows.toKey
|
||||
ELSE graph_rows.fromKey
|
||||
END
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END AS canTrace
|
||||
FROM (
|
||||
<include refid="subjectJoinRowsByCenter"/>
|
||||
) graph_rows
|
||||
WHERE 1 = 1
|
||||
GROUP BY
|
||||
graph_rows.fromKey,
|
||||
graph_rows.toKey,
|
||||
graph_rows.fromObjectKey,
|
||||
graph_rows.toObjectKey,
|
||||
graph_rows.direction,
|
||||
graph_rows.familyRelationType
|
||||
<if test="query.minTotalAmount != null">
|
||||
HAVING SUM(graph_rows.amount) <![CDATA[ >= ]]> #{query.minTotalAmount}
|
||||
</if>
|
||||
ORDER BY totalAmount DESC, transactionCount DESC
|
||||
LIMIT #{query.limit}
|
||||
</select>
|
||||
|
||||
<select id="selectFundGraphManualEdges" resultMap="FundGraphEdgeResultMap">
|
||||
SELECT
|
||||
m.object_key AS edgeKey,
|
||||
CONCAT('idno_node/', m.from_object_key) AS fromKey,
|
||||
CONCAT('idno_node/', m.to_object_key) AS toKey,
|
||||
m.from_object_key AS fromObjectKey,
|
||||
m.to_object_key AS toObjectKey,
|
||||
COALESCE(from_subject.name, m.from_name) AS fromName,
|
||||
COALESCE(to_subject.name, m.to_name) AS toName,
|
||||
m.amount AS totalAmount,
|
||||
m.transaction_count AS transactionCount,
|
||||
DATE_FORMAT(m.created_time, '%Y-%m-%d %H:%i:%s') AS firstTrxDate,
|
||||
DATE_FORMAT(m.created_time, '%Y-%m-%d %H:%i:%s') AS lastTrxDate,
|
||||
m.direction,
|
||||
NULL AS familyRelationType,
|
||||
m.source_type AS sourceType,
|
||||
m.relation_desc AS relationDesc,
|
||||
m.source_desc AS sourceDesc,
|
||||
m.remark,
|
||||
1 AS depth,
|
||||
0 AS canTrace
|
||||
FROM lx_fund_flow_manual_edge m
|
||||
LEFT JOIN lx_fund_flow_subject_node from_subject
|
||||
ON from_subject.object_key = m.from_object_key
|
||||
LEFT JOIN lx_fund_flow_subject_node to_subject
|
||||
ON to_subject.object_key = m.to_object_key
|
||||
WHERE m.source_type = 'MANUAL'
|
||||
AND (
|
||||
m.from_object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
|
||||
OR m.to_object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
|
||||
)
|
||||
ORDER BY m.updated_time DESC
|
||||
LIMIT #{query.limit}
|
||||
</select>
|
||||
|
||||
<select id="selectFundGraphEdgeDetails" resultMap="FundGraphStatementResultMap">
|
||||
SELECT
|
||||
graph_rows.bankStatementId,
|
||||
graph_rows.trxDate,
|
||||
graph_rows.leAccountNo,
|
||||
graph_rows.leAccountName,
|
||||
graph_rows.customerAccountName,
|
||||
graph_rows.customerAccountNo,
|
||||
graph_rows.cashType,
|
||||
graph_rows.userMemo,
|
||||
graph_rows.amount,
|
||||
graph_rows.direction,
|
||||
graph_rows.familyRelationType
|
||||
FROM (
|
||||
<include refid="subjectJoinRows"/>
|
||||
) graph_rows
|
||||
WHERE graph_rows.fromKey = (#{query.fromKey} COLLATE utf8mb4_general_ci)
|
||||
AND graph_rows.toKey = (#{query.toKey} COLLATE utf8mb4_general_ci)
|
||||
<if test="query.direction != null and query.direction != ''">
|
||||
AND graph_rows.direction = (#{query.direction} COLLATE utf8mb4_general_ci)
|
||||
</if>
|
||||
ORDER BY graph_rows.trxDate DESC, graph_rows.bankStatementId DESC
|
||||
</select>
|
||||
|
||||
<select id="countSubjectByObjectKey" resultType="int">
|
||||
SELECT COUNT(1)
|
||||
FROM lx_fund_flow_subject_node
|
||||
WHERE object_key = (#{objectKey} COLLATE utf8mb4_general_ci)
|
||||
</select>
|
||||
|
||||
<insert id="insertManualSubject">
|
||||
INSERT IGNORE INTO lx_fund_flow_subject_node (
|
||||
object_key,
|
||||
idnocfno,
|
||||
name,
|
||||
idno_type,
|
||||
source_type
|
||||
) VALUES (
|
||||
#{objectKey},
|
||||
#{idNo},
|
||||
#{name},
|
||||
CASE
|
||||
WHEN #{idNo} IS NULL OR TRIM(#{idNo}) = '' THEN 'NAME_PROXY'
|
||||
ELSE 'PERSON'
|
||||
END,
|
||||
'MANUAL'
|
||||
)
|
||||
</insert>
|
||||
|
||||
<insert id="insertManualEdge">
|
||||
INSERT INTO lx_fund_flow_manual_edge (
|
||||
object_key,
|
||||
from_object_key,
|
||||
to_object_key,
|
||||
from_name,
|
||||
to_name,
|
||||
amount,
|
||||
transaction_count,
|
||||
direction,
|
||||
relation_desc,
|
||||
source_desc,
|
||||
remark,
|
||||
source_type,
|
||||
created_by,
|
||||
updated_by
|
||||
) VALUES (
|
||||
#{objectKey},
|
||||
#{dto.fromObjectKey},
|
||||
#{dto.toObjectKey},
|
||||
#{dto.fromName},
|
||||
#{dto.toName},
|
||||
#{dto.amount},
|
||||
#{dto.transactionCount},
|
||||
#{dto.direction},
|
||||
#{dto.relationDesc},
|
||||
#{dto.sourceDesc},
|
||||
#{dto.remark},
|
||||
'MANUAL',
|
||||
#{operator},
|
||||
#{operator}
|
||||
)
|
||||
</insert>
|
||||
</mapper>
|
||||
@@ -40,6 +40,9 @@
|
||||
FROM ccdi_project p
|
||||
LEFT JOIN sys_user u ON p.create_by = u.user_name AND u.del_flag = '0'
|
||||
<where>
|
||||
<if test="scope != null and !scope.viewAllProjects">
|
||||
AND p.create_by = #{scope.username}
|
||||
</if>
|
||||
<if test="queryDTO.projectName != null and queryDTO.projectName != ''">
|
||||
AND p.project_name LIKE CONCAT('%', #{queryDTO.projectName}, '%')
|
||||
</if>
|
||||
@@ -61,6 +64,9 @@
|
||||
FROM ccdi_project p
|
||||
<where>
|
||||
p.status in ('1', '2')
|
||||
<if test="scope != null and !scope.viewAllProjects">
|
||||
AND p.create_by = #{scope.username}
|
||||
</if>
|
||||
<if test="queryDTO.projectName != null and queryDTO.projectName != ''">
|
||||
AND p.project_name LIKE CONCAT('%', #{queryDTO.projectName}, '%')
|
||||
</if>
|
||||
@@ -70,7 +76,8 @@
|
||||
|
||||
<update id="updateRiskCountsByProjectId">
|
||||
update ccdi_project
|
||||
set high_risk_count = #{highRiskCount},
|
||||
set target_count = #{targetCount},
|
||||
high_risk_count = #{highRiskCount},
|
||||
medium_risk_count = #{mediumRiskCount},
|
||||
low_risk_count = #{lowRiskCount},
|
||||
update_by = #{updateBy},
|
||||
|
||||
@@ -33,6 +33,38 @@
|
||||
select="selectRiskHitTagsByScope"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="ExternalRiskModelPeopleItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO">
|
||||
<id property="idNo" column="cert_no"/>
|
||||
<result property="staffName" column="person_name"/>
|
||||
<result property="staffCode" column="subject_type"/>
|
||||
<result property="department" column="related_object"/>
|
||||
<collection property="modelNames"
|
||||
column="{projectId=project_id,certNo=cert_no,selectedModelCodes=selected_model_codes}"
|
||||
ofType="java.lang.String"
|
||||
select="selectExternalRiskModelNamesByScope"/>
|
||||
<collection property="hitTagList"
|
||||
column="{projectId=project_id,certNo=cert_no,selectedModelCodes=selected_model_codes}"
|
||||
ofType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO"
|
||||
select="selectExternalRiskHitTagsByScope"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="ExternalPersonWarningItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningItemVO">
|
||||
<result property="name" column="person_name"/>
|
||||
<result property="idNo" column="cert_no"/>
|
||||
<result property="subjectType" column="subject_type"/>
|
||||
<result property="riskLevel" column="risk_level_name"/>
|
||||
<result property="riskLevelType" column="risk_level_type"/>
|
||||
<result property="riskCount" column="risk_count"/>
|
||||
<result property="modelCount" column="model_count"/>
|
||||
<result property="riskPoint" column="risk_point"/>
|
||||
<result property="relatedObject" column="related_object"/>
|
||||
<result property="latestTradeTime" column="latest_trade_time"/>
|
||||
<collection property="riskPointTagList"
|
||||
column="{projectId=project_id,certNo=cert_no,selectedModelCodes=selected_model_codes}"
|
||||
ofType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO"
|
||||
select="selectExternalRiskHitTagsByScope"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="SuspiciousTransactionItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO">
|
||||
<id property="bankStatementId" column="bankStatementId"/>
|
||||
<result property="trxDate" column="trxDate"/>
|
||||
@@ -46,6 +78,7 @@
|
||||
<result property="displayAmount" column="displayAmount"/>
|
||||
<result property="hasModelRuleHit" column="hasModelRuleHit"/>
|
||||
<result property="hasNameListHit" column="hasNameListHit"/>
|
||||
<result property="nameListHitType" column="nameListHitType"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="AbnormalAccountItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO">
|
||||
@@ -114,6 +147,13 @@
|
||||
) tens
|
||||
</sql>
|
||||
|
||||
<sql id="externalModelCodeFilterSql">
|
||||
('EXTERNAL_LARGE_TRANSACTION',
|
||||
'EXTERNAL_ABNORMAL_TRANSACTION',
|
||||
'EXTERNAL_SUSPICIOUS_GAMBLING',
|
||||
'EXTERNAL_SUSPICIOUS_RELATION')
|
||||
</sql>
|
||||
|
||||
<sql id="resolvedEmployeeRiskBaseSql">
|
||||
select distinct
|
||||
tr.id,
|
||||
@@ -493,6 +533,408 @@
|
||||
order by result.staff_name asc, result.staff_id_card asc
|
||||
</select>
|
||||
|
||||
<select id="selectRiskModelPeopleList" resultMap="RiskModelPeopleItemResultMap">
|
||||
<bind name="projectId" value="query.projectId"/>
|
||||
select
|
||||
result.project_id,
|
||||
result.staff_id_card,
|
||||
result.staff_name,
|
||||
result.staff_code,
|
||||
result.dept_name as department,
|
||||
#{query.modelCodesCsv} as selected_model_codes
|
||||
from ccdi_project_overview_employee_result result
|
||||
where 1 = 1
|
||||
and result.project_id = #{query.projectId}
|
||||
<if test="query.modelCodes != null and query.modelCodes.size() > 0">
|
||||
<choose>
|
||||
<when test="query.matchMode == 'ALL'">
|
||||
<foreach collection="query.modelCodes" item="modelCode">
|
||||
and find_in_set(#{modelCode}, result.model_codes_csv)
|
||||
</foreach>
|
||||
</when>
|
||||
<otherwise>
|
||||
and (
|
||||
<foreach collection="query.modelCodes" item="modelCode" separator=" or ">
|
||||
find_in_set(#{modelCode}, result.model_codes_csv)
|
||||
</foreach>
|
||||
)
|
||||
</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="query.keyword != null and query.keyword != ''">
|
||||
and (
|
||||
result.staff_name like concat('%', trim(#{query.keyword}), '%')
|
||||
or result.staff_code like concat('%', trim(#{query.keyword}), '%')
|
||||
)
|
||||
</if>
|
||||
<if test="query.deptId != null">
|
||||
and result.dept_id = #{query.deptId}
|
||||
</if>
|
||||
order by result.staff_name asc, result.staff_id_card asc
|
||||
</select>
|
||||
|
||||
<sql id="externalPersonSubjectSql">
|
||||
select
|
||||
bs.project_id,
|
||||
bs.cret_no as cert_no,
|
||||
coalesce(
|
||||
max(intermediary.name),
|
||||
max(customer.name),
|
||||
max(nullif(trim(bs.LE_ACCOUNT_NAME), '')),
|
||||
'外部人员'
|
||||
) as person_name,
|
||||
case
|
||||
when max(case when intermediary.person_id is not null then 1 else 0 end) > 0 then '中介'
|
||||
when max(case when customer.person_id is not null then 1 else 0 end) > 0 then '客户'
|
||||
else '外部人员'
|
||||
end as subject_type
|
||||
from ccdi_bank_statement bs
|
||||
left join ccdi_base_staff staff
|
||||
on staff.id_card = bs.cret_no
|
||||
left join ccdi_staff_fmy_relation relation
|
||||
on relation.status = 1
|
||||
and relation.relation_cert_no = bs.cret_no
|
||||
left join ccdi_biz_intermediary intermediary
|
||||
on intermediary.person_sub_type = '本人'
|
||||
and intermediary.person_id = bs.cret_no
|
||||
left join ccdi_credit_customer_base customer
|
||||
on customer.person_id = bs.cret_no
|
||||
where bs.project_id = #{externalProjectId}
|
||||
and bs.cret_no is not null
|
||||
and trim(bs.cret_no) != ''
|
||||
and staff.id_card is null
|
||||
and relation.relation_cert_no is null
|
||||
group by bs.project_id, bs.cret_no
|
||||
</sql>
|
||||
|
||||
<sql id="externalPersonSourceSql">
|
||||
select
|
||||
subject.project_id,
|
||||
subject.cert_no,
|
||||
subject.person_name,
|
||||
subject.subject_type,
|
||||
bs.bank_statement_id,
|
||||
bs.TRX_DATE as trx_date,
|
||||
bs.CUSTOMER_ACCOUNT_NAME as customer_account_name,
|
||||
bs.customer_cert_no,
|
||||
case
|
||||
when counter_account.owner_type = 'EMPLOYEE' then '员工'
|
||||
when counter_account.owner_type = 'RELATION' then '员工亲属'
|
||||
when counter_account.owner_type = 'CREDIT_CUSTOMER' then '信贷客户'
|
||||
when counter_account.owner_type = 'INTERMEDIARY' then '中介库人员'
|
||||
when counter_staff.id_card is not null then '员工'
|
||||
when counter_relation.relation_cert_no is not null then '员工亲属'
|
||||
when counter_intermediary.person_id is not null then '中介库人员'
|
||||
else null
|
||||
end as related_object,
|
||||
tr.model_code,
|
||||
tr.model_name,
|
||||
tr.rule_code,
|
||||
tr.rule_name,
|
||||
tr.risk_level,
|
||||
tr.reason_detail
|
||||
from (
|
||||
<include refid="externalPersonSubjectSql"/>
|
||||
) subject
|
||||
inner join ccdi_bank_statement bs
|
||||
on bs.project_id = subject.project_id
|
||||
and bs.cret_no = subject.cert_no
|
||||
inner join ccdi_bank_statement_tag_result tr
|
||||
on tr.project_id = bs.project_id
|
||||
and tr.bank_statement_id = bs.bank_statement_id
|
||||
and tr.model_code in <include refid="externalModelCodeFilterSql"/>
|
||||
left join ccdi_account_info counter_account
|
||||
on trim(bs.CUSTOMER_ACCOUNT_NO) != ''
|
||||
and counter_account.account_no = trim(bs.CUSTOMER_ACCOUNT_NO)
|
||||
and counter_account.owner_type in ('EMPLOYEE', 'RELATION', 'INTERMEDIARY', 'CREDIT_CUSTOMER')
|
||||
left join ccdi_base_staff counter_staff
|
||||
on counter_account.account_no is null
|
||||
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
|
||||
and counter_staff.name = trim(bs.CUSTOMER_ACCOUNT_NAME)
|
||||
left join ccdi_staff_fmy_relation counter_relation
|
||||
on counter_account.account_no is null
|
||||
and counter_relation.status = 1
|
||||
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
|
||||
and counter_relation.relation_name = trim(bs.CUSTOMER_ACCOUNT_NAME)
|
||||
left join ccdi_biz_intermediary counter_intermediary
|
||||
on counter_account.account_no is null
|
||||
and counter_intermediary.person_sub_type = '本人'
|
||||
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
|
||||
and counter_intermediary.name = trim(bs.CUSTOMER_ACCOUNT_NAME)
|
||||
where trim(ifnull(bs.LE_ACCOUNT_NAME, '')) != trim(ifnull(bs.CUSTOMER_ACCOUNT_NAME, ''))
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
subject.project_id,
|
||||
subject.cert_no,
|
||||
subject.person_name,
|
||||
subject.subject_type,
|
||||
null as bank_statement_id,
|
||||
null as trx_date,
|
||||
null as customer_account_name,
|
||||
null as customer_cert_no,
|
||||
'资金' as related_object,
|
||||
tr.model_code,
|
||||
tr.model_name,
|
||||
tr.rule_code,
|
||||
tr.rule_name,
|
||||
tr.risk_level,
|
||||
tr.reason_detail
|
||||
from (
|
||||
<include refid="externalPersonSubjectSql"/>
|
||||
) subject
|
||||
inner join ccdi_bank_statement_tag_result tr
|
||||
on tr.project_id = subject.project_id
|
||||
and tr.object_type = 'EXTERNAL_CERT_NO'
|
||||
and tr.object_key = subject.cert_no
|
||||
and tr.model_code in <include refid="externalModelCodeFilterSql"/>
|
||||
</sql>
|
||||
|
||||
<sql id="externalPersonAggregateSql">
|
||||
select
|
||||
source.project_id,
|
||||
source.cert_no,
|
||||
max(source.person_name) as person_name,
|
||||
max(source.subject_type) as subject_type,
|
||||
count(*) as risk_count,
|
||||
count(distinct source.model_code) as model_count,
|
||||
group_concat(distinct source.rule_name order by source.rule_name separator '、') as risk_point,
|
||||
group_concat(distinct source.related_object order by source.related_object separator '、') as related_object,
|
||||
max(source.trx_date) as latest_trade_time,
|
||||
case
|
||||
when sum(case when source.risk_level = 'HIGH' then 1 else 0 end) > 0 then 'HIGH'
|
||||
when sum(case when source.risk_level = 'MEDIUM' then 1 else 0 end) > 0 then 'MEDIUM'
|
||||
else 'LOW'
|
||||
end as risk_level_code,
|
||||
null as selected_model_codes
|
||||
from (
|
||||
<include refid="externalPersonSourceSql"/>
|
||||
) source
|
||||
group by source.project_id, source.cert_no
|
||||
</sql>
|
||||
|
||||
<sql id="externalPersonWarningSelectSql">
|
||||
select
|
||||
agg.project_id,
|
||||
agg.cert_no,
|
||||
agg.person_name,
|
||||
agg.subject_type,
|
||||
agg.risk_count,
|
||||
agg.model_count,
|
||||
agg.risk_point,
|
||||
coalesce(agg.related_object, '-') as related_object,
|
||||
agg.latest_trade_time,
|
||||
agg.risk_level_code,
|
||||
case
|
||||
when agg.risk_level_code = 'HIGH' then '高风险'
|
||||
when agg.risk_level_code = 'MEDIUM' then '中风险'
|
||||
else '低风险'
|
||||
end as risk_level_name,
|
||||
case
|
||||
when agg.risk_level_code = 'HIGH' then 'danger'
|
||||
when agg.risk_level_code = 'MEDIUM' then 'warning'
|
||||
else 'info'
|
||||
end as risk_level_type,
|
||||
case
|
||||
when agg.risk_level_code = 'HIGH' then 1
|
||||
when agg.risk_level_code = 'MEDIUM' then 2
|
||||
else 3
|
||||
end as risk_level_sort,
|
||||
agg.selected_model_codes
|
||||
from (
|
||||
<include refid="externalPersonAggregateSql"/>
|
||||
) agg
|
||||
</sql>
|
||||
|
||||
<select id="selectExternalPersonWarningPage" resultMap="ExternalPersonWarningItemResultMap">
|
||||
<bind name="externalProjectId" value="query.projectId"/>
|
||||
select *
|
||||
from (
|
||||
<include refid="externalPersonWarningSelectSql"/>
|
||||
) warning
|
||||
order by warning.risk_level_sort asc, warning.model_count desc, warning.risk_count desc, warning.latest_trade_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectExternalPersonWarningList" resultMap="ExternalPersonWarningItemResultMap">
|
||||
<bind name="externalProjectId" value="projectId"/>
|
||||
select *
|
||||
from (
|
||||
<include refid="externalPersonWarningSelectSql"/>
|
||||
) warning
|
||||
order by warning.risk_level_sort asc, warning.model_count desc, warning.risk_count desc, warning.latest_trade_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectExternalRiskSummaryByProjectId" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO">
|
||||
<bind name="externalProjectId" value="projectId"/>
|
||||
select
|
||||
count(*) as total,
|
||||
coalesce(sum(case when risk.risk_level_code = 'HIGH' then 1 else 0 end), 0) as high,
|
||||
coalesce(sum(case when risk.risk_level_code = 'MEDIUM' then 1 else 0 end), 0) as medium,
|
||||
coalesce(sum(case when risk.risk_level_code = 'LOW' then 1 else 0 end), 0) as low,
|
||||
coalesce(sum(case when risk.risk_level_code is null then 1 else 0 end), 0) as noRisk
|
||||
from (
|
||||
<include refid="externalPersonSubjectSql"/>
|
||||
) subject
|
||||
left join (
|
||||
<include refid="externalPersonAggregateSql"/>
|
||||
) risk
|
||||
on risk.project_id = subject.project_id
|
||||
and risk.cert_no = subject.cert_no
|
||||
</select>
|
||||
|
||||
<select id="selectExternalRiskModelCardsByProjectId" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO">
|
||||
<bind name="externalProjectId" value="projectId"/>
|
||||
select
|
||||
model_scope.model_code,
|
||||
max(model_scope.model_name) as model_name,
|
||||
count(*) as warning_count,
|
||||
count(distinct model_scope.cert_no) as people_count
|
||||
from (
|
||||
<include refid="externalPersonSourceSql"/>
|
||||
) model_scope
|
||||
group by model_scope.model_code
|
||||
order by warning_count desc, model_scope.model_code asc
|
||||
</select>
|
||||
|
||||
<select id="selectExternalRiskModelPeoplePage" resultMap="ExternalRiskModelPeopleItemResultMap">
|
||||
<bind name="externalProjectId" value="query.projectId"/>
|
||||
select
|
||||
warning.project_id,
|
||||
warning.cert_no,
|
||||
warning.person_name,
|
||||
warning.subject_type,
|
||||
warning.related_object,
|
||||
#{query.modelCodesCsv} as selected_model_codes
|
||||
from (
|
||||
<include refid="externalPersonWarningSelectSql"/>
|
||||
) warning
|
||||
where 1 = 1
|
||||
<if test="query.modelCodes != null and query.modelCodes.size() > 0">
|
||||
<choose>
|
||||
<when test="query.matchMode == 'ALL'">
|
||||
<foreach collection="query.modelCodes" item="modelCode">
|
||||
and exists (
|
||||
select 1
|
||||
from (
|
||||
<include refid="externalPersonSourceSql"/>
|
||||
) source
|
||||
where source.cert_no = warning.cert_no
|
||||
and source.model_code = #{modelCode}
|
||||
)
|
||||
</foreach>
|
||||
</when>
|
||||
<otherwise>
|
||||
and exists (
|
||||
select 1
|
||||
from (
|
||||
<include refid="externalPersonSourceSql"/>
|
||||
) source
|
||||
where source.cert_no = warning.cert_no
|
||||
and source.model_code in
|
||||
<foreach collection="query.modelCodes" item="modelCode" open="(" separator="," close=")">
|
||||
#{modelCode}
|
||||
</foreach>
|
||||
)
|
||||
</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="query.keyword != null and query.keyword != ''">
|
||||
and (
|
||||
warning.person_name like concat('%', trim(#{query.keyword}), '%')
|
||||
or warning.cert_no like concat('%', trim(#{query.keyword}), '%')
|
||||
)
|
||||
</if>
|
||||
order by warning.person_name asc, warning.cert_no asc
|
||||
</select>
|
||||
|
||||
<select id="selectExternalRiskModelPeopleList" resultMap="ExternalRiskModelPeopleItemResultMap">
|
||||
<bind name="externalProjectId" value="query.projectId"/>
|
||||
select
|
||||
warning.project_id,
|
||||
warning.cert_no,
|
||||
warning.person_name,
|
||||
warning.subject_type,
|
||||
warning.related_object,
|
||||
#{query.modelCodesCsv} as selected_model_codes
|
||||
from (
|
||||
<include refid="externalPersonWarningSelectSql"/>
|
||||
) warning
|
||||
where 1 = 1
|
||||
<if test="query.modelCodes != null and query.modelCodes.size() > 0">
|
||||
<choose>
|
||||
<when test="query.matchMode == 'ALL'">
|
||||
<foreach collection="query.modelCodes" item="modelCode">
|
||||
and exists (
|
||||
select 1
|
||||
from (
|
||||
<include refid="externalPersonSourceSql"/>
|
||||
) source
|
||||
where source.cert_no = warning.cert_no
|
||||
and source.model_code = #{modelCode}
|
||||
)
|
||||
</foreach>
|
||||
</when>
|
||||
<otherwise>
|
||||
and exists (
|
||||
select 1
|
||||
from (
|
||||
<include refid="externalPersonSourceSql"/>
|
||||
) source
|
||||
where source.cert_no = warning.cert_no
|
||||
and source.model_code in
|
||||
<foreach collection="query.modelCodes" item="modelCode" open="(" separator="," close=")">
|
||||
#{modelCode}
|
||||
</foreach>
|
||||
)
|
||||
</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="query.keyword != null and query.keyword != ''">
|
||||
and (
|
||||
warning.person_name like concat('%', trim(#{query.keyword}), '%')
|
||||
or warning.cert_no like concat('%', trim(#{query.keyword}), '%')
|
||||
)
|
||||
</if>
|
||||
order by warning.person_name asc, warning.cert_no asc
|
||||
</select>
|
||||
|
||||
<select id="selectExternalRiskModelNamesByScope" resultType="java.lang.String">
|
||||
<bind name="externalProjectId" value="projectId"/>
|
||||
select distinct source.model_name
|
||||
from (
|
||||
<include refid="externalPersonSourceSql"/>
|
||||
) source
|
||||
where source.cert_no = #{certNo}
|
||||
<if test="selectedModelCodes != null and selectedModelCodes != ''">
|
||||
and find_in_set(source.model_code, #{selectedModelCodes})
|
||||
</if>
|
||||
order by source.model_name asc
|
||||
</select>
|
||||
|
||||
<select id="selectExternalRiskHitTagsByScope" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO">
|
||||
<bind name="externalProjectId" value="projectId"/>
|
||||
select
|
||||
source.model_code as modelCode,
|
||||
max(source.model_name) as modelName,
|
||||
source.rule_code as ruleCode,
|
||||
max(source.rule_name) as ruleName,
|
||||
max(source.risk_level) as riskLevel,
|
||||
coalesce(
|
||||
max(case when source.bank_statement_id is null then nullif(source.reason_detail, '') end),
|
||||
max(nullif(source.reason_detail, ''))
|
||||
) as reasonDetail
|
||||
from (
|
||||
<include refid="externalPersonSourceSql"/>
|
||||
) source
|
||||
where source.cert_no = #{certNo}
|
||||
<if test="selectedModelCodes != null and selectedModelCodes != ''">
|
||||
and find_in_set(source.model_code, #{selectedModelCodes})
|
||||
</if>
|
||||
group by source.model_code, source.rule_code
|
||||
order by source.model_code asc, source.rule_code asc
|
||||
</select>
|
||||
|
||||
<sql id="suspiciousTransactionBaseSql">
|
||||
select
|
||||
bs.bank_statement_id as bankStatementId,
|
||||
@@ -530,57 +972,70 @@
|
||||
from ccdi_bank_statement_tag_result tr
|
||||
where tr.project_id = #{query.projectId}
|
||||
and tr.bank_statement_id is not null
|
||||
and tr.rule_name like '%可疑%'
|
||||
</sql>
|
||||
|
||||
<sql id="suspiciousTransactionNameHitSql">
|
||||
select
|
||||
hits.bankStatementId,
|
||||
hits.suspiciousPersonName,
|
||||
hits.matchPriority
|
||||
hits.matchPriority,
|
||||
hits.nameListHitType
|
||||
from (
|
||||
select
|
||||
bs.bank_statement_id as bankStatementId,
|
||||
intermediary.name as suspiciousPersonName,
|
||||
1 as matchPriority
|
||||
coalesce(credit_customer.name, account.account_name, '信贷客户账号') as suspiciousPersonName,
|
||||
1 as matchPriority,
|
||||
'信贷客户' as nameListHitType
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_biz_intermediary intermediary
|
||||
on trim(bs.customer_cert_no) != ''
|
||||
and intermediary.person_id = bs.customer_cert_no
|
||||
inner join ccdi_account_info account
|
||||
on trim(bs.customer_account_no) != ''
|
||||
and account.owner_type = 'CREDIT_CUSTOMER'
|
||||
and account.account_no = bs.customer_account_no
|
||||
left join ccdi_credit_customer_base credit_customer
|
||||
on credit_customer.person_id = account.owner_id
|
||||
where bs.project_id = #{query.projectId}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
bs.bank_statement_id as bankStatementId,
|
||||
enterprise.enterprise_name as suspiciousPersonName,
|
||||
2 as matchPriority
|
||||
coalesce(intermediary.name, enterprise.enterprise_name, account.account_name, '中介账号') as suspiciousPersonName,
|
||||
2 as matchPriority,
|
||||
'中介' as nameListHitType
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_enterprise_base_info enterprise
|
||||
on trim(bs.customer_social_credit_code) != ''
|
||||
and enterprise.social_credit_code = bs.customer_social_credit_code
|
||||
and enterprise.risk_level = '1'
|
||||
and enterprise.ent_source = 'INTERMEDIARY'
|
||||
inner join ccdi_account_info account
|
||||
on trim(bs.customer_account_no) != ''
|
||||
and account.owner_type = 'INTERMEDIARY'
|
||||
and account.account_no = bs.customer_account_no
|
||||
left join ccdi_biz_intermediary intermediary
|
||||
on intermediary.person_id = account.owner_id
|
||||
left join ccdi_enterprise_base_info enterprise
|
||||
on enterprise.social_credit_code = account.owner_id
|
||||
where bs.project_id = #{query.projectId}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
bs.bank_statement_id as bankStatementId,
|
||||
intermediary.name as suspiciousPersonName,
|
||||
3 as matchPriority
|
||||
3 as matchPriority,
|
||||
'中介' as nameListHitType
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_biz_intermediary intermediary
|
||||
on trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
|
||||
and intermediary.name = bs.CUSTOMER_ACCOUNT_NAME
|
||||
where bs.project_id = #{query.projectId}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
bs.bank_statement_id as bankStatementId,
|
||||
enterprise.enterprise_name as suspiciousPersonName,
|
||||
3 as matchPriority
|
||||
4 as matchPriority,
|
||||
'中介' as nameListHitType
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_enterprise_base_info enterprise
|
||||
on trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
|
||||
@@ -588,9 +1043,38 @@
|
||||
and enterprise.risk_level = '1'
|
||||
and enterprise.ent_source = 'INTERMEDIARY'
|
||||
where bs.project_id = #{query.projectId}
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
|
||||
) hits
|
||||
</sql>
|
||||
|
||||
<sql id="externalSuspiciousTransactionSql">
|
||||
select
|
||||
bs.bank_statement_id as bankStatementId,
|
||||
bs.TRX_DATE as trxDate,
|
||||
source.person_name as relatedPersonName,
|
||||
null as relatedStaffName,
|
||||
null as relatedStaffCode,
|
||||
source.subject_type as relationType,
|
||||
bs.USER_MEMO as userMemo,
|
||||
bs.CASH_TYPE as cashType,
|
||||
case
|
||||
when ifnull(bs.AMOUNT_CR, 0) > 0 then bs.AMOUNT_CR
|
||||
when ifnull(bs.AMOUNT_DR, 0) > 0 then -bs.AMOUNT_DR
|
||||
else 0
|
||||
end as displayAmount,
|
||||
1 as hasModelRuleHit,
|
||||
0 as hasNameListHit,
|
||||
source.person_name as suspiciousPersonName,
|
||||
9 as matchPriority,
|
||||
'外部人员预警' as nameListHitType
|
||||
from (
|
||||
<bind name="externalProjectId" value="query.projectId"/>
|
||||
<include refid="externalPersonSourceSql"/>
|
||||
) source
|
||||
inner join ccdi_bank_statement bs
|
||||
on bs.bank_statement_id = source.bank_statement_id
|
||||
</sql>
|
||||
|
||||
<sql id="suspiciousTransactionMergedSql">
|
||||
select
|
||||
base.bankStatementId,
|
||||
@@ -605,7 +1089,8 @@
|
||||
1 as hasModelRuleHit,
|
||||
0 as hasNameListHit,
|
||||
null as suspiciousPersonName,
|
||||
null as matchPriority
|
||||
null as matchPriority,
|
||||
null as nameListHitType
|
||||
from (
|
||||
<include refid="suspiciousTransactionBaseSql"/>
|
||||
) base
|
||||
@@ -628,13 +1113,35 @@
|
||||
0 as hasModelRuleHit,
|
||||
1 as hasNameListHit,
|
||||
name_hits.suspiciousPersonName,
|
||||
name_hits.matchPriority
|
||||
name_hits.matchPriority,
|
||||
name_hits.nameListHitType
|
||||
from (
|
||||
<include refid="suspiciousTransactionBaseSql"/>
|
||||
) base
|
||||
inner join (
|
||||
<include refid="suspiciousTransactionNameHitSql"/>
|
||||
) name_hits on name_hits.bankStatementId = base.bankStatementId
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
external_hits.bankStatementId,
|
||||
external_hits.trxDate,
|
||||
external_hits.relatedPersonName,
|
||||
external_hits.relatedStaffName,
|
||||
external_hits.relatedStaffCode,
|
||||
external_hits.relationType,
|
||||
external_hits.userMemo,
|
||||
external_hits.cashType,
|
||||
external_hits.displayAmount,
|
||||
external_hits.hasModelRuleHit,
|
||||
external_hits.hasNameListHit,
|
||||
external_hits.suspiciousPersonName,
|
||||
external_hits.matchPriority,
|
||||
external_hits.nameListHitType
|
||||
from (
|
||||
<include refid="externalSuspiciousTransactionSql"/>
|
||||
) external_hits
|
||||
</sql>
|
||||
|
||||
<sql id="suspiciousTransactionAggregatedSql">
|
||||
@@ -663,7 +1170,18 @@
|
||||
max(merged.cashType) as cashType,
|
||||
max(merged.displayAmount) as displayAmount,
|
||||
max(merged.hasModelRuleHit) as hasModelRuleHit,
|
||||
max(merged.hasNameListHit) as hasNameListHit
|
||||
max(merged.hasNameListHit) as hasNameListHit,
|
||||
substring_index(
|
||||
min(
|
||||
case
|
||||
when merged.nameListHitType is not null and merged.nameListHitType != ''
|
||||
then concat(lpad(merged.matchPriority, 2, '0'), '|', merged.nameListHitType)
|
||||
else null
|
||||
end
|
||||
),
|
||||
'|',
|
||||
-1
|
||||
) as nameListHitType
|
||||
from (
|
||||
<include refid="suspiciousTransactionMergedSql"/>
|
||||
) merged
|
||||
@@ -678,6 +1196,9 @@
|
||||
<when test="query.suspiciousType == 'MODEL_RULE'">
|
||||
where final_result.hasModelRuleHit = 1
|
||||
</when>
|
||||
<when test="query.suspiciousType == 'EXTERNAL_PERSON'">
|
||||
where final_result.nameListHitType = '外部人员预警'
|
||||
</when>
|
||||
<otherwise>
|
||||
where final_result.hasModelRuleHit = 1 or final_result.hasNameListHit = 1
|
||||
</otherwise>
|
||||
@@ -685,7 +1206,7 @@
|
||||
</sql>
|
||||
|
||||
<select id="selectSuspiciousTransactionPage" resultMap="SuspiciousTransactionItemResultMap">
|
||||
<!-- rule_name like '%可疑%' -->
|
||||
<!-- ccdi_bank_statement_tag_result -->
|
||||
<!-- ccdi_biz_intermediary -->
|
||||
<!-- ccdi_enterprise_base_info -->
|
||||
<!-- group by merged.bankStatementId -->
|
||||
@@ -701,7 +1222,8 @@
|
||||
final_result.cashType,
|
||||
final_result.displayAmount,
|
||||
final_result.hasModelRuleHit,
|
||||
final_result.hasNameListHit
|
||||
final_result.hasNameListHit,
|
||||
final_result.nameListHitType
|
||||
from (
|
||||
<include refid="suspiciousTransactionAggregatedSql"/>
|
||||
) final_result
|
||||
@@ -722,7 +1244,8 @@
|
||||
final_result.cashType,
|
||||
final_result.displayAmount,
|
||||
final_result.hasModelRuleHit,
|
||||
final_result.hasNameListHit
|
||||
final_result.hasNameListHit,
|
||||
final_result.nameListHitType
|
||||
from (
|
||||
<include refid="suspiciousTransactionAggregatedSql"/>
|
||||
) final_result
|
||||
@@ -742,7 +1265,21 @@
|
||||
final_result.relatedStaffCode,
|
||||
final_result.userMemo,
|
||||
final_result.cashType,
|
||||
tag_result.hitTags,
|
||||
case
|
||||
when final_result.nameListHitType = '中介' then
|
||||
replace(
|
||||
replace(ifnull(tag_result.hitTags, ''), '与客户之间非正常资金往来', '疑似与中介往来'),
|
||||
'异常交易',
|
||||
'疑似与中介往来'
|
||||
)
|
||||
when final_result.nameListHitType = '信贷客户' then
|
||||
replace(
|
||||
replace(ifnull(tag_result.hitTags, ''), '与客户之间非正常资金往来', '与信贷客户之间非正常资金往来'),
|
||||
'异常交易',
|
||||
'与信贷客户之间非正常资金往来'
|
||||
)
|
||||
else tag_result.hitTags
|
||||
end as hitTags,
|
||||
final_result.displayAmount
|
||||
from (
|
||||
<include refid="suspiciousTransactionAggregatedSql"/>
|
||||
@@ -907,7 +1444,8 @@
|
||||
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelName')))) as model_name,
|
||||
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode'))) as rule_code,
|
||||
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleName')))) as rule_name,
|
||||
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].riskLevel')))) as risk_level
|
||||
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].riskLevel')))) as risk_level,
|
||||
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].reasonDetail')))) as reason_detail
|
||||
from ccdi_project_overview_employee_result result
|
||||
join (
|
||||
<include refid="jsonArrayIndexSql"/>
|
||||
@@ -1029,4 +1567,5 @@
|
||||
group by base.staff_id_card
|
||||
) agg
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<result property="spouseTotalAsset" column="asset_spouse_total_asset"/>
|
||||
<result property="totalAsset" column="asset_total_asset"/>
|
||||
<collection property="items"
|
||||
column="{projectId=project_id,staffIdCard=staff_id_card,spouseIdCard=spouse_id_card}"
|
||||
column="{projectId=project_id,staffIdCard=staff_id_card,spouseIdCard=spouse_id_card,spouseIsStaff=spouse_is_staff}"
|
||||
ofType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetItemVO"
|
||||
select="selectFamilyAssetItemsByScope"/>
|
||||
</association>
|
||||
@@ -93,7 +93,7 @@
|
||||
<result property="spouseTotalDebt" column="debt_spouse_total_debt"/>
|
||||
<result property="totalDebt" column="debt_total_debt"/>
|
||||
<collection property="items"
|
||||
column="{projectId=project_id,staffIdCard=staff_id_card,spouseIdCard=spouse_id_card}"
|
||||
column="{projectId=project_id,staffIdCard=staff_id_card,spouseIdCard=spouse_id_card,spouseIsStaff=spouse_is_staff}"
|
||||
ofType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtItemVO"
|
||||
select="selectFamilyDebtItemsByScope"/>
|
||||
</association>
|
||||
@@ -114,45 +114,55 @@
|
||||
|
||||
<sql id="projectEmployeeScopeSql">
|
||||
select distinct
|
||||
coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) as staff_id_card,
|
||||
cast(coalesce(direct_staff.staff_id, statement_staff.staff_id, family_staff.staff_id) as char) as staff_code,
|
||||
coalesce(direct_staff.name, statement_staff.name, family_staff.name) as staff_name,
|
||||
statement_staff.id_card as staff_id_card,
|
||||
cast(statement_staff.staff_id as char) as staff_code,
|
||||
statement_staff.name as staff_name,
|
||||
dept.dept_name
|
||||
from ccdi_bank_statement_tag_result tr
|
||||
left join ccdi_base_staff direct_staff
|
||||
on tr.object_type = 'STAFF_ID_CARD'
|
||||
and tr.object_key = direct_staff.id_card
|
||||
left join ccdi_bank_statement bs
|
||||
on tr.bank_statement_id = bs.bank_statement_id
|
||||
left join ccdi_base_staff statement_staff
|
||||
on (tr.object_key is null or tr.object_key = '')
|
||||
and bs.cret_no = statement_staff.id_card
|
||||
left join ccdi_staff_fmy_relation relation
|
||||
on relation.status = 1
|
||||
and (
|
||||
((tr.object_key is null or tr.object_key = '') and bs.cret_no = relation.relation_cert_no)
|
||||
or ((tr.object_key is not null and tr.object_key != '') and tr.object_type != 'STAFF_ID_CARD'
|
||||
and tr.object_key = relation.relation_cert_no)
|
||||
)
|
||||
left join ccdi_base_staff family_staff
|
||||
on relation.person_id = family_staff.id_card
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_base_staff statement_staff
|
||||
on statement_staff.id_card = trim(bs.cret_no)
|
||||
left join sys_dept dept
|
||||
on dept.dept_id = coalesce(direct_staff.dept_id, statement_staff.dept_id, family_staff.dept_id)
|
||||
where tr.project_id = #{projectId}
|
||||
and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null
|
||||
on dept.dept_id = statement_staff.dept_id
|
||||
where bs.project_id = #{projectId}
|
||||
and bs.cret_no is not null
|
||||
and trim(bs.cret_no) != ''
|
||||
</sql>
|
||||
|
||||
<sql id="spouseRelationSql">
|
||||
select
|
||||
person_id,
|
||||
max(relation_name) as spouse_name,
|
||||
min(relation_cert_no) as spouse_id_card,
|
||||
max(annual_income) as spouse_income
|
||||
from ccdi_staff_fmy_relation
|
||||
where status = 1
|
||||
and is_emp_family = 1
|
||||
and relation_type = '配偶'
|
||||
group by person_id
|
||||
relation_pair.person_id,
|
||||
max(relation_pair.spouse_name) as spouse_name,
|
||||
min(relation_pair.spouse_id_card) as spouse_id_card,
|
||||
max(coalesce(spouse_staff.annual_income, relation_pair.spouse_relation_income, 0)) as spouse_income,
|
||||
max(case when spouse_staff.id_card is not null then 1 else 0 end) as spouse_is_staff
|
||||
from (
|
||||
select
|
||||
relation.person_id,
|
||||
relation.relation_name as spouse_name,
|
||||
relation.relation_cert_no as spouse_id_card,
|
||||
relation.annual_income as spouse_relation_income
|
||||
from ccdi_staff_fmy_relation relation
|
||||
where relation.status = 1
|
||||
and relation.is_emp_family = 1
|
||||
and relation.relation_type = '配偶'
|
||||
union all
|
||||
select
|
||||
relation.relation_cert_no as person_id,
|
||||
base_staff.name as spouse_name,
|
||||
relation.person_id as spouse_id_card,
|
||||
null as spouse_relation_income
|
||||
from ccdi_staff_fmy_relation relation
|
||||
inner join ccdi_base_staff current_staff
|
||||
on current_staff.id_card = relation.relation_cert_no
|
||||
left join ccdi_base_staff base_staff
|
||||
on base_staff.id_card = relation.person_id
|
||||
where relation.status = 1
|
||||
and relation.is_emp_family = 1
|
||||
and relation.relation_type = '配偶'
|
||||
) relation_pair
|
||||
left join ccdi_base_staff spouse_staff
|
||||
on spouse_staff.id_card = relation_pair.spouse_id_card
|
||||
group by relation_pair.person_id
|
||||
</sql>
|
||||
|
||||
<select id="selectFamilyAssetLiabilityList" resultMap="FamilyAssetLiabilityListItemResultMap">
|
||||
@@ -166,14 +176,14 @@
|
||||
aggregated.total_debt,
|
||||
aggregated.comparison_amount,
|
||||
case
|
||||
when aggregated.self_asset_record_count = 0 or aggregated.self_debt_record_count = 0 then 'MISSING_INFO'
|
||||
when aggregated.missing_asset_info = 1 or aggregated.missing_debt_info = 1 then 'MISSING_INFO'
|
||||
when comparison_amount <= total_asset * 1.5 then 'NORMAL'
|
||||
when comparison_amount > total_asset * 1.5 and comparison_amount <= total_asset * 3 then 'RISK'
|
||||
when comparison_amount > total_asset * 3 then 'HIGH'
|
||||
else 'HIGH'
|
||||
end as risk_level_code,
|
||||
case
|
||||
when aggregated.self_asset_record_count = 0 or aggregated.self_debt_record_count = 0 then '缺少信息'
|
||||
when aggregated.missing_asset_info = 1 or aggregated.missing_debt_info = 1 then '缺少信息'
|
||||
when comparison_amount <= total_asset * 1.5 then '正常'
|
||||
when comparison_amount > total_asset * 1.5 and comparison_amount <= total_asset * 3 then '存在风险'
|
||||
when comparison_amount > total_asset * 3 then '高风险'
|
||||
@@ -181,122 +191,106 @@
|
||||
end as risk_level_name
|
||||
from (
|
||||
select
|
||||
scope.staff_id_card,
|
||||
scope.staff_code,
|
||||
scope.staff_name,
|
||||
scope.dept_name,
|
||||
coalesce(base_staff.annual_income, 0) + coalesce(spouse.spouse_income, 0) as total_income,
|
||||
coalesce((
|
||||
select count(1)
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and asset.person_id = scope.staff_id_card
|
||||
), 0) as self_asset_record_count,
|
||||
coalesce((
|
||||
select count(1)
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
), 0) as self_debt_record_count,
|
||||
coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and (
|
||||
asset.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and asset.person_id = spouse.spouse_id_card)
|
||||
)
|
||||
), 0) as total_asset,
|
||||
coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as total_debt,
|
||||
coalesce(base_staff.annual_income, 0)
|
||||
+ coalesce(spouse.spouse_income, 0)
|
||||
+ coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as comparison_amount,
|
||||
source.*,
|
||||
case
|
||||
when coalesce((
|
||||
when source.self_asset_record_count = 0
|
||||
or source.spouse_staff_asset_record_count = 0 then 1
|
||||
else 0
|
||||
end as missing_asset_info,
|
||||
case
|
||||
when source.self_debt_record_count = 0
|
||||
or source.spouse_staff_debt_record_count = 0 then 1
|
||||
else 0
|
||||
end as missing_debt_info,
|
||||
case
|
||||
when source.self_asset_record_count = 0
|
||||
or source.spouse_staff_asset_record_count = 0
|
||||
or source.self_debt_record_count = 0
|
||||
or source.spouse_staff_debt_record_count = 0 then 4
|
||||
when source.comparison_amount <= source.total_asset * 1.5 then 1
|
||||
when source.comparison_amount <= source.total_asset * 3 then 2
|
||||
when source.comparison_amount > source.total_asset * 3 then 3
|
||||
else 3
|
||||
end as risk_level_sort
|
||||
from (
|
||||
select
|
||||
scope.staff_id_card,
|
||||
scope.staff_code,
|
||||
scope.staff_name,
|
||||
scope.dept_name,
|
||||
coalesce(base_staff.annual_income, 0) + coalesce(spouse.spouse_income, 0) as total_income,
|
||||
coalesce((
|
||||
select count(1)
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and asset.person_id = scope.staff_id_card
|
||||
), 0) = 0
|
||||
or coalesce((
|
||||
), 0) as self_asset_record_count,
|
||||
case
|
||||
when spouse.spouse_is_staff = 1 then coalesce((
|
||||
select count(1)
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = spouse.spouse_id_card
|
||||
and asset.person_id = spouse.spouse_id_card
|
||||
), 0)
|
||||
else 1
|
||||
end as spouse_staff_asset_record_count,
|
||||
coalesce((
|
||||
select count(1)
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
), 0) as self_debt_record_count,
|
||||
case
|
||||
when spouse.spouse_is_staff = 1 then coalesce((
|
||||
select count(1)
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
), 0) = 0 then 4
|
||||
when (
|
||||
coalesce(base_staff.annual_income, 0)
|
||||
where debt.person_id = spouse.spouse_id_card
|
||||
), 0)
|
||||
else 1
|
||||
end as spouse_staff_debt_record_count,
|
||||
coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where (asset.family_id = scope.staff_id_card and asset.person_id = scope.staff_id_card)
|
||||
or (
|
||||
spouse.spouse_id_card is not null
|
||||
and (
|
||||
(
|
||||
spouse.spouse_is_staff = 1
|
||||
and asset.family_id = spouse.spouse_id_card
|
||||
and asset.person_id = spouse.spouse_id_card
|
||||
)
|
||||
or (
|
||||
(spouse.spouse_is_staff is null or spouse.spouse_is_staff != 1)
|
||||
and asset.family_id = scope.staff_id_card
|
||||
and asset.person_id = spouse.spouse_id_card
|
||||
)
|
||||
)
|
||||
)
|
||||
), 0) as total_asset,
|
||||
coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as total_debt,
|
||||
coalesce(base_staff.annual_income, 0)
|
||||
+ coalesce(spouse.spouse_income, 0)
|
||||
+ coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0)
|
||||
) <= coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and (
|
||||
asset.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and asset.person_id = spouse.spouse_id_card)
|
||||
)
|
||||
), 0) * 1.5 then 1
|
||||
when (
|
||||
coalesce(base_staff.annual_income, 0)
|
||||
+ coalesce(spouse.spouse_income, 0)
|
||||
+ coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0)
|
||||
) <= coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and (
|
||||
asset.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and asset.person_id = spouse.spouse_id_card)
|
||||
)
|
||||
), 0) * 3 then 2
|
||||
when (
|
||||
coalesce(base_staff.annual_income, 0)
|
||||
+ coalesce(spouse.spouse_income, 0)
|
||||
+ coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0)
|
||||
) > coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and (
|
||||
asset.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and asset.person_id = spouse.spouse_id_card)
|
||||
)
|
||||
), 0) * 3 then 3
|
||||
else 3
|
||||
end as risk_level_sort
|
||||
from (
|
||||
<include refid="projectEmployeeScopeSql"/>
|
||||
) scope
|
||||
left join ccdi_base_staff base_staff
|
||||
on base_staff.id_card = scope.staff_id_card
|
||||
left join (
|
||||
<include refid="spouseRelationSql"/>
|
||||
) spouse
|
||||
on spouse.person_id = scope.staff_id_card
|
||||
), 0) as comparison_amount
|
||||
from (
|
||||
<include refid="projectEmployeeScopeSql"/>
|
||||
) scope
|
||||
left join ccdi_base_staff base_staff
|
||||
on base_staff.id_card = scope.staff_id_card
|
||||
left join (
|
||||
<include refid="spouseRelationSql"/>
|
||||
) spouse
|
||||
on spouse.person_id = scope.staff_id_card
|
||||
) source
|
||||
) aggregated
|
||||
order by risk_level_sort desc, comparison_amount desc, staff_name asc
|
||||
</select>
|
||||
@@ -306,6 +300,7 @@
|
||||
aggregated.project_id,
|
||||
aggregated.staff_id_card,
|
||||
aggregated.spouse_id_card,
|
||||
aggregated.spouse_is_staff,
|
||||
aggregated.staff_code,
|
||||
aggregated.staff_name,
|
||||
aggregated.dept_name,
|
||||
@@ -344,89 +339,136 @@
|
||||
end as summary_risk_level_name
|
||||
from (
|
||||
select
|
||||
#{projectId} as project_id,
|
||||
scope.staff_id_card,
|
||||
scope.staff_code,
|
||||
scope.staff_name,
|
||||
scope.dept_name,
|
||||
spouse.spouse_id_card,
|
||||
coalesce(base_staff.annual_income, 0) as self_income,
|
||||
coalesce(spouse.spouse_income, 0) as spouse_income,
|
||||
coalesce(base_staff.annual_income, 0) + coalesce(spouse.spouse_income, 0) as total_income,
|
||||
source.*,
|
||||
case
|
||||
when coalesce((
|
||||
when source.self_asset_record_count = 0
|
||||
or source.spouse_staff_asset_record_count = 0 then 1
|
||||
else 0
|
||||
end as missing_self_asset_info,
|
||||
case
|
||||
when source.self_debt_record_count = 0
|
||||
or source.spouse_staff_debt_record_count = 0 then 1
|
||||
else 0
|
||||
end as missing_self_debt_info
|
||||
from (
|
||||
select
|
||||
#{projectId} as project_id,
|
||||
scope.staff_id_card,
|
||||
scope.staff_code,
|
||||
scope.staff_name,
|
||||
scope.dept_name,
|
||||
spouse.spouse_id_card,
|
||||
spouse.spouse_is_staff,
|
||||
coalesce(base_staff.annual_income, 0) as self_income,
|
||||
coalesce(spouse.spouse_income, 0) as spouse_income,
|
||||
coalesce(base_staff.annual_income, 0) + coalesce(spouse.spouse_income, 0) as total_income,
|
||||
coalesce((
|
||||
select count(1)
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and asset.person_id = scope.staff_id_card
|
||||
), 0) = 0 then 1
|
||||
else 0
|
||||
end as missing_self_asset_info,
|
||||
coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and asset.person_id = scope.staff_id_card
|
||||
), 0) as self_total_asset,
|
||||
coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and spouse.spouse_id_card is not null
|
||||
and asset.person_id = spouse.spouse_id_card
|
||||
), 0) as spouse_total_asset,
|
||||
coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and (
|
||||
asset.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and asset.person_id = spouse.spouse_id_card)
|
||||
)
|
||||
), 0) as total_asset,
|
||||
case
|
||||
when coalesce((
|
||||
), 0) as self_asset_record_count,
|
||||
case
|
||||
when spouse.spouse_is_staff = 1 then coalesce((
|
||||
select count(1)
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = spouse.spouse_id_card
|
||||
and asset.person_id = spouse.spouse_id_card
|
||||
), 0)
|
||||
else 1
|
||||
end as spouse_staff_asset_record_count,
|
||||
coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where asset.family_id = scope.staff_id_card
|
||||
and asset.person_id = scope.staff_id_card
|
||||
), 0) as self_total_asset,
|
||||
coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where spouse.spouse_id_card is not null
|
||||
and (
|
||||
(
|
||||
spouse.spouse_is_staff = 1
|
||||
and asset.family_id = spouse.spouse_id_card
|
||||
and asset.person_id = spouse.spouse_id_card
|
||||
)
|
||||
or (
|
||||
(spouse.spouse_is_staff is null or spouse.spouse_is_staff != 1)
|
||||
and asset.family_id = scope.staff_id_card
|
||||
and asset.person_id = spouse.spouse_id_card
|
||||
)
|
||||
)
|
||||
), 0) as spouse_total_asset,
|
||||
coalesce((
|
||||
select sum(coalesce(asset.current_value, 0))
|
||||
from ccdi_asset_info asset
|
||||
where (asset.family_id = scope.staff_id_card and asset.person_id = scope.staff_id_card)
|
||||
or (
|
||||
spouse.spouse_id_card is not null
|
||||
and (
|
||||
(
|
||||
spouse.spouse_is_staff = 1
|
||||
and asset.family_id = spouse.spouse_id_card
|
||||
and asset.person_id = spouse.spouse_id_card
|
||||
)
|
||||
or (
|
||||
(spouse.spouse_is_staff is null or spouse.spouse_is_staff != 1)
|
||||
and asset.family_id = scope.staff_id_card
|
||||
and asset.person_id = spouse.spouse_id_card
|
||||
)
|
||||
)
|
||||
)
|
||||
), 0) as total_asset,
|
||||
coalesce((
|
||||
select count(1)
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
), 0) = 0 then 1
|
||||
else 0
|
||||
end as missing_self_debt_info,
|
||||
coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
), 0) as self_total_debt,
|
||||
coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where spouse.spouse_id_card is not null
|
||||
and debt.person_id = spouse.spouse_id_card
|
||||
), 0) as spouse_total_debt,
|
||||
coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as total_debt,
|
||||
coalesce(base_staff.annual_income, 0)
|
||||
+ coalesce(spouse.spouse_income, 0)
|
||||
+ coalesce((
|
||||
), 0) as self_debt_record_count,
|
||||
case
|
||||
when spouse.spouse_is_staff = 1 then coalesce((
|
||||
select count(1)
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = spouse.spouse_id_card
|
||||
), 0)
|
||||
else 1
|
||||
end as spouse_staff_debt_record_count,
|
||||
coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
), 0) as self_total_debt,
|
||||
coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where spouse.spouse_id_card is not null
|
||||
and debt.person_id = spouse.spouse_id_card
|
||||
), 0) as spouse_total_debt,
|
||||
coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as comparison_amount
|
||||
from (
|
||||
<include refid="projectEmployeeScopeSql"/>
|
||||
) scope
|
||||
left join ccdi_base_staff base_staff
|
||||
on base_staff.id_card = scope.staff_id_card
|
||||
left join (
|
||||
<include refid="spouseRelationSql"/>
|
||||
) spouse
|
||||
on spouse.person_id = scope.staff_id_card
|
||||
where scope.staff_id_card = #{staffIdCard}
|
||||
), 0) as total_debt,
|
||||
coalesce(base_staff.annual_income, 0)
|
||||
+ coalesce(spouse.spouse_income, 0)
|
||||
+ coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as comparison_amount
|
||||
from (
|
||||
<include refid="projectEmployeeScopeSql"/>
|
||||
) scope
|
||||
left join ccdi_base_staff base_staff
|
||||
on base_staff.id_card = scope.staff_id_card
|
||||
left join (
|
||||
<include refid="spouseRelationSql"/>
|
||||
) spouse
|
||||
on spouse.person_id = scope.staff_id_card
|
||||
where scope.staff_id_card = #{staffIdCard}
|
||||
) source
|
||||
) aggregated
|
||||
</select>
|
||||
|
||||
@@ -437,7 +479,15 @@
|
||||
asset.asset_sub_type,
|
||||
case
|
||||
when asset.person_id = #{staffIdCard} then base_staff.name
|
||||
else spouse.relation_name
|
||||
else coalesce(holder_staff.name, (
|
||||
select max(relation.relation_name)
|
||||
from ccdi_staff_fmy_relation relation
|
||||
where relation.person_id = #{staffIdCard}
|
||||
and relation.relation_cert_no = asset.person_id
|
||||
and relation.status = 1
|
||||
and relation.is_emp_family = 1
|
||||
and relation.relation_type = '配偶'
|
||||
))
|
||||
end as holder_name,
|
||||
asset.person_id as holder_id_card,
|
||||
asset.current_value,
|
||||
@@ -445,16 +495,24 @@
|
||||
from ccdi_asset_info asset
|
||||
left join ccdi_base_staff base_staff
|
||||
on base_staff.id_card = #{staffIdCard}
|
||||
left join ccdi_staff_fmy_relation spouse
|
||||
on spouse.person_id = #{staffIdCard}
|
||||
and spouse.status = 1
|
||||
and spouse.relation_type = '配偶'
|
||||
and spouse.relation_cert_no = asset.person_id
|
||||
where asset.family_id = #{staffIdCard}
|
||||
and (
|
||||
asset.person_id = #{staffIdCard}
|
||||
or (#{spouseIdCard} is not null and asset.person_id = #{spouseIdCard})
|
||||
)
|
||||
left join ccdi_base_staff holder_staff
|
||||
on holder_staff.id_card = asset.person_id
|
||||
where (asset.family_id = #{staffIdCard} and asset.person_id = #{staffIdCard})
|
||||
or (
|
||||
#{spouseIdCard} is not null
|
||||
and (
|
||||
(
|
||||
#{spouseIsStaff} = 1
|
||||
and asset.family_id = #{spouseIdCard}
|
||||
and asset.person_id = #{spouseIdCard}
|
||||
)
|
||||
or (
|
||||
(#{spouseIsStaff} is null or #{spouseIsStaff} != 1)
|
||||
and asset.family_id = #{staffIdCard}
|
||||
and asset.person_id = #{spouseIdCard}
|
||||
)
|
||||
)
|
||||
)
|
||||
order by
|
||||
case when asset.person_id = #{staffIdCard} then 1 else 2 end,
|
||||
asset.valuation_date desc,
|
||||
@@ -469,7 +527,15 @@
|
||||
debt.creditor_type,
|
||||
case
|
||||
when debt.person_id = #{staffIdCard} then base_staff.name
|
||||
else spouse.relation_name
|
||||
else coalesce(owner_staff.name, (
|
||||
select max(relation.relation_name)
|
||||
from ccdi_staff_fmy_relation relation
|
||||
where relation.person_id = #{staffIdCard}
|
||||
and relation.relation_cert_no = debt.person_id
|
||||
and relation.status = 1
|
||||
and relation.is_emp_family = 1
|
||||
and relation.relation_type = '配偶'
|
||||
))
|
||||
end as owner_name,
|
||||
debt.person_id as owner_id_card,
|
||||
debt.principal_balance,
|
||||
@@ -477,11 +543,8 @@
|
||||
from ccdi_debts_info debt
|
||||
left join ccdi_base_staff base_staff
|
||||
on base_staff.id_card = #{staffIdCard}
|
||||
left join ccdi_staff_fmy_relation spouse
|
||||
on spouse.person_id = #{staffIdCard}
|
||||
and spouse.status = 1
|
||||
and spouse.relation_type = '配偶'
|
||||
and spouse.relation_cert_no = debt.person_id
|
||||
left join ccdi_base_staff owner_staff
|
||||
on owner_staff.id_card = debt.person_id
|
||||
where debt.person_id = #{staffIdCard}
|
||||
or (#{spouseIdCard} is not null and debt.person_id = #{spouseIdCard})
|
||||
order by
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiRelationGraphMapper">
|
||||
|
||||
<resultMap id="RelationGraphNodeResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphNodeVO">
|
||||
<id property="objectKey" column="objectKey"/>
|
||||
<result property="nodeKey" column="nodeKey"/>
|
||||
<result property="nodeName" column="nodeName"/>
|
||||
<result property="idNumber" column="idNumber"/>
|
||||
<result property="subjectType" column="subjectType"/>
|
||||
<result property="sourceType" column="sourceType"/>
|
||||
<result property="createdTime" column="createdTime"/>
|
||||
<result property="updatedTime" column="updatedTime"/>
|
||||
<result property="canExpand" column="canExpand"/>
|
||||
<result property="depth" column="depth"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="RelationGraphEdgeResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphEdgeVO">
|
||||
<id property="objectKey" column="objectKey"/>
|
||||
<result property="fromKey" column="fromKey"/>
|
||||
<result property="toKey" column="toKey"/>
|
||||
<result property="edgeTable" column="edgeTable"/>
|
||||
<result property="relationType" column="relationType"/>
|
||||
<result property="companyName" column="companyName"/>
|
||||
<result property="stockName" column="stockName"/>
|
||||
<result property="stockType" column="stockType"/>
|
||||
<result property="stockPercent" column="stockPercent"/>
|
||||
<result property="shouldCapi" column="shouldCapi"/>
|
||||
<result property="shouldCapiValue" column="shouldCapiValue"/>
|
||||
<result property="shouldCapiUnit" column="shouldCapiUnit"/>
|
||||
<result property="shoudDate" column="shoudDate"/>
|
||||
<result property="pKeyNo" column="pKeyNo"/>
|
||||
<result property="operName" column="operName"/>
|
||||
<result property="operKeyNo" column="operKeyNo"/>
|
||||
<result property="personId" column="personId"/>
|
||||
<result property="relationName" column="relationName"/>
|
||||
<result property="relationCertNo" column="relationCertNo"/>
|
||||
<result property="gender" column="gender"/>
|
||||
<result property="birthDate" column="birthDate"/>
|
||||
<result property="relationCertType" column="relationCertType"/>
|
||||
<result property="mobilePhone1" column="mobilePhone1"/>
|
||||
<result property="mobilePhone2" column="mobilePhone2"/>
|
||||
<result property="wechatNo1" column="wechatNo1"/>
|
||||
<result property="wechatNo2" column="wechatNo2"/>
|
||||
<result property="wechatNo3" column="wechatNo3"/>
|
||||
<result property="contactAddress" column="contactAddress"/>
|
||||
<result property="annualIncome" column="annualIncome"/>
|
||||
<result property="relationDesc" column="relationDesc"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="effectiveDate" column="effectiveDate"/>
|
||||
<result property="invalidDate" column="invalidDate"/>
|
||||
<result property="remark" column="remark"/>
|
||||
<result property="dataSource" column="dataSource"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="SuspectedEnterpriseResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphSuspectedEnterpriseItemVO">
|
||||
<result property="candidateKeyNo" column="candidateKeyNo"/>
|
||||
<result property="personName" column="personName"/>
|
||||
<result property="companyId" column="companyId"/>
|
||||
<result property="companyName" column="companyName"/>
|
||||
<result property="creditCode" column="creditCode"/>
|
||||
<result property="enterpriseStatus" column="enterpriseStatus"/>
|
||||
<result property="industryName" column="industryName"/>
|
||||
<result property="relationType" column="relationType"/>
|
||||
<result property="stockPercent" column="stockPercent"/>
|
||||
<result property="establishDate" column="establishDate"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="nodeColumns">
|
||||
n.object_key AS objectKey,
|
||||
CONCAT('rel_node/', n.object_key) AS nodeKey,
|
||||
n.node_name AS nodeName,
|
||||
n.id_number AS idNumber,
|
||||
n.subject_type AS subjectType,
|
||||
n.source_type AS sourceType,
|
||||
DATE_FORMAT(n.created_time, '%Y-%m-%d %H:%i:%s') AS createdTime,
|
||||
DATE_FORMAT(n.updated_time, '%Y-%m-%d %H:%i:%s') AS updatedTime,
|
||||
1 AS canExpand,
|
||||
0 AS depth
|
||||
</sql>
|
||||
|
||||
<select id="selectRelationGraphSubjects" resultMap="RelationGraphNodeResultMap">
|
||||
SELECT
|
||||
<include refid="nodeColumns"/>
|
||||
FROM lx_rel_node n
|
||||
WHERE 1 = 1
|
||||
<if test="query.objectKey != null and query.objectKey != ''">
|
||||
AND n.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
|
||||
</if>
|
||||
<if test="query.objectKey == null or query.objectKey == ''">
|
||||
<if test="query.keyword != null and query.keyword != ''">
|
||||
AND (
|
||||
n.object_key = (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
|
||||
OR n.id_number = (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
|
||||
OR n.node_name LIKE (CONCAT('%', TRIM(#{query.keyword}), '%') COLLATE utf8mb4_general_ci)
|
||||
)
|
||||
</if>
|
||||
</if>
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN n.object_key = (TRIM(IFNULL(#{query.keyword}, '')) COLLATE utf8mb4_general_ci) THEN 0
|
||||
WHEN n.id_number = (TRIM(IFNULL(#{query.keyword}, '')) COLLATE utf8mb4_general_ci) THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
n.node_name
|
||||
LIMIT 20
|
||||
</select>
|
||||
|
||||
<select id="selectRelationGraphNodesByKeys" resultMap="RelationGraphNodeResultMap">
|
||||
SELECT
|
||||
<include refid="nodeColumns"/>
|
||||
FROM lx_rel_node n
|
||||
WHERE n.object_key IN
|
||||
<foreach collection="objectKeys" item="objectKey" open="(" separator="," close=")">
|
||||
#{objectKey}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<select id="selectRelationGraphEdges" resultMap="RelationGraphEdgeResultMap">
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
e.object_key AS objectKey,
|
||||
e.from_key AS fromKey,
|
||||
e.to_key AS toKey,
|
||||
'lx_rel_family_edge' AS edgeTable,
|
||||
e.relation_type AS relationType,
|
||||
NULL AS companyName,
|
||||
NULL AS stockName,
|
||||
NULL AS stockType,
|
||||
NULL AS stockPercent,
|
||||
NULL AS shouldCapi,
|
||||
NULL AS shouldCapiValue,
|
||||
NULL AS shouldCapiUnit,
|
||||
NULL AS shoudDate,
|
||||
NULL AS pKeyNo,
|
||||
NULL AS operName,
|
||||
NULL AS operKeyNo,
|
||||
e.person_id AS personId,
|
||||
e.relation_name AS relationName,
|
||||
e.relation_cert_no AS relationCertNo,
|
||||
NULL AS gender,
|
||||
NULL AS birthDate,
|
||||
NULL AS relationCertType,
|
||||
NULL AS mobilePhone1,
|
||||
NULL AS mobilePhone2,
|
||||
NULL AS wechatNo1,
|
||||
NULL AS wechatNo2,
|
||||
NULL AS wechatNo3,
|
||||
NULL AS contactAddress,
|
||||
NULL AS annualIncome,
|
||||
e.relation_desc AS relationDesc,
|
||||
NULL AS status,
|
||||
NULL AS effectiveDate,
|
||||
NULL AS invalidDate,
|
||||
NULL AS remark,
|
||||
NULL AS dataSource
|
||||
FROM lx_rel_family_edge e
|
||||
WHERE e.from_key = CONCAT('rel_node/', #{query.objectKey})
|
||||
OR e.to_key = CONCAT('rel_node/', #{query.objectKey})
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
e.object_key AS objectKey,
|
||||
e.from_key AS fromKey,
|
||||
e.to_key AS toKey,
|
||||
'lx_rel_stock_edge' AS edgeTable,
|
||||
e.stock_type AS relationType,
|
||||
e.company_name AS companyName,
|
||||
e.stock_name AS stockName,
|
||||
e.stock_type AS stockType,
|
||||
e.stock_percent AS stockPercent,
|
||||
e.should_capi AS shouldCapi,
|
||||
e.should_capi_value AS shouldCapiValue,
|
||||
e.should_capi_unit AS shouldCapiUnit,
|
||||
e.shoud_date AS shoudDate,
|
||||
e.p_key_no AS pKeyNo,
|
||||
NULL AS operName,
|
||||
NULL AS operKeyNo,
|
||||
NULL AS personId,
|
||||
NULL AS relationName,
|
||||
NULL AS relationCertNo,
|
||||
NULL AS gender,
|
||||
NULL AS birthDate,
|
||||
NULL AS relationCertType,
|
||||
NULL AS mobilePhone1,
|
||||
NULL AS mobilePhone2,
|
||||
NULL AS wechatNo1,
|
||||
NULL AS wechatNo2,
|
||||
NULL AS wechatNo3,
|
||||
NULL AS contactAddress,
|
||||
NULL AS annualIncome,
|
||||
NULL AS relationDesc,
|
||||
NULL AS status,
|
||||
NULL AS effectiveDate,
|
||||
NULL AS invalidDate,
|
||||
NULL AS remark,
|
||||
NULL AS dataSource
|
||||
FROM lx_rel_stock_edge e
|
||||
WHERE e.from_key = CONCAT('rel_node/', #{query.objectKey})
|
||||
OR e.to_key = CONCAT('rel_node/', #{query.objectKey})
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
e.object_key AS objectKey,
|
||||
e.from_key AS fromKey,
|
||||
e.to_key AS toKey,
|
||||
'lx_rel_represent_edge' AS edgeTable,
|
||||
'法定代表人' AS relationType,
|
||||
NULL AS companyName,
|
||||
NULL AS stockName,
|
||||
NULL AS stockType,
|
||||
NULL AS stockPercent,
|
||||
NULL AS shouldCapi,
|
||||
NULL AS shouldCapiValue,
|
||||
NULL AS shouldCapiUnit,
|
||||
NULL AS shoudDate,
|
||||
NULL AS pKeyNo,
|
||||
e.oper_name AS operName,
|
||||
e.oper_key_no AS operKeyNo,
|
||||
NULL AS personId,
|
||||
NULL AS relationName,
|
||||
NULL AS relationCertNo,
|
||||
NULL AS gender,
|
||||
NULL AS birthDate,
|
||||
NULL AS relationCertType,
|
||||
NULL AS mobilePhone1,
|
||||
NULL AS mobilePhone2,
|
||||
NULL AS wechatNo1,
|
||||
NULL AS wechatNo2,
|
||||
NULL AS wechatNo3,
|
||||
NULL AS contactAddress,
|
||||
NULL AS annualIncome,
|
||||
NULL AS relationDesc,
|
||||
NULL AS status,
|
||||
NULL AS effectiveDate,
|
||||
NULL AS invalidDate,
|
||||
NULL AS remark,
|
||||
NULL AS dataSource
|
||||
FROM lx_rel_represent_edge e
|
||||
WHERE e.from_key = CONCAT('rel_node/', #{query.objectKey})
|
||||
OR e.to_key = CONCAT('rel_node/', #{query.objectKey})
|
||||
) graph_edges
|
||||
ORDER BY
|
||||
CASE edgeTable
|
||||
WHEN 'lx_rel_family_edge' THEN 0
|
||||
WHEN 'lx_rel_stock_edge' THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
relationType,
|
||||
objectKey
|
||||
LIMIT #{query.limit}
|
||||
</select>
|
||||
|
||||
<select id="countSuspectedEnterpriseKeyNos" resultType="int">
|
||||
SELECT COUNT(DISTINCT candidate_key_no)
|
||||
FROM (
|
||||
SELECT e.oper_key_no AS candidate_key_no
|
||||
FROM lx_rel_represent_edge e
|
||||
WHERE e.oper_name = (#{personName} COLLATE utf8mb4_general_ci)
|
||||
AND e.oper_key_no IS NOT NULL
|
||||
AND e.oper_key_no != ''
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT e.p_key_no AS candidate_key_no
|
||||
FROM lx_rel_stock_edge e
|
||||
WHERE e.stock_name = (#{personName} COLLATE utf8mb4_general_ci)
|
||||
AND e.stock_type = '自然人股东'
|
||||
AND e.p_key_no IS NOT NULL
|
||||
AND e.p_key_no != ''
|
||||
) same_name_candidates
|
||||
</select>
|
||||
|
||||
<select id="selectSuspectedEnterprises" resultMap="SuspectedEnterpriseResultMap">
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
e.oper_key_no AS candidateKeyNo,
|
||||
e.oper_name AS personName,
|
||||
REPLACE(e.to_key, 'rel_node/', '') AS companyId,
|
||||
COALESCE(ent.enterprise_name, company_node.node_name) AS companyName,
|
||||
COALESCE(ent.social_credit_code, company_node.id_number) AS creditCode,
|
||||
ent.status AS enterpriseStatus,
|
||||
ent.industry_name AS industryName,
|
||||
'法定代表人' AS relationType,
|
||||
NULL AS stockPercent,
|
||||
DATE_FORMAT(ent.establish_date, '%Y-%m-%d') AS establishDate,
|
||||
0 AS relationSort
|
||||
FROM lx_rel_represent_edge e
|
||||
LEFT JOIN lx_rel_node company_node
|
||||
ON company_node.object_key = REPLACE(e.to_key, 'rel_node/', '')
|
||||
LEFT JOIN ccdi_enterprise_base_info ent
|
||||
ON ent.social_credit_code = company_node.id_number
|
||||
WHERE e.oper_name = (#{personName} COLLATE utf8mb4_general_ci)
|
||||
AND e.oper_key_no IS NOT NULL
|
||||
AND e.oper_key_no != ''
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
e.p_key_no AS candidateKeyNo,
|
||||
e.stock_name AS personName,
|
||||
REPLACE(e.to_key, 'rel_node/', '') AS companyId,
|
||||
COALESCE(ent.enterprise_name, e.company_name, company_node.node_name) AS companyName,
|
||||
COALESCE(ent.social_credit_code, company_node.id_number) AS creditCode,
|
||||
ent.status AS enterpriseStatus,
|
||||
ent.industry_name AS industryName,
|
||||
e.stock_type AS relationType,
|
||||
e.stock_percent AS stockPercent,
|
||||
COALESCE(DATE_FORMAT(ent.establish_date, '%Y-%m-%d'), e.shoud_date) AS establishDate,
|
||||
1 AS relationSort
|
||||
FROM lx_rel_stock_edge e
|
||||
LEFT JOIN lx_rel_node company_node
|
||||
ON company_node.object_key = REPLACE(e.to_key, 'rel_node/', '')
|
||||
LEFT JOIN ccdi_enterprise_base_info ent
|
||||
ON ent.social_credit_code = company_node.id_number
|
||||
WHERE e.stock_name = (#{personName} COLLATE utf8mb4_general_ci)
|
||||
AND e.stock_type = '自然人股东'
|
||||
AND e.p_key_no IS NOT NULL
|
||||
AND e.p_key_no != ''
|
||||
) suspected_enterprises
|
||||
ORDER BY relationSort, companyName, companyId
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -281,7 +281,8 @@ class CcdiProjectOverviewControllerTest {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
|
||||
CcdiProjectSuspiciousTransactionExcel row = new CcdiProjectSuspiciousTransactionExcel();
|
||||
row.setSuspiciousPersonName("张三");
|
||||
row.setLeAccountName("张三");
|
||||
row.setCustomerAccountName("测试对手方");
|
||||
row.setDisplayAmount(new java.math.BigDecimal("10.00"));
|
||||
when(overviewService.exportSuspiciousTransactions(same(queryDTO))).thenReturn(List.of(row));
|
||||
|
||||
|
||||
@@ -104,4 +104,26 @@ class CcdiBankStatementTest {
|
||||
assertEquals("330101199001011234", entity.getCustomerCertNo());
|
||||
assertEquals("91330100123456789X", entity.getCustomerSocialCreditCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFromResponse_ShouldNormalizeCretNoBeforeDash() {
|
||||
BankStatementItem item = new BankStatementItem();
|
||||
item.setCretNo(" 330100198801010033 - 测试人员 ");
|
||||
|
||||
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
|
||||
|
||||
assertNotNull(entity);
|
||||
assertEquals("330100198801010033", entity.getCretNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFromResponse_ShouldNormalizeCretNoWithChineseDash() {
|
||||
BankStatementItem item = new BankStatementItem();
|
||||
item.setCretNo("330100198801010033-测试人员");
|
||||
|
||||
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
|
||||
|
||||
assertNotNull(entity);
|
||||
assertEquals("330100198801010033", entity.getCretNo());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,20 @@ class CcdiBankStatementMapperXmlTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetCount_shouldOnlyUseStatementCretNoMatchedStaff() throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
String selectSql = extractSelect(xml, "countMatchedStaffCountByProjectId");
|
||||
|
||||
assertTrue(selectSql.contains("select count(distinct trim(bs.cret_no))"), selectSql);
|
||||
assertTrue(selectSql.contains("inner join ccdi_base_staff staff on staff.id_card = trim(bs.cret_no)"), selectSql);
|
||||
assertFalse(selectSql.contains("ccdi_staff_fmy_relation"), selectSql);
|
||||
assertFalse(selectSql.contains("ccdi_account_info"), selectSql);
|
||||
assertFalse(selectSql.contains("LE_ACCOUNT_NO"), selectSql);
|
||||
}
|
||||
}
|
||||
|
||||
private MappedStatement loadMappedStatement(String statementId) throws Exception {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));
|
||||
@@ -242,6 +256,15 @@ class CcdiBankStatementMapperXmlTest {
|
||||
return boundSql.getSql().replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
|
||||
private String extractSelect(String xml, String selectId) {
|
||||
String start = "<select id=\"" + selectId + "\"";
|
||||
int startIndex = xml.indexOf(start);
|
||||
assertTrue(startIndex >= 0, "missing select: " + selectId);
|
||||
int endIndex = xml.indexOf("</select>", startIndex);
|
||||
assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
|
||||
return xml.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
private void registerTypeAliases(TypeAliasRegistry typeAliasRegistry) {
|
||||
typeAliasRegistry.registerAlias("map", Map.class);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiBankTagAnalysisMapperXmlTest {
|
||||
@@ -26,11 +27,18 @@ class CcdiBankTagAnalysisMapperXmlTest {
|
||||
"selectForexSellAmtStatements",
|
||||
"selectLargePurchaseTransactionStatements",
|
||||
"selectStockTfrLargeStatements",
|
||||
"selectLargeStockTradingStatements"
|
||||
"selectLargeStockTradingStatements",
|
||||
"selectExternalSingleLargeAmountStatements",
|
||||
"selectExternalNightTransactionStatements",
|
||||
"selectExternalGamblingMemoStatements",
|
||||
"selectExternalToStaffOrFamilyTransactionStatements"
|
||||
);
|
||||
private static final List<String> PHASE_TWO_OBJECT_SELECT_IDS = List.of(
|
||||
"selectLowIncomeRelativeLargeTransactionObjects",
|
||||
"selectMultiPartyGamblingTransferObjects",
|
||||
"selectExternalCumulativeTransactionAmountObjects",
|
||||
"selectExternalAnnualTurnoverObjects",
|
||||
"selectExternalMultiPartyGamblingTransferObjects",
|
||||
"selectMonthlyFixedIncomeObjects",
|
||||
"selectFixedCounterpartyTransferObjects",
|
||||
"selectSupplierConcentrationObjects",
|
||||
@@ -100,7 +108,10 @@ class CcdiBankTagAnalysisMapperXmlTest {
|
||||
void placeholderRules_shouldUseEmptyResultSqlTemplate() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
assertTrue(xml.contains("占位SQL,待补充真实规则"));
|
||||
assertEquals(6, countMatches(xml, "where 1 = 0"));
|
||||
assertEquals(
|
||||
countMatches(xml, "占位SQL,待补充真实规则"),
|
||||
countMatches(xml, "where 1 = 0")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -116,6 +127,31 @@ class CcdiBankTagAnalysisMapperXmlTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowIncomeRelativeRule_shouldIgnoreNullAnnualIncome() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String selectSql = extractSelectSql(xml, "selectLowIncomeRelativeLargeTransactionObjects");
|
||||
assertTrue(selectSql.contains("relation.annual_income is not null"));
|
||||
assertTrue(!selectSql.contains("relation.annual_income is null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void abnormalCustomerTransactionRule_shouldUseCreditCustomerAndIntermediaryAccountRules() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String selectSql = extractSelectSql(xml, "selectAbnormalCustomerTransactionStatements");
|
||||
|
||||
assertTrue(selectSql.contains("account.owner_type = 'CREDIT_CUSTOMER'"));
|
||||
assertTrue(selectSql.contains("account.owner_type = 'INTERMEDIARY'"));
|
||||
assertTrue(selectSql.contains("account.account_no = bs.customer_account_no"));
|
||||
assertTrue(selectSql.contains("enterprise.ent_source = 'INTERMEDIARY'"));
|
||||
assertTrue(selectSql.contains("intermediary.name = bs.CUSTOMER_ACCOUNT_NAME"));
|
||||
assertTrue(selectSql.contains("bs.CUSTOMER_ACCOUNT_NAME like concat('%', intermediary.name, '%')"));
|
||||
assertTrue(selectSql.contains("bs.bank in ('ALIPAY', 'WECHAT')"));
|
||||
assertEquals(5, countMatches(selectSql, "GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000"));
|
||||
assertTrue(!selectSql.contains("customer_cert_no"));
|
||||
assertTrue(!selectSql.contains("social_credit_code = bs"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withdrawCntObjectRule_shouldUseRealSqlAndKeepObjectHitFields() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
@@ -131,7 +167,11 @@ class CcdiBankTagAnalysisMapperXmlTest {
|
||||
String xml = readXml(RESOURCE);
|
||||
for (String selectId : PHASE_TWO_OBJECT_SELECT_IDS) {
|
||||
String selectSql = extractSelectSql(xml, selectId);
|
||||
assertTrue(selectSql.contains("'STAFF_ID_CARD' AS objectType"), () -> selectId + " 缺少 objectType");
|
||||
assertTrue(
|
||||
selectSql.contains("'STAFF_ID_CARD' AS objectType")
|
||||
|| selectSql.contains("'EXTERNAL_CERT_NO' AS objectType"),
|
||||
() -> selectId + " 缺少 objectType"
|
||||
);
|
||||
assertTrue(selectSql.contains("AS objectKey"), () -> selectId + " 缺少 objectKey");
|
||||
assertTrue(selectSql.contains("reasonDetail"), () -> selectId + " 缺少 reasonDetail");
|
||||
assertTrue(!selectSql.contains("where 1 = 0"), () -> selectId + " 仍是占位 SQL");
|
||||
@@ -196,6 +236,31 @@ class CcdiBankTagAnalysisMapperXmlTest {
|
||||
factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void externalPersonRules_shouldUseExternalSubjectScopeAndCounterpartyEmployeeMatching() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String scopeSql = extractSqlFragment(xml, "externalPersonPredicateSql");
|
||||
String relationSql = extractSelectSql(xml, "selectExternalToStaffOrFamilyTransactionStatements");
|
||||
String annualSql = extractSelectSql(xml, "selectExternalAnnualTurnoverObjects");
|
||||
String gamblingSql = extractSelectSql(xml, "selectExternalMultiPartyGamblingTransferObjects");
|
||||
|
||||
assertTrue(scopeSql.contains("bs.cret_no is not null"));
|
||||
assertTrue(scopeSql.contains("staff.id_card = bs.cret_no"));
|
||||
assertTrue(scopeSql.contains("relation.relation_cert_no = bs.cret_no"));
|
||||
assertTrue(scopeSql.contains("trim(IFNULL(bs.LE_ACCOUNT_NAME, '')) <> trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''))"));
|
||||
assertFalse(scopeSql.contains("LE_ACCOUNT_NO"));
|
||||
assertTrue(annualSql.contains("'EXTERNAL_CERT_NO' AS objectType"));
|
||||
assertTrue(annualSql.contains("bs.cret_no AS certNo"));
|
||||
assertTrue(gamblingSql.contains("'EXTERNAL_CERT_NO' AS objectType"));
|
||||
assertTrue(gamblingSql.contains("having COUNT(1) > 2"));
|
||||
assertTrue(relationSql.contains("counter_account.owner_type in ('EMPLOYEE', 'RELATION', 'INTERMEDIARY', 'CREDIT_CUSTOMER')"));
|
||||
assertTrue(relationSql.contains("on counter_account.account_no is null"));
|
||||
assertTrue(relationSql.contains("counter_staff.name = trim(bs.CUSTOMER_ACCOUNT_NAME)"));
|
||||
assertTrue(relationSql.contains("counter_relation.relation_name = trim(bs.CUSTOMER_ACCOUNT_NAME)"));
|
||||
assertTrue(relationSql.contains("counter_account.owner_type in ('EMPLOYEE', 'RELATION')"));
|
||||
assertFalse(relationSql.contains("customer_cert_no"));
|
||||
}
|
||||
|
||||
private String readXml(String resource) throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resource)) {
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
@@ -90,13 +90,77 @@ class CcdiProjectOverviewMapperSqlTest {
|
||||
void shouldExposeSuspiciousTransactionAggregationQuery() throws Exception {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
|
||||
String suspiciousSql = extractSelect(xml, "selectSuspiciousTransactionPage");
|
||||
String modelHitSql = extractSqlFragment(xml, "suspiciousTransactionModelHitSql");
|
||||
String aggregatedSql = extractSqlFragment(xml, "suspiciousTransactionAggregatedSql");
|
||||
|
||||
assertTrue(suspiciousSql.contains("rule_name like '%可疑%'"), suspiciousSql);
|
||||
assertTrue(modelHitSql.contains("from ccdi_bank_statement_tag_result tr"), modelHitSql);
|
||||
assertTrue(modelHitSql.contains("tr.bank_statement_id is not null"), modelHitSql);
|
||||
assertFalse(modelHitSql.contains("rule_name like '%可疑%'"), modelHitSql);
|
||||
assertFalse(modelHitSql.contains("ABNORMAL_CUSTOMER_TRANSACTION"), modelHitSql);
|
||||
assertTrue(suspiciousSql.contains("ccdi_biz_intermediary"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("ccdi_enterprise_base_info"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("group by merged.bankStatementId"), suspiciousSql);
|
||||
assertTrue(aggregatedSql.contains("lpad(merged.matchPriority, 2, '0')"), aggregatedSql);
|
||||
assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("hasNameListHit"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("final_result.nameListHitType"), suspiciousSql);
|
||||
|
||||
String reportSuspiciousSql = extractSelect(xml, "selectReportSuspiciousTransactionList");
|
||||
assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '中介'"), reportSuspiciousSql);
|
||||
assertTrue(reportSuspiciousSql.contains("疑似与中介往来"), reportSuspiciousSql);
|
||||
assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '信贷客户'"), reportSuspiciousSql);
|
||||
assertTrue(reportSuspiciousSql.contains("与信贷客户之间非正常资金往来"), reportSuspiciousSql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void externalPersonSourceSql_shouldIncludeStatementAndObjectHits() throws Exception {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
|
||||
String externalSourceSql = extractSqlFragment(xml, "externalPersonSourceSql");
|
||||
String normalizedExternalSourceSql = externalSourceSql.replace("\r\n", "\n");
|
||||
|
||||
assertTrue(externalSourceSql.contains("tr.bank_statement_id = bs.bank_statement_id"), externalSourceSql);
|
||||
assertTrue(externalSourceSql.contains("union all"), externalSourceSql);
|
||||
assertTrue(externalSourceSql.contains("tr.object_type = 'EXTERNAL_CERT_NO'"), externalSourceSql);
|
||||
assertTrue(externalSourceSql.contains("tr.object_key = subject.cert_no"), externalSourceSql);
|
||||
assertTrue(externalSourceSql.contains("'资金' as related_object"), externalSourceSql);
|
||||
assertFalse(externalSourceSql.contains("counter_staff.id_card = bs.customer_cert_no"), externalSourceSql);
|
||||
assertFalse(externalSourceSql.contains("counter_relation.relation_cert_no = bs.customer_cert_no"), externalSourceSql);
|
||||
assertFalse(externalSourceSql.contains("counter_intermediary.person_id = bs.customer_cert_no"), externalSourceSql);
|
||||
assertInOrder(
|
||||
externalSourceSql,
|
||||
"when counter_account.owner_type = 'EMPLOYEE' then '员工'",
|
||||
"when counter_account.owner_type = 'RELATION' then '员工亲属'",
|
||||
"when counter_account.owner_type = 'CREDIT_CUSTOMER' then '信贷客户'",
|
||||
"when counter_account.owner_type = 'INTERMEDIARY' then '中介库人员'",
|
||||
"when counter_staff.id_card is not null then '员工'",
|
||||
"when counter_relation.relation_cert_no is not null then '员工亲属'"
|
||||
);
|
||||
assertTrue(normalizedExternalSourceSql.contains("left join ccdi_base_staff counter_staff\n on counter_account.account_no is null"), externalSourceSql);
|
||||
assertTrue(normalizedExternalSourceSql.contains("left join ccdi_staff_fmy_relation counter_relation\n on counter_account.account_no is null"), externalSourceSql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void suspiciousTransactionNameListSql_shouldKeepCreditCustomerAndIntermediaryRulesScopedByAmount() throws Exception {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
|
||||
String nameHitSql = extractSqlFragment(xml, "suspiciousTransactionNameHitSql");
|
||||
String aggregatedSql = extractSqlFragment(xml, "suspiciousTransactionAggregatedSql");
|
||||
|
||||
assertTrue(nameHitSql.contains("account.owner_type = 'CREDIT_CUSTOMER'"), nameHitSql);
|
||||
assertTrue(nameHitSql.contains("account.owner_type = 'INTERMEDIARY'"), nameHitSql);
|
||||
assertTrue(nameHitSql.contains("account.account_no = bs.customer_account_no"), nameHitSql);
|
||||
assertTrue(nameHitSql.contains("'信贷客户' as nameListHitType"), nameHitSql);
|
||||
assertTrue(nameHitSql.contains("'中介' as nameListHitType"), nameHitSql);
|
||||
assertTrue(nameHitSql.contains("intermediary.name = bs.CUSTOMER_ACCOUNT_NAME"), nameHitSql);
|
||||
assertTrue(nameHitSql.contains("enterprise.ent_source = 'INTERMEDIARY'"), nameHitSql);
|
||||
assertTrue(
|
||||
nameHitSql.contains("GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000"),
|
||||
nameHitSql
|
||||
);
|
||||
assertFalse(nameHitSql.contains("customer_cert_no"), nameHitSql);
|
||||
assertFalse(nameHitSql.contains("social_credit_code = bs"), nameHitSql);
|
||||
assertTrue(aggregatedSql.contains("group by merged.bankStatementId"), aggregatedSql);
|
||||
assertTrue(aggregatedSql.contains("max(merged.hasModelRuleHit) as hasModelRuleHit"), aggregatedSql);
|
||||
assertTrue(aggregatedSql.contains("max(merged.hasNameListHit) as hasNameListHit"), aggregatedSql);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,4 +223,22 @@ class CcdiProjectOverviewMapperSqlTest {
|
||||
assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
|
||||
return xml.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
private String extractSqlFragment(String xml, String sqlId) {
|
||||
String start = "<sql id=\"" + sqlId + "\"";
|
||||
int startIndex = xml.indexOf(start);
|
||||
assertTrue(startIndex >= 0, "missing sql fragment: " + sqlId);
|
||||
int endIndex = xml.indexOf("</sql>", startIndex);
|
||||
assertTrue(endIndex >= 0, "missing closing sql tag: " + sqlId);
|
||||
return xml.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
private void assertInOrder(String sql, String... fragments) {
|
||||
int previousIndex = -1;
|
||||
for (String fragment : fragments) {
|
||||
int currentIndex = sql.indexOf(fragment);
|
||||
assertTrue(currentIndex > previousIndex, () -> "fragment order mismatch: " + fragment + "\n" + sql);
|
||||
previousIndex = currentIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,13 @@ class CcdiProjectSpecialCheckMapperDetailSqlTest {
|
||||
assertTrue(xml.contains("select id=\"selectFamilyAssetItemsByScope\""));
|
||||
assertTrue(xml.contains("select id=\"selectFamilyDebtItemsByScope\""));
|
||||
assertTrue(xml.contains("scope.staff_id_card = #{staffIdCard}"));
|
||||
assertTrue(xml.contains("spouseIsStaff=spouse_is_staff"));
|
||||
assertTrue(xml.contains("relation.relation_cert_no as person_id"));
|
||||
assertTrue(xml.contains("spouse_staff.annual_income"));
|
||||
assertTrue(xml.contains("spouse.spouse_is_staff = 1"));
|
||||
assertTrue(xml.contains("asset.family_id = spouse.spouse_id_card"));
|
||||
assertTrue(xml.contains("source.spouse_staff_asset_record_count = 0"));
|
||||
assertTrue(xml.contains("source.spouse_staff_debt_record_count = 0"));
|
||||
assertTrue(xml.contains("incomeDetail"));
|
||||
assertTrue(xml.contains("assetDetail"));
|
||||
assertTrue(xml.contains("debtDetail"));
|
||||
@@ -31,6 +38,9 @@ class CcdiProjectSpecialCheckMapperDetailSqlTest {
|
||||
assertTrue(xml.contains("asset_main_type"));
|
||||
assertTrue(xml.contains("asset_sub_type"));
|
||||
assertTrue(xml.contains("holder_name"));
|
||||
assertTrue(xml.contains("holder_staff.name"));
|
||||
assertTrue(xml.contains("select max(relation.relation_name)"));
|
||||
assertTrue(xml.contains("relation.relation_cert_no = asset.person_id"));
|
||||
assertTrue(xml.contains("current_value"));
|
||||
assertTrue(xml.contains("valuation_date"));
|
||||
assertTrue(xml.contains("debt_name"));
|
||||
@@ -38,6 +48,8 @@ class CcdiProjectSpecialCheckMapperDetailSqlTest {
|
||||
assertTrue(xml.contains("debt_sub_type"));
|
||||
assertTrue(xml.contains("creditor_type"));
|
||||
assertTrue(xml.contains("owner_name"));
|
||||
assertTrue(xml.contains("owner_staff.name"));
|
||||
assertTrue(xml.contains("relation.relation_cert_no = debt.person_id"));
|
||||
assertTrue(xml.contains("principal_balance"));
|
||||
assertTrue(xml.contains("query_date"));
|
||||
assertFalse(xml.contains("ccdi_project_overview_employee_result"));
|
||||
|
||||
@@ -15,15 +15,31 @@ class CcdiProjectSpecialCheckMapperListSqlTest {
|
||||
String listSql = extractSelect(xml, "selectFamilyAssetLiabilityList");
|
||||
|
||||
assertTrue(listSql.contains("order by risk_level_sort desc, comparison_amount desc, staff_name asc"));
|
||||
assertTrue(xml.contains("from ccdi_bank_statement_tag_result"));
|
||||
assertTrue(xml.contains("from ccdi_bank_statement bs"));
|
||||
assertTrue(xml.contains("statement_staff.id_card = trim(bs.cret_no)"));
|
||||
assertTrue(xml.contains("bs.project_id = #{projectId}"));
|
||||
assertTrue(xml.contains("ccdi_base_staff"));
|
||||
assertTrue(xml.contains("ccdi_staff_fmy_relation"));
|
||||
assertTrue(xml.contains("relation_type = '配偶'"));
|
||||
assertTrue(xml.contains("union all"));
|
||||
assertTrue(xml.contains("relation.relation_cert_no as person_id"));
|
||||
assertTrue(xml.contains("inner join ccdi_base_staff current_staff"));
|
||||
assertTrue(xml.contains("spouse_staff.annual_income"));
|
||||
assertTrue(xml.contains("spouse_is_staff"));
|
||||
assertTrue(xml.contains("annual_income"));
|
||||
assertTrue(xml.contains("current_value"));
|
||||
assertTrue(xml.contains("principal_balance"));
|
||||
assertTrue(listSql.contains("self_asset_record_count"));
|
||||
assertTrue(listSql.contains("spouse_staff_asset_record_count"));
|
||||
assertTrue(listSql.contains("self_debt_record_count"));
|
||||
assertTrue(listSql.contains("spouse_staff_debt_record_count"));
|
||||
assertTrue(listSql.contains("source.self_asset_record_count = 0"));
|
||||
assertTrue(listSql.contains("source.spouse_staff_asset_record_count = 0"));
|
||||
assertTrue(listSql.contains("source.self_debt_record_count = 0"));
|
||||
assertTrue(listSql.contains("source.spouse_staff_debt_record_count = 0"));
|
||||
assertTrue(listSql.contains("asset.family_id = spouse.spouse_id_card"));
|
||||
assertTrue(listSql.contains("asset.person_id = spouse.spouse_id_card"));
|
||||
assertTrue(listSql.contains("debt.person_id = spouse.spouse_id_card"));
|
||||
assertTrue(listSql.contains("then 'MISSING_INFO'"));
|
||||
assertTrue(listSql.contains("then '缺少信息'"));
|
||||
assertTrue(listSql.contains("comparison_amount"));
|
||||
|
||||
@@ -132,6 +132,49 @@ class BankTagRuleConfigResolverTest {
|
||||
assertEquals("8888", config.getThresholdValue("ANNUAL_TURNOVER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_shouldUseLargeTransactionParamsForExternalLargeTransactionRule() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(40L);
|
||||
project.setConfigType("default");
|
||||
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||
when(modelParamMapper.selectByProjectAndModel(0L, "LARGE_TRANSACTION")).thenReturn(List.of(
|
||||
buildParam("LARGE_TRANSACTION", "FREQUENT_TRANSFER", "100000")
|
||||
));
|
||||
|
||||
CcdiBankTagRule ruleMeta = new CcdiBankTagRule();
|
||||
ruleMeta.setModelCode("EXTERNAL_LARGE_TRANSACTION");
|
||||
ruleMeta.setRuleCode("EXTERNAL_SINGLE_LARGE_AMOUNT");
|
||||
ruleMeta.setIndicatorCode("FREQUENT_TRANSFER");
|
||||
|
||||
BankTagRuleExecutionConfig config = resolver.resolve(40L, ruleMeta);
|
||||
|
||||
assertEquals("100000", config.getThresholdValue("FREQUENT_TRANSFER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_shouldUseOriginalModelParamsForExternalObjectRules() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(40L);
|
||||
project.setConfigType("default");
|
||||
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||
when(modelParamMapper.selectByProjectAndModel(0L, "LARGE_TRANSACTION")).thenReturn(List.of(
|
||||
buildParam("LARGE_TRANSACTION", "CUMULATIVE_TRANSACTION_AMOUNT", "500000"),
|
||||
buildParam("LARGE_TRANSACTION", "ANNUAL_TURNOVER", "800000")
|
||||
));
|
||||
when(modelParamMapper.selectByProjectAndModel(0L, "SUSPICIOUS_GAMBLING")).thenReturn(List.of(
|
||||
buildParam("SUSPICIOUS_GAMBLING", "MULTI_PARTY_AMT_MIN", "500"),
|
||||
buildParam("SUSPICIOUS_GAMBLING", "MULTI_PARTY_AMT_MAX", "5000")
|
||||
));
|
||||
|
||||
assertRuleThresholds("EXTERNAL_LARGE_TRANSACTION", "EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT",
|
||||
Map.of("CUMULATIVE_TRANSACTION_AMOUNT", "500000"));
|
||||
assertRuleThresholds("EXTERNAL_LARGE_TRANSACTION", "EXTERNAL_ANNUAL_TURNOVER",
|
||||
Map.of("ANNUAL_TURNOVER", "800000"));
|
||||
assertRuleThresholds("EXTERNAL_SUSPICIOUS_GAMBLING", "EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER",
|
||||
Map.of("MULTI_PARTY_AMT_MIN", "500", "MULTI_PARTY_AMT_MAX", "5000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_shouldMapPhaseOneThresholdRulesToUppercaseParamCodes() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
|
||||
@@ -317,7 +317,7 @@ class CcdiBankTagServiceImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRollbackProjectStatusToProcessingWhenRebuildFails() {
|
||||
void shouldMarkProjectTagFailedWhenRebuildFails() {
|
||||
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
|
||||
|
||||
CcdiBankTagRule rule = buildRule("LARGE_TRANSACTION", "大额交易",
|
||||
@@ -329,7 +329,7 @@ class CcdiBankTagServiceImplTest {
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> service.rebuildProject(40L, null, "tester", TriggerType.MANUAL));
|
||||
|
||||
verify(projectService).updateProjectStatus(40L, "0", "tester");
|
||||
verify(projectService).updateProjectStatus(40L, "4", "tester");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -496,6 +496,95 @@ class CcdiBankTagServiceImplTest {
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rebuildProject_shouldDispatchExternalPersonStatementRules() {
|
||||
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
|
||||
|
||||
CcdiBankTagRule largeRule = buildRule("EXTERNAL_LARGE_TRANSACTION", "外部人员大额交易",
|
||||
"EXTERNAL_SINGLE_LARGE_AMOUNT", "外部人员单笔大额交易", "STATEMENT");
|
||||
CcdiBankTagRule nightRule = buildRule("EXTERNAL_ABNORMAL_TRANSACTION", "外部人员异常交易",
|
||||
"EXTERNAL_NIGHT_TRANSACTION", "外部人员夜间集中交易", "STATEMENT");
|
||||
CcdiBankTagRule gamblingRule = buildRule("EXTERNAL_SUSPICIOUS_GAMBLING", "外部人员可疑赌博",
|
||||
"EXTERNAL_GAMBLING_MEMO", "外部人员疑似赌博摘要", "STATEMENT");
|
||||
CcdiBankTagRule relationRule = buildRule("EXTERNAL_SUSPICIOUS_RELATION", "外部人员可疑关系",
|
||||
"EXTERNAL_TO_STAFF_FAMILY_TRANSACTION", "外部人员与员工或员工亲属交易", "STATEMENT");
|
||||
BankTagRuleExecutionConfig largeConfig = buildConfig(40L, largeRule);
|
||||
largeConfig.setThresholdValues(Map.of("FREQUENT_TRANSFER", "100000"));
|
||||
|
||||
BankTagStatementHitVO hit = new BankTagStatementHitVO();
|
||||
hit.setBankStatementId(100L);
|
||||
hit.setGroupId(40);
|
||||
hit.setLogId(40001);
|
||||
hit.setReasonDetail("外部人员命中");
|
||||
|
||||
when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(largeRule, nightRule, gamblingRule, relationRule));
|
||||
when(configResolver.resolve(40L, largeRule)).thenReturn(largeConfig);
|
||||
when(configResolver.resolve(40L, nightRule)).thenReturn(buildConfig(40L, nightRule));
|
||||
when(configResolver.resolve(40L, gamblingRule)).thenReturn(buildConfig(40L, gamblingRule));
|
||||
when(configResolver.resolve(40L, relationRule)).thenReturn(buildConfig(40L, relationRule));
|
||||
when(analysisMapper.selectExternalSingleLargeAmountStatements(40L, new BigDecimal("100000"))).thenReturn(List.of(hit));
|
||||
when(analysisMapper.selectExternalNightTransactionStatements(40L)).thenReturn(List.of());
|
||||
when(analysisMapper.selectExternalGamblingMemoStatements(40L)).thenReturn(List.of());
|
||||
when(analysisMapper.selectExternalToStaffOrFamilyTransactionStatements(40L)).thenReturn(List.of());
|
||||
|
||||
service.rebuildProject(40L, null, "admin", TriggerType.MANUAL);
|
||||
|
||||
verify(analysisMapper).selectExternalSingleLargeAmountStatements(40L, new BigDecimal("100000"));
|
||||
verify(analysisMapper).selectExternalNightTransactionStatements(40L);
|
||||
verify(analysisMapper).selectExternalGamblingMemoStatements(40L);
|
||||
verify(analysisMapper).selectExternalToStaffOrFamilyTransactionStatements(40L);
|
||||
verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
|
||||
"EXTERNAL_LARGE_TRANSACTION".equals(item.getModelCode())
|
||||
&& "EXTERNAL_SINGLE_LARGE_AMOUNT".equals(item.getRuleCode())
|
||||
&& "STATEMENT".equals(item.getResultType())
|
||||
&& Long.valueOf(100L).equals(item.getBankStatementId())
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rebuildProject_shouldDispatchExternalPersonObjectRules() {
|
||||
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
|
||||
|
||||
CcdiBankTagRule cumulativeRule = buildRule("EXTERNAL_LARGE_TRANSACTION", "外部人员大额交易",
|
||||
"EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT", "外部人员累计交易超限", "OBJECT");
|
||||
CcdiBankTagRule annualRule = buildRule("EXTERNAL_LARGE_TRANSACTION", "外部人员大额交易",
|
||||
"EXTERNAL_ANNUAL_TURNOVER", "外部人员年流水交易额超限", "OBJECT");
|
||||
CcdiBankTagRule gamblingRule = buildRule("EXTERNAL_SUSPICIOUS_GAMBLING", "外部人员可疑赌博",
|
||||
"EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER", "外部人员同日多对手方疑似赌博交易", "OBJECT");
|
||||
BankTagRuleExecutionConfig cumulativeConfig = buildConfig(40L, cumulativeRule);
|
||||
cumulativeConfig.setThresholdValues(Map.of("CUMULATIVE_TRANSACTION_AMOUNT", "500000"));
|
||||
BankTagRuleExecutionConfig annualConfig = buildConfig(40L, annualRule);
|
||||
annualConfig.setThresholdValues(Map.of("ANNUAL_TURNOVER", "800000"));
|
||||
BankTagRuleExecutionConfig gamblingConfig = buildConfig(40L, gamblingRule);
|
||||
gamblingConfig.setThresholdValues(Map.of("MULTI_PARTY_AMT_MIN", "500", "MULTI_PARTY_AMT_MAX", "5000"));
|
||||
|
||||
BankTagObjectHitVO hit = new BankTagObjectHitVO();
|
||||
hit.setObjectType("EXTERNAL_CERT_NO");
|
||||
hit.setObjectKey("330100198801010033");
|
||||
hit.setReasonDetail("外部人员累计交易超限");
|
||||
|
||||
when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(cumulativeRule, annualRule, gamblingRule));
|
||||
when(configResolver.resolve(40L, cumulativeRule)).thenReturn(cumulativeConfig);
|
||||
when(configResolver.resolve(40L, annualRule)).thenReturn(annualConfig);
|
||||
when(configResolver.resolve(40L, gamblingRule)).thenReturn(gamblingConfig);
|
||||
when(analysisMapper.selectExternalCumulativeTransactionAmountObjects(40L, new BigDecimal("500000"))).thenReturn(List.of(hit));
|
||||
when(analysisMapper.selectExternalAnnualTurnoverObjects(40L, new BigDecimal("800000"))).thenReturn(List.of());
|
||||
when(analysisMapper.selectExternalMultiPartyGamblingTransferObjects(40L, new BigDecimal("500"), new BigDecimal("5000"))).thenReturn(List.of());
|
||||
|
||||
service.rebuildProject(40L, null, "admin", TriggerType.MANUAL);
|
||||
|
||||
verify(analysisMapper).selectExternalCumulativeTransactionAmountObjects(40L, new BigDecimal("500000"));
|
||||
verify(analysisMapper).selectExternalAnnualTurnoverObjects(40L, new BigDecimal("800000"));
|
||||
verify(analysisMapper).selectExternalMultiPartyGamblingTransferObjects(40L, new BigDecimal("500"), new BigDecimal("5000"));
|
||||
verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
|
||||
"EXTERNAL_LARGE_TRANSACTION".equals(item.getModelCode())
|
||||
&& "EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT".equals(item.getRuleCode())
|
||||
&& "OBJECT".equals(item.getResultType())
|
||||
&& "EXTERNAL_CERT_NO".equals(item.getObjectType())
|
||||
&& "330100198801010033".equals(item.getObjectKey())
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildSafeTaskErrorMessage_shouldKeepLongMessageForLongTextColumn() throws Exception {
|
||||
Method method = CcdiBankTagServiceImpl.class.getDeclaredMethod(
|
||||
|
||||
@@ -112,7 +112,7 @@ class CcdiBankTagServiceRiskCountRefreshTest {
|
||||
|
||||
verify(taskMapper).updateTask(argThat(task -> "FAILED".equals(task.getStatus())
|
||||
&& "refresh failed".equals(task.getErrorMessage())));
|
||||
verify(projectService).updateProjectStatus(40L, "0", "tester");
|
||||
verify(projectService).updateProjectStatus(40L, "4", "tester");
|
||||
}
|
||||
|
||||
private CcdiBankTagRule buildRule() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user