feat: 代理商资金可见性重构(agent-fund-visibility)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m10s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m10s
- 将 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>
This commit is contained in:
2
openspec/changes/agent-fund-visibility/.openspec.yaml
Normal file
2
openspec/changes/agent-fund-visibility/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-09
|
||||
188
openspec/changes/agent-fund-visibility/design.md
Normal file
188
openspec/changes/agent-fund-visibility/design.md
Normal file
@@ -0,0 +1,188 @@
|
||||
## 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
|
||||
|
||||
无,所有设计决策已在探索阶段与需求方确认。
|
||||
156
openspec/changes/agent-fund-visibility/proposal.md
Normal file
156
openspec/changes/agent-fund-visibility/proposal.md
Normal file
@@ -0,0 +1,156 @@
|
||||
## Why
|
||||
|
||||
目前平台人员和代理账号均无法看到代理的预充值钱包(主钱包)余额与流水,佣金相关接口散落在 `/my/` 前缀下且与 `/shops/:id/` 路径存在职责重叠,亟需统一入口、补全预充值钱包可见性。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING** `GET /shops/commission-summary` 重命名为 `GET /shops/fund-summary`,响应 DTO 由 `ShopCommissionSummaryItem` 改为 `ShopFundSummaryItem`,新增 `main_balance`、`main_frozen_balance` 两个字段
|
||||
- **新增** `GET /shops/:id/main-wallet/transactions` — 预充值钱包流水列表(全新功能)
|
||||
- **新增** `GET /shops/:id/commission-stats` — 佣金统计,迁移自 `/my/commission-stats`
|
||||
- **新增** `GET /shops/:id/commission-daily-stats` — 每日佣金统计,迁移自 `/my/commission-daily-stats`
|
||||
- **新增** `POST /shops/:id/withdrawal-requests` — 发起提现申请,迁移自 `POST /my/withdrawal-requests`
|
||||
- **BREAKING 废弃** `/my/` 全部 6 个路由(详见废弃对照表)
|
||||
|
||||
### 废弃路由对照表
|
||||
|
||||
| 废弃路由 | 作用 | 替代路由 | 状态 |
|
||||
|---------|------|---------|------|
|
||||
| `GET /my/commission-summary` | 代理看自己的佣金钱包余额概览 | `GET /shops/fund-summary` | 本次改造 |
|
||||
| `GET /my/withdrawal-requests` | 代理看自己的提现申请记录 | `GET /shops/:id/withdrawal-requests` | 已有,直接复用 |
|
||||
| `GET /my/commission-records` | 代理看自己的每笔佣金入账明细(订单维度) | `GET /shops/:id/commission-records` | 已有,直接复用 |
|
||||
| `GET /my/commission-stats` | 代理看自己的佣金汇总统计(按时间段) | `GET /shops/:id/commission-stats` | 本次新增 |
|
||||
| `GET /my/commission-daily-stats` | 代理看自己的每日佣金金额趋势 | `GET /shops/:id/commission-daily-stats` | 本次新增 |
|
||||
| `POST /my/withdrawal-requests` | 代理发起提现申请 | `POST /shops/:id/withdrawal-requests` | 本次新增 |
|
||||
|
||||
### API 全景(变更前 vs 变更后)
|
||||
|
||||
```
|
||||
变更前 变更后
|
||||
─────────────────────────────────────────────────────────────────────────
|
||||
GET /shops/commission-summary → GET /shops/fund-summary ★ 改造+扩展
|
||||
GET /shops/:id/withdrawal-requests GET /shops/:id/withdrawal-requests
|
||||
GET /shops/:id/commission-records GET /shops/:id/commission-records
|
||||
→ GET /shops/:id/commission-stats ★ 新增
|
||||
→ GET /shops/:id/commission-daily-stats ★ 新增
|
||||
→ POST /shops/:id/withdrawal-requests ★ 新增
|
||||
→ GET /shops/:id/main-wallet/transactions ★ 新增
|
||||
|
||||
GET /my/commission-summary × 废弃
|
||||
GET /my/withdrawal-requests × 废弃
|
||||
GET /my/commission-records × 废弃
|
||||
GET /my/commission-stats × 废弃
|
||||
GET /my/commission-daily-stats × 废弃
|
||||
POST /my/withdrawal-requests × 废弃
|
||||
|
||||
GET /agent-recharges GET /agent-recharges 不变
|
||||
GET /agent-recharges/:id GET /agent-recharges/:id 不变
|
||||
POST /agent-recharges POST /agent-recharges 不变
|
||||
POST /agent-recharges/:id/offline-pay POST /agent-recharges/:id/offline-pay 不变
|
||||
```
|
||||
|
||||
### 两个视角的完整操作流
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 平台人员 │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ [代理商资金概况列表] │
|
||||
│ GET /shops/fund-summary │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 店铺名 │ 预充值余额 │ 可提现佣金 │ 冻结佣金 │ 提现中 │ │
|
||||
│ │ 代理A │ ¥2,000 │ ¥500 │ ¥100 │ ¥200 │ │
|
||||
│ │ 代理B │ ¥500 │ ¥200 │ ¥0 │ ¥0 │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ 进入某代理详情 │
|
||||
│ ▼ │
|
||||
│ [代理详情 — 分 tab 查看] │
|
||||
│ ├─ 预充值流水 GET /shops/:id/main-wallet/transactions │
|
||||
│ ├─ 佣金明细 GET /shops/:id/commission-records │
|
||||
│ ├─ 佣金统计 GET /shops/:id/commission-stats │
|
||||
│ ├─ 每日统计 GET /shops/:id/commission-daily-stats │
|
||||
│ └─ 提现记录 GET /shops/:id/withdrawal-requests │
|
||||
│ │
|
||||
│ [充值管理] │
|
||||
│ GET /agent-recharges 查看所有充值订单(含状态) │
|
||||
│ POST /agent-recharges/:id/offline-pay 确认线下到账 │
|
||||
│ │
|
||||
│ [提现审批] │
|
||||
│ GET /commission/withdrawal-requests 审批提现申请 │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 代理账号(同接口,按数据权限过滤) │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ [我的资金概况(返回自己 + 所有下级代理店铺)] │
|
||||
│ GET /shops/fund-summary │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ 预充值余额: ¥2,000 │ 可提现佣金: ¥500 │ │
|
||||
│ │ 冻结余额: ¥100 │ 提现中: ¥200 │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [预充值钱包流水] │
|
||||
│ GET /shops/自己ID/main-wallet/transactions │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ 时间 │ 类型 │ 金额 │ 变动后余额 │ │
|
||||
│ │ 04-08 10:00 │ 充值入账 │ +¥500 │ ¥2,000 │ │
|
||||
│ │ 04-07 15:30 │ 套餐扣款 │ -¥100 │ ¥1,500 │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [佣金相关] │
|
||||
│ GET /shops/自己ID/commission-records 佣金明细 │
|
||||
│ GET /shops/自己ID/commission-stats 佣金统计 │
|
||||
│ GET /shops/自己ID/withdrawal-requests 提现记录 │
|
||||
│ POST /shops/自己ID/withdrawal-requests 发起提现 │
|
||||
│ │
|
||||
│ [充值订单(GORM 自动过滤,只见自己的)] │
|
||||
│ GET /agent-recharges │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 数据流向
|
||||
|
||||
```
|
||||
【充值流】
|
||||
平台操作 POST /agent-recharges
|
||||
↓
|
||||
tb_agent_recharge_record(充值订单,含状态:待支付/已完成/已取消)
|
||||
↓ 线下确认 / 微信回调
|
||||
tb_agent_wallet (wallet_type=main) balance +
|
||||
tb_agent_wallet_transaction (transaction_type=recharge)
|
||||
↓ 代理购买套餐
|
||||
tb_agent_wallet (wallet_type=main) balance -
|
||||
tb_agent_wallet_transaction (transaction_type=deduct)
|
||||
|
||||
【佣金流】
|
||||
用户下单 → 佣金计算 → tb_commission_record(订单维度明细)
|
||||
↓ 佣金入账
|
||||
tb_agent_wallet (wallet_type=commission) balance +
|
||||
tb_agent_wallet_transaction (transaction_type=commission)
|
||||
↓ 代理发起提现 POST /shops/:id/withdrawal-requests
|
||||
tb_commission_withdrawal_request(提现申请)
|
||||
↓ 平台审批通过
|
||||
tb_agent_wallet (wallet_type=commission) balance -
|
||||
tb_agent_wallet_transaction (transaction_type=withdrawal)
|
||||
```
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `agent-fund-summary`: 代理商资金概况接口,同一接口支持平台(全量)和代理(仅自己)两种视角,包含预充值余额与佣金钱包字段
|
||||
- `main-wallet-transactions`: 代理预充值钱包(主钱包)流水查询,按 shop_id 分页检索 tb_agent_wallet_transaction(wallet_type=main)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `commission-record-query`: 佣金统计和每日统计从 `/my/` 路径迁移至 `/shops/:id/` 路径,同时新增 POST 提现发起路由;原 `/my/` 路径全部废弃
|
||||
- `agent-wallet`: 资金概况接口现在对外暴露主钱包余额,`AgentWalletStore.GetShopCommissionSummaryBatch` 需扩展以批量拉取主钱包余额
|
||||
|
||||
## Impact
|
||||
|
||||
- **Handler 层**: 扩展 `ShopCommissionHandler`,新增 `ListFundSummary`、`ListMainWalletTransactions`、`GetCommissionStats`、`GetCommissionDailyStats`、`CreateWithdrawal` 五个方法;`MyCommissionHandler` 整体删除
|
||||
- **Service 层**: `ShopCommissionService` 构造函数补两个依赖(`commissionWithdrawalSettingStore`、`agentWalletTransactionStore`),`ListShopCommissionSummary` 改造为 `ListShopFundSummary`,新增 `ListMainWalletTransactions`,并从 `MyCommissionService` 迁移 `GetStats / GetDailyStats / CreateWithdrawalRequest` 三个方法(签名改为显式 `shopID`,入口强制调用 `middleware.CanManageShop` 校验);`MyCommissionService` 整体删除
|
||||
- **Store 层**: `AgentWalletStore` 新增 `GetShopMainWalletBatch`;`AgentWalletTransactionStore` 新增 `ListByWalletIDWithFilters` / `CountByWalletID`,并删除原 `ListByShopID` / `CountByShopID`(全局无其他调用者,且会跨钱包类型返回数据,留着易被误用)
|
||||
- **DTO 层**: `ShopCommissionSummaryItem` → `ShopFundSummaryItem`(新增 `main_balance`、`main_frozen_balance`);新增主钱包流水 DTO;删除 `/my/` 系列 DTO
|
||||
- **路由层**: 删除 `registerMyCommissionRoutes`,在 `registerShopCommissionRoutes` 内将 `/commission-summary` 改为 `/fund-summary`,并追加 4 条 `/:shop_id/...` 路由
|
||||
- **权限层**: 所有迁移过来的 `/shops/:shop_id/...` 入口必须在 Service 层显式调用 `middleware.CanManageShop(ctx, shopID)`,同时覆盖本次新增接口和已有的 `ListShopWithdrawalRequests`、`ListShopCommissionRecords`(当前只做了店铺存在性检查,未做越权校验,迁移后代理只要猜到其他 shopID 就能读取,必须补齐)
|
||||
- **文档生成器**: `cmd/api/docs.go` 和 `cmd/gendocs/main.go` 同步更新
|
||||
@@ -0,0 +1,63 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 代理商资金概况列表
|
||||
|
||||
系统 SHALL 提供 `GET /api/admin/shops/fund-summary` 接口,返回分页的代理商资金概况列表,同时包含预充值钱包(主钱包)和佣金钱包的余额信息。
|
||||
|
||||
**响应字段**(`ShopFundSummaryItem`):
|
||||
- `shop_id`:店铺 ID
|
||||
- `shop_name`:店铺名称
|
||||
- `shop_code`:店铺编码
|
||||
- `username`:主账号用户名
|
||||
- `phone`:主账号手机号
|
||||
- `main_balance`:预充值钱包余额(分)
|
||||
- `main_frozen_balance`:预充值钱包冻结余额(分)
|
||||
- `total_commission`:累计佣金总额(分)
|
||||
- `withdrawn_commission`:已提现佣金(分)
|
||||
- `unwithdraw_commission`:未提现佣金(分)
|
||||
- `frozen_commission`:冻结中佣金(分)
|
||||
- `withdrawing_commission`:提现中佣金(分)
|
||||
- `available_commission`:可提现佣金(分)
|
||||
- `created_at`:店铺创建时间
|
||||
|
||||
**查询参数**(`ShopFundSummaryListReq`):
|
||||
- `page`:页码(默认 1)
|
||||
- `page_size`:每页数量(默认 20,最大 100)
|
||||
- `shop_name`:店铺名称模糊查询
|
||||
- `username`:主账号用户名模糊查询
|
||||
|
||||
**实现要求**:
|
||||
- 主钱包余额通过 `AgentWalletStore.GetShopMainWalletBatch` 批量查询,避免 N+1
|
||||
- 若代理暂无主钱包记录(未充值),`main_balance` 和 `main_frozen_balance` 返回 0
|
||||
- `main_frozen_balance` 字段为未来预留(当前业务无主钱包冻结场景,值恒为 0),前端可暂不展示
|
||||
- 数据权限:列表查询走 `Shop` 表的数据权限过滤(`SubordinateShopIDs`)。平台人员返回全部代理;代理账号返回自己 + 所有下级店铺(而不是只返回自己一条)
|
||||
|
||||
#### Scenario: 平台人员查看所有代理资金概况
|
||||
|
||||
- **WHEN** 平台人员请求 `GET /shops/fund-summary`
|
||||
- **THEN** 系统返回所有代理的分页列表,每条包含 `main_balance` 和佣金钱包字段
|
||||
|
||||
#### Scenario: 无下级代理的账号查看自己的资金概况
|
||||
|
||||
- **WHEN** 无下级代理的代理账号请求 `GET /shops/fund-summary`
|
||||
- **THEN** 系统按数据权限过滤,只返回该代理自己的一条记录
|
||||
|
||||
#### Scenario: 有下级代理的顶级代理查看资金概况
|
||||
|
||||
- **WHEN** 一个拥有多个下级代理的顶级代理账号请求 `GET /shops/fund-summary`
|
||||
- **THEN** 系统返回自己 + 所有下级代理店铺的资金概况列表(按 `SubordinateShopIDs` 过滤)
|
||||
|
||||
#### Scenario: 代理暂无主钱包时返回零值
|
||||
|
||||
- **WHEN** 代理从未充值,`tb_agent_wallet` 中无该店铺的 `wallet_type=main` 记录
|
||||
- **THEN** `main_balance` 和 `main_frozen_balance` 返回 0,其余字段正常返回
|
||||
|
||||
#### Scenario: 按店铺名称过滤
|
||||
|
||||
- **WHEN** 传入 `shop_name=张三`
|
||||
- **THEN** 系统只返回店铺名称包含"张三"的代理记录
|
||||
|
||||
#### Scenario: 企业账号无权访问
|
||||
|
||||
- **WHEN** 企业账号请求此接口
|
||||
- **THEN** 系统返回 403 错误,消息为"企业账号无权访问代理资金功能"
|
||||
@@ -0,0 +1,20 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 批量查询店铺主钱包余额
|
||||
|
||||
系统 SHALL 在 `AgentWalletStore` 中提供 `GetShopMainWalletBatch(ctx, shopIDs []uint) map[uint]*AgentWallet` 方法,一次查询多个店铺的主钱包(`wallet_type=main`)记录,返回以 `shop_id` 为 key 的 map。
|
||||
|
||||
**实现要求**:
|
||||
- 使用 `WHERE shop_id IN (?) AND wallet_type = 'main'` 单次查询,不得逐条查询
|
||||
- 不在 map 中的 shop_id 表示该店铺暂无主钱包,调用方按零值处理
|
||||
- 与现有 `GetShopCommissionSummaryBatch` 对称设计
|
||||
|
||||
#### Scenario: 批量查询多个店铺的主钱包
|
||||
|
||||
- **WHEN** 传入 shopIDs `[1, 2, 3]`,其中 shop 3 无主钱包记录
|
||||
- **THEN** 返回 map `{1: &wallet1, 2: &wallet2}`,shop 3 不在 map 中
|
||||
|
||||
#### Scenario: 传入空列表
|
||||
|
||||
- **WHEN** 传入空 `shopIDs`
|
||||
- **THEN** 直接返回空 map,不执行 DB 查询
|
||||
@@ -0,0 +1,150 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 迁移接口的越权校验(通用要求)
|
||||
|
||||
本规范下所有 `/shops/:shop_id/...` 迁移/新增接口,Service 层方法入口 SHALL 调用 `middleware.CanManageShop(ctx, shopID)` 做显式越权校验。Handler 层只做参数解析,不承担权限逻辑。该要求同时追溯适用于已有但缺少校验的 `ListShopWithdrawalRequests`、`ListShopCommissionRecords` 两个方法(回归漏洞修复)。
|
||||
|
||||
统一错误返回:`errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")`。
|
||||
|
||||
#### Scenario: 代理传非自己管辖店铺 ID 被 Service 层拦截
|
||||
|
||||
- **WHEN** 代理 A(shopID=10)请求本规范任意一个 `/shops/:shop_id/...` 接口,传入代理 B(shopID=20)的 shopID
|
||||
- **THEN** Service 层入口的 `middleware.CanManageShop(ctx, 20)` 返回 error,接口返回 403,消息 "无权限操作该资源或资源不存在"
|
||||
|
||||
#### Scenario: 平台人员不受 CanManageShop 限制
|
||||
|
||||
- **WHEN** 平台人员请求任意 `/shops/:shop_id/...` 接口
|
||||
- **THEN** `middleware.CanManageShop` 直接放行,接口正常返回数据
|
||||
|
||||
#### Scenario: 已有方法 ListShopCommissionRecords / ListShopWithdrawalRequests 补齐校验
|
||||
|
||||
- **WHEN** 代理账号请求 `/shops/{非自己管辖shopID}/commission-records` 或 `/withdrawal-requests`
|
||||
- **THEN** 实现后返回 403(回归漏洞修复前这两个方法会返回数据)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 通过店铺 ID 查询佣金统计
|
||||
|
||||
系统 SHALL 提供 `GET /api/admin/shops/:shop_id/commission-stats` 接口,返回指定代理店铺在给定时间范围内的佣金汇总统计。
|
||||
|
||||
**路径参数**:`shop_id`(必填)
|
||||
**查询参数**:`start_time`、`end_time`(可选,ISO8601 格式)
|
||||
**响应**:与原 `/my/commission-stats` 一致(`CommissionStatsResponse`)
|
||||
**权限**:Service 层入口调用 `middleware.CanManageShop(ctx, shopID)`
|
||||
|
||||
#### Scenario: 平台人员查询某代理的佣金统计
|
||||
|
||||
- **WHEN** 平台人员请求 `GET /shops/123/commission-stats`
|
||||
- **THEN** 系统返回店铺 123 的佣金汇总统计
|
||||
|
||||
#### Scenario: 代理查询自己的佣金统计
|
||||
|
||||
- **WHEN** 代理账号请求 `GET /shops/自己ID/commission-stats`
|
||||
- **THEN** 系统返回该代理自己的佣金统计数据
|
||||
|
||||
#### Scenario: 代理尝试查询他人统计被拦截
|
||||
|
||||
- **WHEN** 代理账号请求 `GET /shops/他人ID/commission-stats`
|
||||
- **THEN** 系统返回 403 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 通过店铺 ID 查询每日佣金统计
|
||||
|
||||
系统 SHALL 提供 `GET /api/admin/shops/:shop_id/commission-daily-stats` 接口,返回指定代理店铺的每日佣金趋势数据。
|
||||
|
||||
**路径参数**:`shop_id`(必填)
|
||||
**查询参数**:`start_date`、`end_date`(可选,默认最近 30 天)
|
||||
**响应**:与原 `/my/commission-daily-stats` 一致(`[]DailyCommissionStatsResponse`)
|
||||
**权限**:Service 层入口调用 `middleware.CanManageShop(ctx, shopID)`
|
||||
|
||||
#### Scenario: 平台人员查询某代理的每日佣金
|
||||
|
||||
- **WHEN** 平台人员请求 `GET /shops/123/commission-daily-stats`
|
||||
- **THEN** 系统返回店铺 123 的每日佣金趋势
|
||||
|
||||
#### Scenario: 代理查询自己的每日佣金
|
||||
|
||||
- **WHEN** 代理账号请求 `GET /shops/自己ID/commission-daily-stats`
|
||||
- **THEN** 系统返回该代理自己的每日佣金数据,默认最近 30 天
|
||||
|
||||
#### Scenario: 代理尝试查询他人数据被拦截
|
||||
|
||||
- **WHEN** 代理账号请求 `GET /shops/他人ID/commission-daily-stats`
|
||||
- **THEN** 系统返回 403 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 通过店铺 ID 发起提现申请
|
||||
|
||||
系统 SHALL 提供 `POST /api/admin/shops/:shop_id/withdrawal-requests` 接口,允许代理为自己的店铺发起佣金提现申请。
|
||||
|
||||
**路径参数**:`shop_id`(必填)
|
||||
**请求体**:与原 `POST /my/withdrawal-requests` 一致(`CreateMyWithdrawalReq`)
|
||||
**响应**:与原 `POST /my/withdrawal-requests` 一致(`CreateMyWithdrawalResp`)
|
||||
|
||||
**权限规则**(严于其他迁移接口,**不使用** `CanManageShop`):
|
||||
- Service 层入口必须满足两个条件:
|
||||
1. `userType == UserTypeAgent`(仅代理商用户)
|
||||
2. `shopID == middleware.GetShopIDFromContext(ctx)`(必须本人店铺,禁止顶级代理替下级提现)
|
||||
- 平台人员/超管/企业账号一律 403:业务上提现必须由代理本人发起,平台不代办
|
||||
- 顶级代理替下级提现也禁止:代理只能给自己的店铺发起提现申请
|
||||
|
||||
#### Scenario: 代理为自己发起提现
|
||||
|
||||
- **WHEN** 代理账号请求 `POST /shops/{自己shopID}/withdrawal-requests`,传入合法金额和收款信息
|
||||
- **THEN** 系统创建提现申请,返回申请 ID、提现单号、手续费信息
|
||||
|
||||
#### Scenario: 代理尝试为他人(非下级)发起提现被拦截
|
||||
|
||||
- **WHEN** 代理账号 A 请求 `POST /shops/{无关代理B的shopID}/withdrawal-requests`
|
||||
- **THEN** 系统返回 403 错误,消息"仅可为本人店铺发起提现"
|
||||
|
||||
#### Scenario: 顶级代理尝试替下级店铺发起提现被拦截
|
||||
|
||||
- **WHEN** 顶级代理 P 请求 `POST /shops/{P的下级shop_id}/withdrawal-requests`(即使 `CanManageShop` 会放行)
|
||||
- **THEN** 系统返回 403 错误,消息"仅可为本人店铺发起提现"。理由:业务上提现必须本人发起
|
||||
|
||||
#### Scenario: 平台人员尝试替代理发起提现被拦截
|
||||
|
||||
- **WHEN** 平台人员或超管请求 `POST /shops/:shop_id/withdrawal-requests`
|
||||
- **THEN** 系统返回 403 错误,消息"仅代理商用户可发起提现"
|
||||
|
||||
#### Scenario: 可提现余额不足
|
||||
|
||||
- **WHEN** 代理发起提现金额超过可提现余额
|
||||
- **THEN** 系统返回业务错误,消息为"可提现余额不足"
|
||||
|
||||
---
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: 通过 /my/ 路径查询佣金统计
|
||||
|
||||
**Reason**: 路径统一到 `/shops/:id/` 前缀,`/my/` 系列接口职责与平台视角重叠,增加维护成本
|
||||
**Migration**: 使用 `GET /api/admin/shops/:shop_id/commission-stats` 替代 `GET /api/admin/my/commission-stats`
|
||||
|
||||
### Requirement: 通过 /my/ 路径查询每日佣金统计
|
||||
|
||||
**Reason**: 同上,路径统一
|
||||
**Migration**: 使用 `GET /api/admin/shops/:shop_id/commission-daily-stats` 替代 `GET /api/admin/my/commission-daily-stats`
|
||||
|
||||
### Requirement: 通过 /my/ 路径发起提现申请
|
||||
|
||||
**Reason**: 同上,路径统一
|
||||
**Migration**: 使用 `POST /api/admin/shops/:shop_id/withdrawal-requests` 替代 `POST /api/admin/my/withdrawal-requests`
|
||||
|
||||
### Requirement: 通过 /my/ 路径查询佣金概览
|
||||
|
||||
**Reason**: 由 `/shops/fund-summary` 替代,新接口同时包含预充值钱包余额
|
||||
**Migration**: 使用 `GET /api/admin/shops/fund-summary` 替代 `GET /api/admin/my/commission-summary`
|
||||
|
||||
### Requirement: 通过 /my/ 路径查询提现记录
|
||||
|
||||
**Reason**: 路径统一
|
||||
**Migration**: 使用 `GET /api/admin/shops/:shop_id/withdrawal-requests` 替代 `GET /api/admin/my/withdrawal-requests`
|
||||
|
||||
### Requirement: 通过 /my/ 路径查询佣金明细
|
||||
|
||||
**Reason**: 路径统一
|
||||
**Migration**: 使用 `GET /api/admin/shops/:shop_id/commission-records` 替代 `GET /api/admin/my/commission-records`
|
||||
@@ -0,0 +1,65 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 预充值钱包流水查询
|
||||
|
||||
系统 SHALL 提供 `GET /api/admin/shops/:shop_id/main-wallet/transactions` 接口,分页返回指定代理店铺的预充值钱包(主钱包)交易流水记录。
|
||||
|
||||
**响应字段**(`MainWalletTransactionItem`):
|
||||
- `id`:流水记录 ID
|
||||
- `transaction_type`:交易类型。主钱包可能出现的值为 `recharge`-充值入账 / `deduct`-套餐扣款 / `refund`-退款(当前 DB 数据仅见 `recharge`,`deduct` / `refund` 随代购扣款/退款业务上线而出现;接口不对类型做枚举白名单,透传 DB 原值)
|
||||
- `transaction_subtype`:交易子类型(细分场景,如 `order_payment`,可为空)
|
||||
- `amount`:变动金额(分,正数为入账,负数为扣款)
|
||||
- `balance_before`:变动前余额(分)
|
||||
- `balance_after`:变动后余额(分)
|
||||
- `remark`:备注(可为空)
|
||||
- `created_at`:流水时间
|
||||
|
||||
**查询参数**(`MainWalletTransactionListRequest`):
|
||||
- `shop_id`:路径参数,店铺 ID(必填)
|
||||
- `page`:页码(默认 1)
|
||||
- `page_size`:每页数量(默认 20,最大 100)
|
||||
- `transaction_type`:按类型过滤(可选)
|
||||
- `start_date`:开始日期,`YYYY-MM-DD`(可选)
|
||||
- `end_date`:结束日期,`YYYY-MM-DD`(可选)
|
||||
|
||||
**实现要求**:
|
||||
- **Service 层入口必须调用 `middleware.CanManageShop(ctx, shopID)` 做越权校验**,校验失败直接返回 `errors.CodeForbidden`;不得依赖 `GetMainWallet` 的隐式过滤(该方法不做权限校验)
|
||||
- 先通过 `AgentWalletStore.GetMainWallet(shopID)` 获取主钱包;若不存在则返回空列表(`total=0`),不报错
|
||||
- 使用 `AgentWalletTransactionStore.ListByWalletIDWithFilters / CountByWalletID`(task 2.2 新增)查询流水,支持 transaction_type 和日期过滤
|
||||
- 旧方法 `ListByShopID / CountByShopID` 已在 task 2.3 删除(会跨钱包类型返回数据,易被误用)
|
||||
- 结果按 `created_at DESC` 排序
|
||||
|
||||
#### Scenario: 平台人员查看指定代理的预充值流水
|
||||
|
||||
- **WHEN** 平台人员请求 `GET /shops/123/main-wallet/transactions`
|
||||
- **THEN** 系统返回店铺 123 的主钱包流水,按时间倒序,含变动前后余额
|
||||
|
||||
#### Scenario: 代理查看自己的预充值流水
|
||||
|
||||
- **WHEN** 代理账号请求 `GET /shops/自己shop_id/main-wallet/transactions`
|
||||
- **THEN** 系统返回该代理自己的主钱包流水记录
|
||||
|
||||
#### Scenario: 代理尝试查看他人流水被拦截
|
||||
|
||||
- **WHEN** 代理账号请求 `GET /shops/他人shop_id/main-wallet/transactions`
|
||||
- **THEN** 系统返回 403 错误,消息为"无权限操作该资源或资源不存在"
|
||||
|
||||
#### Scenario: 代理暂无主钱包时返回空列表
|
||||
|
||||
- **WHEN** 代理从未充值,主钱包不存在
|
||||
- **THEN** 系统返回空列表,`total` 为 0,不报错
|
||||
|
||||
#### Scenario: 按交易类型过滤
|
||||
|
||||
- **WHEN** 传入 `transaction_type=recharge`
|
||||
- **THEN** 系统只返回充值入账类型的流水
|
||||
|
||||
#### Scenario: 按日期范围过滤
|
||||
|
||||
- **WHEN** 传入 `start_date=2026-01-01&end_date=2026-03-31`
|
||||
- **THEN** 系统只返回该日期范围内的流水记录
|
||||
|
||||
#### Scenario: 企业账号无权访问
|
||||
|
||||
- **WHEN** 企业账号请求此接口
|
||||
- **THEN** 系统返回 403 错误
|
||||
90
openspec/changes/agent-fund-visibility/tasks.md
Normal file
90
openspec/changes/agent-fund-visibility/tasks.md
Normal file
@@ -0,0 +1,90 @@
|
||||
## 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 相同查询条件,结果一致)
|
||||
Reference in New Issue
Block a user