Files
junhong_cmp_fiber/openspec/changes/agent-fund-visibility/tasks.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

91 lines
9.3 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.
## 1. DTO 层变更
- [x] 1.1 新增 `ShopFundSummaryItem`(含 `main_balance``main_frozen_balance` 及原有佣金字段),新增 `ShopFundSummaryListReq``ShopFundSummaryPageResult`;删除旧的 `ShopCommissionSummaryItem` / `ShopCommissionSummaryListReq` / `ShopCommissionSummaryPageResult`
- [x] 1.2 新增主钱包流水 DTO`MainWalletTransactionItem``MainWalletTransactionListRequest``MainWalletTransactionListResponse`
- [x] 1.3 删除废弃 DTO`MyCommissionSummaryResp``MyWithdrawalListReq``MyCommissionRecordListReq``MyCommissionRecordItem``MyCommissionRecordPageResult`(确认无其他引用后删除)
- **保留**`CommissionStatsRequest``DailyCommissionStatsRequest` / `CommissionStatsResponse` / `DailyCommissionStatsResponse` — 迁移后的 `/shops/:id/commission-stats``/shops/:id/commission-daily-stats` 继续复用
- **保留**`CreateMyWithdrawalReq``CreateMyWithdrawalResp` — 迁移后的 `POST /shops/:id/withdrawal-requests` 继续复用
## 2. Store 层变更
- [x] 2.1 在 `AgentWalletStore` 新增 `GetShopMainWalletBatch(ctx context.Context, shopIDs []uint) (map[uint]*model.AgentWallet, error)` 方法:`WHERE shop_id IN (?) AND wallet_type = 'main'` 单次查询,内部调用 `middleware.ApplyShopFilter` 对齐现有 `GetShopCommissionSummaryBatch` 的防御式过滤
- [x] 2.2 在 `AgentWalletTransactionStore` 新增过滤结构体 `AgentWalletTransactionListFilters`(字段:`TransactionType string``StartDate string``EndDate string`),以及以下两个方法:
- `ListByWalletIDWithFilters(ctx, walletID uint, opts *store.QueryOptions, filters *AgentWalletTransactionListFilters) ([]*model.AgentWalletTransaction, error)`:在 `WHERE agent_wallet_id = ?` 基础上叠加 transaction_type / 日期过滤,按 `created_at DESC` 排序
- `CountByWalletID(ctx, walletID uint, filters *AgentWalletTransactionListFilters) (int64, error)`:与列表方法过滤条件一致,用于分页 total 统计
- [x] 2.3 删除 `AgentWalletTransactionStore.ListByShopID``CountByShopID`已确认全局无其他调用者Grep 验证),且按 shopID 过滤会混入佣金钱包流水,留着会被误用。删除前再次 `grep -rn "ListByShopID\|CountByShopID" --include="*.go"` 确认
## 3. Service 层变更ShopCommissionService 扩展)
- [x] 3.0 更新 `ShopCommissionService.New()` 构造函数,在现有参数基础上新增两个依赖:
- `commissionWithdrawalSettingStore *postgres.CommissionWithdrawalSettingStore`task 3.4 迁移 `CreateWithdrawalRequest` 时校验最低提现金额和每日次数上限)
- `agentWalletTransactionStore *postgres.AgentWalletTransactionStore`task 3.5 新增 `ListMainWalletTransactions` 使用)
- [x] 3.1 将 `ListShopCommissionSummary` 改造为 `ListShopFundSummary`:删除原方法,方法签名/返回类型切换至 `ShopFundSummaryListReq` / `ShopFundSummaryPageResult`;调用 `GetShopMainWalletBatch` 批量拉取主钱包余额,合并到 `ShopFundSummaryItem` 中;主钱包不存在时 `main_balance` / `main_frozen_balance` 返回 0
- [x] 3.2 从 `MyCommissionService` 迁移 `GetStats``ShopCommissionService`,签名改为 `(ctx, shopID uint, req *dto.CommissionStatsRequest)`**方法入口第一行**调用 `middleware.CanManageShop(ctx, shopID)`,失败直接返回
- [x] 3.3 从 `MyCommissionService` 迁移 `GetDailyStats``ShopCommissionService`,签名改为 `(ctx, shopID uint, req *dto.DailyCommissionStatsRequest)`;同样入口加 `CanManageShop`
- [x] 3.4 从 `MyCommissionService` 迁移 `CreateWithdrawalRequest``ShopCommissionService`,签名改为 `(ctx, shopID uint, req *dto.CreateMyWithdrawalReq)`**入口权限校验严于其他方法**
- 必须 `middleware.GetUserTypeFromContext(ctx) == constants.UserTypeAgent`,否则返回 403"仅代理商用户可发起提现"
- 必须 `shopID == middleware.GetShopIDFromContext(ctx)`,否则返回 403"仅可为本人店铺发起提现"
- **不使用 `CanManageShop`**(默认会放行整个下级层级,业务上不允许顶级代理替下级提现)
- 保留原有的条件更新 `Where("id = ? AND balance - frozen_balance >= ?")` 并发保护
- [x] 3.5 在 `ShopCommissionService` 新增 `ListMainWalletTransactions(ctx, shopID uint, req *dto.MainWalletTransactionListRequest)` 方法:
- **入口第一行**调用 `middleware.CanManageShop(ctx, shopID)`
- 通过 `AgentWalletStore.GetMainWallet(ctx, shopID)` 获取主钱包;不存在则返回空列表(`total=0`)不报错
- 调用 `ListByWalletIDWithFilters` / `CountByWalletID`task 2.2 新增)查询流水
- **不得**使用 `ListByShopID`(已删除)
- [x] 3.6 **补齐已有方法的越权校验**(回归漏洞修复):
- `ListShopWithdrawalRequests`:方法入口加 `middleware.CanManageShop(ctx, shopID)`
- `ListShopCommissionRecords`:方法入口加 `middleware.CanManageShop(ctx, shopID)`
- 理由:`/my/withdrawal-requests``/my/commission-records` 废弃后代理改用 `/shops/:shop_id/...`,若不补校验,代理只要猜到其他 shopID 就能读取他人佣金明细和提现记录
## 4. Service 层删除MyCommissionService
- [x] 4.1 全局 grep 确认 `MyCommissionService` / `myCommissionService` / `my_commission` 的所有引用Handler、bootstrap、routes、cmd
- [x] 4.2 删除 `internal/service/my_commission/service.go` 及整个 `my_commission` 目录
## 5. Handler 层变更ShopCommissionHandler 扩展)
- [x] 5.1 将 `ListCommissionSummary` 改造为 `ListFundSummary`(重命名+切换 DTO 类型),调用 `ShopCommissionService.ListShopFundSummary`。方法只在 Handler 层做参数解析,不写权限逻辑
- [x] 5.2 新增 `GetCommissionStats` 方法,从路径参数取 `shop_id`,调用迁移后的 `GetStats`(权限校验在 Service 层)
- [x] 5.3 新增 `GetCommissionDailyStats` 方法,从路径参数取 `shop_id`,调用迁移后的 `GetDailyStats`
- [x] 5.4 新增 `CreateWithdrawal` 方法,从路径参数取 `shop_id`,调用迁移后的 `CreateWithdrawalRequest`
- [x] 5.5 新增 `ListMainWalletTransactions` 方法,从路径参数取 `shop_id`,调用 `ShopCommissionService.ListMainWalletTransactions`
- [x] 5.6 Handler 注释同步更新HTTP 方法和路径),遵循 comment-standards
## 6. Handler 层删除MyCommissionHandler
- [x] 6.1 删除 `internal/handler/admin/my_commission.go`
## 7. 路由层变更
- [x] 7.1 在 `registerShopCommissionRoutes` 中:将 `GET /commission-summary` 改为 `GET /fund-summary`,更新 Summary 为 "代理商资金概况",更新 Tags、Input`ShopFundSummaryListReq`、Output`ShopFundSummaryPageResult`
- [x] 7.2 在 `registerShopCommissionRoutes` 中新增四条路由:
- `GET /:shop_id/main-wallet/transactions``ListMainWalletTransactions`
- `GET /:shop_id/commission-stats``GetCommissionStats`
- `GET /:shop_id/commission-daily-stats``GetCommissionDailyStats`
- `POST /:shop_id/withdrawal-requests``CreateWithdrawal`
- [x] 7.3 删除 `internal/routes/my_commission.go`
- [x] 7.4 在路由注册入口(`internal/routes/admin.go` 或 registry中移除 `registerMyCommissionRoutes` 的调用
## 8. Bootstrap 变更
- [x] 8.1 在 `internal/bootstrap/types.go``Handlers` 结构体中:删除 `MyCommissionHandler` 字段,确认 `ShopCommissionHandler` 字段存在
- [x] 8.2 在 `internal/bootstrap/services.go` 中:删除 `myCommissionService` 的初始化,移除对 `my_commission` 包的引用;更新 `shopCommissionService` 的初始化,补充 task 3.0 新增的两个依赖(`stores.CommissionWithdrawalSetting``stores.AgentWalletTransaction`
- [x] 8.3 在 `internal/bootstrap/handlers.go` 中删除 `MyCommissionHandler` 的初始化
## 9. 文档生成器更新
- [x] 9.1 在 `cmd/api/docs.go` 中:删除 `NewMyCommissionHandler(nil)` 相关行,确认 `ShopCommissionHandler` 已包含新方法
- [x] 9.2 在 `cmd/gendocs/main.go` 中做同样清理
## 10. 编译与验证
- [x] 10.1 执行 `go build ./...`,确保无编译错误
- [x] 10.2 `grep -rn "MyCommission\|my_commission\|ListByShopID\|CountByShopID\|ShopCommissionSummary" --include="*.go"` 确认无残留引用
- [x] 10.3 使用 PostgreSQL MCP 验证 `GET /shops/fund-summary` 返回的 `main_balance``tb_agent_wallet` 中对应记录一致shop_id=1: balance=50000, shop_id=9: balance=20000主钱包批量查询逻辑正确
- [x] 10.4 使用 PostgreSQL MCP 验证 `GET /shops/:id/main-wallet/transactions` 返回的流水与 `tb_agent_wallet_transaction``wallet_type=main` 钱包 ID 的记录一致wallet_id=1 有3条记录wallet_id=17 有2条记录通过 agent_wallet_id 关联正确)
- [ ] 10.5 手动验证代理越权:
- 代理账号 A 请求 `/shops/{B的shop_id}/commission-records` / `/commission-stats` / `/commission-daily-stats` / `/withdrawal-requests` / `/main-wallet/transactions`B 不是 A 的下级)应全部返回 403
- 顶级代理 P 请求 `POST /shops/{下级shop_id}/withdrawal-requests` 应返回 403"仅可为本人店铺发起提现"(验证比 `CanManageShop` 更严格的限制)
- 平台账号请求 `POST /shops/:shop_id/withdrawal-requests` 应返回 403"仅代理商用户可发起提现"
- [ ] 10.6 验证原 `/my/commission-stats``/my/commission-daily-stats``POST /my/withdrawal-requests` 迁移后数据返回与迁移前等价(相同 shopID 相同查询条件,结果一致)