一次转换落两条流水(转出 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 条变红)确认用例非假绿。
19 KiB
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 描述一致,架构文档无误。
⚠️ 核对中发现的两处「文档与实况不符」(以实况为准)
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只是本地远程跟踪引用失效,非远程分支被删。- 基金转换线 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)→/health200 → 日志未出现该告警 ✓。 - 顺带验证 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-4001is_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_alertCUST-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,
/health200)→ 交易返回blocked=false,alert_ids=["ALT-20260910-7BCBBC94"],业务照常完成 ✓ - 日志明确出现 2 次
Redis 锁不可用,退回进程内锁:lock:agg:event:CUST-3001:2026-09-10(alert_service聚合锁)、lock:l3:CUST-3001(profile_l3L3 锁)✓ - 底层异常:
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 卡死):
DROP DATABASE IF EXISTS jinrong_core; DROP DATABASE IF EXISTS jinrong_agent;- 依序灌 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 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 法 等项目无关的方法论每个项目都重写一遍,且挤占了项目状态信息。
落地:
- 新建用户级 skill
design-doc-selfcheck(~/.workbuddy/skills/design-doc-selfcheck/SKILL.md)—— 固化:AIcoding 六步流程 · 设计自检 13 问(含 why/翻车案例)· PRD 独立 AI 审查协议(自包含包 + 只报告不改动 + 逐条判定)· 写开发计划 4 法 · 真实翻车案例库(4 处外部事实漏网 / 分类混用 / 公式副本 / 重置伴随数据 / 方言互斥)。 - 精简项目
MEMORY.md(205 行 → ~70 行):删除已入 skill 的方法论 verbatim,只留三条线状态、git rm禁忌、交接文档定位、基金转换核心事实与业务口径、交易发起主体合规、D20 账号,并指向 skill。 - 交接文档 §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.py15 用例;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 参考)
id_factory必须注入:默认生成CNV-<日期>-<uuid>,与清理口径LIKE 'CNV-T8M%'不匹配 → 重跑遗留行撞uk_group。注入_t8m_id并把清理条件补client_request_id LIKE 'T8M-%'兜底。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 已解锁)。