From 5797fd0e94cf155b9dc41b4ec21dc0304349b56e Mon Sep 17 00:00:00 2001 From: break Date: Thu, 20 Aug 2026 18:06:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BC=81=E5=BE=AE=E5=85=9C?= =?UTF-8?q?=E5=BA=95=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/deployment/production-runbook.md | 7 +++ internal/service/refund/service.go | 6 +-- .../.openspec.yaml | 2 + .../design.md | 43 +++++++++++++++++++ .../proposal.md | 24 +++++++++++ .../specs/refund-approval/spec.md | 30 +++++++++++++ .../tasks.md | 11 +++++ openspec/specs/refund-approval/spec.md | 32 ++++++++++++++ 8 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 openspec/changes/archive/2026-08-20-allow-zero-refund-approval/.openspec.yaml create mode 100644 openspec/changes/archive/2026-08-20-allow-zero-refund-approval/design.md create mode 100644 openspec/changes/archive/2026-08-20-allow-zero-refund-approval/proposal.md create mode 100644 openspec/changes/archive/2026-08-20-allow-zero-refund-approval/specs/refund-approval/spec.md create mode 100644 openspec/changes/archive/2026-08-20-allow-zero-refund-approval/tasks.md create mode 100644 openspec/specs/refund-approval/spec.md diff --git a/docs/deployment/production-runbook.md b/docs/deployment/production-runbook.md index f80692b..ce174b2 100644 --- a/docs/deployment/production-runbook.md +++ b/docs/deployment/production-runbook.md @@ -74,6 +74,13 @@ DB_PASSWORD='<密码>' DB_NAME=<库名> DB_SSLMODE=<模式> \ 迁移失败时不启动新二进制;按失败迁移的事务状态决定处理,必要时恢复已确认可用的数据库备份。启动失败时覆盖回部署前备份的二进制,再恢复数据库备份(如迁移已改变数据库)。 +### 零金额退款发布后核验 + +发布本次退款审批变更后,维护者应先等待既有重试处理稳定事件 `approval:26:approved`;若重试已耗尽,按受控运维流程重放同一事件,不得直接修改退款、订单或钱包数据。随后核验: + +1. 退款单 `RF20260820170954264700` 已通过,关联订单支付状态为已退款。 +2. 该退款没有代理主钱包或资产钱包的零金额回款流水。 + ### 锁的含义与发布影响 `000171` 会对 `tb_agent_wallet`、`000178` 会对 `tb_iot_card` 使用 PostgreSQL `ACCESS EXCLUSIVE` 锁。该锁执行期间会阻塞该表的读写及其他 DDL,直到迁移事务提交或回滚;若有未结束业务查询/事务,它也会等待。因此必须在 API 和全部 Worker 停止后执行,并在迁移前检查没有长事务。锁持续时间取决于表数据量、索引创建和等待中的旧事务;不能从仓库估算具体秒数。 diff --git a/internal/service/refund/service.go b/internal/service/refund/service.go index cef95ca..7c37fd9 100644 --- a/internal/service/refund/service.go +++ b/internal/service/refund/service.go @@ -409,7 +409,7 @@ func (s *Service) appendCompletedNotification(ctx context.Context, tx *gorm.DB, // refundWalletPayment 处理钱包支付订单的退款回款。 func (s *Service) refundWalletPayment(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, order *model.Order, amount int64, operatorID uint) error { - if order.PaymentMethod != model.PaymentMethodWallet { + if amount == 0 || order.PaymentMethod != model.PaymentMethodWallet { return nil } @@ -1201,8 +1201,8 @@ func validateRequestedRefundAmountByOrder(requestedRefundAmount int64, order *mo // validateApprovedRefundAmount 校验审批退款金额不能超过申请金额和订单实收金额。 func validateApprovedRefundAmount(approvedAmount int64, requestedRefundAmount int64, order *model.Order) error { - if approvedAmount <= 0 { - return errors.New(errors.CodeInvalidParam, "审批退款金额必须大于0") + if approvedAmount < 0 { + return errors.New(errors.CodeInvalidParam, "审批退款金额不能小于0") } if approvedAmount > requestedRefundAmount { return errors.New(errors.CodeInvalidParam, "审批退款金额不能大于申请退款金额") diff --git a/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/.openspec.yaml b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/.openspec.yaml new file mode 100644 index 0000000..f774115 --- /dev/null +++ b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-20 diff --git a/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/design.md b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/design.md new file mode 100644 index 0000000..ba84f27 --- /dev/null +++ b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/design.md @@ -0,0 +1,43 @@ +## Context + +见 proposal.md。退款申请创建已允许 0 元金额,但退款通过路径的公共金额校验拒绝 `<= 0`。企微终态消费者和受控人工审批均复用该校验。退款通过事务还会无条件调用钱包回款;代理主钱包回款命令不接受 0 元。 + +## Goals / Non-Goals + +**Goals:** +- 让零金额退款在两条通过路径中保持一致的状态推进语义。 +- 避免零金额钱包流水、余额变更或调用资金回款用例。 +- 保持退款、订单、通知及既有后处理的事务与幂等边界。 + +**Non-Goals:** +- 不允许负数退款金额。 +- 不修改退款申请、企微审批或订单的数据库结构。 +- 不改变非零退款的校验、资金回款和后处理语义。 + +## Decisions + +### 公共金额校验接受零、拒绝负数 +将公共审批退款金额的下限从“必须大于零”调整为“不得小于零”,继续校验不超过申请金额和订单实收金额。这样企微终态与人工路径不会出现规则漂移。 + +备选方案是在企微消费者单独放行零金额;不采用,因为人工审批仍会拒绝,且重复了金额规则。 + +### 零金额在退款事务内跳过资金回款 +当审批通过金额为零时,退款事务仍更新退款和订单状态、写入审计/通知/既有后处理事件,但跳过 `refundWalletPayment`。非零金额保留原调用。 + +备选方案是让钱包模块接受 0 元退款命令;不采用,因为会创建无资金含义的流水并放宽资金模块的金额不变量。 + +### 已失败的生产决策等待安全重试 +现有 `approval:26:approved` 决策投递已经处于失败待重试,部署后由既有重试机制重新执行;若重试已耗尽,维护者按既有受控运维流程重放同一稳定事件,而不直接修改退款、订单或钱包数据。 + +## Risks / Trade-offs + +- [0 元退款仍将订单标记为已退款] → 这是业务已确认语义;审计中保留审批退款金额为 0。 +- [遗漏某条通过路径] → 两条路径复用公共校验,并在退款服务公共回款接缝处按金额分支。 +- [旧失败任务已耗尽重试] → 发布后先核验决策投递状态;必要时以稳定事件 ID 受控重放。 + +## Migration Plan + +1. 发布 API 与 Worker 二进制,不执行数据库迁移。 +2. 维护者核验 `approval:26:approved` 的决策投递是否被重试并成功,以及退款单 `RF20260820170954264700` 是否更新。 +3. 若未自动重试,由维护者受控重放同一审批终态事件并核验未产生 0 元钱包流水。 +4. 回滚仅恢复旧二进制;已完成的零金额退款不回退业务事实。 diff --git a/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/proposal.md b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/proposal.md new file mode 100644 index 0000000..2f4916b --- /dev/null +++ b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/proposal.md @@ -0,0 +1,24 @@ +## Why + +当前退款申请允许保存 0 元申请金额,但企微审批通过后的退款终态处理会拒绝该金额,导致通用审批已通过而退款申请持续待审批、反复重试。业务已确认 0 元退款是有效场景,需要将其作为不产生资金回款的退款完成。 + +## What Changes + +- 允许 0 元退款申请在企微审批或受控人工审批通过后完成。 +- 0 元退款通过时,将退款申请和关联订单推进为已通过/已退款,但不创建代理主钱包或资产钱包回款流水。 +- 保持负数金额、超过申请金额或超过订单实收金额的审批退款金额无效。 + +## Capabilities + +### New Capabilities +- `refund-approval`: 退款审批终态、订单状态和资金回款的业务语义。 + +### Modified Capabilities + +- 无。 + +## Impact + +- `internal/service/refund/` 的退款金额校验、企微审批终态处理和人工审批处理。 +- 退款状态、订单支付状态、钱包流水和退款审批 Outbox 消费链路。 +- 不新增 API、数据库迁移或第三方依赖。 diff --git a/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/specs/refund-approval/spec.md b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/specs/refund-approval/spec.md new file mode 100644 index 0000000..81aae28 --- /dev/null +++ b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/specs/refund-approval/spec.md @@ -0,0 +1,30 @@ +## Purpose + +定义退款审批终态对退款、订单和资金回款事实的统一推进规则,确保零金额退款能够完成而不制造虚假的资金流水。 + +## ADDED Requirements + +### Requirement: 零金额退款审批通过 +系统 SHALL 接受金额为零的退款申请在企业微信审批或受控人工审批通过后完成;退款申请状态 SHALL 更新为已通过,关联订单支付状态 SHALL 更新为已退款,并记录审批通过时间和零金额审批退款金额。 + +#### Scenario: 企业微信通过零金额退款 +- **WHEN** 本地关联的企业微信退款审批返回通过,且申请退款金额为零 +- **THEN** 系统将退款申请和关联订单分别更新为已通过和已退款 + +#### Scenario: 人工通过零金额退款 +- **WHEN** 启用的人工退款审批入口通过申请退款金额为零的退款申请 +- **THEN** 系统将退款申请和关联订单分别更新为已通过和已退款 + +### Requirement: 零金额退款不产生资金回款 +系统 SHALL 在零金额退款通过时不创建代理主钱包或资产钱包的退款回款流水,且不得调用资金回款处理。 + +#### Scenario: 钱包支付订单的零金额退款通过 +- **WHEN** 钱包支付订单关联的零金额退款审批通过 +- **THEN** 系统不写入任何该退款对应的钱包回款流水 + +### Requirement: 非法退款金额仍被拒绝 +系统 MUST 拒绝负数审批退款金额、超过申请退款金额的审批退款金额,以及超过订单实收金额的审批退款金额。 + +#### Scenario: 负数退款金额 +- **WHEN** 审批退款金额小于零 +- **THEN** 系统拒绝完成退款且保持退款与订单原有状态 diff --git a/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/tasks.md b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/tasks.md new file mode 100644 index 0000000..c187272 --- /dev/null +++ b/openspec/changes/archive/2026-08-20-allow-zero-refund-approval/tasks.md @@ -0,0 +1,11 @@ +## 1. 退款通过规则 + +- [x] 1.1 调整公共审批退款金额校验:接受零、拒绝负数,并保留申请金额和订单实收金额上限校验。 +- [x] 1.2 在企微审批终态退款处理里,对零金额跳过钱包回款,同时保持退款、订单、审计、通知和后处理事件的既有事务语义。 +- [x] 1.3 在受控人工退款审批路径复用相同的零金额回款跳过规则。 + +## 2. 验证与运维核验 + +- [x] 2.1 格式化变更的 Go 文件并执行 `go build ./cmd/api ./cmd/worker`。 +- [x] 2.2 执行 `openspec validate allow-zero-refund-approval --strict`,确认行为契约有效。 +- [x] 2.3 为维护者记录生产发布后的核验项:重试或受控重放 `approval:26:approved`,确认退款单 `RF20260820170954264700` 与关联订单完成,且没有零金额钱包回款流水。 diff --git a/openspec/specs/refund-approval/spec.md b/openspec/specs/refund-approval/spec.md new file mode 100644 index 0000000..a1b86dd --- /dev/null +++ b/openspec/specs/refund-approval/spec.md @@ -0,0 +1,32 @@ +# refund-approval Specification + +## Purpose + +定义退款审批终态对退款、订单和资金回款事实的统一推进规则,确保零金额退款能够完成而不制造虚假的资金流水。 + +## Requirements + +### Requirement: 零金额退款审批通过 +系统 SHALL 接受金额为零的退款申请在企业微信审批或受控人工审批通过后完成;退款申请状态 SHALL 更新为已通过,关联订单支付状态 SHALL 更新为已退款,并记录审批通过时间和零金额审批退款金额。 + +#### Scenario: 企业微信通过零金额退款 +- **WHEN** 本地关联的企业微信退款审批返回通过,且申请退款金额为零 +- **THEN** 系统将退款申请和关联订单分别更新为已通过和已退款 + +#### Scenario: 人工通过零金额退款 +- **WHEN** 启用的人工退款审批入口通过申请退款金额为零的退款申请 +- **THEN** 系统将退款申请和关联订单分别更新为已通过和已退款 + +### Requirement: 零金额退款不产生资金回款 +系统 SHALL 在零金额退款通过时不创建代理主钱包或资产钱包的退款回款流水,且不得调用资金回款处理。 + +#### Scenario: 钱包支付订单的零金额退款通过 +- **WHEN** 钱包支付订单关联的零金额退款审批通过 +- **THEN** 系统不写入任何该退款对应的钱包回款流水 + +### Requirement: 非法退款金额仍被拒绝 +系统 MUST 拒绝负数审批退款金额、超过申请退款金额的审批退款金额,以及超过订单实收金额的审批退款金额。 + +#### Scenario: 负数退款金额 +- **WHEN** 审批退款金额小于零 +- **THEN** 系统拒绝完成退款且保持退款与订单原有状态