C# 接入支付宝沙盒二维码支付时的问题如何解决?
结论
支付宝沙盒二维码支付需要调用 alipay.trade.precreate 接口。服务端先创建预下单交易,再把响应中的 qr_code 转成二维码,供用户扫码。
支付失败时,问题通常不在二维码图片的生成过程,而在以下配置:
- 网关没有指向支付宝沙盒地址;
app_id、应用私钥和支付宝公钥不属于同一个沙盒应用;- 签名方式或字符集配置有误;
- 请求参数格式不符合接口要求;
- 扫码时使用了正式支付宝账号;
- 异步通知地址无法从公网访问;
- 验签时误用了应用公钥。
目前的问题描述中没有错误码和接口响应,因此无法判断具体原因。排查时应先记录支付宝接口的原始响应,再逐项核对配置。
正确的接入流程
二维码支付通常按以下流程处理:
- C# 服务端调用
alipay.trade.precreate。 - 支付宝返回
qr_code。 - 服务端或前端把
qr_code生成二维码图片。 - 用户通过支付宝沙盒客户端扫码付款。
- 支付宝向
notify_url发送异步通知。 - 商户验签成功后,根据
out_trade_no更新订单状态。 - 必要时调用
alipay.trade.query主动查询支付结果。
二维码成功生成或前端完成跳转,都不能证明订单已经支付。订单的最终支付状态应以通过验签的异步通知为准,也可以通过主动查询确认。
检查沙盒配置
沙盒环境必须使用沙盒网关:
https://openapi-sandbox.dl.alipaydev.com/gateway.do
以下配置必须对应同一个沙盒应用:
var appId = "你的沙盒应用APPID";
var privateKey = "你的应用私钥";
var alipayPublicKey = "支付宝公钥";
var gateway = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
var charset = "UTF-8";
var signType = "RSA2";
接入时经常混淆以下几种密钥:
- 应用私钥:由商户自行保管,用来为请求签名。
- 应用公钥:上传到支付宝开放平台。
- 支付宝公钥:从开放平台获取,用来验证支付宝返回的数据。
- 支付宝私钥:不会提供给商户。
使用”公钥模式”时,验签配置应填写支付宝公钥,不能填写应用公钥。如果应用启用了证书模式,就要按照 SDK 的证书模式配置,不能与普通公钥模式混用。
C# 预下单示例
下面使用常见的 Alipay AOP SDK 调用方式。不同 SDK 包或版本的命名空间和构造函数可能略有差异,请以项目实际安装的官方 SDK 接口为准。
using System;
using Alipay.AopSdk.Core;
using Alipay.AopSdk.Core.Domain;
using Alipay.AopSdk.Core.Request;
public class AlipayQrPayService
{
private readonly IAopClient _client;
public AlipayQrPayService(
string appId,
string privateKey,
string alipayPublicKey)
{
_client = new DefaultAopClient(
"https://openapi-sandbox.dl.alipaydev.com/gateway.do",
appId,
privateKey,
"json",
"1.0",
"RSA2",
alipayPublicKey,
"UTF-8",
false
);
}
public string CreateQrCode(
string outTradeNo,
decimal amount,
string subject,
string notifyUrl)
{
var request = new AlipayTradePrecreateRequest();
request.SetNotifyUrl(notifyUrl);
request.SetBizModel(new AlipayTradePrecreateModel
{
OutTradeNo = outTradeNo,
TotalAmount = amount.ToString("0.00",
System.Globalization.CultureInfo.InvariantCulture),
Subject = subject
});
var response = _client.Execute(request);
if (response == null)
{
throw new InvalidOperationException("支付宝接口未返回响应。");
}
if (response.Code != "10000")
{
throw new InvalidOperationException(
$"支付宝预下单失败:Code={response.Code}," +
$"SubCode={response.SubCode}," +
$"Msg={response.Msg}," +
$"SubMsg={response.SubMsg}"
);
}
if (string.IsNullOrWhiteSpace(response.QrCode))
{
throw new InvalidOperationException("预下单成功,但未返回 qr_code。");
}
return response.QrCode;
}
}
调用示例:
var service = new AlipayQrPayService(
appId: "你的沙盒APPID",
privateKey: "你的应用私钥",
alipayPublicKey: "你的支付宝公钥"
);
string qrCodeContent = service.CreateQrCode(
outTradeNo: "ORDER_" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
amount: 0.01m,
subject: "沙盒支付测试订单",
notifyUrl: "https://example.com/api/alipay/notify"
);
qrCodeContent 保存的是二维码内容,不一定是图片地址。需要用二维码组件把这个字符串编码成图片。不要修改、截断或额外拼接字符串。
生成二维码图片
可以使用支持二维码生成的 .NET 库处理支付宝返回的 qr_code:
using QRCoder;
public static byte[] GenerateQrCode(string content)
{
using var generator = new QRCodeGenerator();
using var data = generator.CreateQrCode(
content,
QRCodeGenerator.ECCLevel.Q
);
var qrCode = new PngByteQRCode(data);
return qrCode.GetGraphic(10);
}
在 ASP.NET Core 控制器中,可以直接返回 PNG 图片:
[HttpGet("pay/qrcode")]
public IActionResult GetPayQrCode()
{
string content = _alipayService.CreateQrCode(
"ORDER_" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
0.01m,
"沙盒测试订单",
"https://example.com/api/alipay/notify"
);
byte[] image = GenerateQrCode(content);
return File(image, "image/png");
}
二维码库只负责展示内容。即使图片能够正常生成,也不能说明支付宝预下单配置和订单参数一定正确。
正确处理异步通知
支付完成后,支付宝会以表单形式向 notify_url 发送通知。服务端需要读取全部参数并完成验签。
using Alipay.AopSdk.Core.Util;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
[ApiController]
public class AlipayNotifyController : ControllerBase
{
private readonly string _alipayPublicKey = "你的支付宝公钥";
[HttpPost("/api/alipay/notify")]
public async Task<IActionResult> Notify()
{
var form = await Request.ReadFormAsync();
var parameters = form.ToDictionary(
item => item.Key,
item => item.Value.ToString()
);
bool signVerified = AlipaySignature.RSACheckV1(
parameters,
_alipayPublicKey,
"UTF-8",
"RSA2",
false
);
if (!signVerified)
{
return Content("failure");
}
parameters.TryGetValue("trade_status", out var tradeStatus);
parameters.TryGetValue("out_trade_no", out var outTradeNo);
parameters.TryGetValue("total_amount", out var totalAmount);
parameters.TryGetValue("app_id", out var appId);
// 还应核对 app_id、out_trade_no、total_amount 和订单当前状态。
// 核对无误后,以幂等方式更新本地订单。
if (tradeStatus == "TRADE_SUCCESS" ||
tradeStatus == "TRADE_FINISHED")
{
// UpdateOrderAsPaid(outTradeNo);
return Content("success");
}
return Content("success");
}
}
这段代码只演示验签和基本处理结构。生产环境至少还要检查:
app_id是否等于自己的应用 ID;out_trade_no是否存在;total_amount是否与本地订单金额一致;- 订单是否已经处理;
- 通知中的商户身份信息是否匹配;
- 当前交易状态是否允许更新订单。
通知处理成功后,接口必须返回以下纯文本:
success
如果接口返回其他内容、处理超时或响应状态不成功,支付宝可能再次发送通知。因此,订单更新逻辑必须支持幂等处理。
常见错误及处理方法
isv.invalid-signature
这个错误表示签名验证失败,应重点检查:
- 是否把应用公钥当成了支付宝公钥;
- 应用私钥与上传的应用公钥是否配对;
signType是否一致,通常应为RSA2;- 私钥格式是否符合 SDK 要求;
- 配置内容是否包含多余的空格、换行或错误的头尾标记;
- 公钥模式和证书模式是否混用;
- 是否使用了其他应用或正式环境的密钥。
不要通过关闭签名验证来绕过这个错误。
isv.app-id-not-exist
这个错误通常表示 app_id 有误,或者应用 ID 与当前网关环境不匹配。请确认使用的是沙盒应用 ID,并且请求确实发送到了沙盒网关。
ACQ.INVALID_PARAMETER
这个错误表示业务参数不合法。需要记录并检查 SubCode 和 SubMsg,常见原因有:
out_trade_no为空、过长或包含不合适的字符;total_amount格式错误;subject为空;- 请求模型序列化失败;
- 必填参数没有传入。
金额建议使用 decimal,并按固定的小数格式输出:
amount.ToString(
"0.00",
System.Globalization.CultureInfo.InvariantCulture
);
不要直接采用受系统区域设置影响的格式,否则某些环境可能生成 0,01 这样的金额字符串。
二维码能显示,但无法付款
可以依次检查:
- 是否使用支付宝沙盒客户端和沙盒买家账号;
- 二维码内容是否完整;
- 预下单响应是否确实返回
Code=10000; - 二维码是否已经过期;
- 同一个
out_trade_no是否曾用不同金额提交; - 沙盒应用配置是否仍然有效。
正式支付宝客户端通常不能与沙盒环境混用,应按照开放平台提供的沙盒测试方式操作。
支付成功,但收不到通知
常见原因有:
notify_url使用了localhost、局域网地址或其他无法从公网访问的地址;- 通知地址需要登录、验证码或额外鉴权;
- HTTPS 证书无效;
- 防火墙或反向代理拦截了 POST 请求;
- 接口按 JSON 读取请求,但支付宝发送的是表单数据;
- 通知处理时间过长;
- 路由重定向导致请求失败。
本地开发时,可以使用安全的公网映射地址,或者调用主动查询接口确认订单状态。公网映射工具只适合调试,不应直接用作生产环境的通知方案。
主动查询订单状态
如果前端需要轮询支付状态,可以调用 alipay.trade.query,但需要控制轮询频率。
using Alipay.AopSdk.Core.Domain;
using Alipay.AopSdk.Core.Request;
public string QueryTrade(IAopClient client, string outTradeNo)
{
var request = new AlipayTradeQueryRequest();
request.SetBizModel(new AlipayTradeQueryModel
{
OutTradeNo = outTradeNo
});
var response = client.Execute(request);
if (response == null)
{
throw new InvalidOperationException("查询接口未返回响应。");
}
if (response.Code != "10000")
{
return $"查询失败:{response.SubCode} - {response.SubMsg}";
}
return response.TradeStatus;
}
常见的交易状态包括:
WAIT_BUYER_PAY
TRADE_SUCCESS
TRADE_FINISHED
TRADE_CLOSED
各状态的具体含义和对应操作,应以支付宝当前接口文档为准。
建议的排查顺序
先记录以下字段,不要在捕获异常后只返回”支付失败”:
Code
Msg
SubCode
SubMsg
Body
OutTradeNo
然后按顺序检查:
- 请求是否发往沙盒网关;
app_id是否属于当前沙盒应用;- 私钥和支付宝公钥是否配置正确;
- 签名方式是否统一为
RSA2; alipay.trade.precreate是否返回Code=10000;qr_code是否未经修改就被生成二维码;- 是否使用沙盒账号和沙盒客户端扫码;
- 通知接口能否从公网访问并正确验签;
- 本地订单金额、订单号和通知参数是否一致。
如果按照以上步骤仍无法定位问题,需要提供完整的 Code、SubCode、Msg 和 SubMsg。提交信息前应隐藏私钥、公钥、令牌和个人信息,但不要省略支付宝返回的错误码。仅凭”二维码支付报错”无法判断故障发生在哪个环节。
备注:内容仅供参考。
