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

9.3 KiB
Raw Blame History

1. DTO 层变更

  • 1.1 新增 ShopFundSummaryItem(含 main_balancemain_frozen_balance 及原有佣金字段),新增 ShopFundSummaryListReqShopFundSummaryPageResult;删除旧的 ShopCommissionSummaryItem / ShopCommissionSummaryListReq / ShopCommissionSummaryPageResult
  • 1.2 新增主钱包流水 DTOMainWalletTransactionItemMainWalletTransactionListRequestMainWalletTransactionListResponse
  • 1.3 删除废弃 DTOMyCommissionSummaryRespMyWithdrawalListReqMyCommissionRecordListReqMyCommissionRecordItemMyCommissionRecordPageResult(确认无其他引用后删除)
    • 保留CommissionStatsRequestDailyCommissionStatsRequest / CommissionStatsResponse / DailyCommissionStatsResponse — 迁移后的 /shops/:id/commission-stats/shops/:id/commission-daily-stats 继续复用
    • 保留CreateMyWithdrawalReqCreateMyWithdrawalResp — 迁移后的 POST /shops/:id/withdrawal-requests 继续复用

2. Store 层变更

  • 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 的防御式过滤
  • 2.2 在 AgentWalletTransactionStore 新增过滤结构体 AgentWalletTransactionListFilters(字段:TransactionType stringStartDate stringEndDate 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 统计
  • 2.3 删除 AgentWalletTransactionStore.ListByShopIDCountByShopID已确认全局无其他调用者Grep 验证),且按 shopID 过滤会混入佣金钱包流水,留着会被误用。删除前再次 grep -rn "ListByShopID\|CountByShopID" --include="*.go" 确认

3. Service 层变更ShopCommissionService 扩展)

  • 3.0 更新 ShopCommissionService.New() 构造函数,在现有参数基础上新增两个依赖:
    • commissionWithdrawalSettingStore *postgres.CommissionWithdrawalSettingStoretask 3.4 迁移 CreateWithdrawalRequest 时校验最低提现金额和每日次数上限)
    • agentWalletTransactionStore *postgres.AgentWalletTransactionStoretask 3.5 新增 ListMainWalletTransactions 使用)
  • 3.1 将 ListShopCommissionSummary 改造为 ListShopFundSummary:删除原方法,方法签名/返回类型切换至 ShopFundSummaryListReq / ShopFundSummaryPageResult;调用 GetShopMainWalletBatch 批量拉取主钱包余额,合并到 ShopFundSummaryItem 中;主钱包不存在时 main_balance / main_frozen_balance 返回 0
  • 3.2 从 MyCommissionService 迁移 GetStatsShopCommissionService,签名改为 (ctx, shopID uint, req *dto.CommissionStatsRequest)方法入口第一行调用 middleware.CanManageShop(ctx, shopID),失败直接返回
  • 3.3 从 MyCommissionService 迁移 GetDailyStatsShopCommissionService,签名改为 (ctx, shopID uint, req *dto.DailyCommissionStatsRequest);同样入口加 CanManageShop
  • 3.4 从 MyCommissionService 迁移 CreateWithdrawalRequestShopCommissionService,签名改为 (ctx, shopID uint, req *dto.CreateMyWithdrawalReq)入口权限校验严于其他方法
    • 必须 middleware.GetUserTypeFromContext(ctx) == constants.UserTypeAgent,否则返回 403"仅代理商用户可发起提现"
    • 必须 shopID == middleware.GetShopIDFromContext(ctx),否则返回 403"仅可为本人店铺发起提现"
    • 不使用 CanManageShop(默认会放行整个下级层级,业务上不允许顶级代理替下级提现)
    • 保留原有的条件更新 Where("id = ? AND balance - frozen_balance >= ?") 并发保护
  • 3.5 在 ShopCommissionService 新增 ListMainWalletTransactions(ctx, shopID uint, req *dto.MainWalletTransactionListRequest) 方法:
    • 入口第一行调用 middleware.CanManageShop(ctx, shopID)
    • 通过 AgentWalletStore.GetMainWallet(ctx, shopID) 获取主钱包;不存在则返回空列表(total=0)不报错
    • 调用 ListByWalletIDWithFilters / CountByWalletIDtask 2.2 新增)查询流水
    • 不得使用 ListByShopID(已删除)
  • 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

  • 4.1 全局 grep 确认 MyCommissionService / myCommissionService / my_commission 的所有引用Handler、bootstrap、routes、cmd
  • 4.2 删除 internal/service/my_commission/service.go 及整个 my_commission 目录

5. Handler 层变更ShopCommissionHandler 扩展)

  • 5.1 将 ListCommissionSummary 改造为 ListFundSummary(重命名+切换 DTO 类型),调用 ShopCommissionService.ListShopFundSummary。方法只在 Handler 层做参数解析,不写权限逻辑
  • 5.2 新增 GetCommissionStats 方法,从路径参数取 shop_id,调用迁移后的 GetStats(权限校验在 Service 层)
  • 5.3 新增 GetCommissionDailyStats 方法,从路径参数取 shop_id,调用迁移后的 GetDailyStats
  • 5.4 新增 CreateWithdrawal 方法,从路径参数取 shop_id,调用迁移后的 CreateWithdrawalRequest
  • 5.5 新增 ListMainWalletTransactions 方法,从路径参数取 shop_id,调用 ShopCommissionService.ListMainWalletTransactions
  • 5.6 Handler 注释同步更新HTTP 方法和路径),遵循 comment-standards

6. Handler 层删除MyCommissionHandler

  • 6.1 删除 internal/handler/admin/my_commission.go

7. 路由层变更

  • 7.1 在 registerShopCommissionRoutes 中:将 GET /commission-summary 改为 GET /fund-summary,更新 Summary 为 "代理商资金概况",更新 Tags、InputShopFundSummaryListReq、OutputShopFundSummaryPageResult
  • 7.2 在 registerShopCommissionRoutes 中新增四条路由:
    • GET /:shop_id/main-wallet/transactionsListMainWalletTransactions
    • GET /:shop_id/commission-statsGetCommissionStats
    • GET /:shop_id/commission-daily-statsGetCommissionDailyStats
    • POST /:shop_id/withdrawal-requestsCreateWithdrawal
  • 7.3 删除 internal/routes/my_commission.go
  • 7.4 在路由注册入口(internal/routes/admin.go 或 registry中移除 registerMyCommissionRoutes 的调用

8. Bootstrap 变更

  • 8.1 在 internal/bootstrap/types.goHandlers 结构体中:删除 MyCommissionHandler 字段,确认 ShopCommissionHandler 字段存在
  • 8.2 在 internal/bootstrap/services.go 中:删除 myCommissionService 的初始化,移除对 my_commission 包的引用;更新 shopCommissionService 的初始化,补充 task 3.0 新增的两个依赖(stores.CommissionWithdrawalSettingstores.AgentWalletTransaction
  • 8.3 在 internal/bootstrap/handlers.go 中删除 MyCommissionHandler 的初始化

9. 文档生成器更新

  • 9.1 在 cmd/api/docs.go 中:删除 NewMyCommissionHandler(nil) 相关行,确认 ShopCommissionHandler 已包含新方法
  • 9.2 在 cmd/gendocs/main.go 中做同样清理

10. 编译与验证

  • 10.1 执行 go build ./...,确保无编译错误
  • 10.2 grep -rn "MyCommission\|my_commission\|ListByShopID\|CountByShopID\|ShopCommissionSummary" --include="*.go" 确认无残留引用
  • 10.3 使用 PostgreSQL MCP 验证 GET /shops/fund-summary 返回的 main_balancetb_agent_wallet 中对应记录一致shop_id=1: balance=50000, shop_id=9: balance=20000主钱包批量查询逻辑正确
  • 10.4 使用 PostgreSQL MCP 验证 GET /shops/:id/main-wallet/transactions 返回的流水与 tb_agent_wallet_transactionwallet_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/transactionsB 不是 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-statsPOST /my/withdrawal-requests 迁移后数据返回与迁移前等价(相同 shopID 相同查询条件,结果一致)