开发实战

美团核销 Java 怎么写,签名幂等和 prepare 到 consume 示例, 无限聚核

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

目录(9 节)
  1. 一、两条路先选对
  2. 二、最小闭环:prepare → consume → cancel
  3. 三、HTTP 封装与出站鉴权
  4. 四、预核销:保存 ticketInfo
  5. 五、核销:原参数 + errorCode 分支
  6. 六、撤销
  7. 七、上线前验收
  8. 八、与自研美团路径对照
  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。 与消费者退款是两件事;撤销无幂等。
三栏示意:鉴权签名、幂等、prepare 到 consume
示意图:字段与规则摘自无限聚核公开 API(2026-10-20 查阅),非平台后台截图。

一、两条路先选对

路径 你做什么 适用
自研:美团开放平台 办服务商/自研资质,管商家 token,对接准备/验券/撤销与查询;要抖音再单独接一套 已有多商户能力、要原生能力尽量全开
第三方统一接口 建门店、商家授权、业务后端调统一预核销/核销/撤销;平台差异由服务商消化 要同时核美团+抖音、缩短上线

两条路都不要「跳过准备直接核」。自研侧常见准备/执行接口族名如 receipt.prepare、对应验券(到店餐饮文档里还会见到 msprepare/msconsume 等,属另一路由);聚合侧服务零售对应 ddzh-tuangou-receipt-prepare → ddzh-tuangou-receipt-consume。

自研时请对照美团开放平台当前文档与官方示例(常见有 Go 等),不要假设存在一套完整的「官方美团核销 Java SDK 类名」可直接 import;多数团队用 HTTP + JSON 自封装。下文示意走聚合路径(platform=1)。

二、最小闭环:prepare → consume → cancel

  1. 预核销(输码验券校验) POST …/ddzh-tuangou-receipt-prepare
    传 shopId、platform=1、code。成功后拿到套餐展示信息与不透明 ticketInfo。不改变券状态,不发权益。
  2. 核销(验券) POST …/ddzh-tuangou-receipt-consume
    顾客确认后,原样回传同一组 shopId、platform、code、num、ticketInfo。成功看 success=true,保存 wxdgOrderNo、data[]、consumeCredential[]。
  3. 撤销 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。两篇互补,不要互相改写成一篇。

免费接入 · 按次计费

把美团、抖音团购核销接进你的业务

无限聚核提供美团、抖音、大众点评团购核销 API:不收接入费、无需保证金,核销成功才计费。

技术支持无限聚核白水客户经理联系名片及微信二维码