This commit is contained in:
2026-04-09 14:53:48 +08:00
parent 0627ffec42
commit ff7e749bf2
12 changed files with 182 additions and 0 deletions

View File

@@ -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 错误,消息为"企业账号无权访问代理资金功能"

View File

@@ -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 查询

View File

@@ -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** 代理 AshopID=10请求本规范任意一个 `/shops/:shop_id/...` 接口,传入代理 BshopID=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`

View File

@@ -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 错误