1. 支付服务在微服务架构里的定位做电商项目,前面把商品、库存、订单、用户这些服务都搭起来之后,早晚要面对一个绕不开的环节——支付。我这次在基于Vue和SpringCloudAlibaba的微服务商城项目里,把支付单独拆成了一个平台服务,专门负责对接第三方支付渠道,这一期就讲讲对接支付宝接口的完整过程。为什么要把支付做成独立服务,而不是在订单服务里直接调支付宝SDK?因为在实际业务里,支付不是一个简单接口调用就结束的事情。一个成熟的电商系统,通常要支持多条支付渠道,比如支付宝、微信、银行卡代扣,甚至不同场景下的聚合支付。每次加渠道都要改订单服务代码,订单服务会越来越臃肿,而且支付涉及签名、回调、对账、退款、异常处理,这些逻辑混在订单流程里很容易出问题。把支付拆成独立服务,等于在订单系统和第三方渠道之间加了一道隔离层,订单服务只需要告诉支付服务“我要收多少钱、对应哪笔订单”,支付服务负责跟渠道打交道,钱到账之后再通知订单服务更新状态。我们的项目结构大致是这样的:网关层用SpringCloudGateway做路由,注册中心用Nacos,订单服务负责业务订单生成,支付服务独立部署,内部通过Feign调用订单服务查询订单信息。前端Vue页面拿到支付参数后,负责跳转支付宝收银台。整条链路是:用户在前端下单 → 订单服务生成订单 → 前端请求支付服务获取支付参数 → 前端重定向到支付宝 → 用户完成付款 → 支付宝异步通知支付服务 → 支付服务更新支付流水 → 通知订单服务改状态。1.1 对接支付宝的技术选型思路支付宝开放平台提供了多种支付产品,我们电商PC端场景选的是电脑网站支付,接口名叫alipay.trade.page.pay,用户从浏览器发起支付后跳转到支付宝页面完成付款。如果你是移动端H5场景,就得选手机网站支付alipay.trade.wap.pay;如果是在App里,要用App支付。选型时首先要明确你的用户在哪里付钱,这决定了你用哪个接口。另外就是接入方式的选择。支付宝支持两种方式:一种是老牌的公钥模式,配置应用私钥和支付宝公钥;另一种是公钥证书模式,需要上传三张证书文件。我个人强烈建议新项目直接用证书模式,安全性更高,而且支付宝在逐年收紧公钥模式,密钥泄漏事故太多了,证书模式就算私钥泄漏,也没法直接伪造请求。SDK方面,Java项目直接引入alipay-sdk-java官方依赖就行,版本选择aliyun仓库里最新的稳定版,不建议手动下载jar包塞到lib目录,依赖管理会乱。如果你的项目用了Maven,在pom里加依赖,然后配置一个AlipayClient的单例Bean,所有支付请求都通过这个client发出。1.2 支付服务需要的核心表结构对接支付之前,先设计好数据模型。我们支付服务里建了两张核心表:支付流水表和渠道回调记录表。支付流水表记录每一笔支付请求的完整生命周期,关键字段包括:支付流水号(我们自己生成的唯一编号,格式可以是日期+随机数)、业务订单号(订单服务传过来的outTradeNo)、渠道类型(支付宝/微信)、支付金额(以分为单位存储)、支付状态(待支付/支付中/成功/关单/退款)、渠道交易号(支付宝返回的tradeNo)、回调时间、创建时间、更新时间。回调记录表则是专门记录支付宝发来的每一条异步通知,不管成功失败都要存下来。为什么必须存回调记录?因为支付宝的异步通知会重试,而且可能重复发送,如果你不记录处理结果,后面排查问题的时候根本说不清楚这笔通知是收到了没处理,还是处理成功了又来了一条重复的。我之前排查线上问题,十次里面有八次是靠着回调记录表定位到原因的。这两张表的建表语句就不贴了,核心思路是:流水表保证每笔订单的支付请求有据可查,回调表保证每次通知都有迹可循。后面讲对账和异常排查,你会发现这两张表是救命的。2. 对接前的环境准备与配置2.1 沙箱环境与正式环境支付宝对接最友好的地方在于提供了完整的沙箱环境,不用等到审核通过就能把整个支付流程跑通。你在支付宝开放平台创建应用之后,先申请沙箱使用的AppID,然后用沙箱环境调试代码。沙箱环境的技术要点是接口网关地址不同:正式环境是openapi.alipay.com/gateway.do,沙箱是openapi.alipaydev.com/gateway.do。这个网关地址非常容易被人忽略,尤其是有时候从网上复制粘贴配置,用着用着才发现跑的是别人的测试环境。沙箱环境还提供了模拟的买家账号,沙箱里有初始余额,可以随便测试。不过要注意,沙箱的时间有时跟真实环境有几分钟的偏差,调试订单超时逻辑的时候,别拿沙箱环境的时间作为基准判断线上行为。2.2 应用密钥与支付宝公钥配置流程密钥配置是整个支付对接里最容易出错的一步。简单说一下流程:在支付宝开放平台的后台,选择密钥管理,生成应用公钥和应用私钥。生成方式有两种,一种是用支付宝官方的密钥生成工具,一种是用openssl命令行生成RSA密钥对。我自己习惯用openssl,因为除了解析时方便,还能顺便设置私钥权限。生成命令如下:# 生成2048位RSA私钥 openssl genrsa -out app_private_key.pem 2048 # 提取公钥 openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem生成后,把应用公钥内容粘贴到支付宝后台,支付宝会生成对应的支付宝公钥。这里要特别强调一个常见的混淆点:千万别把应用私钥给第三方,也千万别把支付宝公钥和应用公钥搞混。应用私钥是用来给我们的请求签名的,支付宝公钥是用来验证支付宝返回数据和回调通知的。私钥一旦泄露,别人可以伪造你的支付请求,事故级别是P0。配置证书模式时,你需要下载支付宝公钥证书、应用公钥证书和应用根证书,然后配置在代码里。现在新版SDK支持从本地文件读取证书,也支持从类路径读取,开发环境建议放在resources目录下,生产环境则通过环境变量指定路径。2.3 SpringBoot项目中的AlipayClient配置在SpringBoot项目中,我通常把支付宝配置封装成一个配置类,读取application.yml里的参数。一个清晰的配置类大概是这样的:@Configuration public class AlipayConfig { @Bean public AlipayClient alipayClient(AlipayProperties properties) { String gateway = properties.getGateway(); String appId = properties.getAppId(); String privateKey = properties.getPrivateKey(); String alipayPublicKey = properties.getAlipayPublicKey(); String signType = "RSA2"; return new DefaultAlipayClient(gateway, appId, privateKey, "json", "utf-8", alipayPublicKey, signType, "AES", properties.getEncryptKey()); } }注意上面代码里有加密方式AES,这是新版接口推荐的一种做法。如果你只是做常规支付,且支付宝后台应用设置里没有开启内容加密,那么构造方法里可以不传encryptKey,直接用七参数的构造方法。加密参数加错了反而会导致请求失败。配置文件里还要注意一个细节:在沙箱环境和正式环境之间切换时,建议把网关、AppID、密钥都拆开配置,不要再代码里写死。我们项目里做法是spring.profiles.active区分dev和prod,对应的application-dev.yml里配沙箱参数,application-prod.yml里配正式参数,这样切环境只改一个启动参数,很方便。3. 支付接口对接的核心代码实现3.1 创建支付订单的流程设计支付服务对外提供的核心接口是创建支付,入参包含业务订单号、支付金额、商品标题和回调地址。这里有一个设计决策需要说清楚:到底由谁生成支付流水号。有两种方案,一种是由订单服务生成一个outTradeNo传给支付服务,另一种是支付服务自己生成流水号,然后跟订单号绑定。我们项目最终选择的是订单服务生成,支付服务接收并校验。原因是订单服务是业务上游,它需要在创建订单的同时就预留支付流水号,方便后面做超时关单、状态比对。支付服务接收到下单请求后,先查流水表看这个业务订单号是否已经生成过支付记录,如果已经存在且支付成功,直接返回“订单已支付”的提示,如果存在但待支付,也不要重新生成新流水号,而是复用原记录继续走支付流程。这个判断看似简单,却是保证幂等性的关键。下单接口的实现逻辑分四步:第一步校验订单信息,调用订单服务查询订单状态和金额;第二步判断支付流水是否已存在;第三步生成或复用支付流水记录;第四步构建支付宝请求参数并返回前端跳转需要的form表单。核心代码大致如下:@Override public PayResult createPayOrder(PayCreateRequest request) { // 1. 校验业务订单 OrderDTO order = orderFeignClient.queryOrder(request.getOrderNo()); if (order == null) { throw new BizException("订单不存在"); } if (order.getStatus() != OrderStatus.WAIT_PAY) { throw new BizException("订单状态不允许支付"); } // 2. 幂等检查:同笔订单不允许重复创建支付流水 PayFlow payFlow = payFlowMapper.selectByOrderNo(request.getOrderNo()); if (payFlow != null) { if (payFlow.getStatus() == PayStatus.SUCCESS) { return PayResult.success(payFlow); } // 待支付状态也直接复用,防止重复生成 return buildAlipayForm(payFlow); } // 3. 创建支付流水 PayFlow flow = new PayFlow(); flow.setPayNo(generatePayNo()); flow.setOrderNo(request.getOrderNo()); flow.setChannel(ChannelType.ALIPAY); flow.setAmount(order.getPayAmount()); flow.setStatus(PayStatus.WAIT_PAY); payFlowMapper.insert(flow); // 4. 构建支付宝请求 return buildAlipayForm(flow); }3.2 组装支付宝请求参数与签名构建支付宝电脑网站支付请求时,最关键的是biz_content参数。支付宝的每个开放接口都有一个业务参数对象,通过JSON字符串传递。电脑网站支付的业务参数包括out_trade_no、product_code、total_amount、subject等。total_amount这个字段特别容易踩坑。支付宝的金额单位是元,而且是字符串类型,比如10.00表示十元。我们数据库存储金额用的分,转成元的时候必须除以100,而且要做好四舍五入。我之前见过有人直接拿分去传,结果一笔订单差了100倍,要不是沙箱环境先测出来,上线就出大事故了。组装请求的示例代码:private PayResult buildAlipayForm(PayFlow flow) { AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); request.setNotifyUrl(alipayConfig.getNotifyUrl()); request.setReturnUrl(alipayConfig.getReturnUrl()); // biz_content 必须是一个合法的JSON字符串 JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", flow.getPayNo()); bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); bizContent.put("total_amount", String.format("%.2f", flow.getAmount() / 100.0)); bizContent.put("subject", flow.getSubject()); bizContent.put("timeout_express", "30m"); request.setBizContent(bizContent.toJSONString()); try { AlipayTradePagePayResponse response = alipayClient.pageExecute(request); if (response.isSuccess()) { return PayResult.success(response.getBody()); } log.error("支付宝下单失败: {}", response.getBody()); throw new BizException("支付宝下单失败"); } catch (AlipayApiException e) { log.error("支付宝下单异常", e); throw new BizException("支付宝服务暂不可用"); } }这里有两个细节值得讲。第一是pageExecute和execute的区别。电脑网站支付适合用pageExecute,它返回的是可以直接往浏览器输出的表单HTML;而execute会帮你把返回内容解析成标准对象,但是页面跳转类接口不适合。第二是timeout_express,这个参数控制用户下单后必须在多长时间内完成支付,超过时间订单自动关闭,我们业务设定是30分钟,正好匹配订单服务的超时关单逻辑。两边的时间必须保持一致,否则会出现订单服务说订单已关,支付宝侧却说还能支付的情况。3.3 前端Vue页面如何发起支付前端拿到支付服务返回的表单HTML后,有两种处理方式。一种是把返回的HTML字符串直接嵌入当前页面,通过表单自动提交跳转到支付宝收银台;另一种是解析出里面的action地址和表单字段,自己组装跳转。官方SDK的pageExecute返回的body就是一个完整的form表单,我们前端拿到后,最稳妥的做法是创建一个隐藏的div,把这段HTML塞进去,然后调用form的submit方法。Vue这边我封装了一个支付跳转方法:// 支付服务返回的是支付宝收银台form表单 async function goPay(orderNo) { const res = await createPayOrder(orderNo); const formHtml = res.data.form; const formDiv = document.createElement('div'); formDiv.innerHTML = formHtml; document.body.appendChild(formDiv); const form = formDiv.querySelector('form'); if (form) { form.submit(); } else { // 如果返回的已经是跳转链接,直接location.href window.location.href = res.data.form; } }这个方法有两点要注意。第一,不要把form放到body外部或者用display:none隐藏后再submit,有些浏览器对hidden元素的submit有兼容性问题。我的做法是append到body,提交完成后马上移除这个div节点。第二,createPayOrder这个接口在用户点击“去支付”按钮的瞬间调用,如果接口响应慢,用户连续点了几次,就会重复创建支付流水。前端要做按钮loading和节流,后端要做幂等校验,两层保障缺一不可。3.4 异步回调通知处理用户在支付宝完成支付后,支付宝会往我们配置的notify_url发送一条异步通知,这条通知是服务器到服务器的请求,不依赖浏览器,所以就算用户付款后关掉了页面,我们也能收到结果。这是支付流程里最核心的环节,处理不好就会丢单。先明确一个原则:异步通知是唯一可靠的支付结果来源,同步跳转return_url只是顺带给用户看的页面跳转,不能当作确认支付成功的依据。很多人刚开始写的时候,喜欢在return_url那边直接改订单状态,这是大忌。同步跳转是浏览器行为,完全可能被伪造,而且用户网络中断时根本跳不回来,订单状态永远停留在待支付。回调处理的核心代码,我按逻辑分四步:验签、查单、对账、更新状态。@PostMapping("/alipay/notify") public String alipayNotify(HttpServletRequest request) { // 1. 获取支付宝POST过来的所有参数 Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (String name : requestParams.keySet()) { params.put(name, request.getParameter(name)); } // 2. 验签 boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayProperties.getAlipayPublicKey(), "utf-8", "RSA2"); if (!signVerified) { log.error("支付宝回调验签失败: {}", params.get("out_trade_no")); return "failure"; } // 3. 根据trade_status处理 String tradeStatus = params.get("trade_status"); String outTradeNo = params.get("out_trade_no"); String tradeNo = params.get("trade_no"); String totalAmount = params.get("total_amount"); if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) { // 4. 幂等更新 + 对账 payFlowService.handlePaySuccess(outTradeNo, tradeNo, totalAmount); } return "success"; }这里必须强调返回的字符串格式。支付宝要求回调接口返回纯文本的"success"或者"failure",不要返回JSON格式,不要返回"ok",不要返回空。失败的时候支付宝会按策略重试,第一次失败后间隔4分钟再试,之后频率逐步变长。如果你返回的格式不对,支付宝不识别,会一直重试到出问题为止。4. 支付结果处理与订单闭环设计4.1 幂等更新与金额校验handlePaySuccess这个方法里做了几件重要的事。第一是从回调记录表里查一下这笔通知之前有没有处理过,处理过就直接返回,不重复执行业务逻辑。第二是把支付流水状态从待支付更新为支付成功,这步要加乐观锁或者状态条件更新,防止并发下重复更新。第三是调用订单服务,通知订单状态变更。金额校验是绝对不能跳过的。回调参数里的total_amount虽然是支付宝返回的,但理论上存在参数被篡改的可能,更常见的是业务方自己在回调处理逻辑里写错了币种换算。我的做法是拿支付宝回调金额跟本地支付流水里的金额做比较,必须完全相等才允许更新状态。@Transactional public void handlePaySuccess(String payNo, String tradeNo, String totalAmount) { PayFlow flow = payFlowMapper.selectByPayNoForUpdate(payNo); if (flow == null) { log.error("支付流水不存在: {}", payNo); return; } if (flow.getStatus() == PayStatus.SUCCESS) { return; // 幂等处理 } // 金额校验: 将回调的元转换为分 long callbackAmount = new BigDecimal(totalAmount).multiply(new BigDecimal(100)).longValue(); if (callbackAmount != flow.getAmount()) { log.error("支付金额不一致: payNo={}, expect={}, actual={}", payNo, flow.getAmount(), callbackAmount); // 这里可以发送告警,但不能直接更新状态 return; } flow.setStatus(PayStatus.SUCCESS); flow.setTradeNo(tradeNo); flowMapper.updateById(flow); // 通知订单服务 orderFeignClient.markOrderPaid(flow.getOrderNo(), tradeNo); }订单服务的markOrderPaid接口内部同样要做幂等判断,从待支付改成已支付,如果订单已经是已支付状态,直接返回成功,不要让重复通知把状态改回去。这里我用的是支付流水号payNo作为外部单号去对账,而不是直接用业务订单号。原因在于,理论上允许一笔订单被拆成多次支付(部分支付场景),用业务订单号做唯一关联会出问题。咱们这个电商项目没有拆单需求,但设计上按支付流水号处理更稳妥。4.2 主动查单与超时关单兜底异步通知虽然可靠,但不是百分百不会丢。支付宝文档里明确说会重试,但极端情况下,比如我们的服务刚好在回调期间重启、数据库连接池耗尽,通知就有可能丢失。所以一定要有一个兜底方案:主动查单。实际项目中我在支付服务里写了一个定时任务,每5分钟扫描一次流水表里状态为待支付且创建时间超过10分钟的订单,调用支付宝的查询接口确认最终状态。private void queryAlipayTrade(PayFlow flow) { AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", flow.getPayNo()); request.setBizContent(bizContent.toJSONString()); try { AlipayTradeQueryResponse response = alipayClient.execute(request); if (response.isSuccess()) { String tradeStatus = response.getTradeStatus(); if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) { handlePaySuccess(flow.getPayNo(), response.getTradeNo(), response.getTotalAmount()); } else if ("WAIT_BUYER_PAY".equals(tradeStatus)) { // 用户还在支付中,继续等待 } else if ("TRADE_CLOSED".equals(tradeStatus)) { // 超时关闭 closePayFlow(flow); } } } catch (AlipayApiException e) { log.error("支付宝查询订单异常", e); } }注意查单接口也有频率限制,不能对同一笔订单反复高频调用。我们把定时任务扫描范围限制在创建时间超过10分钟的待支付订单,并且同一笔订单一天最多查三次,再查不到就打告警人工介入。定时查单不只是补漏,它还能提前发现用户在支付宝侧的异常状态,比如用户已经付款但回调丢失,查单就能及时把订单状态纠正过来,这个在线上非常实用。4.3 订单超时关闭与支付流水状态联动订单服务有自己的超时关单逻辑,比如下单后30分钟未支付自动关闭。这时候支付服务也要联动:如果支付流水仍然处于待支付状态,订单服务关单时会同步调用支付服务的关单接口,由支付服务调用支付宝的关单接口alipay.trade.close,并且把本地流水状态改为关闭。支付宝的关单接口不是随便能调的,只有订单在WAIT_BUYER_PAY状态下才能关闭成功。如果用户刚好在下单后、支付前的一瞬间提交了付款,关单接口会返回ORDERNOTEXIST或者关闭失败,这时候订单服务那边要能接受“关闭失败”的结果。我们当时的处理是,订单服务收到关单失败的响应后,不立即置为已关闭,而是把订单状态改为“待确认”,同时查一下支付流水是否已经变成成功,如果变成成功就走支付成功流程,否则过几分钟再关一次。这个细节不做好的话,会出现订单关闭了但用户钱已经付了,或者订单未关闭但支付流水已经关了,两边数据不一致,非常难收场。5. 常见问题与排查实录对接支付宝的过程,我敢说每个做过的人都踩过至少三个坑。这里把我遇到的高频问题整理一下,很多是网上教程不会写的。5.1 签名验证失败的各种原因签名问题是支付对接里出现频率最高的一类。首先确认算法一致,我们配置的sign_type是RSA2,后端代码里校验的时候,签名方法第二个参数传的是支付宝公钥,不是应用公钥。有的同学把两个公钥搞反了,验签永远失败。其次确认密钥格式,从支付宝后台复制的公钥通常带-----BEGIN PUBLIC KEY-----这样的头和尾,复制的时候要注意完整复制。第三,如果用的是证书模式,要确认SDK初始化时用的是CertAlipayRequest而不是DefaultAlipayClient。还有一个隐蔽的问题,就是回调数据里的sign参数本身。如果我们在框架层面对POST参数做了处理,比如部分应用会对请求体做trim或者重新排序再传参,签名校验就会失败。我遇到过一次,支付服务网关层统一把所有POST参数转成了小写key,结果验签一直不过,排查了半天才发现是这个原因。验签的入参必须是支付宝请求原始的key-value对,任何修改都不行。5.2 通知重复到达导致订单状态错乱支付宝异步通知的最大特点就是“可能性送达”,它会重试,直到你明确返回success。所以回调处理器必须设计成天然幂等。我见过有同学在回调里先查订单状态,不是待支付就直接返回success,这个做法本身没错,但要注意如果订单已经进入退款流程,收到一笔成功的回调你又把状态改回已支付,就会导致退款流程错乱。我们的做法是引入一个支付事件表,回调来了先根据通知唯一标识(支付宝的通知ID)去重,处理过的直接返回success。处理成功之后再更新流水状态,最后通过异步消息通知订单服务。整个链路里,每一步都要求重复执行不会产生副作用,这是支付系统的基本功。5.3 金额精度与单位换算金额问题虽然简单,但犯错的概率极高。数据库用什么类型存金额,建议直接用BIGINT存分。前端展示用元,通过工具类转换。支付宝传的total_amount是字符串类型的元,像"10.00"这种,我们用BigDecimal去转换,不要用Double,更不要用Float。Double在精度上天生有缺陷,0.1 + 0.2不等于0.3,涉及到钱的运算坚决不用浮点类型。另外支付宝回调通知里,total_amount后面可能带很多位小数,比如"10.000000",这在某些场景下是合法的。转分的时候要做stripTrailingZeros处理,不然BigDecimal转long会抛异常。网上很多文章没提这个细节,我是在线上收到一条告警之后才发现的。5.4 支付宝后台配置遗漏很多问题根本不是代码问题,而是支付宝开放平台后台的配置没配全。比如应用的信息里没有配置接口加签方式的私钥,或者没有授权回调地址。回调地址notify_url必须是外网能访问的HTTPS地址,沙箱环境也有这个要求。本地调试的时候,可以用内网穿透工具把本机服务暴露到公网,但要注意支付宝服务器访问穿透后的地址时,延迟和稳定性会影响测试结果。正式环境还有一个常见错误:应用没有申请对应的支付产品权限。创建了应用,但没在产品列表里添加“电脑网站支付”,结果调用接口一直报isv.permission-error。第一次对接的人很容易忽略这个步骤,以为创建完应用就能直接调接口了。实际流程是:创建应用 → 添加产品能力 → 签约产品 → 配置密钥 → 设置回调地址,每一步缺了都不行。6. 联调过程中的关键心得最后分享几个我在整个对接过程中体会最深的地方,希望帮你少走弯路。第一个心得是沙箱环境要跟正式环境分开校验。我们项目里出现了这样一个情况:沙箱环境下所有支付都是成功的,订单状态一切正常,但切到正式环境就发现回调偶尔不成功。后来定位到是正式环境回调地址的HTTPS证书在负载均衡层配置有问题,支付宝服务器请求时证书链不完整,导致握手失败。这类问题在沙箱里因为地址不同根本暴露不了,所以上线前一定用正式环境小额订单完整走几遍流程。第二个心得是支付回调的日志一定要详细。我们支付服务里给回调处理加了单独的日志文件,每一条通知进来了,验签是否通过、状态是什么、金额是多少、我们返回了success还是failure,全部打出来。这个习惯在后续排查问题和做对账的时候帮了大忙。没有日志支撑,线上支付异常基本靠猜。第三个心得是不要自己造轮子做签名。支付宝SDK已经把加签、验签、加解密都封装好了,直接用官方SDK就好。我见过有人觉得SDK太重量级,自己手写RSA签名逻辑,结果各式问题不断。官方SDK是经过大规模流量验证的,不要在这种基础能力上创新。还有一点值得单独说。我们做支付服务时,所有对支付宝的调用都加了重试机制,但重试不是无限重试,每次重试的时间间隔采用指数退避,最大重试三次。比如下单接口失败重试,第一次间隔1秒,第二次2秒,第三次4秒。回调处理失败不重试的原因是支付宝本身会重试通知,我们的职责是把每一次通知都处理好然后准确告诉支付宝结果。支付这块做完,聚合支付平台的基本框架就有了,后面再接微信支付或者其他渠道,只需要在渠道层做适配,业务层的流水、回调、对账逻辑完全复用。这就是当初把支付单独拆成微服务的价值所在。
1. 支付服务在微服务架构里的定位做电商项目,前面把商品、库存、订单、用户这些服务都搭起来之后,早晚要面对一个绕不开的环节——支付。我这次在基于Vue和SpringCloudAlibaba的微服务商城项目里,把支付单独拆成了一个平台服务,专门负责对接第三方支付渠道,这一期就讲讲对接支付宝接口的完整过程。为什么要把支付做成独立服务,而不是在订单服务里直接调支付宝SDK?因为在实际业务里,支付不是一个简单接口调用就结束的事情。一个成熟的电商系统,通常要支持多条支付渠道,比如支付宝、微信、银行卡代扣,甚至不同场景下的聚合支付。每次加渠道都要改订单服务代码,订单服务会越来越臃肿,而且支付涉及签名、回调、对账、退款、异常处理,这些逻辑混在订单流程里很容易出问题。把支付拆成独立服务,等于在订单系统和第三方渠道之间加了一道隔离层,订单服务只需要告诉支付服务“我要收多少钱、对应哪笔订单”,支付服务负责跟渠道打交道,钱到账之后再通知订单服务更新状态。我们的项目结构大致是这样的:网关层用SpringCloudGateway做路由,注册中心用Nacos,订单服务负责业务订单生成,支付服务独立部署,内部通过Feign调用订单服务查询订单信息。前端Vue页面拿到支付参数后,负责跳转支付宝收银台。整条链路是:用户在前端下单 → 订单服务生成订单 → 前端请求支付服务获取支付参数 → 前端重定向到支付宝 → 用户完成付款 → 支付宝异步通知支付服务 → 支付服务更新支付流水 → 通知订单服务改状态。1.1 对接支付宝的技术选型思路支付宝开放平台提供了多种支付产品,我们电商PC端场景选的是电脑网站支付,接口名叫alipay.trade.page.pay,用户从浏览器发起支付后跳转到支付宝页面完成付款。如果你是移动端H5场景,就得选手机网站支付alipay.trade.wap.pay;如果是在App里,要用App支付。选型时首先要明确你的用户在哪里付钱,这决定了你用哪个接口。另外就是接入方式的选择。支付宝支持两种方式:一种是老牌的公钥模式,配置应用私钥和支付宝公钥;另一种是公钥证书模式,需要上传三张证书文件。我个人强烈建议新项目直接用证书模式,安全性更高,而且支付宝在逐年收紧公钥模式,密钥泄漏事故太多了,证书模式就算私钥泄漏,也没法直接伪造请求。SDK方面,Java项目直接引入alipay-sdk-java官方依赖就行,版本选择aliyun仓库里最新的稳定版,不建议手动下载jar包塞到lib目录,依赖管理会乱。如果你的项目用了Maven,在pom里加依赖,然后配置一个AlipayClient的单例Bean,所有支付请求都通过这个client发出。1.2 支付服务需要的核心表结构对接支付之前,先设计好数据模型。我们支付服务里建了两张核心表:支付流水表和渠道回调记录表。支付流水表记录每一笔支付请求的完整生命周期,关键字段包括:支付流水号(我们自己生成的唯一编号,格式可以是日期+随机数)、业务订单号(订单服务传过来的outTradeNo)、渠道类型(支付宝/微信)、支付金额(以分为单位存储)、支付状态(待支付/支付中/成功/关单/退款)、渠道交易号(支付宝返回的tradeNo)、回调时间、创建时间、更新时间。回调记录表则是专门记录支付宝发来的每一条异步通知,不管成功失败都要存下来。为什么必须存回调记录?因为支付宝的异步通知会重试,而且可能重复发送,如果你不记录处理结果,后面排查问题的时候根本说不清楚这笔通知是收到了没处理,还是处理成功了又来了一条重复的。我之前排查线上问题,十次里面有八次是靠着回调记录表定位到原因的。这两张表的建表语句就不贴了,核心思路是:流水表保证每笔订单的支付请求有据可查,回调表保证每次通知都有迹可循。后面讲对账和异常排查,你会发现这两张表是救命的。2. 对接前的环境准备与配置2.1 沙箱环境与正式环境支付宝对接最友好的地方在于提供了完整的沙箱环境,不用等到审核通过就能把整个支付流程跑通。你在支付宝开放平台创建应用之后,先申请沙箱使用的AppID,然后用沙箱环境调试代码。沙箱环境的技术要点是接口网关地址不同:正式环境是openapi.alipay.com/gateway.do,沙箱是openapi.alipaydev.com/gateway.do。这个网关地址非常容易被人忽略,尤其是有时候从网上复制粘贴配置,用着用着才发现跑的是别人的测试环境。沙箱环境还提供了模拟的买家账号,沙箱里有初始余额,可以随便测试。不过要注意,沙箱的时间有时跟真实环境有几分钟的偏差,调试订单超时逻辑的时候,别拿沙箱环境的时间作为基准判断线上行为。2.2 应用密钥与支付宝公钥配置流程密钥配置是整个支付对接里最容易出错的一步。简单说一下流程:在支付宝开放平台的后台,选择密钥管理,生成应用公钥和应用私钥。生成方式有两种,一种是用支付宝官方的密钥生成工具,一种是用openssl命令行生成RSA密钥对。我自己习惯用openssl,因为除了解析时方便,还能顺便设置私钥权限。生成命令如下:# 生成2048位RSA私钥 openssl genrsa -out app_private_key.pem 2048 # 提取公钥 openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem生成后,把应用公钥内容粘贴到支付宝后台,支付宝会生成对应的支付宝公钥。这里要特别强调一个常见的混淆点:千万别把应用私钥给第三方,也千万别把支付宝公钥和应用公钥搞混。应用私钥是用来给我们的请求签名的,支付宝公钥是用来验证支付宝返回数据和回调通知的。私钥一旦泄露,别人可以伪造你的支付请求,事故级别是P0。配置证书模式时,你需要下载支付宝公钥证书、应用公钥证书和应用根证书,然后配置在代码里。现在新版SDK支持从本地文件读取证书,也支持从类路径读取,开发环境建议放在resources目录下,生产环境则通过环境变量指定路径。2.3 SpringBoot项目中的AlipayClient配置在SpringBoot项目中,我通常把支付宝配置封装成一个配置类,读取application.yml里的参数。一个清晰的配置类大概是这样的:@Configuration public class AlipayConfig { @Bean public AlipayClient alipayClient(AlipayProperties properties) { String gateway = properties.getGateway(); String appId = properties.getAppId(); String privateKey = properties.getPrivateKey(); String alipayPublicKey = properties.getAlipayPublicKey(); String signType = "RSA2"; return new DefaultAlipayClient(gateway, appId, privateKey, "json", "utf-8", alipayPublicKey, signType, "AES", properties.getEncryptKey()); } }注意上面代码里有加密方式AES,这是新版接口推荐的一种做法。如果你只是做常规支付,且支付宝后台应用设置里没有开启内容加密,那么构造方法里可以不传encryptKey,直接用七参数的构造方法。加密参数加错了反而会导致请求失败。配置文件里还要注意一个细节:在沙箱环境和正式环境之间切换时,建议把网关、AppID、密钥都拆开配置,不要再代码里写死。我们项目里做法是spring.profiles.active区分dev和prod,对应的application-dev.yml里配沙箱参数,application-prod.yml里配正式参数,这样切环境只改一个启动参数,很方便。3. 支付接口对接的核心代码实现3.1 创建支付订单的流程设计支付服务对外提供的核心接口是创建支付,入参包含业务订单号、支付金额、商品标题和回调地址。这里有一个设计决策需要说清楚:到底由谁生成支付流水号。有两种方案,一种是由订单服务生成一个outTradeNo传给支付服务,另一种是支付服务自己生成流水号,然后跟订单号绑定。我们项目最终选择的是订单服务生成,支付服务接收并校验。原因是订单服务是业务上游,它需要在创建订单的同时就预留支付流水号,方便后面做超时关单、状态比对。支付服务接收到下单请求后,先查流水表看这个业务订单号是否已经生成过支付记录,如果已经存在且支付成功,直接返回“订单已支付”的提示,如果存在但待支付,也不要重新生成新流水号,而是复用原记录继续走支付流程。这个判断看似简单,却是保证幂等性的关键。下单接口的实现逻辑分四步:第一步校验订单信息,调用订单服务查询订单状态和金额;第二步判断支付流水是否已存在;第三步生成或复用支付流水记录;第四步构建支付宝请求参数并返回前端跳转需要的form表单。核心代码大致如下:@Override public PayResult createPayOrder(PayCreateRequest request) { // 1. 校验业务订单 OrderDTO order = orderFeignClient.queryOrder(request.getOrderNo()); if (order == null) { throw new BizException("订单不存在"); } if (order.getStatus() != OrderStatus.WAIT_PAY) { throw new BizException("订单状态不允许支付"); } // 2. 幂等检查:同笔订单不允许重复创建支付流水 PayFlow payFlow = payFlowMapper.selectByOrderNo(request.getOrderNo()); if (payFlow != null) { if (payFlow.getStatus() == PayStatus.SUCCESS) { return PayResult.success(payFlow); } // 待支付状态也直接复用,防止重复生成 return buildAlipayForm(payFlow); } // 3. 创建支付流水 PayFlow flow = new PayFlow(); flow.setPayNo(generatePayNo()); flow.setOrderNo(request.getOrderNo()); flow.setChannel(ChannelType.ALIPAY); flow.setAmount(order.getPayAmount()); flow.setStatus(PayStatus.WAIT_PAY); payFlowMapper.insert(flow); // 4. 构建支付宝请求 return buildAlipayForm(flow); }3.2 组装支付宝请求参数与签名构建支付宝电脑网站支付请求时,最关键的是biz_content参数。支付宝的每个开放接口都有一个业务参数对象,通过JSON字符串传递。电脑网站支付的业务参数包括out_trade_no、product_code、total_amount、subject等。total_amount这个字段特别容易踩坑。支付宝的金额单位是元,而且是字符串类型,比如10.00表示十元。我们数据库存储金额用的分,转成元的时候必须除以100,而且要做好四舍五入。我之前见过有人直接拿分去传,结果一笔订单差了100倍,要不是沙箱环境先测出来,上线就出大事故了。组装请求的示例代码:private PayResult buildAlipayForm(PayFlow flow) { AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); request.setNotifyUrl(alipayConfig.getNotifyUrl()); request.setReturnUrl(alipayConfig.getReturnUrl()); // biz_content 必须是一个合法的JSON字符串 JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", flow.getPayNo()); bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); bizContent.put("total_amount", String.format("%.2f", flow.getAmount() / 100.0)); bizContent.put("subject", flow.getSubject()); bizContent.put("timeout_express", "30m"); request.setBizContent(bizContent.toJSONString()); try { AlipayTradePagePayResponse response = alipayClient.pageExecute(request); if (response.isSuccess()) { return PayResult.success(response.getBody()); } log.error("支付宝下单失败: {}", response.getBody()); throw new BizException("支付宝下单失败"); } catch (AlipayApiException e) { log.error("支付宝下单异常", e); throw new BizException("支付宝服务暂不可用"); } }这里有两个细节值得讲。第一是pageExecute和execute的区别。电脑网站支付适合用pageExecute,它返回的是可以直接往浏览器输出的表单HTML;而execute会帮你把返回内容解析成标准对象,但是页面跳转类接口不适合。第二是timeout_express,这个参数控制用户下单后必须在多长时间内完成支付,超过时间订单自动关闭,我们业务设定是30分钟,正好匹配订单服务的超时关单逻辑。两边的时间必须保持一致,否则会出现订单服务说订单已关,支付宝侧却说还能支付的情况。3.3 前端Vue页面如何发起支付前端拿到支付服务返回的表单HTML后,有两种处理方式。一种是把返回的HTML字符串直接嵌入当前页面,通过表单自动提交跳转到支付宝收银台;另一种是解析出里面的action地址和表单字段,自己组装跳转。官方SDK的pageExecute返回的body就是一个完整的form表单,我们前端拿到后,最稳妥的做法是创建一个隐藏的div,把这段HTML塞进去,然后调用form的submit方法。Vue这边我封装了一个支付跳转方法:// 支付服务返回的是支付宝收银台form表单 async function goPay(orderNo) { const res = await createPayOrder(orderNo); const formHtml = res.data.form; const formDiv = document.createElement('div'); formDiv.innerHTML = formHtml; document.body.appendChild(formDiv); const form = formDiv.querySelector('form'); if (form) { form.submit(); } else { // 如果返回的已经是跳转链接,直接location.href window.location.href = res.data.form; } }这个方法有两点要注意。第一,不要把form放到body外部或者用display:none隐藏后再submit,有些浏览器对hidden元素的submit有兼容性问题。我的做法是append到body,提交完成后马上移除这个div节点。第二,createPayOrder这个接口在用户点击“去支付”按钮的瞬间调用,如果接口响应慢,用户连续点了几次,就会重复创建支付流水。前端要做按钮loading和节流,后端要做幂等校验,两层保障缺一不可。3.4 异步回调通知处理用户在支付宝完成支付后,支付宝会往我们配置的notify_url发送一条异步通知,这条通知是服务器到服务器的请求,不依赖浏览器,所以就算用户付款后关掉了页面,我们也能收到结果。这是支付流程里最核心的环节,处理不好就会丢单。先明确一个原则:异步通知是唯一可靠的支付结果来源,同步跳转return_url只是顺带给用户看的页面跳转,不能当作确认支付成功的依据。很多人刚开始写的时候,喜欢在return_url那边直接改订单状态,这是大忌。同步跳转是浏览器行为,完全可能被伪造,而且用户网络中断时根本跳不回来,订单状态永远停留在待支付。回调处理的核心代码,我按逻辑分四步:验签、查单、对账、更新状态。@PostMapping("/alipay/notify") public String alipayNotify(HttpServletRequest request) { // 1. 获取支付宝POST过来的所有参数 Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (String name : requestParams.keySet()) { params.put(name, request.getParameter(name)); } // 2. 验签 boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayProperties.getAlipayPublicKey(), "utf-8", "RSA2"); if (!signVerified) { log.error("支付宝回调验签失败: {}", params.get("out_trade_no")); return "failure"; } // 3. 根据trade_status处理 String tradeStatus = params.get("trade_status"); String outTradeNo = params.get("out_trade_no"); String tradeNo = params.get("trade_no"); String totalAmount = params.get("total_amount"); if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) { // 4. 幂等更新 + 对账 payFlowService.handlePaySuccess(outTradeNo, tradeNo, totalAmount); } return "success"; }这里必须强调返回的字符串格式。支付宝要求回调接口返回纯文本的"success"或者"failure",不要返回JSON格式,不要返回"ok",不要返回空。失败的时候支付宝会按策略重试,第一次失败后间隔4分钟再试,之后频率逐步变长。如果你返回的格式不对,支付宝不识别,会一直重试到出问题为止。4. 支付结果处理与订单闭环设计4.1 幂等更新与金额校验handlePaySuccess这个方法里做了几件重要的事。第一是从回调记录表里查一下这笔通知之前有没有处理过,处理过就直接返回,不重复执行业务逻辑。第二是把支付流水状态从待支付更新为支付成功,这步要加乐观锁或者状态条件更新,防止并发下重复更新。第三是调用订单服务,通知订单状态变更。金额校验是绝对不能跳过的。回调参数里的total_amount虽然是支付宝返回的,但理论上存在参数被篡改的可能,更常见的是业务方自己在回调处理逻辑里写错了币种换算。我的做法是拿支付宝回调金额跟本地支付流水里的金额做比较,必须完全相等才允许更新状态。@Transactional public void handlePaySuccess(String payNo, String tradeNo, String totalAmount) { PayFlow flow = payFlowMapper.selectByPayNoForUpdate(payNo); if (flow == null) { log.error("支付流水不存在: {}", payNo); return; } if (flow.getStatus() == PayStatus.SUCCESS) { return; // 幂等处理 } // 金额校验: 将回调的元转换为分 long callbackAmount = new BigDecimal(totalAmount).multiply(new BigDecimal(100)).longValue(); if (callbackAmount != flow.getAmount()) { log.error("支付金额不一致: payNo={}, expect={}, actual={}", payNo, flow.getAmount(), callbackAmount); // 这里可以发送告警,但不能直接更新状态 return; } flow.setStatus(PayStatus.SUCCESS); flow.setTradeNo(tradeNo); flowMapper.updateById(flow); // 通知订单服务 orderFeignClient.markOrderPaid(flow.getOrderNo(), tradeNo); }订单服务的markOrderPaid接口内部同样要做幂等判断,从待支付改成已支付,如果订单已经是已支付状态,直接返回成功,不要让重复通知把状态改回去。这里我用的是支付流水号payNo作为外部单号去对账,而不是直接用业务订单号。原因在于,理论上允许一笔订单被拆成多次支付(部分支付场景),用业务订单号做唯一关联会出问题。咱们这个电商项目没有拆单需求,但设计上按支付流水号处理更稳妥。4.2 主动查单与超时关单兜底异步通知虽然可靠,但不是百分百不会丢。支付宝文档里明确说会重试,但极端情况下,比如我们的服务刚好在回调期间重启、数据库连接池耗尽,通知就有可能丢失。所以一定要有一个兜底方案:主动查单。实际项目中我在支付服务里写了一个定时任务,每5分钟扫描一次流水表里状态为待支付且创建时间超过10分钟的订单,调用支付宝的查询接口确认最终状态。private void queryAlipayTrade(PayFlow flow) { AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", flow.getPayNo()); request.setBizContent(bizContent.toJSONString()); try { AlipayTradeQueryResponse response = alipayClient.execute(request); if (response.isSuccess()) { String tradeStatus = response.getTradeStatus(); if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) { handlePaySuccess(flow.getPayNo(), response.getTradeNo(), response.getTotalAmount()); } else if ("WAIT_BUYER_PAY".equals(tradeStatus)) { // 用户还在支付中,继续等待 } else if ("TRADE_CLOSED".equals(tradeStatus)) { // 超时关闭 closePayFlow(flow); } } } catch (AlipayApiException e) { log.error("支付宝查询订单异常", e); } }注意查单接口也有频率限制,不能对同一笔订单反复高频调用。我们把定时任务扫描范围限制在创建时间超过10分钟的待支付订单,并且同一笔订单一天最多查三次,再查不到就打告警人工介入。定时查单不只是补漏,它还能提前发现用户在支付宝侧的异常状态,比如用户已经付款但回调丢失,查单就能及时把订单状态纠正过来,这个在线上非常实用。4.3 订单超时关闭与支付流水状态联动订单服务有自己的超时关单逻辑,比如下单后30分钟未支付自动关闭。这时候支付服务也要联动:如果支付流水仍然处于待支付状态,订单服务关单时会同步调用支付服务的关单接口,由支付服务调用支付宝的关单接口alipay.trade.close,并且把本地流水状态改为关闭。支付宝的关单接口不是随便能调的,只有订单在WAIT_BUYER_PAY状态下才能关闭成功。如果用户刚好在下单后、支付前的一瞬间提交了付款,关单接口会返回ORDERNOTEXIST或者关闭失败,这时候订单服务那边要能接受“关闭失败”的结果。我们当时的处理是,订单服务收到关单失败的响应后,不立即置为已关闭,而是把订单状态改为“待确认”,同时查一下支付流水是否已经变成成功,如果变成成功就走支付成功流程,否则过几分钟再关一次。这个细节不做好的话,会出现订单关闭了但用户钱已经付了,或者订单未关闭但支付流水已经关了,两边数据不一致,非常难收场。5. 常见问题与排查实录对接支付宝的过程,我敢说每个做过的人都踩过至少三个坑。这里把我遇到的高频问题整理一下,很多是网上教程不会写的。5.1 签名验证失败的各种原因签名问题是支付对接里出现频率最高的一类。首先确认算法一致,我们配置的sign_type是RSA2,后端代码里校验的时候,签名方法第二个参数传的是支付宝公钥,不是应用公钥。有的同学把两个公钥搞反了,验签永远失败。其次确认密钥格式,从支付宝后台复制的公钥通常带-----BEGIN PUBLIC KEY-----这样的头和尾,复制的时候要注意完整复制。第三,如果用的是证书模式,要确认SDK初始化时用的是CertAlipayRequest而不是DefaultAlipayClient。还有一个隐蔽的问题,就是回调数据里的sign参数本身。如果我们在框架层面对POST参数做了处理,比如部分应用会对请求体做trim或者重新排序再传参,签名校验就会失败。我遇到过一次,支付服务网关层统一把所有POST参数转成了小写key,结果验签一直不过,排查了半天才发现是这个原因。验签的入参必须是支付宝请求原始的key-value对,任何修改都不行。5.2 通知重复到达导致订单状态错乱支付宝异步通知的最大特点就是“可能性送达”,它会重试,直到你明确返回success。所以回调处理器必须设计成天然幂等。我见过有同学在回调里先查订单状态,不是待支付就直接返回success,这个做法本身没错,但要注意如果订单已经进入退款流程,收到一笔成功的回调你又把状态改回已支付,就会导致退款流程错乱。我们的做法是引入一个支付事件表,回调来了先根据通知唯一标识(支付宝的通知ID)去重,处理过的直接返回success。处理成功之后再更新流水状态,最后通过异步消息通知订单服务。整个链路里,每一步都要求重复执行不会产生副作用,这是支付系统的基本功。5.3 金额精度与单位换算金额问题虽然简单,但犯错的概率极高。数据库用什么类型存金额,建议直接用BIGINT存分。前端展示用元,通过工具类转换。支付宝传的total_amount是字符串类型的元,像"10.00"这种,我们用BigDecimal去转换,不要用Double,更不要用Float。Double在精度上天生有缺陷,0.1 + 0.2不等于0.3,涉及到钱的运算坚决不用浮点类型。另外支付宝回调通知里,total_amount后面可能带很多位小数,比如"10.000000",这在某些场景下是合法的。转分的时候要做stripTrailingZeros处理,不然BigDecimal转long会抛异常。网上很多文章没提这个细节,我是在线上收到一条告警之后才发现的。5.4 支付宝后台配置遗漏很多问题根本不是代码问题,而是支付宝开放平台后台的配置没配全。比如应用的信息里没有配置接口加签方式的私钥,或者没有授权回调地址。回调地址notify_url必须是外网能访问的HTTPS地址,沙箱环境也有这个要求。本地调试的时候,可以用内网穿透工具把本机服务暴露到公网,但要注意支付宝服务器访问穿透后的地址时,延迟和稳定性会影响测试结果。正式环境还有一个常见错误:应用没有申请对应的支付产品权限。创建了应用,但没在产品列表里添加“电脑网站支付”,结果调用接口一直报isv.permission-error。第一次对接的人很容易忽略这个步骤,以为创建完应用就能直接调接口了。实际流程是:创建应用 → 添加产品能力 → 签约产品 → 配置密钥 → 设置回调地址,每一步缺了都不行。6. 联调过程中的关键心得最后分享几个我在整个对接过程中体会最深的地方,希望帮你少走弯路。第一个心得是沙箱环境要跟正式环境分开校验。我们项目里出现了这样一个情况:沙箱环境下所有支付都是成功的,订单状态一切正常,但切到正式环境就发现回调偶尔不成功。后来定位到是正式环境回调地址的HTTPS证书在负载均衡层配置有问题,支付宝服务器请求时证书链不完整,导致握手失败。这类问题在沙箱里因为地址不同根本暴露不了,所以上线前一定用正式环境小额订单完整走几遍流程。第二个心得是支付回调的日志一定要详细。我们支付服务里给回调处理加了单独的日志文件,每一条通知进来了,验签是否通过、状态是什么、金额是多少、我们返回了success还是failure,全部打出来。这个习惯在后续排查问题和做对账的时候帮了大忙。没有日志支撑,线上支付异常基本靠猜。第三个心得是不要自己造轮子做签名。支付宝SDK已经把加签、验签、加解密都封装好了,直接用官方SDK就好。我见过有人觉得SDK太重量级,自己手写RSA签名逻辑,结果各式问题不断。官方SDK是经过大规模流量验证的,不要在这种基础能力上创新。还有一点值得单独说。我们做支付服务时,所有对支付宝的调用都加了重试机制,但重试不是无限重试,每次重试的时间间隔采用指数退避,最大重试三次。比如下单接口失败重试,第一次间隔1秒,第二次2秒,第三次4秒。回调处理失败不重试的原因是支付宝本身会重试通知,我们的职责是把每一次通知都处理好然后准确告诉支付宝结果。支付这块做完,聚合支付平台的基本框架就有了,后面再接微信支付或者其他渠道,只需要在渠道层做适配,业务层的流水、回调、对账逻辑完全复用。这就是当初把支付单独拆成微服务的价值所在。