# 流水明细异常标签展示设计

## 背景

当前项目流水标签能力已经会把规则命中结果写入 `ccdi_bank_statement_tag_result`，结果中保留了：

- `rule_name`
- `risk_level`
- `reason_detail`
- `result_type`
- `bank_statement_id`

但现有“流水明细查询”页面和导出能力仍只读取 `ccdi_bank_statement`，列表、详情、导出文件都看不到当前流水命中的异常标签，用户无法直接在查询结果中判断一条流水为何被命中。

本次需求要求在“流水明细查询”页面的流水列表、流水详情和导出文件中，展示当前流水直接命中的异常标签信息。

## 目标

- 在流水列表中展示当前流水命中的异常标签名称。
- 在流水详情中展示当前流水命中的异常标签名称、风险等级、命中原因摘要。
- 在导出文件中追加“异常标签”“命中原因摘要”两列。
- 页面、详情、导出的标签口径保持一致。

## 范围

### In Scope

- `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
- `ruoyi-ui/src/api/ccdiProjectBankStatement.js`
- `ccdi-project` 现有流水查询 Controller / Service / Mapper
- `ccdi_bank_statement_tag_result` 的只读查询与组装
- 流水明细导出列扩展

### Out of Scope

- 新增按异常标签筛选、排序、页签统计
- 对象级标签结果展示
- 标签重算逻辑、规则 SQL、任务调度逻辑调整
- 新增独立标签页面

## 已确认口径

- 列表页只展示标签名称。
- 详情页展示标签名称、风险等级、命中原因摘要。
- 仅展示当前流水直接命中的流水级标签。
- 不把对象级标签混入流水列表、流水详情和导出。
- 导出时需要同时导出异常标签与命中原因摘要。

## 方案对比

### 方案一：服务层二次组装标签

- 列表先按现有分页逻辑查询流水。
- 再按当前页 `bankStatementId` 批量查询标签结果，并回填到列表 VO。
- 详情继续走原有详情查询，再单独补充当前流水标签明细。
- 导出先查导出范围内流水，再批量查询标签结果并拼装导出列。

优点：

- 改动集中在现有查询服务层，最短路径实现。
- 标签查询与主分页 SQL 解耦，便于维护和测试。
- 详情可返回结构化标签数据，适合前端展示。
- 列表、详情、导出口径容易统一。

缺点：

- 列表和导出阶段会增加一次标签批量查询。

### 方案二：在 Mapper 主查询里直接聚合标签

- 在 `CcdiBankStatementMapper.xml` 主查询中左连接标签表，并聚合标签字段。

优点：

- 接口层看起来较简单。

缺点：

- 分页 SQL、详情 SQL、导出 SQL 都会显著变复杂。
- 列表适合聚合字符串，不适合详情返回结构化标签。
- 后续维护成本高。

### 方案三：新增独立标签接口，由前端二次拉取

- 列表接口仍只查流水。
- 前端再调一个“按流水 ID 查询标签”的接口做二次组装。

优点：

- 流水主查询改动少。

缺点：

- 前端需要承担拼装逻辑。
- 导出仍需后端再实现一次同口径查询，容易分叉。

## 选型

采用方案一：服务层二次组装标签。

该方案最符合当前仓库“在既有模块上做局部扩展”的实现方式，不需要引入新的查询页面或复杂聚合 SQL，也能保证详情展示与导出口径一致。

## 设计原则

- 标签展示仅来自 `ccdi_bank_statement_tag_result` 中的流水级结果。
- 列表轻量展示，详情完整展示。
- 导出结果与页面口径一致，不额外创造新的标签解释规则。
- 不为本次需求扩展筛选、排序和统计能力。
- 主功能优先，标签展示失败不能拖垮流水列表查询。

## 数据口径设计

标签查询统一限定以下条件：

- `result_type = 'STATEMENT'`
- `bank_statement_id` 命中当前流水

标签结果读取字段：

- `rule_name`
- `risk_level`
- `reason_detail`
- `bank_statement_id`
- `rule_code`

其中：

- `rule_name` 用于列表、详情、导出展示
- `risk_level` 仅用于详情展示和前端标签样式映射
- `reason_detail` 用于详情展示和导出

对象级结果即使与当前流水所属身份证、账户或对象相关，也不参与本次页面与导出口径。

## 后端设计

### 一、VO 与导出模型扩展

建议新增一个统一标签明细 VO，例如：

- `CcdiBankStatementHitTagVO`
  - `ruleName`
  - `riskLevel`
  - `reasonDetail`

现有 VO 调整：

- `CcdiBankStatementListVO`
  - 新增 `hitTags`
- `CcdiBankStatementDetailVO`
  - 新增 `hitTags`
- `CcdiBankStatementExcel`
  - 新增 `hitTagNames`
  - 新增 `hitTagReasons`

列表 VO 中虽然只展示标签名称，但仍保留结构化 `hitTags`，这样可以减少后续再次改模型的成本，并让列表、详情、导出共享同一组装逻辑。

### 二、Mapper 查询设计

保留现有流水查询 Mapper 不做大改，只新增标签结果只读查询能力，建议放在 `CcdiBankTagResultMapper`：

1. 按流水 ID 集合批量查询标签结果
- 入参：`List<Long> bankStatementIds`
- 条件：`result_type = 'STATEMENT'`
- 返回：标签明细列表

2. 按单个流水 ID 查询标签结果
- 入参：`Long bankStatementId`
- 条件：`result_type = 'STATEMENT'`
- 返回：标签明细列表

排序建议统一按以下顺序之一稳定输出：

- `risk_level` + `rule_code`

这样同一流水的标签顺序在列表、详情、导出中保持一致。

### 三、Service 组装设计

#### 1. 列表查询

`selectStatementPage()` 调整为两阶段：

1. 调用现有 Mapper 查询分页流水。
2. 收集当前页全部 `bankStatementId`。
3. 批量查询这些流水的标签结果。
4. 按 `bankStatementId` 分组后回填到每条 `CcdiBankStatementListVO.hitTags`。

列表无标签时返回空集合，不返回 `null`，减少前端判空分支。

#### 2. 详情查询

`getStatementDetail()` 调整为：

1. 查询原有流水详情。
2. 按当前 `bankStatementId` 查询标签结果。
3. 回填到 `CcdiBankStatementDetailVO.hitTags`。

#### 3. 导出查询

`selectStatementListForExport()` 调整为：

1. 查询导出范围内全部流水。
2. 批量查询这些流水的标签结果。
3. 按流水 ID 分组。
4. 组装到 `CcdiBankStatementExcel`：
   - `异常标签`：按标签名称拼接
   - `命中原因摘要`：按相同顺序拼接

拼接分隔符建议统一使用全角分号 `；`，避免在 Excel 中与金额千分位或英文逗号混淆。

### 四、Controller 设计

不新增接口，继续复用：

- `GET /ccdi/project/bank-statement/list`
- `GET /ccdi/project/bank-statement/detail/{bankStatementId}`
- `POST /ccdi/project/bank-statement/export`

这样不会影响现有菜单入口和前端 API 结构。

## 前端设计

### 一、列表展示

在 `DetailQuery.vue` 的表格中新增“异常标签”列，位置放在“摘要 / 交易类型”和“交易金额”之间。

展示规则：

- 有标签：逐个渲染轻量标签组件，仅显示 `ruleName`
- 无标签：显示 `-`
- 单条流水命中多个标签：同列换行或折行展示

样式规则：

- 沿用 Element UI `el-tag` 轻量视觉
- 可按 `riskLevel` 映射 `type`，但列表不展示风险等级文案
- 不新增 tooltip、展开收起、二级详情等扩展交互

### 二、详情展示

在现有详情弹窗的基础字段区下方新增“命中异常标签”模块。

展示规则：

- 无标签：显示“当前流水未命中异常标签”
- 有标签：按条展示
  - 标签名称
  - 风险等级
  - 命中原因摘要

详情模块使用纵向结构，优先保证原因摘要可读性，不把多个原因压成单个文本段。

### 三、导出展示

导出 Excel 在现有列后追加：

- `异常标签`
- `命中原因摘要`

导出示例：

- `异常标签`：`房车消费支出交易；大额转账交易`
- `命中原因摘要`：`摘要命中购买房产首付款；转账金额 200000.00 元超过阈值`

## 错误处理

### 列表与详情

- 流水主查询成功、标签补充查询失败时，不让整个列表或详情失败。
- 列表标签列回退为空展示。
- 详情标签模块回退为无数据展示。
- 后端记录错误日志，便于排查标签结果查询异常。

### 导出

- 导出属于结果交付场景。
- 若标签结果查询或拼装失败，导出整体失败并返回错误。
- 不允许静默导出缺少标签列内容的文件，避免形成误导。

## 性能考虑

- 列表页只对当前页流水做一次批量标签查询，禁止逐条单查。
- 导出阶段对导出结果范围做一次批量标签查询，再在内存中按流水 ID 分组。
- 不把标签表聚合逻辑直接并入主分页 SQL，避免影响现有分页查询稳定性。

## 测试设计

### 后端单测

- 列表查询：无标签、单标签、多标签三种场景
- 详情查询：返回结构化标签明细
- 导出查询：正确拼装“异常标签”“命中原因摘要”
- 仅返回 `STATEMENT` 标签，不混入对象级结果

### Mapper / SQL 测试

- 批量标签查询能按 `bank_statement_id` 正确分组
- 同一流水多个标签返回顺序稳定

### 前端单测

- 列表标签列正确显示多个标签与空值占位
- 详情弹窗正确显示命中异常标签模块
- 不影响现有翻页、排序、详情打开能力

### 人工验证

1. 进入项目详情页的“流水明细查询”
2. 确认列表中异常标签列展示正常
3. 打开一条命中流水详情，确认名称、风险等级、命中原因摘要可见
4. 导出当前筛选结果，确认 Excel 中新增两列且内容与页面口径一致

## 风险与边界

- 若历史标签结果中 `reason_detail` 缺失，详情和导出只能展示空摘要；本次不补历史数据修复。
- 若同一流水命中标签较多，列表列宽可能变高；本次仅做轻量折行展示，不增加复杂交互。
- 本次不新增异常标签筛选，用户仍需通过导出或详情查看具体命中原因。

## 实施拆分建议

后续进入实施计划时，默认拆为两份文档：

- 后端实施计划：标签结果查询、VO 扩展、导出扩展、测试
- 前端实施计划：列表标签列、详情标签模块、单测与联调验证

## 结论

本次需求采用“现有流水查询 + 服务层批量补标签”的最短路径方案：

- 列表展示标签名称
- 详情展示标签名称、风险等级、命中原因摘要
- 导出追加异常标签与命中原因摘要
- 仅展示当前流水直接命中的流水级标签

该方案不改变现有流水查询入口、筛选项和标签计算逻辑，能在最小改动范围内补齐页面可读性与导出可交付性。
