All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m10s
- 将 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>
189 lines
11 KiB
Markdown
189 lines
11 KiB
Markdown
## 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 就可读他人数据。是本变更必须修复的回归入口。
|
||
|
||
---
|
||
|
||
### 决策 4:Handler 不重命名,路由集中在 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 接口性能 | 数据量有限(代理数量通常 < 1000),IN 查询 < 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
|
||
|
||
无,所有设计决策已在探索阶段与需求方确认。
|