## 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 无,所有设计决策已在探索阶段与需求方确认。