Java支付系统实战:支付宝微信银联三端统一网关与生产级避坑指南
简介:这是一套面向Java后端开发者与支付系统学习者的实战型SpringBoot支付集成源码,聚焦电商、在线教育等场景下的多渠道支付能力构建。资源完整实现支付宝、微信及银联三大主流支付方式的接入,涵盖预支付订单生成、异步通知验签、退款处理、交易状态同步等核心业务流程,并内置签名算法、HTTPS通信、防重放等安全实践。压缩包共250个文件,以57个Java业务逻辑类、24个JS前端交互脚本、15个HTML页面及15个CSS/SCSS样式文件为主干,辅以证书(cer/pfx/p12)、配置(yml/properties)和文档(md/txt),总大小1.53MB,结构清晰便于模块化研读。目前已有774人下载学习,读者可直接复用支付网关封装、统一回调路由、多渠道适配抽象层等设计思路,快速掌握金融级支付系统的开发范式与工程落地细节。
1. 这不是又一个“Hello World”支付Demo:它是一套能跑通真实商户签约链路的Java支付系统源码,含支付宝/微信/银联三端完整闭环
你见过太多“SpringBoot + 支付宝沙箱”那种点个按钮弹出“支付成功”的玩具项目——它连异步回调验签都懒得做,更别说处理重复通知、订单状态机跃迁、退款幂等、对账文件解析这些真正卡在生产环境脖子上的环节。而这份 Java 互联网支付系统源码,是我在某中型电商 SaaS 平台交接时拆出来的可部署、可调试、可审计的实战基线:它不是教你怎么写 Controller,而是直接给你一套已通过三方支付机构联调认证的代码骨架——支付宝用的是alipay-sdk-java5.0.0+(支持 RSA2),微信走的是weixin-java-pay4.5.0(兼容 JSAPI/NATIVE/APP 三类场景),银联对接的是unionpay-sdk1.3.0(含全渠道交易+无跳转收银台)。所有支付网关层都做了统一抽象(PaymentGateway接口),每个渠道实现类里明明白白写着「商户号怎么配」「证书路径在哪」「回调地址怎么验签」「失败重试策略几秒几次」。如果你正被面试官问“微信支付回调怎么防重放”,或者刚接手一个老系统要补上银联通道,又或者想搞懂 SpringBoot 里怎么把支付结果塞进 RocketMQ 做最终一致性——这份源码就是你不用再从零翻文档、抄博客、踩坑三天才搞明白的「血泪经验压缩包」。
2. 三端支付接入不是复制粘贴:SpringBoot 如何用统一网关封装支付宝/微信/银联差异
2.1 统一支付网关设计:为什么不能每个渠道写一套 Controller?
很多团队一开始图省事,给支付宝写/alipay/pay,微信写/wechat/pay,银联再开/unionpay/pay——表面看逻辑隔离,实则埋下三颗雷:一是前端调用方要硬编码渠道判断;二是后续加新渠道(比如 PayPal 或数字人民币)得改 N 处路由和参数解析;三是对账、监控、限流无法统一打点。本项目采用Strategy + Factory 模式构建PaymentGateway抽象层:
public interface PaymentGateway { /** * 发起支付请求(返回跳转URL或预支付ID) */ PaymentResult initiate(PaymentRequest request); /** * 处理异步回调(验签、幂等、状态更新) */ CallbackResult handleCallback(HttpServletRequest request); /** * 主动查询订单状态(用于超时补偿) */ QueryResult queryOrder(String outTradeNo); }提示:
PaymentRequest是统一入参 DTO,包含outTradeNo(商户订单号)、amount(分)、subject(商品标题)、channel(枚举:ALIPAY/WECHAT/UNIONPAY)等字段。渠道差异全部收敛在实现类内部,Controller 层只认这个接口。
2.2 支付宝接入:RSA2 签名 + 异步通知 + 退款幂等的硬核落地
支付宝 SDK 5.x 版本强制要求 RSA2 签名,且沙箱环境与正式环境证书格式不一致——这是新手最容易翻车的第一关。本项目AlipayGatewayImpl的关键配置如下:
@Configuration public class AlipayConfig { @Value("${alipay.app-id}") private String appId; @Value("${alipay.merchant-private-key}") private String merchantPrivateKey; // PKCS8 格式私钥(非 PKCS1!) @Value("${alipay.alipay-public-key}") private String alipayPublicKey; @Value("${alipay.gateway-url}") private String gatewayUrl; // 正式环境用 https://openapi.alipay.com/gateway.do @Bean public AlipayClient alipayClient() { return new DefaultAlipayClient( gatewayUrl, appId, merchantPrivateKey, "json", "UTF-8", alipayPublicKey, "RSA2" // 必须显式指定,否则默认 RSA ); } }发起支付的核心逻辑(JSAPI 场景):
@Override public PaymentResult initiate(PaymentRequest request) { AlipayTradeAppPayRequest alipayRequest = new AlipayTradeAppPayRequest(); alipayRequest.setNotifyUrl("https://your-domain.com/callback/alipay"); // 必须公网可访问 alipayRequest.setBizContent(JSON.toJSONString(Map.of( "out_trade_no", request.getOutTradeNo(), "total_amount", String.format("%.2f", request.getAmount() / 100.0), "subject", request.getSubject(), "product_code", "QUICK_WAP_WAY" // H5 支付 ))); try { AlipayTradeAppPayResponse response = alipayClient.sdkExecute(alipayRequest); return PaymentResult.success(response.getBody()); // 返回 raw XML 字符串(前端需解析) } catch (AlipayApiException e) { throw new PaymentException("Alipay init failed", e); } }参数说明:
QUICK_WAP_WAY表示 H5 支付(适配手机浏览器),若为小程序需换QUICK_MSECURITY_PAY;total_amount单位是元(字符串格式),SDK 会自动转成分;notifyUrl必须是 HTTPS 且域名已在支付宝后台备案,否则回调不触发。
2.3 微信支付接入:V3 API + 证书双向认证 + 回调解密的完整链路
微信 V3 API 要求必须使用平台证书进行双向认证,且回调 Body 是 AES-256-GCM 加密的——这比支付宝复杂得多。本项目WechatGatewayImpl使用weixin-java-pay封装了全部细节:
@Configuration public class WechatConfig { @Value("${wechat.app-id}") private String appId; @Value("${wechat.mch-id}") private String mchId; @Value("${wechat.api-v3-key}") private String apiV3Key; // 商户 API 证书密码(不是私钥密码!) @Value("${wechat.cert-path}") private String certPath; // apiclient_cert.p12 文件路径 @Value("${wechat.key-path}") private String keyPath; // apiclient_key.pem 路径(PKCS#8 格式) @Bean public WxPayService wxPayService() { WxPayService service = new WxPayServiceImpl(); WxPayConfig config = new WxPayConfig(); config.setAppId(appId); config.setMchId(mchId); config.setMchKey(apiV3Key); config.setCertPath(certPath); config.setKeyPath(keyPath); service.setConfig(config); return service; } }发起 JSAPI 支付(需用户 openid):
@Override public PaymentResult initiate(PaymentRequest request) { WxPayUnifiedOrderRequest orderRequest = new WxPayUnifiedOrderRequest(); orderRequest.setAppid(wxPayService.getConfig().getAppId()); orderRequest.setMchId(wxPayService.getConfig().getMchId()); orderRequest.setOutTradeNo(request.getOutTradeNo()); orderRequest.setTotalFee(request.getAmount()); // 单位:分 orderRequest.setBody(request.getSubject()); orderRequest.setOpenid(request.getOpenid()); // 小程序/公众号必需 orderRequest.setNotifyUrl("https://your-domain.com/callback/wechat"); try { WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(orderRequest); Map<String, String> packageParams = new HashMap<>(); packageParams.put("appId", result.getAppid()); packageParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); packageParams.put("nonceStr", UUID.randomUUID().toString().replace("-", "")); packageParams.put("package", "prepay_id=" + result.getPrepayId()); packageParams.put("signType", "RSA"); packageParams.put("paySign", WxPaySignature.sign(packageParams, wxPayService.getConfig().getKeyPrivate())); // 签名用商户私钥 return PaymentResult.success(packageParams); // 返回给前端调起支付 } catch (WxPayException e) { throw new PaymentException("Wechat init failed", e); } }注意:
WxPaySignature.sign()是 SDK 提供的签名方法,传入keyPrivate(商户私钥)而非平台证书私钥;prepay_id是微信生成的预支付交易会话标识,前端调用wx.requestPayment时必须传入。
2.4 银联接入:全渠道交易 + 无跳转收银台 + 对账文件解析的冷门但刚需能力
银联不像支付宝/微信有成熟 SDK,本项目基于unionpay-sdk1.3.0 实现了B2C 网银支付 + 无跳转收银台(APP 支付)双模式,并内置对账文件(.txt)解析器:
@Configuration public class UnionpayConfig { @Value("${unionpay.mer-id}") private String merId; @Value("${unionpay.cert-path}") private String certPath; // 签名证书路径(.pfx) @Value("${unionpay.cert-password}") private String certPassword; @Value("${unionpay.acp-front-url}") private String acpFrontUrl; // 前台交易地址(H5) @Value("${unionpay.acp-back-url}") private String acpBackUrl; // 后台交易地址(APP) @Bean public UnionpayService unionpayService() { UnionpayService service = new UnionpayService(); service.setMerId(merId); service.setCertPath(certPath); service.setCertPassword(certPassword); service.setAcqCode("000000"); // 收单机构代码(银联分配) return service; } }APP 支付(无跳转)核心流程:
@Override public PaymentResult initiate(PaymentRequest request) { Map<String, String> reqData = new HashMap<>(); reqData.put("version", "5.1.0"); reqData.put("encoding", "UTF-8"); reqData.put("certId", unionpayService.getCertId()); // 从证书中读取 reqData.put("signMethod", "01"); // RSA 签名 reqData.put("txnType", "01"); // 交易类型:消费 reqData.put("txnSubType", "01"); // 子类型:APP 支付 reqData.put("bizType", "000201"); // 业务类型:APP 支付 reqData.put("channelType", "07"); // 渠道类型:APP reqData.put("merId", unionpayService.getMerId()); reqData.put("accessType", "0"); // 接入类型:直连 reqData.put("orderId", request.getOutTradeNo()); reqData.put("txnTime", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); reqData.put("txnAmt", String.valueOf(request.getAmount())); // 单位:分 reqData.put("currencyCode", "156"); // 人民币 reqData.put("reqReserved", "custom_data"); // 自定义透传字段 try { Map<String, String> respData = AcpService.signAndPost(reqData, unionpayService.getAcpBackUrl(), "UTF-8"); if ("00".equals(respData.get("respCode"))) { return PaymentResult.success(Map.of( "tn", respData.get("tn"), // 银联交易流水号(APP 端调起 SDK 用) "sign", respData.get("signature") // 签名值 )); } else { throw new PaymentException("Unionpay init failed: " + respData.get("respMsg")); } } catch (Exception e) { throw new PaymentException("Unionpay init exception", e); } }关键点:
tn是银联生成的交易令牌,APP 端需调用银联 SDK 的startPay方法传入该值;signMethod=01表示 RSA 签名,certId必须从.pfx证书中提取(SDK 提供CertUtil.getCertId()方法);reqReserved字段可用于透传用户 ID,避免回调时查不到原始订单。
3. 回调不是“收到就更新订单”:三端验签、幂等、状态机与补偿机制的生死线
3.1 支付宝回调:验签失败的 90% 都栽在字符集和参数顺序上
支付宝回调 URL 是 POST 请求,Body 是application/x-www-form-urlencoded格式,但 SDK 要求验签时必须按字典序拼接未 sign 的所有参数(不含sign和sign_type),且所有值必须 URLDecode 后再拼接——这是最常被忽略的细节:
@Override public CallbackResult handleCallback(HttpServletRequest request) { try { Map<String, String> params = getParameterMap(request); // 获取所有参数(含 sign) String sign = params.remove("sign"); String signType = params.remove("sign_type"); // 1. 按 key 字典序排序并拼接(注意:必须 URLDecode!) String content = params.entrySet().stream() .sorted(Map.Entry.comparingByKey()) .map(e -> e.getKey() + "=" + URLDecoder.decode(e.getValue(), "UTF-8")) .collect(Collectors.joining("&")); // 2. 验签(SDK 提供的工具类) boolean verified = AlipaySignature.rsaCheck(content, sign, alipayPublicKey, "UTF-8", "RSA2"); if (!verified) { return CallbackResult.fail("Alipay sign verify failed"); } // 3. 幂等校验(查 DB 是否已处理过该 out_trade_no) String outTradeNo = params.get("out_trade_no"); if (paymentRecordService.isProcessed(outTradeNo)) { return CallbackResult.success(); // 已处理,直接返回 success } // 4. 状态机更新(根据 trade_status 切换订单状态) String tradeStatus = params.get("trade_status"); switch (tradeStatus) { case "TRADE_SUCCESS": case "TRADE_FINISHED": paymentRecordService.updateSuccess(outTradeNo, params.get("trade_no")); break; case "TRADE_CLOSED": paymentRecordService.updateClosed(outTradeNo); break; default: log.warn("Unknown trade_status: {}", tradeStatus); } return CallbackResult.success(); } catch (Exception e) { log.error("Alipay callback error", e); return CallbackResult.fail("Internal error"); } }血泪经验:
URLDecoder.decode()必须加,否则中文商品名会导致验签失败;AlipaySignature.rsaCheck()第二个参数是content(拼接后的字符串),不是原始 Body;trade_status有 5 种状态,但只有TRADE_SUCCESS和TRADE_FINISHED表示支付成功,TRADE_CLOSED是关闭(未支付),不可误判为失败。
3.2 微信回调:AES 解密 + 平台证书验证 + 时间戳防重放的三重防护
微信 V3 回调 Body 是加密的 JSON,且 Header 中带Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature三要素——必须全部校验:
@Override public CallbackResult handleCallback(HttpServletRequest request) { try { // 1. 提取 Header 信息 String timestamp = request.getHeader("Wechatpay-Timestamp"); String nonce = request.getHeader("Wechatpay-Nonce"); String signature = request.getHeader("Wechatpay-Signature"); // 2. 校验时间戳(5 分钟内有效) long now = System.currentTimeMillis() / 1000; if (Math.abs(now - Long.parseLong(timestamp)) > 300) { return CallbackResult.fail("Timestamp expired"); } // 3. 构造待签名串 String message = timestamp + "\n" + nonce + "\n" + IOUtils.toString(request.getInputStream(), "UTF-8") + "\n"; String platformPublicKey = wxPayService.getConfig().getPlatformPublicKey(); // 从微信平台证书获取 if (!WxPaySignature.verify(message, signature, platformPublicKey)) { return CallbackResult.fail("Wechat signature verify failed"); } // 4. AES 解密 Body String body = IOUtils.toString(request.getInputStream(), "UTF-8"); String decrypted = AesGcmUtil.decrypt( body, wxPayService.getConfig().getApiV3Key(), // 商户 API v3 密钥 wxPayService.getConfig().getMchId() ); // 5. 解析 JSON 并更新订单 JSONObject json = JSONObject.parseObject(decrypted); String outTradeNo = json.getString("resource").getString("out_trade_no"); String transactionId = json.getString("resource").getString("transaction_id"); String tradeState = json.getString("event"); // 事件类型:TRANSACTION.SUCCESS if ("TRANSACTION.SUCCESS".equals(tradeState)) { paymentRecordService.updateSuccess(outTradeNo, transactionId); } return CallbackResult.success(); } catch (Exception e) { log.error("Wechat callback error", e); return CallbackResult.fail("Internal error"); } }注意:
AesGcmUtil.decrypt()是微信 SDK 提供的解密工具,apiV3Key是商户在微信商户平台设置的 32 位密钥(非证书密码);platformPublicKey必须定期从微信平台证书接口拉取并缓存,不能硬编码;event字段才是实际支付结果,trade_state是旧版字段,V3 回调已弃用。
3.3 银联回调:同步应答 + 异步通知双模式与对账文件解析实战
银联回调分两种:同步应答(前台跳转后立即返回)和异步通知(后台服务器间通信)。本项目只处理异步通知(更可靠),且要求必须返回success字符串(不能是 XML 或 JSON):
@PostMapping("/callback/unionpay") public @ResponseBody String handleUnionpayCallback(HttpServletRequest request) { try { Map<String, String> params = getParameterMap(request); String signature = params.remove("signature"); String signatureMethod = params.remove("signatureMethod"); // 1. 按银联规则拼接验签字符串(key 升序,不含 signature 和 signatureMethod) String content = params.entrySet().stream() .sorted(Map.Entry.comparingByKey()) .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining("&")); // 2. 验签(银联 SDK 提供) boolean verified = AcpService.validate(content, signature, signatureMethod, "UTF-8"); if (!verified) { return "fail"; // 必须返回 fail 字符串 } // 3. 幂等校验 & 状态更新 String orderId = params.get("orderId"); if (paymentRecordService.isProcessed(orderId)) { return "success"; } String respCode = params.get("respCode"); if ("00".equals(respCode)) { // 00 表示交易成功 paymentRecordService.updateSuccess(orderId, params.get("queryId")); } else { paymentRecordService.updateFailed(orderId, params.get("respMsg")); } return "success"; // 必须返回 success 字符串,否则银联会重发 } catch (Exception e) { log.error("Unionpay callback error", e); return "fail"; } }关键点:银联验签字符串不 URLDecode(与支付宝相反);
respCode=00才是成功,其他如03(无效商户)、04(交易失败)需记录日志并人工介入;返回字符串必须是纯success或fail,多一个空格都会导致银联重发。
3.4 回调避坑:三端共性问题与血泪排查清单
现象 1:支付宝回调一直收不到,本地测试却正常
→ 原因:支付宝回调地址必须是HTTPS 公网域名,且该域名需在支付宝开放平台「应用信息 → 网站主页」中备案;若用https://ngrok.io临时映射,支付宝会拒绝发送(沙箱环境除外)。
→ 解决:用阿里云 SLB + SSL 证书部署真实域名,或在支付宝后台将回调地址设为https://your-domain.com/callback/alipay(确保 Nginx 转发到 SpringBoot)。
现象 2:微信回调解密失败,报javax.crypto.AEADBadTagException
→ 原因:apiV3Key错误(不是商户 API 密钥,而是微信商户平台「API 安全 → APIv3 密钥」设置的 32 位字符串);或body被 Tomcat 自动 URLDecode 过(微信 Body 是 Base64 编码,不能 decode)。
→ 解决:检查apiV3Key是否为 32 位纯数字/字母;在WebMvcConfigurer中禁用FormContentFilter,或改用@RequestBody byte[]接收原始字节流。
现象 3:银联回调反复重发,日志显示signature verify failed
→ 原因:银联验签字符串拼接时漏掉了encoding=UTF-8参数(即使请求头是 UTF-8,也必须显式加入拼接串);或certId读取错误(.pfx证书密码输错导致CertUtil.getCertId()返回 null)。
→ 解决:打印拼接后的content字符串与银联文档示例对比;用keytool -list -v -keystore your.pfx -storetype PKCS12验证证书密码。
现象 4:订单状态更新错乱,同一笔支付触发多次成功回调
→ 原因:未做幂等校验,或数据库is_processed字段未加唯一索引,导致并发插入重复记录。
→ 解决:在payment_record表建联合唯一索引UNIQUE KEY idx_out_trade_no (out_trade_no, channel);幂等校验 SQL 加INSERT IGNORE或ON DUPLICATE KEY UPDATE。
现象 5:微信退款回调收不到,但查退款结果是 success
→ 原因:微信退款回调 URL 与支付回调 URL必须相同(微信不支持独立退款回调地址);且退款回调事件类型是REFUND.SUCCESS,不是TRANSACTION.SUCCESS。
→ 解决:在微信回调处理器中增加if ("REFUND.SUCCESS".equals(event))分支;确保退款操作调用wxPayService.refund()时传入的notifyUrl与支付一致。
4. 生产级支付系统绕不开的四座大山:对账、退款、风控与监控
4.1 对账不是 Excel 手工比:支付宝/微信/银联对账文件自动下载与解析
对账是支付系统的命脉,本项目提供定时任务每日凌晨拉取三方对账单并入库:
@Component @Scheduled(cron = "0 0 2 * * ?") // 每日凌晨 2 点执行 public class ReconciliationJob { @Autowired private AlipayService alipayService; @Autowired private WechatService wechatService; @Autowired private UnionpayService unionpayService; public void run() { LocalDate yesterday = LocalDate.now().minusDays(1); String dateStr = yesterday.format(DateTimeFormatter.BASIC_ISO_DATE); // 20240520 // 支付宝对账(需先申请开通对账单下载权限) String alipayFile = alipayService.downloadBill(dateStr, "trade"); parseAlipayBill(alipayFile); // 微信对账(V3 API) String wechatFile = wechatService.downloadBill(dateStr); parseWechatBill(wechatFile); // 银联对账(需银联提供 FTP 账号) String unionpayFile = unionpayService.downloadBill(dateStr); parseUnionpayBill(unionpayFile); } private void parseAlipayBill(String filePath) { // 支付宝对账单是 CSV,第一行是表头,字段用逗号分隔,中文用双引号包裹 try (CSVReader reader = new CSVReader(new FileReader(filePath), ',', '"', 1)) { List<String[]> rows = reader.readAll(); for (String[] row : rows) { if (row.length >= 10 && "success".equals(row[9])) { // 第10列是交易状态 String outTradeNo = row[1]; // 商户订单号 String tradeNo = row[2]; // 支付宝交易号 BigDecimal amount = new BigDecimal(row[4]); // 金额(元) reconciliationService.matchRecord(outTradeNo, tradeNo, amount); } } } } }提示:支付宝对账单需在开放平台「资金管理 → 对账单下载」手动开通权限;微信对账单需调用
https://api.mch.weixin.qq.com/v3/bill/combine-day?date=20240520;银联对账单通常通过 FTP 下载,需配置ftp.host/port/username/password。
4.2 退款不是“调个接口就行”:三端幂等、部分退款与资金原路退回的边界
退款必须保证幂等 + 原路退回 + 金额校验,本项目RefundService统一处理:
@Service public class RefundService { public RefundResult refund(String outTradeNo, String channel, BigDecimal amount) { // 1. 校验原始订单是否存在且已支付 PaymentRecord record = paymentRecordService.findByOutTradeNo(outTradeNo); if (!"SUCCESS".equals(record.getStatus())) { throw new BusinessException("Order not paid"); } // 2. 校验退款金额不超过原订单(支持多次部分退款) BigDecimal totalRefunded = refundRepository.sumByOutTradeNo(outTradeNo); BigDecimal maxRefund = record.getAmount().subtract(totalRefunded); if (amount.compareTo(maxRefund) > 0) { throw new BusinessException("Refund amount exceeds remaining balance"); } // 3. 调用渠道退款接口 switch (channel) { case "ALIPAY": return alipayGateway.refund(outTradeNo, amount); case "WECHAT": return wechatGateway.refund(outTradeNo, amount); case "UNIONPAY": return unionpayGateway.refund(outTradeNo, amount); default: throw new BusinessException("Unsupported channel"); } } }支付宝退款实现(支持部分退款):
@Override public RefundResult refund(String outTradeNo, BigDecimal amount) { AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); request.setBizContent(JSON.toJSONString(Map.of( "out_trade_no", outTradeNo), "refund_amount", amount.setScale(2, RoundingMode.HALF_UP).toString(), "out_request_no", UUID.randomUUID().toString().replace("-", "") // 退款请求号(幂等关键) ))); try { AlipayTradeRefundResponse response = alipayClient.execute(request); if ("10000".equals(response.getCode())) { return RefundResult.success(response.getTradeNo(), response.getOutRequestNo()); } else { throw new PaymentException("Alipay refund failed: " + response.getMsg()); } } catch (AlipayApiException e) { throw new PaymentException("Alipay refund exception", e); } }注意:
out_request_no是商户侧退款单号,支付宝用它做幂等控制(同一out_request_no多次调用只退一次);微信退款必须传out_refund_no(同理),且total_fee和refund_fee单位都是分;银联退款需调用acpBackUrl的refund接口,返回respCode=00才成功。
4.3 风控不是“加个 Redis 限流”:基于规则引擎的实时交易拦截
本项目集成Drools规则引擎,定义以下风控规则:
| 规则 ID | 触发条件 | 动作 |
|---|---|---|
| RULE_001 | 同一 IP 1 小时内支付失败 ≥ 5 次 | 拦截并返回 "请求过于频繁" |
| RULE_002 | 同一手机号 24 小时内下单金额 > 50000 | 触发人工审核 |
| RULE_003 | 订单金额为 0.01 元且商品标题含 "测试" | 自动标记为测试订单,不走真实支付 |
Drools 规则文件payment.drl:
rule "IP Frequency Limit" when $p: PaymentRequest($ip: ip != null) $count: Number(intValue > 5) from accumulate( $e: PaymentEvent(ip == $ip, status == "FAILED", createTime > (now - 3600000)), count($e) ) then $p.setBlocked(true); $p.setBlockReason("IP frequency limit exceeded"); end rule "High Amount Review" when $p: PaymentRequest(amount > 5000000, phone != null) $count: Number() from accumulate( $e: PaymentEvent(phone == $p.phone, createTime > (now - 86400000)), count($e) ) then $p.setNeedReview(true); insert(new ReviewTask($p.getOutTradeNo(), "HIGH_AMOUNT")); end提示:
PaymentEvent是支付行为日志实体,由 AOP 切面在PaymentGateway.initiate()后自动记录;规则引擎启动时加载kmodule.xml,SpringBoot 通过KieContainer注入规则。
4.4 监控不是“看 Grafana 图表”:支付成功率、回调延迟、对账差异的黄金指标
本项目暴露/actuator/metrics端点,并定义以下核心指标:
| 指标名 | 类型 | 说明 | 报警阈值 |
|---|---|---|---|
payment.success.rate | Gauge | 支付成功率(成功数 / 总请求数) | < 99.5% |
payment.callback.delay.seconds | Timer | 回调处理耗时(P95) | > 3s |
reconciliation.mismatch.count | Counter | 对账差异笔数 | > 0 |
Prometheus 配置片段:
- job_name: 'payment-service' metrics_path: '/actuator/prometheus' static_configs: - targets: ['payment-service:8080'] relabel_configs: - source_labels: [__meta_kubernetes_pod_label_app] target_label: appGrafana 看板关键查询:
# 支付成功率(最近 1 小时) 100 * sum(rate(payment_success_total{app="payment-service"}[1h])) by (channel) / sum(rate(payment_request_total{app="payment-service"}[1h])) by (channel) # 回调延迟 P95(按渠道) histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{uri=~"/callback/.*"}[1h])) by (le, channel))注意:
payment_success_total是自定义 Counter,每次PaymentResult.success()时increment();http_server_requests_seconds_bucket是 SpringBoot Actuator 自动暴露的 HTTP 耗时直方图。
5. 从开发到上线:支付系统上线 checklist 与线上问题快速定位技巧
5.1 上线前必须完成的 12 项硬性检查
提示:这份 checklist 来自我参与过的 7 次支付系统上线,漏掉任意一项都可能导致资损或客诉。
| 序号 | 检查项 | 检查方式 | 不通过后果 |
|---|---|---|---|
| 1 | 所有渠道的notify_url已在三方平台备案且 HTTPS 可访问 | curl -I https://your-domain.com/callback/alipay | 支付宝/微信回调不触发 |
| 2 | 支付宝merchant_private_key是 PKCS8 格式(非 PKCS1) | openssl pkcs8 -in key.pem -nocrypt -info | 签名失败,支付请求被拒 |
| 3 | 微信apiV3Key是 32 位纯数字/字母,且已填入商户平台 | 登录微信商户平台核对 | 回调解密失败,订单状态不更新 |
| 4 | 银联.pfx证书密码正确,certId可正常提取 | keytool -list -v -keystore cert.pfx -storetype PKCS12 | 签名失败,交易被银联拒绝 |
| 5 | 数据库payment_record表有UNIQUE KEY idx_out_trade_no (out_trade_no, channel) | SHOW CREATE TABLE payment_record | 幂等失效,重复扣款 |
| 6 | Redis 连接池配置max-active=200,超时timeout=2000ms | 查application.yml | 高并发下连接池耗尽,支付请求超时 |
| 7 | Nginx 配置client_max_body_size 10M,proxy_read_timeout 300 | 查nginx.conf | 微信回调 Body 过大被截断,解密失败 |
| 8 | 日志级别logging.level.com.xxx.payment=DEBUG已关闭(生产环境) | 查logback-spring.xml | 磁盘爆满,服务宕机 |
| 9 | 对账任务@Scheduled的 cron 表达式已改为0 0 3 * * ?(避开银联对账生成高峰) | 查 |
本文还有配套的精品资源,点击获取