尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Java的支付平台开发:微信支付、支付宝与状态机设计

基于Java的支付平台开发:微信支付、支付宝与状态机设计 简介这是一个基于Java开发的支付平台项目集成微信支付与支付宝支付两大主流渠道兼顾PC端与移动端场景适合毕业设计、课程设计及企业级项目二次开发。项目源码已经过严格测试可直接参考并在此基础上扩展配套Markdown文档详细说明环境搭建与接口调用方式。压缩包共406个文件大小约40.37MB其中包含216个Java源文件、114个依赖JAR包、24个XML配置、24个JSP页面以及SQL初始化脚本、Properties配置和说明文档等目录按分层结构组织便于快速定位支付核心逻辑与数据库设计。目前已有81人学习下载适合具备Java Web基础、希望快速掌握聚合支付流程的开发者。通过该资源可获取完整前后端联调案例、支付回调处理思路、订单与退款接口实现以及真实项目中的异常处理细节能有效缩短支付功能开发周期辅助完成课程项目或论文落地。1. 支付平台的本质把微信支付和支付宝支付统一成一条交易流水先别急着去申请微信支付商户号和支付宝应用。这类项目最容易翻车的地方不是代码写不出来而是心里只有“调接口”没有“管状态”。等到微信支付的回调到了你要不要立刻把订单改成已支付支付宝同步跳转回来又改一次会不会出事退款到一半用户再次发起支付怎么办这些问题没有一套自己的支付流水模型写到最后全是 if-else 补丁。标题里“基于 java 开发的支付平台支持微信和支付宝支付”说白了是一台“状态机 渠道适配器”。它把用户的下单动作抽象成一条支付单把微信支付、支付宝支付的差异收敛成两个渠道实现再把异步回调处理成唯一合法的“支付成功”入口。不管是毕业设计、课程设计还是作为项目开发练手先把这条主线立住后续所有代码都是在给这条主线填肉。文章适合三类人一是要做 Java 支付相关毕设的学生二是工作中第一次接支付业务、想把回调和对账理清楚的初中级工程师三是准备面试时被“重复回调怎么处理”这类问题卡住的人。下面按“模型 → 建表 → 对接 → 验签 → 加固”的顺序把整条链路盘一遍。2. 订单与交易流水建模状态机、幂等键和金额精度2.1 为什么支付平台要先建流水而不是先写控制器很多新手拿到这个需求第一反应是写一个PayController里面放两个方法一个调微信一个调支付宝然后把支付结果直接写回业务订单表。表面看能跑但业务侧很快会发现问题用户下单后不支付一会换微信一会换支付宝后台发起退款时不知道这笔单走的哪个渠道、渠道流水号是多少渠道账单拿回来后对不上数。支付平台的标准做法是“订单与流水分离”。业务订单表负责业务状态支付平台内部维护一张支付流水表记录每次支付尝试什么时候发起、用的哪个渠道、渠道返回的流水号是什么、金额多少、目前处于什么状态。支付回调只允许更新流水状态再由流水状态去推动业务订单状态避免多渠道写同一行订单数据造成状态错乱。2.2 支付平台的 4 张最小数据表一个能支撑“微信支付 支付宝支付 退款”的最小模型需要四张表支付订单表、交易流水表、退款流水表、渠道配置表。t_payment_order是支付平台的内部单一个业务订单可以对应多条支付记录t_payment_transaction是每次实际调起渠道支付的流水t_payment_refund记录退款请求t_channel_config存各渠道的商户参数。CREATE TABLE t_payment_order ( id BIGINT NOT NULL AUTO_INCREMENT, payment_order_no VARCHAR(64) NOT NULL COMMENT 支付平台单号幂等键, biz_order_no VARCHAR(64) NOT NULL COMMENT 业务方订单号, channel_code VARCHAR(32) NOT NULL COMMENT 渠道编码WECHAT_PAY / ALIPAY, subject VARCHAR(256) NOT NULL COMMENT 商品描述, total_amount BIGINT NOT NULL COMMENT 金额单位分, status VARCHAR(20) NOT NULL COMMENT CREATED/PAYING/SUCCESS/CLOSED/REFUNDING/REFUNDED, notify_url VARCHAR(256) NOT NULL COMMENT 异步通知地址, create_time DATETIME NOT NULL, pay_success_time DATETIME NULL, PRIMARY KEY (id), UNIQUE KEY uk_payment_order_no (payment_order_no), KEY idx_biz_order_no (biz_order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT支付订单表;支付平台的单号payment_order_no必须在创建时就保证唯一它是整个支付平台的根幂等键。业务方下两次单或者用户在同一订单上重新发起支付到这里都表现为不同支付单而不是互相覆盖。channel_code字段让后续退款、对账、回调处理都能直接判断走哪套逻辑而不是靠if (xxx ! null)去猜。交易流水表比支付订单表多记录渠道侧的信息channel_transaction_no微信的 transaction_id 或支付宝的 trade_no、channel_status、raw_notify_data。这张表是后面写对账功能的“本地底账”渠道侧的任何返回都必须先落到这里。2.3 状态机的迁移约束支付成功只能从 PAYING 迁入支付状态不能随便跳。比如一笔支付单处于CREATED此时用户还没扫码如果某个接口误操作把它置为SUCCESS资金和业务就全对不上了。状态迁移必须收敛到一个方法里做校验常见做法是定义一个枚举和一张“合法迁移表”。public enum PaymentStatus { CREATED, PAYING, SUCCESS, CLOSED, REFUNDING, PARTIAL_REFUNDED, REFUNDED; private static final MapPaymentStatus, SetPaymentStatus ALLOWED_TRANSITIONS new EnumMap(PaymentStatus.class); static { ALLOWED_TRANSITIONS.put(CREATED, Set.of(PAYING, CLOSED)); ALLOWED_TRANSITIONS.put(PAYING, Set.of(SUCCESS, CLOSED, PAYING)); ALLOWED_TRANSITIONS.put(SUCCESS, Set.of(REFUNDING, PARTIAL_REFUNDED, REFUNDED)); ALLOWED_TRANSITIONS.put(REFUNDING, Set.of(PARTIAL_REFUNDED, REFUNDED, REFUNDING)); ALLOWED_TRANSITIONS.put(PARTIAL_REFUNDED, Set.of(REFUNDING, REFUNDED)); } public boolean canTransitionTo(PaymentStatus target) { return ALLOWED_TRANSITIONS.getOrDefault(this, Set.of()).contains(target); } }PAYING状态允许原地停留是因为扫码支付场景下用户可能反复刷新支付状态查询动作返回的还是 PAYING这不算状态迁移。真正从PAYING到SUCCESS的迁移只允许发生在“支付回调验签通过之后”的处理方法里同步跳转、前端轮询都不能触发这个动作。数据库层还要加一道保险更新支付单时带上WHERE status PAYING影响行数为 0 说明已经被其他请求改过状态直接返回防止并发回调把同一笔单改两次。2.4 金额一律以“分”存储一张容易踩坑的精度换算表对接微信支付和支付宝支付时最容易出问题的就是金额单位。微信支付 API 的金额单位是分整数类型支付宝的total_amount是元字符串类型且保留两位小数。如果本地用double存支付金额就会出现 0.1 0.2 不等于 0.3 这类经典问题。端侧金额单位字段类型示例值本地存储建议微信支付 V3分inttotal: 100存 BIGINT单位分支付宝 page.pay元Stringtotal_amount: 1.00存 BIGINT单位分本地交易流水分BIGINT100所有计算用 BigInteger 或 BigDecimal前端展示元String1.00由分转换为字符串不用 float代码里换算时用BigDecimal而不是乘除double。从元转到分的标准写法是new BigDecimal(1.00).movePointRight(2).intValueExact()从分转成支付宝的元字符串是BigDecimal.valueOf(100L, 2).toPlainString()。注意支付宝的入参必须是字符串1.0和1.00在有些网关校验下会被拒绝统一用toPlainString生成两位小数。3. 微信支付 V3 与支付宝支付接口对接参数、验签与异步回调3.1 微信支付 V3先理解“V2 退、V3 进”再去填参数近几年的新项目基本都用微信支付 V3 接口老教程里那些V2的 MD5 签名和 32 位密钥回调解密在现代开发里已经不适合新接入。V3 的核心变化是三条请求方用商户私钥对请求头做 RSA-SHA256 签名回调响应用微信支付平台证书验签回调里的业务数据用 APIv3 密钥做 AES-256-GCM 解密。真正开发时不需要自己实现 HTTP 层签名常见做法是引入微信支付官方 Java SDK 或成熟封装但仍需理解商户私钥和平台证书的关系。在微信支付商户平台的“API 安全”里需要完成三项配置申请 API 证书生成商户私钥、设置 APIv3 密钥、下载平台证书。这三样对应代码里的merchant-private-key、api-v3-key、wechatpay-certificate缺一样后续下单或回调就会在验签环节报错。3.2 用 Java 构造一笔微信 JSAPI 下单JSAPI 下单接口是微信公众号和小程序内支付的统一入口接口路径为/v3/pay/transactions/jsapi。请求体中最关键的字段是appid、mchid、out_trade_no、amount.total、payer.openid。代码示意如下MapString, Object amount new HashMap(); amount.put(total, 100); // 金额单位分即 1 元 amount.put(currency, CNY); MapString, Object payer new HashMap(); payer.put(openid, oUpF8uMuAJ...); MapString, Object param new HashMap(); param.put(appid, wx1234567890); param.put(mchid, 1230000109); param.put(description, Java 支付平台测试商品); param.put(out_trade_no, PAY202501010001); param.put(notify_url, https://api.example.com/notify/wechat); param.put(amount, amount); param.put(payer, payer); String resp wechatPayClient.post(/v3/pay/transactions/jsapi, param); String prepayId JsonPath.read(resp, $.prepay_id);out_trade_no就是前面说的支付平台单号不需要把业务订单号直接暴露给渠道。notify_url必须是公网可访问的 HTTPS 地址并且在商户平台配置的支付授权目录下否则下单可能报错。拿到prepay_id后前端还需要用小程序端的wx.requestPayment或者公众号 JSSDK 的WeixinJSBridge.invoke拉起收银台这部分属于前端工作后端只需要把 prepay_id 返回给调用方。注意description不能太长微信支付对商品描述有 127 字节的限制建议只传订单标题或商品简称不要拼接全部商品明细。3.3 电脑网站支付与手机网站支付的选型支付宝的 alipay.trade.page.pay支付宝侧的接入比微信直观因为网页支付场景下有明确的接口区分。桌面浏览器用alipay.trade.page.pay电脑网站支付手机 H5 浏览器用alipay.trade.wap.pay小程序则走小程序支付 SDK。它们的前置条件一致应用公钥与支付宝公钥配对接口用 RSA2 签名。下面以电脑网站支付为例AlipayClient client new DefaultAlipayClient( https://openapi.alipay.com/gateway.do, appId, merchantPrivateKey, AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8, alipayPublicKey, AlipayConstants.SIGN_TYPE_RSA2); AlipayTradePagePayRequest request new AlipayTradePagePayRequest(); request.setNotifyUrl(https://api.example.com/notify/alipay); request.setReturnUrl(https://mall.example.com/order/result); request.setBizContent({\out_trade_no\:\PAY202501010001\, \product_code\:\FAST_INSTANT_TRADE_PAY\, \total_amount\:\1.00\, \subject\:\Java 支付平台测试商品\}); String form client.pageExecute(request).getBody(); // 将 form 表单字符串直接返回给前端页面渲染即可total_amount是字符串形式的元代码里必须由“分”安全转换出来。out_trade_no同样使用支付平台单号。支付宝的notify_url异步通知和return_url同步跳转是两个完全不同的回跳同步跳转只是“用户付完钱被浏览器带回来”它不能作为支付成功的依据真正的到账凭证是支付宝异步通知。如果项目要支持手机浏览器把AlipayTradePagePayRequest换成AlipayTradeWapPayRequestproduct_code换成QUICK_WAP_WAY其余参数含义一致。这层用策略模式包一层AlipayPayStrategy和WechatPayStrategy可以让上面的业务代码完全不感知渠道差异。3.4 异步回调验签微信支付投诉回调与支付宝 RSA2 的落脚点支付回调是整条链路的安全命门。微信支付和支付宝都会往notify_url发送异步通知通知可能到达多次且顺序不保证。微信侧还有个容易忽略的点如果在商户平台配置了“投诉回调”还会额外发送投诉变更通知这两类回调的报文结构完全不同处理时先按消息类型分流再走各自的验签逻辑。微信 V3 回调的大致流程是先取请求头里的Wechatpay-Signature配合微信支付平台证书验签验签通过后取出 body 中resource节点的ciphertext用 APIv3 密钥做 AES-256-GCM 解密解密后的 JSON 里包含out_trade_no、transaction_id、trade_state和amount。验签失败的请求直接返回 401 或 400不能进入业务处理。public String decryptWechatNotify(String ciphertext, String nonce, String associatedData) { // aad 对应 resource.associated_datanonce 对应 resource.nonce // 使用 APIv3 密钥调用 AES/GCM/NoPadding 解密得到明文 JSON } public boolean verifyWechatSignature(HttpServletRequest request, String body) { String signature request.getHeader(Wechatpay-Signature); String timestamp request.getHeader(Wechatpay-Timestamp); String nonce request.getHeader(Wechatpay-Nonce); String serial request.getHeader(Wechatpay-Serial); // 用 serial 找到对应微信支付平台证书对 timestamp \n nonce \n body 做 SHA256withRSA 验签 // 还要检查 timestamp 与当前时间差避免重放攻击 return true; }支付宝的验签路径更简单直接调用 SDK 的AlipaySignature.rsaCheckV1(params, alipayPublicKey, UTF-8, RSA2)验签通过后判断trade_status。WAIT_BUYER_PAY表示未付TRADE_SUCCESS和TRADE_FINISHED才代表支付成功。注意支付宝对TRADE_SUCCESS通知的成功响应是纯文本success不是 JSON很多初学着在这里踩坑。回调处理还有一个通用的幂等要求不管收到几次回调同一个支付平台单号只能触发一次成功变更。实现上用数据库唯一索引或者先查后更都可以我一般会在更新语句里加status条件并在事务内先按payment_order_no查询当前状态已经SUCCESS就直接返回成功应答不再重复修改。3.5 一个必踩的坑金额单位、订单号长度与 notify_url把这三个最常出问题的点单独列出来它们分别是金额单位错了会直接从 1 分变成 1 元订单号长度超限会导致下单接口返回参数错误notify_url 不可达会导致回调被渠道侧重试到通知超时。微信支付的商户订单号限制是 32 个字符以内且只能包含字母、数字、中划线或下划线。自己生成的payment_order_no建议控制在 30 位以内给业务方前缀留足空间。支付宝这边的out_trade_no同样有 64 字符限制但绝对不要在中间拼时间戳加随机数加用户 ID 加渠道代码那会让排查问题的人直接崩溃。生成规则用“短业务前缀 yyyyMMddHHmmss 6 位随机数”就够用了。notify_url是另一个隐蔽坑微信支付要求必须 HTTPS并且回调服务器不能有跳转否则微信会认为通知失败并按照 15 秒、15 秒、30 秒、3 分钟、10 分钟、20 分钟的节奏持续重试最大重试次数达到十几次横跨数小时。排查时如果发现某个测试订单一天后才变成已支付基本就是回调地址超时或者响应体格式不对导致的连续重试。4. 拿到源码之后先跑通再看懂再按上线标准重构4.1 四步定位核心链路比逐行读代码效率高标题里带“源码”但这类项目代码量通常不小直接从头读容易迷失。我自己的顺序是先看pom.xml里有没有引入微信支付和支付宝的 SDK确认技术栈再看application.yml里的支付配置项有哪些占位符能判断作者是把配置写死在代码里还是做成查表的然后找带notify或callback路径的 Controller那是支付平台的咽喉最后顺着创建支付单的 Service 方法找渠道分发逻辑确认微信和支付宝是各自一套实现还是共用一套模板方法。跑本地环境时微信支付需要真实的商户号和回调公网地址没有真商户号的场景下优先用支付宝沙箱验证链路。支付宝开放平台后台提供沙箱环境应用 ID、支付宝网关地址、沙箱账号都是现成了把openapi.alipay.com改成沙箱网关地址再用沙箱工具扫码进去就能完整走通下单、回调、退款三个动作。微信支付没有公开的独立沙箱环境做演示时可以停留在“下单成功拿到 prepay_id”这一步前端拉起收银台的真实验证需要测试商户号。4.2 毕业设计源码里最常见的三个安全缺口评审老师一般不会一句句看代码但会重点问三个点回调有没有验签、退款有没有幂等、商户密钥有没有泄漏到前端。对照下面的表格可以快速自查检查点常见偷懒写法上线标准回调验签直接解析回调参数改订单状态先验签、校验金额、再查流水状态修改订单状态直接用渠道订单号更新业务表只更新 PAYING 到 SUCCESS且限制行数密钥配置私钥字符串写在 application.yml使用环境变量或配置中心私钥文件权限 600额外提醒一个很容易被忽略的地方不要把微信支付商户号或者支付宝 APPID 当成“死代码里的常量”写死在前端页面。APPID 本身不是机密但配合商户号可以做钓鱼页面扣掉这个分在答辩时非常可惜。4.3 最小验证动作一次性跑通“下单 → 回调 → 重复回调”最后给一个能直接复现的验证思路花十分钟就能验证整套状态机是否可靠。第一步在支付宝沙箱中发起一笔 0.01 元的电脑网站支付本地库中生成一条PAYING的流水第二步用沙箱买家账号完成支付观察支付宝异步通知是否打到本地notify接口确认流水状态变成SUCCESS第三步从 notify 接口的日志里把上次请求的完整参数复制下来手动重放一次观察支付单的pay_success_time没有变化、业务侧没有多产生一条发货记录。这个动作把“异步回调处理”和“接口幂等”两个核心知识点合在一个实验里验证了。把第二次回调重放后订单状态仍是SUCCESS且update_time不变说明幂等生效——这个结果比在答辩 PPT 里画十页架构图更能说明你真正理解了支付平台的处理逻辑。本文还有配套的精品资源点击获取
返回列表