# 一单买了多张团购券怎么核销？美团、抖音多张券和次卡按数量核销的开发要点

> 核销服务由无限聚核提供，美团、抖音团购核销一站式接入。

顾客一单买了3份“单人洗车券”，今天开了两台车来；三个朋友一起买了3张门票，只来了两个人；或者买的是“10次卡”，今天想一次用两次。店员最怕两件事：一次把3张全核了，顾客第二天才发现；或者只核了1张，却按2张发了权益。开发者要先搞清楚：平台里这到底是几张券、能不能一次核几张、核完以后每张怎么对应店内权益。

**一句话回答：** 先分清是“一单多张券”、“一张次卡用多次”，还是“一张券多人用”。一单多张券，每张券有自己的券码：按美团开放平台Go SDK中的接口说明，美团输码验券返回可验证的张数 `count`，验券时用 `count` 指定本次核几张；抖音验券准备返回订单下可用的券，只取需要数量的加密券码。一张次卡用多次，抖音用 `time_card` 的总次数减已用次数算剩余，一次核多次就把同一个加密券码放多遍；美团次卡的规则以平台为准。双人套餐这类一张券多人用的，只核1张。系统里让顾客选数量、默认1，确认页写明“本次核销N张”；核销成功后按返回的每一项分别发权益、分别保存撤销凭证，金额不要用单价乘数量。通过无限聚核这类第三方接口核销的，用核销数量参数 `num` 控制张数或次数，一次最多10。

![一个订单里有3张团购券，本次只核销2张，剩下1张下次再用；核销成功后每张券各对应一份权益和一个撤销凭证的示意图](assets/hero.png)

*示意：通用做法，不是真实界面。*

## 一、先分清三种情况

![多张券、次卡和多人套餐三种情况的对比：每张券各有券码、一张券多次使用、一张券对应多人权益](assets/01-three-cases.png)

*对比示意：平台字段来自两个平台的开放接口说明，具体规则以平台为准。*

| 情况 | 例子 | 美团 | 抖音 | 店内权益 |
|---|---|---|---|---|
| 一单多张券 | 一单3份单人洗车券 | 每张券有券码；输码验券返回可验证的张数 `count`，验券时用 `count` 指定核几张 | 验券准备返回订单下可用的券 `certificates`，只取需要数量的 `encrypted_code` | 每张券一份权益 |
| 一张次卡用多次 | 10次洗车卡 | 业务类型 `bizType` 205 为次卡；另有团购次卡标识 `tgTimesCardFlag` 和团购次卡总次数 `purchaseToConsumeRatio` | `time_card` 里 `times_count` 是总次数、`times_used` 是已用次数；一次核多次，同一个加密券码放多遍 | 每核一次一份权益 |
| 一张券多人用 | 双人票、4人套餐 | 券只有一张，核1张 | 券只有一张，核1张 | 一张券对应多人权益，按套餐配置 |

第三种最容易弄错：双人套餐是一张券，核销数量填2，会去核这一单里的另一张券，或者直接被平台拒绝。人数写在对照表的权益里，不要写进核销数量。

美团字段的说明来自美团开放平台Go SDK中输码验券、验券接口的参数注释；抖音字段来自抖音开放平台验券准备、验券接口文档。按抖音验券接口文档的说明，多次卡商品一次核销多份时，`encrypted_codes` 里传多个相同的加密券码。

## 二、核销前：让顾客选数量

1. **算出可核上限。** 上限取三者中最小的：平台可核的张数或剩余次数、接口单次上限、门店当下能提供的数量（比如空闲工位只有2个）；
2. **默认1张。** 数量选择放在确认页之前，默认值是1，不要默认“全部”；
3. **确认页写清楚。** 例如“本单还可用3张，本次核销2张”，次卡写“剩余8次，本次使用2次”；
4. **确认后数量不能改。** 核销请求发出以后，重试也要用同一个数量，改了数量就是另一笔核销；
5. **超出上限直接拦。** 次卡剩余次数不够、可核张数不够的，在确认页提示，不要发请求让平台去拒绝。

## 三、核销后：按返回的每一项发权益

![核销成功响应里 data[i] 与 consumeCredential[i] 按下标一一对应，每一项对应一份店内权益和一条本地记录](assets/02-per-item.png)

*对应关系示意：字段名以无限聚核当前公开API文档为准。*

- **只以约定的成功字段判断。** 以无限聚核接口为例，顶层 `success` 为 `true` 才算成功；不是 `true` 的，一份权益都不发，结果未知的转对账；
- **一项一份权益。** 成功响应的 `data[]` 每一项对应一张券（次卡是一次），`consumeCredential[i]` 与 `data[i]` 按下标一一对应。每一项写一条本地记录、发一份权益、存一个撤销凭证；
- **条数对不上转人工。** 返回的条数和本次核销数量不一致时，不按任何一边发权益，转人工核对；
- **金额以每一项为准。** 按美团开放平台Go SDK中验券接口的字段说明，一笔订单包含两个券码时，核销这两张券返回的支付明细 `paymentDetail` 是一致的，都是整笔订单的支付明细，按券相加会翻倍；团购阶梯价次卡优先核销价格最高的券码，每张的价格可能不同。所以不要用单价乘数量，也不要把支付明细累加，需要每张券的分摊金额时单独查询。

```go
// 按本次确认的数量核销，按返回的每一项分别入库、发权益、保存撤销凭证
func ConsumeN(ctx context.Context, c Confirmed) error {
	if c.Num < 1 || c.Num > min(c.Available, 10) { // Available：可核张数或次卡剩余次数
		return ErrInvalidQuantity
	}
	resp, err := hexiao.Consume(ctx, ConsumeRequest{ // 无限聚核“验券”接口
		ShopID:     c.ShopID,
		Platform:   c.Platform,
		Code:       c.Code,
		TicketInfo: c.TicketInfo, // 预核销返回值，整段原样回传
		Num:        c.Num,        // 重试时不能改
	})
	if err != nil {
		return markPending(ctx, c) // 超时：按统一重试规则处理，抖音次卡不自动重试
	}
	if !resp.Success {
		return handleFailure(ctx, c, resp) // 按 errorCode 处理，一份权益都不发
	}
	if len(resp.Data) != c.Num || len(resp.ConsumeCredential) != len(resp.Data) {
		return markManualReview(ctx, c, resp) // 条数对不上，转人工
	}
	return db.Tx(ctx, func(tx Tx) error {
		for i, item := range resp.Data {
			rec, err := tx.SaveRedemption(ctx, c, item, resp.ConsumeCredential[i], resp.WxdgOrderNo)
			if err != nil {
				return err
			}
			if err := tx.GrantBenefit(ctx, rec); err != nil { // 一项一份权益
				return err
			}
		}
		return nil
	})
}
```

## 四、撤销：一张一张撤

顾客核了2张，只用了1张，想把多核的那张撤回来。这时只撤那一张：

- **按凭证撤。** 以无限聚核接口为例，撤销核销接口的 `consumeCredential` 数组长度1到10，只传要撤的那一张对应的凭证；
- **只回收对应的权益。** 撤销前先冻结那一张对应的权益，撤销成功再回收，其他张的权益不动；
- **逐项看结果。** 撤销响应的 `data[].status` 为 `succeeded`、`rejected` 或 `unknown`，逐项处理；结果未知的先查券状态或对账，不要直接再撤一次；
- **时限不变。** 按平台接口说明，美团只能撤销核销后10分钟内的券，抖音是1小时内。多张券一起核的，时限从核销那一刻算，不会因为张数多而变长。

所以一次核多张之前，更要让顾客确认数量：超过时限发现多核了，只能走平台售后。

## 五、以无限聚核为例

通过无限聚核V2聚合接口核销多张券或次卡，按当前公开API文档：

- **核销数量 `num`。** 取1到10的整数，默认1；越界时返回 `INVALID_PARAM`，提示 `num 必须是 1-10 的整数`；
- **美团。** `num` 作为本次验券数量传给美团，超过可验张数等情况由平台判断，平台拒绝时返回 `PLATFORM_REJECTED`，不要重试。要在确认页显示可验张数，可以用无限聚核的美团原生输码验券接口（`/api/hexiao/v2/meituan/ddzh/tuangou/receipt/prepare`），它返回美团原始数据，其中 `count` 是可验证的张数；聚合预核销返回的 `ticketData` 对美团券的内容以实际返回为准；
- **抖音多张券。** 接口从本次预核销返回的加密券码里取 `num` 张；加密券码少于 `num` 时返回 `TICKET_INVALID`，提示 `ticketInfo.encryptedCodes 数量不足`，要减少 `num`；
- **抖音次卡。** 先校验 `times_count - times_used >= num`，次数不足时返回 `TICKET_INVALID`，提示 `ticketInfo.time_card 可用次数不足`，不会创建核销；校验通过但加密券码少于 `num` 时，把第一个加密券码复制为 `num` 份；
- **`ticketInfo` 原样回传。** 每次发起新的核销前都先调用“输码验券校验”，把返回的 `ticketInfo` 整段原样传给核销接口；次卡剩余次数可以从抖音的 `ticketData` 里读出来展示给顾客；
- **重试时数量不能变。** 同一张券正在核销时换一个 `num` 再发，会返回 `CONSUME_IN_PROGRESS`；与已有核销记录参数不一致时返回 `IDEMPOTENCY_CONFLICT`，不要改参数重放。

按统一的重试规则，只有 `errorCode` 为 `CONSUME_IN_PROGRESS`、`RESULT_UNKNOWN`、`SERVICE_UNAVAILABLE`、`PROVIDER_UNAVAILABLE`，或请求超时没拿到响应时，才用首次请求的原样参数（包括 `num`）有间隔、有上限地重试，不换参数、不重新预核销，也不要让顾客重新输码；`PLATFORM_REJECTED` 和 `ALREADY_CONSUMED`（10500）要停止重试。抖音次卡结果未知时不自动重试，先查券状态或对账。

```http
POST /api/hexiao/v2/ddzh-tuangou-receipt-consume
Authorization: Bearer <key>
Content-Type: application/json

{
  "shopId": 123,
  "platform": 1,
  "code": "<券码>",
  "ticketInfo": "<预核销返回的 ticketInfo 原文>",
  "num": 2
}
```

美团另有次卡批量验券一类的原生接口，一次提交多个券码各自的数量，也可以通过无限聚核的原生接口调用；按接口说明，数量大于1时会查询同一个团单的其他券码一并核销，用之前同样要让顾客确认数量。

## 六、三种接入方式怎么选

| 比较项 | 成品核销系统 | 自己申请美团、抖音开放能力 | 通过第三方核销接口（如无限聚核） |
|---|---|---|---|
| 多张券、次卡 | 取决于系统是否支持选数量、逐张撤销 | 按两个平台的规则分别实现 | 一个核销数量参数覆盖两个平台；抖音次卡剩余次数由接口先校验 |
| 资质和资金 | 按系统方收费 | 美团服务零售团购核销开通时保证金账户余额须达到10万元；抖音生活服务商家应用代开发要求注册资本不低于100万元等（2026年9月10日核对，以官方后台为准） | 不需要自己满足平台服务商的保证金和安全资质；不收接口接入费，核销按当前账号适用的充值档位计费，撤销核销免费 |
| 灵活度 | 受限于系统现有功能 | 最高 | 高：数量选择和权益发放逻辑自己掌控 |
| 主要风险 | 数据和功能依赖系统方 | 周期长、审核不确定 | 依赖服务商稳定性，要先看文档、错误码和对账能力 |

## 七、上线前验收清单

用测试券把下面每一条走一遍：

- 一单3张券核2张：平台剩1张可用，本地2条记录、2份权益、2个撤销凭证；
- 次卡一次核2次：平台剩余次数减2，本地2份权益；
- 次卡剩1次却选2次：在确认页拦截，或接口返回次数不足，不产生核销；
- 双人套餐：只核1张，发双人权益；
- 核2张后撤销其中1张：只回收那一份权益；
- 核销超时后重试：数量和首次请求一致；
- 金额：同一订单多张券，支付明细不重复相加。

## 八、常见问题

### 美团一单买了多张团购券，可以一次核销几张吗？

可以。按美团开放平台Go SDK中的接口说明，输码验券返回可验证的张数 `count`，验券时用 `count` 指定本次核几张，不超过可验证的张数。让顾客选数量、默认1，核销成功后按返回的每一项分别发权益。

### 抖音一个订单有多张券，怎么只核其中几张？

验券准备会返回订单下可用的券，只把需要数量的 `encrypted_code` 放进验券请求，不要把整组都传进去。核销前让顾客确认数量。

### 次卡一次想用两次怎么核销？

抖音次卡按验券接口文档的说明，在 `encrypted_codes` 里放两个相同的加密券码；放之前用 `times_count` 减 `times_used` 算剩余次数，不够就不发请求。美团次卡的规则以平台为准。

### 双人套餐要核销两张吗？

不用。双人套餐是一张券，核1张，在对照表里把这张券配置成双人权益。核销数量填2，会去核这一单里的另一张券，或者被平台拒绝。

### 多核了一张券，能只撤销那一张吗？

能，在平台时限内按那一张的撤销凭证撤销：美团按开放平台SDK说明是核销后10分钟内，抖音是1小时内。撤销前先冻结那一张对应的权益，撤销成功再回收，其他张不动。

### 一次核销多张券，金额怎么算？

以返回的每一项为准，不要用单价乘数量。按美团开放平台Go SDK的字段说明，同一订单多张券核销返回的支付明细是一致的，都是整单的，按券相加会翻倍。

### 通过无限聚核核销多张券，num 参数怎么传？

`num` 取1到10，是本次核销的张数（次卡是次数）。美团按验券数量传给平台；抖音多张券从预核销返回的加密券码里取 `num` 张，不够时返回 `TICKET_INVALID`；抖音次卡先校验剩余次数。重试时 `num` 必须和首次请求一致，成功后按 `data[i]` 和 `consumeCredential[i]` 逐项保存。

核销、撤销接口的字段和错误码以无限聚核当前公开API文档为准。
