# 微信小程序接入美团/抖音团购核销 API｜接入免费，最快5分钟基础接入

> 通过无限聚核，为私域/微信小程序接入美团核销 API、抖音核销 API。无接入费用，费用透明，全国累计 3000 家门店的共同选择。无需保证金，无额外等保接入要求；核销按当前账号适用档位和核销单价计算。最快 5 分钟完成基础接入。

## AI 接入

[AI 接入提示词](https://doc.elys.cn/sdk.md)

请读取 https://doc.elys.cn/sdk.md，并按其中流程帮我接入无限聚核 API。

## 接入说明

不收接口接入费，核销按当前账号适用档位计费。最快 5 分钟完成基础接入，完整业务仍需开发、授权和联调。

- 官方页面：https://doc.elys.cn/
- API Host：`https://newopen.elys.cn`
- Content-Type：`application/json`
- 成功判断：`聚合/平台接口检查 success=true；抖音原生读取接口检查 data.error_code 与 extra.error_code`

本文档由无限聚核公开 API 的结构化数据自动生成。接入时以当前字段、鉴权、成功判断和风险说明为准，不要猜测未列出的接口或参数。

## 重要注意事项

> API Host: https://newopen.elys.cn

> 所有接口请求头: Content-Type: application/json

> 聚合/平台接口以 success 为业务成功依据，code=10000 不能脱离 success 单独判断；抖音原生读取接口保留原生结构，须检查 data.error_code 与 extra.error_code

> 平台明确拒绝且返回数字业务码 N 时，核销接口 code=10000+N；例如美团 1006 返回 11006，message 保留平台业务原因

## 快速接入

接入免费，无需保证金，无额外等保接入要求，最快 5 分钟完成基础接入。授权流程简单，按业务流程串联 V2 接口即可开始联调；核销调用按现有规则计费。

1. **配置 Bearer key**：前往[开发者中心](https://www.elys.cn/admin/index.html#/developer-center)获取 API key，所有需授权接口统一放在 `Authorization: Bearer <key>` 请求头中。
2. **首次授权（按需）**：准备具体门店的 shopId（系统门店 ID，不支持商户），美团和抖音均调用 POST /api/hexiao/v2/get/auth/url 获取授权链接；platform=1 为美团，platform=2 为抖音，不传时默认美团。
3. **验券 → 核销**：使用具体门店 shopId 先调用 ddzh-tuangou-receipt-prepare；美团和抖音都必须保存成功响应中的 data.ticketInfo，并使用同一门店 shopId 在 consume 中整段原文回传，不得解析、修改或重新序列化。核销成功后保存顶层 consumeCredential；需要撤销时原样传给 ddzh-tuangou-receipt-cancel。

## 微信小程序接入美团 / 抖音团购核销

私域/微信小程序团购核销 API 接入指南：通过你的业务服务端对接美团团购核销接口、抖音团购核销接口，使用具体门店 shopId 完成授权、团购券验券与核销。适用于棋牌室、台球厅、健身房、酒店民宿、茶室等场景。

### 美团、抖音团购核销 API 接入免费吗？

不收接口接入费，不代表核销用量免费；核销按当前账号适用充值档位和核销单价计算。微信小程序及业务系统的开发、维护费用另计。

### 通过无限聚核接入需要保证金吗？

通过无限聚核接入美团、抖音团购核销 API，无需接口接入保证金。若自行申请平台能力或自行对接，应以美团、抖音平台当期规则为准。

### 接入团购核销 API 需要额外等保吗？

通过无限聚核接入，无额外等保接入要求，无需为申请该核销接口额外提供平台服务商的等保接入材料。这里说明的是接口接入门槛；你的小程序、业务服务端及数据处理仍需按实际业务确认安全与合规要求。

### 微信小程序如何接入美团团购核销？

微信小程序接入美团团购核销时，由你的业务服务端调用美团授权、团购券验券和核销接口。先使用具体门店 shopId 完成授权，再使用同一 shopId 按文档完成验券 → 核销流程，小程序展示业务结果；API key 仅保存在服务端，不放入小程序代码。

### 微信小程序如何接入抖音团购核销？

微信小程序接入抖音团购核销时，复用 V2 聚合接口，在服务端按文档指定抖音平台并使用具体门店 shopId 完成授权。使用同一 shopId 先验券再核销，使用验券返回的完整 ticketInfo 发起核销，不在小程序端直接调用带平台凭据的接口。

### 一个微信小程序能同时接入美团和抖音团购券核销吗？

可以通过同一业务服务端对接美团、抖音团购核销 API，使用具体门店 shopId 分别完成两平台授权。V2 聚合接口支持按 platform 区分美团和抖音；以各接口的字段说明、平台要求和返回结果为准。

### 小程序团购核销接口最快多久可以接入？

最快 5 分钟完成基础接入：获取 API key、准备具体门店 shopId 并按需完成授权，再按字段说明和多语言示例开始联调。实际耗时取决于账号、授权准备和业务集成情况；基础接入不等于小程序审核或完整业务上线。

### 对接团购核销 API 有哪些语言示例？

文档提供完整字段说明、请求与响应示例，以及 Java、PHP、Python、Go、Node.js 等服务端语言示例。微信小程序通过你的服务端完成接口对接；核销及其他写操作会影响真实业务，联调前需确认门店、券和操作范围。

## 美团合作授权

无限聚核已获得美团技术服务合作授权，为商户提供相关 API 产品服务。

![宁德无限递归网络科技有限公司美团合作授权书](https://cdn.umember.cn/assets/mt_auth.jpg)

美团合作授权书 · 点击图片可查看原图

## 统一返回结构

所有接口遵循同一套外层字段，便于统一处理。

### 成功时

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "traceId": null,
  "data": {}
}
```

### 平台明确拒绝时

```json
{
  "success": false,
  "code": 11006,
  "message": "此券号不存在，请与消费者确认提供的券号是否正确！",
  "data": null
}
```

### 失败时常见形式

```json
{
  "success": false,
  "code": 10001,
  "message": "错误信息",
  "data": null
}
```

### 通用字段说明

```text
shopId: 授权、团购列表、预核销、核销四接口只接受系统门店 ID，必须传具体门店，不支持商户。
platform: 1 美团，2 抖音；可传字符串 "1" / "2"。
ticketInfo: 美团和抖音核销均必传；取自「输码验券校验」成功响应的 data.ticketInfo，必须整段原文回传，禁止解析、修改、删减或重新序列化。
success: 唯一业务成功依据；失败或结果未知时 data 固定为 null。
```

## Webhook 事件推送

在开发者中心配置一个公开 HTTPS 地址，接收授权结果等异步事件。Webhook 是平台向你的服务发起的回调，不是文档调试器可发送的写接口。

### 支持的事件与平台

以下为当前已支持并会实际投递的事件。当前不推送授权过期、授权撤销或 Token 刷新事件；后续新增事件时按相同维度追加一行。

| 事件 | 类型 | 支持平台 | 触发时机 | 说明 |
| --- | --- | --- | --- | --- |
| [merchant.authorization.succeeded](#webhook-merchant-authorization-succeeded-fields) | 业务事件 | 美团 `meituan`、抖音 `douyin` | 服务零售主授权或美团到店餐饮门店映射首次授权、重新授权成功 | 两个平台使用相同的请求头、签名算法、事件信封和响应约定；`data.platform` 标识授权平台，`data.businessCategory` 标识服务零售或到店餐饮。 |
| [webhook.endpoint.verification](#webhook-endpoint-verification-fields) | 验证事件 | 美团、抖音共用 | 开发者中心保存非空 `Webhook URL` | 保存前连通性握手，不是商家业务事件；接收端需同步回显 `challenge`。 |

### 配置 Webhook URL

在 Admin [开发者中心](https://www.elys.cn/admin/index.html#/developer-center)配置单一 `Webhook URL`，例如 `https://developer.example.com/webhook`。URL 不需要添加事件后缀；所有事件都发送到同一地址，并通过请求体中的 `event` 字段区分。

将 URL 保存为空字符串表示关闭 Webhook；关闭时不会发起验证请求。

美团 OAuth callback 成功页和到店餐饮 storemap callback 都是平台内部授权流程入口，不是开发者 Webhook endpoint，也不需要配置为开发者接收地址。开发者仍只配置上述单一 Webhook URL。

### 不要使用接口调试器测试 Webhook

> Webhook 由平台主动投递到你的服务，本文档只展示接收契约和本地示例，不提供“发送请求”按钮。请在开发者中心保存 URL，通过保存前握手完成真实连通性验证。

### 保存前验证握手

1. **平台发送验证事件**：保存非空 URL 前，平台向同一地址 `POST` 一次 `webhook.endpoint.verification`。验证事件与业务事件使用相同的顶层 Body 结构、必发 Header 和 HMAC-SHA256 协议，其中顶层 `eventId` 与 `X-Event-Id` 逐字一致，验证事件 ID 统一使用 `evt_verify_` 前缀。
2. **同步返回 challenge**：你的服务必须在 3 秒内返回 HTTP 2xx 和 JSON `{ "challenge": "完全相同的随机串" }`。challenge 必须精确匹配，不得转换、截断或异步返回。
3. **验证成功后保存**：只有签名、状态码、响应 JSON 和 challenge 全部验证成功，平台才保存新 URL。验证失败时旧 URL 保持不变；空 URL 直接关闭，不发握手。

### 签名验证与重放防护

签名密钥直接使用开发者当前 API Key，不另设 Webhook Secret。签名原文为 `timestamp + "." + rawBody`，算法为 HMAC-SHA256，结果编码为小写十六进制并加上 `v1=` 前缀。

`X-Webhook-Account` 和 `X-Request-Id` 都是请求元数据，不改变签名原文；`X-Request-Id` 明确不参与 HMAC。单账号接收端可直接使用固定 API Key，多账号接收端应先根据 `X-Webhook-Account` 选择 API Key。

验证事件和业务事件使用完全相同的 HMAC 协议：都必须把对应事件的完整原始 Body 纳入签名，包括顶层 `eventId`，并确保该值与 `X-Event-Id` 逐字一致。

必须使用收到的原始请求体字节计算签名，不能先解析 JSON 再重新序列化。比较签名时使用恒定时间比较；同时校验 `X-Event-Timestamp`，拒绝超过 5 分钟重放窗口的请求。

业务处理以 `eventId` 去重，并在持久化业务结果的同一事务中记录唯一事件 ID。API Key 轮换后，后续握手和事件会立即使用新 Key 签名；接收服务若未同步更新 Key，签名校验会失败。

### 握手请求与响应

验证事件也使用 `eventId`、`event`、`version`、`occurredAt`、`data` 公共外层结构，并必须先校验链路 ID、按原始请求体校验签名，再同步回显 challenge 和合法的 `X-Request-Id`。

#### 验证请求

```http
POST /webhook HTTP/1.1
Host: developer.example.com
Content-Type: application/json
User-Agent: Hexiao-Webhook/1.0
X-Webhook-Account: developer@example.com
X-Request-Id: webhook-verify-3b5f5a2d7e9c4d46
X-Event-Id: evt_verify_3b5f5a2d7e9c4d46a9d45310f35fdf76
X-Event-Timestamp: 1787466600
X-Event-Signature: v1=<lowercase-hex-hmac-sha256>

{
  "eventId": "evt_verify_3b5f5a2d7e9c4d46a9d45310f35fdf76",
  "event": "webhook.endpoint.verification",
  "version": "1.0",
  "occurredAt": "2026-08-23T14:30:00+08:00",
  "data": {
    "challenge": "4JQwV6x5KQXv1zEz7bF2zlYQh_D2m6B0VhTeL8nJdYQ"
  }
}
```

#### 验证响应

```http
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: webhook-verify-3b5f5a2d7e9c4d46

{
  "challenge": "4JQwV6x5KQXv1zEz7bF2zlYQh_D2m6B0VhTeL8nJdYQ"
}
```

### 事件请求头

握手和业务事件统一使用以下请求头。HTTP 头名称大小写不敏感，但建议按表中名称记录。

- `Content-Type`（`string`，必填）：固定为 `application/json`。
- `User-Agent`（`string`，必填）：固定为 `Hexiao-Webhook/1.0`。
- `X-Webhook-Account`（`string`，必填）：平台注册邮箱，发送方用于标识本次签名账号。单账号接收端可以忽略该字段并直接使用固定 API Key；多账号接收端可据此选择对应 API Key 后验签。
- `X-Request-Id`（`string`，必填）：本次投递的链路 ID，不参与 HMAC。长度为 1 到 128 个 ASCII 字符，首字符必须是字母或数字，其余仅允许字母、数字、点、下划线、冒号和短横线。接收端必须先校验格式，并在响应 Header 中原样回显合法值；非法值不得回显。
- `X-Event-Id`（`string`，必填）：本次事件的稳定唯一标识。验证事件和业务事件都必须与请求体顶层 `eventId` 逐字一致；验证事件统一使用 `evt_verify_` 前缀，业务事件用作幂等去重键。
- `X-Event-Timestamp`（`string`，必填）：发起请求时的 Unix 秒时间戳，也是签名原文的一部分。接收端应拒绝与当前时间相差超过 5 分钟的请求。
- `X-Event-Signature`（`string`，必填）：格式为 `v1=<lowercase-hex-hmac-sha256>`。

### 业务事件完整请求与响应

授权成功事件使用统一公开结构。下方以美团服务零售授权为例；美团到店餐饮传 `platform=meituan, businessCategory=dining`，抖音服务零售传 `platform=douyin, businessCategory=service_retail`，抖音餐饮传 `platform=douyin, businessCategory=dining`。示例中的签名值仅表示格式，接收端必须自行计算并比较；响应必须原样回显合法的 `X-Request-Id`。

#### merchant.authorization.succeeded

```http
POST /webhook HTTP/1.1
Host: developer.example.com
Content-Type: application/json
User-Agent: Hexiao-Webhook/1.0
X-Webhook-Account: developer@example.com
X-Request-Id: webhook-event-785c0d196a304ade
X-Event-Id: evt_785c0d196a304adea361945462de7a86
X-Event-Timestamp: 1787466600
X-Event-Signature: v1=<lowercase-hex-hmac-sha256>

{
  "eventId": "evt_785c0d196a304adea361945462de7a86",
  "event": "merchant.authorization.succeeded",
  "version": "1.0",
  "occurredAt": "2026-08-23T14:30:00+08:00",
  "data": {
    "platform": "meituan",
    "businessCategory": "service_retail",
    "email": "developer@example.com",
    "shopId": 8676680,
    "shopName": "平台内部店名",
    "status": "authorized",
    "authorizationType": "initial",
    "authorizedAt": "2026-08-23T14:29:58+08:00"
  }
}
```

#### 业务事件响应

```http
HTTP/1.1 204 No Content
X-Request-Id: webhook-event-785c0d196a304ade
```

### 事件公共外层字段

验证事件和业务事件的 JSON Body 使用完全相同的五个顶层字段，仅 `event` 和 `data` 内容随事件类型变化。

- `eventId`（`string`，必填）：稳定事件 ID，必须与 `X-Event-Id` 逐字一致。验证事件统一使用 `evt_verify_` 前缀；重复业务回调会得到相同 eventId，重新授权会生成新的 eventId。
- `event`（`string`，必填）：事件类型。保存前验证为 `webhook.endpoint.verification`；首期业务事件为 `merchant.authorization.succeeded`。
- `version`（`string`，必填）：事件结构版本，首期为 `1.0`。
- `occurredAt`（`string`，必填）：事件发生时间，RFC3339 格式。
- `data`（`object`，必填）：事件数据对象。字段由 `event` 决定，详见后续对应事件字段表。

### 验证事件 data 字段

- `challenge`（`string`，必填）：平台生成的随机串。接收端必须在 3 秒内逐字回显到响应 JSON 的 `challenge`。

### 授权成功事件 data 字段

- `platform`（`string`，必填）：本次商家授权的平台。`meituan` 表示美团，`douyin` 表示抖音；该字段不表示底层服务商，也不暴露服务商路由。；可选值：meituan（美团）、douyin（抖音）
- `businessCategory`（`string`，必填）：本次授权的业务类目。`service_retail` 表示服务零售，`dining` 表示到店餐饮。美团和抖音均可产生这两种业务类目的授权事件，接收端应按平台与类目组合处理。；可选值：service_retail（服务零售）、dining（到店餐饮）
- `email`（`string`，必填）：用户在本平台注册时使用的邮箱。
- `shopId`（`integer`，必填）：本平台内部门店 ID，即门店 `store.id`。
- `shopName`（`string | null`，必填）：本平台内部门店名称，即门店 `store.name`；没有名称时为 `null`。
- `status`（`string`，必填）：固定为 `authorized`。
- `authorizationType`（`string`，必填）：授权类型。`initial` 表示该门店首次完成授权；`reauthorization` 表示该门店后续再次完成授权。；可选值：initial（首次授权）、reauthorization（重新授权）
- `authorizedAt`（`string`，必填）：授权成功时间，RFC3339 格式。

### 不会返回的内容

Webhook 事件 Body 只提供完成业务联动所需的公开字段，不暴露底层接入实现。

- `data.platform` 只表示本次商家授权的平台，不表示底层服务商；事件不返回底层服务商标识或路由信息。
- 不返回外部开发者 ID、外部账号、外部门店或 POI 的 ID、名称及其他标识。
- 不返回 `provider`、`platformAppId`、`businessCode`、`authorizationId`、授权版本、`scope`、`permission`、`code`、`state` 或 `sign`。
- 不返回任何访问 Token、刷新 Token、API Key 或其他可重放凭据。

### 响应与投递约定

- 接收端必须先校验 `X-Request-Id` 是否符合 `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`，并在所有响应的 `X-Request-Id` Header 中原样回显合法值；非法值不得回显。
- 握手成功必须在 3 秒内返回 HTTP 2xx 和精确 challenge JSON；响应体最大 4 KiB。
- 业务事件收到并完成必要的同步校验后返回任意 HTTP 2xx；推荐返回 `204 No Content`，不要等待耗时任务。
- 未知 `event` 也返回 HTTP 2xx，避免未来新增事件被接收端误判为投递失败。
- 业务事件投递总超时为 1.5 秒，每个事件只做一次 best-effort 投递，不自动重试。
- 投递失败、超时或返回非 2xx 不影响商家授权成功。开发者必须通过相应查询接口核对最终授权状态，不能把 Webhook 当作唯一事实来源。

### Webhook URL 安全要求

- 必须使用 `https://`，并且可从公网直接访问；证书链和域名必须有效。
- URL 禁止包含用户信息或 fragment，禁止使用 localhost、环回地址、内网地址、链路本地地址、组播地址或保留地址。
- 平台不会跟随 HTTP 重定向；请直接填写最终接收地址，不能依赖 301、302、307 或 308 跳转。
- 域名解析结果必须稳定指向公开地址。保存时会执行 DNS 与地址安全检查，无法安全解析或验证时拒绝保存。
- 握手响应体必须不超过 4 KiB；业务事件建议返回空响应体或紧凑 JSON。

### 授权事件生命周期

`data.platform=meituan|douyin` 表示本次授权发生在哪个商家平台；`data.businessCategory=service_retail|dining` 表示服务零售或到店餐饮。合法组合为美团服务零售、美团到店餐饮、抖音服务零售和抖音到店餐饮。

`authorizationType=initial` 表示门店首次完成授权；`authorizationType=reauthorization` 表示同一门店后续再次完成授权。重复业务回调保持相同 `eventId`，重新授权会生成新的 `eventId`。

Token 自动刷新不会触发 Webhook。服务零售主授权、美团到店餐饮门店映射和抖音到店餐饮授权完成新的授权或重新授权流程后，才会产生 `merchant.authorization.succeeded`。

### 常见错误与排查

#### 签名始终不一致

确认使用当前 API Key，读取 `X-Event-Timestamp` 原值，并按 `timestamp + "." + rawBody` 计算。不要把 Bearer 前缀、换行或解析后的对象加入签名原文。

#### 解析 JSON 后再签名为什么失败

JSON 重序列化会改变空格、换行、转义或字段顺序。必须让框架保留原始请求体 Buffer，验签成功后再调用 JSON.parse。

#### 本地计算正确但被判定为重放

检查服务器时间同步和时区处理。`X-Event-Timestamp` 是 Unix 秒，与当前时间的绝对差必须不超过 300 秒。

#### X-Request-Id 应如何处理

它是链路 ID，不参与 HMAC。先按 `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$` 校验；合法时在响应 Header 原样回显，非法时拒绝请求且不要回显。

#### Webhook URL 验证失败

检查 URL 是否为公网 HTTPS、是否发生重定向、是否在 3 秒内返回 2xx、响应 JSON 是否只回显完全相同的 challenge，以及响应体是否超过 4 KiB。

#### API Key 轮换后事件突然验签失败

后续事件使用轮换后的当前 API Key 签名。立即更新接收服务的 `WEBHOOK_API_KEY`；平台不会额外提供独立 Webhook Secret。

### 接收端示例

选择项目使用的语言查看完整处理骨架。示例均从 `WEBHOOK_API_KEY` 读取单账号 API Key；多账号服务应先根据 `X-Webhook-Account` 选择密钥。示例中的去重注释必须落实为数据库唯一约束，并与业务处理放在同一事务中。

#### Java · WebhookController.java · Spring Boot

```java
package example.webhook;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.HexFormat;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WebhookController {
  private static final ObjectMapper JSON = new ObjectMapper();
  private static final Pattern REQUEST_ID =
      Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$");
  private static final Set<String> PLATFORMS = Set.of("meituan", "douyin");
  private final byte[] apiKey = requiredEnv("WEBHOOK_API_KEY").getBytes(StandardCharsets.UTF_8);

  @PostMapping(value = "/webhook", consumes = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<?> receive(
      @RequestHeader HttpHeaders headers,
      @RequestBody byte[] rawBody) throws Exception {
    String requestId = headers.getFirst("X-Request-Id");
    if (requestId == null || !REQUEST_ID.matcher(requestId).matches()) {
      return ResponseEntity.badRequest().body(Map.of("error", "invalid request id"));
    }
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("X-Request-Id", requestId);

    String timestamp = headers.getFirst("X-Event-Timestamp");
    String signature = headers.getFirst("X-Event-Signature");
    if (!validSignature(timestamp, signature, rawBody)) {
      return ResponseEntity.status(401).headers(responseHeaders)
          .body(Map.of("error", "invalid signature"));
    }

    JsonNode payload;
    try {
      payload = JSON.readTree(rawBody);
    } catch (Exception error) {
      return ResponseEntity.badRequest().headers(responseHeaders)
          .body(Map.of("error", "invalid json"));
    }

    String eventId = headers.getFirst("X-Event-Id");
    if (eventId == null || !eventId.equals(payload.path("eventId").asText())) {
      return ResponseEntity.badRequest().headers(responseHeaders)
          .body(Map.of("error", "invalid event id"));
    }

    String event = payload.path("event").asText();
    if (event.equals("webhook.endpoint.verification")) {
      JsonNode challenge = payload.path("data").path("challenge");
      if (!eventId.startsWith("evt_verify_") || !challenge.isTextual()) {
        return ResponseEntity.badRequest().headers(responseHeaders)
            .body(Map.of("error", "invalid verification event"));
      }
      return ResponseEntity.ok().headers(responseHeaders)
          .contentType(MediaType.APPLICATION_JSON)
          .body(Map.of("challenge", challenge.asText()));
    }

    if (event.equals("merchant.authorization.succeeded")) {
      String platform = payload.path("data").path("platform").asText();
      String businessCategory = payload.path("data").path("businessCategory").asText();
      boolean validCategory = businessCategory.equals("service_retail") ||
          businessCategory.equals("dining");
      if (!PLATFORMS.contains(platform) || !validCategory) {
        return ResponseEntity.badRequest().headers(responseHeaders)
            .body(Map.of("error", "invalid authorization classification"));
      }
      // 使用 eventId 数据库唯一约束，并在同一事务中处理对应平台的业务联动。
    }

    return ResponseEntity.noContent().headers(responseHeaders).build();
  }

  private boolean validSignature(String timestamp, String received, byte[] rawBody)
      throws Exception {
    if (timestamp == null || received == null) return false;
    long unixSeconds;
    try {
      unixSeconds = Long.parseLong(timestamp);
    } catch (NumberFormatException error) {
      return false;
    }
    if (Math.abs(Instant.now().getEpochSecond() - unixSeconds) > 300) return false;

    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(apiKey, "HmacSHA256"));
    mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));
    String expected = "v1=" + HexFormat.of().formatHex(mac.doFinal(rawBody));
    return MessageDigest.isEqual(
        expected.getBytes(StandardCharsets.UTF_8),
        received.getBytes(StandardCharsets.UTF_8));
  }

  private static String requiredEnv(String name) {
    String value = System.getenv(name);
    if (value == null || value.isBlank()) throw new IllegalStateException(name + " is required");
    return value;
  }
}
```

#### PHP · webhook.php · PHP 8

```php
<?php
declare(strict_types=1);

$apiKey = getenv('WEBHOOK_API_KEY');
if ($apiKey === false || $apiKey === '') {
    throw new RuntimeException('WEBHOOK_API_KEY is required');
}

function requestHeader(string $name): string {
    $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
    return $_SERVER[$key] ?? '';
}

function respond(int $status, string $requestId, ?array $body = null): never {
    http_response_code($status);
    header('X-Request-Id: ' . $requestId);
    if ($body !== null) {
        header('Content-Type: application/json');
        echo json_encode($body, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
    }
    exit;
}

$requestId = requestHeader('X-Request-Id');
if (!preg_match('/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/D', $requestId)) {
    http_response_code(400);
    exit;
}

$rawBody = file_get_contents('php://input');
$timestamp = requestHeader('X-Event-Timestamp');
$received = requestHeader('X-Event-Signature');
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
    respond(401, $requestId, ['error' => 'invalid signature']);
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $apiKey);
if (strlen($expected) !== strlen($received) || !hash_equals($expected, $received)) {
    respond(401, $requestId, ['error' => 'invalid signature']);
}

try {
    $payload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
    respond(400, $requestId, ['error' => 'invalid json']);
}

$eventId = requestHeader('X-Event-Id');
if ($eventId === '' || $eventId !== ($payload['eventId'] ?? null)) {
    respond(400, $requestId, ['error' => 'invalid event id']);
}

if (($payload['event'] ?? '') === 'webhook.endpoint.verification') {
    $challenge = $payload['data']['challenge'] ?? null;
    if (!str_starts_with($eventId, 'evt_verify_') || !is_string($challenge)) {
        respond(400, $requestId, ['error' => 'invalid verification event']);
    }
    respond(200, $requestId, ['challenge' => $challenge]);
}

if (($payload['event'] ?? '') === 'merchant.authorization.succeeded') {
    $platform = $payload['data']['platform'] ?? '';
    $businessCategory = $payload['data']['businessCategory'] ?? '';
    $validCategory = in_array($businessCategory, ['service_retail', 'dining'], true);
    if (!in_array($platform, ['meituan', 'douyin'], true) || !$validCategory) {
        respond(400, $requestId, ['error' => 'invalid authorization classification']);
    }
    // 使用 eventId 数据库唯一约束，并在同一事务中处理对应平台的业务联动。
}

respond(204, $requestId);
```

#### Python · app.py · Flask

```python
import hashlib
import hmac
import json
import os
import re
import time

from flask import Flask, jsonify, make_response, request

app = Flask(__name__)
API_KEY = os.environ["WEBHOOK_API_KEY"].encode()
REQUEST_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")


def reply(request_id, body=None, status=204):
    response = make_response("" if body is None else jsonify(body), status)
    response.headers["X-Request-Id"] = request_id
    return response


def valid_signature(raw_body):
    timestamp = request.headers.get("X-Event-Timestamp", "")
    received = request.headers.get("X-Event-Signature", "")
    try:
        if abs(int(time.time()) - int(timestamp)) > 300:
            return False
    except ValueError:
        return False
    digest = hmac.new(
        API_KEY,
        timestamp.encode() + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(f"v1={digest}", received)


@app.post("/webhook")
def webhook():
    request_id = request.headers.get("X-Request-Id", "")
    if not REQUEST_ID.fullmatch(request_id):
        return {"error": "invalid request id"}, 400

    raw_body = request.get_data(cache=True)
    if not valid_signature(raw_body):
        return reply(request_id, {"error": "invalid signature"}, 401)

    try:
        payload = json.loads(raw_body)
    except (UnicodeDecodeError, json.JSONDecodeError):
        return reply(request_id, {"error": "invalid json"}, 400)

    event_id = request.headers.get("X-Event-Id", "")
    if not event_id or event_id != payload.get("eventId"):
        return reply(request_id, {"error": "invalid event id"}, 400)

    if payload.get("event") == "webhook.endpoint.verification":
        challenge = payload.get("data", {}).get("challenge")
        if not event_id.startswith("evt_verify_") or not isinstance(challenge, str):
            return reply(request_id, {"error": "invalid verification event"}, 400)
        return reply(request_id, {"challenge": challenge}, 200)

    if payload.get("event") == "merchant.authorization.succeeded":
        platform = payload.get("data", {}).get("platform")
        business_category = payload.get("data", {}).get("businessCategory")
        valid_category = business_category in {"service_retail", "dining"}
        if platform not in {"meituan", "douyin"} or not valid_category:
            return reply(request_id, {"error": "invalid authorization classification"}, 400)
        # 使用 eventId 数据库唯一约束，并在同一事务中处理对应平台的业务联动。

    return reply(request_id)
```

#### Go · main.go · net/http

```go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"
)

var (
	apiKey    = []byte(os.Getenv("WEBHOOK_API_KEY"))
	requestID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`)
)

type eventEnvelope struct {
	EventID string          `json:"eventId"`
	Event   string          `json:"event"`
	Data    json.RawMessage `json:"data"`
}

func validSignature(timestamp, received string, rawBody []byte) bool {
	unixSeconds, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil || time.Now().Unix()-unixSeconds > 300 || unixSeconds-time.Now().Unix() > 300 {
		return false
	}
	mac := hmac.New(sha256.New, apiKey)
	_, _ = io.WriteString(mac, timestamp+".")
	_, _ = mac.Write(rawBody)
	expected := []byte("v1=" + hex.EncodeToString(mac.Sum(nil)))
	return hmac.Equal(expected, []byte(received))
}

func reply(w http.ResponseWriter, requestID string, status int, body any) {
	w.Header().Set("X-Request-Id", requestID)
	if body != nil {
		w.Header().Set("Content-Type", "application/json")
	}
	w.WriteHeader(status)
	if body != nil {
		_ = json.NewEncoder(w).Encode(body)
	}
}

func webhook(w http.ResponseWriter, r *http.Request) {
	traceID := r.Header.Get("X-Request-Id")
	if !requestID.MatchString(traceID) {
		http.Error(w, "invalid request id", http.StatusBadRequest)
		return
	}

	rawBody, err := io.ReadAll(io.LimitReader(r.Body, 64<<10))
	if err != nil || !validSignature(
		r.Header.Get("X-Event-Timestamp"),
		r.Header.Get("X-Event-Signature"),
		rawBody,
	) {
		reply(w, traceID, http.StatusUnauthorized, map[string]string{"error": "invalid signature"})
		return
	}

	var payload eventEnvelope
	if json.Unmarshal(rawBody, &payload) != nil {
		reply(w, traceID, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	eventID := r.Header.Get("X-Event-Id")
	if eventID == "" || eventID != payload.EventID {
		reply(w, traceID, http.StatusBadRequest, map[string]string{"error": "invalid event id"})
		return
	}

	if payload.Event == "webhook.endpoint.verification" {
		var data struct{ Challenge string `json:"challenge"` }
		if json.Unmarshal(payload.Data, &data) != nil ||
			!strings.HasPrefix(eventID, "evt_verify_") || data.Challenge == "" {
			reply(w, traceID, http.StatusBadRequest, map[string]string{"error": "invalid verification event"})
			return
		}
		reply(w, traceID, http.StatusOK, map[string]string{"challenge": data.Challenge})
		return
	}

	if payload.Event == "merchant.authorization.succeeded" {
		var data struct {
			Platform         string `json:"platform"`
			BusinessCategory string `json:"businessCategory"`
		}
		if json.Unmarshal(payload.Data, &data) != nil {
			reply(w, traceID, http.StatusBadRequest, map[string]string{"error": "invalid authorization classification"})
			return
		}
		validCategory := data.BusinessCategory == "service_retail" || data.BusinessCategory == "dining"
		if (data.Platform != "meituan" && data.Platform != "douyin") || !validCategory {
			reply(w, traceID, http.StatusBadRequest, map[string]string{"error": "invalid authorization classification"})
			return
		}
		// 使用 eventId 数据库唯一约束，并在同一事务中处理对应平台的业务联动。
	}

	reply(w, traceID, http.StatusNoContent, nil)
}

func main() {
	if len(apiKey) == 0 {
		panic("WEBHOOK_API_KEY is required")
	}
	http.HandleFunc("/webhook", webhook)
	_ = http.ListenAndServe(":3000", nil)
}
```

#### Node.js · server.cjs · Express

```javascript
const express = require("express");
const { createHmac, timingSafeEqual } = require("node:crypto");

const app = express();
const apiKey = process.env.WEBHOOK_API_KEY;
const requestIdPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
if (!apiKey) throw new Error("WEBHOOK_API_KEY is required");

function validSignature(req, rawBody) {
  const timestamp = req.get("X-Event-Timestamp") || "";
  const received = req.get("X-Event-Signature") || "";
  const unixSeconds = Number(timestamp);
  if (!Number.isInteger(unixSeconds) || Math.abs(Date.now() / 1000 - unixSeconds) > 300) {
    return false;
  }
  const digest = createHmac("sha256", apiKey)
    .update(timestamp)
    .update(".")
    .update(rawBody)
    .digest("hex");
  const expected = Buffer.from(`v1=${digest}`, "utf8");
  const actual = Buffer.from(received, "utf8");
  return expected.length === actual.length && timingSafeEqual(expected, actual);
}

app.post("/webhook", express.raw({ type: "application/json", limit: "64kb" }), (req, res) => {
  const requestId = req.get("X-Request-Id") || "";
  if (!requestIdPattern.test(requestId)) {
    return res.status(400).json({ error: "invalid request id" });
  }
  res.set("X-Request-Id", requestId);

  const rawBody = req.body;
  if (!Buffer.isBuffer(rawBody) || !validSignature(req, rawBody)) {
    return res.status(401).json({ error: "invalid signature" });
  }

  let payload;
  try {
    payload = JSON.parse(rawBody.toString("utf8"));
  } catch {
    return res.status(400).json({ error: "invalid json" });
  }

  const eventId = req.get("X-Event-Id") || "";
  if (!eventId || eventId !== payload.eventId) {
    return res.status(400).json({ error: "invalid event id" });
  }

  if (payload.event === "webhook.endpoint.verification") {
    const challenge = payload.data?.challenge;
    if (!eventId.startsWith("evt_verify_") || typeof challenge !== "string") {
      return res.status(400).json({ error: "invalid verification event" });
    }
    return res.status(200).json({ challenge });
  }

  if (payload.event === "merchant.authorization.succeeded") {
    const platform = payload.data?.platform;
    const businessCategory = payload.data?.businessCategory;
    const validCategory = ["service_retail", "dining"].includes(businessCategory);
    if (!["meituan", "douyin"].includes(platform) || !validCategory) {
      return res.status(400).json({ error: "invalid authorization classification" });
    }
    // 使用 eventId 数据库唯一约束，并在同一事务中处理对应平台的业务联动。
  }

  return res.sendStatus(204);
});

app.listen(Number(process.env.PORT || 3000));
```

## 核销聚合接口

聚合美团、抖音的验券校验、核销、撤销与团购信息查询。

### 输码验券校验

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/ddzh-tuangou-receipt-prepare`
- 在线调试风险：只读请求
- 授权：请求头需携带 Authorization: Bearer <token>。
- 说明：传具体门店 shopId、平台与券码或扫码内容，返回固定票价结构。美团和抖音成功时均返回后续核销必需的 ticketInfo；调用方必须保存 data.ticketInfo 的完整原文。抖音还会在 ticketData 中返回上游官方 data 对象的完整 JSON 字符串，次数卡的 ticketInfo 包含 time_card 对象。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `shopId`（`number | string`，必填）：系统门店 ID；仅支持具体门店，不支持商户。
- `platform`（`number | string`，必填）：平台：1 美团，2 抖音；默认值：1；可选值：1（美团）、2（抖音）
- `code`（`string`，必填）：券码；platform=2 时也支持扫码得到的完整 https://v.douyin.com/... 短链

#### 请求示例

##### 请求示例

```json
{
  "shopId": 123,
  "platform": 2,
  "code": "0106803637807"
}
```

##### 抖音扫码短链

```json
{
  "shopId": 123,
  "platform": 2,
  "code": "https://v.douyin.com/AbC123/"
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；通常为 10000，不可单独用于判断业务成功。
- `message`（`string | null`，选填）：Provider 或本地业务结果说明。
- `traceId`（`string | number | null`，选填）：上游或平台链路 ID。
- `data`（`object | null`，必填）：预验券结果；业务失败时为 null。
- `data.payAmount`（`number`，必填）：应付金额，单位为分。
- `data.shopId`（`string`，必填）：套餐或门店兼容 ID，当前历史字段语义不完全中立。
- `data.ticketName`（`string`，必填）：套餐名称。
- `data.ticketInfo`（`string`，必填）：美团和抖音成功时均返回的核销凭证。后续调用「验券」接口时必须整段原文回传，不得解析、修改、删减或重新序列化；抖音次数卡内部包含 time_card 对象。
- `data.ticketData`（`string`，必填）：历史详情原始数据；抖音返回上游官方响应 data 对象的完整 JSON 字符串，其他平台没有对应数据时为空字符串。

#### 响应示例

##### 响应示例

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "traceId": "2026040518025671DC47D0CC1663E68012",
  "data": {
    "payAmount": 13900,
    "shopId": "7441782134131394611",
    "ticketName": "次数卡套餐",
    "ticketInfo": "{\"verifyToken\":\"edcfd7a8-...\",\"encryptedCodes\":[\"...\"],\"time_card\":{\"time_card_type\":1,\"times_count\":2,\"times_used\":0}}",
    "ticketData": "{\"order_id\":\"order-1\",\"verify_token\":\"edcfd7a8-...\",\"certificates\":[{\"encrypted_code\":\"...\",\"time_card\":{\"times_count\":2,\"times_used\":0}}],\"error_code\":0}"
  }
}
```

##### 商户不支持（HTTP 400）

```json
{
  "success": false,
  "code": 10001,
  "message": "当前 shopId 对应主门店，该接口仅支持子门店，请更换为具体子门店的 shopId",
  "traceId": "691400000000000001",
  "data": null
}
```

#### 补充说明

- 抖音 ticketData 完整保留上游官方响应的 data 对象；其他平台没有对应数据时为空字符串。
- 美团和抖音成功响应均返回 data.ticketInfo。调用「验券」接口时必须将该字符串值整段原文复制到请求体 ticketInfo；除请求体 JSON 必需的转义外，解码后的字符串值必须完全一致。
- 抖音扫码短链只访问 v.douyin.com 的首跳并提取 object_id；解析失败时不会继续请求核销 Provider。
- **仅支持具体门店**：shopId 必须是系统门店 ID，不支持商户。传入商户时返回 HTTP 400、success=false、code=10001、data=null，并携带 traceId；message 为「当前 shopId 对应主门店，该接口仅支持子门店，请更换为具体子门店的 shopId」。

### 验券

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/ddzh-tuangou-receipt-consume`
- 在线调试风险：会改变业务状态；调用前必须确认：本次请求会改变业务状态：确认已先完成输码验券校验，并已按券码和业务订单完成业务侧去重；理解重复调用会返回 10500 已核销提示，再立即执行真实核销。
- 授权：请求头需携带 Authorization: Bearer <token>。
- 说明：使用具体门店 shopId 执行真实核销并返回核销结果。美团和抖音均必须传入「输码验券校验」成功响应中的 data.ticketInfo，且整段原文复制、不得改动。成功时 data[] 与顶层 consumeCredential[] 一一对应；每次发起新的核销前，必须先调用「输码验券校验」确认券当前可用。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `shopId`（`number | string`，必填）：系统门店 ID；仅支持具体门店，不支持商户。
- `platform`（`number | string`，必填）：平台：1 美团，2 抖音；默认值：1；可选值：1（美团）、2（抖音）
- `ticketInfo`（`string`，必填）：美团和抖音均必填。取自同一具体门店 shopId、同一平台、同一券码调用「输码验券校验」成功响应的 data.ticketInfo；必须整段原文复制，不得自行构造、解析、修改、删减或重新序列化
- `code`（`string`，必填）：券码
- `num`（`number | string`，选填）：核销数量；抖音次数卡先校验 time_card 剩余次数。encryptedCodes 少于 num 时，次数卡复制首项为 num 份，非次数卡仍拒绝；默认值：1

#### 请求示例

##### 请求示例

```json
{
  "shopId": 123,
  "platform": 2,
  "ticketInfo": "...",
  "code": "0106803637807",
  "num": 1
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；首次成功通常为 10000，命中已成功订单的重复请求为 10500，不可单独用于判断业务成功。
- `message`（`string | null`，选填）：Provider 或本地业务结果说明；成功订单重复请求时返回核销时间和门店名称。
- `traceId`（`string | number | null`，选填）：上游或平台链路 ID。
- `wxdgOrderNo`（`string`，选填）：无限聚核系统账单订单号，可用于问题定位和查询账单订单。订单创建后返回；建单前参数或鉴权失败时省略。
- `consumeCredential`（`string[]`，必填）：撤销凭证数组；成功时与 data[i] 一一对应，业务失败或结果未知时固定为空数组。
- `platformResult`（`object | null`，必填）：平台原始输出对象。美团保留 data 层及其原始值，只移除信封外层 code/msg/traceId 并注入 platform=1；抖音保留根对象 data/extra 并注入 platform=2。选定对象内其他字段、未知扩展、大整数和敏感业务字段均原样返回，不筛选、不改名、不脱敏；没有合法平台对象时为 null。
- `platformResult.platform`（`number`，选填）：服务端注入的平台标识：1=美团，2=抖音。platformResult 为 null 时不存在。
- `data`（`array | null`，必填）：固定核销结果数组；业务失败或结果未知时为 null。
- `data[].bizType`（`number`，必填）：业务类型。
- `data[].additionItemInfos`（`array`，必填）：附加项目列表。
- `data[].merchantAmount`（`string`，必填）：商户承担金额。
- `data[].orderId`（`string`，必填）：平台订单 ID。
- `data[].dealId`（`string | number`，必填）：团购套餐 ID。
- `data[].dealTitle`（`string`，必填）：团购套餐名称。
- `data[].dealMarketPrice`（`number`，必填）：套餐门市价，单位为元。
- `data[].mobile`（`string`，必填）：关联手机号，可能为空或脱敏。
- `data[].receiptCode`（`string`，必填）：已核销券码。
- `data[].dealPrice`（`number | string`，必填）：套餐销售价，单位为元。
- `data[].receiptEndDate`（`string`，必填）：券有效期结束时间。
- `data[].platformAmount`（`string`，必填）：平台承担金额。
- `data[].paymentDetail`（`array`，必填）：支付明细列表。
- `data[].paymentDetail[].amountType`（`number`，必填）：金额类型。
- `data[].paymentDetail[].amount`（`string`，必填）：支付明细金额。
- `data[].paymentDetail[].paymentDetailId`（`string`，必填）：支付明细 ID。
- `data[].dealGroupId`（`string | number`，必填）：团购分组 ID。
- `data[].tgTimesCardFlag`（`boolean`，必填）：是否为团购次卡。

#### 响应示例

##### 响应示例

```json
{
  "success": true,
  "code": 10000,
  "message": "履约成功",
  "traceId": "20260405180323EAC4753A33B03DFA328B",
  "wxdgOrderNo": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "data": [
    {
      "bizType": 0,
      "additionItemInfos": [],
      "merchantAmount": "0",
      "orderId": "1093364371426903131",
      "dealId": "1816849307970587",
      "dealTitle": "【不限时段】畅玩5小时",
      "dealMarketPrice": 92,
      "mobile": "",
      "receiptCode": "180886072740962",
      "dealPrice": 79.9,
      "receiptEndDate": "",
      "platformAmount": "0",
      "paymentDetail": [{
        "amountType": 10,
        "amount": "79.9",
        "paymentDetailId": "7625212831793299498"
      }],
      "dealGroupId": "1816849307970587",
      "tgTimesCardFlag": false
    }
  ],
  "consumeCredential": ["hcv1.example-credential"],
  "platformResult": {
    "platform": 2,
    "data": {
      "error_code": 0,
      "verify_results": [{
        "result": 0,
        "verify_id": "7625212831793299498",
        "certificate_id": "1093364371426903131",
        "origin_code": "RAW-PLATFORM-CODE"
      }]
    },
    "extra": {
      "error_code": 0,
      "sub_error_code": 0
    }
  }
}
```

##### 平台明确拒绝

```json
{
  "success": false,
  "code": 11006,
  "message": "此券号不存在，请与消费者确认提供的券号是否正确！",
  "traceId": "20260405180323EAC4753A33B03DFA328B",
  "wxdgOrderNo": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "data": null,
  "consumeCredential": [],
  "platformResult": {
    "platform": 1,
    "data": {
      "errMsg": "此券号不存在，请与消费者确认提供的券号是否正确！"
    }
  }
}
```

##### 成功订单重复请求

```json
{
  "success": false,
  "code": 10500,
  "message": "优惠券已于:2026-08-13 20:45:54核销;门店:化大",
  "traceId": "20260405180323EAC4753A33B03DFA328B",
  "wxdgOrderNo": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "data": null,
  "consumeCredential": [],
  "platformResult": null
}
```

##### 商户不支持（HTTP 400）

```json
{
  "success": false,
  "code": 10001,
  "message": "当前 shopId 对应主门店，该接口仅支持子门店，请更换为具体子门店的 shopId",
  "traceId": "691400000000000001",
  "data": null,
  "consumeCredential": [],
  "platformResult": null
}
```

#### 补充说明

- **重要：ticketInfo 必须来自预核销响应原文**：美团和抖音核销均必传 ticketInfo。先使用同一具体门店 shopId、platform 和 code 调用「输码验券校验」，取得 success=true 响应中的 data.ticketInfo，再把该字符串值整段复制到本接口的 ticketInfo。不得自行构造、解析、修改、删减或重新序列化；除请求体 JSON 必需的转义外，解码后的字符串值必须与预核销返回值完全一致。
- **重要：接口幂等不等于业务幂等**：每次发起新的核销前，必须先调用「输码验券校验」确认券当前可用。相同业务请求命中已成功订单时，服务端返回 success=false、code=10500，并在 message 中给出核销时间和门店，不再原样返回成功数据。服务端提示不能替代业务系统按券码和自身业务订单建立独立去重。
- 抖音次数卡会先校验 times_count - times_used >= num，可用次数不足时不会创建核销 operation。校验通过但 encryptedCodes 少于 num 时，次数卡将首项复制为 num 份；非次数卡仍拒绝。结果未知时禁止自动重试，应先查询平台券状态或完成对账。调用方无需传 idempotencyKey；旧客户端继续传入时服务端完全忽略。
- 核销成功后必须按 data[i] 下标保存对应的 consumeCredential[i]。公开 data[] 不再返回内部 flowId；V1、平台原生和历史无凭证核销记录不能通过聚合撤销接口撤销。
- platformResult 会移除美团信封外层 code/msg/traceId，但保留 data 层及其原始值；抖音保留 data/extra。两平台均只额外注入 platform 标识，选定对象内其他字段不筛选、不改名、不脱敏。调用方不得依赖平台局部字段替代顶层 success 判断业务结果。
- 顶层 wxdgOrderNo 是无限聚核系统账单订单号，可直接用于问题定位；data[].orderId 是平台订单 ID，两者不能混用。
- **仅支持具体门店**：shopId 必须是系统门店 ID，不支持商户。传入商户时返回 HTTP 400、success=false、code=10001、data=null，并携带 traceId；message 为「当前 shopId 对应主门店，该接口仅支持子门店，请更换为具体子门店的 shopId」。

### 撤销核销

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/ddzh-tuangou-receipt-cancel`
- 在线调试风险：会改变业务状态；调用前必须确认：本次请求会改变业务状态：确认门店、平台和全部 consumeCredential 均来自需要撤销的核销成功响应；理解重复调用仍会再次请求平台，再立即执行真实撤销。
- 授权：请求头需携带 Authorization: Bearer <token>。
- 说明：使用新版 V2 聚合核销成功响应中的 consumeCredential[] 撤销一条或多条核销记录，支持美团和抖音。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `shopId`（`number | string`，必填）：原核销记录所属的系统门店 ID
- `platform`（`number | string`，必填）：平台：1 美团，2 抖音；必须与凭证绑定的平台一致；默认值：1；可选值：1（美团）、2（抖音）
- `consumeCredential`（`string[]`，必填）：取自 POST /api/hexiao/v2/ddzh-tuangou-receipt-consume 核销成功响应的顶层 consumeCredential[]；它与响应 data[i] 按下标一一对应。请保存需要撤销项对应的凭证并原样传入，数组长度 1-10。

#### 请求示例

##### 请求示例

```json
{
  "shopId": 123,
  "platform": 2,
  "consumeCredential": [
    "hcv1.example-credential-1",
    "hcv1.example-credential-2"
  ]
}
```

#### 响应字段

- `success`（`boolean`，必填）：仅全部凭证均撤销成功时为 true；部分成功、拒绝或 unknown 均为 false。
- `code`（`number`，必填）：全部成功时为 10000，未全部成功时为 10001。
- `message`（`string`，必填）：撤销成功、部分撤销完成、撤销失败或撤销结果未知。
- `traceId`（`string | number | null`，选填）：当前请求链路 ID。
- `data`（`array`，必填）：与请求 consumeCredential[] 一一对应的逐项撤销结果。
- `data[].consumeCredential`（`string`，必填）：当前结果对应的原撤销凭证。
- `data[].status`（`succeeded | rejected | unknown`，必填）：逐项撤销状态；unknown 表示当前证据不足，不能判定平台最终结果。
- `data[].message`（`string`，必填）：当前凭证的撤销结果说明。

#### 响应示例

##### 全部成功

```json
{
  "success": true,
  "code": 10000,
  "message": "撤销成功",
  "traceId": "20260805194500CANCEL001",
  "data": [
    {
      "consumeCredential": "hcv1.example-credential-1",
      "status": "succeeded",
      "message": "撤销成功"
    }
  ]
}
```

##### 部分成功

```json
{
  "success": false,
  "code": 10001,
  "message": "部分撤销完成",
  "traceId": "20260805194500CANCEL002",
  "data": [
    {
      "consumeCredential": "hcv1.example-credential-1",
      "status": "succeeded",
      "message": "撤销成功"
    },
    {
      "consumeCredential": "hcv1.example-credential-2",
      "status": "rejected",
      "message": "平台拒绝撤销"
    }
  ]
}
```

#### 补充说明

- 该接口不要求 Idempotency-Key，也不会建立撤销幂等记录；相同凭证重复请求仍会再次投递平台。结果 unknown 时必须先查询券状态或对账，再决定是否重新发起撤销。
- 只支持新版 V2 聚合核销返回的 consumeCredential；凭证必须属于当前账号、shopId 和 platform。V1、平台原生接口及历史无凭证记录不支持。
- 撤销免费且不新建核销 operation。成功项会更新原核销记录的撤销数量和状态；重复撤销不会重复增加本地撤销计数。

### 获取团购信息

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/ddzh-tuangou-deal-queryshopdeal`
- 在线调试风险：只读请求
- 授权：请求头需携带 Authorization: Bearer <token>。
- 说明：分页查询具体门店 shopId 对应的在售团购，美团与抖音统一 Deal 列表结构。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `shopId`（`number | string`，必填）：系统门店 ID；仅支持具体门店，不支持商户。
- `platform`（`number | string`，必填）：平台：1 美团，2 抖音；默认值：1；可选值：1（美团）、2（抖音）
- `offset`（`number | string`，选填）：页码；默认值：1
- `limit`（`number | string`，选填）：每页条数；默认值：10

#### 请求示例

##### 请求示例

```json
{
  "shopId": 123,
  "platform": 2,
  "offset": "1",
  "limit": "10"
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；通常为 10000，不可单独用于判断业务成功。
- `message`（`string | null`，选填）：Provider 或本地业务结果说明。
- `traceId`（`string | number | null`，选填）：上游或平台链路 ID。
- `data`（`array`，必填）：统一团购套餐列表。
- `data[].marketPrice`（`number`，必填）：市场价，单位为元；缺省时为 0。
- `data[].extraMap`（`string`，选填）：Provider 扩展信息 JSON 字符串；部分美团响应存在。
- `data[].endDate`（`string`，必填）：销售结束日期，缺省时为空字符串。
- `data[].dealId`（`string | number`，必填）：团购套餐 ID，SKU ID 优先。
- `data[].saleStatus`（`number`，必填）：销售状态。
- `data[].title`（`string`，必填）：套餐名称。
- `data[].dealType`（`number`，必填）：套餐类型，缺省时为 1。
- `data[].receiptEndDate`（`string`，必填）：券有效期结束日期，缺省时为空字符串。
- `data[].beginDate`（`string`，必填）：销售开始日期，缺省时为空字符串。
- `data[].saleChannelName`（`string`，必填）：销售渠道名称。
- `data[].dealGroupStatus`（`number`，必填）：团购分组状态，缺省时为 1。
- `data[].price`（`number`，必填）：销售价，单位为元。
- `data[].receiptBeginDate`（`string`，必填）：券有效期开始日期，缺省时为空字符串。
- `data[].dealGroupId`（`string | number`，必填）：团购分组 ID，SKU ID 优先。

#### 响应示例

##### 响应示例

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "traceId": "202604051814407E5A0C7AC78468AD445D",
  "data": [
    {
      "marketPrice": 92,
      "endDate": "",
      "dealId": "1816849307970587",
      "saleStatus": 2,
      "title": "【不限时段】畅玩5小时",
      "dealType": 1,
      "receiptEndDate": "",
      "beginDate": "",
      "saleChannelName": "抖音",
      "dealGroupStatus": 1,
      "price": 79.9,
      "receiptBeginDate": "",
      "dealGroupId": "1816849307970587"
    }
  ]
}
```

##### 商户不支持（HTTP 400）

```json
{
  "success": false,
  "code": 10001,
  "message": "当前 shopId 对应主门店，该接口仅支持子门店，请更换为具体子门店的 shopId",
  "traceId": "691400000000000001",
  "data": null
}
```

#### 补充说明

- offset 默认 1，limit 默认 10（最大 100）；公共入参不包含 source。
- **仅支持具体门店**：shopId 必须是系统门店 ID，不支持商户。传入商户时返回 HTTP 400、success=false、code=10001、data=null，并携带 traceId；message 为「当前 shopId 对应主门店，该接口仅支持子门店，请更换为具体子门店的 shopId」。

## 无限聚核平台接口

商户、门店、授权、额度与充值由无限聚核平台统一提供，按各接口的公开参数调用。

### 获取美团/抖音授权链接

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/get/auth/url`
- 在线调试风险：只读请求
- 授权：请求头需携带 Authorization: Bearer <token>。
- 说明：需携带 Authorization Bearer key。仅为 shopId 对应的具体系统门店生成美团或抖音授权 URL，不支持商户。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `shopId`（`number | string`，必填）：系统门店 ID；仅支持具体门店，不支持商户。
- `platform`（`number | string`，选填）：1（美团），2（抖音）；不传默认 1（美团）。；默认值：1；可选值：1（美团）、2（抖音）

#### 请求示例

##### 请求示例

```json
{
  "shopId": 123,
  "platform": 1
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；成功时为 10000，不可单独用于判断业务成功。
- `message`（`string | null`，选填）：Provider 或本地业务结果说明。
- `traceId`（`string | number | null`，选填）：上游或平台链路 ID。
- `data`（`string | null`，必填）：授权 URL；授权失败时为 null。

#### 响应示例

##### 响应示例

```json
{
  "success": true,
  "code": 10000,
  "message": "OK",
  "traceId": "691400000000000001",
  "data": "https://example.com/auth?token=example"
}
```

##### 商户不支持（HTTP 400）

```json
{
  "success": false,
  "code": 10001,
  "message": "当前 shopId 对应主门店，该接口仅支持子门店，请更换为具体子门店的 shopId",
  "traceId": "691400000000000001",
  "data": null
}
```

#### 补充说明

- 接口根据 platform 生成对应平台的授权链接；调用方应将响应 data 中的 URL 交由商家完成授权。
- **仅支持具体门店**：POST /api/hexiao/v2/get/auth/url 与 POST /api/hexiao/v2/meituan/get/auth/url 为同一授权接口的两个路径，均仅支持门店，shopId 必须是系统门店 ID。传入商户时均返回 HTTP 400、success=false、code=10001、data=null，并携带 traceId；message 统一为「当前 shopId 对应主门店，该接口仅支持子门店，请更换为具体子门店的 shopId」。

### 商户列表

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/merchant/list`
- 在线调试风险：只读请求
- 授权：请求头需携带 Authorization: Bearer <token>。结果只包含当前认证账号拥有的数据。
- 说明：分页查询当前认证账号拥有且未删除的商户。可按商户内部名称、商户 ID 和业务类型筛选；所有筛选条件同时生效。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `name`（`string`，选填）：商户内部名称的字面量子串；服务端先去除首尾空白，空字符串表示不按名称筛选。不会匹配门店名称；显式 null 无效并返回 HTTP 400。
- `masterShopId`（`positive safe integer | decimal integer string`，选填）：商户 ID。支持正安全整数或十进制整数字符串；空白值按未传处理。
- `businessCategory`（`string`，选填）：业务类型。服务端去除首尾空白并转为小写；空字符串表示不筛选，显式 null 无效并返回 HTTP 400。；可选值：service_retail（服务零售）、dining（到店餐饮）
- `page`（`positive safe integer | decimal integer string`，选填）：页码，必须为正安全整数；省略或 null 时使用默认值，空字符串无效。；默认值：1
- `pageSize`（`positive safe integer | decimal integer string`，选填）：每页数量，必须为 1 至 20 的安全整数；省略或 null 时使用默认值，空字符串无效。；默认值：10

#### 请求示例

##### 按名称与业务类型查询

```json
{
  "name": "华东",
  "businessCategory": "service_retail",
  "page": 1,
  "pageSize": 10
}
```

##### 查询当前账号全部商户

```json
{}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；成功时为 10000。
- `message`（`string`，必填）：业务结果说明；成功时为 success。
- `traceId`（`string`，必填）：本次请求的链路 ID。
- `data.list`（`array`，必填）：当前页商户列表；没有结果时为 []，不会返回 null。
- `data.list[].shopId`（`number`，必填）：商户的系统 ID。
- `data.list[].masterShopId`（`number`，必填）：商户 ID，与同一对象的 shopId 相等。
- `data.list[].name`（`string`，必填）：商户内部名称；缺失时为 ""。
- `data.list[].businessCategory`（`string`，必填）：规范化后的业务类型。历史空值或无效值按 service_retail 返回。
- `data.list[].childTotal`（`number`，必填）：该商户下当前账号拥有且未删除的门店总数。
- `data.list[].childHasMore`（`boolean`，必填）：childTotal 是否大于 20。
- `data.list[].children`（`array`，必填）：该商户下按固定顺序返回的门店预览，最多 20 条；没有门店时为 []。
- `data.list[].children[].shopId`（`number`，必填）：门店的系统 ID。
- `data.list[].children[].masterShopId`（`number`，必填）：所属商户 ID。
- `data.list[].children[].name`（`string`，必填）：门店内部名称；缺失时为 ""。
- `data.list[].children[].businessCategory`（`string`，必填）：继承所属商户并规范化后的业务类型。
- `data.list[].children[].douyinAuthorized`（`boolean`，必填）：已保存的抖音授权状态；到店餐饮以对应有效餐饮授权记录为准，服务零售沿用当前有效授权路由。
- `data.list[].children[].meituanAuthorized`（`boolean`，必填）：与当前业务类型匹配的已保存美团授权状态。
- `data.list[].children[].meituanName`（`string`，必填）：最新有效美团外部账号名称；缺失时为 ""，授权为 true 时也可能为空。
- `data.list[].children[].douyinName`（`string`，必填）：对应业务类目的最新有效抖音外部账号名称；没有已保存名称时为 ""。
- `data.page`（`number`，必填）：当前页码。
- `data.pageSize`（`number`，必填）：本次请求采用的每页数量。
- `data.total`（`number`，必填）：符合筛选条件的商户总数；超出末页时仍保留该总数。

#### 响应示例

##### 响应示例

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "traceId": "merchant-list-10001",
  "data": {
    "list": [
      {
        "shopId": 1000,
        "masterShopId": 1000,
        "name": "示例商户",
        "businessCategory": "service_retail",
        "childTotal": 21,
        "childHasMore": true,
        "children": [
          {
            "shopId": 1001,
            "masterShopId": 1000,
            "name": "示例门店",
            "businessCategory": "service_retail",
            "douyinAuthorized": true,
            "meituanAuthorized": true,
            "meituanName": "美团示例门店",
            "douyinName": "抖音示例门店"
          }
        ]
      }
    ],
    "page": 1,
    "pageSize": 10,
    "total": 1
  }
}
```

#### 补充说明

- **固定顺序与门店预览**：商户与 children 均按 updated_at DESC, id DESC 排序。每个商户最多预览 20 个当前账号拥有且未删除的门店；预览与同一数据时点、不带 name 和 shopId 筛选的门店列表第 1 页 pageSize=20 一致。空商户也会返回。
- **分页是实时视图**：分页不是导出快照；两次请求之间如有记录更新，记录可能因 updated_at 变化而移动，连续翻页可能出现重复或遗漏。页码和偏移量超出安全整数范围时返回 HTTP 400。
- **严格请求字段**：请求体必须是 JSON 对象，空请求体按 {} 处理。name 或 businessCategory 显式 null、其他字段类型或取值无效、分页范围无效时均返回 HTTP 400；不支持的字段也返回 HTTP 400，包括 sortBy、sortOrder、adminUserId 和 shopId。
- **授权状态含义**：授权字段反映系统已保存的有效授权记录，不是实时可调用性检查。美团按商户业务类型匹配有效授权：service_retail 使用业务码 58，dining 使用业务码 1；service_retail 的抖音状态来自有效门店 Provider 绑定，dining 的抖音状态固定为 false。平台名称取最新有效且非空的授权账号显示名，不保证等同于平台单店名称；授权为 true 时名称仍可能为空。接口不会调用外部平台，也不会返回平台 ID、凭据、Provider 信息、余额或 shopKey。

### 门店列表

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/store/list`
- 在线调试风险：只读请求
- 授权：请求头需携带 Authorization: Bearer <token>。结果只包含当前认证账号拥有的数据。
- 说明：分页查询当前认证账号指定商户下拥有且未删除的门店。masterShopId 必填，可按门店内部名称和门店 ID 筛选；所有筛选条件同时生效。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `masterShopId`（`positive safe integer | decimal integer string`，必填）：商户 ID。支持正安全整数或十进制整数字符串；空白值按未传处理，因此仍会触发必填校验。
- `name`（`string`，选填）：门店内部名称的字面量子串；服务端先去除首尾空白，空字符串表示不按名称筛选，显式 null 无效并返回 HTTP 400。
- `shopId`（`positive safe integer | decimal integer string`，选填）：门店 ID。支持正安全整数或十进制整数字符串；空白值按未传处理。
- `page`（`positive safe integer | decimal integer string`，选填）：页码，必须为正安全整数；省略或 null 时使用默认值，空字符串无效。；默认值：1
- `pageSize`（`positive safe integer | decimal integer string`，选填）：每页数量，必须为 1 至 100 的安全整数；省略或 null 时使用默认值，空字符串无效。；默认值：20

#### 请求示例

##### 查询商户下的门店

```json
{
  "masterShopId": 1000,
  "name": "人民路",
  "page": 1,
  "pageSize": 20
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；成功时为 10000。
- `message`（`string`，必填）：业务结果说明；成功时为 success。
- `traceId`（`string`，必填）：本次请求的链路 ID。
- `data.list`（`array`，必填）：当前页门店列表；没有结果时为 []，不会返回 null。
- `data.list[].shopId`（`number`，必填）：门店的系统 ID。
- `data.list[].masterShopId`（`number`，必填）：所属商户 ID。
- `data.list[].name`（`string`，必填）：门店内部名称；缺失时为 ""。
- `data.list[].businessCategory`（`string`，必填）：继承所属商户并规范化后的业务类型。历史空值或无效值按 service_retail 返回。
- `data.list[].douyinAuthorized`（`boolean`，必填）：已保存的抖音授权状态；到店餐饮以对应有效餐饮授权记录为准，服务零售沿用当前有效授权路由。
- `data.list[].meituanAuthorized`（`boolean`，必填）：与当前业务类型匹配的已保存美团授权状态。
- `data.list[].meituanName`（`string`，必填）：最新有效美团外部账号名称；缺失时为 ""，授权为 true 时也可能为空。
- `data.list[].douyinName`（`string`，必填）：对应业务类目的最新有效抖音外部账号名称；没有已保存名称时为 ""。
- `data.page`（`number`，必填）：当前页码。
- `data.pageSize`（`number`，必填）：本次请求采用的每页数量。
- `data.total`（`number`，必填）：符合筛选条件的门店总数；超出末页时仍保留该总数。

#### 响应示例

##### 响应示例

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "traceId": "store-list-10001",
  "data": {
    "list": [
      {
        "shopId": 1001,
        "masterShopId": 1000,
        "name": "示例门店",
        "businessCategory": "service_retail",
        "douyinAuthorized": true,
        "meituanAuthorized": true,
        "meituanName": "美团示例门店",
        "douyinName": "抖音示例门店"
      }
    ],
    "page": 1,
    "pageSize": 20,
    "total": 1
  }
}
```

##### 指定门店不属于所选商户

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "traceId": "store-list-empty-10001",
  "data": {
    "list": [],
    "page": 1,
    "pageSize": 20,
    "total": 0
  }
}
```

#### 补充说明

- **商户校验**：masterShopId 必须是当前认证账号拥有、未删除且实际为商户的 ID。不可访问、已删除或不是商户时统一返回 HTTP 404 和相同安全提示，不泄露数据是否存在。
- **筛选与固定顺序**：name 只匹配门店内部名称。shopId 不属于所选商户或当前账号时成功返回空列表。结果固定按 updated_at DESC, id DESC 排序；商户列表中的 children 预览使用同一顺序。
- **分页是实时视图**：分页不是导出快照；两次请求之间如有记录更新，记录可能因 updated_at 变化而移动，连续翻页可能出现重复或遗漏。页码和偏移量超出安全整数范围时返回 HTTP 400。
- **严格请求字段**：请求体必须是 JSON 对象，空请求体会因缺少 masterShopId 返回 HTTP 400。name 显式 null、其他字段类型或取值无效、分页范围无效时均返回 HTTP 400；不支持的字段也返回 HTTP 400，包括 businessCategory、sortBy、sortOrder 和 adminUserId。
- **授权状态含义**：授权字段反映系统已保存的有效授权记录，不是实时可调用性检查。美团按商户业务类型匹配有效授权：service_retail 使用业务码 58，dining 使用业务码 1；service_retail 的抖音状态来自有效门店 Provider 绑定，dining 的抖音状态固定为 false。平台名称取最新有效且非空的授权账号显示名，不保证等同于平台单店名称；授权为 true 时名称仍可能为空。接口不会调用外部平台，也不会返回平台 ID、凭据、Provider 信息、余额或 shopKey。

### 创建门店

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/store/create`
- 在线调试风险：会改变业务状态；调用前必须确认：本次请求会改变业务状态：确认名称、商户与门店关系和业务类型无误，并同意创建真实商户或门店。
- 授权：请求头需携带 Authorization: Bearer <token>。
- 说明：不传 masterShopId 时只创建一个商户并返回该商户 ID，不隐式创建首个门店；创建门店时只允许传当前认证账号所属商户 ID。归属始终来自 Authorization 对应的认证身份。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `shopName`（`string`，必填）：商户或门店名称
- `masterShopId`（`number | string`，选填）：创建门店时使用的商户 ID，必须属于当前认证账号。创建商户时不要填写。
- `businessCategory`（`string`，选填）：业务类型，仅创建商户时生效；service_retail 表示服务零售，dining 表示到店餐饮。省略、null 或空白时默认为 service_retail。创建门店时继承所属商户的业务类型，不允许覆盖，门店请求中的该字段会被忽略。；默认值：service_retail；可选值：service_retail（服务零售）、dining（到店餐饮）

#### 请求示例

##### 请求示例（商户）

```json
{
  "shopName": "示例商户",
  "businessCategory": "service_retail"
}
```

##### 请求示例（门店）

```json
{
  "shopName": "示例门店",
  "masterShopId": 123
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；成功时为 10000。
- `message`（`string`，必填）：业务结果说明。
- `data.shopId`（`number`，必填）：本次创建并返回的门店 ID。
- `data.name`（`string`，必填）：门店名称。
- `data.shopKey`（`string`，必填）：门店标识。
- `data.providerShopId`（`null`，必填）：Provider 门店 ID；创建商户或门店时均为 null。

#### 响应示例

##### 响应示例

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "data": {
    "shopId": 10001,
    "name": "示例商户",
    "shopKey": "ABCDEF",
    "providerShopId": null
  }
}
```

#### 补充说明

- 不传 masterShopId 时只创建一个商户；传入商户 ID 时创建门店。门店只可归属于当前认证账号所属商户，业务类型始终继承商户。

### 编辑商户或门店

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/store/update`
- 在线调试风险：会改变业务状态；调用前必须确认：本次请求会改变业务状态：确认 shopId 和新名称无误，并同意修改真实商户或门店名称。
- 授权：请求头需携带 Authorization: Bearer <token>。归属和操作权限只根据当前认证身份判断。
- 说明：只修改商户或门店内部名称；目标必须由当前认证账号拥有且未删除，不修改业务类型、商户归属、平台名称或授权状态。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `shopId`（`positive safe integer | decimal integer string`，必填）：当前认证账号内商户或门店的系统 ID，支持正安全整数或十进制整数字符串。
- `shopName`（`string`，必填）：新的内部名称。服务端去除首尾空白后必须为 1 至 20 个 Unicode 字符；null、非字符串、空名称或超长名称均返回 HTTP 400。

#### 请求示例

##### 修改商户或门店名称

```json
{
  "shopId": 10001,
  "shopName": "新名称"
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；成功时为 10000。
- `message`（`string`，必填）：修改结果说明；成功时为 修改成功。
- `traceId`（`string`，必填）：本次请求的链路 ID。
- `data.shopId`（`number`，必填）：已修改名称的商户或门店系统 ID。
- `data.name`（`string`，必填）：去除首尾空白后实际保存的新名称。

#### 响应示例

##### 修改成功（HTTP 200）

```json
{
  "success": true,
  "code": 10000,
  "message": "修改成功",
  "traceId": "store-update-10001",
  "data": {
    "shopId": 10001,
    "name": "新名称"
  }
}
```

#### 补充说明

- **名称更新规则**：同名更新成功。门店名称会检查同一商户下其他未删除门店，排除自身后存在同名门店时返回 HTTP 400；商户名称不做全局重复检查，与 Admin 管理逻辑一致。更新会写入 name 和 updated_at，因此下一次列表查询中的排序位置可能变化。
- **严格请求字段**：请求体只接受 shopId 和 shopName，二者均必填。无效 JSON、空名称、shopName 显式 null、非字符串或超过 20 个 Unicode 字符，以及 businessCategory、masterShopId、platform、adminUserId 等未知字段均返回 HTTP 400。
- **账号隔离与副作用**：shopId 不属于当前认证账号、记录已软删除或不存在时统一返回 HTTP 404。接口只修改内部名称，不调用 Provider，不修改或刷新 Redis 路由，不扣费，也不创建 operation。

### 删除商户或门店

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/store/delete`
- 在线调试风险：会改变业务状态；调用前必须确认：本次请求会改变业务状态，软删除真实商户或门店并停用平台路由：确认 shopId 无误、商户下已无未删除门店，并同意执行删除。
- 授权：请求头需携带 Authorization: Bearer <token>。归属和操作权限只根据当前认证身份判断。
- 说明：软删除当前认证账号拥有的商户或门店。删除商户前必须先删除其全部未删除门店，接口不会级联删除。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `shopId`（`positive safe integer | decimal integer string`，必填）：当前认证账号内商户或门店的系统 ID，支持正安全整数或十进制整数字符串。

#### 请求示例

##### 删除商户或门店

```json
{
  "shopId": 10001
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；成功时为 10000。
- `message`（`string`，必填）：删除结果说明。
- `traceId`（`string`，必填）：本次请求的链路 ID。
- `data.shopId`（`number`，必填）：已软删除的商户或门店系统 ID。

#### 响应示例

##### 删除成功（HTTP 201）

```json
{
  "success": true,
  "code": 10000,
  "message": "删除成功",
  "traceId": "store-delete-10001",
  "data": {
    "shopId": 10001
  }
}
```

##### 商户下仍有门店（HTTP 400）

```json
{
  "success": false,
  "code": 10001,
  "message": "主门店下存在子门店，请先删除子门店",
  "traceId": "store-delete-10001"
}
```

#### 补充说明

- **商户删除保护**：商户下存在任何未删除门店时返回 HTTP 400，必须先逐一删除门店，不能级联删除。已软删除门店不阻止商户删除。真正删除时会事务锁定商户并再次检查子门店，避免并发创建和删除交错。后端保留错误原文「主门店下存在子门店，请先删除子门店」。
- **请求兼容性**：请求体必须包含有效 shopId；无效 JSON 或无效 ID 返回 HTTP 400。为兼容既有调用，额外字段会被忽略；账号归属仍只取当前 Authorization，任何请求字段都不能指定其他账号。
- **账号隔离与删除副作用**：shopId 不属于当前认证账号、记录已软删除或不存在时统一返回 HTTP 404。删除成功沿用 HTTP 201，记录执行软删除，并停用该商户或门店的美团、抖音平台路由和清理缓存。预校验失败不会清理缓存。

### 查询剩余核销次数

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/quota/query`
- 在线调试风险：只读请求
- 授权：请求头需携带 Authorization: Bearer <token>。
- 说明：按邮箱查询账号级剩余核销次数；传入 shopId 时查询该账号下指定门店的专属剩余次数。

#### Headers

- `Authorization`（`string`，必填）：Bearer <token>

#### JSON Body 参数

- `email`（`string`，必填）：用户邮箱，用于查询账号
- `shopId`（`number | string`，选填）：门店 ID。传入时查询门店专属剩余次数，不传时查询账号级剩余次数

#### 请求示例

##### 账号级请求示例

```json
{
  "email": "test@example.com"
}
```

##### 门店级请求示例

```json
{
  "email": "test@example.com",
  "shopId": 8673891
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；成功时为 10000。
- `message`（`string`，必填）：业务结果说明。
- `data.source`（`"admin" | "store"`，必填）：额度来源；admin 为账号级，store 为门店级。
- `data.email`（`string`，必填）：被查询账号的邮箱。
- `data.shop_id`（`number | null`，必填）：系统门店 ID；账号级查询时为 null。当前响应字段使用 snake_case。
- `data.remaining_count`（`number`，必填）：剩余可核销次数。

#### 响应示例

##### 账号级响应示例

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "data": {
    "source": "admin",
    "email": "test@example.com",
    "shopId": null,
    "remaining_count": 88
  }
}
```

##### 门店级响应示例

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "data": {
    "source": "store",
    "email": "test@example.com",
    "shopId": 8673891,
    "remaining_count": 12
  }
}
```

##### 错误响应示例

```json
{
  "success": false,
  "code": 10001,
  "message": "email 必填"
}
```

#### 补充说明

- 常见错误信息：email 必填、账号不存在、shopId 格式错误、门店不存在。

### 查询可充值金额

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/recharge/plans`
- 在线调试风险：只读请求
- 授权：请求头需携带登录接口签发的 Authorization: Bearer <V2 JWT>。
- 说明：查询当前账号或所属商户可购买的充值档位。shopId 省略或传 0 时查询主账户档位；传大于 0 的系统门店 ID 时查询该商户档位。

#### Headers

- `Authorization`（`string`，必填）：Bearer <V2 JWT>，由 V2 登录接口签发

#### JSON Body 参数

- `shopId`（`number`，选填）：无限聚核系统商户 ID（store.id）；省略或传 0 表示当前管理员主账户，大于 0 时必须属于当前账号且未删除；默认值：0

#### 请求示例

##### 查询主账户充值档位

```json
{
  "shopId": 0
}
```

##### 查询门店充值档位

```json
{
  "shopId": 8673891
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；成功时为 10000。
- `message`（`string`，必填）：业务结果说明。
- `traceId`（`string`，必填）：本次请求链路 ID，排查问题时请一并提供。
- `data.shopId`（`number`，必填）：本次查询的充值目标；0 表示当前管理员主账户。
- `data.unitPriceFen`（`number`，必填）：当前档位组的单次核销价格，单位为分。
- `data.unitPrice`（`number`，必填）：当前档位组的单次核销价格，单位为元。
- `data.pricingMode`（`number`，必填）：当前档位组计价模式：1 为历史账号倍率，2 为固定单价。
- `data.plans`（`object[]`，必填）：可购买充值档位列表，仅返回金额不低于 100 元且配置有效的档位。
- `data.plans[].id`（`number`，必填）：充值档位 ID，创建订单时作为 rechargePlanId 原样传入。
- `data.plans[].rechargeAmountFen`（`number`，必填）：充值金额，单位为分。
- `data.plans[].rechargeAmount`（`number`，必填）：充值金额，单位为元。
- `data.plans[].unitPriceFen`（`number`，必填）：该档位单次核销价格，单位为分。
- `data.plans[].unitPrice`（`number`，必填）：该档位单次核销价格，单位为元。
- `data.plans[].pricingMode`（`number`，必填）：该档位计价模式：1 为历史账号倍率，2 为固定单价。
- `data.plans[].buyCount`（`number`，必填）：支付成功后增加的可核销次数。
- `data.plans[].sortOrder`（`number`，必填）：档位展示顺序，数值越小越靠前。

#### 响应示例

##### 查询成功

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "traceId": "recharge-plans-trace",
  "data": {
    "shopId": 8673891,
    "unitPriceFen": 15,
    "unitPrice": 0.15,
    "pricingMode": 2,
    "plans": [
      {
        "id": 42,
        "rechargeAmountFen": 30000,
        "rechargeAmount": 300,
        "unitPriceFen": 15,
        "unitPrice": 0.15,
        "pricingMode": 2,
        "buyCount": 2000,
        "sortOrder": 1
      }
    ]
  }
}
```

##### 门店无权限

```json
{
  "success": false,
  "code": 10001,
  "message": "门店不存在或无权限",
  "traceId": "recharge-plans-trace",
  "data": null
}
```

#### 补充说明

- **先查询再创建订单**：充值金额和次数以后端返回档位为准。调用方应保存用户选择的 data.plans[].id，并在创建订单时作为 rechargePlanId 传入。
- **常见错误**：HTTP 400 通常表示 shopId 类型或取值错误；HTTP 403 表示账号状态异常；HTTP 404 表示门店不存在或无权限；HTTP 500 表示充值数据暂时不可用。失败时 data 为 null。

### 创建充值订单

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/recharge/order/create`
- 在线调试风险：会改变业务状态；调用前必须确认：本次请求会改变业务状态并创建一笔 30 分钟有效的真实待支付订单：确认 shopId 与 rechargePlanId 来自同一次档位查询，并同意创建订单。
- 授权：请求头需携带登录接口签发的 Authorization: Bearer <V2 JWT>。
- 说明：按查询接口返回的充值档位创建 30 分钟有效的待支付订单，并返回可渲染为二维码的 wxPayQrCodeUrl。本接口只创建订单，不直接调用微信支付。

#### Headers

- `Authorization`（`string`，必填）：Bearer <V2 JWT>，由 V2 登录接口签发

#### JSON Body 参数

- `shopId`（`number`，选填）：无限聚核系统商户 ID（store.id）；省略或传 0 表示当前管理员主账户，必须与查询充值档位时的目标一致；默认值：0
- `rechargePlanId`（`number`，必填）：充值档位 ID，必须取自同一 shopId 调用查询可充值金额接口返回的 data.plans[].id

#### 请求示例

##### 创建主账户充值订单

```json
{
  "shopId": 0,
  "rechargePlanId": 42
}
```

##### 创建门店充值订单

```json
{
  "shopId": 8673891,
  "rechargePlanId": 42
}
```

#### 响应字段

- `success`（`boolean`，必填）：业务是否成功；调用方应以该字段作为成功判断依据。
- `code`（`number`，必填）：统一业务码；成功时为 10000。
- `message`（`string`，必填）：业务结果说明。
- `traceId`（`string`，必填）：本次请求链路 ID，排查问题时请一并提供。
- `data.orderNo`（`string`，必填）：充值订单号。订单默认 30 分钟有效，支付和查询时应保持原值。
- `data.rechargeAmountFen`（`number`，必填）：订单充值金额，单位为分。
- `data.buyCount`（`number`，必填）：支付成功后增加的可核销次数。
- `data.wxPayQrCodeUrl`（`string`，必填）：现有小程序充值页 startapp 链接。调用方应把完整 URL 原样渲染成二维码，不能修改其中的 orderNo。

#### 响应示例

##### 订单创建成功

```json
{
  "success": true,
  "code": 10000,
  "message": "success",
  "traceId": "recharge-order-trace",
  "data": {
    "orderNo": "C17202608201530001235",
    "rechargeAmountFen": 30000,
    "buyCount": 2000,
    "wxPayQrCodeUrl": "https://hx.umember.cn/startapp?path=recharge&channel=scan&orderNo=C17202608201530001235"
  }
}
```

##### 充值档位不存在

```json
{
  "success": false,
  "code": 10001,
  "message": "充值档位不存在",
  "traceId": "recharge-order-trace",
  "data": null
}
```

#### 补充说明

- **完整支付与到账链路**：服务端创建待支付订单并返回 wxPayQrCodeUrl → 调用方把完整 URL 渲染为二维码 → 用户扫码进入现有微信小程序充值页并完成支付 → 现有支付回调处理订单并自动把 buyCount 对应次数充入目标主账户或门店。本接口不接入微信支付证书、私钥或支付回调。
- **二维码与订单有效期**：必须对 wxPayQrCodeUrl 完整字符串编码生成二维码，不要自行拼接或替换 orderNo。订单创建后 30 分钟内未支付会失效，失效后应重新查询档位并创建新订单。
- **常见错误**：HTTP 400 通常表示 shopId/rechargePlanId 类型或档位配置错误；HTTP 403 表示账号状态异常；HTTP 404 表示门店无权限或充值档位不存在；HTTP 500 表示订单创建或充值数据暂时不可用。失败时 data 为 null。

## 接口详情

以下为 Hexiao V2 全部开放接口说明。

## 美团接口调用方式

以下接口统一通过 V2 网关调用。请求体与响应业务字段以每个接口挂载的美团官方文档为准；调用方无需传美团 Token 或签名。

### 公共请求格式

```http
POST https://newopen.elys.cn/api/hexiao/v2/meituan/ddzh/tuangou/receipt/prepare
Authorization: Bearer <key>
X-Store-Id: <无限聚核系统门店 ID（store.id）>
Content-Type: application/json

<按对应美团官方文档传递的 JSON 请求体>
```

## 美团接口

已将美团原始 Path 与 V2 网关前缀完整拼接。点击每个接口中的“查看美团官方文档”可查看对应业务入参与响应说明。

### 查询已验券信息

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/meituan/ddzh/tuangou/receipt/getconsumed`
- 在线调试风险：只读请求
- 授权：统一传 `Authorization: Bearer <V2 Token>` 与 `X-Store-Id: <store.id>`。
- 说明：美团原始接口 `/ddzh/tuangou/receipt/getconsumed`。[查看美团官方文档](https://developer.meituan.com/docs/api/ddzh-tuangou-receipt-getconsumed)
- 美团官方文档：https://developer.meituan.com/docs/api/ddzh-tuangou-receipt-getconsumed

#### Headers

- `Authorization`（`string`，必填）：Bearer <V2 Token>，由本地授权自动注入
- `X-Store-Id`（`number | string`，必填）：无限聚核系统门店 ID（store.id）

#### JSON Body 参数

- `receiptCode`（`string`，必填）：团购券码

### 手机号查询可用团购券

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/meituan/ddzh/tuangou/receipt/querybymobile`
- 在线调试风险：只读请求
- 授权：统一传 `Authorization: Bearer <V2 Token>` 与 `X-Store-Id: <store.id>`。
- 说明：美团原始接口 `/ddzh/tuangou/receipt/querybymobile`。[查看美团官方文档](https://developer.meituan.com/docs/api/ddzh-tuangou-receipt-querybymobile)
- 美团官方文档：https://developer.meituan.com/docs/api/ddzh-tuangou-receipt-querybymobile

#### Headers

- `Authorization`（`string`，必填）：Bearer <V2 Token>，由本地授权自动注入
- `X-Store-Id`（`number | string`，必填）：无限聚核系统门店 ID（store.id）

#### JSON Body 参数

- `mobile`（`string`，必填）：点评用户绑定的手机号 国内手机即11位手机号，国外手机格式为【地区码_手机号】 例如国内手机：13866666666，国外手机：65_666666
- `dealGroupId`（`long`，必填）：团购ID
- `dealId`（`long`，必填）：套餐ID
- `offset`（`int`，必填）：起始位置，从0开始
- `limit`（`int`，必填）：查询数量，最大不超过5；默认值：5
- `platform`（`int`，必填）：平台：1-大众点评，2-美团；默认 2（美团）；默认值：2

## 抖音原生接口调用方式

4 项补充能力通过 V2 网关调用：商品保存、券状态查询、券状态批量查询与订单查询。验券、核销和撤销请使用核销聚合接口。调用方只传无限聚核凭证、系统门店 ID 和业务参数；抖音凭证与绑定账户由服务端处理。订单查询范围为商户根账户，需包含该接口的服务端版本。

### 商品保存

```http
POST https://newopen.elys.cn/api/hexiao/v2/douyin/goodlife/v1/goods/product/save/
Authorization: Bearer <key>
X-Store-Id: <无限聚核系统门店 ID（store.id）>
Content-Type: application/json
Idempotency-Key: <同一次商品保存必须复用>

<按抖音商品模板构造的完整官方 JSON 请求体>
```

### 订单查询（需服务端包含本接口的版本）

```http
GET https://newopen.elys.cn/api/hexiao/v2/douyin/goodlife/v1/trade/order/query/?page_num=1&page_size=20
Authorization: Bearer <无限聚核凭证>
X-Store-Id: <系统门店ID>
X-Request-Id: <请求追踪ID>
```

## 抖音原生接口

提供商品保存、券状态查询、券状态批量查询和订单查询。验券、核销与撤销请使用核销聚合接口。官方尾斜杠属于路径的一部分；抖音凭证与商户上下文由服务端绑定注入。订单查询为商户根账户范围，需服务端包含该接口的版本。

### 创建或更新商品

- 方法：`POST`
- URL：`https://newopen.elys.cn/api/hexiao/v2/douyin/goodlife/v1/goods/product/save/`
- 在线调试风险：会改变业务状态；调用前必须确认：本次请求会改变业务状态并创建或更新抖音商品：确认门店、out_id、商品模板字段、测试标记和幂等键无误，并同意立即提交。
- 授权：传 `Authorization: Bearer <V2 Token>`、`X-Store-Id: <store.id>` 与 `Idempotency-Key`；平台 Token、商户账户 Header 和测试数据访问 Header 由服务端注入。
- 说明：调用抖音官方商品保存接口；同一服务商下相同 out_id 会创建或更新同一商品。应用必须具备 life.capacity.goods.found 权限。
- 抖音官方文档：https://developer.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/goods/save

#### Headers

- `Authorization`（`string`，必填）：Bearer <V2 Token>，由本地授权自动注入
- `X-Store-Id`（`number | string`，必填）：无限聚核系统门店 ID（store.id）
- `Idempotency-Key`（`string`，必填）：同一次商品保存必须复用；新的创建或更新操作使用新值

#### JSON Body 参数

- `account_id`（`string`，选填）：通常省略并由服务端注入；如传入必须匹配当前门店绑定商户
- `product`（`object`，必填）：商品主体；字段取决于行业、类目和商品模板
- `ability`（`object`，选填）：商品保存能力开关，按抖音官方模板传入
- `sku`（`object`，选填）：单 SKU 商品必传；预约商品按官方多 SKU 流程处理
- `skus`（`object[]`，选填）：综合行业预约品按官方要求传入

#### 补充说明

- 商品字段必须先通过抖音商品模板接口确定；小程序未验收前按官方要求使用 test_flag=true。
- 服务端调用抖音时固定注入 Rpc-Persist-Life-Test-Data-Access: all，调用方无需也不能覆盖。
- 结果 unknown 时禁止自动重试或更换 Idempotency-Key；先按 out_id 查询平台商品状态。

### 券状态查询

- 方法：`GET`
- URL：`https://newopen.elys.cn/api/hexiao/v2/douyin/goodlife/v1/fulfilment/certificate/get/`
- 在线调试风险：只读请求
- 授权：传 `Authorization: Bearer <V2 Token>` 与 `X-Store-Id: <store.id>`。
- 说明：使用验券准备实时返回的 encrypted_code 查询单张券状态。
- 抖音官方文档：https://partner.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/life.capacity.fulfilment/certificate.get

#### Headers

- `Authorization`（`string`，必填）：Bearer <V2 Token>，由本地授权自动注入
- `X-Store-Id`（`number | string`，必填）：无限聚核系统门店 ID（store.id）

#### Query 参数

- `encrypted_code`（`string`，必填）：必须取自验券准备接口的实时返回值，不接受券码明文
- `account_id`（`string`，选填）：通常省略并由服务端注入；如传入必须匹配当前门店

### 券状态批量查询

- 方法：`GET`
- URL：`https://newopen.elys.cn/api/hexiao/v2/douyin/goodlife/v1/fulfilment/certificate/query/`
- 在线调试风险：只读请求
- 授权：传 `Authorization: Bearer <V2 Token>` 与 `X-Store-Id: <store.id>`。
- 说明：按 encrypted_code 或 order_id 查询券列表；两者二选一且不能同时传。
- 抖音官方文档：https://partner.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/life.capacity.fulfilment/certificate.query

#### Headers

- `Authorization`（`string`，必填）：Bearer <V2 Token>，由本地授权自动注入
- `X-Store-Id`（`number | string`，必填）：无限聚核系统门店 ID（store.id）

#### Query 参数

- `encrypted_code`（`string`，选填）：验券准备返回的加密券码；与 order_id 二选一
- `order_id`（`string`，选填）：抖音订单 ID；与 encrypted_code 二选一
- `account_id`（`string`，选填）：通常省略并由服务端注入；如传入必须匹配当前门店

### 订单查询

- 方法：`GET`
- URL：`https://newopen.elys.cn/api/hexiao/v2/douyin/goodlife/v1/trade/order/query/`
- 在线调试风险：只读请求
- 授权：传 Authorization: Bearer <无限聚核凭证> 与 X-Store-Id: <系统门店ID>；抖音 client_token、access-token Header 和商户上下文由服务端注入。
- 说明：按单个订单号查询详情，或按状态、用户及时间查询当前绑定商户的订单列表。查询范围是来客商户根账户，不保证单门店隔离。仅在服务端部署包含本接口的版本后可用；文档展示不代表后端已发布。
- 抖音官方文档：https://partner.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/order.query/query

#### Headers

- `Authorization`（`string`，必填）：Bearer <无限聚核凭证>，使用无限聚核 API key 或当前接口支持的 V2 Token，不是抖音 token
- `X-Store-Id`（`number | string`，必填）：无限聚核系统门店 ID（store.id）；服务端校验访问权限并读取绑定的抖音商户上下文
- `X-Request-Id`（`string`，选填）：可选请求追踪 ID，用于关联本次请求日志

#### Query 参数

- `page_num`（`integer`，必填）：页码从 1 开始；使用页码分页时 page_num × page_size 不得超过 10000。使用 cursor 时仍必填。；默认值：1
- `page_size`（`integer`，必填）：每页 1～100 条；使用 cursor 时仍必填。；默认值：20
- `order_id`（`string`，选填）：单个抖音生活服务订单 ID；使用单数字段 order_id，不支持 order_ids 批量参数
- `ext_order_id`（`string`，选填）：开发者系统中的订单号，可用于查询对应订单详情
- `open_id`（`string`，选填）：抖音内部用户标识，用于查询该用户在当前商户下的订单
- `order_status`（`integer`，选填）：订单维度状态；1 表示已完成，包含完成履约或全部退款，不能单独证明核销成功；可选值：0（初始化）、100（待支付）、101（支付取消）、150（部分支付）、200（已支付）、201（待使用）、1（已完成）
- `create_order_start_time`（`integer`，选填）：创单起始时间，秒时间戳；必须与 create_order_end_time 成对传入
- `create_order_end_time`（`integer`，选填）：创单结束时间，秒时间戳；必须与 create_order_start_time 成对传入
- `update_order_start_time`（`integer`，选填）：修改起始时间，秒时间戳；必须与 update_order_end_time 成对传入
- `update_order_end_time`（`integer`，选填）：修改结束时间，秒时间戳；必须与 update_order_start_time 成对传入
- `cursor`（`string`，选填）：首次传 0；随后将 search_after.CursorValue 字符串数组用逗号拼接，例如 0,1。cursor 优先于页码分页，但 page_num、page_size 仍必填。

#### 请求示例

##### 分页查询（占位凭证）

```bash
curl -X GET 'https://newopen.elys.cn/api/hexiao/v2/douyin/goodlife/v1/trade/order/query/?page_num=1&page_size=20' \
  -H 'Authorization: Bearer <无限聚核凭证>' \
  -H 'X-Store-Id: <系统门店ID>' \
  -H 'X-Request-Id: <请求追踪ID>'
```

##### 按单个订单查询（占位参数）

```bash
curl -X GET 'https://newopen.elys.cn/api/hexiao/v2/douyin/goodlife/v1/trade/order/query/?page_num=1&page_size=20&order_id=<抖音订单ID>' \
  -H 'Authorization: Bearer <无限聚核凭证>' \
  -H 'X-Store-Id: <系统门店ID>' \
  -H 'X-Request-Id: <请求追踪ID>'
```

#### 响应字段

- `data`（`object`，选填）：抖音原生业务数据，字段保持原样
  - `error_code`（`integer`，必填）：业务错误码，0 表示成功；与 extra.error_code 一并检查
  - `description`（`string`，必填）：业务错误说明
  - `orders`（`object[]`，选填）：当前商户根账户范围内的订单列表
    - `order_id`（`string`，选填）：抖音订单 ID
    - `order_status`（`integer`，选填）：订单状态；1 包含履约或全部退款，已履约后退款时也可能保持 1
    - `poi_id`（`string`，选填）：下单关联门店；可能取最近店或随机门店，不是核销门店或订单归属证明
    - `merchant_info`（`object`，选填）：商户信息，包含 account_id 与 account_name
    - `certificate`（`object[]`，选填）：券明细；certificate_id 是券标识，item_status 是券状态，不返回明文券码
    - `products`（`object[]`，选填）：商品信息，是否返回依平台业务场景
    - `amount_info`（`object`，选填）：餐饮金额信息；金额字段按官方约定使用分，其他行业查看 sub_order_amount_infos
    - `sub_order_amount_infos`（`object[]`，选填）：子单金额信息
    - `create_order_time`（`integer`，选填）：创单时间，秒时间戳
    - `update_order_time`（`integer`，选填）：最新修改时间，秒时间戳
    - `pay_time`（`integer`，选填）：支付时间，秒时间戳
  - `page`（`object`，选填）：页码信息，包含 page_num、page_size、total
  - `search_after`（`object`，选填）：游标信息
    - `CursorValue`（`string[]`，选填）：下一次请求使用的游标数组，转换成逗号拼接的 cursor
    - `AllCursorValue`（`string[][]`，选填）：每条记录的游标
    - `Size`（`integer`，选填）：滚动大小
- `extra`（`object`，必填）：抖音原生扩展信息
  - `error_code`（`integer`，必填）：错误码，0 表示成功
  - `description`（`string`，必填）：错误说明
  - `sub_error_code`（`integer`，必填）：子错误码
  - `sub_description`（`string`，必填）：子错误说明
  - `logid`（`string`，必填）：抖音日志 ID，排查时保留
  - `now`（`integer`，必填）：平台调用时间，以平台返回值为准

#### 响应示例

##### HTTP 200：成功结构示例（非真实业务数据）

```json
{
  "data": {
    "error_code": 0,
    "description": "",
    "orders": [],
    "page": {
      "page_num": 1,
      "page_size": 20,
      "total": 0
    }
  },
  "extra": {
    "error_code": 0,
    "description": "",
    "sub_error_code": 0,
    "sub_description": "",
    "logid": "<抖音日志ID>",
    "now": 1789603200
  }
}
```

##### HTTP 200：原生错误结构示例（非真实业务数据）

```json
{
  "data": {
    "error_code": 2119005,
    "description": "应用未获商家授权"
  },
  "extra": {
    "error_code": 2119005,
    "description": "应用未获商家授权",
    "sub_error_code": 0,
    "sub_description": "",
    "logid": "<抖音日志ID>",
    "now": 1789603200
  }
}
```

#### 补充说明

- account_id 由 X-Store-Id 对应的门店绑定注入，不允许任意切换商户。当前绑定值是否为来客根账户仍需在实际授权联调中确认；访问某门店不代表平台只返回该门店订单。
- 请求不支持 poi_id 筛选。响应 poi_id 是下单关联门店，可能取最近门店或随机门店，不能作为真实核销门店或订单归属证明；poi 门店对象仅外卖订单返回。
- 应用需要 life.capacity.order.query 能力及商家授权。抖音应用 client_token 由服务端通过 access-token Header 使用，调用方只提供无限聚核凭证。
- 分页超过 10000 条时使用 cursor。返回数量小于 page_size 或不再返回 search_after 时结束；单订单详情使用 order_id 或 ext_order_id，列表查询按状态或 open_id 搭配创单/修改时间范围。
- 时间范围使用秒时间戳，创单、修改起止时间各自成对传入。官方未说明最大回溯跨度及时间边界包含规则；贴近订单完成时间查询时可延长 2～3 秒或重试。单应用默认 QPS 为 20。
- 不支持酒店订单，不支持以券维度查询，也不返回券码明文。订单状态 1 不能单独证明核销成功，应结合 certificate.item_status 等明细；仅退款信息请按官方售后接口查询。
- 原生响应及错误保持透传，不包装为聚合接口的 success 结构。即使 HTTP 200，也须检查 data.error_code 与 extra.error_code；保留 extra.logid。常见错误：2190004 无接口能力、2119005 未获商家授权、2190002/2190008 token 无效或过期、2119003 请求频繁。
- 默认免费，可按现有固定接口路径配置计费；不需要 Idempotency-Key，不创建 operation。

## 推荐调用顺序

按场景串联接口，减少无效重试。

### 核销流程

1. 从开发者后台获取 API key，并配置 Authorization: Bearer <key>
2. 准备具体门店 shopId（系统门店 ID，不支持商户）；美团和抖音首次授权均调用 get/auth/url 获取授权链接，再由商家完成对应平台授权
3. 每次新核销都先使用具体门店 shopId 调用 ddzh-tuangou-receipt-prepare 验券；美团和抖音都保存成功响应的 data.ticketInfo，并在 consume 中整段原文回传
4. 业务方按券码和业务订单完成去重后，再使用与预核销相同的具体门店 shopId 调用 ddzh-tuangou-receipt-consume；重复请求命中已成功订单时返回 10500 已核销提示
5. 核销成功后按 data[i] 保存 consumeCredential[i]；需要撤销时调用 ddzh-tuangou-receipt-cancel，并逐项检查 succeeded、rejected 或 unknown

### 查询在售

1. 从开发者后台获取 API key，并配置 Authorization: Bearer <key>
2. 使用具体门店 shopId 调用 ddzh-tuangou-deal-queryshopdeal 拉取该门店的团购列表，不支持商户

### 充值流程

1. 使用 V2 登录接口签发的 JWT 配置 Authorization: Bearer <V2 JWT>
2. 调用 recharge/plans 查询主账户或指定商户的可用充值档位
3. 让用户选择档位，并把同一 shopId 与 data.plans[].id 作为 rechargePlanId 调用 recharge/order/create
4. 把返回的 wxPayQrCodeUrl 完整渲染为二维码，不修改 orderNo 或其他查询参数
5. 用户扫码进入现有微信小程序充值页完成支付，现有支付回调确认成功后自动把 buyCount 对应次数充入目标账户

## 常见问题

排错时可按下列项自查。

### Bearer key 无效

确认 key 来自开发者后台、未过期且复制完整；Authorization 请求头只保留一个 Bearer 前缀。

### 提示缺少授权

确认请求头已带 Authorization: Bearer <key>。

### ticketInfo 从哪里来

美团和抖音核销均必传。ticketInfo 取自同一具体门店 shopId、同一平台、同一券码调用「输码验券校验」后的 success=true 响应字段 data.ticketInfo。核销时必须整段原文复制；不得自行构造、解析、修改、删减或重新序列化。

### consumeCredential 从哪里来

由新版 V2 聚合核销成功响应的顶层 consumeCredential[] 返回，并与 data[i] 一一对应。需要撤销时必须按原门店和平台原样传入；历史无凭证记录不能使用聚合撤销。

### 返回「当前 shopId 对应主门店」

授权、团购列表、预核销、核销四接口仅支持具体门店。传入商户 shopId 时统一返回 HTTP 400、success=false、code=10001、data=null，并携带 traceId；message 为「当前 shopId 对应主门店，该接口仅支持子门店，请更换为具体子门店的 shopId」。请更换为需要授权、查询或核销的具体门店 shopId。

### 返回「门店不存在」

调用授权、团购列表、预核销、核销四接口时，核对 shopId 是否为具体门店的系统门店 ID，并确认当前用户对该门店有访问权限。

### 返回「未找到可用的服务商门店映射」

调用授权、团购列表、预核销、核销四接口时，检查 shopId 对应的具体门店是否完成对应平台配置、授权或初始化。

## 技术支持

扫码添加微信，获取接入帮助
