commit 457e43fca832192c1f2c00ff297e932502a6dc8e
Author: wangxiu <1422795692@qq.com>
Date: Thu Jul 9 12:28:44 2026 +0800
init commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1ae0b7b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+sh
+.claude/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..46613a1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,8 @@
+FROM openjdk:17.0.2-slim
+
+WORKDIR /app/api
+COPY target/lyxh-api-docking-1.0.0.jar /app/api/lyxh-api-docking.jar
+
+EXPOSE 8049
+
+ENTRYPOINT ["java", "-jar", "/app/api/lyxh-api-docking.jar"]
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..299529e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.7
+
+
+
+ com.lyxh
+ lyxh-api-docking
+ 1.0.0
+ lyxh-api-docking
+ Fortispay API docking service
+
+
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ 3.5.7
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+ commons-codec
+ commons-codec
+
+
+ org.springframework.boot
+ spring-boot-starter-quartz
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/src/main/java/com/lyxh/api/docking/LyxhApiDockingApplication.java b/src/main/java/com/lyxh/api/docking/LyxhApiDockingApplication.java
new file mode 100644
index 0000000..201a625
--- /dev/null
+++ b/src/main/java/com/lyxh/api/docking/LyxhApiDockingApplication.java
@@ -0,0 +1,14 @@
+package com.lyxh.api.docking;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+@MapperScan("com.lyxh.api.docking.mapper")
+public class LyxhApiDockingApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(LyxhApiDockingApplication.class, args);
+ }
+}
diff --git a/src/main/java/com/lyxh/api/docking/auth/AuthStrategy.java b/src/main/java/com/lyxh/api/docking/auth/AuthStrategy.java
new file mode 100644
index 0000000..2ebb23e
--- /dev/null
+++ b/src/main/java/com/lyxh/api/docking/auth/AuthStrategy.java
@@ -0,0 +1,15 @@
+package com.lyxh.api.docking.auth;
+
+import org.springframework.http.HttpHeaders;
+
+/**
+ * 认证策略接口 - 根据 auth_config JSON 构建请求头
+ */
+public interface AuthStrategy {
+
+ /** 返回此策略对应的 auth_type 标识 */
+ String getAuthType();
+
+ /** 根据 auth_config、请求唯一标识、语言标识构建认证相关的 HTTP 请求头 */
+ HttpHeaders prepareHeaders(String authConfigJson, String requestId, String language);
+}
diff --git a/src/main/java/com/lyxh/api/docking/auth/AuthStrategyFactory.java b/src/main/java/com/lyxh/api/docking/auth/AuthStrategyFactory.java
new file mode 100644
index 0000000..19a68a9
--- /dev/null
+++ b/src/main/java/com/lyxh/api/docking/auth/AuthStrategyFactory.java
@@ -0,0 +1,30 @@
+package com.lyxh.api.docking.auth;
+
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 认证策略工厂 - 根据 auth_type 返回对应的 AuthStrategy 实现
+ */
+@Component
+public class AuthStrategyFactory {
+
+ private final Map strategyMap;
+
+ public AuthStrategyFactory(List strategies) {
+ this.strategyMap = strategies.stream()
+ .collect(Collectors.toMap(AuthStrategy::getAuthType, Function.identity()));
+ }
+
+ public AuthStrategy getStrategy(String authType) {
+ AuthStrategy strategy = strategyMap.get(authType);
+ if (strategy == null) {
+ throw new IllegalArgumentException("Unsupported auth_type: " + authType);
+ }
+ return strategy;
+ }
+}
diff --git a/src/main/java/com/lyxh/api/docking/auth/FortispayHmacAuthStrategy.java b/src/main/java/com/lyxh/api/docking/auth/FortispayHmacAuthStrategy.java
new file mode 100644
index 0000000..36926db
--- /dev/null
+++ b/src/main/java/com/lyxh/api/docking/auth/FortispayHmacAuthStrategy.java
@@ -0,0 +1,59 @@
+package com.lyxh.api.docking.auth;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.lyxh.api.docking.exception.ApiCallException;
+import com.lyxh.api.docking.service.SignatureService;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+
+/**
+ * HMAC-SHA256 签名认证(富通等第三方接口)
+ *
+ * auth_config 格式: {"secret_id":"xxx","secret_key":"yyy"}
+ * 在请求头中添加: requestId, secretId, timestamp, signature
+ */
+@Component
+public class FortispayHmacAuthStrategy implements AuthStrategy {
+
+ private final SignatureService signatureService;
+ private final ObjectMapper objectMapper;
+
+ public FortispayHmacAuthStrategy(SignatureService signatureService, ObjectMapper objectMapper) {
+ this.signatureService = signatureService;
+ this.objectMapper = objectMapper;
+ }
+
+ @Override
+ public String getAuthType() {
+ return "FORTISPAY_HMAC";
+ }
+
+ @Override
+ public HttpHeaders prepareHeaders(String authConfigJson, String requestId, String language) {
+ long timestamp = signatureService.currentTimestamp();
+
+ JsonNode node;
+ try {
+ node = objectMapper.readTree(authConfigJson);
+ } catch (Exception e) {
+ throw new ApiCallException("AUTH_CONFIG_ERROR",
+ "Failed to parse auth_config: " + e.getMessage(), "");
+ }
+ String secretId = node.get("secret_id").asText();
+ String secretKey = node.get("secret_key").asText();
+ String signature = signatureService.generateSignature(requestId, timestamp, secretId, secretKey);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.set("requestId", requestId);
+ headers.set("secretId", secretId);
+ headers.set("timestamp", String.valueOf(timestamp));
+ headers.set("signature", signature);
+ if (language != null && !language.isBlank()) {
+ headers.set("language", language);
+ }
+ return headers;
+ }
+}
diff --git a/src/main/java/com/lyxh/api/docking/client/ApiCallResult.java b/src/main/java/com/lyxh/api/docking/client/ApiCallResult.java
new file mode 100644
index 0000000..c121cde
--- /dev/null
+++ b/src/main/java/com/lyxh/api/docking/client/ApiCallResult.java
@@ -0,0 +1,24 @@
+package com.lyxh.api.docking.client;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class ApiCallResult {
+
+ /** 请求唯一标识 */
+ private final String requestId;
+ /** 实际请求URL */
+ private final String url;
+ /** 请求头JSON */
+ private final String reqHeadersJson;
+ /** 请求体JSON */
+ private final String reqBodyJson;
+ /** 响应体原始JSON */
+ private final String respBodyJson;
+ /** HTTP状态码 */
+ private final int httpStatusCode;
+ /** 请求耗时(毫秒) */
+ private final long costMillis;
+}
diff --git a/src/main/java/com/lyxh/api/docking/client/GenericApiClient.java b/src/main/java/com/lyxh/api/docking/client/GenericApiClient.java
new file mode 100644
index 0000000..933032b
--- /dev/null
+++ b/src/main/java/com/lyxh/api/docking/client/GenericApiClient.java
@@ -0,0 +1,151 @@
+package com.lyxh.api.docking.client;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.lyxh.api.docking.auth.AuthStrategy;
+import com.lyxh.api.docking.auth.AuthStrategyFactory;
+import com.lyxh.api.docking.entity.ThirdPartyConfig;
+import com.lyxh.api.docking.exception.ApiCallException;
+import com.lyxh.api.docking.util.AuthConfigCrypto;
+import com.lyxh.api.docking.util.HeaderDesensitizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.*;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.ResourceAccessException;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.time.Duration;
+
+@Component
+public class GenericApiClient {
+
+ private static final Logger log = LoggerFactory.getLogger(GenericApiClient.class);
+
+ private final AuthStrategyFactory authFactory;
+ private final AuthConfigCrypto crypto;
+ private final HeaderDesensitizer desensitizer;
+ private final ObjectMapper objectMapper;
+
+ public GenericApiClient(AuthStrategyFactory authFactory, AuthConfigCrypto crypto,
+ HeaderDesensitizer desensitizer, ObjectMapper objectMapper) {
+ this.authFactory = authFactory;
+ this.crypto = crypto;
+ this.desensitizer = desensitizer;
+ this.objectMapper = objectMapper;
+ }
+
+ /**
+ * 根据 DB 配置调用第三方 API,支持超时、代理、重试,auth_config 解密,请求头脱敏
+ */
+ public ApiCallResult call(ThirdPartyConfig config, Object requestBody,
+ String requestId, String language, String authType) {
+ String url = config.getApiUrl();
+ String authConfigDecrypted = crypto.decrypt(config.getAuthConfig());
+ AuthStrategy authStrategy = authFactory.getStrategy(authType);
+ HttpHeaders headers = authStrategy.prepareHeaders(authConfigDecrypted, requestId, language);
+
+ String reqBodyJson = toJson(requestBody);
+ String reqHeadersJson = toJson(desensitizer.mask(headersToMap(headers)));
+
+ int timeoutSeconds = config.getTimeoutSeconds() != null ? config.getTimeoutSeconds() : 30;
+ int maxRetries = config.getRetryTimes() != null ? config.getRetryTimes() : 0;
+ RestTemplate rt = buildRestTemplate(config, timeoutSeconds);
+
+ HttpEntity