别再重复造轮子了!SpringBoot对接第三方系统模板,拿来即用

大家好,我是小悟。

一、需求描述

假设我们需要对接一个第三方支付系统,实现以下功能:

  1. 调用第三方支付接口,生成支付订单
  2. 接收第三方支付系统的异步回调通知
  3. 查询订单支付状态
  4. 实现接口签名验证、异常重试、日志记录等企业级特性

第三方系统信息

  • 接口地址: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);
    }
}

三、总结

核心要点

  1. 设计原则
    • 隔离性:封装第三方接口调用逻辑,业务代码与对接代码分离
    • 健壮性:做好异常处理、超时控制、重试机制
    • 幂等性:使用Redis或数据库唯一键防止重复处理
  2. 关键技术点
    • HTTP客户端连接池管理,提高性能
    • 签名验证机制,保证数据安全
    • 异步回调处理,使用消息队列解耦(可选)
    • 日志记录完整请求响应,便于问题排查
  3. 最佳实践
    • 配置外部化:所有第三方配置放在配置文件中
    • 监控告警:对接入的第三方服务进行健康检查和监控
    • 降级熔断:使用Sentinel或Hystrix进行保护
    • 单元测试:Mock第三方接口进行测试
  4. 常见问题解决
    • 超时问题:合理设置连接超时和读取超时
    • 签名错误:严格按照第三方文档顺序拼接参数
    • 重复回调:使用Redis实现幂等性控制
    • 数据一致:使用本地消息表+定时任务保证最终一致性
  5. 扩展
    • 使用消息队列(RabbitMQ/RocketMQ)处理异步回调
    • 引入分布式链路追踪(SkyWalking/Jaeger)
    • 实现接口限流,防止第三方系统故障拖垮自身
    • 建立降级策略,第三方不可用时返回兜底数据

通过以上步骤,实现了一个完整的第三方系统对接方案,涵盖了请求、响应、签名、回调、异常处理等所有环节。

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

相关推荐
yaaakaaang2 小时前
十七、迭代器模式
java·迭代器模式
我爱cope2 小时前
【从0开始学设计模式-8| 桥接模式】
java·设计模式·桥接模式
程序员cxuan2 小时前
为什么 Claude 要求实名认证?
人工智能·后端·程序员
Lsk_Smion2 小时前
Hot100(开刷) 之 环形链表(II)-- 随机链表的复制 -- 翻转二叉树
java·后端·kotlin·力扣·hot100
indexsunny2 小时前
互联网大厂Java求职面试实战:Spring Boot与微服务架构解析
java·spring boot·redis·kafka·spring security·flyway·microservices
lulu12165440782 小时前
Claude Code Routines功能深度解析:24小时云端自动化开发指南
java·人工智能·python·ai编程
ch.ju2 小时前
Java程序设计(第3版)第二章——关系运算符
java
Tirzano2 小时前
springsession全能序列化方案
java·开发语言
我叫张土豆2 小时前
让 AI 学会用工具:基于 LangChain4j 的 Skills Agent 全栈落地实战
人工智能·spring boot