Skip to content

支付宝:电脑网站支付接入指南 ​

本文面向自行运营网站的普通商户,介绍从申请产品到使用 tio-boot 后端接入支付宝收款的过程。用户在网站选择商品,后端创建订单并生成支付宝收银台链接,用户跳转后付款,后端通过异步通知和主动查单确认到账。

本文采用官方 Java SDK 的 v2 接口和 RSA2 公钥模式,调用 alipay.trade.page.pay。电脑网站支付、手机网站支付和 App 支付是不同产品,不能用其中一个产品的申请结果代表其他产品也已开通。

所有商户资料、AppID、账号、域名和订单号均应替换为自己的信息。本文仅使用占位符和 example.com 示例域名,不包含真实企业资料、手机号、验证码或密钥。

1. 先准备哪些材料 ​

准备项说明
企业支付宝账号完成对应主体的实名认证,使用本次实际收款的企业账号登录
营业执照信息公司名称、经营范围、法定代表人等,用于核对主体和经营类目;已认证账号可能无需重复上传
网站地址及备案信息网站应能正常访问,并展示实际商品或服务、价格和购买流程;备案主体与收款主体应核对一致
联系人提供真实姓名和能接收短信的手机号,由本人完成验证
应用名称、图标、简介使用实际产品名称及图标,简介如实描述服务内容与交付方式
特殊材料如果使用其他主体的网站,或经营类目要求资质,按页面要求提供授权函、许可证等材料

未上线网站是否可以用首页、商品页和支付页截图申请,以当前申请页面为准。选择类目要结合营业执照和实际业务,不能只挑容易通过审核的类目。

开通费、交易手续费和可能适用的保证金是不同事项。不要把其他平台的认证费直接套用到支付宝。申请时逐项查看当前产品页面和签约合同中的费用说明;示例中若按 0.6% 计算,收款 100 元对应手续费 0.60 元,这只是计算示例,不是对所有商户的费率承诺。

2. 在商家平台申请支付产品 ​

  1. 登录 支付宝商家平台,确认当前企业名称和收款账号。
  2. 在产品中心找到「电脑网站支付」,进入申请流程。
  3. 如实选择经营类目,填写网站地址和联系人;按页面要求补充材料。
  4. 核对收款主体、网站、费率及协议,由联系人完成短信验证后提交。
  5. 查看申请结果。「已提交,正在审核中」只代表受理,须等到产品签约生效。

页面提示的预计审核时间不是批准承诺。若被退回,按原因修改材料,先查看已有申请,避免重复申请。短信验证码有时效且通常只能使用一次,优先由本人在官方页面填写,不应写入教程、截图说明或项目日志。

3. 在开放平台创建网页应用 ​

登录 支付宝开放平台,创建「网页应用」,填写名称、图标、简介和官网,并绑定本次收款的企业账号。首次使用平台可能要求验证联系人信息。

应用类型和绑定主体可能在创建后不能修改,应在提交前核对。上传图标后可能还会出现裁剪确认弹窗,需要确认后才算完成上传。

创建完成后保存 AppID。在应用的「开发设置」配置接口加签方式,再从应用详情提交上线审核。产品签约审核与应用上线审核是两个流程;应用创建成功或密钥保存成功,都不等于可以正式收款。还应在「可调用产品」确认对应支付能力可用。

4. 配置 RSA2 密钥 ​

三个名称要分清:

名称用途保存位置
应用私钥后端给支付请求签名仅保存在自己的服务端或密钥管理系统
应用公钥支付宝验证商户请求上传至该应用的接口加签配置
支付宝公钥商户验证支付宝通知和接口响应从该应用的配置页面获取,供后端使用

本文选择「密钥」加签方式,算法使用 RSA2。若使用要求证书模式的其他产品,需另行按证书模式集成,不能直接照搬本文配置。

可使用支付宝官方密钥工具生成密钥,也可在安装了 OpenSSL 的电脑上执行:

shell
mkdir -p certs/alipay
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out certs/alipay/app-private-key.pem
openssl pkey -in certs/alipay/app-private-key.pem -pubout -out certs/alipay/app-public-key.pem

Windows 可先手动创建目录再执行两个 OpenSSL 命令。执行前确认目标文件不存在,不要覆盖已经绑定应用的私钥。上述私钥为 PKCS#8 格式;在平台粘贴应用公钥时,按页面要求使用去掉 PEM 头尾及换行的 Base64 内容。

保存公钥时可能再次要求主账号短信验证。看到「密钥保存成功」后,获取支付宝公钥,保存为 certs/alipay/alipay-public-key.pem。不要把应用公钥错当成支付宝公钥。

将以下路径加入 .gitignore,并限制运行账号之外的读取权限:

gitignore
certs/alipay/
secrets.txt
.env

.gitignore 不能清除已经提交过的文件。如果私钥已经泄漏,应更换密钥并更新平台配置,不能仅删除文件后继续使用。

5. tio-boot 配置与 SDK ​

使用官方 com.alipay.sdk:alipay-sdk-java 依赖,在项目的依赖管理中选择经过验证的版本。SDK 用法参见 官方 Java SDK 文档。不要自行拼接签名算法或从非官方网页复制密钥。

配置示例:

properties
ALIPAY_ENABLED=false
ALIPAY_APP_ID=YOUR_APP_ID
ALIPAY_SELLER_ID=YOUR_SELLER_ID
ALIPAY_PRIVATE_KEY_PATH=certs/alipay/app-private-key.pem
ALIPAY_PUBLIC_KEY_PATH=certs/alipay/alipay-public-key.pem
ALIPAY_NOTIFY_URL=https://api.example.com/api/pay/alipay/notify
ALIPAY_RETURN_URL=https://www.example.com/pay/result

ALIPAY_SELLER_ID 是实际收款支付宝账号的用户标识(PID),不是 AppID,也不是登录邮箱。应从官方后台核对。配置文件需要被项目启动流程实际加载;仅把文件放进目录不会自动生效。相对密钥路径以服务进程工作目录为基准。

以下是配置和客户端的核心示例,异常由应用启动或接口层处理:

java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import nexus.io.tio.utils.environment.EnvUtils;

public final class AlipayClients {
  public static String required(String key) {
    String value = EnvUtils.get(key);
    if (value == null || value.isBlank()) {
      throw new IllegalStateException("Missing configuration: " + key);
    }
    return value.trim();
  }

  public static String readKey(String pathKey) throws Exception {
    return Files.readString(Path.of(required(pathKey)), StandardCharsets.UTF_8)
        .replaceAll("-----BEGIN [^-]+-----", "")
        .replaceAll("-----END [^-]+-----", "")
        .replaceAll("\\s", "");
  }

  public static AlipayClient create() throws Exception {
    return new DefaultAlipayClient(
        "https://openapi.alipay.com/gateway.do",
        required("ALIPAY_APP_ID"), readKey("ALIPAY_PRIVATE_KEY_PATH"),
        "json", "UTF-8", readKey("ALIPAY_PUBLIC_KEY_PATH"), "RSA2");
  }
}

客户端和密钥可在服务启动时初始化并复用。关闭 ALIPAY_ENABLED 时停止创建新支付订单,但仍需处理已经发起的订单通知。沙箱必须使用沙箱应用、账号和密钥,网关以官方沙箱控制台为准,不能仅改网关就复用正式环境资料。

6. 创建订单并返回收银台链接 ​

下单接口必须要求登录,由服务端根据商品或充值规则确定价格。不要相信前端传来的收款账号、积分数量、商品描述或回调地址。

先保存本地待支付订单,再生成支付链接。订单至少记录用户、支付渠道、应用、收款账号、金额(整数分)、状态、过期时间和支付宝交易号。商户订单号需要唯一,可使用雪花 ID:

java
String orderNo = "ALI" + nexus.io.tio.utils.snowflake.SnowflakeIdUtils.id();

下面只展示生成链接部分;其中的金额和订单号必须来自已经持久化的服务端订单:

java
import java.math.BigDecimal;
import com.alipay.api.AlipayClient;
import com.alipay.api.domain.AlipayTradePagePayModel;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;

public String checkoutUrl(AlipayClient client, String orderNo,
    long amountFen) throws Exception {
  if (amountFen <= 0) {
    throw new IllegalArgumentException("Invalid amount");
  }
  AlipayTradePagePayModel model = new AlipayTradePagePayModel();
  model.setOutTradeNo(orderNo);
  model.setTotalAmount(BigDecimal.valueOf(amountFen, 2).toPlainString());
  model.setSubject("网站服务订单");
  model.setProductCode("FAST_INSTANT_TRADE_PAY");
  model.setTimeoutExpress("15m");
  AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
  request.setBizModel(model);
  request.setNotifyUrl(AlipayClients.required("ALIPAY_NOTIFY_URL"));
  request.setReturnUrl(AlipayClients.required("ALIPAY_RETURN_URL"));
  AlipayTradePagePayResponse response = client.pageExecute(request, "GET");
  return response.getBody();
}

返回给前端订单号和 pay_url,前端通过链接进入收银台。pageExecute 生成签名链接,不表示订单已经付款,也不能用它证明产品签约已生效。复用订单时,应保持原订单的过期时间,不要每次生成链接都延长支付期限。

页面回跳只用于导航。用户访问成功页、关闭窗口或点击“我已支付”,都不能直接发放商品或积分。

6.1 另一种收款形态:二维码支付(当面付) ​

本文主流程用的是「电脑网站支付」,用户从浏览器跳到支付宝收银台完成付款。如果希望像扫码支付那样在当前页面弹出二维码、用户用支付宝 App 扫码付款,需要的是另一个产品 「当面付」的预下单接口 alipay.trade.precreate:

电脑网站支付当面付(预下单)
接口alipay.trade.page.payalipay.trade.precreate
返回签名后的收银台链接qr_code(一段可编码成二维码的字符串)
使用场景浏览器内跳转收银台弹出二维码,用支付宝 App 扫码
产品签约电脑网站支付当面付

两者是独立签约的产品,只开通其中一个不能使用另一个。扫码形态的接入要点:

java
import com.alipay.api.domain.AlipayTradePrecreateModel;
import com.alipay.api.request.AlipayTradePrecreateRequest;
import com.alipay.api.response.AlipayTradePrecreateResponse;

public String precreate(AlipayClient client, String orderNo, long amountFen) throws Exception {
  AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
  model.setOutTradeNo(orderNo);
  model.setTotalAmount(java.math.BigDecimal.valueOf(amountFen, 2).toPlainString());
  model.setSubject("网站服务订单");
  AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
  request.setBizModel(model);
  request.setNotifyUrl(AlipayClients.required("ALIPAY_NOTIFY_URL"));
  AlipayTradePrecreateResponse response = client.execute(request);
  if (!response.isSuccess()) {
    throw new IllegalStateException("预下单失败:" + response.getSubCode());
  }
  return response.getQrCode();
}

拿到 qr_code 后,把它交给二维码库编码成图片(PNG 或 SVG,也可直接生成 data URL 交给前端 <img>)。页面同时展示金额、有效期和倒计时,并轮询服务端订单状态;服务端在订单仍是待支付时用 alipay.trade.query 主动查单。

二维码形态有几个容易忽略的点:

  • 倒计时结束要主动关单。二维码有效期内用户仍可能完成支付,但过期后应调用 alipay.trade.close 关闭该笔交易;只在前端隐藏二维码、不关单,会让一笔已失效的订单仍可被支付。
  • ACQ.TRADE_NOT_EXIST 不是错误。用户尚未付款时查单会返回这个子码,此时必须保持订单为待支付,不能判成失败或关单;这与第 8 节的要求一致。
  • 二维码内容原样使用。不要把 qr_code 再包一层自己的链接,否则支付宝 App 无法识别。
  • 不要依赖前端回调。扫码后的结果仍然只以异步通知或服务端主动查单为准。

两条产品可以并存:PC 端走收银台跳转,需要扫码形态时走预下单,二者共用同一套订单表和幂等结算逻辑。

7. 接收支付宝异步通知 ​

支付宝支付通知使用 POST 和 application/x-www-form-urlencoded,不是 JSON。tio-boot 按 Content-Type 解码表单,业务代码直接读取已解码字段,无需拆分正文或再次调用 URLDecoder。签名中的 %2B 解码后为 +,应原样交给 SDK。

以下工具先校验媒体类型,再调用 getRequestStringMap() 检查重复字段并提取字符串参数,保留所有通知字段供 SDK 验签。框架已完成 URL 解码及表单字段类型处理;无需重复检查参数集合是否为 null 或字段是否为字符串。查询参数与表单字段共同参与验签,重复字段仍需拒绝;空通知交由验签逻辑拒绝。

java
package com.example.payment;

import java.util.Map;

import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.utils.validator.ParameterValidator;

public final class AlipayNotifyParameterValidator {
  private AlipayNotifyParameterValidator() {
  }

  public static Map<String, String> validate(HttpRequest request) {
    ParameterValidator.require(request.isFormUrlEncoded(),
        "Alipay notification must use application/x-www-form-urlencoded");

    return request.getRequestStringMap();
  }
}

SDK 接口要求 Map<String, String>,因此这里使用字符串 Map;一般动态业务字段可以使用 Kv。不要先转为只保留首值的 getRequestMap() 再检查重复字段。

保留 sign、sign_type 等字段供 SDK 验签,不要自己对整个请求正文排序签名:

java
import com.alipay.api.internal.util.AlipaySignature;

boolean verified = "RSA2".equals(params.get("sign_type"))
    && AlipaySignature.rsaCheckV1(
        params, alipayPublicKey, "UTF-8", "RSA2");

验签通过后仍须核对以下业务信息,全部通过才能结算:

  • out_trade_no 对应本地订单,渠道为支付宝。
  • app_id 与当前应用及订单中保存的应用一致。
  • seller_id 与实际收款账号及订单一致。
  • total_amount 与订单金额一致,用 BigDecimal 精确比较,不能使用 double。
  • trade_no 非空;若订单已经支付,必须与已保存的交易号一致。
  • 仅将 TRADE_SUCCESS、TRADE_FINISHED 作为支付成功处理;不能把其他状态当成到账。

处理器实现 HttpRequestHandler,handle(HttpRequest request) 返回 HttpResponse。注册到 HttpRequestRouter 的方式与其他 tio-boot 接口相同:

java
router.post("/api/pay/alipay/notify", new AlipayNotifyHandler());

AlipayNotifyHandler 是业务处理器,需要自行实现上述解析、验签和订单结算。通知路由不要求网站用户登录,应从登录拦截器放行,但始终进行签名与订单校验。只接受 POST,并在服务器和代理层设置合理的正文大小限制。

事务提交成功后,返回 HTTP 200 和纯文本 success:

java
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.server.util.Resps;

return Resps.txt(TioRequestContext.getResponse().setStatus(200), "success", "UTF-8");

不能包装成 JSON,也不要加 HTML 或调试文字。验签失败、金额不匹配或事务失败时返回失败响应,不要提前回复 success。

7.1 从请求里读取正文与表单参数 ​

tio-boot 的 HttpRequest 提供独立的正文与参数读取方法:

方法返回适用场景
getBodyString()原始正文字符串需要原始正文文本的场景,如微信支付通知验签;原始字节使用 getBody()
getRequestMap()参数 Map查询参数与已解码表单字段,重复参数取首值
getBodyObject(Type.class)请求实体JSON 正文,不合并查询或表单参数

getRequestMap() 直接返回框架已经解码的查询和表单参数,不解析 JSON 正文。表单按 Content-Type 在解码层处理;返回的 Map 独立于原有参数表,getParam()、getObject()、getLong() 的取值规则保持不变。JSON 正文通过 getBodyObject() 或 JsonUtils.parse(request.getBodyString(), type) 解析;全局异常处理器捕获 ParameterValidationException 和所用 JSON provider 的解析异常,统一返回 HTTP 400。完整语义见 HttpRequest。

支付宝通知使用 AlipayNotifyParameterValidator.validate(request) 读取框架已解码的表单参数,再交给 SDK 验签。微信支付通知使用 getBodyString() 保留原始 JSON 正文参与验签,不要解析后重新序列化。两种协议的签名输入不同,应分别处理。

普通表单接口无需重复字段检查时,可以直接使用 Kv:

java
import com.jfinal.kit.Kv;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.utils.validator.ParameterValidator;

public class FormOrderParameters {
  public String readOrderNumber(HttpRequest request) {
    Kv parameters = Kv.create().set(request.getRequestMap());
    return ParameterValidator.text(parameters.get("out_trade_no"), "out_trade_no", 64);
  }
}

支付通知按第三方协议返回纯文本回执;其他业务接口通过全局异常处理器捕获 ParameterValidationException,返回 RespBodyVo。

8. 防止重复到账与主动查单 ​

通知可能重复,查单与通知也可能同时返回成功。不能采用“先查状态、再无条件加积分”的方式。

在同一个数据库事务中完成:

  1. 条件更新订单:仅允许 PENDING 变为 PAID,保存支付宝交易号。
  2. 只有更新成功的一次事务,才发放商品权益或充值积分并写流水。
  3. 权益发放失败则回滚订单更新;不能让订单已支付而权益没有到账。
  4. 重复通知核对相同交易号后返回成功,不再发放权益;不同交易号应记录异常供对账。

建议对渠道与支付宝交易号建立适当的唯一约束,对业务流水设置唯一订单标识。数据库并发冲突应作为失败处理,允许支付宝重试或后续查单恢复。

用户查询本地订单状态时先验证订单归属。仍未支付的订单可用 AlipayTradeQueryRequest 调用 alipay.trade.query,由官方 SDK 验证响应签名;核对订单号、金额、收款账号等返回信息后,进入与通知相同的结算函数。查询应限频,避免每次前端轮询都无节制调用渠道。

网络异常、查询超时或交易暂不存在,都不表示已关闭。本地倒计时结束也不能证明用户未付款。确认渠道关闭后才关闭本地订单;已关闭订单收到支付成功时应保留证据并进入对账处理,不能静默丢失资金。

9. 移动端收款与「不跳网页」的支付 ​

「电脑网站支付」是为桌面浏览器设计的产品,在手机浏览器里打开收银台链接体验很差。下面单独说明移动端应该怎么做。

9.1 为什么可以不出现网页中转 ​

移动端不跳网页,靠的是系统级的 URL Scheme(Deep Link):支付 App 在安装时会向系统注册自己的协议,比如支付宝是 alipays://、微信是 weixin://。任何应用或浏览器只要打开这样一个链接,系统就会直接唤起对应的 App,把付款参数交给它,整个过程不经过中间网页。用户看到的是“点一下就直接进了支付 App 的付款页”。

具体到各端:

场景用什么唤起方式
手机浏览器(H5 网页)支付宝手机网站支付 alipay.trade.wap.pay服务端返回签名链接,浏览器打开后立即重定向到 alipays://,唤起支付宝 App
手机浏览器(H5 网页)微信 H5 支付 mweb_url打开 mweb_url 后唤起微信客户端,必须在请求里带真实 referer,并在商户平台配置 H5 支付域名
微信内打开的网页微信 JSAPI 支付网页已在微信内置浏览器里,通过 WeixinJSBridge 直接拉起付款面板,完全不出微信
自家 App(iOS/Android)各家支付 SDKApp 内直接调用 SDK,由 SDK 负责唤起支付宝/微信
小程序小程序支付小程序内 wx.requestPayment

关键点:“不跳网页”的前提是浏览器或应用本身能触发 URL Scheme。微信内置浏览器会拦截第三方 App 的唤起,所以微信内的网页只能用 JSAPI;支付宝在微信内也无法唤起,需要引导用户“在浏览器中打开”。

9.2 产品仍然是独立签约的 ​

上面每一种形态都对应一个独立的支付产品,需要分别签约:

形态产品接口
手机浏览器唤起支付宝手机网站支付alipay.trade.wap.pay
手机浏览器唤起微信H5 支付mweb_url
微信内网页JSAPI 支付prepay_id + WeixinJSBridge
扫码当面付 / Nativealipay.trade.precreate / code_url
PC 网页电脑网站支付alipay.trade.page.pay

只开通了电脑网站支付时,不能用它的链接去实现手机端的唤起;同理,只开通扫码支付也不能替代 H5 支付。接入前先在商户平台确认目标形态的产品已经签约。

9.3 工程上的注意点 ​

  • 回调地址复用同一套。不论哪个形态,异步通知都会打到同一个 notify_url,因此第 8 节的幂等结算逻辑可以完全共用,不需要按形态分叉。
  • 订单要记录支付形态。同一张订单表里建议保留渠道 + 形态(PC / H5 / JSAPI / 扫码),便于对账和排查。
  • H5 支付需要校验 referer。服务端取请求头 referer 传给微信下单接口;缺失或与商户平台配置的域名不一致会下单失败。
  • 微信内网页要单独处理。WeixinJSBridge 未就绪时需要监听 WeixinJSBridgeReady 事件再拉起支付;发起支付的参数里的 package 字段对应服务端返回的 packageVal。
  • JSAPI 需要用户 OpenID。在公众号网页里通过网页授权拿 openid(授权域名要在公众平台配置),再下单。
  • 不要把回跳当成功。移动端尤其容易把“从 App 返回网页”当成支付成功,到账依据仍然只有异步通知和服务端主动查单。

10. 上线前逐项验证 ​

验证项通过标准
产品与应用支付产品签约生效,应用上线,对应能力可调用
密钥应用公钥与本地私钥匹配,支付宝公钥来源正确
公网通知HTTPS 地址可访问,无登录跳转,代理保留表单正文
小额真实付款订单金额正确,通知或查单确认后到账一次
重复处理重复通知、并发查单不重复发放权益
异常处理错误签名、金额、主体和订单渠道均被拒绝
事务回滚权益发放失败时订单不会错误地保持已结算
用户退出关闭收银台、通知延迟和页面回跳都能通过查单恢复
待支付查单用户未付款时查单返回“交易不存在”,订单仍保持待支付,不会被误判为关单
二维码形态倒计时结束后主动关单,二维码内容原样使用且能唤起支付宝 App

先完成测试和部署,再开启 ALIPAY_ENABLED。未部署、未通过审核或未完成真实支付验证时,只能称为“代码接入完成”,不能宣布生产收款已开通。

11. 常见问题与资料脱敏 ​

现象排查方向
应用存在却不能支付应用是否上线、产品是否签约、是否绑定正确主体
验签失败是否误用应用公钥、私钥格式是否正确、正文是否重复解码
前端跳回但没有到账回跳不是到账依据,检查通知接收和主动查单
通知不断重试是否返回纯文本 success、数据库事务是否真正提交
保存密钥又要求验证码平台安全验证与首次联系人验证是独立流程,使用本次新验证码
手机上打开收银台体验很差电脑网站支付面向桌面浏览器,移动端应改用手机网站支付或 H5 支付
手机浏览器下单报错或不唤起 App对应产品(手机网站支付 / H5 支付)未签约,或 H5 支付未传 referer、域名未在商户平台配置
微信内网页无法唤起支付微信会拦截第三方 App 唤起,微信内只能用 JSAPI 支付;支付宝需引导用户改用系统浏览器打开

对外分享文档时,将企业名替换为“示例科技有限公司”,账号使用 YOUR_APP_ID、YOUR_SELLER_ID,域名使用 example.com,手机号使用 1**********。不要发布实际验证码、私钥、签名支付链接、买家账号、交易流水、营业执照图片或带登录状态的浏览器留档。公钥虽然不是私钥,教程也无需复制真实商户公钥。

日志只记录排障必要的状态、错误类型和脱敏订单标识,不记录通知全文或完整配置。生产环境应关闭 tio.devMode,并检查代理和请求日志中间件是否记录支付请求正文;仅在业务处理器里不打印正文还不够。截图需检查页头账号、地址栏参数、弹窗、二维码和文件路径;终端输出已脱敏不代表磁盘中的原始日志也已脱敏。

官方参考:电脑网站支付、接口加签方式、Java SDK。实际准入、费用和审核要求以当前官方页面为准。

MIT Licensed