酒店
This commit is contained in:
1 parent
6398f7f904
commit
d26683f6d5
4 files changed
+266
-6
No files matched your search
+32
-3
@@ -5,6 +5,7 @@ import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.bsd.pjcloud.common.security.annotation.Inner;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamInboundAuth;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.huiding.HuidingSignHelper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelHuidingOrderService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -44,7 +45,7 @@ public class SmHotelHuidingOrderController {
|
||||
public Map<String, Object> check(@RequestBody String rawBody) {
|
||||
try {
|
||||
JSONObject envelope = JSONUtil.parseObj(rawBody);
|
||||
String partner = inboundAuth.verifyForHuiding(envelope);
|
||||
String partner = inboundAuth.verifyForHuiding(rawBody);
|
||||
JSONObject data = parseDataJson(envelope);
|
||||
return huidingOrderService.check(partner, data);
|
||||
}
|
||||
@@ -60,7 +61,7 @@ public class SmHotelHuidingOrderController {
|
||||
public Map<String, Object> create(@RequestBody String rawBody) {
|
||||
try {
|
||||
JSONObject envelope = JSONUtil.parseObj(rawBody);
|
||||
String partner = inboundAuth.verifyForHuiding(envelope);
|
||||
String partner = inboundAuth.verifyForHuiding(rawBody);
|
||||
JSONObject data = parseDataJson(envelope);
|
||||
return huidingOrderService.create(partner, data);
|
||||
}
|
||||
@@ -76,7 +77,7 @@ public class SmHotelHuidingOrderController {
|
||||
public Map<String, Object> cancel(@RequestBody String rawBody) {
|
||||
try {
|
||||
JSONObject envelope = JSONUtil.parseObj(rawBody);
|
||||
String partner = inboundAuth.verifyForHuiding(envelope);
|
||||
String partner = inboundAuth.verifyForHuiding(rawBody);
|
||||
JSONObject data = parseDataJson(envelope);
|
||||
return huidingOrderService.cancel(partner, data);
|
||||
}
|
||||
@@ -106,6 +107,34 @@ public class SmHotelHuidingOrderController {
|
||||
}
|
||||
}
|
||||
|
||||
@Inner(value = false)
|
||||
@Operation(summary = "联调:生成慧订签名报文,或解释为何 check 签名错误")
|
||||
@PostMapping("/debug-sign")
|
||||
public Map<String, Object> debugSign(@RequestBody String rawBody) {
|
||||
JSONObject in = JSONUtil.parseObj(rawBody);
|
||||
if (StrUtil.isNotBlank(in.getStr("secret"))) {
|
||||
Object dataRaw = in.get("data");
|
||||
String dataJson;
|
||||
if (dataRaw instanceof JSONObject jo) {
|
||||
dataJson = jo.toString();
|
||||
}
|
||||
else {
|
||||
dataJson = in.getStr("data");
|
||||
}
|
||||
Map<String, Object> env = HuidingSignHelper.buildEnvelope(
|
||||
StrUtil.blankToDefault(in.getStr("appId"), in.getStr("orderAppId")),
|
||||
in.getStr("secret"), dataJson);
|
||||
Map<String, Object> wrap = new LinkedHashMap<>();
|
||||
wrap.put("code", 200);
|
||||
wrap.put("msg", "把 data 整段贴到 /order/check,Body 选 raw JSON");
|
||||
wrap.put("data", env);
|
||||
return wrap;
|
||||
}
|
||||
Map<String, Object> explain = inboundAuth.explainHuidingSign(rawBody);
|
||||
explain.put("code", Boolean.TRUE.equals(explain.get("ok")) ? 200 : 500);
|
||||
return explain;
|
||||
}
|
||||
|
||||
private static JSONObject parseDataJson(JSONObject envelope) {
|
||||
Object data = envelope.get("data");
|
||||
if (data instanceof JSONObject jo) {
|
||||
|
||||
+139
-3
@@ -9,6 +9,8 @@ import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamAppEntity;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.huiding.HuidingSignHelper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelDownstreamAppMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
@@ -41,6 +43,8 @@ public class DownstreamInboundAuth {
|
||||
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SmHotelDownstreamAppEntity verify(String appKey, String timestamp, String nonce, String sign,
|
||||
String bodyJson) {
|
||||
assertCommon(appKey, timestamp, nonce, sign);
|
||||
@@ -119,11 +123,22 @@ public class DownstreamInboundAuth {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 慧订订单入站验签(HMAC-SHA256)。优先吃原始 JSON,避免 data 被二次序列化。
|
||||
*/
|
||||
public String verifyForHuiding(String rawBody) {
|
||||
return verifyForHuiding(parseHuidingEnvelope(rawBody), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 慧订订单入站验签(HMAC-SHA256)。返回匹配的伙伴编码。
|
||||
* @param envelope 外层字段:appId/nonce/signType/timeStamp/sign/data
|
||||
*/
|
||||
public String verifyForHuiding(Map<String, Object> envelope) {
|
||||
return verifyForHuiding(envelope, true);
|
||||
}
|
||||
|
||||
private String verifyForHuiding(Map<String, Object> envelope, boolean skipRawHint) {
|
||||
if (envelope == null || envelope.isEmpty()) {
|
||||
throw new CheckedException("报文为空");
|
||||
}
|
||||
@@ -132,7 +147,6 @@ public class DownstreamInboundAuth {
|
||||
String signType = StrUtil.trim(str(envelope.get("signType")));
|
||||
String timeStamp = StrUtil.trim(str(envelope.get("timeStamp")));
|
||||
String sign = StrUtil.trim(str(envelope.get("sign")));
|
||||
// data 必须用报文里的字符串原样参与签名;若被解析成对象再 toString,会和对方 HMAC 对不上
|
||||
String data = huidingDataForSign(envelope);
|
||||
if (StrUtil.isBlank(appId) || StrUtil.isBlank(nonce) || StrUtil.isBlank(timeStamp) || StrUtil.isBlank(sign)
|
||||
|| StrUtil.isBlank(data)) {
|
||||
@@ -187,8 +201,9 @@ public class DownstreamInboundAuth {
|
||||
}
|
||||
}
|
||||
if (usedSlot == null) {
|
||||
log.warn("|慧订入站|签名错误|partner={}|appId={}|dataLen={}|", partner, appId, data.length());
|
||||
throw new CheckedException("签名错误(请确认对方 HMAC 密钥是 order-app-secret,data 用 JSON 字符串原样参与签名)");
|
||||
log.warn("|慧订入站|签名错误|partner={}|appId={}|dataLen={}|skipRaw={}|slots={}|", partner, appId,
|
||||
data.length(), skipRawHint, slotSummary(matched));
|
||||
throw new CheckedException("签名错误");
|
||||
}
|
||||
Boolean ok = redisTemplate.opsForValue().setIfAbsent(HUIDING_NONCE_KEY + appId + ":" + nonce, "1", 5,
|
||||
TimeUnit.MINUTES);
|
||||
@@ -198,6 +213,127 @@ public class DownstreamInboundAuth {
|
||||
return partner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postman 联调用:看服务端实际匹配到哪套密钥、用哪段 data 验签(不回密钥明文)。
|
||||
*/
|
||||
public Map<String, Object> explainHuidingSign(String rawBody) {
|
||||
Map<String, Object> envelope = parseHuidingEnvelope(rawBody);
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
String appId = StrUtil.trim(str(envelope.get("appId")));
|
||||
String nonce = StrUtil.trim(str(envelope.get("nonce")));
|
||||
String signType = StrUtil.trim(str(envelope.get("signType")));
|
||||
String timeStamp = StrUtil.trim(str(envelope.get("timeStamp")));
|
||||
String sign = StrUtil.trim(str(envelope.get("sign")));
|
||||
String data = huidingDataForSign(envelope);
|
||||
out.put("appId", appId);
|
||||
out.put("nonce", nonce);
|
||||
out.put("timeStamp", timeStamp);
|
||||
out.put("receivedSign", sign);
|
||||
out.put("dataType", envelope.get("data") == null ? null : envelope.get("data").getClass().getSimpleName());
|
||||
out.put("dataLen", data == null ? 0 : data.length());
|
||||
out.put("dataPreview", data == null ? null : StrUtil.maxLength(data, 180));
|
||||
HotelDownstreamProperties matched = null;
|
||||
String partner = null;
|
||||
for (String code : channelConfigService.listEnabledDownstreamPartners()) {
|
||||
HotelDownstreamProperties p = channelConfigService.resolveDownstreamRuntime(code);
|
||||
if (p == null || !SmHotelDownstreamProtocol.isHuiding(p.getProtocol())) {
|
||||
continue;
|
||||
}
|
||||
String cfgAppId = StrUtil.blankToDefault(StrUtil.trim(p.getOrderAppId()), StrUtil.trim(p.getAppKey()));
|
||||
if (appId != null && appId.equals(cfgAppId)) {
|
||||
matched = p;
|
||||
partner = code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
out.put("partner", partner);
|
||||
out.put("appIdMatched", matched != null);
|
||||
if (matched == null) {
|
||||
out.put("hint", "appId 没匹配到启用的慧订下游配置,检查渠道对接配置是否启用、protocol=huiding、order-app-id 是否一致");
|
||||
return out;
|
||||
}
|
||||
out.put("secretSlots", slotSummary(matched));
|
||||
Map<String, Object> signMap = new LinkedHashMap<>();
|
||||
signMap.put("appId", appId);
|
||||
signMap.put("data", data);
|
||||
signMap.put("nonce", nonce);
|
||||
signMap.put("signType", StrUtil.blankToDefault(signType, HuidingSignHelper.SIGN_TYPE_HMAC));
|
||||
signMap.put("timeStamp", timeStamp);
|
||||
String matchedSlot = null;
|
||||
Map<String, String> expectBySlot = new LinkedHashMap<>();
|
||||
for (String[] slot : huidingHmacSecrets(matched)) {
|
||||
if (StrUtil.isBlank(slot[1])) {
|
||||
continue;
|
||||
}
|
||||
String expect = HuidingSignHelper.sign(signMap, slot[1]);
|
||||
expectBySlot.put(slot[0], expect);
|
||||
if (expect.equalsIgnoreCase(sign)) {
|
||||
matchedSlot = slot[0];
|
||||
}
|
||||
}
|
||||
out.put("expectBySlot", expectBySlot);
|
||||
out.put("matchedSlot", matchedSlot);
|
||||
out.put("ok", matchedSlot != null);
|
||||
if (matchedSlot == null) {
|
||||
out.put("hint", "服务端这几把密钥都对不上你带来的 sign。常见原因:1) 渠道配置未保存/未重启,实际密钥不是 YAML 里那串;"
|
||||
+ "2) Postman 把 data 从字符串改成了对象。用 /debug-sign 生成后原样粘贴,Body 选 raw JSON。");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<String, Object> parseHuidingEnvelope(String rawBody) {
|
||||
if (StrUtil.isBlank(rawBody)) {
|
||||
throw new CheckedException("报文为空");
|
||||
}
|
||||
Map<String, Object> envelope = new LinkedHashMap<>();
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(rawBody);
|
||||
envelope.put("appId", textOrNull(root.get("appId")));
|
||||
envelope.put("nonce", textOrNull(root.get("nonce")));
|
||||
envelope.put("signType", textOrNull(root.get("signType")));
|
||||
envelope.put("timeStamp", textOrNull(root.get("timeStamp")));
|
||||
envelope.put("sign", textOrNull(root.get("sign")));
|
||||
JsonNode dataNode = root.get("data");
|
||||
if (dataNode == null || dataNode.isNull()) {
|
||||
envelope.put("data", null);
|
||||
}
|
||||
else if (dataNode.isTextual()) {
|
||||
envelope.put("data", dataNode.asText());
|
||||
}
|
||||
else {
|
||||
envelope.put("data", objectMapper.writeValueAsString(dataNode));
|
||||
}
|
||||
return envelope;
|
||||
}
|
||||
catch (CheckedException e) {
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new CheckedException("报文不是合法 JSON");
|
||||
}
|
||||
}
|
||||
|
||||
private static String textOrNull(JsonNode n) {
|
||||
if (n == null || n.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (n.isTextual()) {
|
||||
return n.asText();
|
||||
}
|
||||
return n.asText();
|
||||
}
|
||||
|
||||
private static Map<String, Object> slotSummary(HotelDownstreamProperties p) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
for (String[] slot : huidingHmacSecrets(p)) {
|
||||
Map<String, Object> one = new LinkedHashMap<>(2);
|
||||
one.put("configured", StrUtil.isNotBlank(slot[1]));
|
||||
one.put("length", slot[1] == null ? 0 : slot[1].length());
|
||||
m.put(slot[0], one);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
private static String huidingDataForSign(Map<String, Object> envelope) {
|
||||
Object raw = envelope.get("data");
|
||||
if (raw == null) {
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.downstream.huiding;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 本地生成慧订订单入站报文,给 Postman 用。
|
||||
* IDEA:打开本类 → 改下面三个常量 → 右键 Run main。
|
||||
*/
|
||||
public final class HuidingOrderSignMain {
|
||||
|
||||
/** 渠道 YAML 的 order-app-id */
|
||||
private static final String APP_ID = "hd58cd9d5ba87b0e04";
|
||||
|
||||
/** 渠道 YAML 的 order-app-secret(只本地算签) */
|
||||
private static final String SECRET = "f0b66069c04c5b140a8692189513e0a8243bff5d3082133a";
|
||||
|
||||
/**
|
||||
* check 的 data 业务 JSON(必须是一整段字符串,和 Postman 里 data 字段一致)。
|
||||
* create / cancel 换成对应业务 JSON 即可。
|
||||
*/
|
||||
private static final String DATA_JSON = "{"
|
||||
+ "\"checkinDate\":\"2026-10-21\","
|
||||
+ "\"checkoutDate\":\"2026-10-22\","
|
||||
+ "\"hotelId\":\"FZA64210066\","
|
||||
+ "\"productId\":\"15465800502898_-1_77159499\","
|
||||
+ "\"quantity\":1,"
|
||||
+ "\"roomTypeId\":\"77159499\","
|
||||
+ "\"totalPrice\":87.56"
|
||||
+ "}";
|
||||
|
||||
private HuidingOrderSignMain() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Map<String, Object> body = HuidingSignHelper.buildEnvelope(APP_ID, SECRET, DATA_JSON);
|
||||
System.out.println("===== 复制下面整段到 Postman Body(raw JSON),不要改任何字段 =====");
|
||||
System.out.println(JSONUtil.toJsonStr(body));
|
||||
System.out.println("===== 单字段 =====");
|
||||
System.out.println("appId = " + body.get("appId"));
|
||||
System.out.println("nonce = " + body.get("nonce"));
|
||||
System.out.println("signType = " + body.get("signType"));
|
||||
System.out.println("timeStamp= " + body.get("timeStamp"));
|
||||
System.out.println("sign = " + body.get("sign"));
|
||||
System.out.println("POST https://www.zianxin.com/api/supermarket/third/party/huiding/order/check");
|
||||
}
|
||||
}
|
||||
+47
@@ -5,7 +5,11 @@ import cn.hutool.core.util.StrUtil;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -15,9 +19,52 @@ public final class HuidingSignHelper {
|
||||
|
||||
public static final String SIGN_TYPE_HMAC = "HMAC-SHA256";
|
||||
|
||||
private static final DateTimeFormatter TS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private static final String NONCE_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
private HuidingSignHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成慧订入站整包(check/create/cancel 外层),可直接贴进 Postman。
|
||||
* @param appId order-app-id
|
||||
* @param secret order-app-secret(只参与本地算签,不会写进返回字段)
|
||||
* @param dataJson 业务 JSON 字符串,原样参与签名
|
||||
*/
|
||||
public static Map<String, Object> buildEnvelope(String appId, String secret, String dataJson) {
|
||||
return buildEnvelope(appId, secret, dataJson, null, null);
|
||||
}
|
||||
|
||||
public static Map<String, Object> buildEnvelope(String appId, String secret, String dataJson, String nonce,
|
||||
String timeStamp) {
|
||||
if (StrUtil.hasBlank(appId, secret, dataJson)) {
|
||||
throw new IllegalArgumentException("appId / secret / data 不能为空");
|
||||
}
|
||||
String n = StrUtil.blankToDefault(StrUtil.trim(nonce), randomNonce());
|
||||
String ts = StrUtil.blankToDefault(StrUtil.trim(timeStamp), LocalDateTime.now().format(TS));
|
||||
String data = dataJson.trim();
|
||||
Map<String, Object> signMap = new LinkedHashMap<>();
|
||||
signMap.put("appId", appId.trim());
|
||||
signMap.put("data", data);
|
||||
signMap.put("nonce", n);
|
||||
signMap.put("signType", SIGN_TYPE_HMAC);
|
||||
signMap.put("timeStamp", ts);
|
||||
String sign = sign(signMap, secret.trim());
|
||||
Map<String, Object> out = new LinkedHashMap<>(signMap);
|
||||
out.put("sign", sign);
|
||||
return out;
|
||||
}
|
||||
|
||||
public static String randomNonce() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
char[] chars = new char[32];
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
chars[i] = NONCE_CHARS.charAt(random.nextInt(NONCE_CHARS.length()));
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data 参与签名的字段(不含 sign;空值跳过);最后拼接 secret=密钥
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user