From 84aca2fd63043e4a2e454abad2197a14653ce04c Mon Sep 17 00:00:00 2001 From: huang Date: Sat, 28 Mar 2026 10:11:04 +0800 Subject: [PATCH] docs(02): capture phase context --- .../02-perm-fin-realname-poll/02-CONTEXT.md | 143 ++++++++++++++++++ .../02-DISCUSSION-LOG.md | 58 +++++++ 2 files changed, 201 insertions(+) create mode 100644 .planning/phases/02-perm-fin-realname-poll/02-CONTEXT.md create mode 100644 .planning/phases/02-perm-fin-realname-poll/02-DISCUSSION-LOG.md diff --git a/.planning/phases/02-perm-fin-realname-poll/02-CONTEXT.md b/.planning/phases/02-perm-fin-realname-poll/02-CONTEXT.md new file mode 100644 index 0000000..ac04a34 --- /dev/null +++ b/.planning/phases/02-perm-fin-realname-poll/02-CONTEXT.md @@ -0,0 +1,143 @@ +# Phase 2: 权限/财务/实名/轮询修复 - Context + +**Gathered:** 2026-03-28 +**Status:** Ready for planning + + +## Phase Boundary + +修复和补全四个独立业务域的已知断点:企业账号权限边界(PERM)、提现财务流程完整性(FIN)、实名激活架构重构(REALNAME/G)、轮询调度小修(POLL)。共 14 个需求,全部为修复性变更,无新功能模块,REALNAME 涉及破坏性 DB 迁移(DROP 旧字段 + ADD 新字段)。 + + + + +## Implementation Decisions + +### 执行分组策略 + +- **D-01:** 拆分为 **2 个 Plan**,而非 1 个或 4 个: + - **Plan 1(小修合并)**:B(PERM-01/02)+ H(POLL-01/02/03/04)+ C(FIN-01/02/03) + - **Plan 2(REALNAME 独立)**:G(REALNAME-01~05)—— 单独 Plan,独立可回滚 +- **D-02:** Plan 1 内部执行顺序:B → H → C(权限修复最小化先上,轮询小修次之,财务流程收尾) +- **D-03:** Plan 2(REALNAME)必须原子执行,不可拆分 —— 5 个需求在一个 Plan 的同一事务性推进中完成 + +### 企业权限(PERM) + +- **D-04:** `asset.go:Resolve` —— **移除**企业账号拦截(企业应可查询被授权资产) +- **D-05:** `asset.go:Refresh` —— **新增**企业账号拦截(企业只读,不可触发运营商刷新) +- **D-06:** 其他 8 个 AssetHandler 方法维持现状(无拦截/已拦截均不动) + +### 财务流程(FIN) + +- **D-07:** FIN-01:`commission_withdrawal/service.go:Approve()` 补填 `approved_by` / `approved_at` 字段 +- **D-08:** FIN-02:`commission_withdrawal.go:Reject()` 在 BodyParser 后补 `validator.Validate(&req)` +- **D-09:** FIN-03:激活配置并发保护使用 GORM v2 语法 `tx.Clauses(clause.Locking{Strength: "UPDATE"})` —— 规格书写的是 GORM v1 的 `gorm:query_option`,项目实际用 GORM v2,agent 使用正确语法 + +### REALNAME 架构重构(G) + +- **D-10:** DB 迁移策略:**直接 ADD DEFAULT 'from_activation',旧字段 DROP** + - 不做 `enable_realname_activation=false → from_purchase` 的数据迁移 + - 原因:旧字段控制的是"是否等实名",新字段 `expiry_base` 控制的是"计时起点",两者语义不同,无法直接映射;旧字段已无存在意义,直接清理 + - 现有生产套餐记录全部默认 `from_activation`,后续如需 `from_purchase` 由运营手动修改 +- **D-11:** `activateMainPackage` 激活决策逻辑改为按三维判断: + - 行业卡(`card_category == "industry"`)→ 永远直接激活 + - C 端购买(`buyerType == "personal"` 或 `order.OrderType` 标记 C 端)→ 直接激活(已前置实名检查) + - 后台囤货 → 按 `pkg.ExpiryBase`:`from_purchase` 直接激活,`from_activation` 进入等实名队列 +- **D-12:** C 端实名检查改为按卡类型(`card_category == "normal"`)判断,删除 `packagesNeedRealname()` 函数 +- **D-13:** `ActivateByRealname` 按 `ExpiryBase` 选择计时基准:`from_purchase` 用 `CreatedAt`,`from_activation` 用当前时刻 + +### 轮询系统小修(POLL) + +- **D-14:** POLL-01:在 `scheduler.go` 主调度循环中,复用已有的 `processManualQueue` / `processTimedQueue`,补充 `polling:protect` 的调度接入 +- **D-15:** POLL-02:`stopCardWithRetry` / `resumeCardWithRetry` 两处,`gatewayClient == nil` 时改为返回明确业务错误 +- **D-16:** POLL-03:`GetPackages()` 接口新增分页参数,默认 `page=1, pageSize=50`,最大 100 +- **D-17:** POLL-04:`package_series/service.go:Delete()` 前置 `CountBySeriesID()` 检查,有关联套餐则拒绝删除 + +### Agent's Discretion + +- FIN-03 事务内 `FOR UPDATE` 的具体 GORM v2 `clause.Locking` 写法,agent 参考项目现有 `clause.OnConflict` 用法对齐风格 +- `isCEndPurchase` 的具体推断字段(`buyerType == "personal"` 或 order 字段),agent 阅读 `activateMainPackage` 上下文确认最准确的判断路径 +- 每个子修复独立一个 commit(沿用 Phase 1 的粒度规范) + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### 修复规格(核心参考) +- `.sisyphus/plans/修正业务-完整方案.md` — 完整修复规格书,含每个需求的文件路径、代码片段、人工验收清单: + - 方案 B(PERM-01/02):第 766-863 行 + - 方案 C(FIN-01/02/03):第 864-944 行 + - 方案 G(REALNAME-01~05):第 1492-1611 行 + - 方案 H(POLL-01~04):第 1612-1711 行 + +### 核心修改文件 +- `internal/handler/admin/asset.go` — PERM-01(删 Resolve 企业拦截)、PERM-02(加 Refresh 企业拦截) +- `internal/service/commission_withdrawal/service.go` — FIN-01(Approve 补审批人字段) +- `internal/handler/admin/commission_withdrawal.go` — FIN-02(Reject 加 Validate) +- `internal/service/commission_withdrawal_setting/service.go` — FIN-03(并发行锁) +- `internal/store/postgres/wechat_config_store.go` — FIN-03(微信配置并发行锁) +- `internal/model/package.go` — REALNAME-01(drop EnableRealnameActivation,add ExpiryBase) +- `internal/model/dto/package_dto.go` — REALNAME-05(DTO 更新 expiry_base 字段) +- `internal/service/order/service.go` — REALNAME-02(activateMainPackage 三维决策) +- `internal/service/client_order/service.go` — REALNAME-03(按卡类型检查实名) +- `internal/service/package/activation_service.go` — REALNAME-04(ActivateByRealname 计时基准) +- `internal/store/postgres/package_store.go` — REALNAME-01(删 enable_realname_activation 两步写入特殊处理) +- `internal/polling/scheduler.go` — POLL-01(补 protect 调度) +- `internal/task/polling_handler.go` — POLL-02(nil gateway 改返错误) +- `internal/service/asset/service.go` 或相关 package 接口 — POLL-03(GetPackages 分页) +- `internal/service/package_series/service.go` — POLL-04(Delete 前检查关联套餐) + +### DB 迁移 +- `migrations/` 目录 — 遵循现有迁移文件命名规范,REALNAME 迁移需新建一个 .sql 文件 + +### 项目规范 +- `AGENTS.md` — 语言要求、错误处理、分层规范 +- `docs/api-documentation-guide.md` — 若有 DTO 变更需同步更新文档 + + + + +## Existing Code Insights + +### 关键现状确认 +- `asset.go:Resolve`(第 41-44 行):企业拦截代码已确认存在,直接删除即可 +- `asset.go:Refresh`(第 78 行附近):当前无企业检查,需在方法开头新增 +- GORM v2 locking 语法:项目已有 `clause.OnConflict` 使用(`package_usage_daily_record_store.go`),FIN-03 应参照同样风格使用 `clause.Locking` +- `enable_realname_activation`:在 `package.go`、`package_dto.go`(3处)、`package_store.go`(2处两步写入特殊处理)均有引用,全部需清理 +- `card_category`:在 `polling_handler.go:108` 已有 `if card.CardCategory == "industry"` 判断,REALNAME-02 决策逻辑与此对齐 +- `processManualQueue` / `processTimedQueue`:`scheduler.go:243-250` 已有 3 个 polling 类型,补 protect 参照同一模式 + +### Established Patterns +- 行锁:`tx.Clauses(clause.Locking{...})` — 参考 `clause.OnConflict` 已有用法 +- 错误返回:Service 层用 `errors.New(code, msg)` / `errors.Wrap(code, err, msg)` +- 分页:`QueryOptions{Page: page, PageSize: pageSize}` — 参考已有 List 接口 +- Commit 粒度:每个独立 Bug 修复一个 commit(沿用 Phase 1 规范) + +### Integration Points +- REALNAME:`package_store.go` 的两步写入特殊处理在 REALNAME-01 后需删除(字段删除后特殊处理失去意义) +- POLL-03 分页:若 `GetPackages` 上层调用点(`handler/admin/asset.go:102`、`handler/app/client_asset.go:213`)无分页参数传入,需同时更新 Handler 层 + + + + +## Specific Ideas + +- Plan 2(REALNAME)原子执行——规格书明确指出"REALNAME-01~05 是原子重构,必须在一个 plan 内完整执行,不可拆分"(STATE.md Accumulated Context) +- 旧字段 `enable_realname_activation` 直接清理,不做 false→from_purchase 映射,现有生产套餐全部默认 from_activation,后续运营按需手动调整 + + + + +## Deferred Ideas + +None — discussion stayed within phase scope + + + +--- + +*Phase: 02-perm-fin-realname-poll* +*Context gathered: 2026-03-28* diff --git a/.planning/phases/02-perm-fin-realname-poll/02-DISCUSSION-LOG.md b/.planning/phases/02-perm-fin-realname-poll/02-DISCUSSION-LOG.md new file mode 100644 index 0000000..92316df --- /dev/null +++ b/.planning/phases/02-perm-fin-realname-poll/02-DISCUSSION-LOG.md @@ -0,0 +1,58 @@ +# Phase 2: 权限/财务/实名/轮询修复 - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-03-28 +**Phase:** 02-perm-fin-realname-poll +**Areas discussed:** 执行分组策略, REALNAME 数据迁移风险 + +--- + +## 执行分组策略 + +| Option | Description | Selected | +|--------|-------------|----------| +| B+C+H 合一、G 独立 | 2 个 Plan:小修删掉合成一个,REALNAME 单独一个。风险隔离好,G 可以独立回滚 | ✓ | +| B、C、G、H 各独立 | 4 个 Plan:最细粒度,但总费用较高(尤其 B 和 H 都很小) | | +| 全部合并成一个 | 1 个 Plan:最快完成,但如果 G 失败就不知道是哪个需求的问题 | | + +**User's choice:** B+C+H 合一、G 独立 +**Notes:** Plan 1 内部执行顺序确认为 B → H → C + +--- + +## 执行顺序(Plan 1 内部) + +| Option | Description | Selected | +|--------|-------------|----------| +| B → H → C | 先权限修复(最小化验证),然后轮询小修,最后收尾财务流程。财务类改动放最后更安全 | ✓ | +| H → B → C | 先轮询,再权限,最后财务 | | +| Agent 自行决定 | 顺序不重要,由 agent 按依赖关系安排 | | + +**User's choice:** B → H → C + +--- + +## REALNAME 数据迁移风险 + +| Option | Description | Selected | +|--------|-------------|----------| +| 直接默认 from_activation | 新字段 DEFAULT 'from_activation'。旧的 false 记录不做映射。expiry_base 控制计时基准,语义与旧字段不同,无法直接映射 | ✓ | +| 脚本映射 false → from_purchase | UPDATE tb_package SET expiry_base='from_purchase' WHERE enable_realname_activation=false。但实际上旧字段的语义与新字段不完全对应 | | + +**User's choice:** 直接默认 from_activation,旧字段清理掉 +**Notes:** "如果旧的字段没有用了就清理掉吧" + +--- + +## Agent's Discretion + +- FIN-03 GORM v2 行锁语法(`clause.Locking`) +- `isCEndPurchase` 的具体推断字段 +- 每个子修复独立 commit 粒度 +- POLL-03 Handler 层分页参数传入方式 + +## Deferred Ideas + +None mentioned during discussion.