Files
group_xinghuo_jinrong/docs/memory/2026-09-10.md
T
GaoYiYuan_0626 7ad1c8204d 基金转换 T-8:规则引擎改造(_amount_view 去重视图 + process_convert_event)
一次转换落两条流水(转出 redeem + 转入 subscribe,同 convert_group_id),
金额聚合类规则若两条都算会翻倍报假预警,故按「逐笔 / 聚合」拆成两个视图。

- rules.py:新增 _amount_view —— 同组内只保留 redeem 那条(无 convert_group_id
  的交易恒等通过、组内无 redeem 保首条、绝不删行);run_rules 双视图分流:
  RISK-001/003/004 用全量 eligible(逐笔判定),RISK-002/005 走金额视图去重。
- alert_service.py:抽出公开 build_trade_event(事件体结构唯一定义),
  record_trade_alerts 新增可选 events 参数;缺省 None 退化为单条,
  既有调用零改动。新建单落 payload.events 全部,聚合追加只追首条(只认转出端)。
- engine.py:抽 _run 共用实现;process_trade_event 变薄封装(签名与行为不变);
  新增 process_convert_event(out_trade, in_trade, ...) 与 _notify_error_hook
  (hook 自身异常吞掉,原始异常照常上抛)。

实质影响:阶段 1.5 从「ImportError 静默跳过」变为「真跑」——T-7 部署时
process_convert_event 不存在,T-8 落地后同一笔转换会真实出单 + 写 L3 + 落审计;
T-7 真库脚本复跑仍 35/35,无连带破坏。

验证:新增 tests/test_convert_engine.py(15 用例)+ test_convert_service.py
接线回归 1 条;pytest 672 passed / 3 skipped(基线 656 +16,零回归);
新增 scripts/dev/verify_convert_engine.py 真 MySQL 验证 31/31;
突变验证(关掉去重视图 → 4 条变红)确认用例非假绿。
2026-09-10 17:34:35 +08:00

19 KiB
Raw Blame History

2026-09-10 工作日志

本日两条线:架构改进线收尾闭环(本文主体)+ 基金转换线第 4 步开发计划待启动。


一、接手与状态核对(会话起始)

读入:docs/交接文档-基金转换.md(v1.0)· docs/memory/MEMORY.md §0 · docs/交接文档-架构改进.md(v1.1) · docs/项目框架设计/架构设计-基金转换交易.md §15/§15.1 · docs/memory/{TODO,ITERATION}.md · AGENTS.md。

基线复核:python -m pytest -q(系统 Python 3.13.14)→ 510 passed(9.93s)。

设计资产核对:tests/_ddl.py:50-54 的 core_holding 仍为 market_value/quantity 且无 PK —— 与 T-0 描述一致,架构文档无误。

⚠️ 核对中发现的两处「文档与实况不符」(以实况为准)

  1. 037ce7e「未 push」是过期表述。 实测 git ls-remote origin:refs/heads/risk-control-agent = fffb78a = 本地 HEAD; git merge-base --is-ancestor 037ce7e fffb78a → 成立。即架构改进线全部提交(含其后 5 个文档提交)早已推送。 本地 git branch -vv 显示 origin/risk-control-agent: gone 只是本地远程跟踪引用失效,非远程分支被删。
  2. 基金转换线 6 个设计文件全部 untracked(未纳入版本库):docs/PRD/PRD-基金转换交易.md、 docs/项目框架设计/架构设计-基金转换交易.md、基金转换-审查意见处置表.md、 评审待办-风控主架构与基金转换.md、docs/交接文档-基金转换.md、scripts/dev/calc_convert_demo.py。 → 用户拍板:先 commit 入库(只 commit,不 push)。

二、架构改进线 §7.2 手工冒烟(七项,全部代跑,2026-09-10)

用户授权「七项全部代跑」。所有临时改动跑完即恢复。

第 1 项 · 无 key 启动告警 ✅ PASS

  • 本机 .env 的 DEEPSEEK_API_KEY 本就为空(实测 raw_value_len = 0),故「清空」态即当前态。
  • uvicorn app.main:app(端口 8123)→ 1 秒就绪,/health → 200 {"status":"ok","env":"development"}。
  • 进程存活 → 告警不阻塞启动 ✓。
  • 日志命中:DEEPSEEK_API_KEY 未配置,对话将走降级回复(前缀 (LLM 未配置:请在 .env 设置 DEEPSEEK_API_KEY 后重启)),LLM 能力不可用(app/main.py:66-72)。

第 2 项 · 恢复 key → 无告警 ✅ PASS(口径替代,已留痕)

  • 替代说明:本机无真实 DeepSeek key,「恢复 key」一支改用非空占位值(sk-smoke-placeholder-not-a-real-key)。 告警分支只判 not settings.deepseek_api_key,不校验 key 有效性,故该替代足以验证条件分支。
  • 改后 key_present: True → 启动(端口 8124)→ /health 200 → 日志未出现该告警 ✓。
  • 顺带验证 trace 中间件:响应头 x-trace-id: trc-48349abc526e4055 / x-request-id: req-612a3f2acb8349d6 ✓。
  • .env 已还原,key_present: False(原状)✓。

第 3 项 · 交易阻断 + 放行各一次 ✅ PASS

场景 请求(debug 头 X-Debug-Role: risk_demo / X-Debug-Actor: STAFF-DEMO) 响应
阻断(SOP A-2) CUST-4001 / PROD-161725 / subscribe / 20000 blocked=true,block_response_code=SUIT_AGE_CONFIRM,needs_branch_confirm=true,rule_refs=["FM-01"]
放行(SOP A-3) CUST-3001 / PROD-510300 / subscribe / 500000 blocked=false,triggered_rules=["RISK-001","RISK-002"],alert_ids=["ALT-20260910-7BCBBC94"]

与 SOP 记录的预期逐字一致。 库内核验:

  • core_trade:阻断单 TRD-20260910-C1D8AA26 未落库 ✓;放行单 TRD-20260910-FE545DF0 已落库(confirmed, 500000.00)✓
  • risk_suitability_log:CUST-4001 is_blocked=1 ✓;risk_alert 出 suitability 单 ALT-20260910-652280E1(pending_review, score 90)✓
  • audit_log:suitability_block / suitability_alert_created、trade_request / suitability_blocked(rule_id=SUIT_AGE_CONFIRM)、 trade_request / trade_accepted(放行)、risk_judgement / alert_created ✓

第 4 项 · 预警聚合行为不变 ✅ PASS

SOP A-4:CUST-9527 / PROD-510300 / subscribe / 1000 连发 4 笔:

笔次 triggered_rules alert_ids
1 [] []
2 [] []
3 ["RISK-003"] ["ALT-20260910-C8600425"]
4 ["RISK-003"] ["ALT-20260910-C8600425"] ← 与第 3 笔同一张单(并入,非新建)

库内核验:

  • risk_alert CUST-9527 当日 pending_review 单数 = 1 ✓;payload.events 长度 = 2 ✓
  • audit_log 呈 alert_created(1184) → alert_appended(1187)` 序列,聚合语义在审计层可证 ✓
  • 4 笔交易全部落库(TRD-20260910-B66CB0D8 / B39E1456 / 83D58A41 / 79EDC16E)✓

注:risk_alert.status 实际取值为 pending_review(文档口头简称「pending」)。首次核库我按 status='pending' 查得 0 条, 系断言写错而非系统问题,修正后为 1 ✓。

第 5 项 · Redis 不可用 → 退回进程内锁 ✅ PASS(口径替代,已留痕)

  • 替代说明:本机 Redis 为 redis-server.exe(PID 7716,Services 会话),无文档化启动方式、redis-server 不在 PATH、 sc.exe 被本机安全策略拉黑,杀掉后无法保证原样拉起(且 FLOW.md 明示其非权威数据源)。 改用 REDIS_URL=redis://127.0.0.1:6399/0(死端口):应用侧抛 ConnectionError,与真宕机同一条代码路径 (redis_gateway._ensure() → 命令异常 → locks._acquire_redis 的 except Exception → unavailable → 进程内锁),且完全可逆。
  • 结果:启动不被阻塞(1s,/health 200)→ 交易返回 blocked=false,alert_ids=["ALT-20260910-7BCBBC94"],业务照常完成 ✓
  • 日志明确出现 2 次 Redis 锁不可用,退回进程内锁: lock:agg:event:CUST-3001:2026-09-10(alert_service 聚合锁)、lock:l3:CUST-3001(profile_l3 L3 锁)✓
  • 底层异常:redis.exceptions.ConnectionError: Error 10061 connecting to 127.0.0.1:6399,被正确吞噬未上抛 ✓

第 6 项 · Redis 恢复 → 行为一致 ✅ PASS

  • redis.Redis.from_url(settings.redis_url).ping() → True(redis://127.0.0.1:6379/0)。
  • 同笔交易 → blocked=false,alert_ids=["ALT-20260910-7BCBBC94"](并入同单)✓
  • 日志中 退回进程内锁 命中 0 行 → 确实走 Redis 锁路径,未降级 ✓
  • triggered_rules 由 ["RISK-001","RISK-002"] 变为 +RISK-003:CUST-3001 当日累计至第 3 笔触发频次规则, 属预期累积行为,非不一致。

第 7 项 · 调换装饰器验证 T-202 守卫变红 ✅ PASS(本项价值最高)

守用例:tests/test_audit_middleware.py::test_audit_middleware_runs_inside_trace_middleware:210。

步骤 结果
① 基线 1 passed(绿)
② 调换 main.py 两装饰器(trace 先注册 → 变最内层;audit 后注册 → 变最外层) 已调换,行序:trace_middleware@89 / audit_middleware_entry@117
③ 跑守卫 1 failed ✓ 变红
④ 失败断言原文 AssertionError: audit 若先于 trace 执行,此处会静默为空 / assert '' @ test_audit_middleware.py:221
⑤ 还原 sha256 前后同为 d9d7559445dffd2a4576cdc5454030d3acae5fd4c076de8daa5a35f53802510a → 字节级一致 ✓
⑥ 再跑守卫 1 passed(转绿)✓
⑦ 现场核对 git status --short app/main.py 空、无残留 *.smoke.bak ✓

→ 守卫确实能抓住「audit 先于 trace 执行 → 全站审计静默丢 trace_id 且不报错」这一回归。设计意图达成。


三、冒烟副产物与现场恢复

3.1 集成测试失败(防护行为,非回归)

冒烟后在真 MySQL 上跑全量:507 passed / 3 failed,全部落在 tests/test_integration_risk.py:

  • _assert_a3_clean → CUST-3001 今日已有 confirmed 交易(影响 RISK-002 累计/RISK-003 频次口径)(我的 TRD-20260910-FE545DF0)
  • _assert_a4_clean → CUST-9527 今日已有 confirmed 交易(A-4 频次计数失真)(我的 TRD-20260910-B66CB0D8)
  • test_a7_handle_state_machine_compliance_forbidden → 级联 KeyError: 'a3_alert_id'

→ 这正是核查单⑥「演示/测试同日交叉防护」前置断言在按设计工作(交接文档已预警)。

3.2 演示库重灌(用户拍板:现在重灌)

按 docs/项目框架设计/演示SOP-风控模块.md §2 脚本化方式执行(MYSQL_PWD 传密码,避开 reset.ps1 交互 -p 卡死):

  1. DROP DATABASE IF EXISTS jinrong_core; DROP DATABASE IF EXISTS jinrong_agent;
  2. 依序灌 11 个 SQL:scripts/core/00~06 → 表设计/01-mysql-共用底座.sql → 表设计/02-mysql-agent专用.sql → scripts/agent/seed-aml-list.sql → scripts/demo/prepare_risk_demo.sql
  3. python scripts/sync/sync_advisor_rel.py

完成标志三项全部吻合:AML 名单 8 条 · prepare_risk_demo 7 行测评(expires_at 剩余 275 天)· sync_advisor_rel upserted 33 rows。

3.3 全量复跑(用户要求「改完后再跑一次」)

510 passed, 656 warnings in 9.52s → 复绿 ✓

现场状态:.env 已还原(key 为空、REDIS_URL 为 6379)✓ · app/main.py 字节级还原且 git 无改动 ✓ · 无残留 .smoke.bak ✓。


四、回写清单

文档 改动
docs/项目框架设计/TODO-架构改进.md T-107 手工冒烟勾选 + 完成判定块三勾 + 状态块改「全部闭环」+ push 状态纠正
docs/交接文档-架构改进.md v1.1 → v1.2;状态块重写(push 纠正 + 两处替代口径留痕);§7.3 全勾;§10 重写为结项
docs/交接文档-基金转换.md §9「另一条线的遗留」改为已闭环,纠正 push 表述
docs/memory/MEMORY.md §0 末尾「待办两条线」改为「架构改进线已结项」+ push 纠正常见坑
docs/memory/TODO.md 模块侧开放项「架构改进线 · 收尾」勾选并附七项证据摘要
docs/memory/ITERATION.md 新增 2026-09-10 架构改进线收尾闭环 迭代行
docs/memory/2026-09-10.md 本文件(新建)

五、下一步

架构改进线:结项,无待办。(可选:push 后打里程碑 tag,待用户拍板)

基金转换线:第 4 步「产出开发计划」 —— 输入已齐备: docs/项目框架设计/架构设计-基金转换交易.md §15(任务映射 T-0~T-13)+ §15.1(依赖拓扑与并行分组)+ §12(风险表); 产出落点 docs/项目框架设计/开发计划-基金转换交易.md(待创建)。


六、交接文档合并为单一入口 + 全仓指向统一(2026-09-10 晚)

6.1 用户指令与判定

  • 指令:「由版本库的交接文档为主,这两个交接文档你合并成一个新的放到这次更新」+ 指向项目根 交接文档.md。
  • 追加纠正:「统一一下,不是早都说过交接文档是给你 AI 接手用的吗」 —— 交接文档的定位 = AI 接手入口,所以全仓指向必须唯一,且绝不能指向过期文件,否则新会话会被误导。 (docs/ 下两份留档停在 v1.0 / v1.1,不含 T-2/T-2b、609 passed、D20 实施等进度 —— 是个真实陷阱。)

6.2 落地

项目根 交接文档.md → v3.0(545 行),三线合并为单一入口: §0 公共层(项目是什么 / 铁律与禁止事项 / 开发运行手册 / 已知坑 10 条 / 发现问题怎么办)· §A 风控模块主线(原主干)· §B 基金转换线(含工作区最新版 v1.4)· §C 架构改进线(含工作区最新版 v1.2)。 最大价值:这两份工作区最新版从未提交,随时可能丢,现已完整固化。

全仓 10 处指向统一 → 交接文档.md §A/§B/§C: AGENTS.md(含顶部新增「⚡ 需要接手/开工先读交接文档.md」)· docs/memory/{MEMORY,TODO,FRAMEWORK} · docs/PRD/PRD-架构改进与稳定性加固.md · docs/项目框架设计/{开发计划-架构改进,TODO-架构改进,开发计划-基金转换交易}.md。

两份 docs/交接文档-*.md 保留为历史留档,顶部加醒目横幅:「本文件已废弃(2026-09-10)—— 请勿据此开工, 正确入口 交接文档.md §B/§C,内容已过期,勿读」 —— 不删除(保留仓库历史),但明确失效。

未入库:交接文档.md 在 .gitignore:47 内,用户确认「不进提交」。

6.3 ⚠️ 事故:git rm 静默抹除整个 docs/(复现 2 次,已零损失恢复)

  • 现象:git rm(带不带 -f 均如此)删除 docs/ 下两个中文名文件 → docs/ 全部 54 个文件从工作区消失, 命令返回退出码 0 不报错。
  • 定位:① ls/find/Read 三方确认真实删除;② git checkout HEAD -- docs/ 完整还原 54 文件(索引与 HEAD blob 完好); ③ 无自定义 hooks(core.hooksPath 空);④ 同一中文路径 ls 完全正常 → 触发条件就是 git rm 本身; ⑤ 换 纯 rm -f <精确路径> → 正常。
  • 损失:零内容丢失(HEAD blob 完好)。代价:本轮引用修改被连带还原 2 次(重做 3 次)。
  • 纪律(已写入 交接文档.md §0.4 第 10 条 + .workbuddy/memory/MEMORY.md):
    • ❌ 本仓库禁止对 docs/ 下文件用 git rm;
    • ✅ 删文件用纯 rm -f <精确路径> + git add -A 记录删除;
    • ✅ 批量删除后立刻 find <dir> -type f | wc -l 复核,异常立即 git checkout HEAD -- <dir>。
    • 根因未查明,规避即可。

七、设计方法论固化为 skill + 记忆精简(2026-09-10 晚)

动机:项目记忆 MEMORY.md 与交接文档膨胀——13 问 / AIcoding 六步 / 独立审查协议 / 写计划 4 法 等项目无关的方法论每个项目都重写一遍,且挤占了项目状态信息。

落地:

  1. 新建用户级 skill design-doc-selfcheck(~/.workbuddy/skills/design-doc-selfcheck/SKILL.md)—— 固化:AIcoding 六步流程 · 设计自检 13 问(含 why/翻车案例)· PRD 独立 AI 审查协议(自包含包 + 只报告不改动 + 逐条判定)· 写开发计划 4 法 · 真实翻车案例库(4 处外部事实漏网 / 分类混用 / 公式副本 / 重置伴随数据 / 方言互斥)。
  2. 精简项目 MEMORY.md(205 行 → ~70 行):删除已入 skill 的方法论 verbatim,只留三条线状态、git rm 禁忌、交接文档定位、基金转换核心事实与业务口径、交易发起主体合规、D20 账号,并指向 skill。
  3. 交接文档 §B 顶部加 skill 指针:本线设计方法论已固化,避免重复维护。

效果:方法论跨项目可复用(新项目直接加载 skill);本项目记忆回到「状态 + 红线 + 口径」本职。


深夜 · 基金转换线 T-8(规则引擎改造)完成

范围:开发计划 §7.1 —— rules._amount_view + engine.process_convert_event + alert_service.events(并行组 B)。

改码三处(均在代码注释留痕)

  • app/service/risk/rules.py:+_amount_view(trades)(同 convert_group_id 组内只留 redeem;无 gid 恒等通过;组内无 redeem 保首条);run_rules 双视图分流 —— eligible(全量)供 RISK-001/003/004,_amount_view(eligible) 供 RISK-002/005。
  • app/service/risk/alert_service.py:+公开 build_trade_event(trade, hits)(结构定义唯一副本);record_trade_alerts(..., events=None) 缺省退化为单条,既有调用零改动;新建单落 payload.events 全部,聚合追加只追首条(与架构 §5.4「只认转出端」同口径)。
  • app/service/risk/engine.py:抽 _run() 共用实现;process_trade_event 变薄封装(签名与行为不变);新增 process_convert_event(out_trade, in_trade, *, core_ro, risk_repo, thresholds, on_error_hook=None) + _notify_error_hook(hook 自身异常吞掉、原始异常照常上抛)。

⭐ 实质影响:阶段 1.5 从「静默跳过」变「真跑」

T-7 落地时 process_convert_event 不存在 → _run_engine 走 ImportError 分支跳过; T-8 落地后同一笔转换会真实出单 + 写 customer_profile_l3 + 落审计。 → verify_convert_service.py 的执行语义随之变化,复跑仍 35/35 绿(无连带破坏)。

验证

  • 新增 tests/test_convert_engine.py 15 用例;test_convert_service.py +1 条接线回归(大额转换真出单,防 _run_engine 退回跳过)。
  • pytest -q → 672 passed / 3 skipped(基线 656 +16,零回归)。
  • 突变验证(防假绿):临时把 amount_view = eligible → 4 条变红,其中 detail 直接暴露 当日申赎累计 599000 元(共 2 笔) 的翻倍错误;恢复后复绿。
  • 新增 scripts/dev/verify_convert_engine.py 真库 31/31(A 引擎真跑出单 / B RISK-002 不翻倍夹逼:单条 512000 < 800000 < 两条之和 1019975.33 / C 不删行 + DECIMAL 零漂移 / D 幂等重试不重复出单 / E 无命中只落 pass 审计 / F 六表零残留)。

真库脚本两条踩坑(与 T-7 同款 + 新一条,供后续 verify_*.py 参考)

  1. id_factory 必须注入:默认生成 CNV-<日期>-<uuid>,与清理口径 LIKE 'CNV-T8M%' 不匹配 → 重跑遗留行撞 uk_group。注入 _t8m_id 并把清理条件补 client_request_id LIKE 'T8M-%' 兜底。
  2. core_trade.trade_type 在 MySQL 是 ENUM,ORDER BY 按定义序(实测 [subscribe, redeem])→ 断言改用 {trade_type: row} 字典定位,不依赖排序。

发现的既有行为(不在本任务范围,未顺手改)

更正(同日):初次记录误写成「RISK-001 实为当日累计口径」,错误。RISK-001 与 RISK-002 是两条独立规则——前者单笔(rules.py:128-133)、后者当日累计(:137-145)。T-8 只切后者。

真实根因在引擎入参范围:process_trade_event 拉当日全量流水重跑规则 (engine.py 的 list_trades_range(customer, day_start, day_end))→ 触发这笔与命中那笔 可以不是同一笔:真库 E 组实测,同客户同日先有一笔大额转换时,后续一笔 1000 元小额交易也带出 RISK-001。 既有聚合逻辑(同客户同日一张 pending 单)兜住了重复出单,属既有设计,T-8 不改变也不修。

文档回写

开发计划(§0 速览标 ✅ + 测试基线改 672 · §7.1 DoD 全勾 + 新增执行记录 + 3 条实施级收敛 + 2 条脚本坑)· 根 交接文档.md §B → v1.9(§0 导航 · §B 头部 · §B.1 状态与代码改动 · 新增 §B.6.1 T-8 小节 · §B.6 任务树与基线)· docs/memory/{MEMORY,TODO} · 本条。

下一步 = T-9(api/simulate.py 模型与错误码 + trade_gateway convert 分派 · 关键路径)/ T-11(core_tools 与 sum_trades_on_date 汇总去重 · 依赖 T-8 已解锁)。