Files
junhong_cmp_fiber/openspec/changes/agent-fund-visibility/design.md
huang 0627ffec42
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m10s
feat: 代理商资金可见性重构(agent-fund-visibility)
- 将 GET /shops/commission-summary 重命名为 GET /shops/fund-summary,
  响应新增 main_balance、main_frozen_balance 两个预充值钱包字段
- 新增 GET /shops/:id/main-wallet/transactions 预充值钱包流水接口
- 将佣金统计、每日统计、发起提现从 /my/ 路径迁移至 /shops/:id/ 路径:
  GET /shops/:id/commission-stats
  GET /shops/:id/commission-daily-stats
  POST /shops/:id/withdrawal-requests
- 删除 MyCommissionService、MyCommissionHandler 及全部 /my/ 路由
- 补齐 ListShopWithdrawalRequests、ListShopCommissionRecords 的
  CanManageShop 越权校验(安全修复)
- 提现接口增加严格权限:仅代理账号本人可为自己店铺发起提现

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:40:39 +08:00

189 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
### 现状
系统中代理钱包有两种类型(`tb_agent_wallet`
- `wallet_type=main`:预充值钱包,代理购买套餐时扣款
- `wallet_type=commission`:佣金钱包,订单完成后自动入账、可提现
当前 `/shops/commission-summary` 仅聚合佣金钱包数据,主钱包完全不可见。代理自己的操作路径依赖 `/my/` 前缀系列接口,与平台视角的 `/shops/:id/` 系列存在职责重叠,产生维护成本。
### 已有可复用能力
| 组件 | 方法 | 状态 |
|------|------|------|
| `AgentWalletStore` | `GetMainWallet(shopID)` | 已有,单条查询 |
| `AgentWalletStore` | `GetShopCommissionSummaryBatch(shopIDs)` | 已有,批量查佣金钱包 |
| `AgentWalletTransactionStore` | `ListByShopID / CountByShopID` | 已有,直接复用 |
| `ShopCommissionService` | `ListShopCommissionSummary` | 已有,需扩展 |
| `ShopCommissionService` | `ListShopCommissionRecords` | 已有,不动 |
| `ShopCommissionService` | `ListShopWithdrawalRequests` | 已有,不动 |
| `MyCommissionService` | `GetStats / GetDailyStats / CreateWithdrawalRequest` | 业务逻辑迁移到 ShopCommissionService原 service 删除 |
---
## Goals / Non-Goals
**Goals:**
- 平台人员在一个列表(资金概况)中同时看到每个代理的预充值余额和佣金余额
- 代理账号通过相同接口看到自己的资金概况GORM 多租户过滤)
- 提供预充值钱包流水接口,支持平台和代理两个视角
-`/my/` 6 个接口的业务逻辑完整迁移至 `/shops/:id/`,删除冗余路径
**Non-Goals:**
- 不新增纯佣金钱包流水接口(`commission-records` 订单维度已满足需求)
- 不改动充值订单相关接口(`/agent-recharges` 系列不变)
- 不改动提现审批流程(`/commission/withdrawal-*` 不变)
- 不做向后兼容,直接删除 `/my/` 路由
---
## Decisions
### 决策 1资金概况使用批量查询主钱包余额
**问题**`/shops/fund-summary` 分页返回多条记录,每条需要主钱包余额。若逐条查询会产生 N+1。
**决策**:在 `AgentWalletStore` 新增 `GetShopMainWalletBatch(ctx, shopIDs) map[uint]*AgentWallet`,与现有 `GetShopCommissionSummaryBatch` 对称,一次 `WHERE shop_id IN (...)` 查完,在 Service 层合并。
**备选**:单次 SQL JOIN 查两个钱包类型 — 复杂度高,破坏 Store 层单一职责,放弃。
---
### 决策 2主钱包流水接口不做独立 Handler归入 ShopCommissionHandler
**问题**:主钱包流水是代理资金详情的一部分,是否需要独立 Handler。
**决策**:归入扩展后的 `ShopCommissionHandler`(或重命名为 `ShopFundHandler`),保持路由注册集中,避免 Handler 碎片化。
**注意**`AgentWalletTransactionStore.ListByShopID` 已有,只需 Service 层加一个 `ListMainWalletTransactions(ctx, shopID, req)` 方法。
---
### 决策 3`/my/` 路由的业务逻辑迁移方式
**问题**`MyCommissionService` 中的 `GetStats``GetDailyStats``CreateWithdrawalRequest` 当前从 ctx 自动读取 shopID隐式即"自己"),迁移后接口路径从 `:shop_id` 取,必须显式越权校验。
**决策**
- 将三个方法迁移到 `ShopCommissionService`,签名改为接受 `shopID uint` 参数
- **Service 层入口**强制权限校验(不是 Handler 层),对齐 CLAUDE.md 的 Code Review 规范(参考 `internal/service/account/service.go`
- Handler 只做参数解析,不写权限逻辑
- 删除 `MyCommissionService``MyCommissionHandler`
**两档校验策略**(根据业务严格度区分):
| 方法 | 校验策略 | 理由 |
|---|---|---|
| `GetStats` / `GetDailyStats` | `middleware.CanManageShop(ctx, shopID)` | 查询类,平台和顶级代理可看下级数据 |
| `ListMainWalletTransactions` | `middleware.CanManageShop(ctx, shopID)` | 查询类,同上 |
| `ListShopWithdrawalRequests` / `ListShopCommissionRecords`(已有方法补齐) | `middleware.CanManageShop(ctx, shopID)` | 查询类,同上 |
| `CreateWithdrawalRequest` | **更严**`userType==Agent``shopID==GetShopIDFromContext(ctx)` | 写操作,业务规定提现必须本人发起,平台和顶级代理均不允许代办 |
**越权校验覆盖范围**:不仅新增/迁移的方法要加,**已有的 `ListShopWithdrawalRequests` / `ListShopCommissionRecords` 也必须补齐** —— 现状它们只做了 `shopStore.GetByID` 的存在性检查,没有 `CanManageShop``/my/commission-records``/my/withdrawal-requests` 废弃后代理只要猜 shopID 就可读他人数据。是本变更必须修复的回归入口。
---
### 决策 4Handler 不重命名,路由集中在 registerShopCommissionRoutes
`ShopCommissionHandler` 职责扩大(涵盖资金概况、主钱包流水、提现发起、佣金统计),保持原名 `ShopCommissionHandler` 不重命名,避免大范围文件改动。路由全部集中在扩展后的 `registerShopCommissionRoutes` 中,不再新建 `registerShopFundSummaryRoutes`,避免一个模块两处注册入口。
---
### 决策 5`main_frozen_balance` 字段保留但标注预留
**现状**`tb_agent_wallet.frozen_balance` 是通用字段但主钱包当前业务无冻结场景DB 验证 10 条主钱包记录 `frozen_balance` 全为 0
**决策**`ShopFundSummaryItem` 保留 `main_frozen_balance`,作为未来扩展(如主钱包预授权/待扣款场景预留位。Handler 直接返回 `AgentWallet.FrozenBalance`,无额外逻辑。前端展示可暂时不渲染,等业务触发时再暴露。
**备选**:不返回此字段 —— 将来接入时要改 DTO + 前端 + 文档,破坏性更大,放弃。
---
## 分层变更总览
```
Handler 层
ShopCommissionHandler 新增方法: ListFundSummary, ListMainWalletTransactions,
GetCommissionStats, GetCommissionDailyStats,
CreateWithdrawal
删除方法: ListCommissionSummary被 ListFundSummary 替代)
MyCommissionHandler 整体删除
Service 层
ShopCommissionService 构造函数补依赖: commissionWithdrawalSettingStore,
agentWalletTransactionStore
改造方法: ListShopCommissionSummary → ListShopFundSummary
(加批量主钱包查询)
新增方法: ListMainWalletTransactions, GetStats,
GetDailyStats, CreateWithdrawalRequest
(均在入口调 middleware.CanManageShop
补权限校验: ListShopWithdrawalRequests,
ListShopCommissionRecords
(现状无越权校验,必须补齐)
MyCommissionService 整体删除
Store 层
AgentWalletStore 新增: GetShopMainWalletBatch(ctx, shopIDs)
AgentWalletTransactionStore 新增: ListByWalletIDWithFilters, CountByWalletID
删除: ListByShopID, CountByShopID
(全局无调用者,且会跨钱包类型返回
数据,留着易被误用)
DTO 层
ShopCommissionSummaryItem → ShopFundSummaryItem新增 main_balance, main_frozen_balance
ShopCommissionSummaryListReq / PageResult → ShopFundSummaryListReq / PageResult
新增: MainWalletTransactionItem, MainWalletTransactionListRequest/Response
删除: MyCommissionSummaryResp, MyWithdrawalListReq, MyCommissionRecordListReq 等
路由层
registerMyCommissionRoutes 删除
registerShopCommissionRoutes 扩展:
- GET /commission-summary → GET /fund-summary
- 新增 GET /:shop_id/main-wallet/transactions
- 新增 GET /:shop_id/commission-stats
- 新增 GET /:shop_id/commission-daily-stats
- 新增 POST /:shop_id/withdrawal-requests
```
---
## Risks / Trade-offs
| 风险 | 缓解措施 |
|------|---------|
| 批量查主钱包余额新增一次 DB 查询,影响 fund-summary 接口性能 | 数据量有限(代理数量通常 < 1000IN 查询 < 5ms如后期扩大可加 Redis 缓存 |
| 删除 `/my/` 路由是破坏性变更,前端需同步修改 | 开发阶段,不考虑向后兼容;前端在本次变更后同步切换 |
| `MyCommissionService` 删除后,若有其他地方引用会编译报错 | tasks 中包含全局 grep 检查,确保删干净 |
| commission-stats/daily-stats 迁移后shopID 改为显式参数,逻辑路径变化 | 单独验证两个统计接口的查询结果与原 /my/ 接口一致 |
| **越权回归漏洞**`/my/commission-records` / `/my/withdrawal-requests` 废弃后代理改走 `/shops/:shop_id/...`,而已有的 `ListShopCommissionRecords` / `ListShopWithdrawalRequests` 没做 `CanManageShop` 校验,代理只要猜到他人 shopID 就能读到敏感数据 | task 3.6 强制在这两个已有方法的入口补 `CanManageShop`task 10.5 手动验证 403 |
| `CreateWithdrawalRequest` 若用 `CanManageShop` 会允许顶级代理替下级店铺提现 | 已确认产品不允许:`CreateWithdrawalRequest` 不使用 `CanManageShop`,改为更严格的双重校验(`userType==Agent``shopID==自己`),平台人员也禁止调用,与原 `/my/withdrawal-requests` 行为一致 |
| 删除 `AgentWalletTransactionStore.ListByShopID/CountByShopID` 可能导致外部/历史代码编译失败 | task 2.3 在删除前用 grep 确认无其他调用者(已在审查阶段验证过) |
---
## Migration Plan
1. DTO 层:重命名 `ShopCommissionSummaryItem → ShopFundSummaryItem` 并新增字段、新增主钱包流水 DTO
2. Store 层:
- `AgentWalletStore` 新增 `GetShopMainWalletBatch`
- `AgentWalletTransactionStore` 新增 `ListByWalletIDWithFilters` / `CountByWalletID`,删除 `ListByShopID` / `CountByShopID`
3. Service 层:
- 补构造函数依赖
- 改造 `ListShopFundSummary`
- 迁移三个 `/my/` 方法(入口强制 `CanManageShop`
- 新增 `ListMainWalletTransactions`(入口强制 `CanManageShop`
- 给已有的 `ListShopWithdrawalRequests` / `ListShopCommissionRecords``CanManageShop`
4. Handler 层:`ShopCommissionHandler` 新增方法、删除 `ListCommissionSummary`
5. 路由层:扩展 `registerShopCommissionRoutes`、删除 `registerMyCommissionRoutes`
6. Bootstrap删除 `MyCommissionHandler`/`myCommissionService`,更新 `shopCommissionService` 初始化
7. 文档生成器:`cmd/api/docs.go``cmd/gendocs/main.go`
8. 删除 `internal/handler/admin/my_commission.go``internal/service/my_commission/``internal/routes/my_commission.go`
9. `go build ./...` 全量编译 + grep 确认无残留引用
10. PostgreSQL MCP 手动验证
无数据库迁移,无需 rollback 策略。
## Open Questions
无,所有设计决策已在探索阶段与需求方确认。