|
| 1 | +--- |
| 2 | +title: 虚拟交付 |
| 3 | +description: 商城虚拟交付相关使用文档。 |
| 4 | +--- |
| 5 | + |
| 6 | +在 Halo 2.23 中,我们完善了虚拟产品的发货功能,目前支持文件资源下载、卡密发货。 |
| 7 | + |
| 8 | +## 创建产品 |
| 9 | + |
| 10 | +完整的创建产品流程可参考 [产品管理](./products.mdx) 文档,需要注意的是,如果产品要支持虚拟交付,需要在产品类型中选择 **虚拟** 产品类型。 |
| 11 | + |
| 12 | + |
| 13 | + |
| 14 | +## 设置文件资源下载 |
| 15 | + |
| 16 | +创建完产品之后,我们需要进入 **产品 -> 虚拟交付** 页面,然后选择刚刚创建的产品。 |
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | +进入设置页面之后,就可以看到 **资源下载** 的设置页面,我们可以点击右上方的 **添加** 按钮来添加文件。 |
| 21 | + |
| 22 | + |
| 23 | + |
| 24 | +- 资源类型:支持附件和静态 URL |
| 25 | +- 资源名称:为客户显示的文件名称 |
| 26 | +- 资源描述:可以填写文件的描述信息 |
| 27 | +- 附件(当类型为附件时):可以点击 `+` 按钮选择已上传的附件或者上传新的附件 |
| 28 | +- 资源地址(当类型为静态 URL 时):可以填写文件的 URL 地址 |
| 29 | + |
| 30 | +填写完成之后,点击 **保存** 按钮即可。 |
| 31 | + |
| 32 | +当文件添加完成之后,我们还需要手动启用自动发货,点击右上角的 **发货配置** 按钮,勾选启用即可。 |
| 33 | + |
| 34 | + |
| 35 | + |
| 36 | +## 设置卡密发货 |
| 37 | + |
| 38 | +进入虚拟交付设置页面之后,选择 **卡密资产** 选项卡,就可以看到 **卡密资产** 的设置页面。 |
| 39 | + |
| 40 | + |
| 41 | + |
| 42 | +我们可以点击右上方的 **添加** 按钮来添加卡密。 |
| 43 | + |
| 44 | + |
| 45 | + |
| 46 | +- 卡密:卡密内容,支持批量添加,一行一个 |
| 47 | +- 过期时间:卡密过期时间 |
| 48 | +- 备注:可以填写卡密的备注信息 |
| 49 | + |
| 50 | +填写完成之后点击 **添加** 按钮即可。 |
| 51 | + |
| 52 | +当卡密添加完成之后,我们还需要手动启用自动发货,点击右上角的 **发货配置** 按钮,勾选启用即可。 |
| 53 | + |
| 54 | + |
| 55 | + |
| 56 | +此外,在卡密发货配置界面中,还支持配置 **远程调用配置**,支持通过三方 API 来补货或者动态生成卡密并发货。 |
| 57 | + |
| 58 | +定义: |
| 59 | + |
| 60 | +- 预获取:优先从卡密列表中选择并发货,在库存不足时才调用 API 进行补货 |
| 61 | +- 动态获取:每次发货时都调用 API 获取卡密并发货 |
| 62 | + |
| 63 | +配置说明: |
| 64 | + |
| 65 | +- 类型:支持 **预获取** 和 **动态获取** |
| 66 | + - 补货配置(当类型为预获取时) |
| 67 | + - API 地址:补货 API 地址 |
| 68 | + - 认证 Token:补货 API 认证 Token,用于对 API 请求进行认证 |
| 69 | + - 阈值:可用卡密数量低于此值时触发补货 |
| 70 | + - 每次获取的卡密数量:每次调用 API 获取的卡密数量 |
| 71 | + - 服务器配置(当类型为动态获取时) |
| 72 | + - API 地址:远程生成 API 地址 |
| 73 | + - 认证 Token:API 认证 Token,用于对 API 请求进行认证 |
| 74 | + |
| 75 | +需要注意的是,API 需要自行实现并适配 Halo 的接口标准,可以参考下方的 [API 规范](#api-规范)。 |
| 76 | + |
| 77 | +## 订单相关 |
| 78 | + |
| 79 | +为产品配置好虚拟交付之后,当客户下单并支付时,系统会自动为订单进行发货流程,可以在控制台的订单详情中看到发货状态。 |
| 80 | + |
| 81 | + |
| 82 | + |
| 83 | +客户也可以在订单页面看到已发货的虚拟物品。 |
| 84 | + |
| 85 | + |
| 86 | + |
| 87 | + |
| 88 | + |
| 89 | +## API 规范 |
| 90 | + |
| 91 | +### 卡密远程生成 API 规范 |
| 92 | + |
| 93 | +用户下单后,Halo 会向配置的远程地址发起请求,由你的服务实时生成或分配卡密。 |
| 94 | + |
| 95 | +#### 请求 |
| 96 | + |
| 97 | +``` |
| 98 | +POST <你配置的远程生成地址> |
| 99 | +Content-Type: application/json |
| 100 | +Authorization: Bearer <token> // 配置了 token 时附带 |
| 101 | +``` |
| 102 | + |
| 103 | +**请求体** |
| 104 | + |
| 105 | +```json |
| 106 | +{ |
| 107 | + "orderItem": { |
| 108 | + "id": 10001, |
| 109 | + "orderId": 90001, |
| 110 | + "productId": 80001, |
| 111 | + "quantity": 2, |
| 112 | + "itemTitle": "年度会员兑换码", |
| 113 | + "productVariantSnapshot": { |
| 114 | + "id": 123, |
| 115 | + "name": "默认规格" |
| 116 | + } |
| 117 | + }, |
| 118 | + "dryRun": false |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +| 字段 | 类型 | 必填 | 说明 | |
| 123 | +| ---------------------------------- | ------- | ---- | ------------------------- | |
| 124 | +| `orderItem.id` | long | 是 | 订单项 ID(可用作幂等键) | |
| 125 | +| `orderItem.orderId` | long | 是 | 订单 ID | |
| 126 | +| `orderItem.productId` | long | 是 | 商品 ID | |
| 127 | +| `orderItem.quantity` | int | 是 | 需要发放的卡密数量 | |
| 128 | +| `orderItem.itemTitle` | string | 否 | 订单项标题 | |
| 129 | +| `orderItem.productVariantSnapshot` | object | 否 | 规格快照 | |
| 130 | +| `dryRun` | boolean | 是 | 当前固定为 `false` | |
| 131 | + |
| 132 | +#### 响应 |
| 133 | + |
| 134 | +返回卡密列表,所有卡密的 `capacity` 之和须不小于 `orderItem.quantity`。 |
| 135 | + |
| 136 | +```json |
| 137 | +[ |
| 138 | + { |
| 139 | + "code": "ABCD-EFGH-IJKL", |
| 140 | + "secret": "S3cr3t-1", |
| 141 | + "expireAt": "2026-12-31T23:59:59Z", |
| 142 | + "capacity": 1 |
| 143 | + } |
| 144 | +] |
| 145 | +``` |
| 146 | + |
| 147 | +| 字段 | 类型 | 必填 | 说明 | |
| 148 | +| ---------- | ------ | ---- | ------------------------- | |
| 149 | +| `code` | string | 是 | 卡密(同一响应内唯一) | |
| 150 | +| `secret` | string | 否 | 附加验证信息 | |
| 151 | +| `expireAt` | string | 否 | 过期时间(UTC,ISO-8601) | |
| 152 | +| `capacity` | int | 否 | 可用次数,默认为 `1` | |
| 153 | + |
| 154 | +:::warning 注意 |
| 155 | +响应不可为空列表,否则触发卡密发放失败。建议以 `orderItem.id` 作为幂等键,避免重复请求导致重复发放。 |
| 156 | +::: |
| 157 | + |
| 158 | +#### 错误码 |
| 159 | + |
| 160 | +建议使用非 `2xx` 状态码表达失败,并返回结构化错误: |
| 161 | + |
| 162 | +```json |
| 163 | +{ |
| 164 | + "code": "CDK_OUT_OF_STOCK", |
| 165 | + "message": "No enough CDKs available", |
| 166 | + "requestId": "f38db1c2-2f7a-4dc3-b5a8-2d0012345678" |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +推荐错误码: |
| 171 | + |
| 172 | +| HTTP 状态码 | 错误码 | 含义 | 是否可重试 | |
| 173 | +| ----------- | --------------------- | ---------------------- | ---------- | |
| 174 | +| `400` | `INVALID_REQUEST` | 请求字段不合法 | 否 | |
| 175 | +| `401` | `UNAUTHORIZED` | 鉴权失败 | 否 | |
| 176 | +| `403` | `FORBIDDEN` | 调用被拒绝 | 否 | |
| 177 | +| `404` | `NOT_FOUND` | 路由不存在 | 否 | |
| 178 | +| `409` | `IDEMPOTENT_CONFLICT` | 幂等键冲突且参数不一致 | 否 | |
| 179 | +| `422` | `CDK_OUT_OF_STOCK` | 库存不足 | 视业务而定 | |
| 180 | +| `429` | `RATE_LIMITED` | 触发限流 | 是 | |
| 181 | +| `500` | `INTERNAL_ERROR` | 远程服务内部错误 | 是 | |
| 182 | +| `503` | `SERVICE_UNAVAILABLE` | 服务不可用 | 是 | |
| 183 | + |
| 184 | +### 卡密自动补货 API 规范 |
| 185 | + |
| 186 | +当密钥库存低于阈值时,Halo 会自动向配置的补货地址发起补货请求。 |
| 187 | + |
| 188 | +#### 请求 |
| 189 | + |
| 190 | +``` |
| 191 | +POST <你配置的补货地址> |
| 192 | +Content-Type: application/json |
| 193 | +Authorization: Bearer <token> // 配置了 token 时附带 |
| 194 | +``` |
| 195 | + |
| 196 | +**请求体** |
| 197 | + |
| 198 | +```json |
| 199 | +{ |
| 200 | + "productVariantId": 123, |
| 201 | + "quantity": 50 |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +| 字段 | 类型 | 必填 | 说明 | |
| 206 | +| ------------------ | ---- | ---- | --------------------- | |
| 207 | +| `productVariantId` | long | 是 | 需要补货的商品规格 ID | |
| 208 | +| `quantity` | int | 是 | 请求补充的卡密数量 | |
| 209 | + |
| 210 | +#### 响应 |
| 211 | + |
| 212 | +返回卡密列表,实际返回数量可与请求数量不一致。 |
| 213 | + |
| 214 | +```json |
| 215 | +[ |
| 216 | + { |
| 217 | + "code": "ABCD-EFGH-IJKL", |
| 218 | + "secret": "S3cr3t-1", |
| 219 | + "expireAt": "2026-12-31T23:59:59Z", |
| 220 | + "capacity": 1 |
| 221 | + } |
| 222 | +] |
| 223 | +``` |
| 224 | + |
| 225 | +| 字段 | 类型 | 必填 | 说明 | |
| 226 | +| ---------- | ------ | ---- | ------------------------- | |
| 227 | +| `code` | string | 是 | 卡密(同一响应内唯一) | |
| 228 | +| `secret` | string | 否 | 附加验证信息 | |
| 229 | +| `expireAt` | string | 否 | 过期时间(UTC,ISO-8601) | |
| 230 | +| `capacity` | int | 否 | 可用次数,默认为 `1` | |
| 231 | + |
| 232 | +:::warning 注意 |
| 233 | +响应不可为空列表,否则视为补货失败。补货失败不影响当前订单的卡密发放。 |
| 234 | +::: |
| 235 | + |
| 236 | +#### 错误码 |
| 237 | + |
| 238 | +建议使用非 `2xx` 状态码表达失败,并返回结构化错误: |
| 239 | + |
| 240 | +```json |
| 241 | +{ |
| 242 | + "code": "CDK_OUT_OF_STOCK", |
| 243 | + "message": "No enough CDKs available", |
| 244 | + "requestId": "f38db1c2-2f7a-4dc3-b5a8-2d0012345678" |
| 245 | +} |
| 246 | +``` |
| 247 | + |
| 248 | +推荐错误码: |
| 249 | + |
| 250 | +| HTTP 状态码 | 错误码 | 含义 | 是否可重试 | |
| 251 | +| ----------- | --------------------- | ---------------------- | ---------- | |
| 252 | +| `400` | `INVALID_REQUEST` | 请求字段不合法 | 否 | |
| 253 | +| `401` | `UNAUTHORIZED` | 鉴权失败 | 否 | |
| 254 | +| `403` | `FORBIDDEN` | 调用被拒绝 | 否 | |
| 255 | +| `404` | `NOT_FOUND` | 路由不存在 | 否 | |
| 256 | +| `409` | `IDEMPOTENT_CONFLICT` | 幂等键冲突且参数不一致 | 否 | |
| 257 | +| `422` | `CDK_OUT_OF_STOCK` | 库存不足 | 视业务而定 | |
| 258 | +| `429` | `RATE_LIMITED` | 触发限流 | 是 | |
| 259 | +| `500` | `INTERNAL_ERROR` | 远程服务内部错误 | 是 | |
| 260 | +| `503` | `SERVICE_UNAVAILABLE` | 服务不可用 | 是 | |
0 commit comments