C#微信支付代码实战:统一下单、回调验签与APIv3避坑指南

发布时间:2026/10/12 0:40:06
C#微信支付代码实战:统一下单、回调验签与APIv3避坑指南
简介这份资源面向需要在C#项目中集成微信支付能力的开发者尤其适合已有一定.NET基础、希望快速跑通支付全流程的中级程序员。内容围绕微信支付API的C#实现展开覆盖统一下单、生成支付二维码、H5支付、订单查询、异步回调通知、退款处理以及错误码与异常处理等核心环节并涉及HTTP请求、XML解析、签名算法与异步编程等配套技术。压缩包共104个文件以57个cs源码为主辅以12个aspx页面、8个dll类库、5个config配置及js、xml等整体约2.15MB目录结构清晰便于按模块查阅。目前已有456人学习下载。借助其中的SDK类库与示例页面读者可对照理解预支付交易创建、回调验签与订单状态更新等关键逻辑快速搭建可调试的支付环境减少从文档到落地之间的摸索成本。1. C#微信支付代码从统一下单到回调验签一套能跑通的落地路径做过微信支付对接的C#开发者大概都有同感官方文档给的是HTTP接口说明示例代码散落在各个语言片段里真正落到.NET项目里从签名拼接到回调验签中间全是需要自己填的坑。所谓C#微信支付代码核心要解决的就是三件事——怎么在服务端发起统一下单拿到预支付标识、怎么处理微信异步回调并验证签名、怎么保证金额和订单号不被篡改。这套逻辑在ASP.NET Core Web API、MVC甚至WinForm后台里都通用区别只在HttpClient的注入方式和配置读取。适合谁看正在做JSAPI、Native扫码或H5支付的后端开发者手里有商户号和API密钥但卡在签名报错或回调收不到。下面按我实际落地的顺序拆开讲参数怎么设、坑在哪都会给到。2. 先把签名和统一下单跑通C#里最容易翻车的两个环节微信支付V2接口的签名机制是整条链路的地基签名错了后面全是401或签名错误。很多人第一次对接时把参数排序、拼接、加密这三步搞混导致返回签名错误却不知道错在哪。这一章先把签名算法和统一下单请求写清楚让最小链路能返回prepay_id。2.1 签名算法的参数排序与MD5拼接细节V2签名要求将所有非空参数按字段名ASCII码从小到大排序拼接成keyvaluekeyvalue格式末尾拼上keyAPI密钥再做MD5大写。注意三个点参数值为空不参与签名、sign字段本身不参与、编码统一用UTF-8。下面是我常用的签名方法。using System.Security.Cryptography; using System.Text; public static class WxPaySign { // 生成V2签名参数为字典apiKey为商户平台设置的32位密钥 public static string MakeSign(Dictionarystring, string paras, string apiKey) { // 1. 过滤空值并剔除sign字段 var filtered paras .Where(p !string.IsNullOrEmpty(p.Value) p.Key ! sign) .OrderBy(p p.Key, StringComparer.Ordinal) // ASCII升序 .ToList(); // 2. 拼接成 keyvaluekeyvalue var sb new StringBuilder(); foreach (var p in filtered) { sb.Append(p.Key).Append().Append(p.Value).Append(); } sb.Append(key).Append(apiKey); // 末尾拼API密钥 // 3. MD5加密并转大写 using var md5 MD5.Create(); var bytes md5.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString())); var sign new StringBuilder(); foreach (var b in bytes) sign.Append(b.ToString(X2)); return sign.ToString(); } }逻辑说明StringComparer.Ordinal保证按ASCII码排序不能用默认的culture排序否则大小写字母顺序会错。参数值里如果本身带或微信要求原样拼接不转义这点和URL编码是两回事。参数说明apiKey是商户平台APIv2密钥32位字符串不是APIv3的密钥两者不通用。常见翻车点是密钥复制时带了空格或者用了APIv3密钥去签V2接口返回必然是签名错误。2.2 统一下单请求的构造与HttpClient调用拿到签名后把参数转成XML发给统一下单接口。V2接口收XML、返回XML虽然现在V3用JSON但存量项目里V2仍大量存在。下面用HttpClient发请求注意Content-Type必须是text/xml。public async Taskstring UnifiedOrderAsync(Dictionarystring, string paras, string apiKey) { // 补充必填参数 paras[appid] 你的appid; paras[mch_id] 你的商户号; paras[nonce_str] Guid.NewGuid().ToString(N); // 随机字符串 paras[sign_type] MD5; paras[sign] WxPaySign.MakeSign(paras, apiKey); // 字典转XML var xml new StringBuilder(xml); foreach (var p in paras) xml.Append(${p.Key}{p.Value}/{p.Key}); xml.Append(/xml); using var client new HttpClient(); var content new StringContent(xml.ToString(), Encoding.UTF8, text/xml); var resp await client.PostAsync(https://api.mch.weixin.qq.com/pay/unifiedorder, content); return await resp.Content.ReadAsStringAsync(); }逻辑说明nonce_str每次请求都要重新生成不能复用。sign必须在所有参数补齐后最后计算。参数说明trade_type决定支付方式JSAPI要传openidNATIVE返回二维码链接H5返回跳转URL。返回的XML里return_code和result_code都为SUCCESS时才有prepay_id。坑在于XML里如果参数值含特殊字符没做CDATA包裹微信解析会失败稳妥做法是对可能含特殊字符的字段用![CDATA[...]]包起来。3. 回调验签与订单状态处理别让假通知把订单改了支付成功后微信会异步POST通知到你的notify_url这一步如果验签不严别人伪造通知就能把你的订单改成已支付。我见过有项目直接读out_trade_no就发货结果被刷。这一章讲回调怎么验签、怎么应答、怎么防重复处理。3.1 异步通知的验签流程与XML解析回调收到的是XML先解析出参数剔除sign后重新计算签名和回调里的sign比对。一致才认为是微信发的。下面给出解析和验签的完整方法。public bool VerifyNotify(string xmlBody, string apiKey, out Dictionarystring, string paras) { paras new Dictionarystring, string(); var doc new XmlDocument(); doc.LoadXml(xmlBody); var root doc.DocumentElement; foreach (XmlNode node in root.ChildNodes) paras[node.Name] node.InnerText; if (!paras.ContainsKey(sign)) return false; var remoteSign paras[sign]; var localSign WxPaySign.MakeSign(paras, apiKey); return string.Equals(remoteSign, localSign, StringComparison.OrdinalIgnoreCase); }逻辑说明解析时用InnerText能自动处理CDATA。验签前不能修改paras里的任何值否则签名对不上。参数说明回调里的result_code为SUCCESS且return_code为SUCCESS才算支付成功但真正判断发货依据应该是trade_state查询结果回调只作触发。注意验签通过后还要校验total_fee和out_trade_no是否和你本地订单一致防止金额被改。3.2 应答微信的XML格式与幂等处理微信要求收到通知后返回特定XML否则会重复通知。返回格式不对或超时微信会按策略重试可能造成重复发货。所以幂等必须做。public string BuildNotifyResponse(bool success, string msg ) { var sb new StringBuilder(xml); sb.Append(return_code![CDATA[).Append(success ? SUCCESS : FAIL).Append(]]/return_code); sb.Append(return_msg![CDATA[).Append(msg).Append(]]/return_msg); sb.Append(/xml); return sb.ToString(); }逻辑说明只要业务处理成功就返回SUCCESS微信不再重试。参数说明return_msg可空但建议填。幂等做法是在处理前用out_trade_no查本地订单状态已处理直接返回SUCCESS。坑在于有些项目在验签前就返回SUCCESS等于告诉微信收到了但没验签攻击者伪造通知也能触发。正确顺序是验签→校验金额订单→幂等判断→业务处理→返回SUCCESS。4. 避坑与排查签名错误、回调收不到、金额对不上这一章集中讲我踩过的几类问题按现象、原因、解决来写遇到时可以直接对照。4.1 签名错误的三种典型原因现象统一下单返回return_codeFAIL/return_codereturn_msg签名错误/return_msg。原因一参数排序用了默认排序大小写字母顺序错。解决改用StringComparer.Ordinal。原因二API密钥填错比如用了APIv3密钥或复制时带空格。解决到商户平台核对APIv2密钥重新复制。原因三参数值本身含中文没做UTF-8编码或XML里没CDATA。解决统一UTF-8特殊字段加CDATA。4.2 回调收不到或重复收到现象支付成功但本地订单一直待支付或同一订单被处理多次。原因notify_url外网不可达或返回格式不对导致微信重试。解决先用工具确认notify_url能被公网POST到返回体严格按微信格式且处理逻辑做幂等。注意notify_url不能带参数必须是纯路径。4.3 金额单位与订单号长度现象金额对不上或下单报参数错误。原因微信金额单位是分不是元传了小数会报错。解决本地存分展示时再除100。订单号out_trade_no要求32字符以内且同一商户号下不能重复。解决用时间戳加随机数生成长度控制在32内。4.4 证书与退款接口的额外要求现象退款报证书错误。原因退款接口需要双向证书不是普通HTTPS请求。解决在商户平台下载apiclient_cert.p12用HttpClientHandler加载证书。注意证书密码默认是商户号加载时要用X509Certificate2带密码构造。5. 进阶用APIv3和平台证书把安全等级提上去V2接口虽然能跑但MD5签名和XML传输在安全上已经偏弱新项目我一般直接上APIv3。V3用JSON、SHA256-RSA签名、平台证书验签流程更规范但初次接入门槛高。这一章给一个最小可用的V3请求思路和验证方法。5.1 APIv3的签名构造与Authorization头V3签名串格式是HTTP方法\nURL路径\n时间戳\n随机串\n请求体\n用商户私钥做SHA256withRSA签名再拼成Authorization头。下面给出签名核心。public string BuildAuthorization(string method, string urlPath, string body, string mchId, string serialNo, RSA privateKey) { var timestamp DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); var nonce Guid.NewGuid().ToString(N); var message ${method}\n{urlPath}\n{timestamp}\n{nonce}\n{body}\n; var data Encoding.UTF8.GetBytes(message); var sign privateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var signature Convert.ToBase64String(sign); return $WECHATPAY2-SHA256-RSA2048 mchid\{mchId}\,nonce_str\{nonce}\,timestamp\{timestamp}\,serial_no\{serialNo}\,signature\{signature}\; }逻辑说明urlPath要带query stringbody是原始JSON字符串不能重新序列化。参数说明serialNo是商户证书序列号privateKey从apiclient_key.pem加载。注意时间戳和服务器时间偏差不能超过5分钟否则报签名过期。5.2 平台证书验签与回调解密V3回调是加密的需要用APIv3密钥做AES-256-GCM解密再用平台证书验签。验证方法先调/v3/certificates下载平台证书缓存起来回调时用对应serial解密。解密后的明文里out_trade_no和transaction_id才是可信数据。我一般会把平台证书按序列号存到内存缓存定期刷新。这一步比V2复杂但一旦跑通安全性和可维护性都上一个台阶。5.3 一个验证签名是否正确的笨办法如果你不确定签名对不对最直接的办法是拿官方文档里的示例参数和密钥用你的签名方法算一遍和文档给的签名值比对。一致说明算法没问题不一致就逐字段排查排序和拼接。这个办法我用了很多次比反复调接口快。另外把每次请求的签名串和签名值打到日志里出问题时能直接看出是哪个参数变了。希望帮到你。本文还有配套的精品资源点击获取