diff --git a/openspec/changes/refactor-order-payment-module/.openspec.yaml b/openspec/changes/refactor-order-payment-module/.openspec.yaml new file mode 100644 index 0000000..76a85e8 --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-14 diff --git a/openspec/changes/refactor-order-payment-module/design.md b/openspec/changes/refactor-order-payment-module/design.md new file mode 100644 index 0000000..94c8709 --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/design.md @@ -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 次)?(不在本次范围,单独讨论) diff --git a/openspec/changes/refactor-order-payment-module/proposal.md b/openspec/changes/refactor-order-payment-module/proposal.md new file mode 100644 index 0000000..daaee00 --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/proposal.md @@ -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` 后端换表但接口不变 diff --git a/openspec/changes/refactor-order-payment-module/specs/asset-recharge-adaptation/spec.md b/openspec/changes/refactor-order-payment-module/specs/asset-recharge-adaptation/spec.md new file mode 100644 index 0000000..052e89e --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/specs/asset-recharge-adaptation/spec.md @@ -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,旧表彻底删除 diff --git a/openspec/changes/refactor-order-payment-module/specs/client-order-purchase/spec.md b/openspec/changes/refactor-order-payment-module/specs/client-order-purchase/spec.md new file mode 100644 index 0000000..4ab9b06 --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/specs/client-order-purchase/spec.md @@ -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 幂等键,允许重新发起下单 diff --git a/openspec/changes/refactor-order-payment-module/specs/force-recharge-check/spec.md b/openspec/changes/refactor-order-payment-module/specs/force-recharge-check/spec.md new file mode 100644 index 0000000..c5cf618 --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/specs/force-recharge-check/spec.md @@ -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,待支付) diff --git a/openspec/changes/refactor-order-payment-module/specs/order-payment/spec.md b/openspec/changes/refactor-order-payment-module/specs/order-payment/spec.md new file mode 100644 index 0000000..b2d68b1 --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/specs/order-payment/spec.md @@ -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** 直接返回支付成功,不重复扣减钱包余额 diff --git a/openspec/changes/refactor-order-payment-module/specs/payment-record/spec.md b/openspec/changes/refactor-order-payment-module/specs/payment-record/spec.md new file mode 100644 index 0000000..389aa35 --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/specs/payment-record/spec.md @@ -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` diff --git a/openspec/changes/refactor-order-payment-module/specs/recharge-order/spec.md b/openspec/changes/refactor-order-payment-module/specs/recharge-order/spec.md new file mode 100644 index 0000000..1fab18c --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/specs/recharge-order/spec.md @@ -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 降序 diff --git a/openspec/changes/refactor-order-payment-module/specs/wallet-recharge/spec.md b/openspec/changes/refactor-order-payment-module/specs/wallet-recharge/spec.md new file mode 100644 index 0000000..9f1e013 --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/specs/wallet-recharge/spec.md @@ -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。 diff --git a/openspec/changes/refactor-order-payment-module/tasks.md b/openspec/changes/refactor-order-payment-module/tasks.md new file mode 100644 index 0000000..547e052 --- /dev/null +++ b/openspec/changes/refactor-order-payment-module/tasks.md @@ -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 执行迁移,确认表已删除