资讯动态

SpringBoot支付系统实战:支付宝微信对接与订单状态机设计

发布时间:2026/9/12 13:04:08 来源:尧图企业网站定制
简介基于Spring Boot框架开发的支付系统项目完整整合了支付宝、微信支付以及订单管理三大模块覆盖下单、支付回调、订单查询等典型业务场景非常适合Java后端初学者、毕业设计学生以及需要快速搭建支付模块的开发者参考学习。压缩包内共66个文件以44个Java源码文件为主配合6个JSP页面负责前端展示另有5个Jar依赖、多个配置文件和数据库脚本、实体关系图等辅助文件整个包仅6.27MB体积小巧且目录层次分明便于按功能模块查阅与复用。目前该项目已有101人学习作者表示经过完整测试真实可靠可放心使用。通过深入研读代码可以清晰理解Spring Boot的自动配置原理、MVC分层架构、支付接口的对接流程以及订单状态的流转设计同时利用pom.xml、README和数据库脚本能够快速复现运行环境对课程设计、毕业答辩或小型商业项目都具有较高的参考价值。1. 拿下支付系统zip包先别急着把项目跑起来解压支付系统项目的压缩包很多人第一件事就是跑起来然后卡在下单页面出不来二维码。原因往往不在SpringBoot启动而在支付宝与微信支付两套签名、回调的数据流没走通。这个标题下的项目本质是用SpringBoot把支付与订单系统串成闭环下单生成订单回调更新订单超时关单退款反向改流水。它适合两类人用SpringBoot做商城后端、想快速接支付宝支付或微信支付的人以及做毕设、需要一份能讲清表结构与状态流转工程的人。真正值钱的不是接口参数而是订单与支付流水的表设计以及两套回调的验签与幂等逻辑。这篇按支付生命周期展开先拆表结构和状态机再分别落地支付宝、微信支付的最小对接最后补上超时关单与对账。跟着走能把项目从“能启动”推到“能解释每处回调为什么这么写”。2. 先看懂SpringBoot支付系统的表结构与支付链路2.1 订单表与支付流水表为什么必须分开拿到zip包的第一步不是找Controller而是先看entity和sql脚本。一个能跑通的支付系统数据库里三张表跑不掉订单表、支付流水表、退款表。很多项目把支付信息直接塞进订单表省事但后面改需求会很难受。订单表表达的是“用户买了什么”支付流水表记录的是“这笔订单的每一次支付尝试”。用户扫码后没付、付款超时、换支付方式重扫一次在支付宝或微信侧会产生多笔交易单号订单表只有一条流水表则会有多条。把两者分开订单状态机的维护和退款关联都会清晰很多。2.1.1 核心建表SQL与字段说明订单主表和支付流水表的最小结构如下CREATE TABLE t_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(64) NOT NULL COMMENT 业务订单号, user_id BIGINT NOT NULL COMMENT 用户ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单金额(元), order_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已关闭 3已退款, channel VARCHAR(16) COMMENT ALIPAY/WECHAT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; CREATE TABLE t_pay_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, pay_flow_no VARCHAR(64) NOT NULL COMMENT 支付流水号(系统内), order_no VARCHAR(64) NOT NULL COMMENT 关联业务订单号, out_trade_no VARCHAR(64) COMMENT 第三方交易号:支付宝/微信单号, channel VARCHAR(16) NOT NULL COMMENT ALIPAY/WECHAT, pay_amount DECIMAL(10,2) NOT NULL COMMENT 本次支付金额(元), pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0支付中 1成功 2失败 3已退款, fail_reason VARCHAR(255) COMMENT 失败原因, notify_time DATETIME COMMENT 回调时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_pay_flow_no (pay_flow_no), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT支付流水表;order_no是业务侧生成的单号必须唯一t_pay_order里的pay_flow_no是系统内流水号out_trade_no是支付成功回调里带回来的第三方交易号。两表的联系是order_no一对多而“一次支付成功”的判定以t_pay_order.pay_status1为准。金额字段一律用DECIMAL(10,2)不要用FLOAT或DOUBLE避免浮点误差污染对账数据。2.2 订单状态机与枚举定义订单语义的完整状态建议在Java枚举中收敛常量避免业务代码里散落魔法数字常量状态值含义触发点WAIT_PAY0待支付下单成功PAID1已支付支付宝/微信回调成功CLOSED2已关闭超时关单/用户主动取消REFUNDED3已退款退款回调或退款单完成public enum OrderStatus { WAIT_PAY(0, 待支付), PAID(1, 已支付), CLOSED(2, 已关闭), REFUNDED(3, 已退款); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } }状态机里最容易错的一条边是CLOSED之后还能不能回到PAID。如果关单只改数据库、没有同步调用支付平台关单接口用户仍然可能扫旧二维码付款回调会带着TRADE_SUCCESS回来。所以这章末尾先记住关单是一个跨系统动作改库只是最后一步后面第5章会专门给方案。3. 支付宝支付对接Java对接支付宝支付的最小实现3.1 maven依赖与application.yml配置Java对接支付宝支付现在主流做法是引入官方SDK alipay-sdk-java自己拼HTTP请求和RSA2签名没必要也不安全。dependency groupIdcom.alipay.sdk/groupId artifactIdalipay-sdk-java/artifactId version4.39.71.ALL/version /dependency提示版本号以你自己编译通过的为准。若在高版本SpringBoot下出现log相关冲突通常源自SDK传递引用的commons-logging排除即可。配置文件里必须准备好的几项配置项含义获取方式alipay.app-id应用AppID开放平台创建应用后获得alipay.private-key应用私钥RSA密钥对生成后自行保存alipay.public-key支付宝公钥上传应用公钥后在开放平台查看alipay.notify-url异步回调地址公网可访问的HTTPS地址alipay.gateway网关地址沙箱为openapi.alipaydev.com/gateway.do用一个配置类绑定前缀业务代码里只注入这个类Component ConfigurationProperties(prefix alipay) Data public class AlipayProperties { private String appId; private String privateKey; private String alipayPublicKey; private String notifyUrl; private String gateway https://openapi.alipay.com/gateway.do; }这段代码说明两点ConfigurationProperties会把application.yml里alipay前缀下的配置自动映射到字段免去一个个Valuegateway写成默认值后沙箱联调时只要覆盖一行配置代码不用改。3.2 电脑网站下单与Wap下单常用接口有两个电脑网站用alipay.trade.page.pay手机网站用alipay.trade.wap.pay。二者参数结构相同区别在product_code和Request对象类型。public String createAlipayOrder(String orderNo, BigDecimal amount, String subject) { AlipayClient client new DefaultAlipayClient( alipayProperties.getGateway(), alipayProperties.getAppId(), alipayProperties.getPrivateKey(), json, UTF-8, alipayProperties.getAlipayPublicKey(), RSA2); AlipayTradePagePayModel model new AlipayTradePagePayModel(); model.setOutTradeNo(orderNo); model.setTotalAmount(amount.setScale(2, RoundingMode.HALF_UP).toString()); model.setSubject(subject); model.setProductCode(FAST_INSTANT_TRADE_PAY); model.setTimeoutExpress(15m); AlipayTradePagePayRequest request new AlipayTradePagePayRequest(); request.setReturnUrl(https://你的前端地址/pay/result); request.setNotifyUrl(alipayProperties.getNotifyUrl()); request.setBizModel(model); AlipayTradePagePayResponse response client.pageExecute(request); return response.getBody(); // 自动提交表单的HTML }out_trade_no直接复用订单表order_nototal_amount是元为单位的字符串最多两位小数所以先setScale(2)再toStringtimeout_express表示订单支付超时时间单位m表示分钟支付宝到点会自动关闭交易。pageExecute返回的是HTML表单前端直接渲染就能跳转收银台如果用的是Wap支付把Request换成AlipayTradeWapPayRequest、product_code换成QUICK_WAP_WAY即可。3.3 异步回调验签与幂等处理支付宝支付成功后会POST表单到notify_url处理顺序必须是验签、判断交易状态、幂等更新、返回success。PostMapping(/notify/alipay) public String alipayNotify(HttpServletRequest request) throws AlipayApiException { MapString, String params new HashMap(); request.getParameterMap().forEach((key, values) - params.put(key, values[0])); boolean signOk AlipaySignature.rsaCheckV1( params, alipayProperties.getAlipayPublicKey(), UTF-8, RSA2); if (!signOk) { return failure; } if (!TRADE_SUCCESS.equals(params.get(trade_status))) { return success; // WAIT_BUYER_PAY等中间状态不处理 } String orderNo params.get(out_trade_no); String tradeNo params.get(trade_no); orderService.markPaid(orderNo, ALIPAY, tradeNo); return success; }rsaCheckV1的入参是支付宝回调的原始参数Map、支付宝公钥、编码方式和签名类型。验签失败返回failure支付宝会按频率重试验签成功后只有trade_status等于TRADE_SUCCESS才代表买家已付款。这里的幂等由markPaid保证它内部先查订单状态已支付就直接返回避免重复通知把库存扣两遍。3.4 退款与主动查询接口退款推荐用alipay.trade.refund参数结构与下单类似public boolean refund(String orderNo, BigDecimal refundAmount) { AlipayTradeRefundRequest request new AlipayTradeRefundRequest(); AlipayTradeRefundModel model new AlipayTradeRefundModel(); model.setOutTradeNo(orderNo); model.setRefundAmount(refundAmount.setScale(2, RoundingMode.HALF_UP).toString()); request.setBizModel(model); AlipayTradeRefundResponse resp client.execute(request); return resp.isSuccess(); }业务上要记录“已退款金额”和“可退金额”不能只靠订单总金额判断否则部分退款多次执行会导致超退。对账时用alipay.trade.query查询交易状态入参是out_trade_no返回的trade_status和total_amount能与本地流水比对。4. 微信支付接口对接V3协议的SpringBoot集成4.1 V3与支付宝的关键差异微信支付从V2升级到V3后接口风格与支付宝差异很大对接前先列一张对照表维度支付宝微信支付V3金额单位元字符串两位小数分整数int回调内容明文表单AES-256-GCM密文回调验签应用公钥RSA2微信支付平台证书请求签名应用私钥签名商户私钥证书序列号退款接口alipay.trade.refundPOST /v3/refund/domestic/refunds这个差异直接影响实现方式金额换算写错一分钱平台直接报错回调数据结构看错解密出来全是乱码。微信支付V3的商户号、AppID、APIv3密钥、商户证书序列号四样东西缺一不可配错任何一个都进不了下单环节。4.2 V3配置与SDK选择配置项集中在yml里wechat: pay: mch-id: 1600000000 app-id: wx8888888888888888 api-v3-key: 你的32位APIv3密钥 private-key-path: classpath:cert/apiclient_key.pem merchant-serial-number: 商户证书序列号 notify-url: https://api.你的域名.com/notify/wechatapi-v3-key是32字节的对称密钥不是商户平台的登录密码生成后不要在日志里输出。private-key-path指向商户API证书私钥文件即apiclient_key.pem。如果项目里用的是wechatpay-java官方SDK它会在启动时读取这些配置去加载平台证书。4.3 Native下单与二维码生成Native支付的下单路径是POST /v3/pay/transactions/native返回code_url前端拿这个链接生成二维码。核心请求体组装如下public String nativePay(String orderNo, BigDecimal amountYuan, String description) { int amountFen amountYuan.multiply(new BigDecimal(100)) .setScale(0, RoundingMode.HALF_UP).intValue(); JSONObject body new JSONObject(); body.put(appid, wxProperties.getAppId()); body.put(mchid, wxProperties.getMchId()); body.put(description, description); body.put(out_trade_no, orderNo); body.put(notify_url, wxProperties.getNotifyUrl()); JSONObject amount new JSONObject(); amount.put(total, amountFen); amount.put(currency, CNY); body.put(amount, amount); // 用httpclient发起POST请求头携带Authorization // 签名串见官方文档METHOD \n URL \n timestamp \n nonce \n body \n // 返回结果里取 code_url交给前端生成二维码 return codeUrlFromResponse; }amountFen是整数分换算时先乘100再setScale(0)不能先转int再乘100否则小数位会丢。微信要求out_trade_no在商户号下唯一所以业务订单号必须全局唯一下单成功后code_url的有效期通常为2小时比支付宝的默认收银台时长长得多条款要在生成的二维码上做倒计时提醒。4.4 回调解密与投诉回调边界微信回调body本身是明文JSON但业务数据全在resource.ciphertext里需要用APIv3密钥做AES-256-GCM解密。解密后得到的JSON里trade_state等于SUCCESS才算买家支付成功。PostMapping(/notify/wechat) public String wxNotify(RequestBody String body) { // 1. 验签使用微信支付平台证书 JSONObject resource JSON.parseObject(body) .getJSONObject(resource); String plain AesDecryptUtil.decryptToString( resource.getString(associated_data), resource.getString(nonce), resource.getString(ciphertext) ); JSONObject data JSON.parseObject(plain); if (SUCCESS.equals(data.getString(trade_state))) { orderService.markPaid( data.getString(out_trade_no), WECHAT, data.getString(transaction_id) ); } return {\code\:\SUCCESS\,\message\:\成功\}; }解密用的AES还是GCM模式nonce和associated_data都随回调下发任何一项传错都解不开解密后的transaction_id是微信侧交易号与支付宝的trade_no用途一致。还有一类容易被忽略的是微信支付投诉回调它也是密文业务对账时会收到用户投诉的openid和complaint_id建议单独设立一个通知接口落库并告警不要和支付回调混在同一个方法里。5. 订单系统超时关单、状态流转与对账兜底5.1 超时未支付自动关单SpringBoot做超时关单简单可靠的开局方案是定时任务扫描适合大多数单量在一分钟几百单以内的项目。核心是更新语句本身带状态条件UPDATE t_order SET order_status 2, updated_at NOW() WHERE order_status 0 AND created_at #{deadline}配套的定时任务Service public class OrderCloseTask { Resource private OrderMapper orderMapper; Scheduled(fixedDelay 30_000) public void closeTimeoutOrders() { LocalDateTime deadline LocalDateTime.now().minusMinutes(15); int rows orderMapper.closeExpiredOrders(deadline); if (rows 0) { log.info(本次关闭超时订单数{}, rows); } } }参数上注意两点定时任务的fixedDelay是30秒任务执行完才开始计时避免上一轮没跑完下一轮就进来deadline用的是15分钟要和生成二维码时设置的timeout_express对齐最好抽成配置项。若使用SpringBoot的Scheduled别忘了在启动类或配置类开启EnableScheduling。5.2 并发状态下回调与关单的竞争处理支付回调、用户主动取消、定时关单可能同时到达同一笔订单。如果都先查状态再更新中间隔着一个网络请求就会出现覆盖。规范做法是让数据库的状态判断成为更新的一部分更新时带上“当前状态必须是待支付”的条件影响行数为0说明已经被别人改过。Transactional public void markPaid(String orderNo, String channel, String outTradeNo) { Order order orderMapper.selectByOrderNo(orderNo); if (order null || order.getOrderStatus() 2) { return; // 订单不存在或已关闭放弃更新 } int rows orderMapper.casPay(orderNo); if (rows 0) { return; // 状态已被其他线程更新不重复处理 } payFlowMapper.insertPayFlow(orderNo, channel, outTradeNo); }对应SQL里的CAS写法是UPDATE t_order SET order_status 1 WHERE order_no #{orderNo} AND order_status 0。1行被更新说明本次回调抢到了“待支付→已支付”的状态转换权0行说明状态早就被改过。流水表再对channel加out_trade_no唯一索引三重保障下来重复通知最多插入一条重复流水更新不会重复执行。5.3 定时查单对账兜底回调丢失是支付对接中最隐蔽的问题用户确实支付了但平台回调因网络波动没到你的服务。所以订单系统里要有一个对账任务维度是“本地支付中、但支付平台已完成”。实现上就是对超过2分钟仍处于支付中的流水逐个调支付宝的alipay.trade.query或微信的查询单接口比较平台返回的支付状态与本地pay_status。两者不一致时以平台为准修正本地状态并触发补货逻辑。这步可以和上面的关单任务共用线程池但频率要低通常5分钟一轮足够。6. 上线前必查的5个坑验签、金额与回调幂等6.1 验签失败先查网关环境和密钥格式支付宝验签失败八成是支付宝公钥填成了应用公钥或者沙箱环境误用了正式网关。微信侧验签失败则优先确认平台证书是否是最新版本证书轮换期旧证书失效会导致偶发验签报错。6.2 回调里的金额必须校验支付宝回调里的total_amount和微信解密后的amount.total都要和本地订单金额比对不一致就丢弃这次通知并告警。这个校验能挡住重复支付、参数污染和部分中间人篡改场景。6.3 金额统一BigDecimal分元转换写成一个工具支付宝用元字符串微信用分int项目内部统一用BigDecimal存储。转换时写一个MoneyUtil元转分用multiply(100)后setScale(0)分转元用divide(100)后setScale(2)。全项目禁止出现Double.valueOf(amount) * 100这种写法。6.4 二维码有效期与关单时间必须联动微信Native码2小时有效支付宝默认收银台有效期看timeout_express配置。关单deadline如果小于二维码有效期会出现订单已关闭、二维码还能扫的脏场景如果大于则用户付款了本地却迟迟不关单。正确做法是把15分钟这类超时时间做成配置项下单和关单两处读取同一个值。6.5 日志脱敏与敏感配置支付系统的exception日志里不要打印完整回调报文和支付平台返回的签名串私钥和APIv3密钥更不能进日志。如果项目引入的管理端能看到明文配置把支付宝privateKey和微信apiV3Key挪到环境变量或配置中心用ConfigurationProperties绑定读取。上线后先在沙箱把“下单-支付-回调-关单-退款”全流程跑通再切一笔1分钱真实交易做冒烟验证确认回调与对账任务日志里的订单号、金额、状态三处完全一致再放量。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价