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

11 KiB
Raw Blame History

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 中的 GetStatsGetDailyStatsCreateWithdrawalRequest 当前从 ctx 自动读取 shopID隐式即"自己"),迁移后接口路径从 :shop_id 取,必须显式越权校验。

决策

  • 将三个方法迁移到 ShopCommissionService,签名改为接受 shopID uint 参数
  • Service 层入口强制权限校验(不是 Handler 层),对齐 CLAUDE.md 的 Code Review 规范(参考 internal/service/account/service.go
  • Handler 只做参数解析,不写权限逻辑
  • 删除 MyCommissionServiceMyCommissionHandler

两档校验策略(根据业务严格度区分):

方法 校验策略 理由
GetStats / GetDailyStats middleware.CanManageShop(ctx, shopID) 查询类,平台和顶级代理可看下级数据
ListMainWalletTransactions middleware.CanManageShop(ctx, shopID) 查询类,同上
ListShopWithdrawalRequests / ListShopCommissionRecords(已有方法补齐) middleware.CanManageShop(ctx, shopID) 查询类,同上
CreateWithdrawalRequest 更严userType==AgentshopID==GetShopIDFromContext(ctx) 写操作,业务规定提现必须本人发起,平台和顶级代理均不允许代办

越权校验覆盖范围:不仅新增/迁移的方法要加,已有的 ListShopWithdrawalRequests / ListShopCommissionRecords 也必须补齐 —— 现状它们只做了 shopStore.GetByID 的存在性检查,没有 CanManageShop/my/commission-records/my/withdrawal-requests 废弃后代理只要猜 shopID 就可读他人数据。是本变更必须修复的回归入口。


决策 4Handler 不重命名,路由集中在 registerShopCommissionRoutes

ShopCommissionHandler 职责扩大(涵盖资金概况、主钱包流水、提现发起、佣金统计),保持原名 ShopCommissionHandler 不重命名,避免大范围文件改动。路由全部集中在扩展后的 registerShopCommissionRoutes 中,不再新建 registerShopFundSummaryRoutes,避免一个模块两处注册入口。


决策 5main_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 强制在这两个已有方法的入口补 CanManageShoptask 10.5 手动验证 403
CreateWithdrawalRequest 若用 CanManageShop 会允许顶级代理替下级店铺提现 已确认产品不允许:CreateWithdrawalRequest 不使用 CanManageShop,改为更严格的双重校验(userType==AgentshopID==自己),平台人员也禁止调用,与原 /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 / ListShopCommissionRecordsCanManageShop
  4. Handler 层:ShopCommissionHandler 新增方法、删除 ListCommissionSummary
  5. 路由层:扩展 registerShopCommissionRoutes、删除 registerMyCommissionRoutes
  6. Bootstrap删除 MyCommissionHandler/myCommissionService,更新 shopCommissionService 初始化
  7. 文档生成器:cmd/api/docs.gocmd/gendocs/main.go
  8. 删除 internal/handler/admin/my_commission.gointernal/service/my_commission/internal/routes/my_commission.go
  9. go build ./... 全量编译 + grep 确认无残留引用
  10. PostgreSQL MCP 手动验证

无数据库迁移,无需 rollback 策略。

Open Questions

无,所有设计决策已在探索阶段与需求方确认。