大家好,我是小悟。
一、需求描述
假设我们需要对接一个第三方支付系统,实现以下功能:
- 调用第三方支付接口,生成支付订单
- 接收第三方支付系统的异步回调通知
- 查询订单支付状态
- 实现接口签名验证、异常重试、日志记录等企业级特性
第三方系统信息:
- 接口地址:
https://api.partner.com/v1/pay - 请求方式:POST(JSON格式)
- 签名方式:MD5签名(参数排序后拼接密钥)
- 回调地址:
https://yourdomain.com/api/pay/callback
二、详细实现步骤
1. 项目依赖配置(pom.xml)
xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- HTTP客户端 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Redis(用于分布式限流和幂等) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2. 配置文件(application.yml)
yaml
server:
port: 8080
third-party:
pay:
api-url: https://api.partner.com/v1/pay
query-url: https://api.partner.com/v1/query
app-id: YOUR_APP_ID
app-secret: YOUR_APP_SECRET
callback-url: https://yourdomain.com/api/pay/callback
connect-timeout: 5000
read-timeout: 10000
logging:
level:
com.example: DEBUG
3. 配置类
arduino
package com.example.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "third-party.pay")
public class ThirdPartyConfig {
private String apiUrl;
private String queryUrl;
private String appId;
private String appSecret;
private String callbackUrl;
private int connectTimeout;
private int readTimeout;
}
4. HTTP客户端配置
kotlin
package com.example.config;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HttpClientConfig {
@Autowired
private ThirdPartyConfig thirdPartyConfig;
@Bean
public PoolingHttpClientConnectionManager connectionManager() {
PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
manager.setMaxTotal(200);
manager.setDefaultMaxPerRoute(20);
return manager;
}
@Bean
public CloseableHttpClient httpClient(PoolingHttpClientConnectionManager manager) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(thirdPartyConfig.getConnectTimeout())
.setSocketTimeout(thirdPartyConfig.getReadTimeout())
.setConnectionRequestTimeout(3000)
.build();
return HttpClients.custom()
.setConnectionManager(manager)
.setDefaultRequestConfig(requestConfig)
.build();
}
}
5. 签名工具类
java
package com.example.util;
import lombok.extern.slf4j.Slf4j;
import java.security.MessageDigest;
import java.util.Map;
import java.util.TreeMap;
@Slf4j
public class SignUtil {
/**
* 生成MD5签名
* @param params 参数集合
* @param secret 密钥
* @return 签名字符串
*/
public static String generateSign(Map<String, String> params, String secret) {
// 1. 参数排序
TreeMap<String, String> sortedParams = new TreeMap<>(params);
// 2. 拼接字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
sb.append("key=").append(secret);
// 3. MD5加密
return md5(sb.toString()).toUpperCase();
}
/**
* 验证签名
*/
public static boolean verifySign(Map<String, String> params, String secret, String sign) {
String generateSign = generateSign(params, secret);
return generateSign.equals(sign);
}
private static String md5(String source) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(source.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
sb.append("0");
}
sb.append(hex);
}
return sb.toString();
} catch (Exception e) {
log.error("MD5加密失败", e);
throw new RuntimeException("MD5加密失败");
}
}
}
6. 请求和响应DTO
typescript
// 请求DTO
package com.example.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class PayRequestDTO {
private String outTradeNo; // 商户订单号
private BigDecimal amount; // 金额
private String subject; // 商品标题
private String body; // 商品描述
private String returnUrl; // 同步跳转地址
}
// 第三方请求参数
@Data
public class ThirdPartyPayParam {
private String appId;
private String outTradeNo;
private String amount;
private String subject;
private String body;
private String returnUrl;
private String timestamp;
private String sign;
}
// 响应DTO
@Data
public class ThirdPartyResponse {
private String code;
private String message;
private String tradeNo;
private String payUrl;
private String sign;
}
7. 第三方服务接口
arduino
package com.example.service;
import com.example.dto.PayRequestDTO;
import com.example.dto.ThirdPartyResponse;
public interface ThirdPartyPayService {
/**
* 创建支付订单
*/
ThirdPartyResponse createOrder(PayRequestDTO payRequest);
/**
* 查询订单状态
*/
ThirdPartyResponse queryOrder(String outTradeNo);
/**
* 处理异步回调
*/
boolean handleCallback(Map<String, String> callbackParams);
}
8. 第三方服务实现
java
package com.example.service.impl;
import com.alibaba.fastjson.JSON;
import com.example.config.ThirdPartyConfig;
import com.example.dto.PayRequestDTO;
import com.example.dto.ThirdPartyParam;
import com.example.dto.ThirdPartyResponse;
import com.example.service.ThirdPartyPayService;
import com.example.util.SignUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class ThirdPartyPayServiceImpl implements ThirdPartyPayService {
@Autowired
private CloseableHttpClient httpClient;
@Autowired
private ThirdPartyConfig config;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public ThirdPartyResponse createOrder(PayRequestDTO payRequest) {
log.info("开始创建支付订单,订单号:{}", payRequest.getOutTradeNo());
try {
// 1. 构建请求参数
Map<String, String> params = buildRequestParams(payRequest);
// 2. 生成签名
String sign = SignUtil.generateSign(params, config.getAppSecret());
params.put("sign", sign);
// 3. 发送HTTP请求
String responseStr = doPost(config.getApiUrl(), params);
// 4. 解析响应
ThirdPartyResponse response = JSON.parseObject(responseStr, ThirdPartyResponse.class);
// 5. 验证响应签名
if (!verifyResponseSign(response)) {
log.error("响应签名验证失败");
throw new RuntimeException("响应签名验证失败");
}
log.info("创建支付订单成功,订单号:{},第三方订单号:{}",
payRequest.getOutTradeNo(), response.getTradeNo());
return response;
} catch (Exception e) {
log.error("创建支付订单失败", e);
throw new RuntimeException("调用第三方支付接口失败", e);
}
}
@Override
public ThirdPartyResponse queryOrder(String outTradeNo) {
log.info("查询订单状态,订单号:{}", outTradeNo);
try {
Map<String, String> params = new HashMap<>();
params.put("appId", config.getAppId());
params.put("outTradeNo", outTradeNo);
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
String sign = SignUtil.generateSign(params, config.getAppSecret());
params.put("sign", sign);
String responseStr = doPost(config.getQueryUrl(), params);
ThirdPartyResponse response = JSON.parseObject(responseStr, ThirdPartyResponse.class);
return response;
} catch (Exception e) {
log.error("查询订单失败", e);
throw new RuntimeException("查询订单失败", e);
}
}
@Override
public boolean handleCallback(Map<String, String> callbackParams) {
log.info("处理支付回调,参数:{}", callbackParams);
// 1. 幂等性校验(使用Redis)
String outTradeNo = callbackParams.get("outTradeNo");
String idempotentKey = "pay:callback:" + outTradeNo;
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "processing", 10, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(success)) {
log.warn("重复的回调请求,订单号:{}", outTradeNo);
return true;
}
try {
// 2. 验证签名
String sign = callbackParams.get("sign");
Map<String, String> paramsWithoutSign = new HashMap<>(callbackParams);
paramsWithoutSign.remove("sign");
if (!SignUtil.verifySign(paramsWithoutSign, config.getAppSecret(), sign)) {
log.error("回调签名验证失败,订单号:{}", outTradeNo);
return false;
}
// 3. 验证订单状态
String tradeStatus = callbackParams.get("tradeStatus");
if ("SUCCESS".equals(tradeStatus)) {
// 4. 更新本地订单状态(调用业务service)
updateLocalOrderStatus(outTradeNo, callbackParams.get("tradeNo"));
log.info("订单支付成功,订单号:{}", outTradeNo);
}
// 5. 更新Redis状态
redisTemplate.opsForValue().set(idempotentKey, "completed", 7, TimeUnit.DAYS);
return true;
} catch (Exception e) {
log.error("处理回调失败", e);
redisTemplate.delete(idempotentKey);
return false;
}
}
private Map<String, String> buildRequestParams(PayRequestDTO payRequest) {
Map<String, String> params = new HashMap<>();
params.put("appId", config.getAppId());
params.put("outTradeNo", payRequest.getOutTradeNo());
params.put("amount", payRequest.getAmount().toString());
params.put("subject", payRequest.getSubject());
params.put("body", payRequest.getBody());
params.put("returnUrl", config.getCallbackUrl());
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
return params;
}
private String doPost(String url, Map<String, String> params) throws IOException {
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Accept", "application/json");
String json = JSON.toJSONString(params);
httpPost.setEntity(new StringEntity(json, "UTF-8"));
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String responseStr = EntityUtils.toString(response.getEntity(), "UTF-8");
log.debug("第三方接口响应:{}", responseStr);
return responseStr;
}
}
private boolean verifyResponseSign(ThirdPartyResponse response) {
Map<String, String> params = new HashMap<>();
params.put("code", response.getCode());
params.put("message", response.getMessage());
params.put("tradeNo", response.getTradeNo());
return SignUtil.verifySign(params, config.getAppSecret(), response.getSign());
}
private void updateLocalOrderStatus(String outTradeNo, String tradeNo) {
// TODO: 调用业务Service更新订单状态
log.info("更新订单状态,订单号:{},第三方订单号:{}", outTradeNo, tradeNo);
}
}
9. 控制器
typescript
package com.example.controller;
import com.example.dto.PayRequestDTO;
import com.example.dto.ThirdPartyResponse;
import com.example.service.ThirdPartyPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/pay")
public class PayController {
@Autowired
private ThirdPartyPayService payService;
/**
* 创建支付订单
*/
@PostMapping("/create")
public ThirdPartyResponse createOrder(@RequestBody PayRequestDTO payRequest) {
return payService.createOrder(payRequest);
}
/**
* 查询订单
*/
@GetMapping("/query/{outTradeNo}")
public ThirdPartyResponse queryOrder(@PathVariable String outTradeNo) {
return payService.queryOrder(outTradeNo);
}
/**
* 异步回调接口
*/
@PostMapping("/callback")
public String callback(HttpServletRequest request) {
try {
// 获取所有回调参数
Map<String, String> params = new HashMap<>();
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
boolean result = payService.handleCallback(params);
if (result) {
return "success";
} else {
return "fail";
}
} catch (Exception e) {
log.error("处理回调异常", e);
return "fail";
}
}
}
10. 全局异常处理
typescript
package com.example.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Map<String, Object> handleException(Exception e) {
log.error("系统异常", e);
Map<String, Object> result = new HashMap<>();
result.put("code", "500");
result.put("message", e.getMessage());
return result;
}
}
11. 重试机制(使用Spring Retry)
typescript
// 添加依赖
// <dependency>
// <groupId>org.springframework.retry</groupId>
// <artifactId>spring-retry</artifactId>
// </dependency>
package com.example.service.impl;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class RetryableService {
@Retryable(
value = {IOException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public String callThirdPartyWithRetry(String url, Map<String, String> params) {
// 调用第三方接口,失败会自动重试3次,间隔1秒、2秒、4秒
return doPost(url, params);
}
}
三、总结
核心要点
- 设计原则
- 隔离性:封装第三方接口调用逻辑,业务代码与对接代码分离
- 健壮性:做好异常处理、超时控制、重试机制
- 幂等性:使用Redis或数据库唯一键防止重复处理
- 关键技术点
- HTTP客户端连接池管理,提高性能
- 签名验证机制,保证数据安全
- 异步回调处理,使用消息队列解耦(可选)
- 日志记录完整请求响应,便于问题排查
- 最佳实践
- 配置外部化:所有第三方配置放在配置文件中
- 监控告警:对接入的第三方服务进行健康检查和监控
- 降级熔断:使用Sentinel或Hystrix进行保护
- 单元测试:Mock第三方接口进行测试
- 常见问题解决
- 超时问题:合理设置连接超时和读取超时
- 签名错误:严格按照第三方文档顺序拼接参数
- 重复回调:使用Redis实现幂等性控制
- 数据一致:使用本地消息表+定时任务保证最终一致性
- 扩展
- 使用消息队列(RabbitMQ/RocketMQ)处理异步回调
- 引入分布式链路追踪(SkyWalking/Jaeger)
- 实现接口限流,防止第三方系统故障拖垮自身
- 建立降级策略,第三方不可用时返回兜底数据
通过以上步骤,实现了一个完整的第三方系统对接方案,涵盖了请求、响应、签名、回调、异常处理等所有环节。

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海