Files
ccdi/docs/design/2026-03-19-results-overview-risk-api-design.md

484 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 结果总览风险接口设计文档
**日期**: 2026-03-19
**模块**: 初核项目详情 - 结果总览
**作者**: Codex
**状态**: 已确认
## 一、概述
本文档用于沉淀结果总览页面中以下 3 个区块的后端设计方案:
- 风险仪表盘
- 风险人员总览
- 中高风险人员 TOP10
本次设计目标是为现有结果总览页面补齐真实后端接口,并把项目级高、中、低风险人数的统计口径收拢到流水标签打标链路中,保证项目列表、结果总览仪表盘、风险人员榜单三处口径一致。
## 二、设计范围
### 2.1 包含内容
- 新增结果总览 3 个独立后端查询接口
- 新增员工维度风险聚合查询
- 在项目流水标签打标完成后回写项目风险人数
- 输出对应后端实施计划与前端实施计划
### 2.2 不包含内容
- 不改造结果总览中的风险模型区、风险明细区
- 不新增设计需求之外的导出、弹窗、降级、补丁逻辑
- 不在查询接口中触发重新打标
- 不新增客户、中介等非员工维度榜单
## 三、当前上下文
### 3.1 前端现状
结果总览页面已经完成静态结构,相关文件如下:
- `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/OverviewStats.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js`
其中当前页面仍由本地 mock 数据驱动,三块目标区块的字段结构已经稳定:
- 风险仪表盘:`title/subtitle/stats`
- 风险人员总览:`overviewList`
- 中高风险人员 TOP10`topRiskList`
### 3.2 后端现状
当前仓库中还没有结果总览专用后端接口,但已有以下可复用数据基础:
1. `ccdi_project`
- 已有 `target_count`
- 已有 `high_risk_count`
- 已有 `medium_risk_count`
- 已有 `low_risk_count`
2. `ccdi_bank_statement_tag_result`
- 已沉淀项目维度的标签命中结果
-`project_id/model_code/rule_code/risk_level/bank_statement_id/object_type/object_key`
3. `ccdi_base_staff`
- 员工主数据,含 `name/dept_id/id_card`
4. `ccdi_staff_fmy_relation`
- 员工亲属映射,含 `person_id/relation_cert_no/status`
5. `sys_dept`
- 用于补齐所属部门名称
### 3.3 已确认业务口径
用户已确认以下口径:
1. 接口粒度
- 采用 3 个独立接口,而非 1 个汇总接口
2. 风险仪表盘口径
- `总人数 = ccdi_project.target_count`
- `高风险/中风险/低风险 = ccdi_project.high_risk_count / medium_risk_count / low_risk_count`
- `无风险人员 = 总人数 - 高风险 - 中风险 - 低风险`
3. 榜单统计对象
- 统计“员工本人 + 员工亲属”
- 若命中亲属,则归并到所属员工名下
4. 风险人员总览中的 `疑似违规数`
- 统计员工命中的去重规则数
5. 员工风险等级口径
- 命中规则数 `>= 5`:高风险
- 命中规则数 `2-4`:中风险
- 命中规则数 `= 1`:低风险
6. TOP10 排序
- 按员工风险等级优先级排序
- 同等级按命中模型数倒序
- 再按命中规则数倒序
- 最后按员工身份证号升序
7. 项目表风险人数维护方式
- 在项目流水标签打标完成后回写高、中、低风险人数
## 四、方案对比与结论
### 4.1 方案一:新增结果总览专用控制器与 3 个独立接口
优点:
- 与前端 3 个区块一一对应
- 查询职责边界清晰
- 后续风险模型区、风险明细区继续扩展时可沿用同一入口
缺点:
- 需要新增一组 DTO/VO/Mapper 查询
### 4.2 方案二:把接口直接追加到 `CcdiProjectController`
优点:
- 表面改动点更少
缺点:
- 项目管理与结果总览查询混在同一控制器
- 后续结果总览继续扩展时边界会变乱
### 4.3 方案三:提供 1 个汇总接口
优点:
- 请求数更少
缺点:
- 与已确认的“3 个独立接口”目标不符
- 单个区块口径变化会牵动整个响应结构
### 4.4 最终结论
采用方案一:新增结果总览专用控制器,提供 3 个独立接口,并新增员工维度聚合查询与项目风险人数回写逻辑。
## 五、接口设计
### 5.1 控制器落点
建议新增:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java`
接口统一挂载到:
- `/ccdi/project/overview`
### 5.2 接口清单
#### 接口一:查询风险仪表盘
- 方法:`GET`
- 路径:`/ccdi/project/overview/dashboard`
- 参数:`projectId`
- 返回:`AjaxResult.success(data)`
返回结构示例:
```json
{
"title": "风险仪表盘",
"subtitle": "风险仪表盘数据概览",
"stats": [
{ "key": "people", "label": "总人数", "value": 500 },
{ "key": "riskPeople", "label": "高风险", "value": 10 },
{ "key": "medium", "label": "中风险", "value": 20 },
{ "key": "low", "label": "低风险", "value": 38 },
{ "key": "count", "label": "无风险人员", "value": 432 }
]
}
```
#### 接口二:查询风险人员总览
- 方法:`GET`
- 路径:`/ccdi/project/overview/risk-people`
- 参数:`projectId`
- 返回:`AjaxResult.success(data)`
返回结构示例:
```json
{
"overviewList": [
{
"name": "李四",
"idNo": "330000000000000001",
"department": "信息二部",
"riskCount": 5,
"riskPoint": "大额单笔收入",
"actionLabel": "查看详情"
}
]
}
```
#### 接口三:查询中高风险人员 TOP10
- 方法:`GET`
- 路径:`/ccdi/project/overview/top-risk-people`
- 参数:`projectId`
- 返回:`AjaxResult.success(data)`
返回结构示例:
```json
{
"topRiskList": [
{
"name": "张三",
"idNo": "330000000000000002",
"department": "信贷部",
"riskLevel": "高风险",
"riskLevelType": "danger",
"modelCount": 8,
"actionLabel": "查看详情"
}
]
}
```
## 六、数据模型设计
### 6.1 新增 VO
建议新增以下 VO避免与现有 `CcdiProjectVO` 混用:
1. 仪表盘 VO
- `CcdiProjectOverviewDashboardVO`
- `CcdiProjectOverviewStatVO`
2. 风险人员总览 VO
- `CcdiProjectRiskPeopleOverviewVO`
- `CcdiProjectRiskPeopleOverviewItemVO`
3. 中高风险人员 TOP10 VO
- `CcdiProjectTopRiskPeopleVO`
- `CcdiProjectTopRiskPeopleItemVO`
4. 内部聚合中间 VO
- `CcdiProjectEmployeeRiskAggregateVO`
### 6.2 内部聚合字段
员工维度聚合中间结果至少包含:
- `staffIdCard`
- `staffName`
- `deptId`
- `deptName`
- `ruleCount`
- `modelCount`
- `topRuleCode`
- `topRuleName`
- `riskLevelCode`
- `riskLevelName`
- `riskLevelSort`
其中:
- `ruleCount = count(distinct rule_code)`
- `modelCount = count(distinct model_code)`
- `riskLevelCode` 根据 `ruleCount` 映射为 `HIGH/MEDIUM/LOW`
- `riskLevelName` 映射为 `高风险/中风险/低风险`
- `riskLevelSort` 仅供 SQL 排序使用,`HIGH=1``MEDIUM=2``LOW=3`
## 七、员工维度归并设计
### 7.1 归并目标
所有榜单都统一归并到员工维度,页面不直接展示亲属维度行。
### 7.2 归并规则
标签结果转员工归属时,按以下优先级处理:
1.`object_type = STAFF_ID_CARD``object_key` 命中员工身份证
- 直接归到该员工
2.`object_key` 为空,但该条结果关联到流水,且流水 `cret_no` 命中员工身份证
- 归到该员工
3.`object_key` 为空或为亲属证件号,且能通过 `ccdi_staff_fmy_relation.relation_cert_no` 关联到员工 `person_id`
- 归到该员工
4. 若无法归并到员工
- 本次查询中直接丢弃,不进入结果总览榜单
### 7.3 归并实现原则
- 不改造现有标签结果表结构
- 不为本次需求新增缓存表或汇总表
- 统一通过查询层完成员工归并
## 八、风险等级与项目人数回写设计
### 8.1 员工风险等级计算
按员工命中的去重规则数计算:
- `ruleCount >= 5` -> `HIGH` / `高风险`
- `ruleCount between 2 and 4` -> `MEDIUM` / `中风险`
- `ruleCount = 1` -> `LOW` / `低风险`
### 8.2 项目表风险人数统计
在员工维度聚合结果基础上统计:
- `highRiskCount = 员工风险等级为 HIGH 的人数`
- `mediumRiskCount = 员工风险等级为 MEDIUM 的人数`
- `lowRiskCount = 员工风险等级为 LOW 的人数`
### 8.3 回写时机
项目流水标签任务成功结束时执行回写,链路放在 `CcdiBankTagServiceImpl.rebuildProject(...)` 内:
1. 删除旧标签结果
2. 执行全部规则
3. 批量写入新标签结果
4. 重新按员工维度聚合高、中、低风险人数
5. 更新 `ccdi_project`
6. 更新任务状态为成功
这样可保证查询接口读到的项目风险人数与最新标签结果一致。
### 8.4 不采用的方案
不采用“查询时实时扫标签结果并顺手更新项目表”的做法,因为:
- 查询副作用过重
- 口径维护分散
- 项目列表与结果总览可能出现时间差
## 九、SQL 设计
### 9.1 Mapper 落点
建议新增:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml`
### 9.2 查询职责拆分
Mapper 只负责以下查询:
1. 查询项目仪表盘基础数据
2. 查询员工维度风险总览列表
3. 查询员工维度中高风险 TOP10
4. 查询项目员工风险等级分布人数
### 9.3 员工聚合 SQL 结构
建议采用“公共子查询 + 外层聚合”的方式:
1. 先构造标签结果到员工身份证的归并明细
2. 再按员工身份证聚合规则数、模型数和代表性规则
3. 最后外层补部门名称、风险等级和排序字段
建议公共子查询输出字段:
- `project_id`
- `staff_id_card`
- `rule_code`
- `rule_name`
- `model_code`
### 9.4 代表性异常点选择
`风险人员总览.riskPoint` 采用以下稳定选择策略:
1. 先按员工 + 规则维度统计命中次数
2. 按命中次数倒序
3. 再按 `rule_code` 升序
4. 取第一条 `rule_name`
这样不依赖数据库非确定性行为,也不会因为同条规则多次命中而随机波动。
### 9.5 TOP10 筛选规则
TOP10 查询仅保留:
- `ruleCount >= 2`
即仅展示中风险和高风险员工。
## 十、服务层设计
### 10.1 Service 落点
建议新增:
- `ICcdiProjectOverviewService`
- `CcdiProjectOverviewServiceImpl`
### 10.2 服务职责
服务层负责:
1. 调用 Mapper 查询项目仪表盘基础数据
2. 组装前端需要的 `title/subtitle/stats`
3. 调用员工聚合查询并映射到列表 VO
4. 在打标完成后调用项目风险人数回写逻辑
5. 处理空数据场景
### 10.3 空数据处理
1. 仪表盘
- 若项目存在但风险人数为空,按 `0` 返回
2. 风险人员总览
- 返回 `overviewList: []`
3. TOP10
- 返回 `topRiskList: []`
## 十一、控制器与权限设计
控制器沿用现有项目模块风格:
- 使用 `@RestController`
- 使用 `@Tag`
- 使用 `@Operation`
- 使用 `@PreAuthorize("@ss.hasPermi('ccdi:project:query')")`
- 返回 `AjaxResult.success(...)`
本次不新增新的权限标识,直接复用项目查询权限。
## 十二、异常处理设计
### 12.1 查询接口
- 项目不存在:返回业务异常,提示“项目不存在”
- `projectId` 为空:走参数校验失败
### 12.2 回写链路
若项目风险人数回写失败:
- 视为本次打标任务失败
- 不允许出现“标签写入成功但项目人数未更新且任务仍成功”的状态
原因是本次需求要求三处口径一致,项目人数回写属于打标成功链路的一部分,而不是可选附加动作。
## 十三、测试设计
### 13.1 后端单元/集成测试重点
1. 风险仪表盘
- 校验 `无风险人员` 计算逻辑
2. 风险人员总览
- 校验员工本人命中可正常归并
- 校验亲属命中可正常归并到员工
- 校验 `riskCount = 去重规则数`
- 校验 `riskPoint` 选择稳定
3. TOP10
- 校验仅返回中高风险
- 校验排序规则正确
4. 打标回写
- 校验任务成功后项目表风险人数被更新
- 校验回写失败时任务整体失败
### 13.2 前端联调验证重点
1. mock 替换为 3 个真实接口
2. 字段命名与当前页面结构一致
3. 空列表时页面能正确展示空表格
## 十四、风险与约束
### 14.1 已知约束
- 当前标签结果表未直接存员工归属字段,需要通过查询归并
- 部分标签结果可能来自 `bank_statement_id` 关联的流水,而非直接对象命中
### 14.2 风险控制
- 统一通过员工聚合查询输出榜单,避免前后接口各自实现一套归并逻辑
- 项目风险人数只在打标成功后回写,避免查询阶段产生副作用
- 不引入额外缓存与汇总表,保持最短实现路径
## 十五、最终结论
本方案采用“结果总览专用接口 + 标签完成后回写项目风险人数”的最短路径实现:
1. 结果总览新增 3 个独立接口
2. 风险榜单统一按员工维度聚合
3. 员工风险等级按命中规则条数区间计算
4. 项目表高、中、低风险人数在打标完成后统一回写
5. 项目列表、风险仪表盘、风险人员榜单三处口径保持一致