diff --git a/docs/plans/backend/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-implementation.md b/docs/plans/backend/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-implementation.md new file mode 100644 index 00000000..31da6fb2 --- /dev/null +++ b/docs/plans/backend/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-implementation.md @@ -0,0 +1,486 @@ +# LSFX Mock Server 异常账户基线同步后端 Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> 仓库约束:当前仓库明确禁止开启 subagent,执行时统一使用 `superpowers:executing-plans`。 + +**Goal:** 在 `lsfx-mock-server` 创建 `logId` 时一次性把异常账户事实同步到 `ccdi_account_info`,让同一个 `logId` 下的异常账户事实、命中流水和真实打标前提稳定闭环。 + +**Architecture:** 继续复用现有 `FileService -> FileRecord -> StatementService` 主链路,不新增接口,也不把写库副作用混进 `getBSByLogId`。新增一个很小的 `AbnormalAccountBaselineService` 负责按 `account_no` 幂等 upsert `ccdi_account_info`,由 `FileService` 在保存 `file_records[log_id]` 前调用;`StatementService` 仍只读取 `FileRecord` 生成异常账户流水样本。 + +**Tech Stack:** Python 3, FastAPI, PyMySQL, pytest, dataclasses, Markdown docs + +--- + +## File Structure + +- `lsfx-mock-server/services/abnormal_account_baseline_service.py`: 新增异常账户基线写库服务,封装数据库连接、输入校验和按账号幂等 upsert。 +- `lsfx-mock-server/services/file_service.py`: 注入异常账户基线服务,并在 `fetch_inner_flow(...)` / 上传建档链路中于保存 `FileRecord` 前触发基线同步。 +- `lsfx-mock-server/services/statement_service.py`: 只补链路一致性断言,不新增写库逻辑。 +- `lsfx-mock-server/tests/test_abnormal_account_baseline_service.py`: 新增服务层单测,覆盖空输入、异常输入、插入和更新。 +- `lsfx-mock-server/tests/test_file_service.py`: 锁定 `fetch_inner_flow(...)` 调用基线同步服务及失败回滚语义。 +- `lsfx-mock-server/tests/test_statement_service.py`: 锁定异常账户样本流水与 `record.abnormal_accounts` 账号一致。 +- `docs/reports/implementation/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-implementation.md`: 记录本次后端实施结果。 +- `docs/tests/records/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-verification.md`: 记录 pytest 验证命令、结果和进程清理结论。 + +## Task 1: 先锁定 `FileService` 的基线同步触发点 + +**Files:** +- Modify: `lsfx-mock-server/tests/test_file_service.py` +- Modify: `lsfx-mock-server/services/file_service.py` +- Reference: `docs/design/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-design.md` + +- [ ] **Step 1: Write the failing test** + +在 `lsfx-mock-server/tests/test_file_service.py` 中新增 fake baseline service 和两条失败测试: + +```python +class FakeAbnormalAccountBaselineService: + def __init__(self, should_fail=False): + self.should_fail = should_fail + self.calls = [] + + def apply(self, staff_id_card, abnormal_accounts): + self.calls.append( + { + "staff_id_card": staff_id_card, + "abnormal_accounts": [dict(item) for item in abnormal_accounts], + } + ) + if self.should_fail: + raise RuntimeError("baseline sync failed") + + +def test_fetch_inner_flow_should_sync_abnormal_account_baselines_before_caching(): + baseline_service = FakeAbnormalAccountBaselineService() + service = FileService( + staff_identity_repository=FakeStaffIdentityRepository(), + abnormal_account_baseline_service=baseline_service, + ) + + response = service.fetch_inner_flow( + { + "groupId": 1001, + "customerNo": "customer_abnormal_baseline", + "dataChannelCode": "test_code", + "requestDateId": 20240101, + "dataStartDateId": 20240101, + "dataEndDateId": 20240131, + "uploadUserId": 902001, + } + ) + + log_id = response["data"][0] + record = service.file_records[log_id] + + assert baseline_service.calls + assert baseline_service.calls[0]["staff_id_card"] == record.staff_id_card + assert baseline_service.calls[0]["abnormal_accounts"] == record.abnormal_accounts + + +def test_fetch_inner_flow_should_not_cache_log_id_when_abnormal_account_baseline_sync_fails(): + baseline_service = FakeAbnormalAccountBaselineService(should_fail=True) + service = FileService( + staff_identity_repository=FakeStaffIdentityRepository(), + abnormal_account_baseline_service=baseline_service, + ) + + with pytest.raises(RuntimeError, match="baseline sync failed"): + service.fetch_inner_flow( + { + "groupId": 1001, + "customerNo": "customer_abnormal_baseline_fail", + "dataChannelCode": "test_code", + "requestDateId": 20240101, + "dataStartDateId": 20240101, + "dataEndDateId": 20240131, + "uploadUserId": 902001, + } + ) + + assert service.file_records == {} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd lsfx-mock-server +python3 -m pytest tests/test_file_service.py -k "abnormal_account_baseline" -v +``` + +Expected: + +- `FAIL` +- 原因是 `FileService` 还不接受 `abnormal_account_baseline_service` 注入,也没有在建档阶段触发写库 + +- [ ] **Step 3: Write minimal implementation** + +在 `lsfx-mock-server/services/file_service.py` 中按最小路径补齐: + +1. 新增构造参数: + +```python +def __init__(..., abnormal_account_baseline_service=None): + self.abnormal_account_baseline_service = ( + abnormal_account_baseline_service or AbnormalAccountBaselineService() + ) +``` + +2. 新增内部封装: + +```python +def _apply_abnormal_account_baselines(self, file_record: FileRecord) -> None: + if not file_record.abnormal_account_hit_rules: + return + if not file_record.abnormal_accounts: + raise RuntimeError("异常账户命中计划存在,但未生成账户事实") + self.abnormal_account_baseline_service.apply( + staff_id_card=file_record.staff_id_card, + abnormal_accounts=file_record.abnormal_accounts, + ) +``` + +3. 在 `fetch_inner_flow(...)` 和上传链路中,将: + +```python +self.file_records[log_id] = file_record +``` + +调整为: + +```python +self._apply_abnormal_account_baselines(file_record) +self.file_records[log_id] = file_record +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +cd lsfx-mock-server +python3 -m pytest tests/test_file_service.py -k "abnormal_account_baseline" -v +``` + +Expected: + +- `PASS` +- 成功路径会先调用 baseline service +- 失败路径不会把半成品 `logId` 写入 `file_records` + +- [ ] **Step 5: Commit** + +```bash +git add lsfx-mock-server/services/file_service.py lsfx-mock-server/tests/test_file_service.py +git commit -m "接入异常账户基线同步触发点" +``` + +## Task 2: 实现异常账户基线写库服务 + +**Files:** +- Create: `lsfx-mock-server/services/abnormal_account_baseline_service.py` +- Modify: `lsfx-mock-server/tests/test_abnormal_account_baseline_service.py` +- Reference: `docs/design/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-design.md` + +- [ ] **Step 1: Write the failing tests** + +新建 `lsfx-mock-server/tests/test_abnormal_account_baseline_service.py`,先锁定四类行为: + +```python +def test_apply_should_skip_when_abnormal_accounts_is_empty(): + service = AbnormalAccountBaselineService() + fake_connection = FakeConnection() + service._connect = lambda: fake_connection + + service.apply("330101199001010001", []) + + assert fake_connection.executed_sql == [] + + +def test_apply_should_raise_when_fact_owner_mismatches_staff(): + service = AbnormalAccountBaselineService() + + with pytest.raises(RuntimeError, match="owner_id_card"): + service.apply( + "330101199001010001", + [ + { + "account_no": "6222000000000001", + "owner_id_card": "330101199001010099", + "account_name": "测试员工工资卡", + "status": 2, + "effective_date": "2024-01-01", + "invalid_date": "2026-03-20", + "rule_code": "SUDDEN_ACCOUNT_CLOSURE", + } + ], + ) + + +def test_apply_should_insert_new_account_fact_by_account_no(): + ... + + +def test_apply_should_update_existing_account_fact_by_account_no(): + ... +``` + +`FakeConnection` / `FakeCursor` 只需记录 `execute(...)` 调用和提交次数,不需要真实数据库。 + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd lsfx-mock-server +python3 -m pytest tests/test_abnormal_account_baseline_service.py -v +``` + +Expected: + +- `FAIL` +- 原因是服务文件尚不存在 + +- [ ] **Step 3: Write minimal implementation** + +在 `lsfx-mock-server/services/abnormal_account_baseline_service.py` 中实现: + +1. `__init__` 直接复用现有 `settings.CCDI_DB_*` +2. `_connect()` 使用 `pymysql.connect(..., charset="utf8mb4", autocommit=False)` +3. `apply(staff_id_card, abnormal_accounts)` 内部规则: + - 空列表直接返回 + - 若任一 `owner_id_card` 与 `staff_id_card` 不一致,直接抛错 + - 对每条 fact 执行单条 upsert + - 成功后统一 `commit()`,失败则 `rollback()` + +建议 upsert 语句形态: + +```sql +INSERT INTO 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 +) +VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) +ON DUPLICATE KEY UPDATE + account_name = VALUES(account_name), + owner_type = VALUES(owner_type), + owner_id = VALUES(owner_id), + bank = VALUES(bank), + bank_code = VALUES(bank_code), + currency = VALUES(currency), + is_self_account = VALUES(is_self_account), + trans_risk_level = VALUES(trans_risk_level), + status = VALUES(status), + effective_date = VALUES(effective_date), + invalid_date = VALUES(invalid_date), + update_by = VALUES(update_by), + update_time = NOW() +``` + +固定值约束: + +- `account_type = 'DEBIT'` +- `owner_type = 'EMPLOYEE'` +- `bank = '兰溪农商银行'` +- `bank_code = 'LXNCSY'` +- `currency = 'CNY'` +- `is_self_account = 1` +- `trans_risk_level = 'HIGH'` +- `create_by/update_by = 'lsfx-mock-server'` + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +cd lsfx-mock-server +python3 -m pytest tests/test_abnormal_account_baseline_service.py -v +``` + +Expected: + +- `PASS` +- 覆盖空输入、校验失败、插入和更新四类行为 + +- [ ] **Step 5: Commit** + +```bash +git add lsfx-mock-server/services/abnormal_account_baseline_service.py lsfx-mock-server/tests/test_abnormal_account_baseline_service.py +git commit -m "新增异常账户基线写库服务" +``` + +## Task 3: 锁定异常账户事实与返回流水的一致性 + +**Files:** +- Modify: `lsfx-mock-server/tests/test_statement_service.py` +- Reference: `lsfx-mock-server/services/statement_service.py` +- Reference: `lsfx-mock-server/services/statement_rule_samples.py` + +- [ ] **Step 1: Write the failing test** + +在 `lsfx-mock-server/tests/test_statement_service.py` 中新增一条只校验一致性的用例: + +```python +def test_get_bank_statement_should_only_use_abnormal_account_numbers_from_file_record(): + file_service = FileService( + staff_identity_repository=FakeStaffIdentityRepository(), + abnormal_account_baseline_service=FakeAbnormalAccountBaselineService(), + ) + statement_service = StatementService(file_service=file_service) + + response = file_service.fetch_inner_flow( + { + "groupId": 1001, + "customerNo": "customer_abnormal_statement_consistency", + "dataChannelCode": "test_code", + "requestDateId": 20240101, + "dataStartDateId": 20240101, + "dataEndDateId": 20240131, + "uploadUserId": 902001, + } + ) + log_id = response["data"][0] + record = file_service.file_records[log_id] + record.abnormal_account_hit_rules = ["SUDDEN_ACCOUNT_CLOSURE"] + record.abnormal_accounts = [ + { + "account_no": "6222000000000099", + "owner_id_card": record.staff_id_card, + "account_name": "测试员工工资卡", + "status": 2, + "effective_date": "2024-01-01", + "invalid_date": "2026-03-20", + "rule_code": "SUDDEN_ACCOUNT_CLOSURE", + } + ] + + result = statement_service.get_bank_statement( + {"groupId": 1001, "logId": log_id, "pageNow": 1, "pageSize": 500} + ) + abnormal_numbers = { + item["accountMaskNo"] + for item in result["data"]["bankStatementList"] + if "销户" in item["userMemo"] or "异常账户" in item["userMemo"] + } + + assert abnormal_numbers == {"6222000000000099"} +``` + +- [ ] **Step 2: Run test to verify it fails when chain drifts** + +Run: + +```bash +cd lsfx-mock-server +python3 -m pytest tests/test_statement_service.py::test_get_bank_statement_should_only_use_abnormal_account_numbers_from_file_record -v +``` + +Expected: + +- 若当前实现已满足,可直接 `PASS` +- 若失败,只允许修正账号回填链路,禁止引入写库逻辑到 `StatementService` + +- [ ] **Step 3: Keep implementation minimal** + +若失败,仅允许在 `lsfx-mock-server/services/statement_service.py` 中做最小修正: + +- `_apply_primary_binding(...)` 继续只兜底空账号 +- 不覆盖异常账户样本已有 `accountMaskNo` +- 不新增数据库连接或写库逻辑 + +- [ ] **Step 4: Run focused statement tests** + +Run: + +```bash +cd lsfx-mock-server +python3 -m pytest tests/test_statement_service.py -k "abnormal_account" -v +``` + +Expected: + +- `PASS` +- 既有异常账户样本日期/金额测试与新增一致性测试同时通过 + +- [ ] **Step 5: Commit** + +```bash +git add lsfx-mock-server/services/statement_service.py lsfx-mock-server/tests/test_statement_service.py +git commit -m "锁定异常账户流水与账户事实一致性" +``` + +## Task 4: 完成回归验证并补实施记录 + +**Files:** +- Create: `docs/reports/implementation/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-implementation.md` +- Create: `docs/tests/records/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-verification.md` + +- [ ] **Step 1: Run full targeted backend tests** + +Run: + +```bash +cd lsfx-mock-server +python3 -m pytest \ + tests/test_abnormal_account_baseline_service.py \ + tests/test_file_service.py \ + tests/test_statement_service.py -k "abnormal_account or abnormal_account_baseline" -v +``` + +Expected: + +- `PASS` +- 无新增异常账户相关失败 + +- [ ] **Step 2: Write implementation record** + +在 `docs/reports/implementation/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-implementation.md` 中记录: + +- 新增 `AbnormalAccountBaselineService` +- `FileService` 在建 `logId` 时同步异常账户基线 +- 失败回滚语义 +- 异常账户事实与返回流水的一致性约束 + +- [ ] **Step 3: Write verification record** + +在 `docs/tests/records/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-verification.md` 中记录: + +- 执行过的 pytest 命令 +- 关键通过点 +- 本次未启动前后端长驻进程,因此无需额外杀进程 + +- [ ] **Step 4: Verify final diff scope** + +Run: + +```bash +git diff --name-only HEAD~3..HEAD +``` + +Expected: + +- 仅包含本次异常账户基线同步相关服务、测试和文档 + +- [ ] **Step 5: Commit** + +```bash +git add \ + docs/reports/implementation/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-implementation.md \ + docs/tests/records/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-backend-verification.md +git commit -m "记录异常账户基线同步后端实施" +``` diff --git a/docs/plans/frontend/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-implementation.md b/docs/plans/frontend/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-implementation.md new file mode 100644 index 00000000..00d1f55b --- /dev/null +++ b/docs/plans/frontend/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-implementation.md @@ -0,0 +1,128 @@ +# LSFX Mock Server 异常账户基线同步前端 Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> 仓库约束:当前仓库明确禁止开启 subagent,执行时统一使用 `superpowers:executing-plans`。 + +**Goal:** 在不修改 `ruoyi-ui` 源码的前提下,明确本次 `lsfx-mock-server` 异常账户基线同步对前端的影响边界,沉淀“零代码改动”实施计划与核验记录。 + +**Architecture:** 本次需求只增强 Mock 服务内部的异常账户事实落库能力,不修改对外银行流水接口协议,也不新增前端入参、页面或调试入口。前端计划采用“影响面检索 + 协议不变确认 + 文档留痕”的最短路径;若核查发现必须适配新字段或新交互,应停止执行并回到设计阶段,而不是在本计划中扩展 UI。 + +**Tech Stack:** Vue 2, rg, git diff, Markdown docs + +--- + +## File Structure + +- `ruoyi-ui/src/api/`: 只用于检索是否存在直接依赖 `lsfx-mock-server` 异常账户内部事实的新接口封装,不预期修改。 +- `ruoyi-ui/src/views/ccdiProject/`: 只用于确认现有页面是否直接依赖 Mock 内部账户事实,不预期修改。 +- `docs/reports/implementation/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-implementation.md`: 记录前端零代码改动结论。 +- `docs/tests/records/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-verification.md`: 记录检索命令、查验范围和判断依据。 + +## Task 1: 核验前端是否需要承接本次 Mock 基线同步 + +**Files:** +- Reference: `ruoyi-ui/src/api/` +- Reference: `ruoyi-ui/src/views/ccdiProject/` +- Reference: `docs/design/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-design.md` + +- [ ] **Step 1: Check existing frontend touchpoints** + +先确认本次需求是否触达以下任一前端边界: + +- 前端是否直接调用 `lsfx-mock-server` 并依赖异常账户内部事实 +- 前端是否需要新增字段才能继续消费 `/watson/api/project/getBSByLogId` +- 前端是否存在专门围绕 Mock 联调的页面、按钮或测试入口需要跟进 + +若三项都不存在,则本轮前端默认保持零代码改动。 + +- [ ] **Step 2: Verify with search commands** + +Run: + +```bash +cd ruoyi-ui +rg -n "lsfx|mock|异常账户|getBSByLogId|bankStatement|account_info" src +``` + +Expected: + +- 不存在必须新增前端适配的直接依赖 +- 不应因为 Mock 内部写库增强而顺手增加演示页、调试页或临时开关 + +- [ ] **Step 3: Confirm contract stability** + +对照设计文档确认以下三点全部成立: + +- `/watson/api/project/getBSByLogId` 返回结构不变 +- 本次只新增 Mock 内部异常账户基线写库,不新增前端入参 +- 风险页面仍只消费后端聚合结果,不直接读取 `ccdi_account_info` + +若任一点不成立,停止执行并回到设计阶段。 + +- [ ] **Step 4: Record the no-op boundary** + +在后续实施记录中明确写明: + +- 本次需求不涉及 `ruoyi-ui` 源码修改 +- 前端不会为了“方便联调”新增占位页面、按钮或 mock 参数 + +- [ ] **Step 5: Commit** + +```bash +git add \ + docs/reports/implementation/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-implementation.md \ + docs/tests/records/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-verification.md +git commit -m "记录异常账户基线同步前端零改动结论" +``` + +## Task 2: 沉淀前端核验记录并确认源码未被误改 + +**Files:** +- Create: `docs/reports/implementation/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-implementation.md` +- Create: `docs/tests/records/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-verification.md` + +- [ ] **Step 1: Write implementation record** + +在 `docs/reports/implementation/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-implementation.md` 中记录: + +- 需求主体是 `lsfx-mock-server` 后端基线同步 +- 前端不直接消费 Mock 新增的内部写库行为 +- 因此本轮不修改 `ruoyi-ui` 源码 + +- [ ] **Step 2: Write verification record** + +在 `docs/tests/records/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-verification.md` 中记录: + +- 执行过的 `rg` / `git diff` 命令 +- 核验目录范围 +- “无需前端改动”的判断依据 + +- [ ] **Step 3: Verify frontend diff scope** + +Run: + +```bash +git diff --name-only -- ruoyi-ui +``` + +Expected: + +- 无与本次需求相关的新前端改动 +- 若存在既有无关改动,只记录“本计划未新增前端源码变更”,不顺手处理他人改动 + +- [ ] **Step 4: Confirm no frontend build is required** + +在验证记录中明确写明: + +- 因为 `ruoyi-ui` 无本次需求相关源码改动,本次不执行 `npm run build:prod` +- 若后续出现真实前端接入需求,再单独补构建与联调验证 + +- [ ] **Step 5: Commit** + +```bash +git add \ + docs/reports/implementation/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-implementation.md \ + docs/tests/records/2026-03-31-lsfx-mock-server-abnormal-account-baseline-sync-frontend-verification.md +git commit -m "补充异常账户基线同步前端核验记录" +```