Files
junhong_cmp_fiber/openspec/specs/agent-open-api/spec.md
huang 95fc0b0a1b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
修复一些问题,主要是生效套餐
2026-05-12 10:32:38 +08:00

251 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# agent-open-api Specification
## Purpose
定义代理开放接口的签名认证、数据权限、卡查询、套餐列表、预充值钱包和钱包购买能力,确保第三方代理系统可以在受控权限内对接业务接口。
## Requirements
### Requirement: 代理开放接口签名认证
系统 SHALL 为 `/api/open/v1` 下所有代理开放接口提供签名认证。调用方 MUST 在请求 Header 中传入 `X-Agent-Account``X-Agent-Password``X-Agent-Timestamp``X-Agent-Nonce``X-Agent-Sign`。系统 MUST 校验后台账号存在、账号类型为代理账号、账号状态启用、已绑定启用状态的代理店铺、密码正确、时间戳在允许时间窗内、nonce 未被使用、签名正确。系统 MUST NOT 依赖独立开放接口账号授权表;所有启用状态的代理账号默认允许调用开放接口。
签名原文 SHALL 使用以下格式:
```text
METHOD + "\n" +
PATH + "\n" +
canonical_query_without_sign + "\n" +
SHA256(raw_body) + "\n" +
timestamp + "\n" +
nonce + "\n" +
account
```
签名算法 SHALL 使用 `hex(HMAC-SHA256(password, sign_payload))``canonical_query_without_sign` MUST 按参数名升序排列并排除 `sign` 字段。空 body 的 SHA256 MUST 按空字符串计算。
响应 MUST 使用统一格式 `{code, msg, data, timestamp}`。认证失败错误码 MUST 在 `pkg/errors/` 定义,不得把 bcrypt、HMAC、数据库底层错误暴露给调用方。
#### Scenario: 签名认证通过
- **WHEN** 启用状态的代理账号携带正确密码、时间戳、nonce、请求参数和签名调用 `/api/open/v1/cards/status`
- **THEN** 系统通过认证并将代理账号信息写入请求上下文
#### Scenario: 参数被篡改
- **WHEN** 请求中的 query 或 body 在签名后被修改
- **THEN** 系统验签失败并返回签名无效错误
#### Scenario: 密码错误
- **WHEN** 调用方传入的 `X-Agent-Password` 与后台代理账号密码不匹配
- **THEN** 系统返回认证失败错误,不继续执行业务逻辑
#### Scenario: nonce 重放
- **WHEN** 同一账号在有效时间窗内重复使用相同 `X-Agent-Nonce`
- **THEN** 系统返回重复请求错误,并拒绝执行业务逻辑
#### Scenario: 代理账号未启用
- **WHEN** 后台代理账号存在但账号状态为禁用
- **THEN** 系统返回认证失败错误,不继续执行业务逻辑
---
### Requirement: 开放接口代理上下文与数据权限
系统 SHALL 将通过签名认证的代理账号映射为内部代理上下文。上下文 MUST 包含 `ContextKeyUserID``ContextKeyUserType=UserTypeAgent``ContextKeyShopID``ContextKeySubordinateShopIDs`。开放接口业务查询 MUST 只允许访问该代理店铺及其下级店铺范围内的卡、套餐、订单和钱包数据。
#### Scenario: 访问自己店铺的卡
- **WHEN** 代理开放接口账号查询自己店铺名下卡的流量、状态或实名状态
- **THEN** 系统返回该卡数据
#### Scenario: 访问下级店铺的卡
- **WHEN** 代理开放接口账号查询下级店铺名下卡的流量、状态或实名状态
- **THEN** 系统返回该卡数据
#### Scenario: 访问无权限卡
- **WHEN** 代理开放接口账号查询非自己及非下级店铺名下的卡
- **THEN** 系统返回 `CodeForbidden`,消息为“无权限操作该资源或资源不存在”
#### Scenario: 企业账号调用开放接口
- **WHEN** 企业账号使用正确后台密码调用代理开放接口
- **THEN** 系统拒绝调用,因为开放接口仅允许代理账号
---
### Requirement: 卡流量查询开放接口
系统 SHALL 提供 `GET /api/open/v1/cards/traffic` 接口,按 `card_no` 查询单卡流量和套餐信息。`card_no` MUST 支持 ICCID、虚拟号、MSISDN 中任一种可解析为 IoT 卡的标识。接口 MUST 只返回单卡套餐,不返回设备级套餐信息。
响应 data MUST 至少包含:
- `card_no`
- `active_remaining_flow_mb`:当前生效套餐剩余流量
- `active_total_flow_mb`:当前生效套餐总流量
- `active_used_flow_mb`:当前生效套餐已使用流量
- `active_packages`:当前生效套餐列表
- `pending_packages`:待生效套餐列表
- `active_expires_at`:当前生效套餐过期时间
`active_packages` 每项 MUST 包含:`package_code``total_flow_mb``expires_at``start_at``package_name``series_name``package_type``package_type_name``pending_packages` 每项 MUST 包含:`package_code``total_flow_mb``package_name``series_name``package_type``package_type_name``valid_days``priority`
流量口径 MUST 满足:
- 总流量使用套餐真总量快照 `data_limit_mb`
- 已用流量使用换算后的展示已用量 `min(data_usage_mb * display_gain_ratio_snapshot, data_limit_mb)`
- 剩余流量使用 `max(data_limit_mb - used_flow_mb, 0)`
- 响应 MUST NOT 包含 `virtual_*``ratio``threshold``reduction`、虚流量、停机阈值等内部字段或文案
#### Scenario: 查询有生效套餐的卡流量
- **WHEN** 代理查询自己名下单卡,且该卡存在生效中的正式包和加油包
- **THEN** 系统返回当前生效套餐列表、总流量、换算后已用流量、剩余流量和当前过期时间
#### Scenario: 查询待生效套餐
- **WHEN** 代理查询单卡流量,且该卡存在待生效套餐
- **THEN** 系统在 `pending_packages` 中返回套餐编码、真总流量、套餐名称、系列名称、套餐类型、有效天数和生效优先级
#### Scenario: 不暴露虚流量
- **WHEN** 卡的套餐启用了虚流量
- **THEN** 响应只返回真总量、换算后已用量和剩余量,不返回任何虚流量字段或内部倍率字段
#### Scenario: 查询无套餐的卡
- **WHEN** 代理查询有权限但当前无生效或待生效套餐的卡
- **THEN** 系统返回空套餐列表,流量数值为 0
---
### Requirement: 卡状态查询开放接口
系统 SHALL 提供 `GET /api/open/v1/cards/status` 接口,按 `card_no` 查询单卡状态。响应 data MUST 包含 `card_no``card_status``card_status_name``stop_reason``stop_reason_name`。卡状态 MUST 按现有网络状态映射为停机/正常:`network_status=0` 返回停机,`network_status=1` 返回正常。
#### Scenario: 查询正常卡状态
- **WHEN** 代理查询有权限且 `network_status=1` 的卡
- **THEN** 系统返回 `card_status=normal``card_status_name=正常`
#### Scenario: 查询停机卡状态
- **WHEN** 代理查询有权限且 `network_status=0` 的卡
- **THEN** 系统返回 `card_status=stopped``card_status_name=停机` 以及停机原因
---
### Requirement: 卡实名状态查询开放接口
系统 SHALL 提供 `GET /api/open/v1/cards/realname-status` 接口,按 `card_no` 查询单卡实名状态。响应 data MUST 包含 `card_no``is_realnamed`。当 `real_name_status` 为已实名常量时,`is_realnamed` MUST 为 `true`,否则为 `false`
#### Scenario: 查询已实名卡
- **WHEN** 代理查询有权限且 `real_name_status=1` 的卡
- **THEN** 系统返回 `is_realnamed=true`
#### Scenario: 查询未实名卡
- **WHEN** 代理查询有权限且 `real_name_status=0` 的卡
- **THEN** 系统返回 `is_realnamed=false`
---
### Requirement: 代理套餐列表开放接口
系统 SHALL 提供 `GET /api/open/v1/packages` 接口,分页返回代理可购买套餐列表。接口 MUST 复用后台代理套餐列表的权限过滤:只返回当前代理已分配且启用的非赠送套餐,并按代理自己的上下架状态过滤。接口 MUST 支持 `page``page_size``package_type``series_id``package_name` 查询参数,`page_size` 最大 100。
响应每项 MUST 包含:`package_id``package_code``package_name``series_name``package_type``package_type_name``cost_price``retail_price``total_flow_mb``valid_days``retail_price` MUST 返回代理配置的生效零售价;当代理零售价未配置时,按现有策略回退为成本价。
#### Scenario: 代理查询套餐列表
- **WHEN** 代理调用 `GET /api/open/v1/packages?page=1&page_size=20`
- **THEN** 系统分页返回该代理可购买套餐,并包含成本价和生效零售价
#### Scenario: 过滤未分配套餐
- **WHEN** 套餐未分配给当前代理
- **THEN** 该套餐不会出现在开放套餐列表中
#### Scenario: 生效零售价回退
- **WHEN** 代理套餐分配记录未配置零售价
- **THEN** 响应 `retail_price` 返回成本价
---
### Requirement: 代理钱包套餐购买开放接口
系统 SHALL 提供 `POST /api/open/v1/wallet/package-orders` 接口,允许代理使用预充值主钱包为指定卡购买套餐。请求 body MUST 包含 `card_nos``package_code``package_code` MUST 只允许一个,`card_nos` MUST 至少 1 个,最多 100 个。
系统 MUST 按卡独立创建后台钱包订单,复用后台代理钱包支付购买逻辑。每张卡购买成功后 MUST 返回订单号;购买失败时 MUST 返回该卡失败原因。任一卡失败 MUST NOT 回滚其他已成功卡的订单。
响应 data MUST 包含:`batch_no``package_code``success_count``failed_count``orders``failed_cards``orders` 每项 MUST 包含 `card_no``order_no``failed_cards` 每项 MUST 包含 `card_no``code``message`
#### Scenario: 多卡部分成功
- **WHEN** 代理提交 3 张卡购买同一个套餐编码,其中 2 张成功、1 张失败
- **THEN** 系统返回 `success_count=2``failed_count=1`、2 条订单号和 1 条失败卡原因
#### Scenario: 单套餐编码限制
- **WHEN** 请求包含多个套餐编码或套餐编码为空
- **THEN** 系统返回参数错误,不创建订单
#### Scenario: 钱包余额不足
- **WHEN** 某张卡购买套餐时代理主钱包余额不足
- **THEN** 该卡返回失败原因“余额不足”,其他卡已成功订单不受影响
#### Scenario: 复用后台钱包支付
- **WHEN** 某张卡购买套餐成功
- **THEN** 系统创建已支付订单、扣减代理主钱包余额、创建主钱包扣款流水并激活套餐
---
### Requirement: 代理预充值钱包余额开放接口
系统 SHALL 提供 `GET /api/open/v1/wallet/balance` 接口,查询当前代理预充值主钱包余额。响应 data MUST 包含 `balance``frozen_balance``available_balance``currency`。当主钱包不存在时,系统 MUST 返回 0 余额,不报错。
#### Scenario: 查询主钱包余额
- **WHEN** 代理主钱包余额为 10000 分、冻结余额为 2000 分
- **THEN** 系统返回 `balance=10000``frozen_balance=2000``available_balance=8000`
#### Scenario: 主钱包不存在
- **WHEN** 代理从未充值且主钱包不存在
- **THEN** 系统返回 0 余额和币种 CNY
---
### Requirement: 代理预充值钱包流水开放接口
系统 SHALL 提供 `GET /api/open/v1/wallet/transactions` 接口,分页返回当前代理预充值主钱包流水。接口 MUST 与后台 `GET /api/admin/shops/:shop_id/main-wallet/transactions` 的返回字段保持一致,支持 `page``page_size``transaction_type``start_date``end_date` 查询参数,结果按 `created_at DESC` 排序。
响应每项 MUST 包含:`id``transaction_type``transaction_subtype``amount``balance_before``balance_after``remark``created_at`
#### Scenario: 查询钱包流水
- **WHEN** 代理调用 `GET /api/open/v1/wallet/transactions?page=1&page_size=20`
- **THEN** 系统返回当前代理主钱包流水,字段与后台主钱包流水接口一致
#### Scenario: 按日期过滤流水
- **WHEN** 代理传入 `start_date``end_date`
- **THEN** 系统只返回该日期范围内的主钱包流水
---
### Requirement: 开放接口文档和路由注册
系统 SHALL 将代理开放接口注册到路由总入口和 OpenAPI 文档生成器。新增 Handler 后 MUST 同步更新 `cmd/api/docs.go``cmd/gendocs/main.go``bootstrap.Handlers` 初始化,确保接口文档中出现 `/api/open/v1` 下所有开放接口。
#### Scenario: 文档生成器包含开放接口
- **WHEN** 系统生成 OpenAPI 文档
- **THEN** 文档中包含 `/api/open/v1` 下全部代理开放接口