From f5f03d18039dcae269f3e0813a1ab13e5a238a85 Mon Sep 17 00:00:00 2001 From: break Date: Wed, 9 Sep 2026 10:01:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=94=AF=E4=BB=98=E8=B4=AD?= =?UTF-8?q?=E5=8C=85=E5=90=8E=E8=87=AA=E5=8A=A8=E5=A4=8D=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/bootstrap/services.go | 1 + .../.openspec.yaml | 2 + .../design.md | 33 +++++++++++ .../proposal.md | 26 +++++++++ .../specs/package-lifecycle/spec.md | 55 +++++++++++++++++++ .../tasks.md | 10 ++++ 6 files changed, 127 insertions(+) create mode 100644 openspec/changes/fix-immediate-package-payment-resume/.openspec.yaml create mode 100644 openspec/changes/fix-immediate-package-payment-resume/design.md create mode 100644 openspec/changes/fix-immediate-package-payment-resume/proposal.md create mode 100644 openspec/changes/fix-immediate-package-payment-resume/specs/package-lifecycle/spec.md create mode 100644 openspec/changes/fix-immediate-package-payment-resume/tasks.md diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index 5c2d527..8e49687 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -264,6 +264,7 @@ func initServices(s *stores, deps *Dependencies) *services { packageSeriesService := packageSeriesSvc.New(s.PackageSeries, s.ShopSeriesAllocation, s.Package) packageSeriesService.SetAccessAudit(deps.DB, auditWriter) orderService := orderSvc.New(deps.DB, deps.Redis, s.Order, s.OrderItem, s.AgentWallet, s.AssetWallet, s.Payment, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, s.PackageUsage, s.Package, wechatConfig, deps.WechatPayment, paymentLoader, deps.QueueClient, deps.Logger, s.AssetIdentifier, s.PersonalCustomer, s.PersonalCustomerPhone) + orderService.SetResumeCallback(stopResumeService) orderService.SetLifecycleAudit(auditWriter) orderService.SetPaymentIntegrationLog(integrationlog.NewRepository(deps.DB)) orderService.SetObservationSeriesEventWriter(observationSeriesEvents) diff --git a/openspec/changes/fix-immediate-package-payment-resume/.openspec.yaml b/openspec/changes/fix-immediate-package-payment-resume/.openspec.yaml new file mode 100644 index 0000000..1ea7e36 --- /dev/null +++ b/openspec/changes/fix-immediate-package-payment-resume/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-09 diff --git a/openspec/changes/fix-immediate-package-payment-resume/design.md b/openspec/changes/fix-immediate-package-payment-resume/design.md new file mode 100644 index 0000000..2bed6e1 --- /dev/null +++ b/openspec/changes/fix-immediate-package-payment-resume/design.md @@ -0,0 +1,33 @@ +## Context + +API 与 Worker 分别创建订单服务。Worker 已为订单服务注入停复机模块,API 未注入;因此支付回调调用支付后自动复机入口时没有可执行的回调。支付后自动复机入口在事务提交后查询本订单的已生效主套餐,再异步调用停复机模块。 + +## Goals / Non-Goals + +**Goals:** + +- 使 API 支付回调复用既有的支付后自动复机入口。 +- 保持“仅已生效主套餐触发”的现有查询语义和停复机资格判断。 + +**Non-Goals:** + +- 不改变套餐权益状态、支付事务范围、Gateway 重试和轮询调度。 +- 不让支付回调等待 Gateway 复机结果。 +- 不新增队列、端口、数据表或配置。 + +## Decisions + +### 在 API Composition Root 注入既有回调 + +在 API 创建订单服务后调用既有的 `SetResumeCallback(stopResumeService)`。这是 Worker 已使用的装配方式;复用同一模块可将卡/设备载体解析、有效套餐、剩余流量、实名、停机原因、分布式锁和 Gateway 调用保持在一个实现内。 + +不在支付回调中直接调用 Gateway,也不复制资格判断。直接调用会绕过已存在的幂等锁、设备多卡处理和审计/观测写入。 + +### 以事务提交后的已生效权益为复机前置条件 + +保留订单服务当前的查询:仅本订单 `status=Active` 且 `master_usage_id IS NULL` 的主套餐权益可触发复机。查询发生在支付与权益事务成功返回后,避免使用未提交或排队权益快照。复机模块在异步执行时重新读取卡及权益事实,防止并发状态变化导致错误复机。 + +## Risks / Trade-offs + +- Gateway 调用仍是异步副作用;支付成功不等于复机成功。该取舍保持支付事实可靠落库,不将外部网络延迟或失败耦合到支付回调。 +- API 进程现在可发起复机;其依赖已由同一 Composition Root 配置,且执行路径与 Worker 一致。 \ No newline at end of file diff --git a/openspec/changes/fix-immediate-package-payment-resume/proposal.md b/openspec/changes/fix-immediate-package-payment-resume/proposal.md new file mode 100644 index 0000000..f5e9775 --- /dev/null +++ b/openspec/changes/fix-immediate-package-payment-resume/proposal.md @@ -0,0 +1,26 @@ +## Why + +套餐订单的第三方支付回调已在提交套餐权益后调用自动复机入口,但 API 进程的订单服务未注入复机回调。支付成功后,卡只能等待异步观测或套餐轮询才可能恢复网络,不能满足支付成功后立即按最新业务事实检查并复机的要求。 + +## What Changes + +- 在 API 进程为订单服务注入已有的停复机模块,使套餐订单支付事务成功提交后立即触发一次自动复机资格检查。 +- 仅对本订单已持久化为生效中的主套餐触发检查;排队、待实名或其他未生效主套餐不得触发复机。 +- 沿用已有停复机模块的套餐、流量、实名、停机原因和幂等锁判断;支付回调不等待 Gateway 调用完成。 +- 不修改套餐生效状态机、支付状态机、Worker 复机逻辑或轮询兜底机制。 + +## Capabilities + +### New Capabilities + +- 无。 + +### Modified Capabilities + +- `package-lifecycle`: 规定套餐订单支付成功后,已生效主套餐触发一次基于已提交业务事实的即时自动复机检查。 + +## Impact + +- 代码:`internal/bootstrap/services.go`。 +- 运行时:API 支付回调完成数据库事务后异步调用既有 Gateway 复机路径;无需迁移、路由或新的外部协议。 +- 同步:补丁先合入 `main`,再以单独提交定点同步到 `Iteration/8-11`。 \ No newline at end of file diff --git a/openspec/changes/fix-immediate-package-payment-resume/specs/package-lifecycle/spec.md b/openspec/changes/fix-immediate-package-payment-resume/specs/package-lifecycle/spec.md new file mode 100644 index 0000000..eefb725 --- /dev/null +++ b/openspec/changes/fix-immediate-package-payment-resume/specs/package-lifecycle/spec.md @@ -0,0 +1,55 @@ +## MODIFIED Requirements + +### Requirement: 套餐状态流转 + +系统 SHALL 按当前套餐和套餐使用状态控制上架、订购、激活、失效与到期处理。主套餐到期时,系统 MUST 先确定同一载体是否存在待生效的后续主套餐:存在时,后续套餐激活与停复机重新评估 MUST 由同一条顺序流程完成;系统 MUST NOT 依据后续套餐激活前的无套餐快照发起停机。后续套餐成功生效后,系统 MUST 依据最新套餐、流量和实名事实重新判断卡网络状态,且不得遗留 `no_package` 停机。不存在后续套餐或后续套餐经业务校验不能生效时,系统 SHALL 按现有停机规则评估卡状态。后续套餐激活结果未知或任务投递失败不得被当作无后续套餐处理并据此停机,系统 SHALL 保留既有激活恢复与轮询兜底路径。 + +套餐订单支付成功时,系统 MUST 在支付和套餐权益事务提交后,检查本订单是否存在已生效且未挂靠其他主套餐的主套餐权益。存在时,系统 MUST 基于已提交的套餐、流量、实名和停机原因事实异步尝试自动复机;支付回调不得等待上游复机结果。主套餐权益处于待生效、待实名生效或其他非生效状态时,系统 MUST NOT 因本次支付发起自动复机。自动复机失败 SHALL 保留既有失败记录与轮询兜底机制。 + +#### Scenario: 支付后已生效主套餐触发自动复机 + +- **GIVEN** 套餐订单支付成功后,本订单存在已生效且未挂靠其他主套餐的主套餐权益,载体因可自动恢复的原因处于停机状态 +- **WHEN** 支付和套餐权益事务提交成功 +- **THEN** 系统异步按最新套餐、流量、实名和停机原因事实检查载体,并在全部复机条件满足时调用既有上游复机流程;支付回调不等待该调用结束 + +#### Scenario: 支付后主套餐未生效不触发自动复机 + +- **GIVEN** 套餐订单支付成功后,本订单主套餐权益仍处于待生效、待实名生效或其他非生效状态 +- **WHEN** 支付和套餐权益事务提交成功 +- **THEN** 系统不因本次支付发起自动复机,并保留后续套餐激活和轮询处理 + +#### Scenario: 支付后复机条件不满足 + +- **GIVEN** 套餐订单支付成功后,本订单存在已生效主套餐权益 +- **WHEN** 载体为手动停机、无有效套餐、流量已耗尽或不满足实名策略 +- **THEN** 系统不调用上游复机流程,保留当前网络状态与既有停复机处理路径 + +#### Scenario: 套餐状态流转 + +- **GIVEN** 套餐或使用记录处于允许的前置状态 +- **WHEN** 执行状态操作 +- **THEN** 仅发生一次允许的状态变化;不满足前置状态时返回业务错误 + +#### Scenario: 到期主套餐接续后续套餐 + +- **GIVEN** 某载体的当前主套餐到期,且存在满足激活条件的待生效后续主套餐 +- **WHEN** 系统处理该主套餐到期 +- **THEN** 系统先完成后续套餐激活并按最新权益事实重新评估停复机,且不得因到期前的无套餐快照对该载体发起 `no_package` 停机 + +#### Scenario: 到期主套餐无后续可生效套餐 + +- **GIVEN** 某载体的当前主套餐到期,且不存在后续主套餐或队首后续套餐不满足激活条件 +- **WHEN** 系统完成该套餐到期处理 +- **THEN** 系统按当前套餐、流量和实名事实执行既有停机评估 + +#### Scenario: 后续套餐激活结果未知 + +- **GIVEN** 某载体的当前主套餐到期,存在待生效后续主套餐,但激活任务投递或执行结果暂时未知 +- **WHEN** 系统处理该套餐到期 +- **THEN** 系统不得将该未知结果视为不存在后续套餐而依据旧快照发起停机,并保留既有激活恢复与套餐轮询兜底 + +#### Scenario: 卡状态轮询发现缺失的套餐任务 + +- **GIVEN** 启用轮询的卡匹配套餐检查配置,且其 `polling:package` 分片队列项因异常缺失 +- **WHEN** 卡状态轮询成功完成且未命中风险停机 +- **THEN** 系统基于最新卡状态仅补入缺失的套餐任务,不改写已存在套餐任务的执行时间;后续套餐任务仍按既有停复机条件评估该卡 diff --git a/openspec/changes/fix-immediate-package-payment-resume/tasks.md b/openspec/changes/fix-immediate-package-payment-resume/tasks.md new file mode 100644 index 0000000..b805d05 --- /dev/null +++ b/openspec/changes/fix-immediate-package-payment-resume/tasks.md @@ -0,0 +1,10 @@ +## 1. 主干即时复机 + +- [x] 1.1 在 API Composition Root 为订单服务注入既有停复机回调,不改变支付或套餐状态机。 +- [x] 1.2 格式化变更文件并构建 API,核对支付事务提交后仅已生效主套餐进入现有自动复机路径。 +- [ ] 1.3 校验 OpenSpec 变更并将契约和代码以独立中文提交合入 `main`。 + +## 2. 八月迭代同步 + +- [ ] 2.1 在八月迭代分支的在途改动提交且工作区干净后,定点同步主干补丁提交并保留在途支付商户装配。 +- [ ] 2.2 构建八月迭代分支 API,并核对同一 API 回调装配存在。 \ No newline at end of file