# Phase 2: 权限/财务/实名/轮询修复 - Context

**Gathered:** 2026-03-28
**Status:** Ready for planning

<domain>
## Phase Boundary

修复和补全四个独立业务域的已知断点：企业账号权限边界（PERM）、提现财务流程完整性（FIN）、实名激活架构重构（REALNAME/G）、轮询调度小修（POLL）。共 14 个需求，全部为修复性变更，无新功能模块，REALNAME 涉及破坏性 DB 迁移（DROP 旧字段 + ADD 新字段）。

</domain>

<decisions>
## 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 的粒度规范）

</decisions>

<canonical_refs>
## 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 变更需同步更新文档

</canonical_refs>

<code_context>
## 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 层

</code_context>

<specifics>
## Specific Ideas

- Plan 2（REALNAME）原子执行——规格书明确指出"REALNAME-01~05 是原子重构，必须在一个 plan 内完整执行，不可拆分"（STATE.md Accumulated Context）
- 旧字段 `enable_realname_activation` 直接清理，不做 false→from_purchase 映射，现有生产套餐全部默认 from_activation，后续运营按需手动调整

</specifics>

<deferred>
## Deferred Ideas

None — discussion stayed within phase scope

</deferred>

---

*Phase: 02-perm-fin-realname-poll*
*Context gathered: 2026-03-28*
