现在做电商项目,支付功能绝对是刚需。我做过不少电商系统,发现很多开发者最头疼的就是支付模块的接入。传统方式要处理各种加密、签名、回调,代码写起来特别繁琐。但用Spring Boot配合支付宝新版Easy SDK 2.0,整个过程能简化至少60%。
新版SDK最大的改进是把原来分散的API统一封装成了Factory模式。举个例子,老版本要调支付接口得处理十多个参数,现在只需要关注核心业务字段。我去年用旧版SDK接支付功能花了三天,上个月用新版重写同样功能,只用了半天就搞定了。
首先得去支付宝开放平台注册开发者账号。这里有个小技巧:建议直接注册企业账号,个人账号有些高级功能用不了。注册时准备营业执照扫描件,整个过程大约需要1个工作日审核。
注册成功后,进入"控制台"-"网页&移动应用",创建一个新应用。重点要获取三个东西:
安全起见,千万别把私钥直接写在代码里!我见过有人把私钥上传到GitHub导致资金损失的案例。推荐的做法是:
java复制// 安全的密钥读取方式示例
public static String readPrivateKey(String path) {
try {
return Files.readString(Paths.get(path), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("读取密钥文件失败", e);
}
}
用IDEA新建项目时,除了基础的Web依赖,还需要这些:
xml复制<dependencies>
<!-- 支付宝新版SDK -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-easysdk</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 用于处理金额精度 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
建议创建一个专门的配置类来管理支付宝参数:
java复制@Configuration
public class AlipayConfig {
@Value("${alipay.app-id}")
private String appId;
@Bean
public Config alipayConfig() {
Config config = new Config();
config.protocol = "https";
config.gatewayHost = "openapi.alipay.com"; // 正式环境
config.signType = "RSA2";
config.appId = appId;
config.merchantPrivateKey = readPrivateKey("/secure/keys/app_private_key.txt");
config.alipayPublicKey = readPublicKey("/secure/keys/alipay_public_key.txt");
config.notifyUrl = "https://yourdomain.com/callback";
return config;
}
}
实际项目中,我推荐把支付逻辑封装成独立服务。先看支付接口的实现:
java复制@Service
public class AlipayService {
@Autowired
private Config config;
public String createPayment(Order order) {
try {
Factory.setOptions(config);
AlipayTradePagePayResponse response = Factory.Payment.Page()
.pay(
order.getSubject(),
order.getOutTradeNo(),
order.getTotalAmount(),
order.getReturnUrl()
);
if (response.isSuccess()) {
return response.getBody();
} else {
throw new RuntimeException("支付宝接口调用失败: " + response.getMsg());
}
} catch (Exception e) {
throw new RuntimeException("创建支付失败", e);
}
}
}
几个关键点需要注意:
异步通知是支付系统最关键的环节。我遇到过因为通知处理不当导致订单状态不同步的问题。这是经过生产验证的代码:
java复制@RestController
@RequestMapping("/payment")
public class PaymentController {
@PostMapping("/notify")
public String handleNotify(HttpServletRequest request) {
// 将请求参数转换为Map
Map<String, String> params = convertParams(request);
try {
// 验签
boolean verified = Factory.Payment.Common().verifyNotify(params);
if (verified) {
String tradeStatus = params.get("trade_status");
String outTradeNo = params.get("out_trade_no");
if ("TRADE_SUCCESS".equals(tradeStatus)) {
// 更新订单状态为已支付
orderService.updateOrderStatus(outTradeNo, PAID);
return "success";
}
}
return "failure";
} catch (Exception e) {
log.error("处理支付通知异常", e);
return "failure";
}
}
private Map<String, String> convertParams(HttpServletRequest request) {
// 转换逻辑...
}
}
重要提示:
支付状态查询是很多开发者容易忽略的功能。当异步通知没收到时,主动查询就特别有用:
java复制public PaymentStatus queryPayment(String outTradeNo) {
try {
AlipayTradeQueryResponse response = Factory.Payment.Common()
.query(outTradeNo);
if (response.isSuccess()) {
String status = response.getTradeStatus();
return convertStatus(status);
} else {
throw new PaymentException("查询失败: " + response.getSubMsg());
}
} catch (Exception e) {
throw new PaymentException("查询支付状态异常", e);
}
}
private PaymentStatus convertStatus(String alipayStatus) {
switch (alipayStatus) {
case "WAIT_BUYER_PAY": return WAITING;
case "TRADE_SUCCESS": return SUCCESS;
case "TRADE_FINISHED": return FINISHED;
case "TRADE_CLOSED": return CLOSED;
default: return UNKNOWN;
}
}
支付模块必须考虑安全性。我们项目上线前做了这些防护:
建议添加这个拦截器:
java复制@Component
public class PaymentInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 验证IP白名单
if (!isTrustedIp(request.getRemoteAddr())) {
throw new SecurityException("非法访问");
}
// 验证签名
if (!verifySign(request)) {
throw new SecurityException("签名验证失败");
}
return true;
}
}
在支付过程中,我踩过这些坑:
这是我们的异常处理策略:
java复制@RestControllerAdvice
public class PaymentExceptionHandler {
@ExceptionHandler(PaymentException.class)
public ResponseEntity<ErrorResult> handlePaymentException(PaymentException e) {
ErrorResult result = new ErrorResult(e.getCode(), e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResult> handleException(Exception e) {
log.error("支付系统异常", e);
ErrorResult result = new ErrorResult("SYSTEM_ERROR", "系统繁忙");
return ResponseEntity.internalServerError().body(result);
}
}
大促时支付系统压力很大,我们做了这些优化:
java复制// 缓存支付宝公钥示例
public class AlipayPublicKeyCache {
private static String publicKey;
public static String getPublicKey() {
if (publicKey == null) {
synchronized (AlipayPublicKeyCache.class) {
if (publicKey == null) {
publicKey = readPublicKey();
}
}
}
return publicKey;
}
}
支付宝提供了沙箱环境,测试时注意:
建议在application.yml中区分环境:
yaml复制alipay:
gateway-host: ${ALIPAY_GATEWAY:openapi.alipaydev.com}
app-id: ${ALIPAY_APP_ID:沙箱APPID}
我整理了几个常见错误和解决方法:
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 验签失败 | 公钥不匹配 | 检查支付宝后台配置的公钥 |
| 无效APPID | 环境配置错误 | 检查沙箱/正式环境配置 |
| 交易不存在 | 订单号重复 | 确保每次支付使用新订单号 |
| 金额超限 | 沙箱环境限制 | 沙箱单笔金额不超过5000元 |
真实项目中,一定要做每日对账:
java复制public void dailyReconciliation(LocalDate date) {
// 1. 查询支付宝账单
List<AlipayTransaction> alipayTransactions = getAlipayBill(date);
// 2. 查询本地订单
List<LocalOrder> localOrders = getLocalOrders(date);
// 3. 对账
ReconciliationResult result = reconcile(alipayTransactions, localOrders);
// 4. 处理差异
handleDiscrepancies(result);
}
对账时要注意时区问题,支付宝使用北京时间(GMT+8)。