简介这份资源是一套可运行的 Java 版微信 JSAPI 支付实现包面向需要在网页端或公众号内接入微信支付的后端开发者、课程设计者及中小项目团队用于解决统一下单、支付签名、发起支付请求、上报支付结果与配置管理等基础环节的落地问题放入自己的商户号、证书与回调地址等参数后即可联调适合已经掌握 Java Web 基础、希望快速熟悉支付对接流程的中级开发者。压缩包共 111 个文件约 18.29MB以 44 个 jar 依赖包、19 个 class 编译产物、16 个 xml 配置与映射文件、11 个 java 源码以及少量 properties 配置、jsp 页面与 js 脚本为主既有可直接引用的支付工具类也保留了源码便于按需阅读和二次修改。已有 4243 人学习下载。借助其中的支付请求封装、签名与校验工具、结果上报实现和配置示例读者能较快理解 JSAPI 支付的调用链路与参数组织方式减少自行从零封装时在签名和配置上反复试错的成本。1. 装 JSAPI 支付前先认清 wechatpad.zip 到底替你把哪些活干了上周帮人看一台新机器上的支付故障商户号、密钥、证书一个字符没动前端拉起 JSAPI 支付直接弹「签名错误」。折腾两个小时才发现机器上的 Node 从 16 换到 20运行时的 OpenSSL 也跟着换了代签名串里一个不可见字符的处理行为变了商户参数全对签名就是过不去。这种坑不装一遍根本想不到。JSAPI 支付在公众号、小程序场景里几乎是标配接的人多装翻的也多。wechatpad.zip 这套包把签名组装、参数校验、回调验签这几段最容易写错的部分收拢到了一起但收拢不等于免配运行时版本、证书路径、授权目录、回调地址仍然要一项项落地。装不好就是白屏、签名失败、回调收不到三连。这篇写给正在接支付的前后端和运维从版本选型一路走到装完能跑通。2. 环境与版本清单JSAPI 支付依赖的运行时到底怎么选动手解压之前先把运行时定下来这一步定错后面所有排查都是白费。JSAPI 支付这条链上真正吃版本的只有三个环节后端运行时的加密库、HTTP 客户端的 TLS 能力、以及处理证书文件的 OpenSSL。很多人只盯着语言版本忽略加密库的代际差异最后卡在「本地能签、线上签不过」。2.1 运行时版本对照与取舍我把这几年实际跑过的组合整理成一张表按「稳」到「新」排。表里的版本号是运行时本身的通用版本不是这个包绑定的版本包内实际依赖以解压后的依赖清单为准。运行时推荐版本加密库适配情况备注Node.js18 LTSOpenSSL 3.x稳长期支持生态兼容性最好Node.js20 LTSOpenSSL 3.x稳新项目首选注意旧算法默认禁用Node.js16OpenSSL 1.1.1可用但不建议已停止维护新机器别再用PHP8.1 / 8.2OpenSSL 3.x稳扩展需开 openssl、curl、jsonPHP7.4OpenSSL 1.1.1勉强可用部分新语法不兼容谨慎Java17自带稳JDK 8 也能跑但 TLS 默认版本偏低我一般会优先选 Node 18 LTS。原因不是它新而是它的生命周期够长装一次能躺两年不用动。选 Node 20 也没问题但要有心理准备OpenSSL 3 默认把一批老算法挪进了 legacy provider如果你的证书是老格式或者商户侧还在用偏旧的签名方式会直接抛「unsupported」类错误看起来像证书坏了其实是算法被禁用。PHP 这条线要额外注意扩展。装完 PHP 先跑一条检查缺扩展的话后面调签名函数会直接 fatal error# 检查 JSAPI 支付必需的三个扩展是否加载 php -m | grep -E ^(openssl|curl|json)$ # 输出应为三行缺哪个装哪个以 Debian 系为例 # apt install php-openssl php-curl php-json这条命令的逻辑很简单php -m列出所有已加载模块再用 grep 精确匹配三个名字。用^(...)$锚定首尾是为了避免匹配到openssl_x之类的衍生名造成误判。缺扩展时页面通常报「Call to undefined function」但错误可能被框架的异常处理器吞掉看起来就是白屏所以这一步提前做掉。2.2 解压之前先核对文件结构拿到包别急着 unzip先列一遍清单确认里面有配置模板、证书存放目录、依赖描述文件和启动入口。这一步的意义在于如果包里带的是模板配置而不是示例配置你直接解压覆盖就会把别人的占位参数带进生产。# 只看清单不解压确认结构再动手 unzip -l wechatpad.zip # 输出关注三类 # 1) 配置模板文件通常带 example / sample / template 后缀 # 2) 证书目录存放 apiclient_cert / apiclient_key 的地方 # 3) 依赖描述文件package.json / composer.json / pom.xmlunzip -l只读中央目录不改动磁盘是安全的。如果清单里出现.env这类实际配置文件而不是.env.example说明打包的人把自己的环境一起打进去了这种包要格外小心解压后第一件事是重命名而不是直接用。2.3 支付前置条件六样东西缺一不可环境装好只是「能跑」能不能「跑通支付」取决于下面六项是否到位。我把它们和缺失后的典型现象放在一起方便你对照排查。前置项从哪里获取缺失后的现象公众号或小程序 AppID后台账号信息拉起时报「appid 不合法」商户号 mchid商户平台下单报「商户号不存在」APIv3 密钥商户平台自行设置回调验签失败密文解不开商户 API 证书与私钥商户平台下载下单报证书读取失败平台证书通过接口下载或随包附带回调验签拿不到公钥支付授权目录 / JS 接口安全域名商户平台与公众号后台分别配置前端报「当前页面 URL 未注册」常见做法是先把这六项在纸面上列全再去改配置文件。我见过太多先把配置改完再回头找参数的改到一半发现平台证书还没下配置里那个路径一直是空的启动日志里一条 warning 都没有等到真下单才炸。2.4 服务器时间与字符集两个看不见的变量签名和时间戳强相关服务器时间偏超过五分钟商户参数再对也过不了。装完系统第一件事应该是校时# 查看当前时间与时间同步状态 timedatectl status # 若 NTP 未启用打开并立即同步 timedatectl set-ntp truetimedatectl status会输出System clock synchronized: yes才算合格。容器环境要特别注意宿主机时间对了容器里如果没挂载时区文件跑出来仍是 UTC日志时间对不上排查时会误判成「回调延迟」。字符集这条更隐蔽。配置文件必须是 UTF-8 且不带 BOM带 BOM 的文件在多数运行时里读出来头部会多三个字节拼进签名串就变成一个永远对不上的差异。用一条命令确认# 检查配置与证书相关文本文件是否带 BOM输出非空即有 BOM file config/*.json | grep -i bom正常应该没有任何输出。有输出就说明文件被某个编辑器改过用sed -i 1s/^\xEF\xBB\xBF//去掉再存。3. 解压与初始化wechatpad.zip 落地到项目目录的完整步骤前面把版本和前置条件定下来了这一章开始真正往磁盘上落。顺序我坚持三步走先摆目录再装依赖最后生成配置。顺序颠倒的后果是依赖装进了错误路径配置模板复制到了源码目录回头清理比重装还费劲。3.1 解压位置与目录规划不要解压到项目根目录也不要解压到 Web 根目录。我一般统一放/opt/app/下面按应用名建一级目录权限收给专用账号# 1) 建目标目录属主交给运行账号假设运行账号为 deploy sudo mkdir -p /opt/app/wechatpad sudo chown -R deploy:deploy /opt/app/wechatpad # 2) 解压到目标目录-q 静默、-o 覆盖不询问 sudo -u deploy unzip -q -o wechatpad.zip -d /opt/app/wechatpad # 3) 确认解压结果与顶层结构 ls -la /opt/app/wechatpad-d指定目标目录-o表示覆盖已有文件不提示适合自动化脚本手工操作建议去掉-o让它在冲突时停下来问一句避免把已有配置覆盖掉。sudo -u deploy是为了让解压出来的文件属主直接就是运行账号省掉后面一次 chown。关键点在于整个目录树里只有public或web这类子目录可以被 Web 服务器指向配置文件和证书目录必须在 Web 根之外。目录摆错证书就等于公开可下载这是一类被反复提及但每年都有人犯的错误。3.2 依赖安装锁文件优先别用 install依赖安装这一步Node 和 PHP 两条线的做法不一样但原则一致——以锁文件为准不要让它重新解析版本范围。# Node 线ci 严格按 lock 文件安装装不上就失败不会偷偷升级 cd /opt/app/wechatpad npm ci --omitdev # 需要构建产物时再补一次完整安装 # npm ci npm run buildnpm ci和npm install的差别值得说清楚。前者会先删掉node_modules再按 lock 文件精确安装任何对不上就报错退出后者会尝试解析 semver 范围可能装出一个 lock 文件里没有的版本。支付这类对加密库版本敏感的场景我坚持用ci宁可装不上也不要装出一个「看起来能跑」的组合。PHP 线同理# 生产依赖跳过开发包 composer install --no-dev --optimize-autoloader # 确认关键扩展在运行时可用 php -r var_dump(extension_loaded(openssl), extension_loaded(curl));--no-dev跳过测试与调试依赖--optimize-autoloader生成类映射表减少运行时的文件查找开销。第二行那个php -r是兜底检查命令行环境有扩展不代表 FPM 环境也有两者用的php.ini可能不是同一份这一点在 Debian 系上尤其常见。Java 线用 Maven 的话把依赖下载与打包分开避免每次改配置都重新拉一遍仓库# 先离线拉齐依赖 mvn -q dependency:go-offline # 再打包跳过测试 mvn -q clean package -DskipTests3.3 配置生成与首次启动配置永远从模板复制不要手写空文件。模板里通常带齐了所有键手写容易漏漏掉的键在很多实现里不会报错只会取默认值然后行为诡异。# 1) 从模板复制保留模板不动 cd /opt/app/wechatpad cp config/config.example.json config/config.json # 2) 收紧权限配置里有密钥不能给别人读 chmod 600 config/config.json chmod 700 certs/ # 3) 首次启动前台跑方便看启动日志 node app.js第二步的权限设置是必做项。600表示只有属主可读写700表示证书目录只有属主能进。如果运行账号和部署账号不是同一个用chown一并调整否则会出现「明明文件在就是读不到」的经典现象。启动后不要急着测支付先访问健康检查端点确认进程活着、配置加载成功# 健康检查返回 200 且带状态字段说明启动正常 curl -s -o /dev/null -w %{http_code}\n http://127.0.0.1:8080/health # 看启动日志里配置加载的那几行 tail -n 50 logs/app.log | grep -iE config|loaded|cert-o /dev/null丢弃响应体只看状态码-w输出格式化的结果。健康检查通过只代表进程活着日志里那几行才是关键证书路径有没有被解析到、APIv3 密钥长度对不对、回调地址是不是空的这些在启动阶段就该暴露出来不要留到下单时才发现。4. 配置参数逐项拆解JSAPI 支付的签名、回调与证书路径配置文件里字段不多但每一个都有明确的来源和明确的错法。这一章按「参数从哪来、写错了什么现象、怎么改对」逐项过一遍重点是签名方式和证书路径这两块它们占了实际故障的大头。4.1 核心参数对照配置键含义来源典型错法appid应用唯一标识公众号或小程序后台与 mchid 不匹配跨主体绑定mchid商户号商户平台抄成了子商户号serial_no商户证书序列号商户平台证书详情与私钥不配套private_key_path商户私钥路径下载的证书包指向了证书而非私钥api_v3_keyAPIv3 密钥商户平台设置长度不足 32 位notify_url支付结果回调地址自行部署内网地址或带查询串platform_cert_path平台证书路径接口下载过期未更新serial_no和私钥必须配套这一点最容易忽略。重新在商户平台申请过证书的人如果只换了私钥没换序列号签名时会报「证书序列号不存在」报错信息指向证书实际是配置里的号对不上。4.2 签名方式的取舍APIv2 与 APIv3 不要混用APIv2 用 MD5 或 HMAC-SHA256 拼串签名APIv3 用 SHA256-RSA 对请求报文签名。两套并存但同一次调用只能走一套。混用的典型症状是下单成功了、回调验签失败了因为下单走的 v3、回调按 v2 的逻辑验密钥和算法都对不上。// APIv3 请求签名的最小实现注意签名串的构造顺序 const crypto require(crypto); function buildSignature(method, urlPath, timestamp, nonce, body, privateKey) { // 1) 签名串四行加一个换行结尾顺序固定不可调换 const message ${method}\n${urlPath}\n${timestamp}\n${nonce}\n${body}\n; // 2) SHA256-RSA密钥来自 apiclient_key.pem return crypto.createSign(RSA-SHA256).update(message).sign(privateKey, base64); }这段代码的逻辑签名串由请求方法、URL 路径、时间戳、随机串、请求体五段拼成每段之间用\n分隔最后一段后面也要有一个\n。最后那个换行是最常被 trim 掉的地方去掉就永远签不过。urlPath只取路径部分不带域名和查询串带了也会失败。私钥读取要做一次格式校验PEM 头尾必须在# 确认私钥文件格式正确 head -n 1 certs/apiclient_key.pem # 期望输出-----BEGIN PRIVATE KEY----- # 用 openssl 验证私钥与证书是否配套两者模数应一致 openssl x509 -noout -modulus -in certs/apiclient_cert.pem | openssl md5 openssl rsa -noout -modulus -in certs/apiclient_key.pem | openssl md5两条命令输出的 md5 相同说明证书和私钥是配套的。不相同就说明下载时配错了对这在批量申请证书的场景里很常见。4.3 回调地址与授权目录的写法回调地址必须是公网可达的 HTTPS 地址不能带查询参数不能是 IP 加端口这种非标组合——多数实现会直接拒绝。本地开发时回调打不进来常见做法是准备一台有公网地址的测试机做落点把回调日志打到那台机器上不要试图在本地回环地址上收回调。{ notify_url: https://pay-test.example.com/api/pay/notify, auth_dir: https://h5.example.com/pay/ }两个字段的写法要点notify_url路径要精确到处理函数不要指向首页再靠路由分发多一层分发就多一次出错机会auth_dir必须以斜杠结尾表示这是一个目录而不是一个页面少了斜杠同目录下的子页面就匹配不上前端报「当前页面 URL 未注册」而后台配置看起来完全正常。授权目录还要注意协议和端口。配置里写的是 HTTPS前端页面用 HTTP 打开一样不匹配配置里没写端口前端页面跑在 8080 上也不匹配。这条规则严格到有点不近人情但确实如此。4.4 证书路径在 Linux 与 Windows 下的差异Linux 用正斜杠Windows 用反斜杠写在 JSON 里反斜杠还要转义成双反斜杠这是最琐碎的一处。我一般统一在配置里写正斜杠Windows 下的运行时基本都能识别省掉转义的心智负担。{ private_key_path: certs/apiclient_key.pem, platform_cert_path: certs/platform_cert.pem }路径统一用相对路径相对于应用根目录解析不要写绝对路径。绝对路径在容器里换个挂载点就失效相对路径至少还能靠工作目录兜住。真正要写绝对路径时用环境变量注进去别硬编码在配置文件里。5. 排查与避坑JSAPI 支付装完之后最常见的五个翻车点装完能启动和装完能收款中间隔着一段距离。这一章把这几年反复遇到的五个坑按「现象、原因、解决」写下来顺序按出现频率排。5.1 五条踩坑记录第一条签名错误参数核对三遍全对现象是拉起支付时报签名错误把 appid、mchid、时间戳、随机串逐个比对一个字符不差。原因是签名串末尾那个换行在序列化时被去掉了或者配置文件的 BOM 混进了首字段。解决方式是打印出参与签名的原始串用十六进制看一眼首尾字节# 把签名串写入文件后看首尾字节确认有无 BOM 与多余空白 xxd -l 16 /tmp/sign_input.txt xxd -s -16 /tmp/sign_input.txt首部出现efbb bf就是 BOM尾部不是0a就说明换行丢了。两者都清掉再签。第二条回调一直被重复推送业务侧却显示已处理现象是订单状态已更新但平台侧持续重推同一条回调隔几分钟来一次。原因是处理函数没有返回约定的成功应答或者业务处理耗时超过了平台等待窗口。常见做法是把「验签 落库」做成快速路径把「发货、通知」这类动作丢进队列异步做先回成功应答再说。// 回调处理的最小骨架先验签再落库最后立即应答 app.post(/api/pay/notify, async (req, res) { const ok verifySignature(req); // 验签失败直接拒绝不落库 if (!ok) return res.status(401).send(fail); await saveOnce(req.body); // 幂等写入重复推送不重复处理 res.status(200).send(success); // 应答必须同步返回 });saveOnce要做幂等用订单号做唯一键重复推送时命中唯一约束直接跳过不要抛异常。应答必须是同步返回的状态码加固定文本异步回调和自定义文本都可能被判为失败。第三条授权目录配了前端还是报 URL 未注册现象是后台配置里明明有这条域名前端仍然报错。原因集中在三处目录末尾少了斜杠、配置的是 HTTP 而页面跑在 HTTPS、或者页面在带端口的地址上打开。解决方式是拿前端实际的location.href和配置值逐字符对齐包括协议、域名、端口、路径四段一段都不能差。第四条证书读取失败权限看着没问题现象是日志报 permission deniedls -l看文件是 644理论上可读。原因是上级目录缺执行权限进程进不去那个目录自然读不到文件。另一种可能是 SELinux 或 AppArmor 拦住了非标准路径。解决方式是逐级检查目录权限# 逐级检查证书所在路径每一层的权限 namei -l /opt/app/wechatpad/certs/apiclient_key.pemnamei -l会把路径上每一层目录的权限都列出来哪一层缺x一眼就能看到。常见的修法是给目录补chmod 750而不是把文件改成 777。第五条测试环境能付生产环境不行现象是同一份代码测试跑得好好的切到生产就报商户号不存在或者签名失败。原因是两个环境用了不同的 appid / mchid 组合或者配置文件被测试环境的覆盖了。解决方式是在启动日志里把 appid 和 mchid 的后四位打出来做交叉确认生产与测试的配置目录彻底分开不要共用同一份config.json。5.2 日志该看哪几行排查支付问题日志比调试器好使。启动阶段看三行配置加载结果、证书路径解析结果、回调地址是否为空。运行阶段看两行签名前的原始串摘要、回调验签的布尔结果。这两行不要打全量明文打摘要和长度就够避免密钥进日志。# 按关键字过滤出与支付相关的日志行 grep -iE sign|notify|cert|mchid logs/app.log | tail -n 100日志级别调到 debug 只在一段时间内有效排查完立刻调回否则日志量会把磁盘写满这件事我吃过一次亏。6. 装完怎么验证一条从下单到回调落库的完整链路验证不要只看「支付页面能弹出来」弹出来只是第一步。完整的验证链路是下单接口返回预支付参数、前端用参数拉起、支付完成后回调到达、回调验签通过、订单状态落库。五步里任何一步断了都不能算装完。先在下单接口上打一发确认返回结构里带齐了appId、timeStamp、nonceStr、package、signType、paySign六个字段# 调本地下单接口检查返回的预支付参数是否齐全 curl -s -X POST http://127.0.0.1:8080/api/pay/create \ -H Content-Type: application/json \ -d {orderNo:TEST20240101001,amount:1,openid:测试用户openid} \ | python3 -m json.toolpython3 -m json.tool只是把返回体格式化方便肉眼核对字段。重点看两处paySign不为空且长度合理package是以prepay_id开头的串。缺字段说明后端组装那步没走通先回去看日志不要在前端瞎调。再模拟一次回调绕过真实支付直接验证验签与落库逻辑# 用测试工具向回调地址发一条构造报文验证验签与幂等落库 curl -s -X POST https://pay-test.example.com/api/pay/notify \ -H Content-Type: application/json \ -H Wechatpay-Signature: 构造的签名 \ -d notify_sample.json # 连续发两次检查订单表是否只有一条记录验证幂等第二条的意义在于验证幂等。第一次应当返回成功并写入订单第二次应当同样返回成功但不产生重复记录。如果第二次报唯一键冲突并返回 500说明幂等逻辑写成了「靠异常兜底」平台会一直重推永远收不到成功应答。把五步的期望结果和失败时该先看哪里列成一张表验证时照着走验证项期望结果失败先看健康检查200配置加载成功启动日志的 config 行下单接口返回六个预支付字段证书路径、mchid前端拉起出现支付面板授权目录、当前页面 URL回调到达日志出现 notify 记录回调地址是否公网可达回调落库订单表一条记录重复推送不新增幂等键与应答文本这套流程我从那次 Node 版本升级导致的签名故障之后就没断过每次重装环境哪怕只是换台机器也强制把五步走完再交付。跳过任何一步后面都会以更难查的形式回来找你。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?