美团核销 Java 怎么写,签名幂等和 prepare 到 consume 示例, 无限聚核
核销服务由无限聚核提供,美团、抖音团购核销一站式接入。

目录(9 节)
搜「美团核销 Java示例」时,结果里常混着不确定的 SDK 帖、过期字段说明和「直接 msconsume」半截示例。开发真正卡的是:走自研开放平台还是接第三方统一接口?prepare → consume → 撤销怎么串?超时与未知态怎么分支?
一句话回答: 自研走美团开放平台「验券准备 → 执行验券 → 撤销」;接第三方则用同一套「预核销 → 核销 → 撤销」,platform=1 表示美团。出站请求用 Authorization: Bearer <API Key>;成功只认顶层 success=true;失败看 errorCode;业务侧按券码 + 本地订单去重(聚合核销无需调方传幂等键)。下文用 Java 11+ JDK HttpClient + Jackson 写可运行示意,不是官方完整 Java SDK 类库。业态以 V2 服务零售聚合 为主;到店餐饮请改用餐饮路由,勿照搬本文路径。依据无限聚核公开 API 与接入 FAQ 口径整理,查阅日期 2026-10-20,字段以当前公开文档为准。
- 先选路径。 自研管商家 token;聚合用具体门店
shopId+platform=1; - 出站鉴权是 Bearer。 API Key 只放服务端,不要臆造「官方美团核销 Java SDK」类名去 import;
- 幂等分两层。 接口避免重复核成多笔;业务仍要自己去重并保存首次成功响应;
- prepare 不改券状态。 真正扣券在 consume,
ticketInfo必须整段原文回传; - 撤销用 consumeCredential。 与消费者退款是两件事;撤销无幂等。

一、两条路先选对
| 路径 | 你做什么 | 适用 |
|---|---|---|
| 自研:美团开放平台 | 办服务商/自研资质,管商家 token,对接准备/验券/撤销与查询;要抖音再单独接一套 | 已有多商户能力、要原生能力尽量全开 |
| 第三方统一接口 | 建门店、商家授权、业务后端调统一预核销/核销/撤销;平台差异由服务商消化 | 要同时核美团+抖音、缩短上线 |
两条路都不要「跳过准备直接核」。自研侧常见准备/执行接口族名如 receipt.prepare、对应验券(到店餐饮文档里还会见到 msprepare/msconsume 等,属另一路由);聚合侧服务零售对应 ddzh-tuangou-receipt-prepare → ddzh-tuangou-receipt-consume。
自研时请对照美团开放平台当前文档与官方示例(常见有 Go 等),不要假设存在一套完整的「官方美团核销 Java SDK 类名」可直接 import;多数团队用 HTTP + JSON 自封装。下文示意走聚合路径(platform=1)。
二、最小闭环:prepare → consume → cancel
- 预核销(输码验券校验)
POST …/ddzh-tuangou-receipt-prepare
传shopId、platform=1、code。成功后拿到套餐展示信息与不透明ticketInfo。不改变券状态,不发权益。 - 核销(验券)
POST …/ddzh-tuangou-receipt-consume
顾客确认后,原样回传同一组shopId、platform、code、num、ticketInfo。成功看success=true,保存wxdgOrderNo、data[]、consumeCredential[]。 - 撤销
POST …/ddzh-tuangou-receipt-cancel
传原shopId、platform与consumeCredential数组(1–10 项)。美团侧撤销窗口常见约 10 分钟(以平台当前说明为准)。撤销与消费者退款是两件事。
shopId 必须是聚合侧具体门店 ID,不是商户 ID、不是你自己系统门店主键、不是美团平台门店码。API Host 与完整 URL 以当前公开 API 文档为准,下文只写路径后半段。
三、HTTP 封装与出站鉴权
示意省略部分 import;apiHost、apiKey 只放服务端配置。出站统一:
Authorization: Bearer <API Key>
需要接收授权结果 Webhook 时,入站验签用当前 API Key 对 timestamp + "." + rawBody 做 HMAC-SHA256,结果为小写十六进制并加 v1= 前缀;必须用原始 Body 字节,不能先解析再序列化。验签细节以公开文档「Webhook」章节为准,本文重点仍是核销主链路。
private final ObjectMapper json = new ObjectMapper();
private final HttpClient http = HttpClient.newHttpClient();
private String apiHost; // 从服务端配置注入,以当前公开文档为准
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();
}
String errorCode() {
return body.path("errorCode").asText("");
}
}
ApiResponse postJson(String path, String requestJson)
throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(
URI.create(apiHost + "/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();
}
return new ApiResponse(response.statusCode(), body == null ? NullNode.getInstance() : body);
}
也可用 OkHttp 等价替换;关键是不要假装有官方完整 Java SDK,并固定超时、日志脱敏、traceId 落库。
四、预核销:保存 ticketInfo
请求体关键字段:
{
"shopId": 123,
"platform": 1,
"code": "顾客输入的美团券码"
}
ObjectNode prepare(long shopId, String code, int num) throws Exception {
if (num < 1 || num > 10) throw new IllegalArgumentException("num 1–10");
ObjectNode req = json.createObjectNode()
.put("shopId", shopId).put("platform", 1).put("code", code);
ApiResponse reply = postJson("ddzh-tuangou-receipt-prepare", req.toString());
if (!reply.isSuccess()) throw new IllegalStateException("prepare failed: " + reply.errorCode());
JsonNode data = reply.body.path("data");
String ticketInfo = data.path("ticketInfo").asText("");
if (ticketInfo.isEmpty()) throw new IllegalStateException("missing ticketInfo");
// 美团预核销常用 ticketName + dealPrice(元)与已配置商品预匹配;无统一商品 ID
// ticketInfo 当不透明串整段保存,禁止解析重组
ObjectNode consumeReq = req.deepCopy().put("num", num).put("ticketInfo", ticketInfo);
// 业务侧:落 pending / 确认单,保存 consumeReq 原文与 prepare data
return json.createObjectNode()
.put("ticketName", data.path("ticketName").asText())
.put("payAmountFen", data.path("payAmount").asLong())
.set("consumeRequest", consumeReq);
}
注意:payAmount/actualPayAmount 多为分,dealPrice 多为元;响应里没有门店 ID,后续始终用你保存的 shopId。prepare 成功不等于已核销。
五、核销:原参数 + errorCode 分支
static final Set<String> RETRYABLE = Set.of(
"CONSUME_IN_PROGRESS", "RESULT_UNKNOWN",
"SERVICE_UNAVAILABLE", "PROVIDER_UNAVAILABLE");
void consume(String pendingConsumeRequestJson) throws InterruptedException {
// 发请求前业务侧写 pending / 加锁:门店+平台+券码+本地订单
final int maxAttempts = 3;
JsonNode last = NullNode.getInstance();
for (int i = 0; i < maxAttempts; i++) {
if (i > 0) Thread.sleep(1000L * i);
ApiResponse reply;
try {
reply = postJson("ddzh-tuangou-receipt-consume", pendingConsumeRequestJson);
} catch (IOException e) {
continue; // 超时/断连:原样重试
}
last = reply.body;
if (reply.isSuccess()) {
// 保存首次完整响应:wxdgOrderNo、data[]、consumeCredential[]
return;
}
String ec = reply.errorCode();
if ("ALREADY_CONSUMED".equals(ec)) {
// code 常见 10500;保存 wxdgOrderNo,停重试,转核对;勿补发、勿编造凭证
return;
}
if (!RETRYABLE.contains(ec)) {
// PLATFORM_REJECTED、参数/授权等:停,展示原因
return;
}
}
// 仍未知:标记 unknown,查已验券信息或对账
}
核销请求体示例:
{
"shopId": 123,
"platform": 1,
"code": "同一券码",
"num": 1,
"ticketInfo": "预核销返回的原值,整段回传"
}
幂等键怎么理解
公开 FAQ 口径:当前聚合核销无需调方传 idempotencyKey(旧客户端传入也会被忽略)。接口侧保证同一业务请求不会核成多笔;不保证重复调用返回同一份首次成功明细。业务系统必须:
- 按「门店 + 平台 + 券码 + 本地订单」去重加锁;
- 保存首次完整成功响应,并按
data[i]与consumeCredential[i]下标对应; - 收到
ALREADY_CONSUMED(常见code=10500)时用wxdgOrderNo核对,而不是再点一次当补偿。
六、撤销
void cancel(long shopId, List<String> credentials) throws Exception {
if (credentials.isEmpty() || credentials.size() > 10) {
throw new IllegalArgumentException("consumeCredential 1–10");
}
ObjectNode req = json.createObjectNode()
.put("shopId", shopId).put("platform", 1);
ArrayNode arr = req.putArray("consumeCredential");
credentials.forEach(arr::add);
ApiResponse reply = postJson("ddzh-tuangou-receipt-cancel", req.toString());
// 整笔拒绝:CREDENTIAL_INVALID / CANCEL_UNSUPPORTED 等
// 到达平台后看 data[].status:succeeded / rejected / unknown
// 撤销无幂等:未知时先查券状态,勿盲目重发
}
请求体示例:
{
"shopId": 123,
"platform": 1,
"consumeCredential": ["核销成功时保存的凭证"]
}
七、上线前验收
- 预核销成功只进确认页,未发权益;
- 核销
success=true后wxdgOrderNo、明细、凭证齐全,权益只发一次; - 超时用原请求原参数有限重试,不重新预核销、不换
ticketInfo; PLATFORM_REJECTED停;ALREADY_CONSUMED核对;- 撤销在时限内可回退本地权益;未知项先查已验券信息。
八、与自研美团路径对照
| 环节 | 自研美团开放平台 | 聚合(platform=1,服务零售) |
|---|---|---|
| 准备 | receipt.prepare 等(以当期文档为准) | ddzh-tuangou-receipt-prepare |
| 执行 | 对应验券接口 | ddzh-tuangou-receipt-consume |
| 撤销 | 以开放平台当期字段为准 | consumeCredential 数组 |
| 门店 | 商家 token+门店绑定 | 具体门店 shopId |
| 成功判断 | 以开放平台当期说明为准 | success 布尔值 |
| Java | HTTP/自封装;勿臆造 SDK 类名 | HttpClient+Jackson 示意 |
到店餐饮是另一套接口族,本文不展开;餐厅请另查餐饮路由文档,勿把上表服务零售路径直接套过去。
常见问题
美团核销的 Java 示例要自己算请求签名吗?
出站调用无限聚核 V2 聚合接口时,请求头使用 Authorization: Bearer <API Key> 即可,调用方不必再拼美团开放平台那套出站 sign,也不要臆造官方完整 Java SDK 类名。若配置了 Webhook,入站才需要按公开文档用 API Key 做 HMAC-SHA256 验签。
prepare 和 consume 可以合并成一次调用吗?
不可以。每次新的核销前都要先调输码验券校验确认券仍可用,再把返回的 ticketInfo 原文交给验券接口。跳过 prepare 或篡改 ticketInfo 会导致核销失败或凭证非法。
接口说幂等,为什么业务还要按券码去重?
接口幂等保证同一业务请求不会核成多笔,但重复请求命中已成功订单时只返回 10500 提示和 wxdgOrderNo,不会再次给出首次成功明细与撤销凭证。权益发放必须以本地保存的首次成功记录为准,并按券码与业务订单去重。聚合核销无需调方传 idempotencyKey。
失败后怎样安全重试?
仅当 errorCode 属于 CONSUME_IN_PROGRESS、RESULT_UNKNOWN、SERVICE_UNAVAILABLE、PROVIDER_UNAVAILABLE,或请求超时未拿到响应时,使用完全相同的原 JSON 有间隔、有上限地重试。PLATFORM_REJECTED 与 ALREADY_CONSUMED 停止自动重试。
本文和「小程序接入 Java 实战」怎么分工?
小程序全流程接入(授权、商品映射、确认页、权益)见开发实战里的小程序集成文;本文专打检索意图「美团核销 Java示例」,聚焦服务零售下鉴权、幂等与 prepare → consume → cancel。两篇互补,不要互相改写成一篇。




