fix: 修正零售价架构错误 + 清理旧微信配置 + 归档提案 + 前端接口文档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
1. 修正 retail_price 架构:
- 删除 batch-pricing 接口的 pricing_target 字段和 retail_price 分支
(上级只能改下级成本价,不能改零售价)
- 新增 PATCH /api/admin/packages/:id/retail-price 接口
(代理自己改自己的零售价,校验 retail_price >= cost_price)
2. 清理旧微信 YAML 配置(已全部迁移到数据库 tb_wechat_config):
- 删除 config.yaml 中 wechat.official_account 配置节
- 删除 NewOfficialAccountApp() 旧工厂函数
- 清理 personal_customer service 中的死代码(旧登录/绑定微信方法)
- 清理 docker-compose.prod.yml 中旧微信环境变量和证书挂载注释
3. 归档四个已完成提案到 openspec/changes/archive/
4. 新增前端接口变更说明文档(docs/前端接口变更说明.md)
5. 修正归档提案和 specs 中关于 pricing_target 的错误描述
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
- 所有字段新增使用 `NOT NULL DEFAULT` 确保存量数据兼容
|
||||
- 数据库迁移可在线执行,不需停机
|
||||
- 旧接口删除后 bootstrap、路由注册、文档生成器必须同步清理,否则编译失败
|
||||
- 本提案不涉及任何新 API 接口,纯粹是模型/字段/BUG 修复
|
||||
- 本提案新增 1 个后台接口:`PATCH /api/admin/packages/:id/retail-price`(代理修改自己的零售价)
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
- 不实现任何客户端 API 接口(属于提案 1~3)
|
||||
- 不实现 ExchangeOrder 换货模型(属于提案 3)
|
||||
- 不实现 PersonalCustomerOpenID 模型(属于提案 1)
|
||||
- 不修改后台管理界面或 Admin API
|
||||
- 不新增 API 路由
|
||||
- 不修改后台管理界面
|
||||
- 除 `PATCH /api/admin/packages/:id/retail-price` 外,不新增其他 API 路由
|
||||
- 不实现 asset_status 的状态流转逻辑(仅新增字段,流转逻辑在后续提案中实现)
|
||||
|
||||
## Decisions
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
### BUG 修复
|
||||
|
||||
- **BUG-1 代理零售价修复**:`ShopPackageAllocation` 新增 `retail_price` 字段;`GetPurchasePrice()` 改为代理渠道查 `allocation.retail_price`、平台渠道用 `Package.SuggestedRetailPrice`;`validatePackages()` 内部价格累加同步修正;新增 cost_price 分配锁定规则(存在下级分配时禁止修改 cost_price);**扩展现有 `BatchUpdatePricing` 接口支持 `retail_price` 调整**(新增 `pricing_target` 字段区分调 cost_price 还是 retail_price,默认 `cost_price` 保持向后兼容);**代理套餐列表(`PackageResponse`)新增 `retail_price` 字段**,代理可查看自己的零售价;**利润计算修正**为 `RetailPrice - CostPrice`(代理实际利润 = 零售价 - 成本价,而非建议售价 - 成本价)
|
||||
- **BUG-1 代理零售价修复**:`ShopPackageAllocation` 新增 `retail_price` 字段;`GetPurchasePrice()` 改为代理渠道查 `allocation.retail_price`、平台渠道用 `Package.SuggestedRetailPrice`;`validatePackages()` 内部价格累加同步修正;新增 cost_price 分配锁定规则(存在下级分配时禁止修改 cost_price);`BatchUpdatePricing` 接口仅支持成本价批量调整;新增独立接口 `PATCH /api/admin/packages/:id/retail-price` 供代理修改自己的零售价;**代理套餐列表(`PackageResponse`)新增 `retail_price` 字段**,代理可查看自己的零售价;**利润计算修正**为 `RetailPrice - CostPrice`(代理实际利润 = 零售价 - 成本价,而非建议售价 - 成本价)
|
||||
- **BUG-2 一次性佣金触发修复**:`Order` 新增 `source` 字段(`admin`/`client`);佣金触发条件从 `!order.IsPurchaseOnBehalf` 改为 `!order.IsPurchaseOnBehalf && order.Source == "client"`,确保只有客户端个人客户购买才触发
|
||||
- **BUG-4 充值回调事务修复**:`HandlePaymentCallback` 中 `UpdateStatusWithOptimisticLock` 和 `UpdatePaymentInfo` 从 `s.db.WithContext(ctx)` 改为事务内 `tx`,确保充值单状态变更和钱包入账原子完成
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
- `asset-lifecycle-status`:资产业务生命周期状态管理。IotCard/Device 新增 `asset_status` 字段(在库→已销售→已换货→已停用),定义状态流转规则,与运营商 `network_status` 完全独立
|
||||
- `asset-generation`:资产世代机制。IotCard/Device 的 `generation` 字段,关联表(Order/PackageUsage/AssetRechargeRecord)的 generation 写时快照规则,客户端按世代过滤、后台不过滤的查询规则
|
||||
- `carrier-realname-config`:运营商实名链接配置。Carrier 模型新增 `realname_link_type`/`realname_link_template` 字段,支持 none/template/gateway 三种模式,URL 模板占位符替换。**Carrier admin DTO 同步更新**,后台可通过现有运营商管理接口配置实名链接
|
||||
- `agent-retail-price`:代理零售价管理。ShopPackageAllocation 新增 `retail_price` 字段,支持代理设定面向终端客户的零售价,约束 `retail_price >= cost_price`,cost_price 分配锁定规则。**扩展 `BatchUpdatePricing` 接口**支持 `pricing_target=retail_price` 批量调整零售价;**代理套餐列表展示 retail_price**;**利润计算修正**为 `RetailPrice - CostPrice`
|
||||
- `agent-retail-price`:代理零售价管理。ShopPackageAllocation 新增 `retail_price` 字段,支持代理设定面向终端客户的零售价,约束 `retail_price >= cost_price`,cost_price 分配锁定规则。新增独立接口 `PATCH /api/admin/packages/:id/retail-price` 供代理修改自己的零售价;**代理套餐列表展示 retail_price**;**利润计算修正**为 `RetailPrice - CostPrice`
|
||||
- `asset-manual-deactivation`:资产手动停用。新增后台接口 `PATCH /api/admin/iot-cards/:id/deactivate` 和 `PATCH /api/admin/devices/:id/deactivate`,将 `asset_status` 设为 4(已停用),仅 `asset_status=1`(在库)或 `asset_status=2`(已销售)时可操作
|
||||
- `h5-legacy-cleanup`:旧 H5 接口和旧登录接口的完整删除,包括 handler、route、bootstrap 注册、文档生成器引用的清理
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
- `package-purchase-validation`:`GetPurchasePrice()` 价格来源改为按渠道区分(代理→retail_price,平台→SuggestedRetailPrice);`validatePackages()` 价格累加逻辑同步修正
|
||||
- `package-list`:代理查询套餐列表时,`PackageResponse` 新增 `retail_price` 字段;`ProfitMargin` 计算从 `SuggestedRetailPrice - CostPrice` 改为 `RetailPrice - CostPrice`
|
||||
- `batch-pricing`:`BatchUpdatePricing` 接口扩展支持 `pricing_target` 参数(`cost_price`/`retail_price`),默认 `cost_price` 保持向后兼容;retail_price 调整时校验 `>= cost_price`
|
||||
- `batch-pricing`:`BatchUpdatePricing` 接口仅支持 `cost_price` 批量调整;保留 `cost_price` 锁定校验(存在下级分配时不可修改)
|
||||
- `one-time-commission-trigger`:触发条件增加 `order.Source == "client"` 判断,确保仅客户端个人客户购买才触发
|
||||
- `wallet-recharge`:`HandlePaymentCallback` 事务一致性修复,Store 方法支持传入事务 `tx`
|
||||
- `iot-order`:Order 模型新增 `source`(订单来源)和 `generation`(世代)字段;`CreateAdminOrder()` 创建订单时从资产快照当前 `generation` 写入订单(而非依赖默认值 1)
|
||||
@@ -57,8 +57,8 @@
|
||||
## Impact
|
||||
|
||||
- **模型文件**:`shop_package_allocation.go`、`carrier.go`、`order.go`、`iot_card.go`、`device.go`、`package_usage.go`、`asset_recharge_record.go`、`personal_customer.go`
|
||||
- **Service 文件**:`purchase_validation/service.go`(价格计算)、`commission_calculation/service.go`(佣金触发)、`recharge/service.go`(回调事务)、`shop_package_batch_pricing/service.go`(扩展支持 retail_price + cost_price 锁定)、`shop_series_grant/service.go`(cost_price 锁定)、`order/service.go`(source 设置 + generation 快照)、`package/service.go`(利润计算修正 + PackageResponse 新增 retail_price)
|
||||
- **Handler/DTO 文件**:`shop_package_batch_pricing.go` Handler(扩展)、`shop_package_batch_pricing_dto.go`(新增 `pricing_target` 字段)、`package_dto.go`(`PackageResponse` 新增 `retail_price`)、`carrier_dto.go`(新增实名链接字段)
|
||||
- **Service 文件**:`purchase_validation/service.go`(价格计算)、`commission_calculation/service.go`(佣金触发)、`recharge/service.go`(回调事务)、`shop_package_batch_pricing/service.go`(仅成本价批量调价 + cost_price 锁定)、`shop_series_grant/service.go`(cost_price 锁定)、`order/service.go`(source 设置 + generation 快照)、`package/service.go`(新增代理改零售价接口逻辑 + 利润计算修正 + PackageResponse 新增 retail_price)
|
||||
- **Handler/DTO 文件**:`shop_package_batch_pricing.go` Handler(仅保留成本价批量调价)、`shop_package_batch_pricing_dto.go`(移除 `pricing_target` 字段)、`package.go` Handler(新增 `PATCH /packages/:id/retail-price`)、`package_dto.go`(`PackageResponse` 新增 `retail_price` + 新增更新零售价请求 DTO)、`carrier_dto.go`(新增实名链接字段)
|
||||
- **Store 文件**:`asset_recharge_store.go`(支持事务传入)
|
||||
- **删除文件**:`internal/handler/h5/` 全部(5 个文件)、`internal/routes/h5*.go`(3 个文件)、`internal/handler/app/personal_customer.go` 中旧方法
|
||||
- **数据库迁移**:7 张表共 15+ 个字段变更,1 个索引变更
|
||||
@@ -22,18 +22,12 @@
|
||||
|
||||
### Requirement: 零售价约束规则
|
||||
|
||||
系统 MUST 强制校验:`retail_price >= cost_price` 且 `retail_price <= suggested_retail_price * 2`。
|
||||
系统 MUST 强制校验:`retail_price >= cost_price`。
|
||||
|
||||
#### Scenario: 零售价低于成本价
|
||||
- **WHEN** 代理设置 `retail_price < cost_price`
|
||||
- **THEN** 系统 MUST 拒绝保存并返回价格约束错误
|
||||
|
||||
#### Scenario: 零售价超过建议价两倍
|
||||
- **WHEN** 代理设置 `retail_price > suggested_retail_price * 2`
|
||||
- **THEN** 系统 MUST 拒绝保存并返回价格约束错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 成本价分配锁定规则
|
||||
|
||||
当某分配存在下级分配记录时,系统 MUST 禁止修改该分配的 `cost_price`。
|
||||
@@ -46,7 +40,7 @@
|
||||
|
||||
### Requirement: 代理零售价可调与存量迁移
|
||||
|
||||
系统 MUST 允许代理修改自己分配记录的 `retail_price`(在约束范围内);系统 MUST 对存量数据执行迁移:将 `retail_price` 批量更新为对应套餐的 `SuggestedRetailPrice`。
|
||||
系统 MUST 提供独立接口 `PATCH /api/admin/packages/:id/retail-price` 供代理修改自己分配记录的 `retail_price`(在约束范围内);系统 MUST 对存量数据执行迁移:将 `retail_price` 批量更新为对应套餐的 `SuggestedRetailPrice`。
|
||||
|
||||
#### Scenario: 代理调整自己的零售价
|
||||
- **WHEN** 代理修改自己分配记录的 `retail_price` 且满足价格约束
|
||||
@@ -1,6 +1,6 @@
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 资产充值表结构变更
|
||||
### Requirement: 资产充值记录扩展字段(操作人与代际)
|
||||
|
||||
系统 MUST 在 `tb_asset_recharge_record` 新增以下字段:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 设备实体定义
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: IoT 卡实体定义
|
||||
### Requirement: IoT 卡资产生命周期字段
|
||||
|
||||
系统 SHALL 在 `IotCard` 模型新增以下字段:
|
||||
系统 SHALL 在 `IotCard` 模型新增以下资产生命周期追踪字段:
|
||||
- `asset_status int NOT NULL DEFAULT 1`
|
||||
- `generation int NOT NULL DEFAULT 1`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 订单实体定义
|
||||
### Requirement: 订单来源与代际字段
|
||||
|
||||
系统 SHALL 定义订单(Order)实体并新增来源与代际字段:
|
||||
系统 SHALL 在订单(Order)实体新增来源与代际字段:
|
||||
- `source varchar(20) NOT NULL DEFAULT 'admin'`,取值 `admin/client`
|
||||
- `generation int NOT NULL DEFAULT 1`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 一次性佣金触发条件
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 获取购买价格
|
||||
### Requirement: 代理渠道购买价格规则
|
||||
|
||||
系统 MUST 根据购买渠道返回正确的购买价格。
|
||||
系统 MUST 根据购买渠道返回正确的购买价格:代理渠道使用 `allocation.retail_price`,平台渠道使用 `Package.SuggestedRetailPrice`。
|
||||
|
||||
#### Scenario: 代理渠道使用分配零售价
|
||||
- **WHEN** 客户通过代理渠道购买套餐
|
||||
@@ -1,4 +1,4 @@
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 微信标识索引策略
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 充值支付回调处理
|
||||
### Requirement: 充值回调事务一致性
|
||||
|
||||
系统 SHALL 处理微信和支付宝的支付回调,验证签名,更新充值订单状态,增加钱包余额。
|
||||
|
||||
关键一致性修复:`HandlePaymentCallback` 内的 `UpdateStatusWithOptimisticLock` 与 `UpdatePaymentInfo` MUST 使用同一个事务内 `tx` 执行。
|
||||
`HandlePaymentCallback` 内的 `UpdateStatusWithOptimisticLock` 与 `UpdatePaymentInfo` MUST 使用同一个事务内 `tx` 执行,保证充值状态与支付信息的原子性。
|
||||
|
||||
#### Scenario: 回调处理中状态更新与支付信息更新同事务
|
||||
- **WHEN** 收到支付成功回调并进入 `HandlePaymentCallback`
|
||||
@@ -42,11 +42,15 @@
|
||||
|
||||
## 5. 代理零售价后台管理
|
||||
|
||||
- [x] 5.1 修改 `internal/model/dto/shop_package_batch_pricing_dto.go` 的 `BatchUpdateCostPriceRequest`:新增 `PricingTarget string` 字段(`json:"pricing_target" validate:"omitempty,oneof=cost_price retail_price" description:"调价目标 cost_price-成本价(默认) retail_price-零售价"`),不传时默认为 `cost_price` 保持向后兼容
|
||||
- [x] 5.2 修改 `internal/service/shop_package_batch_pricing/service.go` 的 `BatchUpdatePricing()` 方法:根据 `req.PricingTarget` 分流——`cost_price` 走现有逻辑(含 4.5 的锁定检查),`retail_price` 走新逻辑:计算新零售价、校验 `newRetailPrice >= allocation.CostPrice`(不满足则跳过并报错"零售价不能低于成本价")、更新 `allocation.RetailPrice`、记录价格历史(`ShopPackageAllocationPriceHistory` 增加 `old_retail_price`/`new_retail_price` 字段或复用 `OldCostPrice`/`NewCostPrice` 字段并新增 `price_type` 标识)
|
||||
- [x] 5.3 修改 `internal/model/dto/package_dto.go` 的 `PackageResponse` 结构体:新增 `RetailPrice *int64` 字段(`json:"retail_price,omitempty" description:"代理零售价(分),仅代理用户可见"`)
|
||||
- [x] 5.4 修改 `internal/service/package/service.go` 的 `toResponse()` 方法(约第 530-541 行):代理用户查询时,从 allocation 读取 `RetailPrice` 设入 `resp.RetailPrice`;同时修正 `ProfitMargin` 计算:从 `pkg.SuggestedRetailPrice - allocation.CostPrice` 改为 `allocation.RetailPrice - allocation.CostPrice`
|
||||
- [x] 5.5 修改 `internal/service/package/service.go` 的 `toResponseWithAllocation()` 方法(约第 595-603 行):同 5.4,从 allocation 读取 `RetailPrice`、修正 `ProfitMargin` 计算
|
||||
- [x] 5.1 修改 `internal/model/dto/shop_package_batch_pricing_dto.go` 的 `BatchUpdateCostPriceRequest`:删除 `PricingTarget` 字段,批量调价接口仅保留成本价路径
|
||||
- [x] 5.2 修改 `internal/service/shop_package_batch_pricing/service.go` 的 `BatchUpdatePricing()` 方法:删除 `retail_price` 分支与默认分流逻辑,仅保留 `cost_price` 调整(包含 4.5 的锁定检查)
|
||||
- [x] 5.3 在 `internal/model/dto/package_dto.go` 新增 `UpdateRetailPriceRequest` 与 `UpdateRetailPriceParams`,用于代理修改自己零售价
|
||||
- [x] 5.4 在 `internal/store/postgres/shop_package_allocation_store.go` 新增 `UpdateRetailPrice(ctx context.Context, id uint, retailPrice int64, updater uint) error`
|
||||
- [x] 5.5 在 `internal/service/package/service.go` 新增 `UpdateRetailPrice(ctx context.Context, packageID uint, retailPrice int64) error`:仅代理可调用、校验 `retail_price >= cost_price`
|
||||
- [x] 5.6 在 `internal/handler/admin/package.go` 新增 `UpdateRetailPrice`,并在 `internal/routes/package.go` 注册 `PATCH /api/admin/packages/:id/retail-price`
|
||||
- [x] 5.7 修改 `internal/model/dto/package_dto.go` 的 `PackageResponse` 结构体:新增 `RetailPrice *int64` 字段(`json:"retail_price,omitempty" description:"代理零售价(分),仅代理用户可见"`)
|
||||
- [x] 5.8 修改 `internal/service/package/service.go` 的 `toResponse()` 方法(约第 530-541 行):代理用户查询时,从 allocation 读取 `RetailPrice` 设入 `resp.RetailPrice`;同时修正 `ProfitMargin` 计算:从 `pkg.SuggestedRetailPrice - allocation.CostPrice` 改为 `allocation.RetailPrice - allocation.CostPrice`
|
||||
- [x] 5.9 修改 `internal/service/package/service.go` 的 `toResponseWithAllocation()` 方法(约第 595-603 行):同 5.8,从 allocation 读取 `RetailPrice`、修正 `ProfitMargin` 计算
|
||||
|
||||
## 6. Carrier 管理 DTO 更新
|
||||
|
||||
@@ -102,6 +106,6 @@
|
||||
- [x] 13.2 对所有修改的文件执行 `lsp_diagnostics` 确认无错误和警告
|
||||
- [x] 13.3 使用 PostgreSQL MCP 工具验证数据库:确认 7 张表的新字段存在、默认值正确、存量 `retail_price` 已填充、`wx_open_id` 索引已变更
|
||||
- [x] 13.4 验证删除的 H5 路由不再注册:检查代码中无 `/api/h5` 相关路由残留
|
||||
- [x] 13.5 验证 `BatchUpdatePricing` 接口扩展:确认 `pricing_target=retail_price` 参数可正常使用,不传时默认走 `cost_price` 逻辑(向后兼容)
|
||||
- [x] 13.5 验证 `BatchUpdatePricing` 接口仅支持成本价调整;并验证 `PATCH /api/admin/packages/:id/retail-price` 可供代理修改自己的零售价
|
||||
- [x] 13.6 验证代理套餐列表:确认 `PackageResponse` 包含 `retail_price` 字段,`profit_margin` 计算基于 `retail_price - cost_price`
|
||||
- [x] 13.7 撰写功能总结文档 `docs/client-api-data-model-fixes/功能总结.md`,记录所有变更内容
|
||||
@@ -1,6 +1,6 @@
|
||||
# personal-customer Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 个人客户登录主流程改为微信授权
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# wechat-official-account Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 微信配置源从 YAML 改为数据库动态读取
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# 设计文档:client-core-business-api
|
||||
|
||||
## Context
|
||||
|
||||
认证系统(提案 1)就绪后,客户端需要一套完整业务接口以覆盖资产查询、钱包充值、套餐购买、实名跳转与设备操作。当前后台接口的 Service 层大部分能力可复用,但客户端场景存在以下关键差异:
|
||||
|
||||
1. 个人客户访问资源不应受 shop_id 数据权限过滤影响,需要在调用链中显式绕过。
|
||||
2. 资产操作必须先做归属校验(绑定关系),避免跨客户操作。
|
||||
3. 历史数据查询需要按资产当前 generation 过滤,避免展示转手前历史。
|
||||
4. 支付 OpenID 必须后端查表获取,禁止客户端传入,降低伪造风险。
|
||||
|
||||
现有代码参考(复用/改造基线):
|
||||
|
||||
- `asset.Service.Resolve()`:`internal/service/asset/service.go:71`
|
||||
- `asset.Service.Refresh()`:`internal/service/asset/service.go:295`
|
||||
- `asset.Service.GetPackages()`:`internal/service/asset/service.go:347`
|
||||
- `recharge.Service.GetRechargeCheck()`:`internal/service/recharge/service.go:168`
|
||||
- `recharge.Service.Create()`:`internal/service/recharge/service.go:83`
|
||||
- `order.Service.CreateH5Order()`:`internal/service/order/service.go:632`
|
||||
- `order.Service.checkForceRechargeRequirement()`:`internal/service/order/service.go:2216`
|
||||
- `order.Service.WechatPayJSAPI()`:`internal/service/order/service.go:2095`
|
||||
- `purchaseValidation.ValidateCardPurchase()`:`internal/service/purchase_validation/service.go:44`
|
||||
- Gateway 设备能力:`internal/gateway/device.go:41-67`
|
||||
- Gateway 实名链接:`internal/gateway/flow_card.go:44`
|
||||
|
||||
## Goals
|
||||
|
||||
本次变更目标是交付 `/api/c/v1/` 下 18 个客户端业务端点,覆盖 5 个模块:
|
||||
|
||||
- 模块 B(资产信息):B1~B4
|
||||
- 模块 C(钱包与充值):C1~C5
|
||||
- 模块 D(套餐购买):D1~D3
|
||||
- 模块 E(实名跳转):E1
|
||||
- 模块 F(设备能力):F1~F5
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- 不改动后台管理端 API 行为与路由。
|
||||
- 不引入或改造 exchange(交易所)体系。
|
||||
- 不重做既有支付网关对接协议,仅在客户端入口补齐调用与安全约束。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1) Handler 组织
|
||||
|
||||
客户端 Handler 按模块拆分为 5 个文件,统一放在 `internal/handler/app/`:
|
||||
|
||||
- `client_asset.go`
|
||||
- `client_wallet.go`
|
||||
- `client_order.go`
|
||||
- `client_realname.go`
|
||||
- `client_device.go`
|
||||
|
||||
这样可保持模块边界清晰,减少单文件复杂度,便于后续迭代。
|
||||
|
||||
### 2) Service 复用策略
|
||||
|
||||
- 直接复用:B1/B3/B4/C1/C2/C3 复用现有 Service 能力(补充客户端上下文约束)。
|
||||
- 新增逻辑:B2 需新增“渠道价 + 加油包前置校验 + 不可售过滤 + 价格排序”逻辑。
|
||||
- 新建 `client_order` Service:C4/D1 引入客户端订单编排,复用 `order/recharge` 的底层能力但增加客户端专属流程控制。
|
||||
|
||||
### 3) 数据权限绕过
|
||||
|
||||
客户端调用 `asset/wallet` 等后台复用 Service 时,统一使用 `gorm.SkipDataPermission(ctx)`,绕过 shop_id 自动过滤,避免个人客户因非店铺主体被误拦截。
|
||||
|
||||
### 4) 归属校验方案
|
||||
|
||||
所有涉及资产操作接口统一前置:
|
||||
|
||||
- 查询 `PersonalCustomerDevice` 条件:`customer_id = 当前登录客户` 且 `virtual_no = 资产虚拟号`
|
||||
- 未命中即返回 403:`无权限操作该资产或资源不存在`
|
||||
|
||||
该规则覆盖 B/C/D/E/F 全模块写操作与敏感读操作。
|
||||
|
||||
### 5) Generation 过滤
|
||||
|
||||
客户端历史查询统一附加条件:`WHERE generation = 资产当前 generation`,适用于订单、充值、套餐历史。
|
||||
|
||||
- 客户端:必须过滤
|
||||
- 后台:不加该过滤(保留全量视图)
|
||||
|
||||
### 6) OpenID 安全规范 + 微信支付 SDK 实例选择
|
||||
|
||||
支付相关接口(C4/D1)所需 OpenID 必须由后端按 `customer_id + app_type` 查询 `PersonalCustomerOpenID`。
|
||||
|
||||
- 客户端请求体禁止携带 `openid`,仅传 `app_type`(`official_account` 或 `miniapp`)
|
||||
- 缺失时返回 `OPENID_NOT_FOUND`
|
||||
|
||||
**微信支付 SDK 实例选择逻辑**:
|
||||
|
||||
支付时需根据 `app_type` 创建不同的 `PaymentService` 实例,因为微信 JSAPI 支付绑定的 AppID 必须与用户 OpenID 所属的应用一致:
|
||||
|
||||
```
|
||||
客户端传入 app_type
|
||||
│
|
||||
├─ "official_account" → 用 WechatConfig.oa_app_id 创建 Payment 实例
|
||||
│ → 查 PersonalCustomerOpenID WHERE app_id=oa_app_id 获取 openid
|
||||
│
|
||||
└─ "miniapp" → 用 WechatConfig.miniapp_app_id 创建 Payment 实例
|
||||
→ 查 PersonalCustomerOpenID WHERE app_id=miniapp_app_id 获取 openid
|
||||
```
|
||||
|
||||
**使用的现有 SDK 方法**(`pkg/wechat/payment.go`,不需要修改):
|
||||
|
||||
| 方法 | 签名 | 用途 |
|
||||
|------|------|------|
|
||||
| `CreateJSAPIOrder` | `(ctx, orderNo, description, openID string, amount int) (*JSAPIPayResult, error)` | 公众号/小程序内拉起支付,返回 `prepay_id` + `PayConfig`(可直接传给前端 `wx.requestPayment`) |
|
||||
| `HandlePaymentNotify` | `(r *http.Request, callback PaymentNotifyCallback) (*http.Response, error)` | 支付回调验签+解密,回调函数接收 `*PaymentNotifyResult` |
|
||||
| `QueryOrder` | `(ctx, orderNo string) (*OrderInfo, error)` | 主动查询订单状态 |
|
||||
| `CloseOrder` | `(ctx, orderNo string) error` | 关闭未支付订单 |
|
||||
|
||||
**SDK 实例创建**(使用提案 1 新增的工厂函数):
|
||||
|
||||
```go
|
||||
// 在 client_order Service 中:
|
||||
config, _ := s.wechatConfigService.GetActiveConfig(ctx) // 从 DB/Redis 缓存
|
||||
appID := config.OaAppID
|
||||
if req.AppType == "miniapp" {
|
||||
appID = config.MiniappAppID
|
||||
}
|
||||
paymentApp, _ := wechat.NewPaymentAppFromConfig(config, appID, cache, logger)
|
||||
paymentService := wechat.NewPaymentService(paymentApp, logger)
|
||||
// 调用 paymentService.CreateJSAPIOrder(ctx, orderNo, desc, openID, amount)
|
||||
```
|
||||
|
||||
**注意**:`CreateH5Order`(外部浏览器 H5 支付)在客户端场景中**不使用**——客户端始终在微信内(公众号 H5 或小程序),一律走 JSAPI 支付。
|
||||
|
||||
### 7) 强充两阶段设计
|
||||
|
||||
强充场景采用“同步入账 + 异步自动购买”两阶段:
|
||||
|
||||
- 第一阶段(同步事务内):
|
||||
1. 钱入钱包
|
||||
2. 更新充值记录状态
|
||||
3. 更新累计充值/首充状态
|
||||
- 第二阶段(异步 Asynq):
|
||||
1. 从钱包扣款
|
||||
2. 创建套餐订单
|
||||
3. 激活套餐
|
||||
|
||||
`AssetRechargeRecord` 新增 `auto_purchase_status` 字段追踪异步状态(pending/success/failed)。
|
||||
|
||||
### 8) D1 返回结构分流
|
||||
|
||||
`POST /api/c/v1/orders/create` 根据是否触发强充返回不同结构:
|
||||
|
||||
- `order_type = "package"`:直接返回 `order + pay_config`
|
||||
- `order_type = "recharge"`:返回 `recharge + pay_config + linked_package_info`
|
||||
|
||||
前端据 `order_type` 决定支付结果页与文案。
|
||||
|
||||
### 9) 实名闭环说明
|
||||
|
||||
运营商实名为外部流程,无平台回调。用户完成实名后需主动触发 B4 刷新资产状态,再重新发起购买流程。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
1. **强充异步失败风险**:第二阶段失败会导致“钱已到账、套餐未生效”的中间态。权衡后采用可重试 + `auto_purchase_status=failed` + 用户可手动购买的降级方案。
|
||||
2. **Gateway 超时风险**:B4/F2/F3/F4/F5/E1(gateway) 依赖外部网关,网络抖动可能放大请求延迟。需统一超时、重试与可观测日志。
|
||||
3. **OpenID 缺失风险**:用户未完成公众号授权时无法拉起支付。需明确错误码 `OPENID_NOT_FOUND` 并引导重新授权。
|
||||
4. **Generation 不一致风险**:资产转手或切换后,若查询未按 generation 过滤会出现历史串数据。客户端侧强制过滤会增加查询条件复杂度,但可换取数据隔离正确性。
|
||||
5. **服务复用边界风险**:复用后台 Service 可加速交付,但若遗漏客户端前置条件(归属、权限绕过),会造成逻辑缺口。需在 Handler 层统一封装公共校验。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
数据库迁移新增字段:
|
||||
|
||||
- 表:`tb_asset_recharge_record`
|
||||
- 字段:`auto_purchase_status`(建议 `varchar(20)`,默认 `pending`)
|
||||
- 用途:记录强充回调后二阶段自动购买状态
|
||||
|
||||
迁移步骤:
|
||||
|
||||
1. 新增迁移文件(up/down)。
|
||||
2. 执行迁移并确认版本无 dirty。
|
||||
3. 更新对应 Model 与常量枚举。
|
||||
4. 回调逻辑写入状态流转:`pending -> success/failed`。
|
||||
@@ -0,0 +1,175 @@
|
||||
## Why
|
||||
|
||||
认证系统就绪后(提案 1),客户端需要完整的业务接口来支撑核心使用场景:查看资产信息、购买套餐、钱包充值、查看订单、实名认证跳转、设备操作。本提案覆盖客户端**全部 15 个业务接口**,是 C 端体验的核心支撑。
|
||||
|
||||
其中**套餐购买含强充两阶段处理**是最复杂的接口,涉及强充判断 → 微信支付 → 充值回调 → 异步套餐购买的完整链路,需要特别严谨的设计。
|
||||
|
||||
**前置依赖**:提案 0(数据模型修复)、提案 1(认证系统)。
|
||||
|
||||
## What Changes
|
||||
|
||||
### 模块 B:资产信息(4 个接口)
|
||||
|
||||
- **B1 资产基本信息** `GET /api/c/v1/asset/info?identifier=xxx`:复用 `asset.Service.Resolve()`,个人客户调用不走 shop_id 数据权限过滤
|
||||
- **B2 可购买套餐列表** `GET /api/c/v1/asset/packages?identifier=xxx`:按渠道区分价格(代理→`allocation.retail_price`,平台→`SuggestedRetailPrice`);过滤条件包含 `Package.status`、`shelf_status`、加油包前置校验;按价格升序排序
|
||||
- **B3 历史套餐列表** `GET /api/c/v1/asset/package-history?identifier=xxx`:按资产当前 `generation` 过滤,复用 `dto.AssetPackageResponse`
|
||||
- **B4 手动刷新** `POST /api/c/v1/asset/refresh`:卡类型调 Gateway 刷新;设备类型有 Redis 冷却时间
|
||||
|
||||
### 模块 C:钱包与充值(5 个接口)
|
||||
|
||||
- **C1 钱包详情** `GET /api/c/v1/wallet/detail?identifier=xxx`:不存在则自动创建空钱包
|
||||
- **C2 钱包流水列表** `GET /api/c/v1/wallet/transactions?identifier=xxx`:通过 wallet_id 天然隔离(不需 generation 过滤),支持 transaction_type / 时间范围筛选
|
||||
- **C3 充值预检** `GET /api/c/v1/wallet/recharge-check?identifier=xxx`:复用 `recharge.Service.GetRechargeCheck()`,返回是否需要强充、强充金额、触发类型
|
||||
- **C4 创建充值订单** `POST /api/c/v1/wallet/recharge`:客户端仅支持微信支付;OpenID 由后端查表获取(安全规范);`operator_type=personal_customer`;`generation` 写时快照;拉起 JSAPI 支付
|
||||
- **C5 充值订单列表** `GET /api/c/v1/wallet/recharges?identifier=xxx`:按 `generation` 过滤
|
||||
|
||||
### 模块 D:套餐购买(3 个接口,含核心强充流程)
|
||||
|
||||
- **D1 创建套餐购买订单** `POST /api/c/v1/orders/create`:**核心接口**。含资产归属校验、套餐校验、实名校验、强充两阶段处理、幂等性保证
|
||||
- **D2 套餐订单列表** `GET /api/c/v1/orders?identifier=xxx`:按 `generation` 过滤
|
||||
- **D3 套餐订单详情** `GET /api/c/v1/orders/:id`:归属校验(通过资产虚拟号匹配 PersonalCustomerDevice)
|
||||
|
||||
### 模块 E:实名认证(1 个接口)
|
||||
|
||||
- **E1 获取实名跳转链接** `GET /api/c/v1/realname/link?identifier=xxx&iccid=xxx`:两个入口(购买拦截 / 设备卡列表主动选择);三种模式(none/template/gateway)
|
||||
|
||||
### 模块 F:设备能力(5 个接口)
|
||||
|
||||
- **F1 设备卡列表** `GET /api/c/v1/device/cards?identifier=xxx`:从 CMP 数据库查,不调 Gateway
|
||||
- **F2 设备重启** `POST /api/c/v1/device/reboot`
|
||||
- **F3 恢复出厂** `POST /api/c/v1/device/factory-reset`
|
||||
- **F4 设置 WiFi** `POST /api/c/v1/device/wifi`:注意 Gateway WiFiReq 的 cardNo 字段实际传入设备 IMEI
|
||||
- **F5 切卡** `POST /api/c/v1/device/switch-card`
|
||||
|
||||
### D1 套餐购买核心流程(含强充两阶段)
|
||||
|
||||
```
|
||||
客户端发起 POST /api/c/v1/orders/create
|
||||
│
|
||||
▼
|
||||
① 解析标识符 → card/device + asset_type + asset_id
|
||||
│
|
||||
▼
|
||||
② 资产归属校验
|
||||
查 PersonalCustomerDevice WHERE customer_id=? AND virtual_no=?
|
||||
未绑定 → 403 "无权操作该资产"
|
||||
│
|
||||
▼
|
||||
③ 套餐购买校验
|
||||
调 purchaseValidationService.ValidateCardPurchase/ValidateDevicePurchase
|
||||
检查: series_id、Package.status、shelf_status、加油包前置条件
|
||||
│
|
||||
▼
|
||||
④ 实名校验
|
||||
套餐 enable_realname_activation=true 且卡 real_name_status=0
|
||||
→ 返回 { code: NEED_REALNAME, need_realname: true }
|
||||
│
|
||||
▼
|
||||
⑤ OpenID 查询
|
||||
根据 customer_id + app_type 从 PersonalCustomerOpenID 查询 openid
|
||||
找不到 → 返回 { code: OPENID_NOT_FOUND }
|
||||
│
|
||||
▼
|
||||
⑥ 幂等性检查(Redis 业务键 + 分布式锁)
|
||||
│
|
||||
▼
|
||||
⑦ 强充检查
|
||||
调 checkForceRechargeRequirement()
|
||||
│
|
||||
├─── 不需要强充 ──────────────────────────────┐
|
||||
│ │
|
||||
│ ⑧A 创建 Order │
|
||||
│ (source="client", generation=当前) │
|
||||
│ 拉起微信 JSAPI 支付 │
|
||||
│ → 返回 { order_type: "package", │
|
||||
│ order, pay_config } │
|
||||
│ │
|
||||
└─── 需要强充 ────────────────────────┐ │
|
||||
│ │ │
|
||||
▼ │ │
|
||||
pay_amount = max(force_amount, │ │
|
||||
package_total_price) │ │
|
||||
│ │ │
|
||||
▼ │ │
|
||||
⑧B 创建 AssetRechargeRecord │ │
|
||||
(linked_package_ids=package_ids, │ │
|
||||
generation=当前) │ │
|
||||
拉起微信 JSAPI 支付 │ │
|
||||
→ 返回 { order_type: "recharge", │ │
|
||||
recharge, pay_config, │ │
|
||||
linked_package_info } │ │
|
||||
│ │
|
||||
═══════════════════════════════════════════╧════════╧═══
|
||||
|
||||
强充支付成功后的两阶段回调处理:
|
||||
|
||||
微信支付成功 → 充值回调
|
||||
│
|
||||
▼
|
||||
第一阶段(同步,事务内):
|
||||
├── 1. 钱入钱包(余额增加)
|
||||
├── 2. 更新充值单状态为已完成
|
||||
├── 3. 更新累计/首充状态
|
||||
└── 4. 检查一次性佣金触发
|
||||
│
|
||||
▼
|
||||
第二阶段(异步,Asynq 任务):
|
||||
├── 5. 入队 AutoPurchaseAfterRecharge(recharge_record_id)
|
||||
├── 6. 异步执行:
|
||||
│ ├── a. 从钱包扣款(payment_method=wallet)
|
||||
│ ├── b. 创建 Order(source="client", generation=当前)
|
||||
│ └── c. 激活套餐
|
||||
└── 7. 失败处理:
|
||||
├── a. Asynq 自动重试(最多 3 次)
|
||||
├── b. 全部失败 → 标记 auto_purchase_status="failed"
|
||||
└── c. 钱已在钱包中,用户可手动操作
|
||||
```
|
||||
|
||||
### 实名跳转流程
|
||||
|
||||
```
|
||||
前端调用 GET /api/c/v1/realname/link?identifier=xxx[&iccid=yyy]
|
||||
│
|
||||
▼
|
||||
解析标识符 → 确定目标卡:
|
||||
├── 直接是卡 → 用该卡
|
||||
├── 是设备 + 传了 iccid → 查该 iccid 对应的卡
|
||||
└── 是设备 + 没传 iccid → 查 DeviceSimBinding 中 isActive=1 的卡
|
||||
│
|
||||
▼
|
||||
检查 card.real_name_status == 1?
|
||||
├── YES → "该卡已完成实名"
|
||||
└── NO
|
||||
│
|
||||
▼
|
||||
查 Carrier WHERE id=card.carrier_id → 获取 realname_link_type:
|
||||
├── 'none' → "该运营商暂不支持在线实名"
|
||||
├── 'template' → 替换占位符 {iccid}/{msisdn}/{virtual_no} → 返回 URL
|
||||
└── 'gateway' → 调 gateway.GetRealnameLink(card.ICCID) → 返回 URL
|
||||
```
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `client-asset-info`:客户端资产信息查询(B1)、可购买套餐列表(B2,含渠道价格、加油包校验、上下架过滤)、历史套餐列表(B3,含 generation 过滤)、手动刷新(B4)
|
||||
- `client-wallet-recharge`:客户端钱包详情(C1)、流水列表(C2)、充值预检(C3)、创建充值订单(C4,含 OpenID 安全规范、operator_type、generation 快照)、充值订单列表(C5)
|
||||
- `client-order-purchase`:套餐购买订单创建(D1,含归属校验、实名校验、强充两阶段、幂等性)、订单列表(D2)、订单详情(D3)、强充回调异步购买(AutoPurchaseAfterRecharge Asynq 任务)
|
||||
- `client-realname-link`:实名跳转链接(E1),三种模式、两个入口、设备多卡选择
|
||||
- `client-device-capability`:设备卡列表(F1)、重启(F2)、恢复出厂(F3)、WiFi 设置(F4)、切卡(F5)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `asset-resolve`:Resolve 方法增加"无数据权限过滤"的客户端调用入口
|
||||
- `wallet-recharge`:充值回调增加两阶段处理——同步入账 + 异步自动购买;AssetRechargeRecord 新增 `auto_purchase_status` 字段跟踪异步购买状态
|
||||
- `package-purchase-validation`:增加客户端场景的归属校验(PersonalCustomerDevice)和实名校验拦截
|
||||
- `iot-order`:Order 新增客户端创建路径(source="client"),支持 generation 写入
|
||||
- `force-recharge-check`:强充检查结果输出给客户端,支持前端提示强充金额和套餐价格拆分
|
||||
|
||||
## Impact
|
||||
|
||||
- **新增文件**:`internal/handler/app/client_asset.go`、`client_wallet.go`、`client_order.go`、`client_realname.go`、`client_device.go`(5 个 Handler);`internal/service/client_order/service.go`(客户端订单 Service);新增 Asynq 任务 `AutoPurchaseAfterRecharge`;新增 DTO 文件;常量和错误码
|
||||
- **修改文件**:`internal/service/order/service.go`(提取强充逻辑供客户端复用);`internal/service/recharge/service.go`(充值回调增加两阶段处理);`internal/service/asset/service.go`(增加无数据权限调用方式);`internal/routes/personal.go`(新增客户端业务路由);`internal/bootstrap/`(注册新模块);`cmd/api/docs.go` + `cmd/gendocs/main.go`(文档生成器)
|
||||
- **新增 API 路由**:`/api/c/v1/` 下 18 个端点
|
||||
- **数据库变更**:AssetRechargeRecord 新增 `auto_purchase_status` 字段
|
||||
- **新增 Asynq 任务类型**:`task:auto_purchase_after_recharge`
|
||||
@@ -0,0 +1,41 @@
|
||||
# Capability: 客户端资产信息
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: B1 资产基本信息查询接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/asset/info?identifier=xxx`,并且 MUST 要求个人客户认证(C 端 Token)。接口 MUST 复用 `asset.Service.Resolve()` 解析标识符,并在调用时使用 `gorm.SkipDataPermission(ctx)` 以绕过 shop_id 数据权限过滤。请求参数 MUST 包含 `identifier`(ICCID、虚拟号、设备号之一)。响应体 SHALL 返回 `asset_type`、`asset_id`、`identifier`、`virtual_no`、`status`、`real_name_status`、`carrier`、`generation`、`wallet_balance`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`ASSET_NOT_FOUND/资产不存在`。
|
||||
|
||||
#### Scenario: 个人客户查询已绑定资产
|
||||
- **WHEN** 客户携带有效 Token 调用 `GET /api/c/v1/asset/info?identifier=8986xxxx` 且资产已绑定到本人
|
||||
- **THEN** 系统返回 200,包含资产基础信息与当前 generation
|
||||
|
||||
---
|
||||
|
||||
### Requirement: B2 可购买套餐列表接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/asset/packages?identifier=xxx`,并且 MUST 要求个人客户认证。接口 MUST 在归属校验通过后返回可购买套餐列表。价格规则 MUST 为:代理渠道取 `allocation.retail_price`,平台渠道取 `Package.SuggestedRetailPrice`。过滤规则 MUST 同时满足:`Package.status=1`、`shelf_status` 可售、加油包前置主套餐条件成立、`retail_price >= cost_price`。结果 MUST 按展示价格升序。响应体 SHALL 包含 `packages[]`,每项至少含 `package_id`、`package_name`、`package_type`、`retail_price`、`cost_price`、`validity`、`is_addon`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`PACKAGE_NOT_AVAILABLE/当前无可购买套餐`。
|
||||
|
||||
#### Scenario: 代理渠道价格与过滤生效
|
||||
- **WHEN** 客户查询可购套餐且其销售链路为代理渠道,部分套餐存在 `retail_price < cost_price`
|
||||
- **THEN** 系统仅返回可售且满足价格约束的套餐,并按价格升序输出
|
||||
|
||||
---
|
||||
|
||||
### Requirement: B3 历史套餐列表接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/asset/package-history?identifier=xxx&page=1&page_size=20`,并且 MUST 要求个人客户认证。接口 MUST 基于标识符解析资产并进行归属校验。查询条件 MUST 自动追加 `generation = 资产当前generation`。请求参数 SHALL 支持 `page`、`page_size`(默认 20,最大 100)。响应体 SHALL 返回 `list[]`、`total`、`page`、`page_size`,列表项复用 `dto.AssetPackageResponse` 结构。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`。
|
||||
|
||||
#### Scenario: 转手后历史隔离
|
||||
- **WHEN** 资产已发生转手且存在历史套餐记录
|
||||
- **THEN** 系统只返回当前 generation 的记录,不返回旧 generation 数据
|
||||
|
||||
---
|
||||
|
||||
### Requirement: B4 手动刷新接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/c/v1/asset/refresh`,并且 MUST 要求个人客户认证。请求体 MUST 包含 `identifier`。当资产为卡时 MUST 调用 Gateway 刷新卡信息;当资产为设备时 MUST 先检查 Redis 冷却窗口,再对设备下卡执行批量刷新。响应体 SHALL 返回 `refresh_type`(`card`/`device`)、`accepted`、`cooldown_seconds`(设备场景)。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`TOO_MANY_REQUESTS/刷新过于频繁,请稍后重试`、`GATEWAY_ERROR/网关调用失败`。
|
||||
|
||||
#### Scenario: 设备刷新冷却拦截
|
||||
- **WHEN** 客户在冷却时间内重复调用设备刷新
|
||||
- **THEN** 系统返回频率限制错误并告知剩余冷却时间
|
||||
@@ -0,0 +1,51 @@
|
||||
# Capability: 客户端设备能力
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: F1 设备卡列表接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/device/cards?identifier=xxx`,并且 MUST 要求个人客户认证。接口 MUST 仅允许设备类型资产调用,且设备 MUST 具备 IMEI。响应体 SHALL 返回 `cards[]`,每项至少包含:`card_id`、`iccid`、`msisdn`、`carrier_name`、`network_status`、`real_name_status`、`slot_position`、`is_active`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`ASSET_TYPE_INVALID/仅设备资产支持该操作`、`DEVICE_IMEI_REQUIRED/设备IMEI缺失`。
|
||||
|
||||
#### Scenario: 返回设备绑定卡列表
|
||||
- **WHEN** 客户查询已绑定设备卡列表
|
||||
- **THEN** 系统返回设备下全部卡及活跃标记
|
||||
|
||||
---
|
||||
|
||||
### Requirement: F2 设备重启接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/c/v1/device/reboot`,并且 MUST 要求个人客户认证。请求体 MUST 包含 `identifier`。接口 MUST 仅允许设备类型且 IMEI 存在,并调用 `gateway.RebootDevice(imei)`。响应体 SHALL 返回 `accepted=true` 与 `request_id`(如网关返回)。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`ASSET_TYPE_INVALID/仅设备资产支持该操作`、`DEVICE_IMEI_REQUIRED/设备IMEI缺失`、`GATEWAY_ERROR/网关调用失败`。
|
||||
|
||||
#### Scenario: 设备重启成功受理
|
||||
- **WHEN** 客户对合法设备发起重启
|
||||
- **THEN** 系统调用网关成功并返回受理结果
|
||||
|
||||
---
|
||||
|
||||
### Requirement: F3 设备恢复出厂接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/c/v1/device/factory-reset`,并且 MUST 要求个人客户认证。请求体 MUST 包含 `identifier`。接口 MUST 仅允许设备类型且 IMEI 存在,并调用 `gateway.ResetDevice(imei)`。响应体 SHALL 返回 `accepted=true`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`ASSET_TYPE_INVALID/仅设备资产支持该操作`、`DEVICE_IMEI_REQUIRED/设备IMEI缺失`、`GATEWAY_ERROR/网关调用失败`。
|
||||
|
||||
#### Scenario: 恢复出厂失败返回网关错误
|
||||
- **WHEN** 网关返回失败
|
||||
- **THEN** 系统返回网关调用失败错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: F4 设备 WiFi 设置接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/c/v1/device/wifi`,并且 MUST 要求个人客户认证。请求体 MUST 包含 `identifier`、`ssid`、`password`、`enabled`。接口 MUST 仅允许设备类型且 IMEI 存在,并调用 `gateway.SetWiFi(imei, ssid, password, enabled)`。实现 MUST 将 Gateway 的 `WiFiReq.cardNo` 填充为设备 IMEI。响应体 SHALL 返回 `accepted=true`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`ASSET_TYPE_INVALID/仅设备资产支持该操作`、`DEVICE_IMEI_REQUIRED/设备IMEI缺失`、`GATEWAY_ERROR/网关调用失败`。
|
||||
|
||||
#### Scenario: WiFi 请求 cardNo 使用 IMEI
|
||||
- **WHEN** 客户调用设备 WiFi 设置
|
||||
- **THEN** 系统向网关发送的 `cardNo` 字段值为设备 IMEI
|
||||
|
||||
---
|
||||
|
||||
### Requirement: F5 设备切卡接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/c/v1/device/switch-card`,并且 MUST 要求个人客户认证。请求体 MUST 包含 `identifier`、`target_iccid`。接口 MUST 仅允许设备类型且 IMEI 存在,并调用 `gateway.SwitchCard(imei, target_iccid)`。响应体 SHALL 返回 `accepted=true`、`target_iccid`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`ASSET_TYPE_INVALID/仅设备资产支持该操作`、`DEVICE_IMEI_REQUIRED/设备IMEI缺失`、`GATEWAY_ERROR/网关调用失败`。
|
||||
|
||||
#### Scenario: 切卡成功返回目标卡号
|
||||
- **WHEN** 客户请求切换到目标 ICCID 且网关执行成功
|
||||
- **THEN** 系统返回 `accepted=true` 与目标 ICCID
|
||||
@@ -0,0 +1,46 @@
|
||||
# Capability: 客户端套餐购买
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: D1 创建套餐购买订单接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/c/v1/orders/create`,并且 MUST 要求个人客户认证。请求体 MUST 包含 `identifier`、`package_ids[]`、`app_type`。接口流程 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`
|
||||
|
||||
响应体 MUST 包含前端可直接渲染字段。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`NEED_REALNAME/该套餐需实名认证后购买`、`OPENID_NOT_FOUND/未找到微信授权信息,请先完成授权`、`IDEMPOTENT_CONFLICT/请求处理中,请勿重复提交`、`PACKAGE_NOT_AVAILABLE/套餐不可购买`。
|
||||
|
||||
#### Scenario: 命中强充返回 recharge 结构
|
||||
- **WHEN** 客户购买套餐触发强充要求
|
||||
- **THEN** 系统返回 `order_type="recharge"`,包含充值单与关联套餐信息
|
||||
|
||||
---
|
||||
|
||||
### 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`
|
||||
@@ -0,0 +1,23 @@
|
||||
# Capability: 客户端实名跳转
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: E1 获取实名跳转链接接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/realname/link?identifier=xxx&iccid=xxx`,并且 MUST 要求个人客户认证。该接口 MUST 支持两类入口:购买拦截入口与设备卡列表主动入口。目标卡定位 MUST 支持三种路径:
|
||||
|
||||
1. 标识符直达卡:直接使用该卡
|
||||
2. 标识符为设备且传 `iccid`:定位对应设备下卡
|
||||
3. 标识符为设备且未传 `iccid`:定位设备当前活跃卡
|
||||
|
||||
当 `real_name_status=1` 时 MUST 返回“该卡已完成实名”错误。运营商实名模式 MUST 支持:
|
||||
|
||||
- `none`:不支持在线实名,直接报错
|
||||
- `template`:按模板替换占位符 `{iccid}` `{msisdn}` `{virtual_no}` 返回 URL
|
||||
- `gateway`:调用网关获取实名链接
|
||||
|
||||
响应体 SHALL 至少包含 `realname_mode`、`realname_url`、`card_info{iccid,msisdn,virtual_no}`、`expire_at`(可空)。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`REALNAME_ALREADY_DONE/该卡已完成实名`、`REALNAME_NOT_SUPPORTED/该运营商暂不支持在线实名`、`GATEWAY_ERROR/获取实名链接失败`。
|
||||
|
||||
#### Scenario: 设备未传 iccid 自动选活跃卡
|
||||
- **WHEN** 客户传入设备标识符且不传 `iccid`
|
||||
- **THEN** 系统自动选择设备活跃卡并返回实名跳转链接
|
||||
@@ -0,0 +1,51 @@
|
||||
# Capability: 客户端钱包与充值
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: C1 钱包详情接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/wallet/detail?identifier=xxx`,并且 MUST 要求个人客户认证。接口 MUST 先完成资产解析与归属校验;钱包不存在时 MUST 自动创建空钱包。响应体 SHALL 包含 `wallet_id`、`resource_type`、`resource_id`、`balance`、`frozen_balance`、`updated_at`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`。
|
||||
|
||||
#### Scenario: 首次访问自动建钱包
|
||||
- **WHEN** 客户查询资产钱包详情且钱包记录不存在
|
||||
- **THEN** 系统自动创建钱包并返回余额 0
|
||||
|
||||
---
|
||||
|
||||
### Requirement: C2 钱包流水列表接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/wallet/transactions?identifier=xxx`,并且 MUST 要求个人客户认证。接口 MUST 通过归属校验解析出唯一 `wallet_id` 后查询流水,实现天然隔离。请求参数 SHALL 支持 `transaction_type`、`start_time`、`end_time`、`page`、`page_size`。响应体 SHALL 包含 `list[]`、`total`、`page`、`page_size`,每条记录至少含 `transaction_id`、`type`、`amount`、`balance_after`、`created_at`、`remark`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`。
|
||||
|
||||
#### Scenario: wallet_id 隔离生效
|
||||
- **WHEN** 客户查询某资产流水
|
||||
- **THEN** 系统仅返回该资产钱包对应流水,不返回其他钱包数据
|
||||
|
||||
---
|
||||
|
||||
### Requirement: C3 充值预检接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/wallet/recharge-check?identifier=xxx`,并且 MUST 要求个人客户认证。接口 MUST 复用 `recharge.Service.GetRechargeCheck()` 计算强充规则。响应体 SHALL 包含 `need_force_recharge`、`force_recharge_amount`、`trigger_type`、`min_amount`、`max_amount`、`message`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`。
|
||||
|
||||
#### Scenario: 返回强充预检结果
|
||||
- **WHEN** 资产命中强充规则
|
||||
- **THEN** 系统返回 `need_force_recharge=true` 与对应强充金额和触发类型
|
||||
|
||||
---
|
||||
|
||||
### Requirement: C4 创建充值订单接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/c/v1/wallet/recharge`,并且 MUST 要求个人客户认证。请求体 MUST 包含:`identifier`、`amount`(100~10000000 分)、`payment_method=wechat`、`app_type`。接口 MUST 禁止客户端传入 OpenID,并由后端按 `customer_id + app_type` 查询 OpenID。订单创建时 MUST 写入:`operator_type=personal_customer` 与资产当前 `generation` 快照。响应体 SHALL 返回 `recharge` 与 `pay_config`,其中 `recharge` 至少含 `recharge_id`、`recharge_no`、`amount`、`status`,`pay_config` 为微信 JSAPI 拉起参数。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`OPENID_NOT_FOUND/未找到微信授权信息,请先完成授权`、`FORBIDDEN/无权限操作该资产或资源不存在`、`PAYMENT_NOT_SUPPORTED/仅支持微信支付`。
|
||||
|
||||
#### Scenario: 后端查 OpenID 并返回支付参数
|
||||
- **WHEN** 客户传入合法参数且后端成功查询到 OpenID
|
||||
- **THEN** 系统创建充值单并返回 `recharge + pay_config`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: C5 充值订单列表接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/wallet/recharges?identifier=xxx`,并且 MUST 要求个人客户认证。接口 MUST 在归属校验后按资产当前 generation 过滤充值记录。请求参数 SHALL 支持 `status`、`page`、`page_size`。响应体 SHALL 返回 `list[]`、`total`、`page`、`page_size`,每项至少含 `recharge_id`、`recharge_no`、`amount`、`status`、`payment_method`、`created_at`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`。
|
||||
|
||||
#### Scenario: generation 过滤充值历史
|
||||
- **WHEN** 资产存在多代充值记录
|
||||
- **THEN** 系统仅返回当前 generation 对应的充值记录
|
||||
@@ -0,0 +1,21 @@
|
||||
# Capability: 强充预检
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 强充检查结果对客户端透出
|
||||
|
||||
系统 MUST 将强充检查结果输出给客户端接口(充值预检与购买预检),用于前端明确展示支付拆分。输出字段 SHALL 至少包含:`need_force_recharge`、`force_recharge_amount`、`trigger_type`、`total_package_amount`、`actual_payment`、`wallet_credit`、`message`。若无强充,`need_force_recharge=false` 且 `actual_payment=total_package_amount`。
|
||||
|
||||
#### Scenario: 客户端购买预检命中强充
|
||||
- **WHEN** 客户端调用购买预检且命中强充规则
|
||||
- **THEN** 系统返回强充金额、实际支付金额和钱包入账金额
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 前端展示套餐价与强充金额拆分
|
||||
|
||||
系统 SHALL 在强充场景提供可直接渲染的拆分语义:套餐总价、需支付金额、充值入钱包金额,并给出中文提示文案。当前端调用客户端下单接口(D1)时,若命中强充 MUST 返回 `order_type="recharge"` 与 `linked_package_info`,以便前端保持与预检展示一致。
|
||||
|
||||
#### Scenario: 套餐价低于强充金额
|
||||
- **WHEN** 套餐总价 5000 分,强充金额 10000 分
|
||||
- **THEN** 预检返回 `actual_payment=10000`、`wallet_credit=5000`、提示文案可用于前端直接展示
|
||||
@@ -0,0 +1,33 @@
|
||||
# Capability: 钱包充值
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 充值回调采用两阶段处理
|
||||
|
||||
系统 MUST 将强充场景的充值回调改为两阶段:第一阶段同步事务内完成入账与状态更新,第二阶段异步执行自动购买。第一阶段 SHALL 包含:更新充值状态、钱包加款、累计充值更新、首充佣金判断。第二阶段 SHALL 通过 Asynq 任务执行钱包扣款、创建套餐订单、激活套餐。该改造适用于客户端触发的强充路径,且不影响非强充充值主流程。
|
||||
|
||||
#### Scenario: 强充回调同步入账成功并触发异步任务
|
||||
- **WHEN** 强充充值支付回调验签成功
|
||||
- **THEN** 系统在事务内完成钱包入账与充值单状态更新
|
||||
- **AND** 入队 `AutoPurchaseAfterRecharge` 异步任务
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值记录新增 auto_purchase_status 状态追踪
|
||||
|
||||
系统 MUST 在 `AssetRechargeRecord` 增加 `auto_purchase_status` 字段,用于追踪强充后二阶段自动购买状态。状态集 SHALL 至少包括:`pending`、`success`、`failed`。创建强充充值单时 MUST 初始化为 `pending`;异步购买成功后 MUST 更新为 `success`;重试耗尽后 MUST 更新为 `failed`。
|
||||
|
||||
#### Scenario: 强充充值单创建时默认 pending
|
||||
- **WHEN** 系统创建与套餐联动的强充充值单
|
||||
- **THEN** 充值记录 `auto_purchase_status` 初始化为 `pending`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 异步自动购买失败处理规范
|
||||
|
||||
系统 SHALL 对 `AutoPurchaseAfterRecharge` 失败场景执行统一处理:任务 MUST 自动重试(最多 3 次);全部失败后 MUST 记录错误日志并将 `auto_purchase_status` 置为 `failed`;用户资金 SHALL 保留在钱包中,允许后续手动购买,不得回滚已成功的充值入账。
|
||||
|
||||
#### Scenario: 异步任务最终失败
|
||||
- **WHEN** 自动购买任务连续失败并达到最大重试次数
|
||||
- **THEN** 系统将 `auto_purchase_status` 标记为 `failed`
|
||||
- **AND** 钱包余额保持可用,用户可手动下单
|
||||
@@ -0,0 +1,70 @@
|
||||
## 1. 常量与错误码
|
||||
|
||||
- [x] 1.1 在 `pkg/constants/constants.go` 增加 `auto_purchase_status` 状态常量(pending/success/failed)
|
||||
- [x] 1.2 在 `pkg/errors/codes.go` 增加 `NEED_REALNAME` 与 `OPENID_NOT_FOUND` 错误码并补充中文消息
|
||||
- [x] 1.3 在 `pkg/constants/redis.go` 增加客户端购买幂等键与锁键生成函数
|
||||
|
||||
## 2. DTO 定义
|
||||
|
||||
- [x] 2.1 新增资产模块 DTO:B1/B2/B3/B4 请求与响应结构(含 description/validate 标签)
|
||||
- [x] 2.2 新增钱包充值模块 DTO:C1~C5 请求与响应结构(含支付返回 `pay_config`)
|
||||
- [x] 2.3 新增订单模块 DTO:D1~D3 请求与响应结构(含 `order_type` 分流结构)
|
||||
- [x] 2.4 新增实名与设备模块 DTO:E1、F1~F5 请求与响应结构
|
||||
|
||||
## 3. 模型变更与迁移
|
||||
|
||||
- [x] 3.1 在 `internal/model/asset_recharge_record.go` 增加 `auto_purchase_status` 字段与中文注释
|
||||
- [x] 3.2 创建迁移文件为 `tb_asset_recharge_record` 添加 `auto_purchase_status` 字段(up/down)
|
||||
- [x] 3.3 执行迁移并确认版本状态正常(非 dirty)
|
||||
|
||||
## 4. 资产信息模块(B1~B4)
|
||||
|
||||
- [x] 4.1 新建 `internal/handler/app/client_asset.go` 并实现 B1~B4 路由处理
|
||||
- [x] 4.2 抽取公共方法 `resolveAssetFromIdentifier`(解析标识符 + 归属校验 + 权限绕过上下文)
|
||||
- [x] 4.3 实现 B2 渠道价格计算、加油包前置校验、上下架过滤与价格升序
|
||||
- [x] 4.4 实现 B4 刷新分流:卡走 Gateway、设备走 Redis 冷却 + 批量刷新
|
||||
|
||||
## 5. 钱包与充值模块(C1~C5)
|
||||
|
||||
- [x] 5.1 新建 `internal/handler/app/client_wallet.go` 并实现 C1~C5 路由处理
|
||||
- [x] 5.2 实现 C1 钱包不存在自动创建逻辑与 C2 wallet_id 隔离查询
|
||||
- [x] 5.3 复用并接入 C3 强充预检返回结构
|
||||
- [x] 5.4 实现 C4 创建充值订单:根据 `app_type` 查 PersonalCustomerOpenID 获取 openid → 根据 `app_type` 选择 AppID(`official_account` 用 `oa_app_id`,`miniapp` 用 `miniapp_app_id`)→ 调用提案 1 新增的 `wechat.NewPaymentAppFromConfig(config, appID)` 创建支付实例 → 调用现有 `PaymentService.CreateJSAPIOrder(orderNo, desc, openID, amount)` 拉起支付 → 设置 `operator_type=personal_customer`、写入 `generation` 快照
|
||||
- [x] 5.5 实现 C5 充值记录 generation 过滤查询
|
||||
|
||||
## 6. 套餐购买模块(D1~D3)
|
||||
|
||||
- [x] 6.1 新建 `internal/handler/app/client_order.go` 并实现 D1~D3 路由处理
|
||||
- [x] 6.2 新建 `internal/service/client_order/service.go` 编排 D1 全流程(归属/套餐/实名/OpenID/幂等/强充分流)。支付调用链:根据 `app_type` 选择 AppID → `wechat.NewPaymentAppFromConfig(config, appID)` → `PaymentService.CreateJSAPIOrder()` → 返回 `pay_config` 给前端。**客户端一律走 JSAPI 支付(微信内环境),不使用 H5 支付**
|
||||
- [x] 6.3 实现强充两阶段:同步入账 + 异步自动购买(Asynq 入队)。注意第二阶段自动购买创建的订单使用 `payment_method=wallet`(钱包扣款),不涉及微信支付
|
||||
- [x] 6.4 新增 `AutoPurchaseAfterRecharge` 任务处理器(钱包扣款→创建订单→激活套餐,失败重试 3 次)
|
||||
- [x] 6.5 实现 D1 `order_type` 双结构返回(package/recharge)
|
||||
- [x] 6.6 实现 D2/D3 generation 与归属校验约束
|
||||
|
||||
## 7. 实名跳转(E1)
|
||||
|
||||
- [x] 7.1 新建 `internal/handler/app/client_realname.go` 并实现 E1 接口
|
||||
- [x] 7.2 实现目标卡定位三路径(直接卡/设备+iccid/设备活跃卡)
|
||||
- [x] 7.3 实现实名模式三分支(none/template/gateway)与模板占位符替换
|
||||
|
||||
## 8. 设备能力(F1~F5)
|
||||
|
||||
- [x] 8.1 新建 `internal/handler/app/client_device.go` 并实现 F1~F5 路由处理
|
||||
- [x] 8.2 实现通用前置校验(必须设备类型且 IMEI 非空)
|
||||
- [x] 8.3 对接 Gateway:`RebootDevice`、`ResetDevice`、`SetWiFi`、`SwitchCard`
|
||||
- [x] 8.4 实现 F4 特殊映射:`WiFiReq.cardNo = 设备IMEI`
|
||||
|
||||
## 9. 路由注册与文档
|
||||
|
||||
- [x] 9.1 在 `internal/bootstrap/types.go` 增加客户端业务 Handler 字段
|
||||
- [x] 9.2 在 `internal/bootstrap/handlers.go` 完成客户端业务 Handler 实例化
|
||||
- [x] 9.3 在 `internal/routes/personal.go` 注册 `/api/c/v1/` 18 个端点(使用 `Register()`)
|
||||
- [x] 9.4 在 `cmd/api/docs.go` 注册新增 Handler 供文档生成器使用
|
||||
- [x] 9.5 在 `cmd/gendocs/main.go` 注册新增 Handler 并生成 OpenAPI 文档
|
||||
|
||||
## 10. 验证
|
||||
|
||||
- [x] 10.1 运行 `go build ./...` 确认构建通过
|
||||
- [x] 10.2 运行 LSP diagnostics,确保改动文件无错误
|
||||
- [x] 10.3 使用数据库验证流程确认 `auto_purchase_status` 字段已生效
|
||||
- [x] 10.4 补充 `docs/client-core-business-api/功能总结.md` 并更新相关索引文档
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-18
|
||||
Reference in New Issue
Block a user