重构用的提案,现在不敢跑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 49s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 49s
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-14
|
||||
143
openspec/changes/refactor-order-payment-module/design.md
Normal file
143
openspec/changes/refactor-order-payment-module/design.md
Normal file
@@ -0,0 +1,143 @@
|
||||
## Context
|
||||
|
||||
当前 `tb_asset_recharge_record` 混合了两个职责:业务订单(用户关心:状态、金额、套餐)和支付凭证(系统关心:交易号、支付渠道、回调)。强充流程创建的是充值记录而非订单,导致用户在 `/api/c/v1/orders` 看不到强充产生的记录。幂等键命中时直接报错而非引导用户恢复支付,造成"列表为空 + 无法重新下单"的死锁体验。
|
||||
|
||||
**现有数据流**:
|
||||
```
|
||||
强充发起 → 写 tb_asset_recharge_record (单号前缀 CRCH)
|
||||
微信回调 → 更新 tb_asset_recharge_record + 增加钱包余额
|
||||
auto_purchase → 读 tb_asset_recharge_record.linked_package_ids → 创建 tb_order (套餐) + 扣款
|
||||
```
|
||||
|
||||
**目标数据流**:
|
||||
```
|
||||
强充发起 → 写 tb_recharge_order + tb_payment (CRCH 前缀保持兼容)
|
||||
微信回调 → 更新 tb_payment + 更新 tb_recharge_order + 增加钱包余额
|
||||
auto_purchase → 读 tb_recharge_order.linked_package_ids → 创建 tb_order (套餐) + 创建 tb_payment (钱包) + 扣款
|
||||
```
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 彻底替换 `tb_asset_recharge_record`,用 `tb_recharge_order` + `tb_payment` 完整承接其所有功能
|
||||
- 充值订单通过独立接口 `/api/c/v1/recharge-orders` 展示;套餐订单接口 `/api/c/v1/orders` 保持不变
|
||||
- 幂等键命中时返回已有待支付充值订单,引导前端拉起支付而非报错
|
||||
- 普通钱包充值(非强充)同样写 `tb_recharge_order` + `tb_payment`
|
||||
- 全部功能验证通过后,作为最后一步 DROP `tb_asset_recharge_record`
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不合并 `tb_order` 和 `tb_recharge_order`(各自独立,不引入多态 order_type)
|
||||
- 不做 UNION 查询(充值订单和套餐订单各有独立入口,不混合展示)
|
||||
- 不引入父子订单关联关系
|
||||
- 不考虑回退(一次性完整替换,无过渡期)
|
||||
- 不迁移历史数据(历史充值记录业务已完成,DROP 旧表即可)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1:新建 `tb_recharge_order` 而非扩展 `tb_order`
|
||||
|
||||
**选择**:独立的 `tb_recharge_order` 表,不将 `wallet_recharge` 作为 `order_type` 注入 `tb_order`。
|
||||
|
||||
**原因**:充值订单与套餐订单领域边界清晰——充值订单无佣金、无卖家、无套餐明细、有自动购包状态;合并进 `tb_order` 会使该表充满特定类型才有意义的 NULL 列,且所有查询都需要加 `order_type` 判断。独立表让每张表的字段 100% 有效,业务逻辑无需条件分支。
|
||||
|
||||
**放弃的替代方案**:多态 `order_type` 方案——混合 NULL 列问题和业务逻辑污染问题无法解决。
|
||||
|
||||
### 决策 2:充值订单和套餐订单各自独立接口
|
||||
|
||||
**选择**:`/api/c/v1/orders` 保持不变,仅返回套餐购买订单(`tb_order`)。新增 `/api/c/v1/recharge-orders` 独立返回充值订单(`tb_recharge_order`)。
|
||||
|
||||
**原因**:两类订单业务语义不同——充值订单有 auto_purchase_status,套餐订单有佣金状态;强行混合展示需要 UNION 和统一 DTO,引入不必要的复杂度和长期维护负担。前端各看各的,接口职责清晰,DTO 字段 100% 有意义。
|
||||
|
||||
### 决策 3:`tb_payment` 统一承接所有支付凭证
|
||||
|
||||
**选择**:新建 `tb_payment` 表,`order_id` + `order_type`(`package_order` / `recharge_order`)多态关联到对应业务表。
|
||||
|
||||
**原因**:支付是基础设施,与业务订单解耦后,未来新增支付方式(富途、积分等)只需修改支付层,不触碰业务订单表。支付回调路由从"按单号前缀分发到不同 Service"简化为"查 `tb_payment` → 关联业务订单"。
|
||||
|
||||
**字段设计**:
|
||||
```sql
|
||||
CREATE TABLE tb_payment (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
payment_no VARCHAR(40) NOT NULL UNIQUE, -- 保持 CRCH/PAY 前缀兼容
|
||||
order_id BIGINT NOT NULL,
|
||||
order_type VARCHAR(30) NOT NULL, -- 'package_order' | 'recharge_order'
|
||||
payment_method VARCHAR(20) NOT NULL, -- wallet | wechat | alipay | offline
|
||||
amount BIGINT NOT NULL, -- 分
|
||||
status SMALLINT NOT NULL DEFAULT 1, -- 1待支付 2已支付 3已失败 4已退款
|
||||
third_party_trade_no VARCHAR(100),
|
||||
payment_config_id BIGINT,
|
||||
payment_voucher_key VARCHAR(500),
|
||||
paid_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_payment_order ON tb_payment(order_id, order_type);
|
||||
CREATE INDEX idx_payment_no ON tb_payment(payment_no) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_payment_status ON tb_payment(status) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
### 决策 4:`tb_recharge_order` 字段设计
|
||||
|
||||
承接 `tb_asset_recharge_record` 的业务字段 + 增加与资产的直接关联:
|
||||
|
||||
```sql
|
||||
CREATE TABLE tb_recharge_order (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recharge_order_no VARCHAR(40) NOT NULL UNIQUE, -- 沿用 CRCH 前缀
|
||||
user_id BIGINT NOT NULL, -- 发起用户(个人客户 ID)
|
||||
asset_wallet_id BIGINT NOT NULL,
|
||||
resource_type VARCHAR(20) NOT NULL, -- iot_card | device
|
||||
resource_id BIGINT NOT NULL,
|
||||
iot_card_id BIGINT, -- 单卡充值时有值(用于 ListOrders 查询)
|
||||
device_id BIGINT, -- 设备充值时有值
|
||||
amount BIGINT NOT NULL, -- 充值金额(分)
|
||||
status SMALLINT NOT NULL DEFAULT 1, -- 1待支付 2已支付 3已关闭 4已退款
|
||||
paid_at TIMESTAMPTZ,
|
||||
shop_id_tag BIGINT NOT NULL DEFAULT 0,
|
||||
enterprise_id_tag BIGINT,
|
||||
operator_type VARCHAR(30) NOT NULL, -- personal_customer | admin_user
|
||||
generation INT NOT NULL DEFAULT 1,
|
||||
-- 强充专有字段(普通充值为 NULL)
|
||||
linked_package_ids JSONB,
|
||||
linked_order_type VARCHAR(20),
|
||||
linked_carrier_type VARCHAR(20),
|
||||
linked_carrier_id BIGINT,
|
||||
auto_purchase_status VARCHAR(20), -- pending | success | failed
|
||||
-- 累计充值(用于强充阈值判断)
|
||||
accumulated_recharge_series BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_recharge_order_card ON tb_recharge_order(iot_card_id, generation) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_recharge_order_device ON tb_recharge_order(device_id, generation) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_recharge_order_user ON tb_recharge_order(user_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_recharge_order_no ON tb_recharge_order(recharge_order_no) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_recharge_order_status ON tb_recharge_order(status) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
### 决策 5:旧表在所有验证通过后作为最后一步 DROP
|
||||
|
||||
**选择**:迁移过程中保留 `tb_asset_recharge_record`(只读,不再写入);全部功能验证通过后,最后一个任务执行 `DROP TABLE tb_asset_recharge_record`。
|
||||
|
||||
**原因**:保留旧表到最后是唯一的安全网——若验证发现遗漏的调用路径,还能对照旧表数据排查。一旦验证完整通过,立即 DROP,不拖延,不留历史包袱。不做重命名归档,不设 3-6 个月观察期。
|
||||
|
||||
### 决策 6:幂等键修改
|
||||
|
||||
**现有问题**:`SetNX(redisKey, "processing")` 命中时直接返回 "订单正在创建中",不区分"真正在处理"和"已创建待支付"。
|
||||
|
||||
**修改方案**:`!claimed` 时 `GET` 当前值:
|
||||
- 值为 `"processing"` → 并发请求,返回原有报错
|
||||
- 值为 `recharge_order_no` → 查 `tb_recharge_order`,若 `status=1`(待支付)则返回已有充值订单,前端拉起支付
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[历史充值记录不可见]** → 接受,`/api/c/v1/recharge-orders` 仅展示新表数据,用户在新表建立前的历史充值不展示;历史充值已到账,业务无影响
|
||||
- **[`/api/c/v1/wallet/recharges` 数据断层]** → 接受,旧接口后端改为查 `tb_recharge_order`,历史数据不补填
|
||||
|
||||
## Open Questions
|
||||
|
||||
- `auto_purchase_status` 触发失败时的重试策略是否调整(当前 Asynq 最多重试 3 次)?(不在本次范围,单独讨论)
|
||||
41
openspec/changes/refactor-order-payment-module/proposal.md
Normal file
41
openspec/changes/refactor-order-payment-module/proposal.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## Why
|
||||
|
||||
当前 `tb_asset_recharge_record` 表同时承担"充值业务订单"和"第三方支付凭证"两个职责,导致强充订单对用户不可见、幂等键锁死后无法引导用户恢复支付、支付与业务逻辑强耦合难以扩展。需要彻底替换为职责单一的新结构:独立的充值订单表 `tb_recharge_order` + 独立的支付记录表 `tb_payment`,一次性完整删除 `tb_asset_recharge_record` 及其所有相关代码。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **新增** `tb_recharge_order` 表:承接充值业务订单(用户可见、有状态流转、挂 `iot_card_id`/`device_id`)
|
||||
- **新增** `tb_payment` 表:统一记录所有支付凭证(微信/支付宝/钱包),与业务订单解耦
|
||||
- **新增** `/api/c/v1/recharge-orders` 充值订单列表接口:独立展示充值订单,不与套餐订单混合
|
||||
- **修改** `/api/c/v1/orders` 列表接口:保持不变,仅展示套餐购买订单(`tb_order`)
|
||||
- **修改** 支付回调链路:完全重写为查 `tb_payment` 路由,彻底移除旧链路
|
||||
- **修改** `auto_purchase` 异步任务:从 `tb_recharge_order` 读取关联套餐 IDs
|
||||
- **修改** 幂等检查逻辑:幂等键命中时返回已有充值订单,引导前端拉起支付
|
||||
- **BREAKING** 直接删除 `tb_asset_recharge_record` 表及全部相关代码(`AssetRechargeStore`、`RechargeService` 写路径等),无过渡期,无 fallback
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `recharge-order`:充值订单管理——创建、列表查询、详情、状态流转(待支付→已支付→自动购包完成),独立于套餐订单展示
|
||||
- `payment-record`:支付记录管理——统一记录微信/支付宝/钱包支付凭证,关联业务订单(`tb_order` 或 `tb_recharge_order`),支持幂等回调处理
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `client-order-purchase`:强充下单改写新表;幂等检查命中时返回已有充值订单而非报错;`/api/c/v1/orders` 接口本身保持不变(仍只返回套餐订单)
|
||||
- `force-recharge-check`:强充判断逻辑不变,创建数据改写 `tb_recharge_order` + `tb_payment`
|
||||
- `order-payment`:`PayOrder` 接口支付成功时同步创建 `tb_payment` 记录
|
||||
- `wallet-recharge`:普通充值全部改写 `tb_recharge_order` + `tb_payment`;`/api/c/v1/wallet/recharges` 接口后端数据源改为 `tb_recharge_order`
|
||||
- `asset-recharge-adaptation`:该 spec 描述的能力全部迁移至新 spec,原 spec 废弃
|
||||
|
||||
## Impact
|
||||
|
||||
**数据库**:新增 2 张表(`tb_recharge_order`、`tb_payment`),直接 DROP `tb_asset_recharge_record`
|
||||
|
||||
**代码删除**:`AssetRechargeRecord` 模型、`asset_recharge_store.go`、`recharge/service.go` 中所有写方法及回调处理、bootstrap 中相关注入
|
||||
|
||||
**代码新增**:`internal/model/recharge_order.go`、`internal/model/payment.go`、`recharge_order_store.go`、`payment_store.go`、`recharge_order/service.go`
|
||||
|
||||
**代码修改**:`client_order/service.go`(强充创建 + 幂等恢复)、`auto_purchase.go`(改读新表)、`callback/payment.go`(完全重写路由)、`client_wallet.go`(GetRechargeRecords 改查新表)、`wechat_config/service.go`(resolvePaymentConfigID 改查新表)、`wechat_config_store.go`(CountPendingRechargesByConfigID 改查新表)、bootstrap 注入
|
||||
|
||||
**API 变化**:新增 `/api/c/v1/recharge-orders` 端点;`/api/c/v1/orders` 不变;`/api/c/v1/wallet/recharges` 后端换表但接口不变
|
||||
@@ -0,0 +1,30 @@
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: 资产充值关联支付配置
|
||||
**Reason**: `tb_asset_recharge_record` 表删除,支付配置关联迁移至 `tb_payment.payment_config_id`。
|
||||
**Migration**: 查询支付配置时从 `tb_payment` 读取,`PaymentStore.GetByPaymentNo()` 返回含 `payment_config_id` 的完整记录。
|
||||
|
||||
### Requirement: 资产充值表结构变更
|
||||
**Reason**: `tb_asset_recharge_record` 表重命名为 `tb_asset_recharge_record_archive` 并停止写入,相关表结构变更不再需要维护。
|
||||
**Migration**: 新的充值数据结构参见 `recharge-order` spec(`tb_recharge_order`)和 `payment-record` spec(`tb_payment`)。
|
||||
|
||||
### Requirement: 资产充值回调按配置验签
|
||||
**Reason**: 充值回调路由改为通过 `tb_payment` 查找配置,验签逻辑迁移至 `payment-record` spec 的支付路由统一需求。
|
||||
**Migration**: 回调处理查 `tb_payment.payment_config_id` 加载对应配置验签,与原逻辑等价。
|
||||
|
||||
### Requirement: 资产充值记录扩展字段(操作人与代际)
|
||||
**Reason**: 这些字段(operator_type、generation)迁移至 `tb_recharge_order` 表,继续有效。
|
||||
**Migration**: `tb_recharge_order.operator_type` 和 `tb_recharge_order.generation` 字段语义不变,字段值枚举不变。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 旧表在验证完成后删除
|
||||
系统 SHALL 在所有新功能验证通过后,作为最后一步执行 `DROP TABLE tb_asset_recharge_record`。迁移期间旧表保留但不写入,仅用于人工对照核查。
|
||||
|
||||
#### Scenario: 旧表无新写入
|
||||
- **WHEN** 新代码部署完成后
|
||||
- **THEN** `tb_asset_recharge_record` 的 updated_at 最新记录时间早于部署时间,确认无新写入
|
||||
|
||||
#### Scenario: DROP 旧表
|
||||
- **WHEN** 所有验证任务通过(tasks.md 第 11 组全部完成)
|
||||
- **THEN** 执行迁移文件 DROP TABLE,旧表彻底删除
|
||||
@@ -0,0 +1,33 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: D2 套餐订单列表接口
|
||||
`/api/c/v1/orders` 接口 SHALL 保持不变,仅查询 `tb_order` 返回套餐购买订单。充值订单通过独立接口 `/api/c/v1/recharge-orders` 展示,两者不混合。
|
||||
|
||||
#### Scenario: 订单列表只返回套餐订单
|
||||
- **WHEN** 用户查询某张卡的订单列表
|
||||
- **THEN** 仅返回该卡下的套餐订单(来自 `tb_order`),不包含充值订单
|
||||
- **THEN** 响应结构与原有接口完全兼容,无新增字段
|
||||
|
||||
### Requirement: AutoPurchaseAfterRecharge 异步任务
|
||||
auto_purchase 任务 SHALL 从 `tb_recharge_order` 读取 linked_package_ids,执行完成后更新 `tb_recharge_order.auto_purchase_status`,同时创建套餐订单(`tb_order`)和对应的 `tb_payment`(payment_method=wallet)。不再读取 `tb_asset_recharge_record`。
|
||||
|
||||
#### Scenario: 自动购包成功写入新表
|
||||
- **WHEN** auto_purchase 任务从 `tb_recharge_order` 获取 linked_package_ids 并完成购包
|
||||
- **THEN** 创建 `tb_order`(type=single_card/device,payment_status=2)
|
||||
- **THEN** 创建 `tb_payment`(payment_method=wallet,status=2,关联新订单)
|
||||
- **THEN** `tb_recharge_order.auto_purchase_status` 更新为 success
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: D1 下单幂等恢复
|
||||
系统 SHALL 在幂等键命中且对应充值订单为待支付状态时,返回已有充值订单信息(含 payment_config 引导),而非返回错误 "订单正在创建中"。
|
||||
|
||||
#### Scenario: 幂等键命中且订单待支付
|
||||
- **WHEN** 用户重复发起下单请求,Redis 幂等键已存在且值为 recharge_order_no
|
||||
- **THEN** 查询 `tb_recharge_order`,status=1(待支付)
|
||||
- **THEN** 接口返回已有充值订单信息,HTTP 200,包含 `idempotent: true` 标志
|
||||
- **THEN** 前端可凭此引导用户拉起已有支付而非展示报错
|
||||
|
||||
#### Scenario: 幂等键命中但订单已支付或关闭
|
||||
- **WHEN** 幂等键存在,但对应充值订单 status=2/3(已支付或已关闭)
|
||||
- **THEN** 删除 Redis 幂等键,允许重新发起下单
|
||||
@@ -0,0 +1,14 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 强充检查结果对客户端透出
|
||||
强充判断逻辑 SHALL 不变(检查余额是否低于阈值),但触发强充后的创建流程 SHALL 写入 `tb_recharge_order` + `tb_payment`,不再写入 `tb_asset_recharge_record`。强充订单的 linked_package_ids、linked_carrier_type、linked_carrier_id 存储在 `tb_recharge_order` 中。
|
||||
|
||||
#### Scenario: 余额不足触发强充,写入新表
|
||||
- **WHEN** 用户购买套餐时余额低于强充阈值,触发强充流程
|
||||
- **THEN** 创建 `tb_recharge_order` 记录(含 linked_package_ids、linked_carrier_type、linked_carrier_id、auto_purchase_status=pending)
|
||||
- **THEN** 创建 `tb_payment` 记录(payment_method=wechat,status=1)
|
||||
- **THEN** 不再写入 `tb_asset_recharge_record`
|
||||
|
||||
#### Scenario: 强充发起后订单在列表可见
|
||||
- **WHEN** 强充发起成功(微信支付参数返回前端)
|
||||
- **THEN** 用户立刻可在 `/api/c/v1/orders` 列表中看到该充值订单(status=1,待支付)
|
||||
@@ -0,0 +1,29 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 钱包支付
|
||||
钱包支付 SHALL 在扣款成功的同一事务中创建 `tb_payment` 记录(payment_method=wallet,status=2,paid_at=当前时间),关联到 `tb_order`(order_type=package_order)。不再仅通过 `tb_order.payment_method` 隐式记录支付信息。
|
||||
|
||||
#### Scenario: 钱包支付同步创建支付记录
|
||||
- **WHEN** 用户通过钱包支付套餐订单,扣款成功
|
||||
- **THEN** 在同一事务中:创建 `tb_payment`(method=wallet,status=2)、更新 `tb_order.payment_status=2`、扣减钱包余额、创建钱包流水
|
||||
|
||||
#### Scenario: 钱包余额不足时不创建支付记录
|
||||
- **WHEN** 用户发起钱包支付但余额不足
|
||||
- **THEN** 事务回滚,不创建 `tb_payment` 记录,返回余额不足错误
|
||||
|
||||
### Requirement: 第三方支付回调
|
||||
第三方支付回调处理 SHALL 从 `tb_payment` 查找 payment_no;找到后更新 `tb_payment.status=2` 并更新关联的 `tb_order` 或 `tb_recharge_order`。
|
||||
|
||||
#### Scenario: 新单号回调走新路由
|
||||
- **WHEN** 第三方支付回调,payment_no 在 `tb_payment` 中存在
|
||||
- **THEN** 更新 `tb_payment`(status=2,third_party_trade_no,paid_at)
|
||||
- **THEN** 根据 `tb_payment.order_type` 更新关联业务订单(`tb_order` 或 `tb_recharge_order`)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 支付记录幂等保障
|
||||
系统 SHALL 通过 `tb_payment` 的 `payment_no` 唯一约束 + 状态检查确保 PayOrder 接口的幂等性:同一 order_id 在 `tb_payment` 中已有 status=2 的记录时,直接返回成功,不重复扣款。
|
||||
|
||||
#### Scenario: 重复调用 PayOrder 幂等返回
|
||||
- **WHEN** 相同 order_id 的 PayOrder 请求重复到达,`tb_payment` 中已存在 status=2 记录
|
||||
- **THEN** 直接返回支付成功,不重复扣减钱包余额
|
||||
@@ -0,0 +1,40 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 支付记录创建
|
||||
系统 SHALL 在每次发起支付时创建 `tb_payment` 记录,关联到业务订单(`tb_order` 或 `tb_recharge_order`),记录 order_type 用于多态关联。钱包直接支付时 status 直接设为 2(已支付),第三方支付时 status 为 1(待支付)。
|
||||
|
||||
#### Scenario: 微信支付创建支付记录
|
||||
- **WHEN** 用户发起微信支付(强充或套餐直接微信支付)
|
||||
- **THEN** `tb_payment` 创建 status=1 的记录,payment_method=wechat,关联 order_id 和 order_type
|
||||
|
||||
#### Scenario: 钱包支付创建支付记录
|
||||
- **WHEN** 用户通过钱包支付套餐订单
|
||||
- **THEN** `tb_payment` 创建 status=2 的记录,payment_method=wallet,paid_at=当前时间
|
||||
|
||||
### Requirement: 支付回调幂等处理
|
||||
系统 SHALL 通过 `tb_payment.payment_no` 唯一约束和状态检查确保支付回调幂等,重复回调不产生重复业务效果。
|
||||
|
||||
#### Scenario: 首次支付成功回调
|
||||
- **WHEN** 第三方支付成功回调,payment_no 在 `tb_payment` 中存在且 status=1
|
||||
- **THEN** 更新 `tb_payment.status`=2,记录 third_party_trade_no 和 paid_at
|
||||
- **THEN** 触发关联业务订单的后续处理
|
||||
|
||||
#### Scenario: 重复支付成功回调
|
||||
- **WHEN** 相同 payment_no 的支付成功回调再次到达,`tb_payment.status` 已为 2
|
||||
- **THEN** 直接返回成功,不执行任何业务更新
|
||||
|
||||
### Requirement: 支付路由统一
|
||||
系统 SHALL 在支付回调处理时,优先从 `tb_payment` 查找 payment_no;若存在则走新路由(更新 `tb_payment` + 关联业务订单);若不存在则 fallback 到旧 `tb_asset_recharge_record` 查询(兼容迁移过渡期)。
|
||||
|
||||
#### Scenario: 新单号走新路由
|
||||
- **WHEN** 支付回调的 payment_no 在 `tb_payment` 中存在
|
||||
- **THEN** 走新支付路由:更新 `tb_payment` → 更新关联 `tb_recharge_order` 或 `tb_order` → 触发后续业务
|
||||
|
||||
|
||||
|
||||
### Requirement: 支付配置关联
|
||||
系统 SHALL 在 `tb_payment` 中存储 payment_config_id,关联发起支付时使用的微信/支付宝配置,支持多配置场景下的对账。
|
||||
|
||||
#### Scenario: 记录支付配置快照
|
||||
- **WHEN** 创建支付记录时
|
||||
- **THEN** 将当前使用的 payment_config_id 写入 `tb_payment`
|
||||
@@ -0,0 +1,50 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 充值订单创建
|
||||
系统 SHALL 在强充发起时创建 `tb_recharge_order` 记录,记录充值金额、关联资产、关联套餐 IDs,初始状态为待支付(status=1)。同时创建对应的 `tb_payment` 记录(payment_method=wechat,status=1)。
|
||||
|
||||
#### Scenario: 强充发起成功
|
||||
- **WHEN** 用户发起强充(余额不足场景),系统成功调用微信支付 API
|
||||
- **THEN** `tb_recharge_order` 创建一条记录(status=1,linked_package_ids 已设置)
|
||||
- **THEN** `tb_payment` 创建一条关联记录(order_type=recharge_order,payment_method=wechat,status=1)
|
||||
- **THEN** 接口返回充值订单信息 + 微信支付参数
|
||||
|
||||
#### Scenario: 微信支付 API 调用失败
|
||||
- **WHEN** 系统在调用微信支付 API 时发生错误
|
||||
- **THEN** `tb_recharge_order` 记录被回滚(或标记为已关闭 status=3)
|
||||
- **THEN** `tb_payment` 记录被回滚
|
||||
- **THEN** Redis 幂等键被清除,允许用户重新发起
|
||||
|
||||
### Requirement: 充值订单状态流转
|
||||
系统 SHALL 通过支付回调更新充值订单状态:待支付(1)→ 已支付(2),关闭(3),退款(4)。状态更新使用乐观锁确保幂等性。
|
||||
|
||||
#### Scenario: 微信支付回调成功
|
||||
- **WHEN** 微信支付成功回调到达,payment_no 存在于 `tb_payment`
|
||||
- **THEN** `tb_payment.status` 更新为 2,记录 third_party_trade_no 和 paid_at
|
||||
- **THEN** 关联的 `tb_recharge_order.status` 更新为 2
|
||||
- **THEN** 钱包余额增加,创建钱包流水记录
|
||||
- **THEN** 若 linked_package_ids 非空,触发 auto_purchase 异步任务
|
||||
|
||||
#### Scenario: 重复回调幂等处理
|
||||
- **WHEN** 同一 payment_no 的支付成功回调重复到达
|
||||
- **THEN** 系统检测到 `tb_payment.status` 已为 2,直接返回成功
|
||||
- **THEN** 不重复更新钱包余额,不重复触发 auto_purchase
|
||||
|
||||
### Requirement: 自动购包状态追踪
|
||||
系统 SHALL 在 `tb_recharge_order` 上记录 auto_purchase_status,由 Asynq Worker 更新,支持 pending / success / failed 三种状态。
|
||||
|
||||
#### Scenario: 自动购包成功
|
||||
- **WHEN** auto_purchase 任务成功创建套餐订单并激活套餐
|
||||
- **THEN** `tb_recharge_order.auto_purchase_status` 更新为 success
|
||||
|
||||
#### Scenario: 自动购包失败
|
||||
- **WHEN** auto_purchase 任务在最大重试次数后仍失败
|
||||
- **THEN** `tb_recharge_order.auto_purchase_status` 更新为 failed
|
||||
- **THEN** 记录错误日志,告警通知运营
|
||||
|
||||
### Requirement: 充值订单列表查询
|
||||
系统 SHALL 支持按 iot_card_id 或 device_id + generation 查询充值订单列表,支持按 status 过滤。
|
||||
|
||||
#### Scenario: 按资产查询充值订单
|
||||
- **WHEN** 系统查询某张卡的充值订单列表
|
||||
- **THEN** 返回该卡(iot_card_id 匹配)且 generation 匹配的所有充值订单,按 created_at 降序
|
||||
@@ -0,0 +1,31 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 创建钱包充值订单
|
||||
创建钱包充值订单 SHALL 写入 `tb_recharge_order`(非强充时 linked_package_ids 为 NULL),并同时创建 `tb_payment` 记录。不再写入 `tb_asset_recharge_record`。充值订单号格式保持 CRCH 前缀兼容。
|
||||
|
||||
#### Scenario: 用户主动充值写新表
|
||||
- **WHEN** 用户发起钱包充值(非强充场景)
|
||||
- **THEN** 创建 `tb_recharge_order`(linked_package_ids=NULL,auto_purchase_status=NULL)
|
||||
- **THEN** 创建 `tb_payment`(payment_method=wechat/alipay,status=1)
|
||||
- **THEN** 不写 `tb_asset_recharge_record`
|
||||
|
||||
### Requirement: 充值支付回调处理
|
||||
充值支付回调 SHALL 查 `tb_payment` 获取充值订单,更新 `tb_payment.status=2`,更新 `tb_recharge_order.status=2`,增加钱包余额,创建钱包流水。原有 `RechargeService.HandlePaymentCallback` 中对 `tb_asset_recharge_record` 的操作全部迁移到新表。
|
||||
|
||||
#### Scenario: 充值回调更新新表
|
||||
- **WHEN** 充值支付回调到达
|
||||
- **THEN** 查 `tb_payment` by payment_no
|
||||
- **THEN** 事务:更新 `tb_payment.status=2` → 更新 `tb_recharge_order.status=2` → 增加 `tb_asset_wallet.balance` → 创建 `tb_asset_wallet_transaction`(reference_type=recharge)
|
||||
|
||||
### Requirement: 充值记录新增 auto_purchase_status 状态追踪
|
||||
此需求 SHALL 迁移至 `tb_recharge_order.auto_purchase_status` 字段,语义和状态枚举不变(pending / success / failed)。`tb_asset_recharge_record` 上的该字段随表一起归档停用。
|
||||
|
||||
#### Scenario: 强充后 auto_purchase 完成,更新新表字段
|
||||
- **WHEN** auto_purchase 任务完成套餐激活
|
||||
- **THEN** `tb_recharge_order.auto_purchase_status` 更新为 success(不再更新 `tb_asset_recharge_record`)
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Store 方法签名支持事务参数
|
||||
**Reason**: `AssetRechargeStore` 连同 `tb_asset_recharge_record` 一起删除,其事务方法签名不再需要维护。
|
||||
**Migration**: 事务支持迁移至 `RechargeOrderStore` 和 `PaymentStore`,方法签名设计参考 `recharge-order` spec。
|
||||
79
openspec/changes/refactor-order-payment-module/tasks.md
Normal file
79
openspec/changes/refactor-order-payment-module/tasks.md
Normal file
@@ -0,0 +1,79 @@
|
||||
## 1. 数据库迁移(建新表)
|
||||
|
||||
- [ ] 1.1 创建迁移文件:新建 `tb_recharge_order` 表(含所有字段和索引,参考 design.md 决策 4 的字段定义)
|
||||
- [ ] 1.2 创建迁移文件:新建 `tb_payment` 表(含所有字段和索引,参考 design.md 决策 3 的字段定义)
|
||||
|
||||
## 2. Model 层
|
||||
|
||||
- [ ] 2.1 新建 `internal/model/recharge_order.go`:定义 `RechargeOrder` 模型、状态常量(1待支付 2已支付 3已关闭 4已退款)、`AutoPurchaseStatus` 常量(pending/success/failed)
|
||||
- [ ] 2.2 新建 `internal/model/payment.go`:定义 `Payment` 模型、状态常量(1待支付 2已支付 3已失败 4已退款)、`OrderType` 常量(package_order / recharge_order)
|
||||
|
||||
## 3. Store 层
|
||||
|
||||
- [ ] 3.1 新建 `internal/store/postgres/recharge_order_store.go`:实现 Create、GetByRechargeOrderNo、GetByID、UpdateStatus(乐观锁)、ListByCard、ListByDevice
|
||||
- [ ] 3.2 新建 `internal/store/postgres/payment_store.go`:实现 Create、GetByPaymentNo、UpdateStatus、UpdatePaymentInfo、ListByOrderID
|
||||
- [ ] 3.3 在 `internal/bootstrap/stores.go` 中注册 `RechargeOrderStore` 和 `PaymentStore`
|
||||
|
||||
## 4. 强充创建流程重写
|
||||
|
||||
- [ ] 4.1 修改 `internal/service/client_order/service.go` 的 `createForceRechargeOrder`:改为写 `tb_recharge_order` + `tb_payment`,移除对 `rechargeRecordStore` 的调用
|
||||
- [ ] 4.2 修改幂等键逻辑:`!claimed` 时 GET Redis 值,若为 recharge_order_no 则查 `tb_recharge_order`,status=1 时返回已有充值订单(含 `idempotent: true` 标志),status≠1 时删 key 允许重建
|
||||
- [ ] 4.3 调整 `markClientPurchaseCreated`:存入 `recharge_order_no`(来自 `tb_recharge_order`,不再存充值记录号)
|
||||
- [ ] 4.4 修改 `ClientOrderService` 结构体:注入 `rechargeOrderStore` 和 `paymentStore`,移除 `rechargeRecordStore` 字段
|
||||
- [ ] 4.5 修改 `internal/bootstrap/handlers.go`:更新 `ClientOrderService` 的依赖注入
|
||||
|
||||
## 5. 支付回调链路完全重写
|
||||
|
||||
- [ ] 5.1 新建 `internal/service/recharge_order/service.go`:实现 `HandlePaymentCallback(ctx, paymentNo, paymentMethod, transactionID)`——查 `tb_payment` → 乐观锁更新 `tb_payment.status=2` → 更新 `tb_recharge_order.status=2` → 事务内增加钱包余额 + 创建钱包流水 + 更新累计充值 → 触发 auto_purchase 任务(若 linked_package_ids 非空)
|
||||
- [ ] 5.2 修改 `internal/handler/callback/payment.go`:`dispatchWechatCallback` 改为查 `tb_payment` by payment_no,找到则调 `rechargeOrderService.HandlePaymentCallback`;CRCH 前缀单号不再路由到旧 `RechargeService`
|
||||
- [ ] 5.3 修改 `internal/bootstrap/handlers.go`:注入 `rechargeOrderService` 到支付回调 Handler
|
||||
- [ ] 5.4 修改 `internal/service/wechat_config/service.go` 的 `resolvePaymentConfigID`:CRCH 前缀路由从 `AssetRechargeStore.GetByRechargeNo` 改为 `RechargeOrderStore.GetByRechargeOrderNo`,获取 `payment_config_id` 用于微信支付回调验签
|
||||
- [ ] 5.5 修改 `internal/store/postgres/wechat_config_store.go` 的 `CountPendingRechargesByConfigID`:SQL 从 `tb_asset_recharge_record` 改为查询 `tb_recharge_order`(按 `payment_config_id` 统计待支付充值数,用于删除配置前检查)
|
||||
|
||||
## 6. 普通充值流程迁移
|
||||
|
||||
- [ ] 6.1 修改 `internal/handler/app/client_wallet.go` 的 `CreateRecharge`:改为调用新的充值订单创建逻辑,写 `tb_recharge_order` + `tb_payment`,移除对旧 `RechargeService.Create` 的调用
|
||||
- [ ] 6.2 修改 `internal/handler/app/client_wallet.go` 的 `GetRechargeRecords`:改为从 `tb_recharge_order` 查询
|
||||
|
||||
## 7. PayOrder 增加支付记录
|
||||
|
||||
- [ ] 7.1 修改 `internal/service/order/service.go` 的 `WalletPay`:在扣款事务中增加 `INSERT tb_payment`(method=wallet,status=2,order_type=package_order)
|
||||
- [ ] 7.2 在 `WalletPay` 中增加幂等检查:若 `tb_payment` 中已有该 order_id 且 status=2 的记录,直接返回成功,不重复扣款
|
||||
|
||||
## 8. auto_purchase 任务迁移
|
||||
|
||||
- [ ] 8.1 修改 `internal/task/auto_purchase.go`:任务 payload 改为传 `recharge_order_id`,从 `tb_recharge_order` 读取 linked_package_ids,移除对 `tb_asset_recharge_record` 的读取
|
||||
- [ ] 8.2 修改 `auto_purchase.go`:创建套餐订单(`tb_order`)的同时在同一事务内创建 `tb_payment`(method=wallet,status=2,order_type=package_order)
|
||||
- [ ] 8.3 修改 `auto_purchase.go`:任务完成/失败后更新 `tb_recharge_order.auto_purchase_status`,移除对 `tb_asset_recharge_record` 的写入
|
||||
|
||||
## 9. 新增充值订单独立接口
|
||||
|
||||
- [ ] 9.1 新增 `internal/handler/app/client_recharge_order.go`:实现 `ListRechargeOrders`(GET `/api/c/v1/recharge-orders`,按 identifier + generation 查 `tb_recharge_order`,支持 status 过滤)和 `GetRechargeOrderDetail`(GET `/api/c/v1/recharge-orders/:id`)
|
||||
- [ ] 9.2 新增对应 DTO:`ClientRechargeOrderListRequest`、`ClientRechargeOrderListItem`(含 auto_purchase_status、recharge_order_no、amount、status、status_name、created_at)
|
||||
- [ ] 9.3 注册路由:在 C 端路由组中注册 `/api/c/v1/recharge-orders` 相关路由
|
||||
- [ ] 9.4 修改 `ClientCreateOrderResponse`:`recharge` 分支返回 `recharge_order_no`(来自新表),增加 `idempotent` bool 字段
|
||||
|
||||
## 10. 删除旧代码
|
||||
|
||||
- [ ] 10.1 删除 `internal/service/recharge/service.go` 中全部写方法和回调处理(HandlePaymentCallback、Create 等),若无其他引用则删除整个文件
|
||||
- [ ] 10.2 删除 `internal/store/postgres/asset_recharge_store.go` 全部内容(无需保留任何方法)
|
||||
- [ ] 10.3 删除 `internal/model/asset_wallet.go` 中的 `AssetRechargeRecord` 模型定义
|
||||
- [ ] 10.4 从 `internal/bootstrap/stores.go` 和 `handlers.go` 中移除 `AssetRechargeStore` 的所有注册和注入
|
||||
- [ ] 10.5 从 `internal/service/client_order/service.go` 中移除 `rechargeRecordStore` 字段和全部引用
|
||||
- [ ] 10.6 运行 `go build ./...`,修复所有因删除旧代码引起的编译错误
|
||||
|
||||
## 11. 验证
|
||||
|
||||
- [ ] 11.1 用 PostgreSQL MCP 验证:发起强充后 `tb_recharge_order` 和 `tb_payment` 均有新记录,`tb_asset_recharge_record` 无新记录
|
||||
- [ ] 11.2 用 PostgreSQL MCP 验证:微信支付回调后 `tb_payment.status=2`,`tb_recharge_order.status=2`,钱包余额正确增加
|
||||
- [ ] 11.3 用 PostgreSQL MCP 验证:auto_purchase 完成后 `tb_order` 新增套餐订单,`tb_recharge_order.auto_purchase_status=success`
|
||||
- [ ] 11.4 手动验证:GET `/api/c/v1/recharge-orders` 正确返回充值订单列表;GET `/api/c/v1/orders` 仍只返回套餐订单
|
||||
- [ ] 11.5 手动验证:重复发起相同参数的下单请求,返回 HTTP 200 且 `idempotent=true`,不报"订单正在创建中"
|
||||
- [ ] 11.6 手动验证:普通充值(非强充)写 `tb_recharge_order`,回调后钱包余额正确增加
|
||||
- [ ] 11.7 手动验证:PayOrder 支付套餐后 `tb_payment` 有对应 wallet 支付记录,重复调用幂等返回成功
|
||||
|
||||
## 12. 删除旧表(最后执行)
|
||||
|
||||
- [ ] 12.1 确认 `tb_asset_recharge_record` 无任何新写入(检查该表 updated_at 最新时间是否在部署前)
|
||||
- [ ] 12.2 创建迁移文件:`DROP TABLE tb_asset_recharge_record`
|
||||
- [ ] 12.3 执行迁移,确认表已删除
|
||||
Reference in New Issue
Block a user