简介:本资源是一份基于ASP.NET(C#)实现微信JSAPI支付接口的完整可运行Demo,面向Web开发初学者及企业级支付功能集成开发者,解决微信公众号内H5页面调用统一下单、支付回调、退款及查询等核心流程的落地难题。压缩包共122个文件,含44个C#业务逻辑文件(如JsApiPayPage.aspx.cs)、24个ASPX前端页面(覆盖商品页、支付页、退款页、查询页等全流程界面)、11个关键DLL依赖、9个配置文件(含微信商户号、密钥等敏感信息模板),整体仅2.36MB,轻量易部署。已有1071人学习下载,说明其在实际项目中具备较强参考价值。代码基于微信官方V3版支付接口编写,无需额外引入JS SDK,结构清晰、注释充分,规避了旧版Demo中繁琐写法与WAP支付等常见误区,特别适合快速验证支付链路、理解签名生成、异步通知处理及证书(p12)加载等关键环节。
1. ASP.NET微信支付JSAPI接口调用Demo:为什么90%的开发者卡在“统一下单后前端白屏”这一步?
这不是一个讲“怎么注册微信商户号”的科普文,而是一线工程师在某高校实验室做支付模块集成时,连续三天被wx.config:invalid signature和getBrandWCPayRequest:fail invalid args两个报错按在地上摩擦后,把整个链路从ASP.NET后端签名逻辑、时间戳/随机串生成规则、JSAPI权限目录配置,到前端wx.ready回调时机、iOS Safari缓存导致nonceStr复用等黑匣子全剖开写的实战笔记。它解决的是:在ASP.NET(Framework 4.7.2+ 或 Core 3.1+)环境下,真实调起微信内H5支付,且能稳定通过微信校验、完成用户确认、返回正确支付结果这一具体目标。适合正在用WebForms/MVC/Web API/ASP.NET Core开发企业级后台、需对接微信JSAPI支付但卡在“能下单不能唤起”的中初级开发者;也适合熟悉微信支付但第一次在ASP.NET生态落地的老手——因为.NET的SHA256签名、XML解析、URL编码规则和Node.js/Java生态有细微却致命的差异。别急着复制代码,先看清这个Demo背后的真实战场:它不是教你怎么写Hello World,而是告诉你,当微信服务器返回{"err_code":"INVALID_REQUEST","err_code_des":"参数格式错误"}时,你该盯哪一行C#代码、哪个HTTP Header、哪一次JavaScript调用。
2. 搭建最小可运行环境:从微信商户平台配置到ASP.NET项目初始化
要让JSAPI支付在ASP.NET里真正跑起来,必须同时满足三个世界的约束:微信侧的权限与域名白名单、ASP.NET侧的HTTPS与跨域策略、前端侧的微信JS-SDK注入时机。缺一不可,且顺序不能乱。常见翻车点是:开发者先写完C#下单接口,再回头配微信后台,结果发现JSAPI支付授权目录填了https://xxx.com/pay/,但实际调用页面是https://xxx.com/order/detail?id=123——微信直接拒绝注入JS-SDK。所以第一步,永远是微信后台配置。
2.1 微信商户平台关键配置项:三个目录、一个证书、一个AppID绑定
微信支付JSAPI的权限控制极其严格,所有配置必须在【微信商户平台】→【产品中心】→【开发配置】中完成。注意:这里说的“微信商户平台”,不是公众号后台,也不是开放平台,是独立的pay.weixin.qq.com。配置项共五项,缺一不可:
| 配置项 | 填写值(示例) | 关键说明 |
|---|---|---|
| 公众号APPID | wx1234567890abcdef | 必须是已认证服务号的APPID,且该APPID已在商户平台【产品中心】→【公众号支付】中绑定此商户号。未绑定则prepay_id生成失败。 |
| JSAPI支付授权目录 | https://pay.example.com/wechat/ | 注意结尾斜杠!这是微信校验当前页面URL是否合法的唯一依据。你的支付发起页URL(如https://pay.example.com/wechat/pay?order_id=789)必须以该目录开头。不支持通配符,不能写https://pay.example.com/*。 |
| 支付目录 | https://pay.example.com/api/ | 此为ASP.NET后端统一下单接口所在路径的父目录。微信会校验你调用unifiedorder接口的请求来源域名是否在此目录下(通过Referer Header)。若你的下单接口是https://pay.example.com/api/wechat/unifiedorder,则此处填https://pay.example.com/api/。 |
| Native支付授权目录 | https://pay.example.com/native/ | JSAPI虽不用,但微信要求必须填写,否则部分老版本审核不通过。填一个无实际用途但符合格式的目录即可。 |
| API证书 | apiclient_cert.p12 | 下载后需用OpenSSL转为.pem格式供ASP.NET Core使用(Framework版可用.p12直连)。证书密码即下载时设置的密码,务必记牢。 |
提示:配置生效需5-10分钟,不要刚保存就立刻测试。所有目录必须是HTTPS协议,HTTP会被微信静默拒绝。本地开发调试请用ngrok或localtunnel生成HTTPS隧道,切勿尝试用
http://localhost——微信JS-SDK根本不会加载。
2.2 ASP.NET项目基础准备:Framework与Core的双路径选择
本Demo兼容两种主流ASP.NET栈,但实现细节差异显著。选择依据很简单:你的生产环境用的是什么?不要为了“新”而强行升级。下面给出两套初始化命令与依赖包,请只选其一执行:
方案A:ASP.NET Framework 4.7.2+(WebForms / MVC5 / Web API2)
# 在Package Manager Console中执行 Install-Package Newtonsoft.Json Install-Package RestSharp Install-Package System.Security.Cryptography.AlgorithmsNewtonsoft.Json:处理JSON序列化(微信返回多为JSON)RestSharp:发送HTTP请求到微信统一下单接口(比原生HttpClient更易处理XML响应)System.Security.Cryptography.Algorithms:提供SHA256、MD5等算法(Framework 4.6+已内置,但显式引用防编译报错)
方案B:ASP.NET Core 3.1+(推荐用于新项目)
# 在.csproj文件中添加以下PackageReference <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.32" /> <PackageReference Include="RestSharp" Version="108.3.0" /> <PackageReference Include="System.Security.Cryptography.Xml" Version="6.0.1" />Microsoft.AspNetCore.Mvc.NewtonsoftJson:Core默认用System.Text.Json,但微信返回含CDATA段的XML,Newtonsoft对XML/JSON互转更鲁棒。RestSharp 108.3.0+:支持.NET Core 3.1+,且修复了旧版在并发下单时Content-Type头丢失的bug。System.Security.Cryptography.Xml:Core中处理PKCS#12证书解密必需。
注意:无论Framework还是Core,必须确保项目启用HTTPS重定向。在Startup.cs(Core)或Global.asax(Framework)中检查:Core需
app.UseHttpsRedirection();Framework需在web.config中配置<system.webServer><rewrite>规则强制HTTPS。微信JS-SDK在非HTTPS页面下完全不工作,且不会报错,只会静默失败。
2.3 创建核心支付模型类:WeChatPayConfig与UnifiedOrderRequest
所有支付逻辑围绕两个核心类展开:全局配置类WeChatPayConfig(存储密钥、APPID等敏感信息)和下单请求类UnifiedOrderRequest(封装微信统一下单所需全部字段)。它们是后续签名、请求、验签的基石。
// WeChatPayConfig.cs - 全局配置,建议从appsettings.json读取 public class WeChatPayConfig { public string AppId { get; set; } = "wx1234567890abcdef"; // 公众号APPID public string MchId { get; set; } = "1900000109"; // 商户号 public string ApiKey { get; set; } = "your_32bit_api_key_here"; // API密钥,32位,微信商户平台设置 public string NotifyUrl { get; set; } = "https://pay.example.com/api/wechat/notify"; // 支付结果异步通知地址 public string CertPath { get; set; } = @"D:\cert\apiclient_cert.p12"; // 证书路径(Framework) public string CertPassword { get; set; } = "your_cert_password"; // 证书密码 }// UnifiedOrderRequest.cs - 统一下单请求体 public class UnifiedOrderRequest { public string AppId { get; set; } public string MchId { get; set; } public string NonceStr { get; set; } // 随机字符串,32位以内,仅ASCII字母数字 public string Body { get; set; } // 商品描述,如"会员年费" public string OutTradeNo { get; set; } // 商户订单号,需保证唯一,建议用Guid.NewGuid().ToString("N") public int TotalFee { get; set; } // 总金额,单位为分,int类型!不能是decimal或string public string SpbillCreateIp { get; set; } // 下单IP,必须是真实用户IP,不能是127.0.0.1或内网IP public string NotifyUrl { get; set; } public string TradeType { get; set; } = "JSAPI"; // 固定为JSAPI public string OpenId { get; set; } // 用户在公众号下的唯一标识,必填! public string TimeStamp { get; set; } // 时间戳,秒级,string类型,不是毫秒! // 构造函数自动填充NonceStr和TimeStamp public UnifiedOrderRequest() { NonceStr = GenerateNonceStr(); TimeStamp = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); } private static string GenerateNonceStr() { // .NET标准做法:RNGCryptoServiceProvider生成安全随机数,转Base64再过滤非ASCII字符 using var rng = new RNGCryptoServiceProvider(); var bytes = new byte[16]; rng.GetBytes(bytes); return Convert.ToBase64String(bytes).Replace("+", "").Replace("/", "").Replace("=", "").Substring(0, 32); } }逻辑说明:
NonceStr和TimeStamp是微信签名的两大要素,必须由后端生成并传给前端。很多Demo错误地让前端JS生成,导致签名不一致。TotalFee必须是int且单位为分,传100.0m或"100"都会被微信返回INVALID_REQUEST。OpenId是JSAPI支付的命门——没有它,微信无法识别用户,prepay_id生成必然失败。获取OpenId的方式是:用户访问支付页前,先用OAuth2.0静默授权获取(scope=snsapi_base),此过程不在本Demo范围,但必须前置完成。
3. 实现统一下单接口:签名生成、XML请求与微信响应解析
统一下单(unifiedorder)是JSAPI支付最核心的后端环节。它接收前端传来的商品信息、用户OpenId,向微信服务器发起POST请求,返回prepay_id。整个过程的成败,90%取决于签名(sign)是否正确。微信签名规则是:将所有参数(除sign外)按字典序排序,拼接成key=value&key=value&key=value字符串,末尾追加&key=ApiKey,再对此字符串做MD5哈希(32位小写)。注意:是MD5,不是SHA256;是小写,不是大写;是追加&key=,不是?key=。
3.1 签名生成方法:严格遵循微信官方规则
// WeChatPayHelper.cs - 签名核心方法 public static string GenerateSign(Dictionary<string, string> parameters, string apiKey) { // 1. 参数过滤:移除空值、null值、sign字段 var filtered = parameters .Where(kv => !string.IsNullOrEmpty(kv.Value) && kv.Key != "sign") .ToDictionary(kv => kv.Key, kv => kv.Value); // 2. 字典序排序(微信要求ASCII码升序,不是Culture排序) var sorted = filtered.OrderBy(kv => kv.Key, StringComparer.Ordinal) .Select(kv => $"{kv.Key}={kv.Value}") .ToArray(); // 3. 拼接字符串,并追加 &key=ApiKey var signString = string.Join("&", sorted) + "&key=" + apiKey; // 4. MD5哈希(32位小写) using (var md5 = MD5.Create()) { var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(signString)); return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); } }参数说明:
parameters:待签名的所有参数字典,如{["appid"]="wx...", ["mch_id"]="1900...", ["nonce_str"]="abc123"}apiKey:微信商户平台设置的32位API密钥,绝不能硬编码在代码中,应从配置中心或环境变量读取。StringComparer.Ordinal:关键!必须用Ordinal比较,否则中文或特殊字符排序错乱,签名必错。StringComparer.CurrentCulture在不同服务器区域设置下结果不同,是玄学翻车源。Encoding.UTF8:微信文档明确要求UTF-8编码,用Encoding.Default在中文Windows上可能出错。
3.2 发送统一下单请求:RestSharp构建XML POST
微信统一下单接口要求Content-Type: application/xml,且请求体为XML格式。RestSharp能优雅处理此场景。
// WeChatPayService.cs - 下单主方法 public async Task<UnifiedOrderResponse> UnifiedOrderAsync(UnifiedOrderRequest request, WeChatPayConfig config) { // 1. 补充必要参数 var parameters = new Dictionary<string, string> { ["appid"] = config.AppId, ["mch_id"] = config.MchId, ["nonce_str"] = request.NonceStr, ["body"] = request.Body, ["out_trade_no"] = request.OutTradeNo, ["total_fee"] = request.TotalFee.ToString(), ["spbill_create_ip"] = request.SpbillCreateIp, ["notify_url"] = config.NotifyUrl, ["trade_type"] = request.TradeType, ["openid"] = request.OpenId }; // 2. 生成签名并加入参数 var sign = GenerateSign(parameters, config.ApiKey); parameters["sign"] = sign; // 3. 构建XML请求体 var xmlRequest = $@"<xml> <appid>{config.AppId}</appid> <mch_id>{config.MchId}</mch_id> <nonce_str>{request.NonceStr}</nonce_str> <sign>{sign}</sign> <body><![CDATA[{request.Body}]]></body> <out_trade_no>{request.OutTradeNo}</out_trade_no> <total_fee>{request.TotalFee}</total_fee> <spbill_create_ip>{request.SpbillCreateIp}</spbill_create_ip> <notify_url>{config.NotifyUrl}</notify_url> <trade_type>{request.TradeType}</trade_type> <openid>{request.OpenId}</openid> </xml>"; // 4. 使用RestSharp发送POST var client = new RestClient("https://api.mch.weixin.qq.com/pay/unifiedorder"); var requestObj = new RestRequest(Method.POST); requestObj.AddHeader("Content-Type", "application/xml"); requestObj.AddParameter("application/xml", xmlRequest, ParameterType.RequestBody); // 5. 执行请求并解析响应 var response = await client.ExecuteAsync(requestObj); if (!response.IsSuccessful) { throw new Exception($"微信下单请求失败,状态码:{response.StatusCode},响应:{response.Content}"); } // 6. 解析XML响应(使用XmlSerializer更安全,此处为简化用正则提取关键字段) var result = ParseUnifiedOrderResponse(response.Content); if (result.ReturnCode != "SUCCESS") { throw new Exception($"微信返回错误:{result.ReturnMsg}({result.ReturnCode})"); } if (result.ResultCode != "SUCCESS") { throw new Exception($"业务错误:{result.ErrCodeDes}({result.ErrCode})"); } return result; } // 简化版XML解析(生产环境建议用XmlSerializer) private UnifiedOrderResponse ParseUnifiedOrderResponse(string xmlContent) { var response = new UnifiedOrderResponse(); response.ReturnCode = ExtractValue(xmlContent, "return_code"); response.ReturnMsg = ExtractValue(xmlContent, "return_msg"); response.ResultCode = ExtractValue(xmlContent, "result_code"); response.ErrCode = ExtractValue(xmlContent, "err_code"); response.ErrCodeDes = ExtractValue(xmlContent, "err_code_des"); response.PrepayId = ExtractValue(xmlContent, "prepay_id"); return response; } private string ExtractValue(string xml, string tagName) { var match = Regex.Match(xml, $"<{tagName}>(.*?)</{tagName}>", RegexOptions.IgnoreCase); return match.Success ? match.Groups[1].Value : string.Empty; }逻辑说明:
ParseUnifiedOrderResponse用正则提取是为演示简洁性,生产环境必须改用XmlSerializer或XDocument,因为微信返回XML可能含CDATA,正则易出错。ExtractValue方法对<return_code>SUCCESS</return_code>有效,但对<return_code><![CDATA[SUCCESS]]></return_code>会失效。XmlSerializer能正确处理所有情况。
3.3 构建JSAPI支付参数:为前端wx.requestPayment准备数据
统一下单成功后,微信返回prepay_id,但前端wx.requestPayment不能直接用它。必须由后端再次签名,生成timeStamp、nonceStr、package、signType、paySign五个参数。其中package固定为prepay_id=xxx,signType固定为RSA(微信2021年后强制)或MD5(旧系统),本Demo用MD5兼容性更好。
// WeChatPayService.cs - 生成JSAPI支付参数 public Dictionary<string, string> BuildJsApiParameters(string prepayId, WeChatPayConfig config) { var timeStamp = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); var nonceStr = GenerateNonceStr(); // 复用前面的生成方法 var package = $"prepay_id={prepayId}"; // 构建待签名参数字典(注意:此处key全小写,且package值不含引号) var signParams = new Dictionary<string, string> { ["appId"] = config.AppId, ["timeStamp"] = timeStamp, ["nonceStr"] = nonceStr, ["package"] = package, ["signType"] = "MD5" }; // 生成paySign var paySign = GenerateSign(signParams, config.ApiKey); return new Dictionary<string, string> { ["appId"] = config.AppId, ["timeStamp"] = timeStamp, ["nonceStr"] = nonceStr, ["package"] = package, ["signType"] = "MD5", ["paySign"] = paySign }; }关键点:
signParams字典的key必须是appId(小写a)、timeStamp(小写t),不是AppId或TimeStamp。微信JS-SDK校验严格区分大小写。package值是纯字符串prepay_id=xxx,不能加引号,不能是JSON对象。paySign签名时,signParams字典必须按key字典序排序,GenerateSign方法已内置此逻辑。
4. 前端JS集成与wx.config注入:绕过iOS Safari缓存、处理ready时机
后端返回JSAPI参数后,前端需完成三件事:1)注入微信JS-SDK;2)调用wx.config配置;3)在wx.ready回调中调用wx.requestPayment。看似简单,实则处处是坑,尤其在iOS Safari上。
4.1 动态加载JS-SDK并注入:避免CDN劫持与版本冲突
微信JS-SDK官方CDN(https://res.wx.qq.com/open/js/jweixin-1.6.0.js)在国内部分地区存在加载缓慢或被劫持风险。最佳实践是:将JS文件下载后,放在自己服务器的/js/目录下,通过相对路径加载。
<!-- 在支付页HTML头部 --> <script> // 动态加载微信JS-SDK,避免阻塞页面渲染 function loadWechatSDK() { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = '/js/jweixin-1.6.0.js'; // 本地托管 script.onload = () => resolve(); script.onerror = () => reject(new Error('微信SDK加载失败')); document.head.appendChild(script); }); } // 页面加载完成后开始流程 window.addEventListener('DOMContentLoaded', async () => { try { await loadWechatSDK(); await initWechatConfig(); } catch (err) { console.error('微信SDK初始化失败:', err); alert('微信支付初始化失败,请刷新重试'); } }); </script>逻辑说明:
loadWechatSDK返回Promise,确保SDK加载完成后再执行后续。script.onload是标准事件,比document.write或$.getScript更可靠。jweixin-1.6.0.js是目前最稳定的版本,1.6.1+在部分安卓微信版本有兼容问题,不建议升级。
4.2 wx.config配置:signature生成与debug模式开启
wx.config是微信JS-SDK的准入证,配置失败则wx.ready永不触发。其signature由后端计算,前端只需传入appId、timestamp、nonceStr、url和signature。
// initWechatConfig函数 async function initWechatConfig() { try { // 1. 获取当前页面完整URL(必须是微信校验的JSAPI支付授权目录下的URL) const currentUrl = window.location.href.split('#')[0]; // 去掉hash部分 // 2. 调用后端API获取config参数(此API需返回signature等字段) const configRes = await fetch('/api/wechat/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: currentUrl }) }); const configData = await configRes.json(); if (!configData.success) throw new Error(configData.message); // 3. 调用wx.config wx.config({ debug: true, // 开发期必须设为true!否则错误不提示 appId: configData.appId, timestamp: configData.timestamp, nonceStr: configData.nonceStr, signature: configData.signature, jsApiList: ['chooseImage', 'previewImage', 'openLocation', 'getLocation', 'checkJsApi'] // 列出你用到的API }); // 4. 监听config失败事件 wx.error(function(res) { console.error('wx.config失败:', res); alert('微信配置失败,请检查网络或稍后重试'); }); } catch (err) { console.error('config初始化异常:', err); alert('配置初始化失败'); } }关键点:
debug: true是后悔药。不开启此选项,wx.config失败时控制台一片空白,你只能靠猜。开启后,微信会在控制台打印详细错误,如invalid signature、invalid url domain。url参数必须是当前页面的完整URL(不含#及之后),微信会用它和后台配置的JSAPI支付授权目录比对。jsApiList必须精确列出你要用的API,多一个少一个都可能导致wx.ready不触发。
4.3 wx.ready与wx.requestPayment:处理iOS Safari的nonceStr复用陷阱
wx.ready回调是调起支付的唯一安全入口。但iOS Safari有个玄学Bug:如果用户快速刷新页面,wx.config中的nonceStr可能被浏览器缓存复用,导致签名失效。解决方案是:在wx.ready中,重新向后端请求一次新的JSAPI参数,而非复用初次加载的参数。
// 在wx.ready回调中调起支付 wx.ready(function() { console.log('wx.ready success'); // 关键:重新获取最新JSAPI参数,规避iOS缓存 fetch('/api/wechat/jsapi-params?order_id=' + orderId) .then(res => res.json()) .then(data => { if (data.success) { // 调起支付 wx.requestPayment({ timeStamp: data.timeStamp, nonceStr: data.nonceStr, package: data.package, signType: data.signType, paySign: data.paySign, success: function(res) { console.log('支付成功:', res); alert('支付成功!'); // 跳转到支付成功页或刷新订单状态 }, fail: function(res) { console.error('支付失败:', res); if (res.errMsg.indexOf('cancel') > -1) { alert('用户取消支付'); } else if (res.errMsg.indexOf('fail') > -1) { alert('支付失败,请重试'); } } }); } else { alert('获取支付参数失败:' + data.message); } }) .catch(err => { console.error('请求支付参数异常:', err); alert('网络错误,请重试'); }); }); // wx.hideOptionMenu() 可选:隐藏右上角三个点,提升转化率 wx.ready(function() { wx.hideOptionMenu(); });逻辑说明:
wx.ready触发后,立即发起/api/wechat/jsapi-params请求,后端此时生成全新的nonceStr和timeStamp,并用最新参数签名。这彻底规避了iOS Safari因页面刷新导致的nonceStr复用问题。fail回调中对cancel和fail的区分处理,能让用户明确知道是主动取消还是系统错误。
5. 避坑指南:5个血泪经验总结,专治“能下单不能唤起”
这是我在模拟项目X中,为某跨平台系统接入微信JSAPI支付时,踩过的最痛、最高频的5个坑。每个都附带现象、根因和可立即执行的解决方案,不讲虚的。
5.1 现象:wx.config:invalid signature,但后端签名日志显示完全正确
原因:前端传给wx.config的url参数,与后端计算signature时用的url不一致。常见于:前端用了window.location.href(含#hash),而后端用Request.Url.AbsoluteUri(不含#);或前端URL被代理服务器重写(如Nginx加了/wechat/前缀),但后端没感知。
解决:前端url必须调用window.location.href.split('#')[0];后端计算签名时,url参数必须来自微信服务器实际收到的请求URL(可通过HttpContext.Request.Headers["Referer"]或记录原始请求日志验证)。在wx.config前加console.log('config url:', url)和后端日志打点对比,是最快定位手段。
5.2 现象:getBrandWCPayRequest:fail invalid args,控制台无其他报错
原因:wx.requestPayment传入的参数中,timeStamp是毫秒时间戳(如1678886400000),但微信要求秒级时间戳(如1678886400)。nonceStr含中文、空格或长度超32位;package值不是prepay_id=xxx格式,而是{"prepay_id":"xxx"}或prepay_id:"xxx"。
解决:前端timeStamp必须用Math.floor(Date.now()/1000);nonceStr用Math.random().toString(36).substr(2, 15)生成后,再replace(/[^a-zA-Z0-9]/g, '')过滤;package必须是纯字符串,用data.package直接传入,不要JSON.stringify。
5.3 现象:支付成功后,用户看到“支付成功”弹窗,但后端notify_url从未收到微信回调
原因:notify_url在微信商户平台配置的域名,与实际部署域名不一致(如配置了https://pay.example.com,但实际是https://www.example.com/pay);或notify_url接口返回非200状态码(如500错误、302跳转、超时);或返回内容不是纯字符串success(如{"result":"success"}或带HTML标签)。
解决:微信回调只认200 OK+ 响应体为纯文本success。在notify_url接口开头加Response.ContentType = "text/plain"; Response.Write("success"); Response.End();,并确保无任何其他输出。用curl模拟微信回调:curl -X POST https://pay.example.com/api/wechat/notify -d "xml=...",检查返回。
5.4 现象:Android手机能正常支付,iOS手机点击支付按钮无反应
原因:iOS Safari对wx.config的nonceStr和timestamp有强缓存,且wx.ready回调在页面DOMContentLoaded后可能延迟触发,导致wx.requestPayment在wx.config未完成时就被调用。
解决:严格按4.3节方案,在wx.ready回调内重新请求JSAPI参数;并在wx.config前加wx.checkJsApi验证requestPayment是否可用:wx.checkJsApi({jsApiList: ['requestPayment'], success: ...})。
5.5 现象:统一下单接口返回{"err_code":"INVALID_REQUEST","err_code_des":"参数格式错误"}
原因:total_fee传了decimal类型(如100.0m)或字符串"100",微信只接受int类型的纯数字;spbill_create_ip传了127.0.0.1或内网IP(如192.168.1.100),微信要求公网IP;body含非法字符(如<,>,&未转义),导致XML解析失败。
解决:total_fee在C#中必须声明为int,赋值用(int)Math.Round(price * 100);spbill_create_ip从HttpContext.Request.UserHostAddress获取,若为内网IP,需通过Nginx的X-Real-IP头获取真实IP;body用System.Net.WebUtility.HtmlEncode()转义。
注意:以上5条,每一条都对应一次线上事故的回滚操作。不要跳过日志验证,
invalid signature90%是URL不一致,invalid args90%是时间戳/nonceStr格式错。把console.log和后端日志打点当成呼吸一样自然。
6. 支付结果验证与异步通知处理:用Redis幂等队列防重复通知、用本地日志做最终兜底
微信支付的异步通知(notify_url)是业务资金安全的生命线。它不可靠:可能延迟、可能重复、可能丢失。一个健壮的系统,必须设计三层防御:1)通知到达时的即时幂等处理;2)定时任务扫描未终态订单;3)人工对账时的本地日志溯源。本章聚焦第一层,用最轻量的Redis实现高并发下的幂等。
6.1 设计幂等通知处理器:Redis SETNX + 过期时间
微信可能在几秒内重复推送同一笔订单的notify,若不做幂等,会导致库存扣减两次、财务记账两次。最佳实践是:收到通知后,先用订单号+notify作为Redis Key,尝试SETNX(set if not exists),成功则处理,失败则直接返回success。
// NotifyController.cs - 异步通知接口 [HttpPost] [Route("notify")] public async Task<IActionResult> Notify([FromBody] string xmlBody) { try { // 1. 解析XML,提取out_trade_no和result_code var notifyData = ParseNotifyXml(xmlBody); if (notifyData == null || string.IsNullOrEmpty(notifyData.OutTradeNo)) { return Content("fail", "text/plain"); } // 2. Redis幂等校验:key = "wechat:notify:" + out_trade_no var redisKey = $"wechat:notify:{notifyData.OutTradeNo}"; var redisValue = Guid.NewGuid().ToString(); // 防止误删 var isLocked = await _redisDatabase.StringSetAsync(redisKey, redisValue, TimeSpan.FromMinutes(30), When.NotExists); if (!isLocked) { // 已处理过,直接返回success return Content("success", "text/plain"); } // 3. 验证通知签名(微信要求必须验签) if (!ValidateNotifySign(notifyData, _config.ApiKey)) { await _redisDatabase.KeyDeleteAsync(redisKey); return Content("fail", "text/plain"); } // 4. 更新订单状态(此处为伪代码,实际调用仓储层) if (notifyData.ResultCode == "SUCCESS") { await _orderService.MarkAsPaidAsync(notifyData.OutTradeNo, notifyData.TransactionId); } else { await _orderService.MarkAsFailedAsync(notifyData.OutTradeNo, notifyData.ErrCodeDes); } return Content("success", "text/plain"); } catch (Exception ex) { _logger.LogError(ex, "微信通知处理异常"); return Content("fail", "text/plain"); } } private bool ValidateNotifySign(Dictionary<string, string> data, string apiKey) { // 1. 移除sign字段 var sign = data["sign"]; data.Remove("sign"); // 2. 按字典序拼接 var sorted = data.OrderBy(kv => kv.Key, StringComparer.Ordinal) .Select(kv => $"{kv.Key}={kv.Value}") .ToArray(); var signString = string.Join("&", sorted) + "&key=" + apiKey; // 3. MD5校验 using (var md5 = MD5.Create()) { var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(signString)); var computedSign = BitConverter <p> <a href="https://download.csdn.net/download/xkylw/9556447" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>