# 小程序扫码核销团购券：美团券码、抖音二维码短链分别怎么处理？

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

顾客到店，不想手输十几位券码，希望在小程序里点一下“扫码”就能验券。做这个功能的开发者，最常卡在几个地方：`wx.scanCode` 扫出来的是什么？扫到一个网址能不能直接传给核销接口？美团和抖音的码要不要分开处理？

**一句话回答：** 扫码只负责把内容读进来，验券和核销仍然在服务端调接口完成。`wx.scanCode` 返回的 `res.result` 分三种处理：去掉空格后是纯数字的，当作券码，和顾客选定的平台一起提交；抖音团购二维码扫出来是 `v.douyin.com` 短链，按抖音开放平台文档，要取跳转后长链接里的 `object_id` 作为 `encrypted_data`，通过无限聚核这类第三方接口时可以把完整短链直接作为券码、`platform=2` 提交；其他网址（包括美团券二维码扫出的网址）不要自己猜格式，提示顾客输入二维码下方的数字券码。不要按长度判断平台，也不要截断短链。

![小程序扫码后，按纯数字、抖音短链、其他内容三种结果分流，再由服务端验券和核销的流程图](assets/hero.png)

*流程示意：按扫码内容分流的通用做法，不是真实界面。*

## 一、先想清楚：谁扫谁

顾客在自己手机上打开团购券，没法再用同一台手机的摄像头去扫它。所以“扫码核销”要先分清场景：

| 场景 | 谁扫谁 | 做法 |
|---|---|---|
| 店员端小程序 | 店员手机扫顾客手机上的券 | 调 `wx.scanCode` 用相机扫 |
| 顾客自助 | 顾客只有一台手机 | 先截图券码页面，在扫码界面从相册选图；或者直接手输券码 |
| 门店固定设备 | 扫码盒、扫码枪扫顾客手机 | 这类设备通常把扫到的内容当键盘输入，页面用输入框接收，回车后提交 |

顾客自助的场景，手输入口一定要保留。截图选图多了一步，不少顾客会直接选择输码。

## 二、wx.scanCode 返回什么

根据微信官方文档（2026年9月28日查看）：

- `scanType` 默认是 `['barCode', 'qrCode', 'wxCode']`，还可以加 `datamatrix`、`pdf417`。团购券一般是二维码或条形码，设成 `['qrCode', 'barCode']` 就够了；
- `onlyFromCamera` 为 `true` 时不允许从相册选图。顾客自助场景不要设成 `true`；
- 成功回调里，`res.result` 是所扫码的内容，`res.scanType` 是码的类型。

小程序端只把 `res.result` 原样发给自己的服务端，不在前端判断平台，更不在前端调用核销接口。核销接口的凭证只能放在服务端。

```javascript
// 小程序端：只负责读码，原样交给自己的服务端
wx.scanCode({
  scanType: ['qrCode', 'barCode'],
  success: (res) => {
    wx.request({
      url: SERVER_BASE_URL + '/coupon/scan', // 自己业务服务端的地址
      method: 'POST',
      data: { raw: res.result, storeId: this.data.storeId },
    });
  },
});
```

## 三、扫出来的三种结果，分别怎么处理

| 扫到的内容 | 示例 | 服务端处理 |
|---|---|---|
| 纯数字（可能带空格） | `1234 5678 9012`（示例） | 去掉空白字符后作为券码，平台由顾客选择或门店默认 |
| 抖音券码短链 | `v.douyin.com/xxxx/` | 按第四节处理，`platform=2` |
| 其他网址 | 美团券二维码扫出的网址等 | 不自己解析，提示“请输入二维码下方的数字券码” |
| 其他文本 | 小程序码、收款码等 | 提示“这不是团购券码” |

**美团券为什么只收数字？** 部分收银系统的帮助文档提到，用摄像头识别美团团购二维码时返回的是一个网址，也可以用二维码下方的纯数字券码核销。这个网址的格式，我们没有在美团开放平台的公开文档里找到说明。自己去截取网址里的某一段，平台一调整就会出错，稳妥的做法是只提交数字券码。

**纯数字为什么要顾客选平台？** 美团和抖音都有纯数字券码，长度并不固定，按位数猜平台迟早会猜错。只开了一个平台的门店，可以默认那个平台；两个平台都开的，页面上放“美团/抖音”两个选项。也不建议两个平台轮流试：错误提示会变得混乱，请求量也翻倍。

```javascript
// 服务端：只按内容分流，不按长度猜平台
function classifyScan(raw) {
  const text = String(raw).trim();
  const digits = text.replace(/\s+/g, '');
  if (/^\d+$/.test(digits)) return { kind: 'digits', code: digits }; // 平台由顾客选择
  let url;
  try { url = new URL(text); } catch { return { kind: 'unknown' }; }
  if (url.protocol === 'https:' && url.hostname === 'v.douyin.com') {
    return { kind: 'douyin-link', code: text, platform: 2 }; // 完整短链，不截断
  }
  return { kind: 'ask-digits' }; // 其他网址：提示输入二维码下方的数字券码
}
```

## 四、抖音二维码短链：取 object_id，不要截断

抖音开放平台“验券准备”接口文档（2026年9月28日查看）写明：

- 二维码扫出来是一个短链，文档示例形如 `v.douyin.com/eHHc1ft/`；
- 访问短链得到长链接，长链接中 `object_id` 参数的值，就是验券准备接口要传的 `encrypted_data`；
- 请仅感知链接中的 `object_id` 参数，其他参数、域名、路径以后都有调整的可能；
- `encrypted_data` 和 `code` 二选一必传；
- 验券准备和验券的调用次数要一一对应。验券成功后，当前二维码会失效，要使用刷新后的二维码再调用。

自己对接抖音开放平台时，解析短链要放在服务端，并做好限制：只接受 `https` 协议、域名为 `v.douyin.com` 的链接，只读取第一跳响应的 `Location`，不要跟随任意跳转，也不要用正则按固定长度去截链接。

```javascript
// 服务端：自己对接抖音开放平台时，从短链取 encrypted_data
async function resolveDouyinObjectId(link) {
  const u = new URL(link);
  if (u.protocol !== 'https:' || u.hostname !== 'v.douyin.com' || u.username || u.password) {
    throw new Error('不是抖音券码短链');
  }
  const res = await fetch(u, { redirect: 'manual', signal: AbortSignal.timeout(3000) });
  const location = res.headers.get('location');
  if (res.status < 300 || res.status >= 400 || !location) throw new Error('短链无法解析');
  const objectId = new URL(location, u).searchParams.get('object_id');
  if (!objectId) throw new Error('短链响应缺少 object_id');
  return objectId; // 作为验券准备接口的 encrypted_data
}
```

通过无限聚核的V2聚合接口时，这一步可以省掉。当前公开API文档写明：“输码验券校验”接口在 `platform=2` 时，`code` 也支持扫码得到的完整 `v.douyin.com` 短链；服务端只访问 `v.douyin.com` 的首跳并提取 `object_id`，解析失败时不会继续请求平台，返回 `INVALID_COUPON_LINK`（code 10001），这时提示顾客改输明文券码即可。文档同样提醒：短链长度不固定，禁止按长度截断。

补三条联调里常见的短链细节（2026-10-17 对照抖音开放平台验券准备说明与无限聚核公开文档）：

1. **短链只是入口，真正核销仍走两段式。** 自己对接时：短链 → `object_id`（`encrypted_data`）→ 验券准备 `certificate.prepare` → 验券确认 `certificate.verify`。通过无限聚核时：完整短链进预核销的 `code`，服务端完成首跳解析与准备，你仍要保存 `ticketInfo` 再核销；
2. **解析失败不要改去试美团。** 返回 `INVALID_COUPON_LINK` 时，提示顾客改输抖音券页面上的明文券码，或刷新二维码再扫；不要把同一短链改 `platform=1` 重试；
3. **日志与缓存。** 完整短链等同于券，日志只留域名与末尾若干字符；不要把短链缓存在小程序本地当“下次还能用”——核销成功后旧码失效，次卡也要重新打开券页刷新二维码。

## 五、扫码之后：验券、确认、核销

扫码只是入口，后面的步骤和手输券码完全一样：

1. **验券。** 服务端用具体门店的 `shopId`、平台和券码（或抖音短链）调用无限聚核的“输码验券校验”（预核销）接口，成功后保存响应里的 `ticketInfo` 原文；
2. **让顾客确认。** 页面显示套餐名称、售卖价和适用门店，顾客点“确认使用”才进入下一步。扫错券、扫到别人的券，都在这一步拦下来；
3. **核销。** 把 `ticketInfo` 整段原样传给核销接口，只以顶层 `success` 判断成功，按下标保存 `consumeCredential`；
4. **发放权益。** 核销成功后再开门、上电或记入会员账户。

抖音券还有一点要提醒顾客：核销成功后，原来的二维码就失效了。次卡下次到店要重新打开券页面，用刷新后的二维码。

## 六、防误扫、防连点、防刷

- **确认按钮防连点。** 核销请求在途时按钮置灰。以无限聚核为例，同一张券的核销在途时再提交，会返回 `CONSUME_IN_PROGRESS`；
- **限制验券频率。** 同一个用户、同一台设备，短时间内连续验券失败多次，就暂停一会儿，防止有人批量试券码；
- **日志不要打印完整券码和完整短链。** 抖音短链等同于券本身，日志里只保留脱敏后的片段和关联ID；
- **友好提示平台拒绝。** 券已使用、已过期或不适用本店时，平台会拒绝，页面按错误码给出对应提示，不要只显示“核销失败”。

顾客实在找不到券码时，还有一个店员端的兜底办法。以无限聚核为例，公开文档开放了美团原生“手机号查询可用团购券”接口，需要传用户绑定的手机号、团购ID和套餐ID，每次最多查5条。它要求店员知道顾客买的是哪个套餐，并且涉及手机号，只适合放在店员端，不适合做成顾客自助入口。

## 七、常见问题

### 小程序能直接识别美团团购券的二维码吗？

能扫，但不建议解析。美团券二维码扫出的网址格式，我们没有在美团开放平台公开文档中找到说明。稳妥的做法是提示顾客输入二维码下方的数字券码，服务端只提交数字券码。

### 抖音短链核销失败，最常见是哪几类？

一类是短链域名或跳转不符合约定，服务端应只接受 `https://v.douyin.com/…` 的首跳并取 `object_id`；一类是码已核销或转赠中，验券准备拿不到可用券；一类是门店未完成抖音授权或 `poi_id`／`shopId` 映射错误。通过无限聚核时，先看是 `INVALID_COUPON_LINK` 还是 `PLATFORM_REJECTED`，再决定提示刷新二维码、改输明文，还是检查授权。

### 抖音团购二维码扫出来是一个网址，怎么核销？

那是 `v.douyin.com` 短链。自己对接抖音开放平台时，在服务端读取短链跳转后的长链接，取其中的 `object_id` 作为验券准备接口的 `encrypted_data`；通过无限聚核接口时，可以把完整短链作为 `code`、`platform=2` 直接提交。

### 可以按券码长度判断是美团还是抖音吗？

不可以。两个平台的纯数字券码长度都不固定，抖音短链的长度也不固定。纯数字券码让顾客选择平台，或者按门店开通的平台默认；抖音短链按域名识别。

### 顾客只有一部手机，怎么扫自己的团购券？

让顾客先截图券码页面，再在小程序的扫码界面从相册选图，`wx.scanCode` 不要把 `onlyFromCamera` 设为 `true`。同时保留手动输码入口。

### 抖音团购二维码核销后还能再用吗？

不能。抖音开放平台文档写明，验券成功后当前二维码会失效，需要使用刷新后的二维码再调用。次卡每次到店都要重新打开券页面。

### 扫到的券码提交给无限聚核后，预核销会返回什么？

成功时顶层 `success=true`，`data` 里有套餐名称 `ticketName`、售卖价 `dealPrice`（元）和 `payAmount`（分）、用户实付 `actualPayAmount`（分）、核销凭证 `ticketInfo`，抖音券还有抖音官方券信息 `ticketData`。`ticketInfo` 要整段原样保存，核销时传回，不要解析；抖音次卡的剩余次数从 `ticketData` 里读取。短链解析失败返回 `INVALID_COUPON_LINK`，券不存在、已使用或不适用本店返回 `PLATFORM_REJECTED`。

接口字段以无限聚核当前公开API文档为准。
