# 项目详情风险明细异常账户人员信息设计文档

**模块**: 项目详情 - 结果总览 - 风险明细  
**日期**: 2026-03-31  
**作者**: Codex  
**状态**: 已确认

## 一、背景

当前项目详情页 `结果总览 -> 风险明细` 已经具备以下能力：

1. `涉疑交易明细` 已接入真实分页查询与统一导出。
2. `员工负面征信信息` 已接入真实分页查询，并已纳入统一导出。
3. `异常账户人员信息` 仍停留在前端静态占位与统一导出空 sheet。

与此同时，`2026-03-31` 已完成异常账户模型接入银行流水打标主链路：

- 模型编码：`ABNORMAL_ACCOUNT`
- 规则编码：
  - `SUDDEN_ACCOUNT_CLOSURE`
  - `DORMANT_ACCOUNT_LARGE_ACTIVATION`
- 命中结果已写入 `ccdi_bank_statement_tag_result`
- 员工风险聚合已能承接异常账户模型命中

因此，本次需求不是新增模型能力，而是将已有的异常账户命中结果正式接入 `风险明细` 区域展示，并保证统一导出中的 `异常账户人员信息` sheet 导出真实数据。

## 二、目标

本次设计目标如下：

1. 将 `异常账户人员信息` 区块从占位数据改为真实查询结果。
2. 页面展示字段与统一导出字段完全一致。
3. 风险明细统一导出中的第 3 个 sheet 改为真实导出异常账户人员信息。
4. 保持最短路径实现，不扩展详情弹窗、筛选器或平行链路。

## 三、范围

### 3.1 本次范围

- 新增结果总览专用异常账户人员分页查询接口
- 新增异常账户人员导出查询
- `RiskDetailSection.vue` 接入真实异常账户数据与独立分页
- `risk-details/export` 第 3 个 sheet 改为真实数据
- 补充本次设计文档与设计记录

### 3.2 不在本次范围

- 不新增异常账户详情弹窗
- 不新增异常账户区块筛选条件
- 不扩展到关系人或外部账户
- 不新增单独的异常账户导出接口
- 不改造项目分析弹窗
- 不新增兼容性补丁、兜底链路或降级方案

## 四、现状分析

### 4.1 前端现状

当前核心组件为：

- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`

当前 `异常账户人员信息` 区块仍直接读取：

- `sectionData.abnormalAccountList || []`

现有列结构仍是早期占位字段：

1. `账户号`
2. `账户人姓名`
3. `开户银行`
4. `异常发生时间`
5. `状态`
6. `操作`

这意味着当前页面展示既没有真实接口，也与本次统一导出的字段口径不完全一致。

### 4.2 后端现状

当前结果总览控制器为：

- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java`

当前已具备：

1. `GET /ccdi/project/overview/suspicious-transactions`
2. `GET /ccdi/project/overview/employee-credit-negative`
3. `POST /ccdi/project/overview/risk-details/export`

其中统一导出由：

- `CcdiProjectOverviewServiceImpl.exportRiskDetails(...)`
- `CcdiProjectRiskDetailWorkbookExporter`

共同完成。

但当前导出器对第 3 个 sheet 仅写入表头：

1. `账号`
2. `开户人`
3. `银行`
4. `异常类型`
5. `异常发生时间`
6. `状态`

没有真实数据查询与写出逻辑。

### 4.3 已有数据基础

异常账户模型命中结果已存在于：

- `ccdi_bank_statement_tag_result`

并且当前模型设计已明确：

- `model_code = 'ABNORMAL_ACCOUNT'`
- `result_type = 'OBJECT'`
- `object_type = 'STAFF_ID_CARD'`
- `object_key = 员工身份证号`

账户主数据已存在于：

- `ccdi_account_info`

因此本次展示与导出的最短路径，是直接基于异常账户对象型命中结果与账户信息表构造结果总览专用查询，而不是从聚合结果或前端 mock 数据反推。

## 五、方案对比

### 5.1 方案 A：新增结果总览专用异常账户查询链路，页面与导出共用同一口径

做法：

- 新增 `GET /ccdi/project/overview/abnormal-account-people`
- 新增服务层内部导出查询方法
- 查询源直接使用 `ccdi_bank_statement_tag_result + ccdi_account_info`
- 页面展示与统一导出共用同一套字段口径

优点：

- 页面与导出完全同口径
- 不需要解析占位数据
- 不依赖聚合快照反推细节
- 与“一条命中结果一行”的确认口径天然一致
- 改动集中在结果总览域内，符合最短路径

缺点：

- 需要补充新的 VO、Mapper SQL、Excel 行对象和测试

### 5.2 方案 B：复用项目分析弹窗对象型异常查询，再补字段拼装

做法：

- 基于 `selectPersonAnalysisObjectRows` 再改造成风险明细列表

问题：

- 现有查询主要返回标题、摘要和 `reasonDetail`
- 不直接提供 `账号 / 银行 / 状态 / 异常发生时间`
- 需要从 `reasonDetail` 反解析字段，稳定性差
- 不适合作为统一导出数据源

### 5.3 方案 C：基于员工风险聚合表反推异常账户明细

做法：

- 以 `ccdi_project_overview_employee_result` 为主，再关联账户表补全字段

问题：

- 聚合层已经丢失“每条命中结果一行”的细粒度
- 难以稳定还原 `异常类型` 与 `异常发生时间`
- 容易导致页面与导出口径偏移

### 5.4 结论

采用 **方案 A：新增结果总览专用异常账户查询链路，页面与导出共用同一口径**。

## 六、总体设计

### 6.1 设计原则

本次设计遵循以下原则：

1. 以已有异常账户打标结果为唯一事实来源。
2. 页面展示与导出字段保持完全一致。
3. 一条命中结果一行，不做账号合并、不做员工合并。
4. 仅识别员工本人账户，不扩展关系人或外部账户。
5. 不新增平行模块，所有改动收口在结果总览域。

### 6.2 数据流

页面查询链路：

1. `RiskDetailSection.vue` 加载当前项目的异常账户人员分页数据。
2. 前端调用 `GET /ccdi/project/overview/abnormal-account-people`。
3. 控制器调用 `overviewService.getAbnormalAccountPeople(...)`。
4. 服务层校验项目存在，调用 Mapper 分页 SQL。
5. Mapper 从异常账户对象命中结果与账户信息表中返回结果。
6. 前端渲染 `异常账户人员信息` 表格。

统一导出链路：

1. 用户点击 `风险明细` 卡片右上角 `导出`。
2. 前端调用 `POST /ccdi/project/overview/risk-details/export`。
3. 服务层查询：
   - 涉疑交易全量数据
   - 员工负面征信全量数据
   - 异常账户人员全量数据
4. `CcdiProjectRiskDetailWorkbookExporter` 统一生成 3 个 sheet。
5. 第 3 个 sheet `异常账户人员信息` 写出真实数据。

## 七、字段与业务口径

### 7.1 页面与导出统一字段

本次 `异常账户人员信息` 页面与导出统一使用以下 6 个字段：

1. `账号`
2. `开户人`
3. `银行`
4. `异常类型`
5. `异常发生时间`
6. `状态`

不保留“操作”列，也不新增辅助列。

### 7.2 粒度口径

展示与导出粒度固定为：

- 一条异常账户命中结果一行

规则说明：

- 同一员工命中多条异常账户规则时，保留多行
- 同一账号命中多条规则时，也保留多行
- 不按员工汇总
- 不按账号合并

### 7.3 字段映射规则

#### 1. 账号

- 取 `ccdi_account_info.account_no`

#### 2. 开户人

- 优先取 `ccdi_account_info.account_name`
- 若为空，则回退员工姓名

#### 3. 银行

- 取 `ccdi_account_info.bank`

#### 4. 异常类型

- 取 `ccdi_bank_statement_tag_result.rule_name`

#### 5. 异常发生时间

- 对 `SUDDEN_ACCOUNT_CLOSURE` 取账户销户日期 `invalid_date`
- 对 `DORMANT_ACCOUNT_LARGE_ACTIVATION` 取首次交易日期 `first_tx_date`
- 统一格式化为日期字符串

#### 6. 状态

- 取 `ccdi_account_info.status`
- 映射文案固定为：
  - `1 -> 正常`
  - `2 -> 已销户`

本次不额外扩展更多状态码解释。

## 八、后端设计

### 8.1 控制器接口

在 `CcdiProjectOverviewController` 下新增接口：

- `GET /ccdi/project/overview/abnormal-account-people`

入参：

- `projectId`
- `pageNum`
- `pageSize`

权限：

- 沿用结果总览查询权限 `ccdi:project:query`

返回结构：

- `rows`
- `total`

### 8.2 服务层职责

在 `ICcdiProjectOverviewService` 与实现类中新增：

1. `getAbnormalAccountPeople(queryDTO)`
2. `exportAbnormalAccountPeople(projectId)`

服务层职责如下：

1. 校验项目存在
2. 处理分页参数
3. 查询异常账户人员分页或导出数据
4. 将查询结果映射为页面 VO 或导出 Excel 对象
5. 在 `exportRiskDetails(...)` 中将异常账户全量数据传入工作簿导出器

### 8.3 Mapper 查询策略

查询必须满足以下约束：

1. 仅查询当前项目：
   - `tr.project_id = 当前项目`
2. 仅查询异常账户模型：
   - `tr.model_code = 'ABNORMAL_ACCOUNT'`
3. 仅查询对象型结果：
   - `tr.bank_statement_id is null`
4. 仅查询员工本人账户：
   - `account.owner_type = 'EMPLOYEE'`
   - `account.owner_id = tr.object_key`
5. 每条命中结果唯一关联到一条账户记录

### 8.4 账户唯一关联规则

由于异常账户对象型结果以“员工身份证号”为主键落库，本次查询必须保证命中结果可稳定回溯到具体账户。

设计约束如下：

1. 优先依据异常账户规则 `reason_detail` 中的账号信息匹配 `ccdi_account_info.account_no`
2. 仅在账号匹配成功时返回该条结果
3. 不允许仅凭员工身份证号将同一员工下全部账户全部展开，避免误导

这意味着本次实现同时要求异常账户对象型结果具备“可唯一回溯到账号”的查询条件，不使用模糊补数方案。

### 8.5 导出收口

`POST /ccdi/project/overview/risk-details/export` 继续保持统一导出入口，不额外新增独立异常账户导出接口。

服务层导出步骤调整为：

1. 查询涉疑交易全量数据
2. 查询员工负面征信全量数据
3. 查询异常账户人员全量数据
4. 将三类数据统一传入 `CcdiProjectRiskDetailWorkbookExporter`

导出文件顺序保持不变：

1. `涉疑交易明细`
2. `员工负面征信信息`
3. `异常账户人员信息`

## 九、前端设计

### 9.1 页面位置

本次前端改动集中在：

- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`

### 9.2 区块展示

`异常账户人员信息` 区块调整为真实业务表格，字段顺序固定为：

1. `账号`
2. `开户人`
3. `银行`
4. `异常类型`
5. `异常发生时间`
6. `状态`

副标题保持语义一致，可调整为：

- `展示异常账户命中人员及账户状态`

### 9.3 交互规则

- 不新增区块级导出按钮
- 不新增查看详情按钮
- 不新增行操作列
- 保持独立分页
- 保持独立 loading
- 查询失败时仅影响该区块，不影响其他两个风险明细区块

### 9.4 空态文案

空态文案统一为：

- `当前项目暂无异常账户人员信息`

## 十、测试设计

### 10.1 后端测试

新增或调整以下验证：

1. Mapper SQL 测试
   - 校验异常账户分页查询与导出查询包含 `ABNORMAL_ACCOUNT`、项目过滤、对象型过滤和账户关联条件
2. Service 测试
   - 校验异常账户分页查询 `rows/total` 返回正确
   - 校验统一导出会将异常账户真实数据传入导出器
3. Workbook 导出测试
   - 校验第 3 个 sheet 存在真实数据行
   - 校验列顺序为：
     - `账号`
     - `开户人`
     - `银行`
     - `异常类型`
     - `异常发生时间`
     - `状态`

### 10.2 前端测试

新增或调整以下验证：

1. `RiskDetailSection.vue` 异常账户真实字段渲染测试
2. 异常账户区块独立分页测试
3. 统一导出按钮仍走 `risk-details/export` 的测试
4. 移除旧占位“操作 / 查看详情”列的静态断言

## 十一、边界与异常处理

### 11.1 空数据场景

当项目下没有异常账户命中结果时：

- 页面显示空态
- 导出 sheet 仅保留表头，不输出数据行

### 11.2 查询失败场景

页面查询失败时：

- 清空当前异常账户列表
- 提示：`加载异常账户人员信息失败`
- 不联动清空涉疑交易或员工负面征信区块

统一导出失败时：

- 沿用当前服务层异常提示：`导出风险明细失败`

### 11.3 非本次范围约束

本次明确不做以下扩展：

- 不新增详情弹窗
- 不增加筛选条件
- 不补关系人账户
- 不增加异步导出任务
- 不在导出中追加页面外字段

## 十二、后续文档规划

设计确认并完成文档复核后，继续补充两份实施计划：

1. 后端实施计划：`docs/plans/backend/`
2. 前端实施计划：`docs/plans/frontend/`

随后按实际改动沉淀实施记录。
