Files
ccdi/docs/design/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-design.md

351 lines
11 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.
# LSFX Mock Server 异常账户基线同步设计文档
**模块**: `lsfx-mock-server`
**日期**: 2026-03-31
## 一、背景
当前 `lsfx-mock-server` 已完成异常账户命中流水的主链路接入:
- `FileService` 可为 `logId` 生成稳定的 `abnormal_account_hit_rules`
- `FileRecord` 内已保存 `abnormal_accounts`
- `StatementService` 已能按异常账户事实拼接 `SUDDEN_ACCOUNT_CLOSURE``DORMANT_ACCOUNT_LARGE_ACTIVATION` 命中流水
但现阶段异常账户事实仅存在于 Mock 进程内存中,尚未同步到主项目真实规则依赖的关联表 `ccdi_account_info`。这会导致两个问题:
1. Mock 返回的流水看起来满足异常账户规则,但真实打标 SQL 缺少账户事实,命中不稳定
2. 同一个 `logId` 下,“命中流水”与“真实账户事实”没有形成完整闭环
本次需求要求在生成可以命中异常账户的流水时,同时向关联表插入最小事实数据,保证真实规则命中条件成立。
## 二、目标
-`fetch_inner_flow(...)` / 上传创建 `logId` 时一次性同步异常账户事实到 `ccdi_account_info`
- 保持同一个 `logId` 的异常账户事实、返回流水、真实打标条件三者一致
- 保持现有 `/watson/api/project/getBSByLogId` 接口协议不变
- 保持 `StatementService` 只负责读 `FileRecord` 生成流水,不新增写库副作用
- 对异常账户基线写库失败采用显式失败语义,不返回半成功 `logId`
## 三、非目标
- 不新增异常账户独立接口
- 不修改现有随机规则命中策略
- 不扩展 `ccdi_account_info` 为完整账户域模型
- 不在 `getBSByLogId` 首次查询时补做异常账户写库
- 不新增兜底、补丁或降级链路
## 四、方案对比
### 4.1 方案 A在创建 `logId` 时同步异常账户基线,推荐
做法:
- `FileService` 生成 `FileRecord``abnormal_accounts`
- 在保存 `file_records[log_id]` 之前,同步将异常账户事实幂等写入 `ccdi_account_info`
- 后续 `StatementService` 只读内存事实生成流水
优点:
- 触发点单一,同一个 `logId` 只写一次
- 不把写库副作用混进读接口
- “命中前提未建好就不返回 `logId`” 的语义最清晰
- 与现有 `fetch_inner_flow -> getBSByLogId` 主链路最一致
缺点:
- 需要新增一个很小的异常账户基线写库服务
### 4.2 方案 B在 `getBSByLogId` 首次生成流水时再写库
优点:
- 只有真正查询流水时才落库
缺点:
- 读接口承担写库副作用,职责变重
- 缓存、重试和并发下更容易出现重复写库或半成功状态
- 不符合当前 Mock 服务“先建上传记录,再查流水”的链路习惯
### 4.3 方案 C继续只保留内存事实不做运行时写库
优点:
- 实现最省事
缺点:
- 无法保证真实规则稳定命中
- 不满足当前需求
## 五、结论
采用方案 A。
原因如下:
- 最短路径实现真实闭环
- 不破坏现有服务职责边界
- 最容易保证“同一个 `logId` 一次建好全部命中前提”
- 最符合你要求的“生成可以命中的流水时,同时向关联表插入数据”
## 六、总体设计
### 6.1 新增服务边界
新增 `AbnormalAccountBaselineService`,职责仅有一项:
-`FileRecord.abnormal_accounts` 幂等同步到 `ccdi_account_info`
职责划分如下:
- `FileService`
- 生成 `logId`
- 选择员工身份
- 生成异常账户命中计划
- 生成 `abnormal_accounts`
- 调用异常账户基线同步服务
- `AbnormalAccountBaselineService`
- 连接数据库
-`account_no` 为键执行幂等写入
- `StatementService`
- 继续只根据 `FileRecord` 生成命中流水
- 不负责数据库写入
### 6.2 调用顺序
改造后的 `fetch_inner_flow(...)` 主链路如下:
1. 生成 `logId`
2. 生成规则命中计划
3. 创建 `FileRecord`
4. 生成 `record.abnormal_accounts`
5. 调用 `_apply_abnormal_account_baselines(file_record)`
6. 基线写库成功后,再将 `file_record` 放入 `self.file_records`
7. 继续后续现有逻辑并返回响应
这个顺序的关键点是:
- 不把异常账户写库放到 `StatementService`
- 不在基线未落库成功时返回可用 `logId`
### 6.3 失败语义
-`abnormal_account_hit_rules` 为空:直接跳过,不写库
- 若命中了异常账户规则但 `abnormal_accounts` 为空:视为内部状态异常,直接失败
- 若数据库连接失败或 upsert 失败:`fetch_inner_flow(...)` 直接失败,本次 `logId` 不写入内存
- 不做补丁式重试,不返回半成功结果
## 七、数据模型设计
### 7.1 内存事实结构
继续复用当前 `FileRecord.abnormal_accounts` 结构,最小字段为:
- `account_no`
- `owner_id_card`
- `account_name`
- `status`
- `effective_date`
- `invalid_date`
- `rule_code`
说明:
- `rule_code` 仅作为 Mock 内部路由字段使用
- 对外接口不返回这批事实
### 7.2 `ccdi_account_info` 同步字段
本次只同步真实规则命中所需的最小字段:
- `account_no`
- `account_type`
- `account_name`
- `owner_type`
- `owner_id`
- `bank`
- `bank_code`
- `currency`
- `is_self_account`
- `trans_risk_level`
- `status`
- `effective_date`
- `invalid_date`
- `create_by`
- `update_by`
其中字段值约束如下:
- `account_no`
- 直接使用 `record.abnormal_accounts[*].account_no`
- `account_name`
- 直接使用 `record.abnormal_accounts[*].account_name`
- `owner_type`
- 固定写 `EMPLOYEE`
- `owner_id`
-`owner_id_card`
- `bank`
- 固定写当前异常账户样本对齐的银行名称
- `bank_code`
- 固定写当前异常账户样本对齐的银行编码
- `currency`
- 固定 `CNY`
- `is_self_account`
- 固定 `1`
- `trans_risk_level`
- 固定 `HIGH`
- `status`
- 由规则事实决定
- `effective_date`
- 由规则事实决定
- `invalid_date`
- 仅销户规则写值
本次不补充 `monthly_avg_trans_count``monthly_avg_trans_amount``dr_max_single_amount` 等推导型字段,因为当前两条真实规则命中依赖的是账户状态与流水窗口,不依赖这些预统计字段。
## 八、幂等策略设计
### 8.1 唯一定位键
`account_no` 作为异常账户事实的唯一定位键。
原因:
- Mock 内部异常账户事实和异常账户样本流水都以账号为唯一桥梁
- 同一个员工可能存在多个账户,按 `owner_id` 先删后插会扩大影响面
- 账号粒度最符合异常账户明细展示与后续回溯链路
### 8.2 Upsert 规则
对每条异常账户事实执行单条幂等 upsert
- 若账号不存在:插入
- 若账号已存在:覆盖本次 Mock 负责的核心字段
覆盖范围仅限:
- `account_name`
- `owner_type`
- `owner_id`
- `bank`
- `bank_code`
- `currency`
- `is_self_account`
- `trans_risk_level`
- `status`
- `effective_date`
- `invalid_date`
- `update_by`
- `update_time`
明确不做的事:
- 不按员工先删整批账户
- 不清空其他来源的账户数据
- 不以 `owner_id` 做批量覆盖
## 九、一致性约束
必须同时满足以下约束:
1. `record.abnormal_accounts[*].account_no` 必须等于对应异常账户样本流水的 `accountMaskNo`
2. `record.abnormal_accounts[*].owner_id_card` 必须等于对应异常账户样本流水的 `cretNo`
3. 同一个 `logId` 下,异常账户事实与异常账户流水必须来自同一份 `FileRecord`
4. `StatementService` 返回流水时不得覆盖已存在的异常账户样本账号
这意味着“内存事实 -> 返回流水 -> 数据库账户事实”三者会围绕同一个 `account_no` 对齐,后端真实 SQL 与结果回溯链路不会漂移。
## 十、模块改动设计
### 10.1 `lsfx-mock-server/services/file_service.py`
改动点:
- 注入新的 `abnormal_account_baseline_service`
- 新增 `_apply_abnormal_account_baselines(file_record)` 封装方法
-`fetch_inner_flow(...)` 与上传建档链路中,于 `self.file_records[log_id] = file_record` 之前调用该方法
职责保持:
- 仍是异常账户规则计划和事实的唯一生成入口
- 不直接拼装 SQL 字符串,数据库写入交给独立服务
### 10.2 `lsfx-mock-server/services/abnormal_account_baseline_service.py`
新增文件,提供:
- 数据库连接
- 输入校验
- 单条异常账户事实 upsert
- 批量 apply 入口
建议方法签名:
```python
def apply(self, staff_id_card: str, abnormal_accounts: List[dict]) -> None:
...
```
说明:
- `staff_id_card` 用于做最小一致性校验
- `abnormal_accounts` 为当前 `logId` 已生成好的异常账户事实列表
### 10.3 `lsfx-mock-server/services/statement_service.py`
本次不新增写库逻辑,仅维持现有一致性保证:
- 继续从 `FileRecord` 读取 `abnormal_accounts`
- 继续根据 `rule_code` 选择异常账户样本构造器
- 保持 `_apply_primary_binding(...)` 只兜底缺失账号,不覆盖异常账户样本账号
## 十一、测试设计
### 11.1 `tests/test_file_service.py`
补充断言:
- 命中异常账户规则时,`fetch_inner_flow(...)` 会调用异常账户基线同步服务
- 同步服务收到的账号、员工身份证、状态、生效日、销户日与 `record.abnormal_accounts` 完全一致
- 基线同步失败时,`file_records` 中不会残留该 `logId`
这里优先使用 fake service / stub 断言调用参数,不直接依赖真实数据库。
### 11.2 `tests/test_statement_service.py`
保留现有异常账户流水样本测试,再补充链路一致性断言:
- 同一个 `logId` 下,异常账户样本流水中的 `accountMaskNo` 必须全部来自 `record.abnormal_accounts`
- `StatementService` 不会因本次改造新增数据库写入副作用
### 11.3 `tests/test_abnormal_account_baseline_service.py`
新增服务层单测,覆盖:
- 空异常账户列表直接跳过
- 命中规则但事实为空时报错
- 新账号插入
- 已有账号按 `account_no` 幂等更新
## 十二、验收标准
本次设计实施后,应满足以下验收结果:
1. 创建 `logId` 时,命中的异常账户事实会一次性写入 `ccdi_account_info`
2. 同一个 `logId` 后续查询流水不会再次写库
3. `ccdi_account_info.account_no` 与异常账户样本流水 `accountMaskNo` 完全一致
4. 写库失败时,不返回半成功 `logId`
5. 现有异常账户命中流水生成、分页与缓存语义保持不变
## 十三、结论
本次采用“创建 `logId` 时一次性同步异常账户基线”的方式改造 `lsfx-mock-server`
- 让异常账户命中样本不再停留在 Mock 进程内存
-`ccdi_account_info` 与返回流水围绕同一个账号闭环
- 保持现有接口不变
- 保持最短路径实现,不引入兼容性和补丁式方案
这能确保 Mock 生成的异常账户流水不仅“看起来能命中”,而且“真实规则一定具备命中所需的账户事实前提”。