# 结果总览风险接口设计文档

**日期**: 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`
- `hitCount`
- `topRuleCode`
- `topRuleName`
- `riskPoint`
- `riskLevelCode`
- `riskLevelName`
- `riskLevelSort`

其中：

- `ruleCount = count(distinct rule_code)`
- `modelCount = count(distinct model_code)`
- `hitCount = count(1)`，表示归并到员工名下的打标明细数
- `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`

风险人员总览中的“疑似违规数”使用员工聚合后的 `hitCount`；风险等级、TOP10 和项目人数回写继续使用 `ruleCount`。

### 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. 项目列表、风险仪表盘、风险人员榜单三处口径保持一致
