
简介面向使用C#与.NET平台的开发者提供一套可直接参考的微信支付集成封装源码覆盖二维码扫码支付、APP内发起支付等高频场景适用于需要快速接入微信支付接口的Web站点、移动端后台或企业内部系统。整个压缩包包含151个文件大小约3.04MB其中C#源代码文件有71个是资源主体其余还包括XML配置、DLL依赖库、NuGet打包文件、MVC视图cshtml以及Web.config等分别承担数据配置、依赖引用、界面展示和运行环境设置等职责整体覆盖从接口签名、下单请求到支付结果回调的完整流程。代码内WxPayAPI类负责支付核心逻辑WeixinExecutor类处理微信接口执行与异常Global.asax与MVC视图页面则展现出基于ASP.NET的宿主与展示层结构便于理解整个支付模块的交互关系。已有3202人学习下载适合初中级.NET开发者在真实项目中参考签名规则、参数组装、回调验签等细节也可直接作为现有支付模块的改造样板减少重复踩坑成本。1. C#.NET 微信支付封装源码被低估的工程量搜C# .NET 微信支付封装源码的人多半不是缺官方文档而是被v3接口的签名算法、平台证书轮换、回调AES-256-GCM解密这套组合拳折腾过。这套封装源码的价值不在能发一个POST请求而是把微信支付的认证模型、异常码映射、退款幂等、投诉回调和对账单解析这些高频重复劳动收敛成可维护的组件让业务代码里不再出现裸的HttpClient。注意这里的封装不是面向对象三大特性里的抽象封装而是把协议细节收敛进一层方便整体替换的SDK代码。内容按认证骨架 → 主流程 → 周边接口 → 上线检查展开适合正在自建商户系统的服务端工程师也适合想评估现成库选型的人。2. 签名、证书与请求层微信支付封装源码的骨架微信支付API v3的认证不是简单的token而是商户私钥签名 平台证书验签的双向通道外加一把APIv3密钥专门做回调数据解密。封装的第一步是把这三套密钥的职责分清楚再决定各自在代码里的存放位置和加载方式。2.1 微信支付v3的认证流程与封装切入点每次请求要带一个Authorization头格式如下WECHATPAY2-SHA256-RSA2048 mchid1900000109,nonce_stra6b6c8d8,timestamp1700000000,serial_no45B2D8A1...,signaturebase64签名串其中signature是对下面这串明文做RSA-SHA256签名得到的POST\n/v3/pay/transactions/native\n1700000000\na6b6c8d8\n{请求体原文}\n五个部分之间用换行符\n连接HTTP方法、带查询串的完整路径、10位时间戳、随机串、请求体原文。GET请求的请求体为空但末尾的\n不能少这是接入初期最常见的签名失败原因。字段来源作用mchid商户号标识调用方身份serial_no商户API证书序列号让微信侧找到对应公钥timestamp当前Unix时间戳防重放与微信服务器时间差超过5分钟会被拒nonce_str随机生成防重放signature商户私钥签名请求完整性证明封装的第一个切入点就是把上面这串逻辑收成一个SignAndSend(method, url, body)方法业务代码只传业务参数不接触任何与协议有关的拼接。认证细节和业务参数分离是这份封装源码最核心的代码组织原则。2.2 用C#定义商户配置类与请求客户端先有一个不会散落在业务代码里的配置对象public class WechatPayOptions { public string AppId { get; set; } // 公众号/小程序/APP的AppId public string MchId { get; set; } // 商户号 public string ApiV3Key { get; set; } // 32字节的APIv3密钥仅用于回调解密 public string MerchantSerialNo { get; set; } // 商户API证书序列号 public string MerchantPrivateKeyPem { get; set; } // apiclient_key.pem 的文本内容 public string PlatformCertificatePem { get; set; }// 平台证书公钥验回调签名用 }三个容易混的点ApiV3Key是商户平台自己设置的32字节字符串不是证书私钥MerchantSerialNo是apiclient_cert.pem的序列号不是证书文件路径PlatformCertificatePem必须用微信签发的平台证书来验回调签名拿商户证书验自己的回调会报验签失败。证书序列号不用写代码openssl一行就能查把输出的大写十六进制串去掉冒号后填进MerchantSerialNoopenssl x509 -in apiclient_cert.pem -noout -serial配置类在WechatPayClient构造函数里做一次有效性校验实例化后不允许修改避免多租户场景下某次误改密钥导致线上串号。2.3 RSA-SHA256签名与平台证书验签的封装实现签名和验签用.NET内置的System.Security.Cryptography即可不依赖第三方库using System.Security.Cryptography; using System.Text; public static class WechatSigner { public static string Sign(string method, string url, string timestamp, string nonce, string body, RSA privateKey) { var message ${method}\n{url}\n{timestamp}\n{nonce}\n{body}\n; var sig privateKey.SignData( Encoding.UTF8.GetBytes(message), HashAlgorithmName.SHA256, RSASignaturePadding.PKCS1); return Convert.ToBase64String(sig); } public static bool Verify(string message, string signatureBase64, RSA publicKey) { var sig Convert.FromBase64String(signatureBase64); return publicKey.VerifyData( Encoding.UTF8.GetBytes(message), sig, HashAlgorithmName.SHA256, RSASignaturePadding.PKCS1); } }SignData和VerifyData的填充方式必须是PKCS1用PSS会报验签失败这点在v3协议里写死封装时不要做成可配置项。私钥加载用RSA.Create()后调用ImportFromPem(pem)该方法要求PKCS#8格式商户平台下载的apiclient_key.pem如果是PKCS#1先转一次格式再交给代码openssl pkcs8 -topk8 -inform PEM -in apiclient_key.pem -outform PEM -nocrypt -out apiclient_key_pkcs8.pem提示.NET Framework项目没有ImportFromPem封装层要么升级到.NET 6要么用BouncyCastle读私钥。源码做选型时要把运行框架写进README避免接入方拿.NET Framework的包编译不过。3. 用这套封装跑通Native下单与回调解密Native支付是PC扫码场景最常见的接入方式一次完整流程包含后端下单拿到code_url、前端生成二维码、用户扫码支付、微信异步回调通知结果。封装要覆盖的核心动作是两个下单和接回调其余都是围绕这两个动作的参数变体。3.1 Native下单的最小调用与code_url处理下单接口是POST /v3/pay/transactions/native请求体关键字段如下var body new { appid opts.AppId, mchid opts.MchId, description 订单20250101001, out_trade_no 20250101001, notify_url https://api.example.com/wxpay/notify, time_expire DateTimeOffset.Now.AddMinutes(30) .ToString(yyyy-MM-ddTHH:mm:sszzz), amount new { total 100, currency CNY } }; var response await client.ExecuteJsonAsync( POST, /v3/pay/transactions/native, body); string codeUrl response.code_url?.ToString();total单位是分100表示1元微信支付的支付金额不支持小数Decimal必须先换算成Int32再传否则序列化后会被网关拒绝。time_expire建议显式设置不传时默认有效期是2小时库存类订单支付窗口太长容易超卖。notify_url必须是公网可访问的HTTPS地址微信侧固定走443端口。ExecuteJsonAsync内部完成时间戳生成、随机串生成、签名、发送以及非2xx状态时的异常抛出。返回的code_url有效期2小时交给前端qrcode类库渲染即可。JSAPI支付和Native的差别只在请求体多一个payer.openid封装里把这两种下单方式收敛为同一个方法的不同参数对象业务侧不用关心URL差异。同商户订单号重复下单会报OUT_TRADE_NO_USED封装里通常附带一个以out_trade_no为键的请求去重缓存避免并发点击下单按钮打穿网关。3.2 回调通知的验签与AES-256-GCM解密支付完成后微信把结果POST到notify_url请求头带Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Serial请求体是加密资源。先验签再解密顺序不能反public static string DecryptResource( NotificationResource resource, string apiV3Key) { var key Encoding.UTF8.GetBytes(apiV3Key); // 必须32字节 var nonce Encoding.UTF8.GetBytes(resource.Nonce); var authData Encoding.UTF8.GetBytes(resource.AssociatedData); var cipher Convert.FromBase64String(resource.Ciphertext); var plain new byte[cipher.Length - 16]; using var aes new AesGcm(key, 16); // 认证标签16字节 aes.Decrypt( nonce, cipher.AsSpan(0, cipher.Length - 16), // 密文部分 cipher.AsSpan(cipher.Length - 16), // 尾部16字节是tag plain, authData); return Encoding.UTF8.GetString(plain); }解密后的JSON包含out_trade_no、transaction_id、trade_state、amount.total等字段。两个高频坑一是老.NET Framework项目没有内置AesGcm需要换库或自己按测试向量验证二是微信返回的Ciphertext是密文16字节tag拼在一起的直接整个丢给Decrypt会抛CryptographicException必须先把尾部16字节切出来当tag参数。验签的载荷拼接格式是时间戳\n随机串\n响应体原文\n响应体原文就是收到的加密JSON不是解密后的明文。请求头的Wechatpay-Serial要和本地平台证书序列号比对不一致时先调/v3/certificates拉新证书再验否则证书轮换当天回调会全部失败。3.3 回调应答与订单状态幂等处理完业务后响应必须是特定结构微信才不再重推// 处理成功 return new JsonResult(new { code SUCCESS, message 成功 }); // 处理失败微信会按递增间隔重试 return new JsonResult(new { code FAIL, message 订单不存在 });应答体必须含code和message两个字段且HTTP状态码需要是200。返回非200或直接抛异常微信会按递增间隔重试最多24次。回调处理逻辑必须幂等先查本地订单trade_state已经是SUCCESS就直接应答成功否则重复入账。trade_state常见取值如下trade_state含义业务动作SUCCESS支付成功更新订单、触发发货REFUND转入退款只记录流水不发货NOTPAY未支付保持等待状态CLOSED已关闭释放库存REVOKED已撤销按未支付处理PAYERROR支付失败允许用户重试封装里把查单-更新-应答包成一个事务模板业务方只注册订单状态变更的回调方法比每个业务方各写一遍查单逻辑少出错。4. 封装要补的周边退款、投诉回调与对账单很多开源封装源码只覆盖支付主链路退款、投诉、对账这三件事才是拉开差距的部分。它们复用同一套签名和HttpClient但各有各的参数约束和幂等要求。4.1 退款接口的幂等键与金额校验退款接口是POST /v3/refund/domestic/refunds关键参数如下var refundBody new { out_trade_no 20250101001, // 原商户订单号 out_refund_no R20250101001001, // 退款单号唯一幂等键 amount new { refund 50, // 退款金额单位分 total 100, // 原订单总额单位分 currency CNY } }; var resp await client.ExecuteJsonAsync( POST, /v3/refund/domestic/refunds, refundBody);out_trade_no和transaction_id二选一传都传时微信优先使用transaction_id。out_refund_no是退款幂等键同一个退款单号重复提交微信返回原退款单而不是新建封装里的重试逻辑要利用这一点。退款金额不能大于原订单实付金额多次部分退款时累计退款额也不能超过total对账时要把REFUND和SUCCESS两种状态分开统计。退款结果通过独立的异步通知推送字段比支付通知多了refund_status和user_received_account解密逻辑直接复用3.2节的DecryptResource不要另写一套GCM。4.2 投诉回调的接入与状态机投诉回调是商户平台开启能力后才会触发的推送用户在支付遇到问题发起投诉时微信实时通知商户。通知里带complaint_id和complaint_state先解密拿到ID再拉详情var detail await client.ExecuteJsonAsync(GET, $/v3/merchant-service/complaints-v2/{complaintId}, null);complaint_state是一个三态状态机complaint_state含义下一步动作PENDING用户投诉待处理查询详情主动联系用户PROCESSING处理中继续跟进不要重复完结PROCESSED已完结归档做满意率统计商户给出解决方案后调POST /v3/merchant-service/complaints-v2/{id}/complete完结投诉。封装一般把状态流转包进ComplaintService业务侧不直接拼URL同时把完结后再次收到同ID投诉的重复推送幂等掉避免一个投诉被处理两次。4.3 对账单下载的流式读取与解析对账单接口GET /v3/bill/tradebill传bill_date和bill_type响应体里不是账单本身而是download_url。账单文件以反引号做分隔每行以\n结尾且第一列前会多一个分隔符。整个文件读进内存再Split文件大时容易撑爆进程我一般用流式读取using var stream await client.GetStreamAsync(downloadUrl); using var reader new StreamReader(stream, Encoding.UTF8); await reader.ReadLineAsync(); // 跳过表头 string line; while ((line await reader.ReadLineAsync()) ! null) { line line.TrimStart(); // 去掉行首多余分隔符 var fields line.Split(); // fields[0] 交易时间fields[4] 商户订单号fields[6] 金额 }账单里每笔记录有SUCCESS、REFUND、REVOKED等状态对账时用商户订单号关联本地流水任何一端有记录而另一端没有的都进差异队列第二天人工复核。封装里把这块做成BillParser内部处理行尾\r\n兼容和转义业务侧只接收IEnumerableBillRecord。5. 封装源码跑上线前要过的三道检查最后说三个上线前必查的点都是支付类项目里最疼的线上问题。5.1 用日志还原签名失败的现场微信返回SIGN_ERROR时不要猜。把签名串和Authorization头原样打出来人工还原一遍拼接过程logger.LogInformation(签名串{Message}, signMessage.Replace(\n, \\n)); logger.LogInformation(Authorization{Auth}, authHeader);重点看三处时间戳与服务器当前时间的差值超过5分钟会直接报错查询类请求的签名URL是否带上了%2F这类URL编码残留查单时路径必须用原始路径serial_no是否和私钥来自同一个证书文件证书轮换时最容易出私钥和序列号不配对的问题。5.2 HttpClient的复用与超时参数设置封装里每次请求new一个HttpClient高并发下会把连接池打满表现为偶发超时。注册为单例并把连接超时和总超时分开控制services.AddHttpClient(wechatpay, c { c.Timeout TimeSpan.FromSeconds(15); }).ConfigurePrimaryHttpMessageHandler(() new HttpClientHandler { AllowAutoRedirect false, // 对账单download_url不需要自动跳转 MaxConnectionsPerServer 50 });重试只对429、502、504做400、401这类参数或签名错误重试没有意义还会放大日志噪音。回调解密失败时不要立刻回500先记录原始报文再决定是否重试因为微信重推的是同一份密文代码没修好之前反复解密只会反复失败。本文还有配套的精品资源点击获取