支付宝:电脑网站支付接入指南
本文面向自行运营网站的普通商户,介绍从申请产品到使用 tio-boot 后端接入支付宝收款的过程。用户在网站选择商品,后端创建订单并生成支付宝收银台链接,用户跳转后付款,后端通过异步通知和主动查单确认到账。
本文采用官方 Java SDK 的 v2 接口和 RSA2 公钥模式,调用 alipay.trade.page.pay。电脑网站支付、手机网站支付和 App 支付是不同产品,不能用其中一个产品的申请结果代表其他产品也已开通。
所有商户资料、AppID、账号、域名和订单号均应替换为自己的信息。本文仅使用占位符和 example.com 示例域名,不包含真实企业资料、手机号、验证码或密钥。
1. 先准备哪些材料
| 准备项 | 说明 |
|---|---|
| 企业支付宝账号 | 完成对应主体的实名认证,使用本次实际收款的企业账号登录 |
| 营业执照信息 | 公司名称、经营范围、法定代表人等,用于核对主体和经营类目;已认证账号可能无需重复上传 |
| 网站地址及备案信息 | 网站应能正常访问,并展示实际商品或服务、价格和购买流程;备案主体与收款主体应核对一致 |
| 联系人 | 提供真实姓名和能接收短信的手机号,由本人完成验证 |
| 应用名称、图标、简介 | 使用实际产品名称及图标,简介如实描述服务内容与交付方式 |
| 特殊材料 | 如果使用其他主体的网站,或经营类目要求资质,按页面要求提供授权函、许可证等材料 |
未上线网站是否可以用首页、商品页和支付页截图申请,以当前申请页面为准。选择类目要结合营业执照和实际业务,不能只挑容易通过审核的类目。
开通费、交易手续费和可能适用的保证金是不同事项。不要把其他平台的认证费直接套用到支付宝。申请时逐项查看当前产品页面和签约合同中的费用说明;示例中若按 0.6% 计算,收款 100 元对应手续费 0.60 元,这只是计算示例,不是对所有商户的费率承诺。
2. 在商家平台申请支付产品
- 登录 支付宝商家平台,确认当前企业名称和收款账号。
- 在产品中心找到「电脑网站支付」,进入申请流程。
- 如实选择经营类目,填写网站地址和联系人;按页面要求补充材料。
- 核对收款主体、网站、费率及协议,由联系人完成短信验证后提交。
- 查看申请结果。「已提交,正在审核中」只代表受理,须等到产品签约生效。
页面提示的预计审核时间不是批准承诺。若被退回,按原因修改材料,先查看已有申请,避免重复申请。短信验证码有时效且通常只能使用一次,优先由本人在官方页面填写,不应写入教程、截图说明或项目日志。
3. 在开放平台创建网页应用
登录 支付宝开放平台,创建「网页应用」,填写名称、图标、简介和官网,并绑定本次收款的企业账号。首次使用平台可能要求验证联系人信息。
应用类型和绑定主体可能在创建后不能修改,应在提交前核对。上传图标后可能还会出现裁剪确认弹窗,需要确认后才算完成上传。
创建完成后保存 AppID。在应用的「开发设置」配置接口加签方式,再从应用详情提交上线审核。产品签约审核与应用上线审核是两个流程;应用创建成功或密钥保存成功,都不等于可以正式收款。还应在「可调用产品」确认对应支付能力可用。
4. 配置 RSA2 密钥
三个名称要分清:
| 名称 | 用途 | 保存位置 |
|---|---|---|
| 应用私钥 | 后端给支付请求签名 | 仅保存在自己的服务端或密钥管理系统 |
| 应用公钥 | 支付宝验证商户请求 | 上传至该应用的接口加签配置 |
| 支付宝公钥 | 商户验证支付宝通知和接口响应 | 从该应用的配置页面获取,供后端使用 |
本文选择「密钥」加签方式,算法使用 RSA2。若使用要求证书模式的其他产品,需另行按证书模式集成,不能直接照搬本文配置。
可使用支付宝官方密钥工具生成密钥,也可在安装了 OpenSSL 的电脑上执行:
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,并限制运行账号之外的读取权限:
certs/alipay/
secrets.txt
.env
.gitignore 不能清除已经提交过的文件。如果私钥已经泄漏,应更换密钥并更新平台配置,不能仅删除文件后继续使用。
5. tio-boot 配置与 SDK
使用官方 com.alipay.sdk:alipay-sdk-java 依赖,在项目的依赖管理中选择经过验证的版本。SDK 用法参见 官方 Java SDK 文档。不要自行拼接签名算法或从非官方网页复制密钥。
配置示例:
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,也不是登录邮箱。应从官方后台核对。配置文件需要被项目启动流程实际加载;仅把文件放进目录不会自动生效。相对密钥路径以服务进程工作目录为基准。
以下是配置和客户端的核心示例,异常由应用启动或接口层处理:
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:
String orderNo = "ALI" + nexus.io.tio.utils.snowflake.SnowflakeIdUtils.id();
下面只展示生成链接部分;其中的金额和订单号必须来自已经持久化的服务端订单:
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 生成签名链接,不表示订单已经付款,也不能用它证明产品签约已生效。复用订单时,应保持原订单的过期时间,不要每次生成链接都延长支付期限。
页面回跳只用于导航。用户访问成功页、关闭窗口或点击“我已支付”,都不能直接发放商品或积分。
7. 接收支付宝异步通知
支付宝支付通知是表单 POST,通常为 application/x-www-form-urlencoded。tio-boot 可从 HttpRequest.getBodyString() 读取原始正文。必须只进行一次表单解码;签名中的 + 经 %2B 解码后不能再解码为空格。
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
public static Map<String, String> parseForm(String body) {
if (body == null || body.isBlank()) throw new IllegalArgumentException("Empty form");
Map<String, String> values = new LinkedHashMap<>();
for (String field : body.split("&")) {
String[] pair = field.split("=", 2);
if (pair.length != 2) throw new IllegalArgumentException("Invalid form");
String key = URLDecoder.decode(pair[0], StandardCharsets.UTF_8);
String value = URLDecoder.decode(pair[1], StandardCharsets.UTF_8);
if (values.putIfAbsent(key, value) != null) {
throw new IllegalArgumentException("Duplicate field");
}
}
return values;
}
保留 sign、sign_type 等字段供 SDK 验签,不要自己对整个请求正文排序签名:
boolean verified = "RSA2".equals(params.get("sign_type"))
&& com.alipay.api.internal.util.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 接口相同:
router.add("/api/pay/alipay/notify", new AlipayNotifyHandler());
AlipayNotifyHandler 是业务处理器,需要自行实现上述解析、验签和订单结算。通知路由不要求网站用户登录,应从登录拦截器放行,但始终进行签名与订单校验。只接受 POST,并在服务器和代理层设置合理的正文大小限制。
事务提交成功后,返回 HTTP 200 和纯文本 success:
return nexus.io.tio.boot.http.TioRequestContext.getResponse()
.setStatus(200)
.setString("success", "UTF-8", "text/plain;charset=UTF-8");
不能包装成 JSON,也不要加 HTML 或调试文字。验签失败、金额不匹配或事务失败时返回失败响应,不要提前回复 success。
8. 防止重复到账与主动查单
通知可能重复,查单与通知也可能同时返回成功。不能采用“先查状态、再无条件加积分”的方式。
在同一个数据库事务中完成:
- 条件更新订单:仅允许
PENDING变为PAID,保存支付宝交易号。 - 只有更新成功的一次事务,才发放商品权益或充值积分并写流水。
- 权益发放失败则回滚订单更新;不能让订单已支付而权益没有到账。
- 重复通知核对相同交易号后返回成功,不再发放权益;不同交易号应记录异常供对账。
建议对渠道与支付宝交易号建立适当的唯一约束,对业务流水设置唯一订单标识。数据库并发冲突应作为失败处理,允许支付宝重试或后续查单恢复。
用户查询本地订单状态时先验证订单归属。仍未支付的订单可用 AlipayTradeQueryRequest 调用 alipay.trade.query,由官方 SDK 验证响应签名;核对订单号、金额、收款账号等返回信息后,进入与通知相同的结算函数。查询应限频,避免每次前端轮询都无节制调用渠道。
网络异常、查询超时或交易暂不存在,都不表示已关闭。本地倒计时结束也不能证明用户未付款。确认渠道关闭后才关闭本地订单;已关闭订单收到支付成功时应保留证据并进入对账处理,不能静默丢失资金。
9. 上线前逐项验证
| 验证项 | 通过标准 |
|---|---|
| 产品与应用 | 支付产品签约生效,应用上线,对应能力可调用 |
| 密钥 | 应用公钥与本地私钥匹配,支付宝公钥来源正确 |
| 公网通知 | HTTPS 地址可访问,无登录跳转,代理保留表单正文 |
| 小额真实付款 | 订单金额正确,通知或查单确认后到账一次 |
| 重复处理 | 重复通知、并发查单不重复发放权益 |
| 异常处理 | 错误签名、金额、主体和订单渠道均被拒绝 |
| 事务回滚 | 权益发放失败时订单不会错误地保持已结算 |
| 用户退出 | 关闭收银台、通知延迟和页面回跳都能通过查单恢复 |
先完成测试和部署,再开启 ALIPAY_ENABLED。未部署、未通过审核或未完成真实支付验证时,只能称为“代码接入完成”,不能宣布生产收款已开通。
10. 常见问题与资料脱敏
| 现象 | 排查方向 |
|---|---|
| 应用存在却不能支付 | 应用是否上线、产品是否签约、是否绑定正确主体 |
| 验签失败 | 是否误用应用公钥、私钥格式是否正确、正文是否重复解码 |
| 前端跳回但没有到账 | 回跳不是到账依据,检查通知接收和主动查单 |
| 通知不断重试 | 是否返回纯文本 success、数据库事务是否真正提交 |
| 保存密钥又要求验证码 | 平台安全验证与首次联系人验证是独立流程,使用本次新验证码 |
对外分享文档时,将企业名替换为“示例科技有限公司”,账号使用 YOUR_APP_ID、YOUR_SELLER_ID,域名使用 example.com,手机号使用 1**********。不要发布实际验证码、私钥、签名支付链接、买家账号、交易流水、营业执照图片或带登录状态的浏览器留档。公钥虽然不是私钥,教程也无需复制真实商户公钥。
日志只记录排障必要的状态、错误类型和脱敏订单标识,不记录通知全文或完整配置。生产环境应关闭 tio.devMode,并检查代理和请求日志中间件是否记录支付请求正文;仅在业务处理器里不打印正文还不够。截图需检查页头账号、地址栏参数、弹窗、二维码和文件路径;终端输出已脱敏不代表磁盘中的原始日志也已脱敏。
