Files
junhong_cmp_fiber/openspec/specs/client-order-purchase/spec.md
huang 80c6f6c756
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
主要变更:
- 新增 AssetIdentifier 模型及 Store,统一管理资产标识符(ICCID/IMEI/SN 等)
- 新增迁移:asset_identifier 表、order 表新增 asset_identifier 字段、iot_card.virtual_no NOT NULL 约束
- 资产 Handler/Service/Route 全面重构,支持标识符路由查询与解析
- 新增资产历史订单查询接口,支持跨设备/卡/钱包维度的订单聚合
- 设备与物联卡导入任务强制校验虚拟号,缺失时直接拒绝
- Excel 工具函数优化,前端导入指引文档同步更新
- 归档三个 OpenSpec 提案:asset-identifier-standardization、asset-historical-orders、import-mandatory-virtual-no
- 更新 OpenAPI 文档及相关 DTO

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-07 17:39:36 +08:00

94 lines
5.9 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.
# Capability: 客户端套餐购买
## ADDED Requirements
### Requirement: D1 创建套餐购买订单接口
系统 SHALL 提供 `POST /api/c/v1/orders/create`,并且 MUST 要求个人客户认证。**[BREAKING]** 请求体改为使用资产标识符identifier废弃 `iot_card_id`/`device_id` 主键字段和 `order_type` 字段。请求体 MUST 包含 `identifier``package_ids[]``payment_method`。接口流程 MUST 按顺序执行:归属校验 → 套餐校验(含加油包前置)→ 实名校验 → OpenID 查询 → 幂等检查 → 强充检查 → 分流创建。实名不满足时 MUST 返回 `NEED_REALNAME`。OpenID 缺失时 MUST 返回 `OPENID_NOT_FOUND`。幂等 MUST 使用 Redis 业务键 + 分布式锁。分流规则 MUST 为:
- 无强充:创建套餐订单并返回 `order_type="package"``order``pay_config`
- 需强充:创建充值单并返回 `order_type="recharge"``recharge``pay_config``linked_package_info`
**客户端订单创建请求CreateOrderRequest**
```json
{
"identifier": "string资产标识符ICCID 或 VirtualNo必填1-50字符",
"package_ids": "[uint](套餐 ID 列表必填1-10 个)",
"payment_method": "stringwallet|wechat|alipay必填"
}
```
**废弃字段**
- `iot_card_id`(原单卡购买时必填)
- `device_id`(原设备购买时必填)
- `order_type`(改为系统根据 identifier 解析结果自动填入)
响应体 MUST 包含前端可直接渲染字段。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误``FORBIDDEN/无权限操作该资产或资源不存在``NEED_REALNAME/该套餐需实名认证后购买``OPENID_NOT_FOUND/未找到微信授权信息,请先完成授权``IDEMPOTENT_CONFLICT/请求处理中,请勿重复提交``PACKAGE_NOT_AVAILABLE/套餐不可购买`
#### Scenario: 命中强充返回 recharge 结构
- **WHEN** 客户购买套餐触发强充要求
- **THEN** 系统返回 `order_type="recharge"`,包含充值单与关联套餐信息
#### Scenario: 个人客户使用 VirtualNo 购买设备套餐
- **WHEN** 个人客户发送 `{ identifier: "DEV-001", package_ids: [5], payment_method: "wechat" }`
- **THEN** 系统解析 identifier 为设备,创建设备购买订单,微信支付流程正常触发
#### Scenario: 个人客户使用 ICCID 购买单卡套餐
- **WHEN** 个人客户发送 `{ identifier: "898600XXXXX", package_ids: [3], payment_method: "wallet" }`
- **THEN** 系统解析为独立 IoT 卡(`is_standalone = true`),创建单卡购买订单
#### Scenario: 个人客户尝试为绑定设备的卡购买套餐
- **WHEN** 个人客户发送 identifier 对应一张 `is_standalone = false` 的卡
- **THEN** 系统返回错误"该卡已绑定设备,请前往设备页面购买套餐"
#### Scenario: 个人客户只能操作自己绑定的资产
- **WHEN** 个人客户发送的 identifier 对应的资产不属于该客户
- **THEN** 系统返回 HTTP 403"无权限操作该资源"
---
### Requirement: D2 套餐订单列表接口
系统 SHALL 提供 `GET /api/c/v1/orders?identifier=xxx`,并且 MUST 要求个人客户认证。接口 MUST 做归属校验并按资产当前 generation 过滤订单。请求参数 SHALL 支持 `payment_status``page``page_size`。响应体 SHALL 返回 `list[]``total``page``page_size`,列表项至少含 `order_id``order_no``total_amount``payment_status``created_at`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误``FORBIDDEN/无权限操作该资产或资源不存在`
#### Scenario: 支持支付状态筛选
- **WHEN** 客户带 `payment_status=paid` 查询订单
- **THEN** 系统仅返回当前 generation 且支付状态匹配的订单
---
### Requirement: D3 套餐订单详情接口
系统 SHALL 提供 `GET /api/c/v1/orders/:id`,并且 MUST 要求个人客户认证。接口 MUST 基于订单关联资产执行归属校验(通过资产虚拟号匹配 `PersonalCustomerDevice`)。响应体 SHALL 返回订单详情、套餐明细、支付信息、状态流转时间。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误``FORBIDDEN/无权限操作该资产或资源不存在``ORDER_NOT_FOUND/订单不存在`
#### Scenario: 查询他人订单被拦截
- **WHEN** 客户请求不属于本人资产的订单详情
- **THEN** 系统返回 403错误消息为无权限操作该资产或资源不存在
---
### Requirement: AutoPurchaseAfterRecharge 异步任务
系统 SHALL 增加 `AutoPurchaseAfterRecharge` Asynq 任务处理强充二阶段。任务输入 MUST 包含 `recharge_record_id`。处理流程 MUST 为:从钱包扣款(`payment_method=wallet`)→ 创建套餐订单(`source="client"`、写入当前 generation→ 激活套餐。任务失败 MUST 自动重试,最大 3 次。全部失败后 MUST 将 `auto_purchase_status` 标记为 `failed`,并保留钱包余额供用户手动购买。成功时 MUST 标记为 `success`
#### Scenario: 异步任务连续失败
- **WHEN** AutoPurchaseAfterRecharge 连续执行失败且达到最大重试次数
- **THEN** 系统将充值记录 `auto_purchase_status` 更新为 `failed`
---
## MODIFIED Requirements
### Requirement: C端支付状态枚举统一
C端订单查询接口返回的 `payment_status` SHALL 直接使用管理端统一枚举值1=待支付, 2=已支付, 3=已取消, 4=已退款),不再通过映射函数转换为 0/1/2。
#### Scenario: C端查询订单返回统一枚举
- **WHEN** C端客户查询订单列表或订单详情
- **THEN** 返回的 `payment_status` SHALL 为 1/2/3/4 之一,与管理端一致
## REMOVED Requirements
### Requirement: C端支付状态映射
**Reason**: `orderStatusToClientStatus()` 函数将管理端 1/2/3/4 映射为 C端 0/1/2造成前后端枚举不一致增加维护负担。
**Migration**: C端前端需更新支付状态枚举解析1=待支付, 2=已支付, 3=已取消, 4=已退款。