diff --git a/openspec/changes/agent-fund-visibility/.openspec.yaml b/openspec/changes/archive/2026-04-09-agent-fund-visibility/.openspec.yaml similarity index 100% rename from openspec/changes/agent-fund-visibility/.openspec.yaml rename to openspec/changes/archive/2026-04-09-agent-fund-visibility/.openspec.yaml diff --git a/openspec/changes/agent-fund-visibility/design.md b/openspec/changes/archive/2026-04-09-agent-fund-visibility/design.md similarity index 100% rename from openspec/changes/agent-fund-visibility/design.md rename to openspec/changes/archive/2026-04-09-agent-fund-visibility/design.md diff --git a/openspec/changes/agent-fund-visibility/proposal.md b/openspec/changes/archive/2026-04-09-agent-fund-visibility/proposal.md similarity index 100% rename from openspec/changes/agent-fund-visibility/proposal.md rename to openspec/changes/archive/2026-04-09-agent-fund-visibility/proposal.md diff --git a/openspec/changes/agent-fund-visibility/specs/agent-fund-summary/spec.md b/openspec/changes/archive/2026-04-09-agent-fund-visibility/specs/agent-fund-summary/spec.md similarity index 100% rename from openspec/changes/agent-fund-visibility/specs/agent-fund-summary/spec.md rename to openspec/changes/archive/2026-04-09-agent-fund-visibility/specs/agent-fund-summary/spec.md diff --git a/openspec/changes/agent-fund-visibility/specs/agent-wallet/spec.md b/openspec/changes/archive/2026-04-09-agent-fund-visibility/specs/agent-wallet/spec.md similarity index 100% rename from openspec/changes/agent-fund-visibility/specs/agent-wallet/spec.md rename to openspec/changes/archive/2026-04-09-agent-fund-visibility/specs/agent-wallet/spec.md diff --git a/openspec/changes/agent-fund-visibility/specs/commission-record-query/spec.md b/openspec/changes/archive/2026-04-09-agent-fund-visibility/specs/commission-record-query/spec.md similarity index 100% rename from openspec/changes/agent-fund-visibility/specs/commission-record-query/spec.md rename to openspec/changes/archive/2026-04-09-agent-fund-visibility/specs/commission-record-query/spec.md diff --git a/openspec/changes/agent-fund-visibility/specs/main-wallet-transactions/spec.md b/openspec/changes/archive/2026-04-09-agent-fund-visibility/specs/main-wallet-transactions/spec.md similarity index 100% rename from openspec/changes/agent-fund-visibility/specs/main-wallet-transactions/spec.md rename to openspec/changes/archive/2026-04-09-agent-fund-visibility/specs/main-wallet-transactions/spec.md diff --git a/openspec/changes/agent-fund-visibility/tasks.md b/openspec/changes/archive/2026-04-09-agent-fund-visibility/tasks.md similarity index 100% rename from openspec/changes/agent-fund-visibility/tasks.md rename to openspec/changes/archive/2026-04-09-agent-fund-visibility/tasks.md diff --git a/openspec/specs/agent-fund-summary/spec.md b/openspec/specs/agent-fund-summary/spec.md new file mode 100644 index 0000000..8ab444c --- /dev/null +++ b/openspec/specs/agent-fund-summary/spec.md @@ -0,0 +1,68 @@ +# agent-fund-summary Specification + +## Purpose +代理商资金概况列表,聚合展示代理的预充值钱包(主钱包)和佣金钱包余额信息,供平台和代理多层级查看资金概况。 + +## 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 错误,消息为"企业账号无权访问代理资金功能" diff --git a/openspec/specs/agent-wallet/spec.md b/openspec/specs/agent-wallet/spec.md index 0a3c416..00dd1b3 100644 --- a/openspec/specs/agent-wallet/spec.md +++ b/openspec/specs/agent-wallet/spec.md @@ -316,3 +316,24 @@ func RedisAgentWalletLockKey(shopID uint, walletType string) string - **THEN** 系统使用 Redis 分布式锁 `agent_wallet:lock:10:main`,第一个请求获得锁,第二个请求等待或失败 --- + +### 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 查询 + +--- diff --git a/openspec/specs/commission-record-query/spec.md b/openspec/specs/commission-record-query/spec.md index 6bdb050..83e4349 100644 --- a/openspec/specs/commission-record-query/spec.md +++ b/openspec/specs/commission-record-query/spec.md @@ -1,5 +1,28 @@ ## 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: 查询佣金记录列表 系统 SHALL 提供佣金记录列表查询,支持按店铺、佣金来源、时间范围、状态筛选。 diff --git a/openspec/specs/main-wallet-transactions/spec.md b/openspec/specs/main-wallet-transactions/spec.md new file mode 100644 index 0000000..3de1592 --- /dev/null +++ b/openspec/specs/main-wallet-transactions/spec.md @@ -0,0 +1,70 @@ +# main-wallet-transactions Specification + +## Purpose +预充值钱包(主钱包)交易流水查询,为管理员和代理提供主钱包的充值、扣款、退款等流水明细查看能力。 + +## 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 错误