This commit is contained in:
@@ -2,10 +2,14 @@
|
||||
|
||||
### Requirement: 订单支付后触发佣金计算
|
||||
|
||||
系统 SHALL 在订单支付成功后自动触发佣金计算。计算通过异步任务执行。
|
||||
系统 SHALL 在订单支付成功后自动触发佣金计算。计算通过异步任务执行。代购订单和普通订单的佣金计算逻辑不同。
|
||||
|
||||
#### Scenario: 支付成功触发计算
|
||||
- **WHEN** 订单支付状态变为已支付
|
||||
#### Scenario: 普通订单支付成功触发计算
|
||||
- **WHEN** 普通订单(is_purchase_on_behalf = false)支付状态变为已支付
|
||||
- **THEN** 系统发送佣金计算异步任务
|
||||
|
||||
#### Scenario: 代购订单支付成功触发计算
|
||||
- **WHEN** 代购订单(is_purchase_on_behalf = true)创建成功(自动已支付)
|
||||
- **THEN** 系统发送佣金计算异步任务
|
||||
|
||||
#### Scenario: 重复支付不重复计算
|
||||
@@ -48,35 +52,36 @@
|
||||
|
||||
### Requirement: 更新累计充值金额
|
||||
|
||||
订单支付成功后系统 SHALL 更新卡/设备的累计充值金额。
|
||||
订单支付成功后系统 SHALL 更新卡/设备的累计充值金额,但代购订单除外。
|
||||
|
||||
**关键修复**:每次支付成功都必须写回累计充值金额,确保累计值能正确用于一次性佣金的累计触发判断。
|
||||
**关键修复**:每次真实充值(个人客户充值或购买套餐)都必须写回累计充值金额,代购订单不更新。
|
||||
|
||||
#### Scenario: 单卡订单更新累计充值
|
||||
|
||||
- **WHEN** 单卡订单支付成功,金额 100 元
|
||||
#### Scenario: 普通单卡订单更新累计充值
|
||||
- **WHEN** 普通单卡订单(is_purchase_on_behalf = false)支付成功,金额 100 元
|
||||
- **THEN** 系统读取 IotCard.accumulated_recharge 当前值
|
||||
- **AND** 增加 10000 分(100 元 = 10000 分)
|
||||
- **AND** 将新值写回 IotCard.accumulated_recharge
|
||||
- **AND** 使用更新后的累计值判断是否触发一次性佣金
|
||||
|
||||
#### Scenario: 设备订单更新累计充值
|
||||
|
||||
- **WHEN** 设备订单支付成功,金额 300 元
|
||||
#### Scenario: 普通设备订单更新累计充值
|
||||
- **WHEN** 普通设备订单(is_purchase_on_behalf = false)支付成功,金额 300 元
|
||||
- **THEN** 系统读取 Device.accumulated_recharge 当前值
|
||||
- **AND** 增加 30000 分(300 元 = 30000 分)
|
||||
- **AND** 将新值写回 Device.accumulated_recharge
|
||||
- **AND** 使用更新后的累计值判断是否触发一次性佣金
|
||||
|
||||
#### Scenario: 累计充值更新使用原子操作
|
||||
#### Scenario: 代购订单不更新累计充值
|
||||
- **WHEN** 代购订单(is_purchase_on_behalf = true)完成,金额 100 元
|
||||
- **THEN** 系统不更新卡/设备的 accumulated_recharge 字段
|
||||
- **AND** accumulated_recharge 保持原值
|
||||
|
||||
#### Scenario: 累计充值更新使用原子操作
|
||||
- **WHEN** 更新累计充值金额
|
||||
- **THEN** 系统使用 SQL 原子操作(如 `accumulated_recharge = accumulated_recharge + ?`)
|
||||
- **OR** 使用 GORM 乐观锁(version 字段)
|
||||
- **AND** 确保并发场景下累计值不会丢失
|
||||
|
||||
#### Scenario: 更新失败不影响佣金计算
|
||||
|
||||
- **WHEN** 累计充值金额更新失败(数据库错误、并发冲突等)
|
||||
- **THEN** 系统记录错误日志
|
||||
- **AND** 继续执行后续的佣金计算流程(成本价差、一次性佣金等)
|
||||
@@ -140,3 +145,48 @@
|
||||
|
||||
- **WHEN** 累计充值金额更新失败
|
||||
- **THEN** 系统在日志中记录:订单 ID、资源 ID、失败原因(错误信息)、重试次数(如适用)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单佣金计算规则
|
||||
|
||||
代购订单 SHALL 计算差价佣金,但不触发一次性佣金。
|
||||
|
||||
#### Scenario: 代购订单计算差价佣金
|
||||
- **WHEN** 代购订单(is_purchase_on_behalf = true)完成,买家有上级代理
|
||||
- **THEN** 系统计算差价佣金(买家成本价 - 上级成本价),发放给上级代理链
|
||||
|
||||
#### Scenario: 代购订单不触发一次性佣金
|
||||
- **WHEN** 代购订单完成,佣金计算时检查订单类型
|
||||
- **THEN** 系统跳过一次性佣金判断逻辑,不发放一次性佣金
|
||||
|
||||
#### Scenario: 代购订单示例
|
||||
- **WHEN** 平台为三级代理代购,订单金额 100 元(三级成本价),各级成本价:一级 60 → 二级 70 → 三级 80
|
||||
- **THEN** 二级获得 10 元(80 - 70)差价佣金,一级获得 10 元(70 - 60)差价佣金
|
||||
- **AND** 三级、二级、一级都不获得一次性佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包充值触发一次性佣金
|
||||
|
||||
钱包充值成功后 SHALL 更新累计充值,并检查是否触发一次性佣金。
|
||||
|
||||
#### Scenario: 充值成功更新累计充值
|
||||
- **WHEN** 卡钱包充值 100 元成功,当前累计充值 200 元
|
||||
- **THEN** 系统更新卡的 accumulated_recharge 为 300 元
|
||||
|
||||
#### Scenario: 充值达到首次充值阈值
|
||||
- **WHEN** 卡配置为首次充值触发,阈值 100 元,充值 100 元成功,未发放过佣金
|
||||
- **THEN** 系统触发一次性佣金计算,发放佣金,标记 first_commission_paid = true
|
||||
|
||||
#### Scenario: 充值达到累计充值阈值
|
||||
- **WHEN** 卡配置为累计充值触发,阈值 1000 元,充值后累计达到 1000 元,未发放过佣金
|
||||
- **THEN** 系统触发一次性佣金计算,发放佣金,标记 first_commission_paid = true
|
||||
|
||||
#### Scenario: 充值未达阈值不触发
|
||||
- **WHEN** 充值后累计充值未达到阈值
|
||||
- **THEN** 系统不触发一次性佣金计算
|
||||
|
||||
#### Scenario: 已发放过不重复触发
|
||||
- **WHEN** 卡的一次性佣金已发放过(first_commission_paid = true)
|
||||
- **THEN** 系统不触发一次性佣金计算
|
||||
|
||||
115
openspec/specs/force-recharge-check/spec.md
Normal file
115
openspec/specs/force-recharge-check/spec.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Capability: 强充预检
|
||||
|
||||
## Purpose
|
||||
|
||||
本 capability 定义强充预检接口,在充值或购买套餐前返回强充要求、允许的充值金额等信息,帮助前端正确引导用户完成支付。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 钱包充值预检
|
||||
|
||||
系统 SHALL 提供钱包充值预检接口,返回强充要求、允许的充值金额等信息。
|
||||
|
||||
#### Scenario: 无强充要求
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡配置为累计充值触发且未启用强充
|
||||
- **THEN** 系统返回 need_force_recharge = false,min_amount = 100(1元),max_amount = null
|
||||
|
||||
#### Scenario: 首次充值强充
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡配置为首次充值触发,阈值 10000 分(100元),未发放佣金
|
||||
- **THEN** 系统返回 need_force_recharge = true,force_recharge_amount = 10000,trigger_type = "single_recharge",message = "首次充值需充值100元"
|
||||
|
||||
#### Scenario: 累计充值启用强充
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡配置为累计充值触发,启用强充,强充金额 10000 分(100元)
|
||||
- **THEN** 系统返回 need_force_recharge = true,force_recharge_amount = 10000,trigger_type = "accumulated_recharge",message = "每次充值需充值100元"
|
||||
|
||||
#### Scenario: 一次性佣金已发放
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡的一次性佣金已发放过
|
||||
- **THEN** 系统返回 need_force_recharge = false(不再强充)
|
||||
|
||||
#### Scenario: 未启用一次性佣金
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡关联系列未启用一次性佣金
|
||||
- **THEN** 系统返回 need_force_recharge = false
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 套餐购买预检
|
||||
|
||||
系统 SHALL 提供套餐购买预检接口,计算实际支付金额、钱包到账金额等信息。
|
||||
|
||||
#### Scenario: 无强充要求正常购买
|
||||
- **WHEN** 客户购买 90 元套餐,无强充要求
|
||||
- **THEN** 系统返回 total_package_amount = 9000,need_force_recharge = false,actual_payment = 9000,wallet_credit = 0
|
||||
|
||||
#### Scenario: 首次充值强充,套餐价低于阈值
|
||||
- **WHEN** 客户购买 90 元套餐,首次充值阈值 100 元
|
||||
- **THEN** 系统返回 total_package_amount = 9000,need_force_recharge = true,force_recharge_amount = 10000,actual_payment = 10000,wallet_credit = 1000,message = "需充值100元,购买套餐后余额10元"
|
||||
|
||||
#### Scenario: 首次充值强充,套餐价高于阈值
|
||||
- **WHEN** 客户购买 150 元套餐,首次充值阈值 100 元
|
||||
- **THEN** 系统返回 total_package_amount = 15000,need_force_recharge = true,force_recharge_amount = 10000,actual_payment = 15000,wallet_credit = 0,message = "套餐总价150元,无需额外充值"
|
||||
|
||||
#### Scenario: 首次充值强充,套餐价等于阈值
|
||||
- **WHEN** 客户购买 100 元套餐,首次充值阈值 100 元
|
||||
- **THEN** 系统返回 total_package_amount = 10000,need_force_recharge = true,force_recharge_amount = 10000,actual_payment = 10000,wallet_credit = 0
|
||||
|
||||
#### Scenario: 累计充值启用强充,套餐价低于强充金额
|
||||
- **WHEN** 客户购买 50 元套餐,累计充值启用强充,强充金额 100 元
|
||||
- **THEN** 系统返回 actual_payment = 10000,wallet_credit = 5000,message = "需充值100元,购买套餐后余额50元"
|
||||
|
||||
#### Scenario: 累计充值启用强充,套餐价高于强充金额
|
||||
- **WHEN** 客户购买 150 元套餐,累计充值启用强充,强充金额 100 元
|
||||
- **THEN** 系统返回 actual_payment = 15000,wallet_credit = 0,message = "套餐总价150元,无需额外充值"
|
||||
|
||||
#### Scenario: 购买多个套餐
|
||||
- **WHEN** 客户购买 3 个套餐,总价 120 元,首次充值阈值 100 元
|
||||
- **THEN** 系统返回 total_package_amount = 12000,actual_payment = 12000,wallet_credit = 0
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 预检接口响应格式
|
||||
|
||||
预检接口响应 SHALL 包含完整的充值/购买指引信息。
|
||||
|
||||
#### Scenario: 充值预检响应字段
|
||||
- **WHEN** 调用钱包充值预检接口
|
||||
- **THEN** 响应包含:need_force_recharge, force_recharge_amount, trigger_type, min_amount, max_amount, current_accumulated, threshold, message
|
||||
|
||||
#### Scenario: 购买预检响应字段
|
||||
- **WHEN** 调用套餐购买预检接口
|
||||
- **THEN** 响应包含:total_package_amount, need_force_recharge, force_recharge_amount, actual_payment, wallet_credit, message
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 预检接口性能
|
||||
|
||||
预检接口响应时间 MUST 小于 100ms。
|
||||
|
||||
#### Scenario: 快速响应
|
||||
- **WHEN** 调用预检接口
|
||||
- **THEN** 系统在 100ms 内返回结果
|
||||
|
||||
#### Scenario: 缓存系列分配配置
|
||||
- **WHEN** 频繁查询同一卡的预检信息
|
||||
- **THEN** 系统可以缓存系列分配配置,减少数据库查询
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 预检接口错误处理
|
||||
|
||||
预检接口 SHALL 正确处理异常情况。
|
||||
|
||||
#### Scenario: 卡不存在
|
||||
- **WHEN** 查询不存在的卡的充值预检
|
||||
- **THEN** 系统返回错误 "卡不存在"
|
||||
|
||||
#### Scenario: 卡未关联系列
|
||||
- **WHEN** 查询未关联套餐系列的卡的充值预检
|
||||
- **THEN** 系统返回 need_force_recharge = false(无系列分配,无强充要求)
|
||||
|
||||
#### Scenario: 设备不存在
|
||||
- **WHEN** 查询不存在的设备的充值预检
|
||||
- **THEN** 系统返回错误 "设备不存在"
|
||||
|
||||
#### Scenario: 套餐不存在
|
||||
- **WHEN** 套餐购买预检时,套餐 ID 不存在
|
||||
- **THEN** 系统返回错误 "套餐不存在"
|
||||
220
openspec/specs/gateway-client/spec.md
Normal file
220
openspec/specs/gateway-client/spec.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Gateway Client Specification
|
||||
|
||||
Gateway API 统一客户端,提供 14 个接口的类型安全封装。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Gateway 客户端结构
|
||||
|
||||
系统 SHALL 提供 `gateway.Client` 结构体,封装所有 Gateway API 调用。
|
||||
|
||||
客户端字段:
|
||||
- `baseURL string` - Gateway API 基础 URL
|
||||
- `appID string` - 应用 ID
|
||||
- `appSecret string` - 应用密钥
|
||||
- `httpClient *http.Client` - HTTP 客户端(支持连接复用)
|
||||
- `timeout time.Duration` - 请求超时时间
|
||||
|
||||
#### Scenario: 创建 Gateway 客户端
|
||||
|
||||
- **WHEN** 调用 `gateway.NewClient(baseURL, appID, appSecret)`
|
||||
- **THEN** 返回已初始化的 `Client` 实例
|
||||
- **AND** HTTP 客户端配置正确(支持 Keep-Alive)
|
||||
|
||||
#### Scenario: 配置超时时间
|
||||
|
||||
- **WHEN** 调用 `client.WithTimeout(30 * time.Second)`
|
||||
- **THEN** 客户端的 `timeout` 字段更新为 30 秒
|
||||
- **AND** 返回客户端自身(支持链式调用)
|
||||
|
||||
### Requirement: 统一请求方法
|
||||
|
||||
系统 SHALL 提供 `doRequest` 方法,统一处理加密、签名、HTTP 请求和响应解析。
|
||||
|
||||
#### Scenario: 成功的 API 调用
|
||||
|
||||
- **WHEN** 调用 `doRequest(ctx, "/flow-card/status", businessData)`
|
||||
- **THEN** 业务数据使用 AES-128-ECB 加密
|
||||
- **AND** 请求使用 MD5 签名
|
||||
- **AND** HTTP POST 发送到 `{baseURL}/flow-card/status`
|
||||
- **AND** 响应中的 `data` 字段解密并返回
|
||||
|
||||
#### Scenario: 网络错误
|
||||
|
||||
- **WHEN** HTTP 请求失败(网络中断、DNS 解析失败)
|
||||
- **THEN** 返回 `CodeGatewayError` 错误
|
||||
- **AND** 错误信息包含原始网络错误
|
||||
|
||||
#### Scenario: 请求超时
|
||||
|
||||
- **WHEN** HTTP 请求超过配置的超时时间
|
||||
- **THEN** 返回 `CodeGatewayTimeout` 错误
|
||||
- **AND** Context 超时错误被正确识别
|
||||
|
||||
#### Scenario: 响应格式错误
|
||||
|
||||
- **WHEN** Gateway 响应无法解析为 JSON
|
||||
- **THEN** 返回 `CodeGatewayInvalidResp` 错误
|
||||
- **AND** 错误信息包含原始响应内容(限制 200 字符)
|
||||
|
||||
#### Scenario: Gateway 业务错误
|
||||
|
||||
- **WHEN** Gateway 响应中 `code != 200`
|
||||
- **THEN** 返回 `CodeGatewayError` 错误
|
||||
- **AND** 错误信息包含 Gateway 的 code 和 msg
|
||||
|
||||
### Requirement: 流量卡 API 封装
|
||||
|
||||
系统 SHALL 提供 7 个流量卡相关的 API 方法。
|
||||
|
||||
#### Scenario: 查询流量卡状态
|
||||
|
||||
- **WHEN** 调用 `client.QueryCardStatus(ctx, &CardStatusReq{CardNo: "898608070422D0010269"})`
|
||||
- **THEN** 返回 `CardStatusResp` 包含 ICCID 和卡状态
|
||||
- **AND** 卡状态为:"准备"、"正常" 或 "停机" 之一
|
||||
|
||||
#### Scenario: 查询流量使用
|
||||
|
||||
- **WHEN** 调用 `client.QueryFlow(ctx, &FlowQueryReq{CardNo: "898608070422D0010269"})`
|
||||
- **THEN** 返回 `FlowUsageResp` 包含已用流量和单位
|
||||
- **AND** 流量单位为 "MB"
|
||||
|
||||
#### Scenario: 查询实名认证状态
|
||||
|
||||
- **WHEN** 调用 `client.QueryRealnameStatus(ctx, &CardStatusReq{CardNo: "898608070422D0010269"})`
|
||||
- **THEN** 返回实名认证状态信息
|
||||
|
||||
#### Scenario: 流量卡停机
|
||||
|
||||
- **WHEN** 调用 `client.StopCard(ctx, &CardOperationReq{CardNo: "898608070422D0010269"})`
|
||||
- **THEN** Gateway 执行停机操作
|
||||
- **AND** 方法返回 nil(成功)或错误
|
||||
|
||||
#### Scenario: 流量卡复机
|
||||
|
||||
- **WHEN** 调用 `client.StartCard(ctx, &CardOperationReq{CardNo: "898608070422D0010269"})`
|
||||
- **THEN** Gateway 执行复机操作
|
||||
- **AND** 方法返回 nil(成功)或错误
|
||||
|
||||
#### Scenario: 获取实名认证链接
|
||||
|
||||
- **WHEN** 调用 `client.GetRealnameLink(ctx, &CardStatusReq{CardNo: "898608070422D0010269"})`
|
||||
- **THEN** 返回实名认证跳转链接
|
||||
- **AND** 链接格式为有效的 HTTPS URL
|
||||
|
||||
#### Scenario: 广电国网扩展参数
|
||||
|
||||
- **WHEN** 停机/复机请求中 `Extend` 字段不为空
|
||||
- **THEN** 请求包含 `extend` 参数
|
||||
- **AND** Gateway 正确处理广电国网特殊逻辑
|
||||
|
||||
### Requirement: 设备 API 封装
|
||||
|
||||
系统 SHALL 提供 7 个设备相关的 API 方法。
|
||||
|
||||
#### Scenario: 查询设备信息
|
||||
|
||||
- **WHEN** 调用 `client.GetDeviceInfo(ctx, &DeviceInfoReq{CardNo: "898608070422D0010269"})`
|
||||
- **THEN** 返回 `DeviceInfoResp` 包含设备详细信息
|
||||
- **AND** 信息包括:IMEI、在线状态、信号强度、WiFi 配置、速率等
|
||||
|
||||
#### Scenario: 通过设备 ID 查询
|
||||
|
||||
- **WHEN** 调用 `client.GetDeviceInfo(ctx, &DeviceInfoReq{DeviceID: "868123456789012"})`
|
||||
- **THEN** 通过设备 IMEI 查询设备信息
|
||||
- **AND** 返回结果与通过卡号查询一致
|
||||
|
||||
#### Scenario: 查询设备卡槽信息
|
||||
|
||||
- **WHEN** 调用 `client.GetSlotInfo(ctx, &DeviceInfoReq{CardNo: "898608070422D0010269"})`
|
||||
- **THEN** 返回设备中已安装的物联网卡信息
|
||||
|
||||
#### Scenario: 设置设备限速
|
||||
|
||||
- **WHEN** 调用 `client.SetSpeedLimit(ctx, &SpeedLimitReq{DeviceID: "868123456789012", UploadSpeed: 1024, DownloadSpeed: 2048})`
|
||||
- **THEN** 设备上下行速率设置为指定值(KB/s)
|
||||
|
||||
#### Scenario: 设置设备 WiFi
|
||||
|
||||
- **WHEN** 调用 `client.SetWiFi(ctx, &WiFiReq{DeviceID: "868123456789012", SSID: "MyWiFi", Password: "12345678", Enabled: true})`
|
||||
- **THEN** 设备 WiFi 配置更新
|
||||
- **AND** WiFi 名称、密码和启用状态正确设置
|
||||
|
||||
#### Scenario: 设备切换卡
|
||||
|
||||
- **WHEN** 调用 `client.SwitchCard(ctx, &SwitchCardReq{DeviceID: "868123456789012", TargetICCID: "898608070422D0010270"})`
|
||||
- **THEN** 多卡设备切换到目标 ICCID
|
||||
|
||||
#### Scenario: 设备恢复出厂设置
|
||||
|
||||
- **WHEN** 调用 `client.ResetDevice(ctx, &DeviceOperationReq{DeviceID: "868123456789012"})`
|
||||
- **THEN** 设备恢复为出厂状态
|
||||
|
||||
#### Scenario: 设备重启
|
||||
|
||||
- **WHEN** 调用 `client.RebootDevice(ctx, &DeviceOperationReq{DeviceID: "868123456789012"})`
|
||||
- **THEN** 设备执行重启操作
|
||||
|
||||
### Requirement: 类型安全的 DTO
|
||||
|
||||
系统 SHALL 为所有请求和响应定义类型安全的结构体。
|
||||
|
||||
#### Scenario: 请求 DTO 包含验证标签
|
||||
|
||||
- **WHEN** 定义 `CardStatusReq` 结构体
|
||||
- **THEN** `CardNo` 字段包含 `validate:"required"` 标签
|
||||
- **AND** 可以使用 Validator 库进行验证
|
||||
|
||||
#### Scenario: 响应 DTO 正确解析
|
||||
|
||||
- **WHEN** Gateway 返回 JSON 响应
|
||||
- **THEN** `CardStatusResp` 结构体正确解析 `iccid`、`cardStatus`、`extend` 字段
|
||||
- **AND** 字段类型与 Gateway 文档一致
|
||||
|
||||
### Requirement: 并发安全
|
||||
|
||||
系统 SHALL 确保 `Client` 结构体可以安全地并发调用。
|
||||
|
||||
#### Scenario: 多个 Goroutine 并发调用
|
||||
|
||||
- **WHEN** 10 个 Goroutine 同时调用 `client.QueryCardStatus`
|
||||
- **THEN** 所有请求都正确执行
|
||||
- **AND** 不发生 race condition
|
||||
|
||||
#### Scenario: HTTP 连接复用
|
||||
|
||||
- **WHEN** 多次调用相同的 Gateway API
|
||||
- **THEN** HTTP 客户端复用 TCP 连接
|
||||
- **AND** 减少连接建立开销
|
||||
|
||||
### Requirement: 错误处理一致性
|
||||
|
||||
系统 SHALL 使用项目统一的错误码系统。
|
||||
|
||||
#### Scenario: Gateway 错误返回统一错误码
|
||||
|
||||
- **WHEN** Gateway API 调用失败
|
||||
- **THEN** 返回 `errors.AppError` 类型
|
||||
- **AND** 错误码为 `CodeGatewayError`、`CodeGatewayTimeout` 等之一
|
||||
|
||||
#### Scenario: 错误包含上下文信息
|
||||
|
||||
- **WHEN** 加密失败
|
||||
- **THEN** 错误信息为 "数据加密失败"
|
||||
- **AND** 包含底层错误的详细信息
|
||||
|
||||
### Requirement: Context 支持
|
||||
|
||||
系统 SHALL 支持通过 Context 控制请求超时和取消。
|
||||
|
||||
#### Scenario: 使用 Context 控制超时
|
||||
|
||||
- **WHEN** 调用 `client.QueryCardStatus(ctx, req)` 且 ctx 设置了 30 秒超时
|
||||
- **THEN** 请求在 30 秒后自动超时
|
||||
- **AND** 返回 `CodeGatewayTimeout` 错误
|
||||
|
||||
#### Scenario: 取消请求
|
||||
|
||||
- **WHEN** 调用 `client.QueryCardStatus(ctx, req)` 且 ctx 被取消
|
||||
- **THEN** 请求立即停止
|
||||
- **AND** 返回 context canceled 错误
|
||||
175
openspec/specs/gateway-config/spec.md
Normal file
175
openspec/specs/gateway-config/spec.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Gateway Config Specification
|
||||
|
||||
Gateway API 的配置集成规范,定义配置结构和加载方式。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Gateway 配置结构
|
||||
|
||||
系统 SHALL 在 `pkg/config/config.go` 中添加 `GatewayConfig` 结构体。
|
||||
|
||||
配置字段:
|
||||
- `BaseURL string` - Gateway API 基础 URL
|
||||
- `AppID string` - 应用 ID
|
||||
- `AppSecret string` - 应用密钥
|
||||
- `Timeout int` - 请求超时时间(秒)
|
||||
|
||||
#### Scenario: 配置结构定义
|
||||
|
||||
- **WHEN** 定义 `GatewayConfig` 结构体
|
||||
- **THEN** 包含 `mapstructure` 标签用于 Viper 解析
|
||||
- **AND** 字段名使用 snake_case(如 `base_url`、`app_id`)
|
||||
|
||||
#### Scenario: 集成到主配置
|
||||
|
||||
- **WHEN** 在 `Config` 结构体中添加 `Gateway GatewayConfig` 字段
|
||||
- **THEN** 使用 `mapstructure:"gateway"` 标签
|
||||
- **AND** 配置可通过 `config.Get().Gateway` 访问
|
||||
|
||||
### Requirement: 默认配置嵌入
|
||||
|
||||
系统 SHALL 在 `pkg/config/defaults/config.yaml` 中添加 Gateway 默认配置。
|
||||
|
||||
#### Scenario: 嵌入默认配置
|
||||
|
||||
- **WHEN** 读取嵌入的默认配置文件
|
||||
- **THEN** 包含 `gateway` 配置节
|
||||
- **AND** 配置包含:
|
||||
```yaml
|
||||
gateway:
|
||||
base_url: "https://lplan.whjhft.com/openapi"
|
||||
app_id: "60bgt1X8i7AvXqkd"
|
||||
app_secret: "BZeQttaZQt0i73moF"
|
||||
timeout: 30
|
||||
```
|
||||
|
||||
### Requirement: 环境变量覆盖
|
||||
|
||||
系统 SHALL 支持通过环境变量覆盖 Gateway 配置。
|
||||
|
||||
环境变量格式:`JUNHONG_GATEWAY_{KEY}`
|
||||
|
||||
#### Scenario: 覆盖 BaseURL
|
||||
|
||||
- **WHEN** 设置环境变量 `JUNHONG_GATEWAY_BASE_URL=https://test.example.com`
|
||||
- **THEN** `config.Gateway.BaseURL` 的值为 "https://test.example.com"
|
||||
- **AND** 覆盖嵌入配置中的默认值
|
||||
|
||||
#### Scenario: 覆盖 AppID
|
||||
|
||||
- **WHEN** 设置环境变量 `JUNHONG_GATEWAY_APP_ID=test_app_id`
|
||||
- **THEN** `config.Gateway.AppID` 的值为 "test_app_id"
|
||||
|
||||
#### Scenario: 覆盖 AppSecret
|
||||
|
||||
- **WHEN** 设置环境变量 `JUNHONG_GATEWAY_APP_SECRET=test_secret`
|
||||
- **THEN** `config.Gateway.AppSecret` 的值为 "test_secret"
|
||||
|
||||
#### Scenario: 覆盖 Timeout
|
||||
|
||||
- **WHEN** 设置环境变量 `JUNHONG_GATEWAY_TIMEOUT=60`
|
||||
- **THEN** `config.Gateway.Timeout` 的值为 60
|
||||
|
||||
### Requirement: 配置验证
|
||||
|
||||
系统 SHALL 在配置加载后验证 Gateway 配置的有效性。
|
||||
|
||||
#### Scenario: 必填字段验证
|
||||
|
||||
- **WHEN** 配置加载完成
|
||||
- **THEN** 验证 `BaseURL`、`AppID`、`AppSecret` 不为空
|
||||
- **AND** 如果为空,返回明确的错误信息
|
||||
|
||||
#### Scenario: BaseURL 格式验证
|
||||
|
||||
- **WHEN** 验证 `BaseURL` 字段
|
||||
- **THEN** 必须以 `http://` 或 `https://` 开头
|
||||
- **AND** 不能以 `/` 结尾
|
||||
|
||||
#### Scenario: Timeout 范围验证
|
||||
|
||||
- **WHEN** 验证 `Timeout` 字段
|
||||
- **THEN** 值必须在 5 到 300 秒之间
|
||||
- **AND** 如果超出范围,返回验证错误
|
||||
|
||||
#### Scenario: AppID 格式验证
|
||||
|
||||
- **WHEN** 验证 `AppID` 字段
|
||||
- **THEN** 长度必须 > 0
|
||||
- **AND** 不包含特殊字符(仅允许字母、数字、下划线)
|
||||
|
||||
### Requirement: 敏感配置处理
|
||||
|
||||
系统 SHALL 确保 `AppSecret` 不记录到日志中。
|
||||
|
||||
#### Scenario: 配置日志脱敏
|
||||
|
||||
- **WHEN** 记录配置加载成功的日志
|
||||
- **THEN** `AppSecret` 字段显示为 "***"
|
||||
- **AND** 实际值不出现在日志中
|
||||
|
||||
#### Scenario: 错误日志脱敏
|
||||
|
||||
- **WHEN** 配置验证失败并记录错误日志
|
||||
- **THEN** `AppSecret` 字段显示为 "***"
|
||||
|
||||
### Requirement: Gateway 客户端初始化
|
||||
|
||||
系统 SHALL 在 `internal/bootstrap/bootstrap.go` 中初始化 Gateway 客户端。
|
||||
|
||||
#### Scenario: Bootstrap 中初始化
|
||||
|
||||
- **WHEN** 调用 `bootstrap.Bootstrap(deps)`
|
||||
- **THEN** 从 `deps.Config.Gateway` 读取配置
|
||||
- **AND** 调用 `gateway.NewClient(baseURL, appID, appSecret).WithTimeout(...)`
|
||||
- **AND** 将客户端赋值给 `deps.GatewayClient`
|
||||
|
||||
#### Scenario: 配置错误时启动失败
|
||||
|
||||
- **WHEN** Gateway 配置验证失败
|
||||
- **THEN** `bootstrap.Bootstrap` 返回错误
|
||||
- **AND** 应用启动失败
|
||||
|
||||
### Requirement: 多环境配置支持
|
||||
|
||||
系统 SHALL 支持通过环境变量切换不同环境的 Gateway 配置。
|
||||
|
||||
#### Scenario: 开发环境配置
|
||||
|
||||
- **WHEN** 使用默认嵌入配置(未设置环境变量)
|
||||
- **THEN** 使用生产环境的 Gateway URL 和凭证
|
||||
|
||||
#### Scenario: 测试环境配置
|
||||
|
||||
- **WHEN** 设置环境变量指向测试 Gateway
|
||||
- **AND** `JUNHONG_GATEWAY_BASE_URL=https://test-gateway.example.com`
|
||||
- **AND** `JUNHONG_GATEWAY_APP_ID=test_app_id`
|
||||
- **THEN** 客户端连接到测试环境
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Config 结构体扩展
|
||||
|
||||
系统 SHALL 在现有的 `Config` 结构体中添加 `Gateway` 字段。
|
||||
|
||||
#### Scenario: 配置结构兼容性
|
||||
|
||||
- **WHEN** 添加 `Gateway GatewayConfig` 字段
|
||||
- **THEN** 不影响现有配置字段的加载
|
||||
- **AND** 现有配置(Server、Database、Redis 等)继续正常工作
|
||||
|
||||
### Requirement: Dependencies 结构体扩展
|
||||
|
||||
系统 SHALL 在 `internal/bootstrap/bootstrap.go` 的 `Dependencies` 结构体中添加 `GatewayClient` 字段。
|
||||
|
||||
#### Scenario: 依赖注入扩展
|
||||
|
||||
- **WHEN** 在 `Dependencies` 中添加 `GatewayClient *gateway.Client` 字段
|
||||
- **THEN** 不影响现有依赖的注入
|
||||
- **AND** Gateway 客户端可以注入到需要的 Service
|
||||
|
||||
#### Scenario: Service 层使用
|
||||
|
||||
- **WHEN** Service 需要调用 Gateway API
|
||||
- **THEN** 在 Service 构造函数中接收 `gatewayClient *gateway.Client` 参数
|
||||
- **AND** 从 Bootstrap 中传递 `deps.GatewayClient`
|
||||
155
openspec/specs/gateway-crypto/spec.md
Normal file
155
openspec/specs/gateway-crypto/spec.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Gateway Crypto Specification
|
||||
|
||||
Gateway API 的加密和签名工具函数,实现 AES-128-ECB 加密和 MD5 签名机制。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: AES-128-ECB 加密
|
||||
|
||||
系统 SHALL 提供 `aesEncrypt` 函数,使用 AES-128-ECB 模式加密业务数据。
|
||||
|
||||
加密流程:
|
||||
1. 密钥生成:`MD5(appSecret)` 的原始字节数组(16字节)
|
||||
2. 加密算法:AES-128-ECB
|
||||
3. 填充方式:PKCS5Padding
|
||||
4. 编码输出:Base64
|
||||
|
||||
#### Scenario: 加密业务数据
|
||||
|
||||
- **WHEN** 调用 `aesEncrypt(data, appSecret)`
|
||||
- **AND** `data` 为业务数据的 JSON 字节数组
|
||||
- **THEN** 返回 Base64 编码的加密字符串
|
||||
- **AND** 密钥为 `MD5(appSecret)` 的 16 字节数组
|
||||
|
||||
#### Scenario: PKCS5 填充正确性
|
||||
|
||||
- **WHEN** 业务数据长度不是 AES 块大小(16 字节)的整数倍
|
||||
- **THEN** 使用 PKCS5Padding 进行填充
|
||||
- **AND** 填充字节值等于填充长度
|
||||
|
||||
#### Scenario: 加密输出格式
|
||||
|
||||
- **WHEN** 加密成功
|
||||
- **THEN** 输出为 Base64 字符串
|
||||
- **AND** 字符串不包含换行符
|
||||
|
||||
#### Scenario: 加密失败
|
||||
|
||||
- **WHEN** AES 加密过程失败
|
||||
- **THEN** 返回 `CodeGatewayEncryptError` 错误
|
||||
- **AND** 错误信息包含原始错误
|
||||
|
||||
### Requirement: MD5 签名生成
|
||||
|
||||
系统 SHALL 提供 `generateSign` 函数,生成 MD5 签名。
|
||||
|
||||
签名流程:
|
||||
1. 参数排序:`appId`、`data`、`timestamp` 按字母升序
|
||||
2. 拼接字符串:`appId=xxx&data=xxx×tamp=xxx&key=appSecret`
|
||||
3. MD5 加密
|
||||
4. 转大写十六进制
|
||||
|
||||
#### Scenario: 生成正确的签名
|
||||
|
||||
- **WHEN** 调用 `generateSign(appID, encryptedData, timestamp, appSecret)`
|
||||
- **THEN** 参数按字母序拼接:`appId` → `data` → `timestamp`
|
||||
- **AND** 追加 `&key=appSecret`
|
||||
- **AND** MD5 加密后转大写十六进制
|
||||
|
||||
#### Scenario: 签名输出格式
|
||||
|
||||
- **WHEN** 签名生成成功
|
||||
- **THEN** 输出为 32 位大写十六进制字符串
|
||||
- **AND** 例如:"ABCDEF1234567890ABCDEF1234567890"
|
||||
|
||||
#### Scenario: 签名可重现
|
||||
|
||||
- **WHEN** 使用相同的 `appID`、`encryptedData`、`timestamp`、`appSecret`
|
||||
- **THEN** 多次调用 `generateSign` 生成相同的签名
|
||||
|
||||
#### Scenario: 时间戳格式
|
||||
|
||||
- **WHEN** 签名中使用时间戳
|
||||
- **THEN** 时间戳为 Unix 秒级时间戳(10 位数字)
|
||||
- **AND** 例如:1704067200
|
||||
|
||||
### Requirement: 参数序列化
|
||||
|
||||
系统 SHALL 正确序列化请求参数,确保与 Gateway 期望格式一致。
|
||||
|
||||
#### Scenario: 业务数据序列化
|
||||
|
||||
- **WHEN** 业务数据为 Go 结构体
|
||||
- **THEN** 使用 `sonic.Marshal` 序列化为 JSON 字符串
|
||||
- **AND** JSON 格式与 Gateway 文档一致
|
||||
|
||||
#### Scenario: 空字段处理
|
||||
|
||||
- **WHEN** 请求结构体中某些字段为空(omitempty)
|
||||
- **THEN** 序列化时忽略空字段
|
||||
- **AND** 减少请求体大小
|
||||
|
||||
### Requirement: 加密/签名测试验证
|
||||
|
||||
系统 SHALL 提供加密和签名的单元测试,验证与 Gateway 文档一致性。
|
||||
|
||||
#### Scenario: 加密测试用例
|
||||
|
||||
- **WHEN** 使用已知的业务数据和 appSecret
|
||||
- **THEN** 加密输出与 Gateway 文档示例一致
|
||||
- **AND** 可以被 Gateway 正确解密
|
||||
|
||||
#### Scenario: 签名测试用例
|
||||
|
||||
- **WHEN** 使用已知的参数和 appSecret
|
||||
- **THEN** 签名输出与 Gateway 文档示例一致
|
||||
- **AND** Gateway 验证签名成功
|
||||
|
||||
#### Scenario: 端到端验证
|
||||
|
||||
- **WHEN** 运行集成测试,实际调用 Gateway API
|
||||
- **THEN** 加密和签名被 Gateway 接受
|
||||
- **AND** 响应状态码为 200
|
||||
|
||||
### Requirement: 性能要求
|
||||
|
||||
系统 SHALL 确保加密和签名操作的性能满足要求。
|
||||
|
||||
#### Scenario: 加密性能
|
||||
|
||||
- **WHEN** 加密 1KB 的业务数据
|
||||
- **THEN** 加密时间 < 1ms
|
||||
- **AND** 内存分配最小化
|
||||
|
||||
#### Scenario: 签名性能
|
||||
|
||||
- **WHEN** 生成签名
|
||||
- **THEN** 签名时间 < 0.5ms
|
||||
- **AND** 无不必要的内存分配
|
||||
|
||||
### Requirement: 安全性说明
|
||||
|
||||
系统 SHALL 在文档中说明 AES-ECB 模式的安全性限制。
|
||||
|
||||
#### Scenario: 安全性文档
|
||||
|
||||
- **WHEN** 查看加密函数的文档注释
|
||||
- **THEN** 注释中说明 ECB 模式不推荐用于生产环境
|
||||
- **AND** 说明这是 Gateway 强制要求,无法改变
|
||||
- **AND** 建议使用 HTTPS 加密传输层
|
||||
|
||||
### Requirement: 字符编码一致性
|
||||
|
||||
系统 SHALL 确保所有字符串操作使用 UTF-8 编码。
|
||||
|
||||
#### Scenario: 字符串编码
|
||||
|
||||
- **WHEN** 序列化业务数据
|
||||
- **THEN** 使用 UTF-8 编码
|
||||
- **AND** 中文字符正确处理
|
||||
|
||||
#### Scenario: 签名字符串编码
|
||||
|
||||
- **WHEN** 生成签名的拼接字符串
|
||||
- **THEN** 使用 UTF-8 编码
|
||||
- **AND** 与 Gateway 期望的编码一致
|
||||
@@ -1,20 +1,46 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 订单类型标识
|
||||
|
||||
系统 SHALL 在订单模型中增加 is_purchase_on_behalf 字段,标识是否为代购订单。
|
||||
|
||||
#### Scenario: 普通订单创建
|
||||
- **WHEN** 个人客户或代理为自己创建订单
|
||||
- **THEN** 系统设置 is_purchase_on_behalf = false
|
||||
|
||||
#### Scenario: 代购订单创建
|
||||
- **WHEN** 平台或代理为其他代理创建代购订单
|
||||
- **THEN** 系统设置 is_purchase_on_behalf = true
|
||||
|
||||
#### Scenario: 查询订单列表返回订单类型
|
||||
- **WHEN** 查询订单列表或详情
|
||||
- **THEN** 响应包含 is_purchase_on_behalf 字段
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 创建套餐购买订单
|
||||
|
||||
系统 SHALL 允许买家创建套餐购买订单。订单类型分为单卡购买和设备购买。创建前 MUST 验证购买权限。
|
||||
系统 SHALL 允许买家创建套餐购买订单。订单类型分为单卡购买和设备购买。创建前 MUST 验证购买权限和强充要求。
|
||||
|
||||
#### Scenario: 个人客户创建单卡订单
|
||||
- **WHEN** 个人客户为自己的卡创建订单,选择一个套餐
|
||||
- **THEN** 系统创建订单,状态为待支付,返回订单信息
|
||||
- **THEN** 系统创建订单,状态为待支付,is_purchase_on_behalf = false,返回订单信息
|
||||
|
||||
#### Scenario: 个人客户创建设备订单
|
||||
- **WHEN** 个人客户为自己的设备创建订单
|
||||
- **THEN** 系统创建订单,订单类型为设备购买
|
||||
- **THEN** 系统创建订单,订单类型为设备购买,is_purchase_on_behalf = false
|
||||
|
||||
#### Scenario: 代理创建订单
|
||||
- **WHEN** 代理为店铺关联的卡/设备创建订单
|
||||
- **THEN** 系统创建订单,买家类型为代理商,买家ID为店铺ID
|
||||
- **THEN** 系统创建订单,买家类型为代理商,买家ID为店铺ID,is_purchase_on_behalf = false
|
||||
|
||||
#### Scenario: 平台创建代购订单
|
||||
- **WHEN** 平台账号为代理的卡/设备创建订单,支付方式选择 offline
|
||||
- **THEN** 系统创建订单,is_purchase_on_behalf = true,payment_method = "offline",payment_status = 2(已支付)
|
||||
|
||||
#### Scenario: 套餐购买验证强充要求
|
||||
- **WHEN** 个人客户创建订单,存在强充要求,订单金额低于强充金额
|
||||
- **THEN** 系统返回错误 "支付金额不符合强充要求"
|
||||
|
||||
#### Scenario: 套餐不在可购买范围
|
||||
- **WHEN** 买家尝试购买不在关联系列下的套餐
|
||||
@@ -28,7 +54,7 @@
|
||||
|
||||
### Requirement: 查询订单列表
|
||||
|
||||
系统 SHALL 提供订单列表查询,支持按支付状态、订单类型、时间范围筛选。
|
||||
系统 SHALL 提供订单列表查询,支持按支付状态、订单类型、是否代购筛选。
|
||||
|
||||
#### Scenario: 个人客户查询自己的订单
|
||||
- **WHEN** 个人客户查询订单列表
|
||||
@@ -36,7 +62,11 @@
|
||||
|
||||
#### Scenario: 代理查询店铺订单
|
||||
- **WHEN** 代理查询订单列表
|
||||
- **THEN** 系统返回该店铺及下级店铺的订单
|
||||
- **THEN** 系统返回该店铺及下级店铺的订单(包含代购订单和普通订单)
|
||||
|
||||
#### Scenario: 按代购类型筛选
|
||||
- **WHEN** 指定 is_purchase_on_behalf = true 筛选
|
||||
- **THEN** 系统只返回代购订单
|
||||
|
||||
#### Scenario: 按支付状态筛选
|
||||
- **WHEN** 指定支付状态筛选
|
||||
@@ -60,16 +90,20 @@
|
||||
|
||||
### Requirement: 取消订单
|
||||
|
||||
系统 SHALL 允许买家取消未支付的订单。
|
||||
系统 SHALL 允许买家取消未支付的订单,但代购订单不可取消。
|
||||
|
||||
#### Scenario: 取消待支付订单
|
||||
- **WHEN** 买家取消一个待支付的订单
|
||||
#### Scenario: 取消待支付的普通订单
|
||||
- **WHEN** 买家取消一个待支付的普通订单(is_purchase_on_behalf = false)
|
||||
- **THEN** 系统更新订单状态为已取消
|
||||
|
||||
#### Scenario: 取消已支付订单
|
||||
- **WHEN** 买家尝试取消已支付的订单
|
||||
- **THEN** 系统返回错误 "已支付订单无法取消"
|
||||
|
||||
#### Scenario: 尝试取消代购订单
|
||||
- **WHEN** 买家尝试取消代购订单(is_purchase_on_behalf = true)
|
||||
- **THEN** 系统返回错误 "代购订单不可取消"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 订单号生成
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 线下支付方式
|
||||
|
||||
系统 SHALL 支持线下支付方式(offline),仅用于代购订单。线下支付的订单创建后直接标记为已支付,跳过支付流程。
|
||||
|
||||
#### Scenario: 创建线下支付订单
|
||||
- **WHEN** 平台账号创建订单时选择支付方式为 offline
|
||||
- **THEN** 系统创建订单,payment_status 直接设为 2(已支付),payment_method = "offline"
|
||||
|
||||
#### Scenario: 线下支付权限限制
|
||||
- **WHEN** 非平台账号(代理/个人客户)尝试使用线下支付
|
||||
- **THEN** 系统返回错误 "只有平台账号可以使用线下支付"
|
||||
|
||||
#### Scenario: 线下支付订单自动激活套餐
|
||||
- **WHEN** 创建线下支付订单成功
|
||||
- **THEN** 系统自动激活套餐,创建 PackageUsage 记录
|
||||
|
||||
#### Scenario: 线下支付不扣钱包
|
||||
- **WHEN** 订单使用线下支付
|
||||
- **THEN** 系统不扣减任何钱包余额
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包支付
|
||||
|
||||
系统 SHALL 支持使用钱包余额支付订单。支付成功后 MUST 扣减钱包余额并激活套餐。
|
||||
@@ -24,16 +46,24 @@
|
||||
|
||||
### Requirement: 第三方支付回调
|
||||
|
||||
系统 SHALL 处理微信支付和支付宝的支付回调。回调处理 MUST 幂等。
|
||||
系统 SHALL 处理微信支付和支付宝的支付回调,支持订单支付和钱包充值两种场景。回调处理 MUST 幂等。
|
||||
|
||||
#### Scenario: 微信支付成功回调
|
||||
- **WHEN** 收到微信支付成功回调
|
||||
#### Scenario: 微信支付成功回调(订单)
|
||||
- **WHEN** 收到微信支付成功回调,订单号格式为 ORD 开头
|
||||
- **THEN** 系统验证签名,更新订单状态,激活套餐,返回成功响应
|
||||
|
||||
#### Scenario: 支付宝成功回调
|
||||
- **WHEN** 收到支付宝支付成功回调
|
||||
#### Scenario: 微信支付成功回调(充值)
|
||||
- **WHEN** 收到微信支付成功回调,订单号格式为 RCH 开头
|
||||
- **THEN** 系统验证签名,更新充值订单状态,增加钱包余额,更新累计充值,触发佣金判断,返回成功响应
|
||||
|
||||
#### Scenario: 支付宝成功回调(订单)
|
||||
- **WHEN** 收到支付宝支付成功回调,订单号格式为 ORD 开头
|
||||
- **THEN** 系统验证签名,更新订单状态,激活套餐,返回成功响应
|
||||
|
||||
#### Scenario: 支付宝成功回调(充值)
|
||||
- **WHEN** 收到支付宝支付成功回调,订单号格式为 RCH 开头
|
||||
- **THEN** 系统验证签名,更新充值订单状态,增加钱包余额,更新累计充值,触发佣金判断,返回成功响应
|
||||
|
||||
#### Scenario: 重复回调
|
||||
- **WHEN** 收到已处理订单的重复回调
|
||||
- **THEN** 系统返回成功响应,不重复处理
|
||||
@@ -46,7 +76,7 @@
|
||||
|
||||
### Requirement: 套餐激活
|
||||
|
||||
支付成功后系统 MUST 激活套餐,创建 PackageUsage 记录。
|
||||
支付成功后系统 MUST 激活套餐,创建 PackageUsage 记录。代购订单也需激活套餐,但不更新累计充值。
|
||||
|
||||
#### Scenario: 单卡套餐激活
|
||||
- **WHEN** 单卡订单支付成功
|
||||
@@ -60,6 +90,10 @@
|
||||
- **WHEN** 套餐激活
|
||||
- **THEN** 有效期 = 激活时间 + 套餐时长(月)
|
||||
|
||||
#### Scenario: 代购订单激活套餐
|
||||
- **WHEN** 代购订单(is_purchase_on_behalf = true)创建成功
|
||||
- **THEN** 系统激活套餐,但不更新卡/设备的 accumulated_recharge
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 支付事务保证
|
||||
|
||||
145
openspec/specs/purchase-on-behalf/spec.md
Normal file
145
openspec/specs/purchase-on-behalf/spec.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Capability: 代购订单
|
||||
|
||||
## Purpose
|
||||
|
||||
本 capability 定义代购订单功能,允许平台或代理为其他代理创建套餐购买订单,使用线下支付方式,订单创建后直接完成支付并激活套餐。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 平台创建代购订单
|
||||
|
||||
系统 SHALL 允许平台账号为代理创建代购订单,使用线下支付方式,订单创建后直接标记为已支付。
|
||||
|
||||
#### Scenario: 平台为一级代理代购
|
||||
- **WHEN** 平台账号为一级代理的卡创建代购订单,选择套餐,支付方式为线下支付
|
||||
- **THEN** 系统创建订单,buyer_id = 一级代理店铺ID,is_purchase_on_behalf = true,payment_method = "offline",payment_status = 2(已支付)
|
||||
|
||||
#### Scenario: 平台为二级代理代购
|
||||
- **WHEN** 平台账号为二级代理的卡创建代购订单
|
||||
- **THEN** 系统创建订单,buyer_id = 二级代理店铺ID,is_purchase_on_behalf = true
|
||||
|
||||
#### Scenario: 代购订单价格使用代理成本价
|
||||
- **WHEN** 平台为代理创建代购订单,套餐价格 100 元,代理成本价 80 元
|
||||
- **THEN** 订单金额为 80 元(代理成本价)
|
||||
|
||||
#### Scenario: 查询卡归属代理
|
||||
- **WHEN** 平台选择卡创建代购订单
|
||||
- **THEN** 系统查询卡的 shop_id,作为订单的 buyer_id
|
||||
|
||||
#### Scenario: 查询设备归属代理
|
||||
- **WHEN** 平台选择设备创建代购订单
|
||||
- **THEN** 系统查询设备的 shop_id,作为订单的 buyer_id
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代理创建代购订单
|
||||
|
||||
系统 SHALL 允许代理账号为其他代理(通常是下级代理)创建代购订单。
|
||||
|
||||
#### Scenario: 一级代理为二级代理代购
|
||||
- **WHEN** 一级代理为二级代理的卡创建代购订单,选择套餐,支付方式为线下支付
|
||||
- **THEN** 系统创建订单,buyer_id = 二级代理店铺ID,is_purchase_on_behalf = true,payment_method = "offline"
|
||||
|
||||
#### Scenario: 代购订单使用买家成本价
|
||||
- **WHEN** 一级代理为二级代理代购,套餐价格 100 元,二级代理成本价 90 元
|
||||
- **THEN** 订单金额为 90 元(买家成本价)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单自动完成
|
||||
|
||||
代购订单创建后 SHALL 自动完成支付流程,激活套餐,但不触发一次性佣金。
|
||||
|
||||
#### Scenario: 代购订单自动激活套餐
|
||||
- **WHEN** 创建代购订单成功
|
||||
- **THEN** 系统自动激活套餐(创建 PackageUsage 记录)
|
||||
|
||||
#### Scenario: 代购订单不扣钱包
|
||||
- **WHEN** 创建代购订单
|
||||
- **THEN** 系统不扣减任何钱包余额(线下已收款)
|
||||
|
||||
#### Scenario: 代购订单不更新累计充值
|
||||
- **WHEN** 代购订单完成
|
||||
- **THEN** 系统不更新卡/设备的 accumulated_recharge 字段
|
||||
|
||||
#### Scenario: 代购订单计算差价佣金
|
||||
- **WHEN** 代购订单完成,买家有上级代理
|
||||
- **THEN** 系统计算差价佣金(买家成本价 - 上级成本价),发放给上级代理
|
||||
|
||||
#### Scenario: 代购订单不触发一次性佣金
|
||||
- **WHEN** 代购订单完成
|
||||
- **THEN** 系统不检查一次性佣金阈值,不发放一次性佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单查询
|
||||
|
||||
系统 SHALL 在订单列表中正确显示代购订单。
|
||||
|
||||
#### Scenario: 平台查询代购订单
|
||||
- **WHEN** 平台账号查询订单列表
|
||||
- **THEN** 系统返回所有代购订单(is_purchase_on_behalf = true),包含创建人信息
|
||||
|
||||
#### Scenario: 代理查询收到的代购订单
|
||||
- **WHEN** 代理查询订单列表,包含别人为自己代购的订单
|
||||
- **THEN** 系统返回买家为自己的代购订单
|
||||
|
||||
#### Scenario: 代购订单标识
|
||||
- **WHEN** 查询订单详情
|
||||
- **THEN** 订单响应包含 is_purchase_on_behalf 字段,前端可以显示"代购订单"标签
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单权限控制
|
||||
|
||||
系统 SHALL 严格控制代购订单的创建权限。
|
||||
|
||||
#### Scenario: 只有平台账号可以使用线下支付
|
||||
- **WHEN** 代理账号尝试创建订单时选择支付方式为 offline
|
||||
- **THEN** 系统返回错误 "只有平台账号可以使用线下支付"
|
||||
|
||||
#### Scenario: 平台账号可以为任何代理代购
|
||||
- **WHEN** 平台账号为任意层级代理创建代购订单
|
||||
- **THEN** 系统允许创建
|
||||
|
||||
#### Scenario: 代理账号只能为下级代理代购
|
||||
- **WHEN** 代理账号尝试为上级或平级代理创建代购订单
|
||||
- **THEN** 系统返回错误 "只能为下级代理代购套餐"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单记录
|
||||
|
||||
系统 SHALL 完整记录代购订单的创建人和买家信息。
|
||||
|
||||
#### Scenario: 记录创建人
|
||||
- **WHEN** 平台/代理创建代购订单
|
||||
- **THEN** 订单的 creator 字段记录创建人账号ID
|
||||
|
||||
#### Scenario: 区分创建人和买家
|
||||
- **WHEN** 查询代购订单详情
|
||||
- **THEN** creator(创建人)!= buyer_id(买家),可以追溯是谁代购的
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单不可取消
|
||||
|
||||
代购订单创建后 SHALL 不可取消,因为已自动完成。
|
||||
|
||||
#### Scenario: 尝试取消代购订单
|
||||
- **WHEN** 尝试取消一个代购订单(is_purchase_on_behalf = true)
|
||||
- **THEN** 系统返回错误 "代购订单不可取消"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 线下支付方式常量
|
||||
|
||||
系统 SHALL 定义线下支付方式常量。
|
||||
|
||||
#### Scenario: 支付方式枚举
|
||||
- **WHEN** 创建订单时选择支付方式
|
||||
- **THEN** payment_method 可选值包含:wallet, wechat, alipay, offline
|
||||
|
||||
#### Scenario: 线下支付只用于代购
|
||||
- **WHEN** payment_method = "offline"
|
||||
- **THEN** 订单必须标记为 is_purchase_on_behalf = true
|
||||
@@ -6,14 +6,40 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 强充配置
|
||||
|
||||
系统 SHALL 在套餐系列分配中支持强充配置。仅累计充值触发时可选启用强充,首次充值触发时强充是必须的(无需配置)。
|
||||
|
||||
#### Scenario: 累计充值启用强充
|
||||
- **WHEN** 创建系列分配,一次性佣金触发类型为累计充值,设置 enable_force_recharge = true,force_recharge_amount = 10000(100元)
|
||||
- **THEN** 系统保存强充配置,下级客户每次充值/购买必须充值 100 元
|
||||
|
||||
#### Scenario: 累计充值不启用强充
|
||||
- **WHEN** 创建系列分配,一次性佣金触发类型为累计充值,设置 enable_force_recharge = false
|
||||
- **THEN** 系统保存配置,下级客户可以自由充值任意金额
|
||||
|
||||
#### Scenario: 首次充值无需设置强充
|
||||
- **WHEN** 创建系列分配,一次性佣金触发类型为首次充值,阈值 10000(100元)
|
||||
- **THEN** 系统使用阈值作为强充金额,无需单独配置 force_recharge_amount
|
||||
|
||||
#### Scenario: 强充金额为0表示使用阈值
|
||||
- **WHEN** 创建系列分配,启用强充,force_recharge_amount = 0
|
||||
- **THEN** 系统使用一次性佣金阈值作为强充金额
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 为下级店铺分配套餐系列
|
||||
|
||||
系统 SHALL 允许代理为其直属下级店铺分配套餐系列。分配时 MUST 指定基础返佣配置(返佣模式和返佣值),MAY 启用一次性佣金。分配者只能分配自己已被分配的套餐系列。
|
||||
系统 SHALL 允许代理为其直属下级店铺分配套餐系列。分配时 MUST 指定基础返佣配置(返佣模式和返佣值),MAY 启用一次性佣金和强充配置。分配者只能分配自己已被分配的套餐系列。
|
||||
|
||||
#### Scenario: 成功分配套餐系列
|
||||
- **WHEN** 代理为直属下级店铺分配一个自己拥有的套餐系列,设置基础返佣为百分比200(20%)
|
||||
- **THEN** 系统创建分配记录
|
||||
|
||||
#### Scenario: 分配时启用一次性佣金和强充
|
||||
- **WHEN** 代理为下级分配系列,启用一次性佣金,触发类型为累计充值,阈值 100000(1000元),启用强充,强充金额 10000(100元)
|
||||
- **THEN** 系统保存配置:enable_one_time_commission = true,trigger = "accumulated_recharge",threshold = 100000,enable_force_recharge = true,force_recharge_amount = 10000
|
||||
|
||||
#### Scenario: 尝试分配未拥有的系列
|
||||
- **WHEN** 代理尝试分配自己未被分配的套餐系列
|
||||
- **THEN** 系统返回错误 "您没有该套餐系列的分配权限"
|
||||
@@ -44,12 +70,20 @@
|
||||
|
||||
### Requirement: 更新套餐系列分配
|
||||
|
||||
系统 SHALL 允许代理更新分配的基础返佣配置和一次性佣金配置。更新返佣配置时 MUST 创建新的配置版本。
|
||||
系统 SHALL 允许代理更新分配的基础返佣配置、一次性佣金配置和强充配置。更新返佣配置时 MUST 创建新的配置版本。
|
||||
|
||||
#### Scenario: 更新基础返佣配置时创建新版本
|
||||
- **WHEN** 代理将基础返佣从20%改为25%
|
||||
- **THEN** 系统更新分配记录,并创建新配置版本
|
||||
|
||||
#### Scenario: 更新强充配置
|
||||
- **WHEN** 代理将 enable_force_recharge 从 false 改为 true,设置 force_recharge_amount = 10000
|
||||
- **THEN** 系统更新分配记录,后续下级客户需遵守新强充要求
|
||||
|
||||
#### Scenario: 禁用强充
|
||||
- **WHEN** 代理将 enable_force_recharge 从 true 改为 false
|
||||
- **THEN** 系统更新分配记录,后续下级客户可以自由充值
|
||||
|
||||
#### Scenario: 更新不存在的分配
|
||||
- **WHEN** 代理更新不存在的分配 ID
|
||||
- **THEN** 系统返回 "分配记录不存在" 错误
|
||||
@@ -86,12 +120,16 @@
|
||||
|
||||
### Requirement: 平台分配套餐系列
|
||||
|
||||
平台管理员 SHALL 能够为一级代理分配套餐系列。平台的成本价基准为 Package.suggested_cost_price。
|
||||
平台管理员 SHALL 能够为一级代理分配套餐系列,可配置强充要求。平台的成本价基准为 Package.suggested_cost_price。
|
||||
|
||||
#### Scenario: 平台为一级代理分配
|
||||
- **WHEN** 平台管理员为一级代理分配套餐系列
|
||||
- **THEN** 系统创建分配记录
|
||||
|
||||
#### Scenario: 平台配置强充要求
|
||||
- **WHEN** 平台为一级代理分配系列,启用强充,force_recharge_amount = 10000
|
||||
- **THEN** 系统保存强充配置,一级代理的客户需遵守强充要求
|
||||
|
||||
---
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
188
openspec/specs/wallet-recharge/spec.md
Normal file
188
openspec/specs/wallet-recharge/spec.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Capability: 钱包充值
|
||||
|
||||
## Purpose
|
||||
|
||||
本 capability 定义钱包充值功能,允许个人客户为卡/设备钱包充值,支持强充验证、第三方支付和充值后的累计充值更新与一次性佣金触发。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 创建钱包充值订单
|
||||
|
||||
系统 SHALL 允许个人客户创建钱包充值订单。创建前 MUST 验证强充要求,强充场景下充值金额必须等于要求的强充金额。
|
||||
|
||||
#### Scenario: 无强充要求时自由充值
|
||||
- **WHEN** 个人客户为卡/设备创建充值订单,该卡/设备无强充要求,充值金额 100 元
|
||||
- **THEN** 系统创建充值订单,状态为待支付,金额 10000 分
|
||||
|
||||
#### Scenario: 首次充值强充
|
||||
- **WHEN** 卡关联系列配置为首次充值触发,阈值 100 元,客户尝试充值 100 元
|
||||
- **THEN** 系统验证通过,创建充值订单,金额 10000 分
|
||||
|
||||
#### Scenario: 首次充值金额不符
|
||||
- **WHEN** 卡关联系列配置为首次充值触发,阈值 100 元,客户尝试充值 50 元
|
||||
- **THEN** 系统返回错误 "必须充值100元"
|
||||
|
||||
#### Scenario: 累计充值启用强充
|
||||
- **WHEN** 卡关联系列配置为累计充值触发,启用强充,强充金额 100 元,客户尝试充值 100 元
|
||||
- **THEN** 系统验证通过,创建充值订单
|
||||
|
||||
#### Scenario: 累计充值强充金额不符
|
||||
- **WHEN** 卡关联系列配置为累计充值触发,启用强充,强充金额 100 元,客户尝试充值 50 元
|
||||
- **THEN** 系统返回错误 "必须充值100元"
|
||||
|
||||
#### Scenario: 累计充值未启用强充
|
||||
- **WHEN** 卡关联系列配置为累计充值触发,未启用强充,客户充值任意金额
|
||||
- **THEN** 系统创建充值订单
|
||||
|
||||
#### Scenario: 充值订单号唯一
|
||||
- **WHEN** 创建充值订单
|
||||
- **THEN** 系统生成唯一充值单号,格式为 RCH + 14位时间戳 + 6位随机数
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询充值订单列表
|
||||
|
||||
系统 SHALL 提供充值订单列表查询,支持按状态筛选、时间范围筛选。
|
||||
|
||||
#### Scenario: 查询个人客户的充值订单
|
||||
- **WHEN** 个人客户查询充值订单列表
|
||||
- **THEN** 系统返回该客户的所有充值订单
|
||||
|
||||
#### Scenario: 按状态筛选
|
||||
- **WHEN** 客户指定充值状态筛选(待支付/已支付/已完成)
|
||||
- **THEN** 系统只返回匹配状态的充值订单
|
||||
|
||||
#### Scenario: 分页查询
|
||||
- **WHEN** 查询充值订单列表
|
||||
- **THEN** 系统使用分页返回,默认每页 20 条,最大 100 条
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询充值订单详情
|
||||
|
||||
系统 SHALL 允许个人客户查询充值订单详情。
|
||||
|
||||
#### Scenario: 查询自己的充值订单
|
||||
- **WHEN** 客户查询自己的充值订单详情
|
||||
- **THEN** 系统返回订单信息(充值单号、金额、支付方式、状态、时间等)
|
||||
|
||||
#### Scenario: 查询他人充值订单
|
||||
- **WHEN** 客户尝试查询不属于自己的充值订单
|
||||
- **THEN** 系统返回 "充值订单不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值支付(微信/支付宝)
|
||||
|
||||
系统 SHALL 支持通过微信支付和支付宝支付完成充值。
|
||||
|
||||
#### Scenario: 微信 JSAPI 支付
|
||||
- **WHEN** 客户在微信内选择充值,使用微信支付
|
||||
- **THEN** 系统调用微信支付 JSAPI 接口,返回支付参数
|
||||
|
||||
#### Scenario: 微信 H5 支付
|
||||
- **WHEN** 客户在浏览器内选择充值,使用微信支付
|
||||
- **THEN** 系统调用微信支付 H5 接口,返回支付跳转 URL
|
||||
|
||||
#### Scenario: 支付宝支付
|
||||
- **WHEN** 客户选择支付宝支付充值
|
||||
- **THEN** 系统调用支付宝接口,返回支付参数
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值支付回调处理
|
||||
|
||||
系统 SHALL 处理微信和支付宝的支付回调,验证签名,更新充值订单状态,增加钱包余额。
|
||||
|
||||
#### Scenario: 微信支付回调成功
|
||||
- **WHEN** 收到微信支付成功回调,验证签名通过
|
||||
- **THEN** 系统更新充值订单状态为已支付
|
||||
- **AND** 增加对应钱包余额
|
||||
- **AND** 创建钱包交易记录
|
||||
- **AND** 返回成功响应给微信
|
||||
|
||||
#### Scenario: 支付宝回调成功
|
||||
- **WHEN** 收到支付宝支付成功回调,验证签名通过
|
||||
- **THEN** 系统更新充值订单状态为已支付
|
||||
- **AND** 增加对应钱包余额
|
||||
- **AND** 创建钱包交易记录
|
||||
|
||||
#### Scenario: 签名验证失败
|
||||
- **WHEN** 收到支付回调,签名验证失败
|
||||
- **THEN** 系统记录错误日志,不处理订单,返回失败响应
|
||||
|
||||
#### Scenario: 重复回调幂等处理
|
||||
- **WHEN** 收到同一充值订单的重复支付回调
|
||||
- **THEN** 系统检查订单状态,如果已支付则直接返回成功,不重复处理
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值成功更新累计充值金额
|
||||
|
||||
充值支付成功后系统 SHALL 更新卡/设备的累计充值金额(AccumulatedRecharge)。
|
||||
|
||||
#### Scenario: 充值成功累加充值金额
|
||||
- **WHEN** 卡钱包充值 100 元成功,当前累计充值 200 元
|
||||
- **THEN** 系统更新卡的累计充值为 300 元(200 + 100)
|
||||
|
||||
#### Scenario: 设备充值成功累加充值金额
|
||||
- **WHEN** 设备钱包充值 200 元成功,当前累计充值 500 元
|
||||
- **THEN** 系统更新设备的累计充值为 700 元(500 + 200)
|
||||
|
||||
#### Scenario: 使用原子操作更新
|
||||
- **WHEN** 更新累计充值金额
|
||||
- **THEN** 系统使用 SQL 原子操作或 GORM 乐观锁确保并发安全
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值成功触发一次性佣金判断
|
||||
|
||||
充值支付成功后系统 SHALL 检查是否达到一次性佣金阈值,如果达到则触发佣金计算。
|
||||
|
||||
#### Scenario: 首次充值达到阈值
|
||||
- **WHEN** 卡配置为首次充值触发,阈值 100 元,客户充值 100 元
|
||||
- **THEN** 系统触发一次性佣金计算,发放佣金
|
||||
|
||||
#### Scenario: 累计充值达到阈值
|
||||
- **WHEN** 卡配置为累计充值触发,阈值 1000 元,累计充值已达到 1000 元
|
||||
- **THEN** 系统触发一次性佣金计算,发放佣金
|
||||
|
||||
#### Scenario: 未达阈值不触发
|
||||
- **WHEN** 充值后累计充值未达到阈值
|
||||
- **THEN** 系统不触发一次性佣金计算
|
||||
|
||||
#### Scenario: 已发放过不重复触发
|
||||
- **WHEN** 卡的一次性佣金已发放过(first_commission_paid = true)
|
||||
- **THEN** 系统不触发一次性佣金计算
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值订单状态流转
|
||||
|
||||
充值订单状态 SHALL 按以下流程流转:待支付 → 已支付 → 已完成。
|
||||
|
||||
#### Scenario: 正常流转
|
||||
- **WHEN** 创建充值订单 → 支付成功 → 钱包余额增加完成
|
||||
- **THEN** 订单状态依次为:1(待支付)→ 2(已支付)→ 3(已完成)
|
||||
|
||||
#### Scenario: 超时未支付
|
||||
- **WHEN** 充值订单创建 30 分钟后仍未支付
|
||||
- **THEN** 系统标记订单为已关闭(状态 4)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值金额限制
|
||||
|
||||
系统 SHALL 限制单次充值金额范围。
|
||||
|
||||
#### Scenario: 充值金额范围
|
||||
- **WHEN** 创建充值订单
|
||||
- **THEN** 充值金额必须在 1 元到 100000 元之间
|
||||
|
||||
#### Scenario: 充值金额过小
|
||||
- **WHEN** 客户尝试充值 0.5 元
|
||||
- **THEN** 系统返回错误 "充值金额不能小于1元"
|
||||
|
||||
#### Scenario: 充值金额过大
|
||||
- **WHEN** 客户尝试充值 200000 元
|
||||
- **THEN** 系统返回错误 "单次充值金额不能超过100000元"
|
||||
Reference in New Issue
Block a user