微信小程序接入美团、抖音团购核销实战:从门店授权到核销与撤销
顾客买好团购券,打开你的微信小程序,选门店、输入券码、确认套餐。核销成功后,系统给他一份三小时的门店服务权益,接着就能预约或使用服务。

微信小程序接入美团、抖音团购核销,可以通过无限聚核的 V2 服务零售聚合接口串起门店授权、预核销、核销与撤销。本文面向已有小程序和业务后端的开发团队,用 Java 示例把接口调用接回自己的预约或权益系统。
阅读导航
一、先把完整效果串起来
顾客侧需要选店、输码、确认和结果页。后台则要知道:这是谁的券、在哪家门店使用、对应什么套餐,以及成功后发什么权益。尚未确定接口申请路径,可以先看微信小程序怎么接入美团、抖音团购核销?申请步骤、材料与成本对比。
建议采用“小程序 → 自己的业务后端 → 无限聚核”的调用方式。小程序交互围绕自己的业务门店和确认单展开,后端负责权限、门店映射和接口调用,再把核销结果接回预约或会员系统。

下文使用 Java 11+ 与 Jackson 写接入示例,保留真实 HTTP 请求、JSON 参数和关键控制流,省略 import、完整服务类和框架配置。代码片段可放在同一服务类中,使用第三章的
postJson封装;这些是接入示例,不是聚核 SDK,本文未编译代码或调用真实业务接口。
business 是接入方的 BusinessPort 业务接口,负责权限、门店和商品映射、确认上下文、核销记录及权益操作;PreparedRedemption、Receipt 是自己的业务对象,只使用代码中所示访问器。adapter 的 String productId(int platform, JsonNode prepareData) 负责商品识别,errors 的 FailureKind classify(ApiResponse response) 负责按已核对的接口语义分类,二者也由接入方实现。
二、把自己的门店对应到聚核子门店
先分清三个层次:自己的业务门店、聚核主子门店、美团或抖音门店。聚核主门店用于组织一组子门店;本文把每家实际经营的业务门店对应到一个聚核子门店,再核对它关联的平台门店。
业务调用使用保存下来的聚核子门店 shopId。 别把自己系统的门店 ID、聚核主门店 ID或平台门店 ID混在同一个字段里。

在无限聚核开发者后台的门店管理页,可以看到主门店分组,以及子门店对应的美团、抖音名称、授权状态和授权入口。

使用当前控制台准备门店时,先新建主门店,再在该主门店下添加子门店。主门店表单填写名称并选择业务类目,本文使用服务零售;子门店表单会提示所属主门店,再填写具体门店名称。


如果要把门店入驻放进自己的 SaaS 后台,可以接 V2 创建门店接口。这里与控制台有一个区别:API 不传上级时,会创建主门店及首个同名子门店,返回的 shopId 是子门店 ID,parentShopId 是主门店 ID。已有主门店时,传 masterShopId 添加子门店。
String createStore(String localStoreId, String shopName, String masterShopId)
throws IOException, InterruptedException {
business.requireStoreAdmin(localStoreId);
boolean firstStore = masterShopId == null || masterShopId.isBlank();
ObjectNode request = json.createObjectNode().put("shopName", shopName);
if (!firstStore) {
request.put("masterShopId", masterShopId);
}
JsonNode data = postJson("store/create", request.toString()).requireData();
String shopId = data.path("shopId").asText();
String parentId = firstStore ? data.path("parentShopId").asText() : masterShopId;
if (shopId.isBlank() || parentId.isBlank()) {
throw new IllegalStateException("Missing store identifiers");
}
business.saveStoreMapping(localStoreId, shopId, parentId);
return shopId;
}
示例中 masterShopId 为空时创建主门店和首个子门店;已有主门店时传入保存的主门店 ID。创建出来的门店还要完成平台授权。建议在映射记录旁保留平台门店的核对结果,方便后续发现“顾客选的是 A 店,实际关联了 B 店”这类配置问题。
三、完成商家授权,把凭据放到服务端
门店创建、获取授权链接、商家完成授权,是三个步骤。先在控制台从对应子门店发起美团或抖音授权,商家完成后,再核对平台门店与授权状态。拿到一个链接,还不能把门店标成已授权。
自建入驻流程时,文档中的“获取美团/抖音授权链接”接受 shopId 和 platform;平台值为 1 美团、2 抖音,成功时 data 是授权 URL。
String authorizationUrl(String localStoreId, int platform)
throws IOException, InterruptedException {
business.requireStoreAdmin(localStoreId);
String shopId = business.shopId(localStoreId);
ObjectNode request = json.createObjectNode()
.put("shopId", shopId).put("platform", platform);
JsonNode data = postJson("get/auth/url", request.toString()).requireData();
if (!data.isTextual() || data.textValue().isBlank()) {
throw new IllegalStateException("Missing authorization URL");
}
return data.textValue(); // 展示给商家;授权完成后另行核对门店和状态
}
从开发者后台的开发者中心获取当前账号 API key,保存在自己的服务端。本轮快速开始文档给出的 API Host 是 https://newopen.elys.cn,请求头示例为 Authorization: Bearer <key>。
private final ObjectMapper json = new ObjectMapper();
private final HttpClient http = HttpClient.newHttpClient();
private String apiKey; // 从自己的服务端配置注入,不放进小程序
static final class ApiResponse {
final int httpStatus;
final JsonNode body;
ApiResponse(int httpStatus, JsonNode body) {
this.httpStatus = httpStatus;
this.body = body;
}
boolean isSuccess() {
return body.path("success").isBoolean() && body.path("success").booleanValue();
}
JsonNode requireData() {
if (!isSuccess()) throw new IllegalStateException("API result requires handling");
return body.path("data");
}
}
ApiResponse postJson(String path, String requestJson)
throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(URI.create(
"https://newopen.elys.cn/api/hexiao/v2/" + path))
.timeout(Duration.ofSeconds(10)) // 本例的客户端超时设置
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestJson)).build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
JsonNode body;
try {
body = json.readTree(response.body());
} catch (JsonProcessingException e) {
body = NullNode.getInstance(); // 保留 HTTP 状态,由调用处分类
}
return new ApiResponse(response.statusCode(), body == null ? NullNode.getInstance() : body);
}
小程序请求到达自己的后端时,要先检查用户和业务门店权限,再查映射调用接口。聚核会校验系统门店的账号归属,你自己的用户权限仍由业务后端负责。
四、接口按用途接,不必一次全部铺开
先做顾客确认与核销,再补商品配置、门店入驻和售后页面。下表都使用 POST,路径以 /api/hexiao/v2/ 为前缀;参数与响应详见公开 API 文档中的对应导航。
| 能力 | 接口路径后半段 | 本文如何使用 |
|---|---|---|
| 预核销 | ddzh-tuangou-receipt-prepare |
必接,检查具体券并取得核销凭证 |
| 核销 | ddzh-tuangou-receipt-consume |
必接,顾客确认使用后执行 |
| 团购列表 | ddzh-tuangou-deal-queryshopdeal |
用于展示套餐、配置本地权益映射 |
| 创建门店 | store/create |
自建门店入驻后台时接 |
| 授权链接 | get/auth/url |
把商家授权整合进自己的后台时接 |
| 撤销核销 | ddzh-tuangou-receipt-cancel |
有撤销售后流程时接 |
文档把预核销称为“输码验券校验”,把真正执行核销的接口称为“验券”。开发时可以在自己的封装里明确命名为 prepare 和 consume,避免在顾客尚未确认时误调后者。
五、先认识套餐,再决定发什么权益
商品列表解决的是“这家店卖什么”。例如将某个平台商品配置成“三小时服务权益”,后续识别到这款商品,自己的系统才知道该给顾客什么。
建议用 平台+聚核子门店+平台商品 ID 作为映射条件。套餐名称用于展示,不能只靠名称判断商品;不同平台的同名套餐也应分别配置。
void configureProduct(String localStoreId, int platform, int page, int pageSize,
String selectedDealId, String benefitId)
throws IOException, InterruptedException {
business.requireStoreAdmin(localStoreId);
String shopId = business.shopId(localStoreId);
ObjectNode request = json.createObjectNode().put("shopId", shopId)
.put("platform", platform).put("offset", page).put("limit", pageSize);
JsonNode deals = postJson("ddzh-tuangou-deal-queryshopdeal", request.toString()).requireData();
for (JsonNode deal : deals) {
if (selectedDealId.equals(deal.path("dealId").asText())) {
business.saveProductMapping(platform, shopId, selectedDealId, benefitId);
return;
}
}
throw new IllegalArgumentException("Selected product is not in this page");
}
示例把管理员在当前页选中的 selectedDealId 与本地 benefitId 关联;三小时服务对应哪一个 benefitId,由自己的权益系统配置。
当前“获取团购信息”文档将 offset 定义为页码,limit 为每页条数;列表中的 price、marketPrice 单位是元。
套餐配置完成后,可以按自己的业务需要同步或维护。顾客每次用券仍要预核销:列表有这款商品,不等于他输入的这张券可以使用。
六、预核销后展示确认页,保存原始凭证
顾客选好门店、平台并输入券码,后端先找出聚核子门店 shopId,以同一组 shopId、platform、code 调用预核销。成功后展示套餐和本地对应权益,等顾客确认。
“输码验券校验”文档的统一 data 包含 payAmount、shopId、ticketName、ticketInfo 和 ticketData。其中有三处容易接错:
ticketInfo是后续核销要原样回传的字符串。不要解析后重组、删字段或替换它。data.shopId是套餐或门店的兼容 ID,不能覆盖自己保存的聚核子门店 ID。payAmount单位是分;展示为元时要转换,不能与商品列表的金额单位混用。
ObjectNode prepare(String localStoreId, int platform, String code, int num)
throws IOException, InterruptedException {
String userId = business.requireUserAccess(localStoreId);
String shopId = business.shopId(localStoreId);
if (num <= 0) throw new IllegalArgumentException("Invalid quantity");
ObjectNode request = json.createObjectNode().put("shopId", shopId)
.put("platform", platform).put("code", code);
JsonNode data = postJson("ddzh-tuangou-receipt-prepare", request.toString()).requireData();
String productId = adapter.productId(platform, data.deepCopy());
if (productId == null || productId.isBlank()) {
throw new IllegalStateException("Product needs review");
}
String benefitId = business.findBenefit(platform, shopId, productId);
if (benefitId == null) throw new IllegalStateException("Configure product mapping first");
String ticketInfo = data.path("ticketInfo").textValue();
if (ticketInfo == null || ticketInfo.isEmpty() || !data.path("payAmount").isNumber()) {
throw new IllegalStateException("Incomplete prepare response");
}
ObjectNode consumeRequest = request.deepCopy().put("num", num).put("ticketInfo", ticketInfo);
String contextId = business.savePending(userId, consumeRequest.toString(), benefitId);
return json.createObjectNode().put("contextId", contextId)
.put("ticketName", data.path("ticketName").asText()).put("benefitId", benefitId)
.put("payAmountYuan", data.path("payAmount").decimalValue().movePointLeft(2));
}
这里以 deepCopy() 把完整 data 的副本交给商品适配器,约定只读分析,并按各平台已核对的返回语义提取可靠商品标识。ticketData 为空,不等于整个响应没有可识别信息。预核销没有统一的 data.dealId;也不能把兼容字段 data.shopId 一概当成商品 ID,更不能用它覆盖输入的聚核子门店 ID。ticketInfo 只作为不透明凭证原样保存、回传,不拆解它来做商品匹配。仍无法可靠识别商品时,先完善适配与配置,别按套餐名称猜测后发权益。代码中的数量来自本次业务确认,也不能假定统一响应里有可直接读取的数量字段。
返回对象中的 contextId、benefitId、payAmountYuan 是自己的小程序接口字段;聚核请求体只有代码中列出的真实参数。savePending 要原样保存 consumeRequest.toString(),核销时直接读取该字符串。确认页只给小程序必要的展示数据与自己的上下文 ID。券码、门店、数量和原始凭证留在服务端,后续从同一个上下文读取。预核销成功时,顾客还没有完成实际核销。
七、确认核销;超时就用原参数重试
顾客点击确认后,小程序提交上下文 ID。后端核对上下文归属,取出已保存的 shopId、platform、code、num、ticketInfo,调用核销接口。进入成功处理的依据是顶层 success 为布尔值 true,不能只看 HTTP 状态或某个平台内部字段。核销接口说明
核销幂等由平台保证。调用方遇到超时、连接中断等非业务错误,使用老参数重复请求。 同一次核销的五个参数全部保持不变,包括 ticketInfo;不要为了重试再预核销,也不要换券码、门店或数量。
enum FailureKind { REJECTED, CONFIG_ERROR, RETRYABLE, UNKNOWN, REVIEW }
void consume(String contextId) throws InterruptedException {
PreparedRedemption context = business.loadOwnedPending(contextId);
if (business.hasSuccess(context.id())) return;
final String requestJson = context.requestJson(); // 循环前固定,逐字复用
ApiResponse last = null;
for (int attempt = 0; attempt < 3; attempt++) {
if (attempt > 0) Thread.sleep(500L * attempt); // 本例的有限串行退避
ApiResponse reply;
try {
reply = postJson("ddzh-tuangou-receipt-consume", requestJson);
} catch (IOException e) { // 包含 HttpTimeoutException;不捕获本地履约异常
continue;
}
last = reply;
if (reply.isSuccess()) {
saveFirstSuccess(context, reply.body); // 在网络重试的 catch 之外
return;
}
if (reply.body.path("code").asInt() == 10500) {
business.markPending(context.id(), reply.body.deepCopy());
return; // 缺少首次成功记录时,不凭已核销提示发权益
}
switch (errors.classify(reply)) {
case REJECTED:
case CONFIG_ERROR:
business.stopAttempt(context.id(), reply.body.deepCopy());
return;
case RETRYABLE:
case UNKNOWN:
break;
default:
business.markPending(context.id(), reply.body.deepCopy());
return;
}
}
business.markPending(context.id(), last == null ? NullNode.getInstance() : last.body.deepCopy());
}
上述重试上限、间隔与客户端超时是本例的配置值。InterruptedException 向上传播,调用方可终止等待;首次成功的保存和权益处理不在网络异常的 catch 内,本地业务异常不会触发这一循环再次核销。
ErrorPolicy 将明确业务拒绝、配置错误分别归为 REJECTED、CONFIG_ERROR;可重试技术失败与已识别的结果未知归为 RETRYABLE、UNKNOWN,无法识别的响应归为 REVIEW。这组枚举是自己的分类,不是聚核响应字段。success=false 不能一概当成业务拒绝。当前结果未知分支会使用 code=10001 并提示原请求重试,但参数、鉴权等错误也可能使用这个码,不能写成“10001 就重试”。分类函数应依据接口的具体语义实现;拿不准的结果进入待核对,不靠中文文案模糊匹配自动发权益。
还有一个边界:幂等不等于每次重复请求都返回同一份成功数据。 当前已成功请求的重试可能返回 success=false、code=10500,并清空 data、consumeCredential、platformResult。如果首次响应丢失,后来只收到已核销提示,应停止自动核销重试、核对原记录;不能凭这个提示直接补发权益,也不能据此编造撤销凭证。
对顾客而言,超时意味着结果还没确定。页面可显示“正在确认核销结果”,避免立刻提示失败并诱导他重新开始一笔核销。
八、先保存成功结果,再完成自己的业务
首次收到明确成功时,保存原 shopId、platform、关联号、核销明细和顶层 consumeCredential 数组。凭证与 data 明细按下标对应,后续撤销需要使用相应项的原凭证。成功响应与凭证说明
void saveFirstSuccess(PreparedRedemption context, JsonNode response) {
// 保存完整首次响应:data、consumeCredential、关联号,以及上下文中的原请求
Receipt receipt = business.saveFirstSuccess(context, response.deepCopy());
try {
business.grantOnce(receipt.id(), context.benefitId());
business.markFulfilled(receipt.id());
} catch (RuntimeException e) {
business.enqueueFulfillment(receipt.id()); // 只补处理自己的权益
}
}
saveFirstSuccess 要保存首次完整响应,并把 data[i] 与 consumeCredential[i] 对应起来,同时保留上下文中的原请求;后续调用不能覆盖掉这份首次记录。这里的权益控制属于自己的业务设计。平台保证核销幂等,并不会代替你的预约、时长或会员系统完成发放。
如果核销已明确成功,写入权益时发生本地异常,就根据保存的成功记录补处理本地业务。不要重新核销已成功的券;页面可以分别表达“核销成功”和“权益正在处理”,让顾客知道当前进度。
九、需要撤销时,使用首次成功的原凭证
顾客选错套餐或取消使用时,先按自己的规则检查权益能否撤回。已使用的服务如何处理,由业务规则决定。撤销核销与消费者付款退款要分开处理,调用撤销接口不等于完成退款。
统一撤销支持美团、抖音,传原核销的 shopId、platform 和 consumeCredential 字符串数组。它适用于有有效凭证且符合平台规则的记录,不能承诺所有券、所有历史核销都能撤销。“撤销核销”接口说明
void cancel(String receiptId) throws InterruptedException {
Receipt receipt = business.loadCancellableOwned(receiptId); // 校验归属及权益可撤回
List<String> credentials = business.selectedOriginalCredentials(receipt);
if (credentials.isEmpty()) throw new IllegalStateException("No original credentials");
ObjectNode request = json.createObjectNode().put("shopId", receipt.shopId())
.put("platform", receipt.platform());
ArrayNode values = request.putArray("consumeCredential");
credentials.forEach(values::add);
ApiResponse reply;
try {
reply = postJson("ddzh-tuangou-receipt-cancel", request.toString());
} catch (IOException e) {
for (String credential : credentials) business.markCancellationPending(receiptId, credential);
return;
}
Map<String, JsonNode> items = new HashMap<>();
if (reply.body.path("data").isArray()) {
for (JsonNode item : reply.body.path("data")) {
items.put(item.path("consumeCredential").asText(), item);
}
}
for (String credential : credentials) {
JsonNode item = items.get(credential);
String status = item == null ? "unknown" : item.path("status").asText("unknown");
switch (status) {
case "succeeded":
business.saveCancellationSucceeded(receiptId, credential);
try {
business.revokeOnce(receiptId, credential);
} catch (RuntimeException e) {
business.enqueueRevocation(receiptId, credential);
}
break;
case "rejected":
business.recordCancellationRejected(receiptId, credential, item.path("message").asText());
break;
case "unknown":
default:
business.markCancellationPending(receiptId, credential);
}
}
}
selectedOriginalCredentials 只取已保存的首次成功凭证,按本次选定明细组成字符串数组;无法找到某项响应时,示例会将该项记为待核对。撤销响应要逐项看:succeeded 表示该项成功,rejected 表示拒绝,unknown 表示结果未知。只有全部成功时顶层 success 才是 true;部分成功时,即使顶层为 false,也要处理已明确成功的项。若撤销已成功但本地权益撤回失败,同样留下本地补处理记录。
十、联调按业务结果验收
接口能返回 JSON,还要确认页面、门店和权益串得上。下面是供你的团队执行的联调清单,本文未运行这些示例,也未进行真实券预核销、核销或撤销测试。
| 检查场景 | 预期处理 |
|---|---|
| 基础配置 | 核对服务端凭据、小程序后端 HTTPS 和微信请求合法域名配置 |
| 门店与授权 | 业务门店对应正确子门店,美团、抖音门店和授权状态分别核对 |
| 商品配置 | 平台、门店、商品映射正确;同名商品不会误发权益 |
| 预核销成功 | 只展示确认页,原 ticketInfo 已保存,尚未发权益 |
| 核销明确成功 | 首次结果、明细及凭证保存完整,自己的权益只发放一次 |
| 明确业务拒绝 | 展示原因,停止该次自动核销重试 |
| 超时或结果未知 | 复用原五个参数,有间隔、有上限;未确定前不报成功或确定失败 |
| 已核销提示但缺首次结果 | 停止自动重试,核对原记录,不直接补发权益 |
| 本地履约异常 | 根据已保存的成功记录补处理自己的业务 |
| 撤销与部分成功 | 按原凭证逐项处理,保留未知项,分别更新对应权益 |
十一、微信小程序接入团购核销常见问题
微信小程序接入美团团购核销怎么做?
本文的服务零售接入流程是:将业务门店映射到无限聚核子门店,完成美团门店授权并核对绑定关系;业务后端使用 platform=1 调用预核销,取得原始 ticketInfo。顾客确认后,再带上同一门店、平台、券码和核销数量完成核销,保存首次成功结果后处理自己的权益。具体请求见预核销示例和核销示例。
微信小程序接入抖音团购核销怎么做?
先完成无限聚核子门店对应的抖音门店授权,并核对抖音商品与自己业务权益的映射。小程序通过自己的业务后端调用接口,后端使用 platform=2,按“预核销、顾客确认、核销”的顺序处理,原样保存和回传 ticketInfo。美团与抖音的门店及授权状态分别核对,不能用美团授权代替抖音授权;步骤见门店授权和商品映射。
微信小程序团购核销必须接哪些接口?
本文采用的无限聚核 V2 服务零售两阶段流程中,预核销与核销是核心接口。门店创建和商家授权可先在控制台完成,也可按业务需要接入相应接口;需要在自己的后台配置套餐权益时接团购商品列表,需要撤回核销时再接撤销接口。具体取舍见接口清单,不是把所有接口都列为必接。
微信小程序接入团购核销,为什么要先预核销?
在本文使用的无限聚核 V2 服务零售聚合接口中,美团和抖音的新核销请求都要先调用“输码验券校验”,取得同一 shopId、platform、code 对应的 ticketInfo。预核销用于检查和准备确认上下文,不代表核销成功,也不发放权益;顾客确认后才核销。核销超时后的重试复用原请求,不重新预核销,见预核销流程。
团购核销接口超时后怎么重试?
核销幂等由平台保证。超时等非业务错误、可重试技术失败或已识别的结果未知,由业务后端有间隔、有上限地重复首次请求的 shopId、platform、code、num、ticketInfo,不换参数。明确业务拒绝应停止自动重试;若仅收到已核销提示但缺少首次成功记录,转入核对,不直接补发权益。处理边界见原参数重试示例。
开始开发时,打开无限聚核 API 文档,按门店映射、授权、预核销、核销、撤销的顺序联调,再把结果接回自己的预约或权益系统。