- 将 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>
11 KiB
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
- DTO 层:重命名
ShopCommissionSummaryItem → ShopFundSummaryItem并新增字段、新增主钱包流水 DTO - Store 层:
AgentWalletStore新增GetShopMainWalletBatchAgentWalletTransactionStore新增ListByWalletIDWithFilters/CountByWalletID,删除ListByShopID/CountByShopID
- Service 层:
- 补构造函数依赖
- 改造
ListShopFundSummary - 迁移三个
/my/方法(入口强制CanManageShop) - 新增
ListMainWalletTransactions(入口强制CanManageShop) - 给已有的
ListShopWithdrawalRequests/ListShopCommissionRecords补CanManageShop
- Handler 层:
ShopCommissionHandler新增方法、删除ListCommissionSummary - 路由层:扩展
registerShopCommissionRoutes、删除registerMyCommissionRoutes - Bootstrap:删除
MyCommissionHandler/myCommissionService,更新shopCommissionService初始化 - 文档生成器:
cmd/api/docs.go和cmd/gendocs/main.go - 删除
internal/handler/admin/my_commission.go、internal/service/my_commission/、internal/routes/my_commission.go go build ./...全量编译 + grep 确认无残留引用- PostgreSQL MCP 手动验证
无数据库迁移,无需 rollback 策略。
Open Questions
无,所有设计决策已在探索阶段与需求方确认。