feat:外部接口接入
This commit is contained in:
@@ -239,6 +239,7 @@ public enum ErrorCodeEnum {
|
|||||||
TALLY_BOOK_NOT_EXIST(180001, "记账本数据不存在", null),
|
TALLY_BOOK_NOT_EXIST(180001, "记账本数据不存在", null),
|
||||||
|
|
||||||
THIRD_API_ERROR(151001,"第三方服务异常->{0}",null),
|
THIRD_API_ERROR(151001,"第三方服务异常->{0}",null),
|
||||||
|
THIRD_API_SIGN_ERROR(151002,"签名失败->{0}",null),
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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<String, String> 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<String, String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,7 @@ import javax.crypto.Mac;
|
|||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,11 +17,6 @@ import java.util.*;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class SignatureUtils {
|
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 签名
|
* 生成 HmacSHA256 签名
|
||||||
* @param data 待签名字符串
|
* @param data 待签名字符串
|
||||||
@@ -34,8 +25,8 @@ public class SignatureUtils {
|
|||||||
*/
|
*/
|
||||||
public static String hmacSha256(String data, String secret) {
|
public static String hmacSha256(String data, String secret) {
|
||||||
try {
|
try {
|
||||||
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||||
mac.init(secretKey);
|
mac.init(secretKey);
|
||||||
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||||
return bytesToHex(hash).toLowerCase();
|
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<String, String> 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<String, String> 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)
|
* 生成待签名字符串(参数按字母排序 + appkey + timestamp)
|
||||||
* @param params 请求参数Map(需提前过滤空值)
|
* @param params 请求参数Map(需提前过滤空值)
|
||||||
@@ -130,7 +70,4 @@ public class SignatureUtils {
|
|||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
package com.cool.store.service.impl;
|
package com.cool.store.service.impl;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
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.request.huoma.ShopBasicInfoRequest;
|
||||||
import com.cool.store.response.bigdata.ApiResponse;
|
import com.cool.store.response.bigdata.ApiResponse;
|
||||||
import com.cool.store.response.huoma.ShopBaseInfoResponse;
|
import com.cool.store.response.huoma.ShopBaseInfoResponse;
|
||||||
import com.cool.store.response.oppty.OpportunityApiResponse;
|
import com.cool.store.response.oppty.OpportunityApiResponse;
|
||||||
import com.cool.store.service.HuoMaService;
|
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.cool.store.utils.SignatureUtils;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.JavaType;
|
import com.fasterxml.jackson.databind.JavaType;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import okhttp3.*;
|
import okhttp3.*;
|
||||||
import org.apache.poi.ss.formula.functions.T;
|
import org.apache.poi.ss.formula.functions.T;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -24,6 +30,7 @@ import java.util.Map;
|
|||||||
* @Version 1.0
|
* @Version 1.0
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
|
@Slf4j
|
||||||
public class HuoMaServiceImpl implements HuoMaService {
|
public class HuoMaServiceImpl implements HuoMaService {
|
||||||
|
|
||||||
@Value("${api.auth.url}")
|
@Value("${api.auth.url}")
|
||||||
@@ -45,42 +52,130 @@ public class HuoMaServiceImpl implements HuoMaService {
|
|||||||
@Override
|
@Override
|
||||||
public ShopBaseInfoResponse getShopInfo(ShopBasicInfoRequest requestBody) {
|
public ShopBaseInfoResponse getShopInfo(ShopBasicInfoRequest requestBody) {
|
||||||
String apiUrl = url + "/dzgV1/shop/getShopBasicInfo";
|
String apiUrl = url + "/dzgV1/shop/getShopBasicInfo";
|
||||||
|
return executeApiCall(apiUrl,requestBody,ShopBaseInfoResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, String> 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. 执行请求
|
private <T> T executeApiCall(String url, Object requestBody, Class<T> responseType) {
|
||||||
try (Response response = okHttpClient.newCall(request).execute()) {
|
// 1. 打印请求前日志
|
||||||
|
logRequest(url, requestBody);
|
||||||
|
|
||||||
String responseBody = response.body().string();
|
try {
|
||||||
|
Request request = buildRequest(requestBody, url);
|
||||||
|
|
||||||
JavaType javaType = objectMapper.getTypeFactory()
|
try (Response response = okHttpClient.newCall(request).execute()) {
|
||||||
.constructParametricType(ApiResponse.class, ShopBaseInfoResponse.class);
|
// 2. 获取原始响应内容
|
||||||
|
String responseBody = response.body().string();
|
||||||
|
|
||||||
ApiResponse<ShopBaseInfoResponse> apiResponse = objectMapper.readValue(responseBody, javaType);
|
// 3. 打印响应日志
|
||||||
|
logResponse(url, response.code(), responseBody);
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
throw new IOException("请求失败: " + response.code());
|
throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR,
|
||||||
|
"HTTP请求失败,状态码: " + response.code());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 解析响应
|
||||||
|
JavaType javaType = objectMapper.getTypeFactory()
|
||||||
|
.constructParametricType(OpportunityApiResponse.class, responseType);
|
||||||
|
|
||||||
|
OpportunityApiResponse<T> 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 (ServiceException e) {
|
||||||
} catch (IOException e) {
|
throw e;
|
||||||
e.printStackTrace();
|
} 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<String, String> 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user