Skip to content

价态增量回调接口

简要描述

  • 酒店价态增量回调接口

请求URL

  • /Hotel/HotelRoomRateIncrement

请求方式

  • POST

参数

联系业务配置回调基础url

验签方法

java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public final class DistributorSignatureUtil {

    private static final String SIGN_TYPE = "HMAC-SHA256";
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final String SHA256_ALGORITHM = "SHA-256";

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private DistributorSignatureUtil() {
    }

    /**
     * 验证分销商推送签名。
     *
     * @param payload 请求JSON
     * @param secret  PartnerId对应的共享密钥
     * @return true表示验签成功
     */
    public static boolean verifyDistributorPayload(
            JsonNode payload,
            String secret
    ) {
        if (payload == null || !payload.isObject()) {
            return false;
        }

        String sign = getText(payload, "Sign");
        String signType = getText(payload, "SignType");

        if (secret == null
                || secret.isEmpty()
                || sign.isEmpty()
                || !SIGN_TYPE.equals(signType)
                || !sign.matches("(?i)^[a-f0-9]{64}$")) {
            return false;
        }

        try {
            JsonNode data = payload.has("Data")
                    ? payload.get("Data")
                    : OBJECT_MAPPER.createObjectNode();

            JsonNode normalizedData = normalizeDistributorSignPayload(data);

            // Jackson默认不会转义中文和斜杠,数组顺序保持不变。
            String dataJson = OBJECT_MAPPER.writeValueAsString(normalizedData);
            String dataSha256 = sha256Hex(dataJson);

            String signString = String.join(
                    "\n",
                    "PartnerId=" + getText(payload, "PartnerId"),
                    "Event=" + getText(payload, "Event"),
                    "Guid=" + getText(payload, "guid"),
                    "SignType=" + signType,
                    "DataSha256=" + dataSha256
            );

            String expectedSign = hmacSha256Hex(signString, secret);

            return MessageDigest.isEqual(
                    expectedSign.getBytes(StandardCharsets.US_ASCII),
                    sign.toLowerCase(Locale.ROOT)
                            .getBytes(StandardCharsets.US_ASCII)
            );
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 递归规范化JSON:
     * 1. 对象字段按字段名升序排序;
     * 2. 数组保持原始顺序;
     * 3. 基础类型保持不变。
     */
    private static JsonNode normalizeDistributorSignPayload(JsonNode value) {
        if (value == null || value.isNull()) {
            return OBJECT_MAPPER.nullNode();
        }

        if (value.isArray()) {
            ArrayNode normalizedArray = OBJECT_MAPPER.createArrayNode();

            for (JsonNode item : value) {
                normalizedArray.add(normalizeDistributorSignPayload(item));
            }

            return normalizedArray;
        }

        if (value.isObject()) {
            ObjectNode normalizedObject = OBJECT_MAPPER.createObjectNode();

            List<String> fieldNames = new ArrayList<>();
            Iterator<Map.Entry<String, JsonNode>> fields = value.fields();

            while (fields.hasNext()) {
                fieldNames.add(fields.next().getKey());
            }

            Collections.sort(fieldNames);

            for (String fieldName : fieldNames) {
                normalizedObject.set(
                        fieldName,
                        normalizeDistributorSignPayload(value.get(fieldName))
                );
            }

            return normalizedObject;
        }

        return value.deepCopy();
    }

    private static String sha256Hex(String content) throws Exception {
        MessageDigest digest = MessageDigest.getInstance(SHA256_ALGORITHM);

        byte[] hash = digest.digest(
                content.getBytes(StandardCharsets.UTF_8)
        );

        return toHex(hash);
    }

    private static String hmacSha256Hex(
            String content,
            String secret
    ) throws Exception {
        Mac mac = Mac.getInstance(HMAC_ALGORITHM);

        SecretKeySpec secretKey = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8),
                HMAC_ALGORITHM
        );

        mac.init(secretKey);

        byte[] hash = mac.doFinal(
                content.getBytes(StandardCharsets.UTF_8)
        );

        return toHex(hash);
    }

    private static String toHex(byte[] bytes) {
        StringBuilder result = new StringBuilder(bytes.length * 2);

        for (byte value : bytes) {
            result.append(String.format("%02x", value & 0xff));
        }

        return result.toString();
    }

    private static String getText(JsonNode payload, String fieldName) {
        JsonNode value = payload.get(fieldName);

        if (value == null || value.isNull()) {
            return "";
        }

        return value.isTextual() ? value.textValue() : value.asText("");
    }
}

请求示例

{
  "PartnerId": "1234567",
  "Event": "HotelRoomRateIncrement",
  "Data": {
    "HotelIds": [
      185768,
      319076,
      705368,
      765569,
      957543,
      1120517,
      1249401,
      1392241,
      1509236,
      1857941,
      2012950,
      2031026,
      2793712,
      2917578
    ],
    "CurrentTimestamp": 1788847866
  },
  "guid": "b5c532d2c7c5f36eb6a954b9fae3c591",
  "SignType": "HMAC-SHA256",
  "Sign": "402fd68f53be82e21ca2621ac766d2b764b3ad38c9ae800c9cf6a2a6ff527632"
}

响应参数(返回的Code请默认返回成功值200)

{
	"Data":{},
	"Msg":"",
	"Code":200
}

返回参数说明

参数名类型说明
HotelIdsarray奈斯酒店ID列表
PartnerIdstring分销商ID
Eventstring事件名
Signstring签名