Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/developer-guide/shop/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@
"name": "theme",
"label": "主题开发",
"collapsed": false
},
{
"type": "dir",
"name": "integration",
"label": "集成",
"collapsed": false
}
]
11 changes: 8 additions & 3 deletions docs/developer-guide/shop/index.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
---
title: 商城开发
description: Halo 商城版开发指南,涵盖商城主题适配、模板与数据、前台交互。
description: Halo 商城版开发指南,涵盖商城主题适配、前台交易流程、订阅集成、Webhook 与履约回调。
overview: true
---

:::note 仅限 Halo 商城版
本板块面向 [Halo 商城版](../../guide/prepare.md#发行版本)。主题开发文档以 **Halo 商城版 2.27** 为基线;使用其他版本时,请同时核对模板分支和对应版本的 API。
本板块面向 [Halo 商城版](../../guide/prepare.md#发行版本),以 **Halo 商城版 2.27** 为基线。使用其他版本时,请同时核对模板分支和对应版本的 API。
:::

商城主题沿用 Halo 的主题机制,可以复用内置商城模板,也可以自行实现商品展示和交易界面。从[主题开发概述](./theme/overview.md)开始,选择适合的开发方式。
根据开发目标选择入口:

- [主题开发](./theme/overview.md):复用内置商城模板,定制商品展示、购物车、结算和支付页面。
- [集成](./integration/overview.md):通过 Webhook 与 RESTful API 对接外部服务,处理订阅权益和履约回执。

开店、商品与订阅配置、订单管理等控制台操作,参见[商城使用指南](../../guide/shop/index.mdx)。
6 changes: 6 additions & 0 deletions docs/developer-guide/shop/integration/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
"overview",
"subscription-lifecycle",
"subscription-webhook",
"fulfillment-callback"
]
161 changes: 161 additions & 0 deletions docs/developer-guide/shop/integration/fulfillment-callback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
title: 履约回调
description: 订阅订单的交付请求通知与发货上报接口:鉴权准备、请求与响应字段、幂等语义、错误处理与对账。
---

:::note 适用范围
本页适用于 **Halo 商城版 2.27.0 及以上版本**。
:::

订阅订单行不走物流发货,Halo **不会自行判定订阅权益是否已经交付**:订单支付后 Halo 通知接入方交付,接入方交付完成后调用 Halo 的接口回执,Halo 据此记账。

```mermaid
sequenceDiagram
participant H as Halo 商城
participant I as 接入方服务
H->>I: ① Webhook FULFILLMENT_REQUESTED(请求交付)
I->>I: 完成交付(开通权益、发放授权等)
I->>H: ② POST /orders/{id}/fulfillment-reports(回执)
H->>H: 记账并刷新订单履约状态
```

- ① 在订单支付成功后自动发出一次,运营也可以在控制台订单详情页重新发送。
- ② 只接受**订阅订单行**。实物行与虚拟商品行沿用原有发货流程,上报会被拒绝。
- 上报不会创建发货单,也不会发放卡密或数字资源;虚拟交付见[商城 / 虚拟交付](../../../guide/shop/virtual-delivery.mdx)。

## 鉴权准备

1. 在控制台创建一个专用 Halo 用户,并绑定角色**「订单发货上报」**(`role-template-report-ecommerce-fulfillments`)。
2. 用该用户登录用户中心,创建个人令牌,只勾选该角色。
3. 在接入方系统中配置令牌,请求时携带 `Authorization: Bearer pat_xxx`。

该角色只包含两项权限:

| API 分组 | 资源 | 权限 |
| -------------------------------- | ---------------------------- | ------------- |
| `console.api.ecommerce.halo.run` | `orders` | `get`、`list` |
| `console.api.ecommerce.halo.run` | `orders/fulfillment-reports` | `create` |

:::warning 先授角色,再签令牌
个人令牌只能申请签发者已有的角色,因此必须先在控制台给该用户授权,再用它创建令牌。请勿把管理员令牌交给接入方服务。
:::

## 交付请求通知(出站)

订单支付成功后,只要订单中还有未履约的订阅行,Halo 就会投递一次 `FULFILLMENT_REQUESTED`:

```
POST {你的回调地址}
X-Halo-Event: FULFILLMENT_REQUESTED
X-Halo-Signature-256: sha256={hex}
X-Halo-Webhook-Id: {uuid}
```

`data.order` 与 `ORDER_PAID` 完全一致,包含订单行列表;订阅行的标识字段见[订阅 Webhook](./subscription-webhook.md#关联的订单事件)。

- 收到该事件表示「Halo 希望你交付这个订单」,**不代表订单已经发货**。
- 该事件在支付后自动发送一次。自动重试与 Webhook **投递记录**中的手动重新投递保留原 `X-Halo-Webhook-Id`;在**订单详情页**重新发送履约请求会产生新事件和新 ID,不能仅凭投递 ID 避免重复交付。
- 漏收时可以请运营在控制台订单详情页重新发送,也可以核对订单后直接上报,Halo 不要求必须先收到通知。

## 发货上报(入站)

```
POST /apis/console.api.ecommerce.halo.run/v1alpha1/orders/{id}/fulfillment-reports
Authorization: Bearer pat_xxx
Content-Type: application/json
```

`{id}` 是订单 ID,即 Webhook 载荷中的 `data.order.id`,不是订单编号。

| 字段 | 类型 | 必填 | 说明 |
| --------------------- | ------ | ---- | ------------------------------------------------------------------------------ |
| `items` | 数组 | 否 | 上报的订单行。**省略、传空数组或使用空请求体表示上报该订单全部未履约的订阅行** |
| `items[].orderItemId` | 数字 | 是 | 订单行 ID,取自载荷中的 `data.order.items[].id`,必须大于 0 |
| `items[].quantity` | 数字 | 是 | 本次交付数量,必须大于 0 |
| `externalReference` | 字符串 | 否 | 接入方单据号,最长 128 字符,仅用于审计留痕 |

请求示例:

```bash
curl -X POST \
'https://demo.halo.run/apis/console.api.ecommerce.halo.run/v1alpha1/orders/10/fulfillment-reports' \
-H 'Authorization: Bearer pat_1234567890abcdef' \
-H 'Content-Type: application/json' \
-d '{"items":[{"orderItemId":1001,"quantity":1}],"externalReference":"SUB-2026-0001"}'
```

响应 `200`:

```json
{
"orderId": 10,
"orderCode": "ORD-20260917-001",
"fulfillmentStatus": "FULFILLED",
"items": [
{
"orderItemId": 1001,
"quantity": 1,
"appliedQuantity": 1,
"fulfilledQuantity": 1
}
]
}
```

| 字段 | 说明 |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `fulfillmentStatus` | 上报后的订单履约状态:`PENDING`(还有未履约行)/ `PROCESSING` / `FULFILLED`。该状态按订单的全部订单行(实物与订阅)计算 |
| `items[].quantity` | 本次上报的数量 |
| `items[].appliedQuantity` | 本次实际记账的数量;重复上报已全部履约的行时为 `0` |
| `items[].fulfilledQuantity` | 记账后该行的累计已履约数量 |

整单上报(省略 `items`)时,目标为该订单所有还有剩余数量的订阅行,已经履约完成的行不会出现在响应中;显式上报的行都会出现在响应里,本次未记账的行 `appliedQuantity` 为 `0`。

## 幂等与重试

| 场景 | 行为 |
| ---------------------------------------------------- | ------------------------------------------------------------------ |
| 订单行已全部履约,再次上报相同或更少数量(网络重试) | 返回 `200`,`appliedQuantity` 为 `0`,不重复计数,也不写订单时间线 |
| 订单行还有剩余,上报数量不超过剩余数量 | 累加本次 `quantity`,返回 `200`;相同参数再次上报仍会计数 |
| 订单行还有剩余,上报数量超过剩余数量 | 返回 `409`,需核对实际交付与已记账数量后再决定是否补报 |
| 并发上报同一订单 | 服务端串行化记账,不会超额 |

:::warning 部分上报不是幂等操作
`quantity` 表示本次新增的交付数量,不是累计数量。例如订单行购买了 3 份,首次上报 1 份已经记账但响应丢失,再次上报 1 份会把累计已履约数量变为 2。`externalReference` 只用于审计,不是服务端幂等键;并发串行化也只能防止超额,不能识别重复报告。
:::

建议:

- 交付成功后立即上报。超时、断网等结果不确定的情况,不要直接重发原参数;先[查询订单](#对账与运维),用 `items[].fulfilledQuantity` 与本地实际交付记录核对,只补报确认尚未记账的数量。
- 以「订单 ID + 订单行 ID」组织本地交付与上报记录,同一订单行串行处理;分批交付时分别记录每批数量和上报结果,避免重复交付或重复记账。
- 查询时若前一次上报仍在处理中,或有其他上报方并发写入,暂时读到的数量不能证明首次请求未生效;应先确认请求结果或交由运营对账,不要根据一次查询立即重试。
- 收到 `409` 时不要盲目重试,也不要直接把数量改成剩余数量;先核对实际交付与已记账数量。

## 错误处理

错误响应遵循 RFC 7807(`application/problem+json`),`detail` 为可读原因并会随 `Accept-Language` 变化,请按 HTTP 状态码判断处理方式。

| HTTP | 含义 | 是否可重试 |
| ----- | --------------------------------------------------------------------------------------------- | -------------- |
| `400` | 请求体校验失败(`orderItemId` 与 `quantity` 必须大于 0,`externalReference` 不超过 128 字符) | 否,修正请求 |
| `400` | 订单中没有订阅行 | 否 |
| `400` | `items` 中同一订单行重复出现 | 否 |
| `400` | `orderItemId` 不属于该订单 | 否 |
| `400` | `orderItemId` 不是订阅行(实物或虚拟商品行) | 否 |
| `401` | 个人令牌无效、已撤销或已过期 | 否,更换令牌 |
| `403` | 令牌缺少上报角色 | 否,补齐角色 |
| `404` | 订单不存在 | 否 |
| `409` | 订单未支付或已取消 | 否 |
| `409` | 上报数量超过剩余可履约数量 | 对账确认后补报 |
| `409` | 并发记账冲突(订单行可能已被超额上报) | 查询订单后重试 |

## 对账与运维

- **查询订单**:`GET /apis/console.api.ecommerce.halo.run/v1alpha1/orders/{id}`(同一令牌可读),用 `items[].quantity`、`items[].fulfilledQuantity` 与 `fulfillmentStatus` 核对是否还有未履约行。
- **查看时间线**:运营可以在控制台订单详情页看到「请求发货」「接入方上报发货」「订单履约状态已更新」等记录,上报写入的内容包含接入方的 `externalReference`。
- **漏收通知**:请运营在订单详情页重新发送发货请求,或核对订单后直接上报。
- **人工兜底**:订阅订单行不能在控制台手动发货,只能由接入方上报。

:::note 上报之后
上报只会更新订单行的已履约数量、刷新订单履约状态并写入一条订单时间线:**不会创建发货单,不会发放卡密或数字资源,也不会触发 `FULFILLMENT_SHIPPED` / `FULFILLMENT_COMPLETED` 事件**。请以接口响应作为记账结果。
:::
5 changes: 5 additions & 0 deletions docs/developer-guide/shop/integration/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: 商城集成
description: Halo 商城订阅集成文档索引,包含接入准备、订阅生命周期、Webhook 与履约回调。
overview: true
---
96 changes: 96 additions & 0 deletions docs/developer-guide/shop/integration/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
title: 商城订阅对接
description: 面向接入方的 Halo 商城订阅对接总览:职责分工、接入方式、个人令牌准备、端到端时序与最小闭环。
---

:::note 适用范围
本组文档适用于 **Halo 商城版 2.27.0 及以上版本**。订阅能力仅 [Halo 商城版](../../../guide/prepare.md#发行版本) 提供。
:::

订阅对接的目标是让 Halo 商城负责**售卖、收款、记录周期**,接入方负责**解释权益、实际交付并回执**。Halo 不解释权益的具体含义,也不会自行判定接入方是否已经交付——这两件事通过 Webhook 与履约上报接口交给接入方。

商家配置订阅产品的操作步骤见[订阅管理](../../../guide/shop/subscriptions.md)。如果需要调整购买、结算或支付页面,参见[商城主题开发](../theme/overview.md)。

## 职责分工

| Halo 商城 | 接入方 |
| ------------------------------------------ | ------------------------------ |
| 产品线、档次(权益契约)、计划、价格与周期 | 定义 `entitlements` 的键与语义 |
| 购物车、结算、支付、订单 | 按事件开通、调整、关闭业务权益 |
| 订阅状态机、续费订单、宽限期、过期 | 用量计量、并发与功能限制 |
| 计划变更报价与折算、变更订单 | 读取订阅快照做鉴权 |
| 订单与订阅事件 Webhook | 交付完成后调用履约上报接口回执 |
| 客户中心(客户自助续费、变更、取消) | 通知、风控、退款等业务规则 |

接入方**不需要**自己实现支付页:店面购买,以及客户中心的续费、变更、取消都由 Halo 提供。

## 接入方式

推荐的接入形态是一个**独立的外部服务**:接收 Halo 的出站 Webhook,并在需要时调用 Halo 的 Console API。这也是当前完整支持订阅业务流程的形态。

| 方向 | 通道 | 用途 |
| --------------------- | -------------------------------------- | ------------------------------------ |
| 出站(Halo → 接入方) | Webhook(HTTP POST,HMAC-SHA256 签名) | 订阅生命周期事件、订单事件、交付请求 |
| 入站(接入方 → Halo) | Console REST API + 个人令牌 | 履约上报、订单与订阅查询 |

### 关于 Halo 插件

订阅对接**不需要也不支持**通过 Halo 插件扩展商城模块:商城与订阅模块当前不提供插件扩展点(extension point),也不会把订单、订阅事件派发给插件。插件可以自行提供 REST API、自定义模型和角色,这是 Halo 的通用能力,但订阅业务流程仍然只能通过上面的两种通道完成。

因此,除为 Halo 增加与订阅无关的自定义功能外,请把接入方实现为独立的外部服务。

## 端到端时序

```mermaid
sequenceDiagram
participant C as 客户
participant H as Halo 商城
participant I as 接入方服务
C->>H: 购买订阅计划并完成支付
H->>H: 开通订阅,写入周期与权益快照
H->>I: Webhook SUBSCRIPTION_CREATED / ORDER_PAID
H->>I: Webhook FULFILLMENT_REQUESTED(请求交付)
I->>I: 为客户开通权益并完成交付
I->>H: POST /orders/{id}/fulfillment-reports(回执)
H->>H: 记账,订单履约状态变为 FULFILLED
Note over H,I: 到期前发送 SUBSCRIPTION_RENEWAL_REMINDER
C->>H: 在客户中心手动续费并支付
H->>I: Webhook SUBSCRIPTION_RENEWED
```

## 接入准备

1. **创建专用用户与角色**:在控制台新建一个 Halo 用户(建议只用于对接),并绑定角色**「订单发货上报」**(`role-template-report-ecommerce-fulfillments`)。
2. **签发个人令牌**:用该用户登录用户中心,进入**个人令牌**创建令牌,只勾选上一步的角色。令牌只在创建时显示一次,请妥善保存。
3. **确认访问地址**:Console API 前缀为 `https://{host}/apis/console.api.ecommerce.halo.run/v1alpha1`,请求头携带 `Authorization: Bearer pat_xxx`。令牌创建方式见[个人中心 / 个人令牌](../../../guide/use/user-center.md#个人令牌),认证方式说明见 [RESTful API 介绍](../../restful-api/introduction.md#认证方式)。
4. **配置 Webhook**:在控制台 **Webhook** 中新建配置,填写回调 URL 与密钥,并订阅需要的[订阅事件](./subscription-webhook.md)。操作步骤见[商城 / Webhook](../../../guide/shop/webhooks.md)。
5. **记录对接标识**:产品线的 `productId`、档次的 `handle`、计划的 `planId` 与 `variantId`。后续所有对账都依赖这些标识。

:::warning 令牌权限范围
「订单发货上报」角色只有读取订单与上报发货两项权限。控制台的订阅查询、权益查询等接口属于 Console 分组,需要管理员权限的个人令牌;请勿把管理员令牌交给第三方服务。
:::

## 最小闭环

1. 运营在控制台配置 `productType=SUBSCRIPTION` 的产品线、档次与计划,操作步骤见[商城 / 订阅管理](../../../guide/shop/subscriptions.md),数据模型见[订阅生命周期](./subscription-lifecycle.md#数据模型)。
2. 接入方订阅 `SUBSCRIPTION_*` 事件与 `FULFILLMENT_REQUESTED`。
3. 客户下单支付后,接入方收到 `SUBSCRIPTION_CREATED` / `SUBSCRIPTION_TRIAL_STARTED`,按[幂等与顺序](./subscription-webhook.md#幂等与顺序)查询确认当前订阅后,使用其 `effectiveEntitlements` 开通权益。
4. 收到 `FULFILLMENT_REQUESTED` 后完成交付,并调用[履约上报接口](./fulfillment-callback.md)回执;**不上报,订单会一直停留在待发货**。
5. 收到取消、过期、变更或续费事件后,按同一流程查询当前订阅,决定停用或刷新权益,避免迟到的旧事件覆盖新状态。

## 本组文档

| 文档 | 内容 |
| ------------------------------------------- | ---------------------------------------------------------------- |
| [订阅生命周期](./subscription-lifecycle.md) | 数据模型、状态机、周期与试用、续费与变更、权益截止判定、查询接口 |
| [订阅 Webhook](./subscription-webhook.md) | 投递格式、验签、重试与去重、订阅事件参考、载荷字段与处理建议 |
| [履约回调](./fulfillment-callback.md) | 交付请求通知、发货上报接口、幂等语义、错误处理与对账 |

相关文档:

- [商城 / Webhook](../../../guide/shop/webhooks.md)
- [商城 / 订阅管理](../../../guide/shop/subscriptions.md)
- [商城 / 订单管理](../../../guide/shop/orders.mdx)
- [商城 / 虚拟交付](../../../guide/shop/virtual-delivery.mdx)
- [RESTful API 介绍](../../restful-api/introduction.md)
- [Halo 商城版 2.27 RESTful API](https://api.halo.run/?version=2.27-paid)
Loading
Loading