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

11 KiB
Raw Blame History

LSFX Mock Server 异常账户基线同步设计文档

模块: lsfx-mock-server 日期: 2026-03-31

一、背景

当前 lsfx-mock-server 已完成异常账户命中流水的主链路接入:

  • FileService 可为 logId 生成稳定的 abnormal_account_hit_rules
  • FileRecord 内已保存 abnormal_accounts
  • StatementService 已能按异常账户事实拼接 SUDDEN_ACCOUNT_CLOSUREDORMANT_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 生成 FileRecordabnormal_accounts
  • 在保存 file_records[log_id] 之前,同步将异常账户事实幂等写入 ccdi_account_info
  • 后续 StatementService 只读内存事实生成流水

优点:

  • 触发点单一,同一个 logId 只写一次
  • 不把写库副作用混进读接口
  • “命中前提未建好就不返回 logId” 的语义最清晰
  • 与现有 fetch_inner_flow -> getBSByLogId 主链路最一致

缺点:

  • 需要新增一个很小的异常账户基线写库服务

4.2 方案 BgetBSByLogId 首次生成流水时再写库

优点:

  • 只有真正查询流水时才落库

缺点:

  • 读接口承担写库副作用,职责变重
  • 缓存、重试和并发下更容易出现重复写库或半成功状态
  • 不符合当前 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_countmonthly_avg_trans_amountdr_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 入口

建议方法签名:

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 生成的异常账户流水不仅“看起来能命中”,而且“真实规则一定具备命中所需的账户事实前提”。