幂等、补偿与 Outbox
适用版本:0.7.x 受众:后端开发者、模块维护者
项目的跨模块写操作需要统一处理重复请求、回调重试、补偿重放和异步投递。当前已落地三段共享 contract:spring-open-starter-common 提供稳定幂等键工具,spring-open-core-outbox 提供持久化 Outbox 事件、可靠投递执行器、补偿命令、Webhook 投递和后台运维 API。
幂等键形态
推荐使用四段式作用域键:
consumer:resource:action:businessId字段含义:
| 字段 | 说明 | 示例 |
|---|---|---|
consumer | 消费方或业务域,使用 kebab-case | mall、open-platform |
resource | 资源或单据类型,使用 kebab-case | order、package-order |
action | 本次幂等动作,使用 kebab-case | pay、consume、refund |
businessId | 业务唯一标识,保留原始大小写,不能包含 : | ORDER-100、PO-100 |
示例:
String key = IdempotencyKeys.scoped("Open Platform", "Package Order", "Pay", "PO-100");
// open-platform:package-order:pay:PO-100Core Contract
com.springopen.core.idempotency 提供三个入口:
| 类型 | 用途 |
|---|---|
IdempotencyKey | 包装和校验普通幂等键,保留原始业务大小写 |
ScopedIdempotencyKey | 构造和解析四段式作用域键 |
IdempotencyKeys | 静态工具,包含 normalize(...)、scoped(...) 和长度 / 字符校验 |
校验规则:
- 最大长度为 128 个字符。
- 普通 key 支持 ASCII 字母、数字、
.、_、@、:、/、-。 - 四段式 key 的
businessId不允许包含:,避免解析歧义。 consumer、resource、action会转为 kebab-case,适合跨模块稳定复用。
使用建议
- API 请求里的
idempotencyKey可以先用IdempotencyKey.of(...)校验,再进入业务 Service。 - 服务端从订单号、流水号、回调号等业务事实派生 key 时,优先用
IdempotencyKeys.scoped(...)。 - 同一业务动作必须使用同一 key;不同动作不能复用同一 key。
- Outbox、补偿、重放和失败观测记录应保存同一 key,方便跨表追踪。
Core Outbox Contract
Core Outbox 模块提供后台 Outbox 事件 contract:
| 能力 | API | 权限 |
|---|---|---|
| 分页 / 详情 / 失败汇总 | GET /api/v1/admin/outbox/events、GET /api/v1/admin/outbox/events/{id}、GET /api/v1/admin/outbox/events/failure-summary | outbox:event:view |
| 入队 | POST /api/v1/admin/outbox/events | outbox:event:create |
| 标记失败 / 已投递 | POST /api/v1/admin/outbox/events/{id}/fail、POST /api/v1/admin/outbox/events/{id}/dispatch | outbox:event:update |
| 重放 | POST /api/v1/admin/outbox/events/{id}/replay、POST /api/v1/admin/outbox/events/replay-batch | outbox:event:replay |
| 跳过 | POST /api/v1/admin/outbox/events/{id}/skip | outbox:event:skip |
状态包括 pending、dispatching、dispatched、failed 和 skipped。入队时若传入 idempotencyKey 且未删除记录已存在,会直接返回既有事件,避免重复写入同一业务动作。失败标记会增加 retryCount 并在未达到 maxRetries 前计算 nextRetryAt;重放只允许 failed / skipped 事件回到 pending;已投递事件不能再跳过。
入队时显式传入的 traceId 优先;未传入时会沿用当前 TraceIdContext,仍为空则生成新的安全 traceId。补偿命令使用同样规则,因此 HTTP 请求、异步事件、Outbox、Webhook 和后台重试可以按同一 traceId 串联排查。
失败汇总接口只读取 failed / skipped 事件,按 consumer、eventType、aggregateType 和 status 聚合,返回总数、仍可重试数量、已耗尽重试数量和最近更新时间。Admin Outbox 运维页的“跨业务失败观测”区域会调用该接口,点击某个汇总项后把事件列表筛选到对应消费方和事件类型,便于继续查看详情、重放、批量重放或人工跳过。
可靠投递执行器
Core Outbox 模块提供 OutboxEventHandler 消费方 SPI 和 OutboxEventDispatcher 批量执行器。消费方只需要注册 handler bean 并在 supports(...) 中匹配事件来源 / 类型,执行器会领取可投递事件、调用 handler,并按结果回写状态。
执行器会原子领取 pending、到达 nextRetryAt 的 failed、以及超过锁超时时间的 dispatching 事件。handler 成功后事件标记为 dispatched;handler 抛出异常或没有匹配 handler 时事件标记为 failed,并继续复用既有 retryCount / nextRetryAt / maxRetries 重试语义。handler 必须按事件 idempotencyKey 或业务唯一键实现幂等,因为失败重试和超时锁回收都可能再次投递同一事件。
默认调度配置前缀为 spring.open.outbox.dispatcher:
| 配置 | 默认值 | 说明 |
|---|---|---|
task-name | outbox.dispatcher | Scheduler 任务名 |
instance-id | default | Worker 标识,会写入事件锁字段 |
fixed-delay | 1m | 批量投递间隔 |
lock-timeout | 10m | dispatching 锁超时回收窗口 |
batch-size | 50 | 单轮领取上限 |
自动调度只会在至少存在一个 OutboxEventHandler bean 时注册,避免未接入消费方时把事件批量标记为无 handler 失败。
Webhook 投递底座
Core Outbox 内置 WebhookOutboxEventHandler,可把 Outbox 事件按 HTTP Webhook 投递到外部系统。它复用既有 OutboxEventDispatcher 的领取、失败、重试、锁超时和投递日志语义;HTTP 非配置成功状态响应或网络异常会抛出投递异常,事件继续按 Outbox 的 retryCount / nextRetryAt / maxRetries 规则重试。
默认配置前缀为 spring.open.outbox.webhook:
| 配置 | 默认值 | 说明 |
|---|---|---|
enabled | false | 是否启用 Webhook handler |
consumer | webhook | handler 匹配的 Outbox consumer |
endpoint | 空 | 默认投递地址 |
secret | 空 | 默认 HMAC 签名密钥 |
method | POST | HTTP 方法 |
content-type | application/json | 请求体类型 |
signing-enabled | true | 是否生成签名头 |
success-status-min | 200 | 成功状态码下界,包含边界 |
success-status-max | 299 | 成功状态码上界,包含边界 |
connect-timeout | 5s | HTTP 连接超时 |
request-timeout | 15s | 单次请求超时 |
ssrf.block-private-networks | true | 拦截投递到内网 / 回环 / 链路本地 / CGNAT / 任播 / 多播地址的回调 |
ssrf.allowed-hosts | 空 | 显式放行的目标主机名(精确匹配),用于可信内网 webhook |
ssrf.restrict-to-http-schemes | true | 仅允许 http / https,拦截 file / ftp / gopher 等协议 |
endpoints.<key>.* | 空 | 命名 endpoint,可按事件类型或 header 选择 |
SSRF 防护
Webhook 投递常面向业务方自配置的回调地址,投递前由 WebhookSsrfGuard 校验目标:限制协议为 http/https,解析目标主机的全部 IP,拒绝投递到内网、回环、链路本地、CGNAT(100.64.0.0/10)、IPv6 ULA(fc00::/7)、任播或多播地址。校验所有解析结果(而非首个)以降低 DNS rebinding 风险。
合法的内网 webhook(如服务间回调)应通过 ssrf.allowed-hosts 精确放行对应主机,而不是整体关闭 ssrf.block-private-networks;运行时连接由 JDK HttpClient 完成、仍可能重新解析,因此白名单是更安全的放行方式。
endpoint 选择顺序为:Outbox headers 中的 webhook.endpoint-key、事件 eventType 对应的命名 endpoint、默认 endpoint。headers 也可以覆盖 webhook.endpoint、webhook.secret、webhook.method、webhook.content-type、webhook.signing-enabled、webhook.enabled,并可通过 webhook.header.<Header-Name> 写入业务自定义请求头。
投递请求会自动附带事件 ID、事件类型、聚合类型、聚合 ID、trace、时间戳、body SHA-256 和签名头。签名格式为 sha256=<hex>,签名正文由 METHOD、path、query、时间戳、事件 ID 和 body hash 组成,接收方应使用同一 secret 校验。
Outbox 投递失败通知规则
当可靠投递执行器遇到 handler 异常或没有匹配 handler 时,事件仍会先按既有语义标记为 failed,随后尝试发布通知规则事件源 outbox.dispatch-failed。该事件源只在后台已配置启用的运行时通知规则且规则含用户接收人时发送,不猜测管理员;通知发送异常会被隔离,不影响 Outbox 失败重试。
通知 payload 会携带 Outbox 事件 ID、业务事件 ID、消费方、事件类型、聚合类型、聚合 ID、幂等键、当前状态、重试次数、最大重试次数、下次重试时间、trace、是否无 handler、错误码和错误信息。默认跳转目标为 /outbox,去重键按 outbox.dispatch-failed:<consumer>:<eventType>:<eventId>:<errorCode> 生成,并继续复用通知规则里的去重窗口。
业务模块消费桥接
业务模块可按自身事件语义提供 OutboxEventHandler:
BlogArticleStatusOutboxEventHandler、ForumReportStatusOutboxEventHandler和MallDomainStatusOutboxEventHandler分别由 Blog、Forum、Mall 模块提供;投递时会解析 Outbox payload,并重新发布对应领域事件,确保重放仍沿用既有业务事件语义。OutboxAuditEventHandler由 Core Outbox 模块提供,支持 Payment 回调、用户余额流水和 Web3 结算审计流;投递时发布OutboxAuditEvent,默认不产生额外业务副作用,后续消费方可订阅该审计事件做运营、通知或报表扩展。
支付回调业务适配
Payment 异步回调已作为首个真实业务消费方接入 Outbox。回调验签通过、订单存在、交易流水和业务成功 / 退款处理器执行完成后,会在同一事务中写入 Outbox 事件:
| 字段 | 取值 |
|---|---|
consumer | cashier-payment-callback |
eventType | payment.callback.notify / payment.callback.refund |
aggregateType | payment-order |
aggregateId | 支付订单 ID |
idempotencyKey | cashier-payment:callback:<eventType>:<hash> |
payload 会携带 callbackLogId、orderId、outTradeNo、transactionId、provider、eventType、回调状态、交易状态、金额、币种、traceId 和处理时间,headers 会携带 sourceType=cashier.payment.notify 与 traceId。
验签失败、订单不存在或回调无法进入既有支付处理链路时,只保存回调日志,不写 Outbox 事件。重复回调仍会保留各次 callback log,但 Outbox 通过幂等键复用同一业务事件,避免后续消费方重复投递。
用户余额流水业务适配
Money 用户余额流水已接入 Outbox。MoneyBalanceLedgerService 只有在首次写入新流水并完成账户余额更新后,才会在同一事务中写入 Outbox 事件:
| 字段 | 取值 |
|---|---|
consumer | money-balance-ledger |
eventType | money.balance.ledger.applied |
aggregateType | user-balance-account |
aggregateId | 余额账户 ID |
idempotencyKey | money-balance:ledger:<ledgerIdempotencyKey> |
payload 会携带 ledgerId、accountId、userId、资产编码、流水类型、业务来源、操作人、金额变动、余额前后快照、汇率快照、traceId 和发生时间,headers 会携带 sourceType=money.balance.ledger 与 traceId。
重复 idempotencyKey 调用会直接返回既有余额流水,不重复扣减 / 入账,也不会重复创建 Outbox 事件。
Web3 支付结算业务适配
Web3 支付结算执行器已接入 Outbox。人工复核通过后,执行器会在订单条件更新为成功、写入 web3-settlement 支付流水并完成 Payment 成功处理器后,同事务写入 Outbox 事件:
| 字段 | 取值 |
|---|---|
consumer | money-web3-settlement |
eventType | money.web3.payment-settlement.applied |
aggregateType | payment-order |
aggregateId | 支付订单 ID |
idempotencyKey | money-web3:settlement:<chainName>:<transactionHash> |
payload 会携带复核日志、支付订单、支付流水、Provider、订单状态、金额、币种、链、交易哈希、确认数、区块高度、traceId 和发生时间,headers 会携带 sourceType=money.web3.payment-settlement、traceId、复核指纹、动作和链标识。
重复结算会先命中既有订单成功状态并返回 applied=false,不重复写支付流水、不重复分发 Payment 成功处理器,也不会重复创建 Outbox 事件。
Mall 状态事件业务适配
Mall 订单和售后状态变更已接入 Outbox。订单支付成功、支付状态同步成功、取消订单,以及售后创建、审批、拒绝、关闭、退货收货和退款处理等状态变更,会在发布本地事件前同事务写入 Outbox 事件:
| 字段 | 订单取值 | 售后取值 |
|---|---|---|
consumer | mall-order-status | mall-after-sale-status |
eventType | mall.order.status-changed | mall.after-sale.status-changed |
aggregateType | mall-order | mall-after-sale |
aggregateId | 订单 ID | 售后单 ID |
idempotencyKey | mall-order:status:<orderId>:<previousStatus>:<currentStatus>:<reason> | mall-after-sale:status:<afterSaleId>:<previousStatus>:<currentStatus>:<refundStatus>:<returnStatus>:<reason> |
订单 payload 会携带订单 ID、订单号、用户 ID、变更前后状态、原因、支付状态、支付流水、Provider、Gateway 和发生时间;售后 payload 会携带售后单 ID、售后单号、订单 ID、用户 ID、变更前后状态、退款状态、退货状态、原因、备注和发生时间。headers 会携带 sourceType=mall.order.status 或 sourceType=mall.after-sale.status,并带上业务编号,便于 Outbox 运维页按消费方、事件类型或聚合筛选。
如果状态未变化,Mall 不发布状态事件,也不会写 Outbox。重复执行相同状态快照会通过幂等键复用既有 Outbox 事件,避免后续消费方重复投递。
Mall 模块消费桥接会在 Outbox 投递时重新发布 MallOrderStatusChangedEvent / MallAfterSaleStatusChangedEvent,跨模块重放时仍能触发现有 Mall 状态事件监听链。
Blog 文章状态事件业务适配
Blog 文章状态变更已接入 Outbox。后台处理文章发布、下线、恢复发布或调整发布时间时,BlogArticleService.updateStatus 会在文章状态或发布时间快照变化后写入 Outbox 事件:
| 字段 | 取值 |
|---|---|
consumer | blog-article-status |
eventType | blog.article.status-changed |
aggregateType | blog-article |
aggregateId | 文章 ID |
idempotencyKey | blog-article:status:<articleId>:<previousStatus>:<currentStatus>:<previousPublishedAt>:<currentPublishedAt> |
payload 会携带文章 ID、slug、标题、作者 ID、作者名、变更前后状态、变更前后发布时间和发生时间。headers 会携带 module=blog、sourceType=blog.article.status、sourceId 和发生时间,便于 Outbox 运维页按消费方、事件类型或文章聚合筛选。
如果状态和发布时间快照均未变化,Blog 不写 Outbox。重复提交相同状态快照会通过幂等键复用既有 Outbox 事件,避免后续消费方重复投递。
Blog 模块消费桥接会在 Outbox 投递时重新发布 BlogArticleStatusChangedEvent,跨模块重放时仍能触发现有 Blog 文章状态事件监听链。
Forum 举报状态事件业务适配
Forum 举报状态变更已接入 Outbox。后台处理举报状态时,ForumInteractionService.updateReportStatus 会在状态真实变化后通过可选生产方写入 Outbox 事件:
| 字段 | 取值 |
|---|---|
consumer | forum-report-status |
eventType | forum.report.status-changed |
aggregateType | forum-report |
aggregateId | 举报 ID |
idempotencyKey | forum-report:status:<reportId>:<previousStatus>:<currentStatus>:<operatorUserId> |
payload 会携带举报 ID、举报人、目标类型、目标 ID、变更前后状态、操作人和发生时间。headers 会携带 module=forum、sourceType=forum.report.status、sourceId 和发生时间,便于 Outbox 运维页按消费方、事件类型或举报聚合筛选。
Forum 模块消费桥接会在 Outbox 投递时重新发布 ForumReportStatusChangedEvent,跨模块重放时仍能触发现有 Forum 举报状态事件监听链。
Outbox Compensation Command Contract
Core Outbox 模块提供后台补偿命令 contract:
| 能力 | API | 权限 |
|---|---|---|
| 分页 / 详情 / 失败汇总 | GET /api/v1/admin/outbox/compensation-commands、GET /api/v1/admin/outbox/compensation-commands/{id}、GET /api/v1/admin/outbox/compensation-commands/failure-summary | outbox:compensation-command:view |
| 入队 | POST /api/v1/admin/outbox/compensation-commands | outbox:compensation-command:create |
| 标记运行 / 成功 / 失败 | POST /api/v1/admin/outbox/compensation-commands/{id}/running、POST /api/v1/admin/outbox/compensation-commands/{id}/succeed、POST /api/v1/admin/outbox/compensation-commands/{id}/fail | outbox:compensation-command:update |
| 重放 | POST /api/v1/admin/outbox/compensation-commands/{id}/replay、POST /api/v1/admin/outbox/compensation-commands/replay-batch | outbox:compensation-command:replay |
| 跳过 | POST /api/v1/admin/outbox/compensation-commands/{id}/skip | outbox:compensation-command:skip |
状态包括 pending、running、succeeded、failed 和 skipped。入队时若传入 idempotencyKey 且未删除记录已存在,会直接返回既有命令,避免重复创建同一补偿动作。失败标记会增加 retryCount 并在未达到 maxRetries 前计算 nextRetryAt;重放只允许 failed / skipped 命令回到 pending;成功命令不能再跳过,已跳过命令不能标记成功。
失败汇总接口只读取 failed / skipped 补偿命令,按 consumer、commandType、aggregateType 和 status 聚合,返回总数、仍可重试数量、已耗尽重试数量和最近更新时间。Admin Outbox 运维页的“补偿失败观测”区域会调用该接口,点击某个汇总项后把补偿命令列表筛选到对应消费方和命令类型,便于继续查看详情、重放、批量重放或人工跳过。
Core Outbox 模块提供 CompensationCommandHandler 消费方 SPI 和 CompensationCommandExecutor 批量执行器。消费方注册 handler bean 后在 supports(...) 中匹配 consumer / commandType,执行器会领取可执行命令、调用 handler,并按结果回写状态。
执行器会原子领取 pending、到达 nextRetryAt 的 failed、以及超过锁超时时间的 running 命令。handler 成功后命令标记为 succeeded;handler 抛出异常或没有匹配 handler 时命令标记为 failed,并继续复用既有 retryCount / nextRetryAt / maxRetries 重试语义。handler 必须按命令 idempotencyKey 或业务唯一键实现幂等,因为失败重试和超时锁回收都可能再次执行同一命令。
执行器测试已覆盖真实失败重放链路:handler 首次抛错时命令进入 failed 并触发失败通知;管理员或调度侧执行重放后,命令回到 pending,待 handler 恢复后再次执行会标记为 succeeded,同时保留既有 retryCount 轨迹,便于排障追踪。
当补偿命令执行器遇到 handler 异常或没有匹配 handler 时,命令仍会先按既有语义标记为 failed,随后尝试发布通知规则事件源 outbox.compensation-command.failed。该事件源只在后台已配置启用的运行时通知规则且规则含用户接收人时发送;通知发送异常会被隔离,不影响命令重试、人工重放或跳过。
通知 payload 会携带补偿命令 ID、业务命令 ID、消费方、命令类型、聚合类型、聚合 ID、幂等键、当前状态、重试次数、最大重试次数、下次重试时间、trace、是否无 handler、错误码和错误信息。默认跳转目标为 /outbox,去重键按 outbox.compensation-command.failed:<consumer>:<commandType>:<commandId>:<errorCode> 生成,并继续复用通知规则里的去重窗口。
默认调度配置前缀为 spring.open.outbox.compensation-command:
| 配置 | 默认值 | 说明 |
|---|---|---|
task-name | outbox.compensation-command.executor | Scheduler 任务名 |
instance-id | default | Worker 标识,会写入命令锁字段 |
fixed-delay | 1m | 批量执行间隔 |
lock-timeout | 10m | running 锁超时回收窗口 |
batch-size | 50 | 单轮领取上限 |
自动调度只会在至少存在一个 CompensationCommandHandler bean 时注册,避免未接入业务 handler 时把命令批量标记为无 handler 失败。
已内置的业务 handler:
| 消费方 | 命令类型 | 说明 |
|---|---|---|
cashier-payment | cashier.payment.compensate-pending-orders | 复用 Cashier 支付补偿 Service,可按 payload.outTradeNo 单笔补偿,或按 limit / olderThanMinutes 扫描 pending 订单 |
mall | mall.order.release-expired-pending | 复用 Mall 超时待支付订单释放能力,可按 payload.expiredBefore 和 payload.limit 扫描并释放过期待支付订单 |
blog | blog.article.update-status | 复用 Blog 文章状态更新能力,可按 articleId、status 和 publishedAt 重放发布 / 下线状态 |
blog | blog.comment.update-status | 复用 Blog 评论状态更新能力,可按 commentId 和 status 重放显示 / 隐藏状态 |
forum | forum.report.update-status | 复用 Forum 举报状态处理能力,可按 reportId、status 和 operatorId 重放举报处理状态 |
cms | cms.content.update-status | 复用 CMS 内容状态更新能力,可按 contentId、status、visibleAt 和 expiredAt 重放发布 / 下线与展示窗口 |
short-link | short-link.link.update-status | 复用短链接保存能力,可按 shortLinkId 和 status 重放启用 / 停用状态 |
live-code | live-code.live-code.update-status | 复用 Live Code 活码保存能力,可按 liveCodeId 和 status 重放启用 / 禁用状态 |
open-platform | open-platform.app.update-status | 复用 Open Platform 应用保存能力,可按 appId 和 status 重放启用 / 禁用状态 |
open-platform | open-platform.package.update-status | 复用 Open Platform 套餐保存能力,可按 packageId 和 status 重放启用 / 禁用状态 |
支付补偿命令的 payload 与 /api/v1/admin/cashier/payment-compensations 请求体一致,支持 outTradeNo、limit、olderThanMinutes 和 traceId。handler 会优先使用 payload 中的 traceId,未传时使用补偿命令自身 traceId。如果补偿结果包含失败项,命令会标记为 failed,继续使用 retryCount / nextRetryAt / maxRetries 重试语义。
Mall 超时订单释放补偿命令的 payload 支持 expiredBefore(ISO-8601 本地时间,例如 2026-06-03T10:15:30)和 limit;字段为空时沿用 Mall 订单服务默认超时窗口和单次数量口径。该命令适合在自动调度异常、人工排障或跨模块补偿演练时,通过 Outbox 运维页统一入队、重放和观察执行状态。
Blog 文章状态补偿命令的 payload 支持 articleId、status 和 publishedAt;也可直接复用 Blog 文章状态 Outbox payload 中的 currentStatus / currentPublishedAt 字段。articleId 缺失时会尝试读取补偿命令的 aggregateId,便于从文章聚合维度重建命令。
Blog 评论状态补偿命令的 payload 支持 commentId、id 和 status;也可复用状态事件常见 payload 中的 currentStatus 字段。commentId 缺失时会尝试读取补偿命令的 aggregateId,handler 只允许重放 Blog 评论已发布 / 隐藏状态。
Forum 举报状态补偿命令的 payload 支持 reportId、status、operatorId、reason、content 和 extraJson;也可直接复用 Forum 举报状态 Outbox payload 中的 currentStatus / operatorUserId 字段。reportId 缺失时会尝试读取补偿命令的 aggregateId,便于从举报聚合维度重建命令。
CMS 内容状态补偿命令的 payload 支持 contentId、status、visibleAt 和 expiredAt;也可复用内容状态事件常见 payload 中的 currentStatus、currentVisibleAt 和 currentExpiredAt 字段。contentId 缺失时会尝试读取补偿命令的 aggregateId,便于从内容聚合维度重建命令。
Short Link 状态补偿命令的 payload 支持 shortLinkId、linkId、id 和 status;也可复用状态事件常见 payload 中的 currentStatus 字段。shortLinkId 缺失时会尝试读取补偿命令的 aggregateId,handler 会先读取短链接当前详情,再使用 accessPasswordAction=keep 构造保存命令,只重放启用 / 停用状态并保留原短码、目标 URL、UTM、过期时间和访问密码。
Live Code 活码状态补偿命令的 payload 支持 liveCodeId、codeId、id 和 status;也可复用状态事件常见 payload 中的 currentStatus 字段。liveCodeId 缺失时会尝试读取补偿命令的 aggregateId,handler 会先读取活码当前详情,再使用 accessPasswordAction=keep 构造保存命令,只重放启用 / 禁用状态并保留原编码、目标 URL、规则分流、二维码样式、有效期和访问密码。
Open Platform 应用状态补偿命令的 payload 支持 appId、applicationId、id 和 status;也可复用状态事件常见 payload 中的 currentStatus 字段。appId 缺失时会尝试读取补偿命令的 aggregateId,handler 会先读取应用当前详情,再构造保存命令,只重放启用 / 禁用状态并保留名称、套餐、白名单、限流、日 / 月配额和备注。
Open Platform 套餐状态补偿命令的 payload 支持 packageId、planId、id 和 status;也可复用状态事件常见 payload 中的 currentStatus 字段。packageId 缺失时会尝试读取补偿命令的 aggregateId,handler 会先读取套餐当前详情和阶梯价格,再构造保存命令,只重放启用 / 禁用状态并保留套餐编码、名称、配额、单价、币种、有效期、排序、备注和阶梯价格。
后续边界
Admin 前端运维页、跨业务失败观测、补偿命令失败观测、补偿命令执行器与调度任务、补偿命令失败通知规则事件源、支付补偿命令 handler、Mall 超时订单释放补偿命令 handler、Blog 文章状态补偿命令 handler、Blog 评论状态补偿命令 handler、Forum 举报状态补偿命令 handler、CMS 内容状态补偿命令 handler、Short Link 状态补偿命令 handler、Live Code 活码状态补偿命令 handler、Open Platform 应用状态补偿命令 handler、Open Platform 套餐状态补偿命令 handler、补偿命令执行器失败后恢复重放演练、支付补偿命令执行器成功 / 失败演练、支付回调消费方适配、用户余额流水生产方适配、Web3 支付结算生产方适配、Mall / Blog / Forum 状态消费桥接和 Audit 消费桥接已完成;更多补偿命令业务 handler 适配和其它跨业务失败演练仍属于后续 P1-FDN-05 功能簇。