Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion docs/user-guide/shop/orders.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ description: 商城订单管理相关使用文档。

:::info 提示
1. 目前仅支持填写物流单号等信息,目前没有对接物流查询平台,所以需要用户手动根据物流单号查询物流信息。
2. 目前仅支持实体物流发货,虚拟产品暂不支持,后续会提供虚拟物品池、付费后下载文件、Webhook、自动发货等功能
2. 虚拟产品发货可参考:[虚拟交付](./virtual-delivery.mdx)
:::
4 changes: 4 additions & 0 deletions docs/user-guide/shop/products.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import TabItem from '@theme/TabItem';
2. 虚拟:不需要实体配送,使用线上发货的产品类型
3. 链接:直接跳转到第三方链接的产品类型

:::info 提示
虚拟产品支持文件资源下载、卡密发货,可参考 [虚拟交付](./virtual-delivery.mdx) 进行发货配置。
:::

#### 属性配置

属性的作用是为产品添加一些额外的信息,比如颜色、尺寸、型号等,同时也可以用于生成产品规格。
Expand Down
260 changes: 260 additions & 0 deletions docs/user-guide/shop/virtual-delivery.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
---
title: 虚拟交付
description: 商城虚拟交付相关使用文档。
---

在 Halo 2.23 中,我们完善了虚拟产品的发货功能,目前支持文件资源下载、卡密发货。

## 创建产品

完整的创建产品流程可参考 [产品管理](./products.mdx) 文档,需要注意的是,如果产品要支持虚拟交付,需要在产品类型中选择 **虚拟** 产品类型。

![](/img/user-guide/shop/products-create-virtual.png)

## 设置文件资源下载

创建完产品之后,我们需要进入 **产品 -> 虚拟交付** 页面,然后选择刚刚创建的产品。

![](/img/user-guide/shop/virtual-delivery-products.png)

进入设置页面之后,就可以看到 **资源下载** 的设置页面,我们可以点击右上方的 **添加** 按钮来添加文件。

![](/img/user-guide/shop/virtual-delivery-resource-add.png)

- 资源类型:支持附件和静态 URL
- 资源名称:为客户显示的文件名称
- 资源描述:可以填写文件的描述信息
- 附件(当类型为附件时):可以点击 `+` 按钮选择已上传的附件或者上传新的附件
- 资源地址(当类型为静态 URL 时):可以填写文件的 URL 地址

填写完成之后,点击 **保存** 按钮即可。

当文件添加完成之后,我们还需要手动启用自动发货,点击右上角的 **发货配置** 按钮,勾选启用即可。

![](/img/user-guide/shop/virtual-delivery-resource-config.png)

## 设置卡密发货

进入虚拟交付设置页面之后,选择 **卡密资产** 选项卡,就可以看到 **卡密资产** 的设置页面。

![](/img/user-guide/shop/virtual-delivery-cdks.png)

我们可以点击右上方的 **添加** 按钮来添加卡密。

![](/img/user-guide/shop/virtual-delivery-cdk-add.png)

- 卡密:卡密内容,支持批量添加,一行一个
- 过期时间:卡密过期时间
- 备注:可以填写卡密的备注信息

填写完成之后点击 **添加** 按钮即可。

当卡密添加完成之后,我们还需要手动启用自动发货,点击右上角的 **发货配置** 按钮,勾选启用即可。

![](/img/user-guide/shop/virtual-delivery-cdk-config.png)

此外,在卡密发货配置界面中,还支持配置 **远程调用配置**,支持通过三方 API 来补货或者动态生成卡密并发货。

定义:

- 预获取:优先从卡密列表中选择并发货,在库存不足时才调用 API 进行补货
- 动态获取:每次发货时都调用 API 获取卡密并发货

配置说明:

- 类型:支持 **预获取** 和 **动态获取**
- 补货配置(当类型为预获取时)
- API 地址:补货 API 地址
- 认证 Token:补货 API 认证 Token,用于对 API 请求进行认证
- 阈值:可用卡密数量低于此值时触发补货
- 每次获取的卡密数量:每次调用 API 获取的卡密数量
- 服务器配置(当类型为动态获取时)
- API 地址:远程生成 API 地址
- 认证 Token:API 认证 Token,用于对 API 请求进行认证

需要注意的是,API 需要自行实现并适配 Halo 的接口标准,可以参考下方的 [API 规范](#api-规范)。

## 订单相关

为产品配置好虚拟交付之后,当客户下单并支付时,系统会自动为订单进行发货流程,可以在控制台的订单详情中看到发货状态。

![](/img/user-guide/shop/virtual-delivery-console-order.png)

客户也可以在订单页面看到已发货的虚拟物品。

![](/img/user-guide/shop/virtual-delivery-uc-order-1.png)

![](/img/user-guide/shop/virtual-delivery-uc-order-2.png)

## API 规范

### 卡密远程生成 API 规范

用户下单后,Halo 会向配置的远程地址发起请求,由你的服务实时生成或分配卡密。

#### 请求

```
POST <你配置的远程生成地址>
Content-Type: application/json
Authorization: Bearer <token> // 配置了 token 时附带
```

**请求体**

```json
{
"orderItem": {
"id": 10001,
"orderId": 90001,
"productId": 80001,
"quantity": 2,
"itemTitle": "年度会员兑换码",
"productVariantSnapshot": {
"id": 123,
"name": "默认规格"
}
},
"dryRun": false
}
```

| 字段 | 类型 | 必填 | 说明 |
| ---------------------------------- | ------- | ---- | ------------------------- |
| `orderItem.id` | long | 是 | 订单项 ID(可用作幂等键) |
| `orderItem.orderId` | long | 是 | 订单 ID |
| `orderItem.productId` | long | 是 | 商品 ID |
| `orderItem.quantity` | int | 是 | 需要发放的卡密数量 |
| `orderItem.itemTitle` | string | 否 | 订单项标题 |
| `orderItem.productVariantSnapshot` | object | 否 | 规格快照 |
| `dryRun` | boolean | 是 | 当前固定为 `false` |

#### 响应

返回卡密列表,所有卡密的 `capacity` 之和须不小于 `orderItem.quantity`。

```json
[
{
"code": "ABCD-EFGH-IJKL",
"secret": "S3cr3t-1",
"expireAt": "2026-12-31T23:59:59Z",
"capacity": 1
}
]
```

| 字段 | 类型 | 必填 | 说明 |
| ---------- | ------ | ---- | ------------------------- |
| `code` | string | 是 | 卡密(同一响应内唯一) |
| `secret` | string | 否 | 附加验证信息 |
| `expireAt` | string | 否 | 过期时间(UTC,ISO-8601) |
| `capacity` | int | 否 | 可用次数,默认为 `1` |

:::warning 注意
响应不可为空列表,否则触发卡密发放失败。建议以 `orderItem.id` 作为幂等键,避免重复请求导致重复发放。
:::

#### 错误码

建议使用非 `2xx` 状态码表达失败,并返回结构化错误:

```json
{
"code": "CDK_OUT_OF_STOCK",
"message": "No enough CDKs available",
"requestId": "f38db1c2-2f7a-4dc3-b5a8-2d0012345678"
}
```

推荐错误码:

| HTTP 状态码 | 错误码 | 含义 | 是否可重试 |
| ----------- | --------------------- | ---------------------- | ---------- |
| `400` | `INVALID_REQUEST` | 请求字段不合法 | 否 |
| `401` | `UNAUTHORIZED` | 鉴权失败 | 否 |
| `403` | `FORBIDDEN` | 调用被拒绝 | 否 |
| `404` | `NOT_FOUND` | 路由不存在 | 否 |
| `409` | `IDEMPOTENT_CONFLICT` | 幂等键冲突且参数不一致 | 否 |
| `422` | `CDK_OUT_OF_STOCK` | 库存不足 | 视业务而定 |
| `429` | `RATE_LIMITED` | 触发限流 | 是 |
| `500` | `INTERNAL_ERROR` | 远程服务内部错误 | 是 |
| `503` | `SERVICE_UNAVAILABLE` | 服务不可用 | 是 |

### 卡密自动补货 API 规范

当密钥库存低于阈值时,Halo 会自动向配置的补货地址发起补货请求。

#### 请求

```
POST <你配置的补货地址>
Content-Type: application/json
Authorization: Bearer <token> // 配置了 token 时附带
```

**请求体**

```json
{
"productVariantId": 123,
"quantity": 50
}
```

| 字段 | 类型 | 必填 | 说明 |
| ------------------ | ---- | ---- | --------------------- |
| `productVariantId` | long | 是 | 需要补货的商品规格 ID |
| `quantity` | int | 是 | 请求补充的卡密数量 |

#### 响应

返回卡密列表,实际返回数量可与请求数量不一致。

```json
[
{
"code": "ABCD-EFGH-IJKL",
"secret": "S3cr3t-1",
"expireAt": "2026-12-31T23:59:59Z",
"capacity": 1
}
]
```

| 字段 | 类型 | 必填 | 说明 |
| ---------- | ------ | ---- | ------------------------- |
| `code` | string | 是 | 卡密(同一响应内唯一) |
| `secret` | string | 否 | 附加验证信息 |
| `expireAt` | string | 否 | 过期时间(UTC,ISO-8601) |
| `capacity` | int | 否 | 可用次数,默认为 `1` |

:::warning 注意
响应不可为空列表,否则视为补货失败。补货失败不影响当前订单的卡密发放。
:::

#### 错误码

建议使用非 `2xx` 状态码表达失败,并返回结构化错误:

```json
{
"code": "CDK_OUT_OF_STOCK",
"message": "No enough CDKs available",
"requestId": "f38db1c2-2f7a-4dc3-b5a8-2d0012345678"
}
```

推荐错误码:

| HTTP 状态码 | 错误码 | 含义 | 是否可重试 |
| ----------- | --------------------- | ---------------------- | ---------- |
| `400` | `INVALID_REQUEST` | 请求字段不合法 | 否 |
| `401` | `UNAUTHORIZED` | 鉴权失败 | 否 |
| `403` | `FORBIDDEN` | 调用被拒绝 | 否 |
| `404` | `NOT_FOUND` | 路由不存在 | 否 |
| `409` | `IDEMPOTENT_CONFLICT` | 幂等键冲突且参数不一致 | 否 |
| `422` | `CDK_OUT_OF_STOCK` | 库存不足 | 视业务而定 |
| `429` | `RATE_LIMITED` | 触发限流 | 是 |
| `500` | `INTERNAL_ERROR` | 远程服务内部错误 | 是 |
| `503` | `SERVICE_UNAVAILABLE` | 服务不可用 | 是 |
32 changes: 16 additions & 16 deletions i18n/zh-Hans/code.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"message": "页面已崩溃。",
"description": "The title of the fallback page when the page crashed"
},
"theme.BackToTopButton.buttonAriaLabel": {
"message": "回到顶部",
"description": "The ARIA label for the back to top button"
},
"theme.blog.archive.title": {
"message": "历史博文",
"description": "The page & hero title of the blog archive page"
Expand All @@ -17,10 +21,6 @@
"message": "历史博文",
"description": "The page & hero description of the blog archive page"
},
"theme.BackToTopButton.buttonAriaLabel": {
"message": "回到顶部",
"description": "The ARIA label for the back to top button"
},
"theme.blog.paginator.navAriaLabel": {
"message": "博文列表分页导航",
"description": "The ARIA label for the blog pagination"
Expand Down Expand Up @@ -144,10 +144,6 @@
"message": "标签:",
"description": "The label alongside a tag list"
},
"theme.AnnouncementBar.closeButtonAriaLabel": {
"message": "关闭",
"description": "The ARIA label for close button of announcement bar"
},
"theme.admonition.caution": {
"message": "警告",
"description": "The default label used for the Caution admonition (:::caution)"
Expand Down Expand Up @@ -176,6 +172,10 @@
"message": "最近博文导航",
"description": "The ARIA label for recent posts in the blog sidebar"
},
"theme.AnnouncementBar.closeButtonAriaLabel": {
"message": "关闭",
"description": "The ARIA label for close button of announcement bar"
},
"theme.DocSidebarItem.expandCategoryAriaLabel": {
"message": "展开侧边栏分类 '{label}'",
"description": "The ARIA label to expand the sidebar category"
Expand Down Expand Up @@ -204,6 +204,10 @@
"message": "本页总览",
"description": "The label used by the button on the collapsible TOC component"
},
"theme.navbar.mobileLanguageDropdown.label": {
"message": "选择语言",
"description": "The label for the mobile language switcher dropdown"
},
"theme.blog.post.readMore": {
"message": "阅读更多",
"description": "The label used in blog post item excerpts to link to full blog posts"
Expand All @@ -212,10 +216,6 @@
"message": "阅读 {title} 的全文",
"description": "The ARIA label for the link to full blog posts from excerpts"
},
"theme.navbar.mobileLanguageDropdown.label": {
"message": "选择语言",
"description": "The label for the mobile language switcher dropdown"
},
"theme.blog.post.readingTime.plurals": {
"message": "阅读需 {readingTime} 分钟",
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
Expand All @@ -228,6 +228,10 @@
"message": "文档侧边栏",
"description": "The ARIA label for the sidebar navigation"
},
"theme.docs.breadcrumbs.home": {
"message": "主页面",
"description": "The ARIA label for the home page in the breadcrumbs"
},
"theme.docs.sidebar.collapseButtonTitle": {
"message": "收起侧边栏",
"description": "The title attribute for collapse button of doc sidebar"
Expand All @@ -236,10 +240,6 @@
"message": "收起侧边栏",
"description": "The title attribute for collapse button of doc sidebar"
},
"theme.docs.breadcrumbs.home": {
"message": "主页面",
"description": "The ARIA label for the home page in the breadcrumbs"
},
"theme.CodeBlock.copy": {
"message": "复制",
"description": "The copy button label on code blocks"
Expand Down
1 change: 1 addition & 0 deletions sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ module.exports = {
"user-guide/shop/sales-channels",
"user-guide/shop/products",
"user-guide/shop/orders",
"user-guide/shop/virtual-delivery",
"user-guide/shop/theme-dev",
],
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading