重构用的提案,现在不敢跑
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:
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 次)?(不在本次范围,单独讨论)
|
||||
Reference in New Issue
Block a user