diff --git a/coolstore-partner-common/src/main/java/com/cool/store/enums/ErrorCodeEnum.java b/coolstore-partner-common/src/main/java/com/cool/store/enums/ErrorCodeEnum.java index 7c8790b24..72ccb58c6 100644 --- a/coolstore-partner-common/src/main/java/com/cool/store/enums/ErrorCodeEnum.java +++ b/coolstore-partner-common/src/main/java/com/cool/store/enums/ErrorCodeEnum.java @@ -239,6 +239,7 @@ public enum ErrorCodeEnum { TALLY_BOOK_NOT_EXIST(180001, "记账本数据不存在", null), THIRD_API_ERROR(151001,"第三方服务异常->{0}",null), + THIRD_API_SIGN_ERROR(151002,"签名失败->{0}",null), ; diff --git a/coolstore-partner-common/src/main/java/com/cool/store/utils/HmacSigner.java b/coolstore-partner-common/src/main/java/com/cool/store/utils/HmacSigner.java new file mode 100644 index 000000000..3da0383b7 --- /dev/null +++ b/coolstore-partner-common/src/main/java/com/cool/store/utils/HmacSigner.java @@ -0,0 +1,76 @@ +package com.cool.store.utils; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.ZonedDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * @Author suzhuhong + * @Date 2025/4/2 9:36 + * @Version 1.0 + */ + +public class HmacSigner { + + private static final String HMAC_ALGORITHM = "HmacSHA256"; + private static final DateTimeFormatter GMT_FORMATTER = + DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH) + .withZone(ZoneId.of("GMT")); + + /** + * 生成与Postman完全一致的签名头 + */ + public static Map generateHeaders( + String username, + String secret, + String requestBody) throws Exception { + + // 1. 生成RFC 1123格式时间戳 + String curDate = ZonedDateTime.now(ZoneId.of("GMT")) + .format(GMT_FORMATTER); + + // 2. 计算请求体摘要 + String computedDigest = computeDigest(requestBody); + + // 3. 构建签名字符串 + String signingString = "x-date: " + curDate + "\n" + "digest: " + computedDigest; + + // 4. 计算HMAC签名 + String signature = hmacSha256(secret, signingString); + + // 5. 组装Authorization头 + String authorization = String.format( + "hmac username=\"%s\", algorithm=\"hmac-sha256\", headers=\"x-date digest\", signature=\"%s\"", + username, + signature + ); + + // 6. 返回头信息Map + Map headers = new HashMap<>(); + headers.put("x-Date", curDate); + headers.put("Digest", computedDigest); + headers.put("Authorization", authorization); + return headers; + } + + private static String computeDigest(String data) throws Exception { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(data.getBytes(StandardCharsets.UTF_8)); + return "SHA-256=" + Base64.getEncoder().encodeToString(hash); + } + + private static String hmacSha256(String secret, String data) throws Exception { + Mac mac = Mac.getInstance(HMAC_ALGORITHM); + mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM)); + byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hash); + } +} \ No newline at end of file diff --git a/coolstore-partner-common/src/main/java/com/cool/store/utils/SignatureUtils.java b/coolstore-partner-common/src/main/java/com/cool/store/utils/SignatureUtils.java index f2ca40e55..07f69a9dd 100644 --- a/coolstore-partner-common/src/main/java/com/cool/store/utils/SignatureUtils.java +++ b/coolstore-partner-common/src/main/java/com/cool/store/utils/SignatureUtils.java @@ -6,11 +6,7 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; /** @@ -21,11 +17,6 @@ import java.util.*; @Slf4j public class SignatureUtils { - private static final String HMAC_ALGORITHM = "HmacSHA256"; - private static final DateTimeFormatter RFC_1123_FORMATTER = - DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH) - .withZone(ZoneId.of("GMT")); - /** * 生成 HmacSHA256 签名 * @param data 待签名字符串 @@ -34,8 +25,8 @@ public class SignatureUtils { */ public static String hmacSha256(String data, String secret) { try { - Mac mac = Mac.getInstance(HMAC_ALGORITHM); - SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM); + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(secretKey); byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return bytesToHex(hash).toLowerCase(); @@ -44,57 +35,6 @@ public class SignatureUtils { } } - - /** - * 生成与Postman一致的签名头 - * @param username API用户名 - * @param secret API密钥 - * @param requestBody 请求体JSON字符串 - * @return 包含所有签名头的Map - */ - public static Map generateHeaders(String username, - String secret, - String requestBody) { - // 1. 生成RFC 1123格式时间戳 - String date = ZonedDateTime.now(ZoneId.of("GMT")).format(RFC_1123_FORMATTER); - - - // 2. 计算请求体摘要 - String digest = "SHA-256=" + calculateDigest(requestBody); - - // 3. 构建签名字符串 - String signingString = "x-date: " + date + "\n" + "digest: " + digest; - - // 4. 计算HMAC签名 - String signature = hmacSha256( signingString,secret); - - // 5. 组装Authorization头 - String authorization = String.format( - "hmac username=\"%s\", algorithm=\"hmac-sha256\", headers=\"x-date digest\", signature=\"%s\"", - username, - signature - ); - - // 6. 返回所有头信息 - Map headers = new HashMap<>(); - headers.put("x-date", date); - headers.put("Digest", digest); - headers.put("Authorization", authorization); - return headers; - } - - private static String calculateDigest(String data) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] hash = md.digest(data.getBytes(StandardCharsets.UTF_8)); - return Base64.getEncoder().encodeToString(hash); - } catch (Exception e) { - throw new RuntimeException("Digest计算失败", e); - } - } - - - /** * 生成待签名字符串(参数按字母排序 + appkey + timestamp) * @param params 请求参数Map(需提前过滤空值) @@ -130,7 +70,4 @@ public class SignatureUtils { } return sb.toString(); } - - - } diff --git a/coolstore-partner-service/src/main/java/com/cool/store/service/impl/HuoMaServiceImpl.java b/coolstore-partner-service/src/main/java/com/cool/store/service/impl/HuoMaServiceImpl.java index 4fe43a7e5..7f8f98232 100644 --- a/coolstore-partner-service/src/main/java/com/cool/store/service/impl/HuoMaServiceImpl.java +++ b/coolstore-partner-service/src/main/java/com/cool/store/service/impl/HuoMaServiceImpl.java @@ -1,14 +1,20 @@ package com.cool.store.service.impl; import com.alibaba.fastjson.JSONObject; +import com.cool.store.enums.ErrorCodeEnum; +import com.cool.store.exception.ServiceException; import com.cool.store.request.huoma.ShopBasicInfoRequest; import com.cool.store.response.bigdata.ApiResponse; import com.cool.store.response.huoma.ShopBaseInfoResponse; import com.cool.store.response.oppty.OpportunityApiResponse; import com.cool.store.service.HuoMaService; +import com.cool.store.utils.HmacSigner; +import com.cool.store.utils.JsonUtils; import com.cool.store.utils.SignatureUtils; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; import okhttp3.*; import org.apache.poi.ss.formula.functions.T; import org.springframework.beans.factory.annotation.Value; @@ -24,6 +30,7 @@ import java.util.Map; * @Version 1.0 */ @Service +@Slf4j public class HuoMaServiceImpl implements HuoMaService { @Value("${api.auth.url}") @@ -45,42 +52,130 @@ public class HuoMaServiceImpl implements HuoMaService { @Override public ShopBaseInfoResponse getShopInfo(ShopBasicInfoRequest requestBody) { String apiUrl = url + "/dzgV1/shop/getShopBasicInfo"; + return executeApiCall(apiUrl,requestBody,ShopBaseInfoResponse.class); + } - Map authHeaders = SignatureUtils.generateHeaders( - username, secret, JSONObject.toJSONString(requestBody) - ); - // 2. 构建请求体 - RequestBody body = RequestBody.create(okhttp3.MediaType.parse("application/json"), - JSONObject.toJSONString(requestBody)); - // 3. 构建请求 - Request request = new Request.Builder() - .url(apiUrl) - .post(body) - .addHeader("Content-Type", "application/json") - .addHeader("x-Date", authHeaders.get("x-date")) - .addHeader("Digest", authHeaders.get("Digest")) - .addHeader("Authorization", authHeaders.get("Authorization")) - .build(); - // 4. 执行请求 - try (Response response = okHttpClient.newCall(request).execute()) { + private T executeApiCall(String url, Object requestBody, Class responseType) { + // 1. 打印请求前日志 + logRequest(url, requestBody); - String responseBody = response.body().string(); + try { + Request request = buildRequest(requestBody, url); - JavaType javaType = objectMapper.getTypeFactory() - .constructParametricType(ApiResponse.class, ShopBaseInfoResponse.class); + try (Response response = okHttpClient.newCall(request).execute()) { + // 2. 获取原始响应内容 + String responseBody = response.body().string(); - ApiResponse apiResponse = objectMapper.readValue(responseBody, javaType); + // 3. 打印响应日志 + logResponse(url, response.code(), responseBody); - if (!response.isSuccessful()) { - throw new IOException("请求失败: " + response.code()); + if (!response.isSuccessful()) { + throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR, + "HTTP请求失败,状态码: " + response.code()); + } + + // 4. 解析响应 + JavaType javaType = objectMapper.getTypeFactory() + .constructParametricType(OpportunityApiResponse.class, responseType); + + OpportunityApiResponse apiResponse = objectMapper.readValue(responseBody, javaType); + + if (apiResponse.getCode() != 200) { + throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR, + "业务逻辑错误: " + apiResponse.getMsg()); + } + + return apiResponse.getData(); } - return apiResponse.getData(); - } catch (IOException e) { - e.printStackTrace(); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + log.error("API调用异常 - URL: {}, 错误: {}", url, e.getMessage(), e); + throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR, "接口调用异常: " + e.getMessage()); + } + } + + + /** + * 记录请求日志 + */ + private void logRequest(String url, Object requestBody) { + if (log.isInfoEnabled()) { + try { + log.info("\n======= 请求开始 =======\n" + + "API地址: {}\n" + + "请求参数: {}\n" + + "======= 请求结束 =======", + url, + objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(requestBody)); + } catch (JsonProcessingException e) { + log.warn("日志JSON序列化失败", e); + } + } + } + + /** + * 记录响应日志 + */ + private void logResponse(String url, int statusCode, String responseBody) { + if (log.isInfoEnabled()) { + try { + // 尝试美化JSON输出 + Object json = objectMapper.readValue(responseBody, Object.class); + String prettyResponse = objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(json); + + log.info("\n======= 响应开始 =======\n" + + "API地址: {}\n" + + "HTTP状态码: {}\n" + + "响应内容: {}\n" + + "======= 响应结束 =======", + url, + statusCode, + prettyResponse); + } catch (Exception e) { + // 非JSON响应或解析失败时直接输出原始内容 + log.info("\n======= 响应开始 =======\n" + + "API地址: {}\n" + + "HTTP状态码: {}\n" + + "原始响应: {}\n" + + "======= 响应结束 =======", + url, + statusCode, + responseBody); + } + } + } + + + + private Request buildRequest(Object requestBody, String url) { + + try { + Map authHeaders = HmacSigner.generateHeaders( + username, secret, JSONObject.toJSONString(requestBody)); + + log.debug("签名生成 - 签名结果: {}", JSONObject.toJSONString(authHeaders)); + + RequestBody body = RequestBody.create(MediaType.parse("application/json"), + JSONObject.toJSONString(requestBody) + ); + + return new Request.Builder() + .url(url) + .post(body) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .addHeader("x-Date", authHeaders.get("x-Date")) + .addHeader("Digest", authHeaders.get("Digest")) + .addHeader("Authorization", authHeaders.get("Authorization")) + .build(); + } catch (Exception e) { + throw new ServiceException(ErrorCodeEnum.THIRD_API_SIGN_ERROR); } - return null; } }