简介一套基于C#语言、面向.NET平台的微信支付封装源码覆盖二维码扫码支付、APP内发起支付等高频场景目标是让.NET开发者避开繁琐的官方接口对接流程直接通过封装好的类与方法完成接入适合正在做商城、预约系统或移动端应用的工程师参考。压缩包共151个文件其中71个cs源码是核心包含支付请求、回调处理、签名工具等逻辑15个xml用于存放配置与注释14个dll提供运行依赖10个nuspec与10个nupkg支撑NuGet包管理另有cshtml示例视图、config配置文件、样式脚本等整体仅3.04MB结构紧凑、层级清晰。目前已有3202人参与学习。翻阅源码可完整看到扫码支付与APP支付的实现思路包括统一下单、回调验签、订单查询、证书与异步通知等关键环节配套的配置文件与示例页面也能减少重复搭建成本帮助开发者快速迁移到实际项目遇到支付签名错误、回调地址配置等问题时也能更快定位原因。1. 当扫码支付和 APP 支付挤在同一个支付服务里封装源码要怎么拆一个电商项目同时要支持 PC 端扫码支付和 App 内唤起微信支付两个入口共用同一个订单中心但微信官方文档把 Native 和 App 的参数、签名时序拆成两套说明。官方 Demo 的问题也很明显请求发送、响应解析、证书加载全堆在几个类里业务代码被 XML 字符串塞满想加一个字段都要翻半天。这套 C# 源码包的思路明确WxPayAPI.cs 收敛统一下单、签名、回调验签WeixinExecutor.cs 做交易类型分发Global.asax 和 Web.config 负责初始化和多环境切换。适合正在接手微信支付的 .NET 工程师也适合准备把支付代码收敛到服务层的老项目。下面按一次真实支付的流转链路拆开讲。2. 微信支付接口调用链与 WxPayAPI 的签名封装原理2.1 一个支付请求从入口到微信接口的完整流转路径微信支付的每次业务请求都要经历“业务参数组装 → 按字典序拼接 → 加签 → 请求微信 API → 解析 XML/JSON 响应 → 验签”这六个环节。这个源码包把前四个环节收进 WxPayAPI.cs后两个环节按场景拆到不同方法。文件列表里的 Global.asax 和 Web.config 不参与协议但决定了运行时配置从哪来这是很多 .NET 开发者忽略的部分支付网关地址、证书路径、API Key 都应该在应用启动时加载而不是散落在业务方法里。从调用链看Native 扫码支付和 App 支付共用统一下单接口区别只在最终请求参数中 trade_type 和 product_id 不同。统一下单成功后会返回 prepay_idNative 流程直接从响应里取 code_url 生成二维码App 流程则需要对 prepay_id 做二次签名再把签名参数返回给客户端。WeixinExecutor.cs 在这里充当路由层它根据请求体里的支付场景决定走哪条分支。这里我一般会强调不能把微信支付请求和普通 HTTP 请求混在一起。微信支付对参数顺序、编码和空值过滤有严格要求任何额外的字段都会导致签名不一致。因此封装源码使用 SortedDictionary 而不是 Dictionary 来保存请求参数这是第一个值得留意的实现细节。2.2 参数签名MD5 与 HMAC-SHA256 的差异微信支付 API v2 的签名规则是所有请求参数sign 本身除外值不为空的键值对按参数名 ASCII 码升序排列拼接成key1value1key2value2的形式末尾追加key商户API密钥对拼接结果做摘要并转大写。当前微信支付商户平台主要支持 MD5 和 HMAC-SHA256 两种摘要算法。MD5 输出 32 位大写字符HMAC-SHA256 输出 64 位大写字符。两者混用的风险在于统一下单时用 HMAC-SHA256 签名但回调通知验签时使用了默认 MD5结果就是回调一直验签失败。这个源码包的解决方案是在配置项里维护一份 SignType所有签名和验签入口都读取同一个配置。// 微信支付请求参数签名生成 public static string MakeSign(SortedDictionarystring, object parameters, string apiKey, string signType) { var sb new StringBuilder(); foreach (var param in parameters) { string value param.Value?.ToString(); if (param.Key sign || string.IsNullOrEmpty(value)) { continue; // 过滤空值与 sign 字段 } sb.Append(${param.Key}{value}); } sb.Append(key).Append(apiKey); // 末尾拼接商户 API Key byte[] contentBytes Encoding.UTF8.GetBytes(sb.ToString()); if (signType HMAC-SHA256) { using (var hmac new HMACSHA256(Encoding.UTF8.GetBytes(apiKey))) { byte[] hash hmac.ComputeHash(contentBytes); return BitConverter.ToString(hash).Replace(-, ).ToUpper(); } } using (var md5 MD5.Create()) { byte[] hash md5.ComputeHash(contentBytes); return BitConverter.ToString(hash).Replace(-, ).ToUpper(); } }这段代码的逻辑是先用 SortedDictionary 保证参数名升序接着跳过 sign 本身和值为空的字段最终拼出待签名字符串。HMAC-SHA256 分支需要额外注意HMAC 的密钥也是 apiKey因此我在 using 块里重复读取 apiKey 字节避免外部对密钥做不必要的编码转换。MD5 分支则是微信支付 v2 最常见的默认算法。使用这段代码时有一个典型的误用很多同学会先把参数放进 Dictionary再手动调用 OrderBy 排序最后转换为 string 拼接——这没问题但一旦漏掉值为 null 的字段签名结果就和你自己在微信商户平台调试工具里得到的值不一致。直接把参数放进 SortedDictionary 并在循环里过滤能少犯这类错误。2.3 统一下单参数对照与选型说明统一下单是 Native、App、JSAPI、MWEB 四种支付方式共同的第一步因此封装源码里的 UnifiedOrder 方法只接收一个 WxPayData 对象不把每种支付方式单独开方法。这里给出该源码包中最常用的字段说明。协议参数名对应封装属性/字段必填值说明appidAppId是微信开放平台或公众号的 AppIDmch_idMchId是微信支付商户号out_trade_noOutTradeNo是商户订单号建议字母数字组合长度不超过 32bodyBody是商品描述会展示在用户账单total_feeTotalFee是支付金额单位为分不能带小数点spbill_create_ipSpbillCreateIp是用户终端 IPApp 支付填客户端公网 IPNative 填服务器出口 IPnotify_urlNotifyUrl是接收支付结果回调的地址必须公网可访问trade_typeTradeType是NATIVE、APP、JSAPI、MWEB 之一product_idProductId否Native 支付必填用于扫码时映射商品openidOpenId否JSAPI 支付必填上表里最容易忽略的是 total_fee 的单位。接口收的是“分”但业务库常用“元”存储封装源码在构建 WxPayData 时没有自动做单位换算因为自动换算会导致调用方对精度失控。我习惯在业务 Service 层统一把元转换为分后再传给 Executor这样支付模块保持无状态也方便单元测试。另外spbill_create_ip这个参数在 App 支付时不能填服务器的内网 IP否则部分风控策略会直接拦截。Native 支付则可以填服务器公网 IP因为扫码请求实际由微信服务器发起终端 IP 对微信不可见。封装源码把该字段设计成必填就是为了逼调用方显式传入 IP而不是包一层默认值。3. WxPayAPI 中统一下单、二维码扫码支付与 APP 支付的实现3.1 Native 扫码支付code_url 的生成与二维码渲染解耦Native 支付的完整流程是服务端调用统一下单trade_type 为 NATIVEproduct_id 为商品标识微信返回 code_url这是一个专门用于生成二维码的链接用户扫码后微信内部把 code_url 转换成一笔授权支付请求。封装源码在 WxPayAPI.UnifiedOrder 返回的 WxPayData 里直接暴露 GetValue(code_url) 方法把二维码渲染交给前端或服务端图形库不在支付模块里引入二维码库。这样设计的好处是支付 API 和 UI 层彻底分离。如果项目用的是 Vue/React前端拿 code_url 调用 qrcode.js 即可如果服务端需要直接输出图片常见做法是在 Controller 里用 QRCoder 把 code_url 转成 Bitmap再以 image/png 响应。封装源码不掺和这一步避免为了支付引入一个只服务二维码的依赖。示例代码给出一个 Native 下单方法的完整写法public WxPayData NativeOrder(string orderNo, string body, int totalFee, string productId, string notifyUrl, string ip) { var req new WxPayData(); req.SetValue(out_trade_no, orderNo); req.SetValue(body, body); req.SetValue(total_fee, totalFee); req.SetValue(product_id, productId); req.SetValue(notify_url, notifyUrl); req.SetValue(spbill_create_ip, ip); req.SetValue(trade_type, NATIVE); return WxPayAPI.UnifiedOrder(req); }注意这里没有单独传 appid 和 mch_id因为 WxPayAPI.UnifiedOrder 内部会从全局支付配置读取。这样封装既方便普通场景也不会让每次调用都被一堆固定参数填满。当你需要同时服务多个商户号时再给方法增加一个 config 参数即可源码包预留了这样的扩展面。拿到 code_url 后如果扫码后一直提示“找不到商品”优先检查 product_id 是否为空。很多 .NET 项目在数据库里存的是 long 型商品主键序列化成 JSON 后变成字符串前端再传回来时可能被截断导致 product_id 和统一下单时不一致。建议 product_id 统一用稳定字符串不要依赖数据库自增主键。3.2 App 支付prepay_id 生成后进行第二次签名App 支付里“统一下单 → 取 prepay_id → 生成客户端参数”这三步是连续动作。拿到 prepay_id 后不能直接返回给客户端因为客户端调起微信 SDK 时还需要 appid、partnerid、prepayid、package、timestamp、noncestr 以及由这些参数生成的 sign。顺序错了或者参数名大小写错了客户端就会报签名错误。源码包里提供的方法通常命名为 GetAppPayParams内部封装第二次签名逻辑。下面是我基于该源码整理的一个版本public Dictionarystring, string BuildAppPayParams(WxPayData unifiedOrderResult, string appId, string mchId, string apiKey) { string prepayId unifiedOrderResult.GetValue(prepay_id).ToString(); var parameters new SortedDictionarystring, object { [appid] appId, [partnerid] mchId, [prepayid] prepayId, [package] SignWXPay, [timestamp] DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), [noncestr] Guid.NewGuid().ToString(N) }; string sign MakeSign(parameters, apiKey, MD5); parameters.Add(sign, sign); return parameters.ToDictionary(kv kv.Key, kv kv.Value.ToString()); }这段代码有几个关键点timestamp 使用的是 Unix 秒级时间戳C# 开发者最容易写成DateTime.Now.ToString(yyyyMMddHHmmss)微信 SDK 要求的是秒级时间戳格式错误会直接导致调不起支付页面。noncestr 是随机字符串这里用 Guid 去掉连字符后的 32 位能满足长度要求但要注意 Guid 的随机性足够但牺牲一点性能高频调用可以用 RNGCryptoServiceProvider 生成随机字符串。package 固定为 SignWXPay不能改成其他值。签名算法在这里统一使用 MD5如果统一下单时用的 HMAC-SHA256这里需要把参数扩展到包含sign_typeHMAC-SHA256后再计算。这块必须根据商户平台实际设置调整不能想当然。最后返回 Dictionarystring, string 而不是 JObject是因为 Unity、iOS、安卓的对接层分别有自己的 JSON 解析方式服务端只提供扁平字典由客户端自己序列化成各自需要的数据结构反而减少跨端格式冲突。3.3 支付结果回调验签、金额核对与投诉回调分流支付回调是风险最高的一环微信服务器在订单支付完成后多次 POST 通知到 notify_url。封装源码处理的第一个动作是读取原始 XML 报文并验签验签通过后再反序列化业务字段。第二个动作是核对金额和订单状态除了 out_trade_no 必须存在还要把微信返回的 total_fee 和本地订单金额做精确比较这一点官方文档没有强制要求但业务上不做就是资损漏洞。验签示例public bool VerifyNotify(string xml, string apiKey, string signType) { var data WxPayData.FromXml(xml); string receivedSign data.GetValue(sign)?.ToString(); data.Remove(sign); string calculatedSign MakeSign(data.ToSortedDictionary(), apiKey, signType); return string.Equals(receivedSign, calculatedSign, StringComparison.OrdinalIgnoreCase); }这个方法的执行顺序是先把 XML 解析成 WxPayData取出 sign 字段后立刻从集合中移除再用剩余字段计算签名最后统一转大写比较。比较时使用 OrdinalIgnoreCase 而不是 ToUpper可以避免某些语言环境下的土耳其文化问题虽然微信返回的是大写防御性写法并不多余。验签通过后封装源码返回给微信服务器的响应必须是纯文本SUCCESS如果业务处理失败必须返回FAIL或非 SUCCESS 文本微信会按一定周期重试。这里有个源码包没有直接处理的边界数据库更新成功但响应超时微信会立刻重试导致回调幂等逻辑必须存在。我一般会在 Executor 里加入“订单状态检查”或“唯一键去重表”确保同一笔订单被重复通知时不会二次退款或二次发货。注意验证回调签名和业务处理必须放在支持重试的流程里生产环境对同一通知至少保证一次幂等处理。此外微信支付投诉回调是另一种独立的回调它和支付结果回调共用同一个通知入口时接口字段完全不一样。投诉回调中出现的字段是 complaint_id、complaint_time、amount 等事件类型由 msg_type 区分。源码包里没有单独实现投诉处理器扩展时建议在 WeixinExecutor 中增加一个专门的方法按 msg_type 分发到支付通知、退款通知、投诉通知三个方向避免一个 Handler 里堆满 if/else。4. WeixinExecutor 支付分发、Global.asax 初始化与 Web.config 多环境切换4.1 WeixinExecutor 如何把支付类型映射到业务处理器这个源码包里的 WeixinExecutor.cs 是所有支付入口的门面。它没有直接继承某个接口而是通过一个简单的策略映射把 NATIVE、APP 两个支付场景分发到 WxPayAPI 的不同方法。如果你扩展出 JSAPI 或 MWEB 支付只需要在 Execute 方法里增加分支并补上各自特有的参数。我建议把 Executor 设计成尽量只依赖两个方法一个负责组装统一下单数据另一个负责处理支付结果。这样业务层调用时不需要知道 WxPayAPI 的底层细节也让单元测试可以 mock 整个 Executor。示例代码演示这种分发方式public WxPayData Execute(PayRequest request, PaymentConfig config) { var order new WxPayData(); order.SetValue(out_trade_no, request.OrderNo); order.SetValue(body, request.Body); order.SetValue(total_fee, request.TotalFee); order.SetValue(notify_url, config.NotifyUrl); order.SetValue(spbill_create_ip, request.Ip); switch (request.TradeType) { case NATIVE: order.SetValue(trade_type, NATIVE); order.SetValue(product_id, request.ProductId); var nativeResult WxPayAPI.UnifiedOrder(order); return BuildNativeResult(nativeResult); case APP: order.SetValue(trade_type, APP); var appUnified WxPayAPI.UnifiedOrder(order); return BuildAppResult(appUnified, config); default: throw new NotSupportedException($不支持的支付类型{request.TradeType}); } }这里有几个值得注意的细节PayRequest 是入参 DTOPaymentConfig 是全局配置对象Execute 方法本身不读取 Web.config所有外部配置都通过参数传递。这样做的原因是单元测试时可以传入一个内存配置而不必依赖 ConfigurationManager。还有total_fee 在这个 DTO 里已经由元转换成 int 类型避免在 Executor 内部做金额单位换算减少隐藏 bug。这个分发方法也承担了错误边界职责微信方返回异常时统一抛出自定义异常类型上层 Controller 捕获后返回标准化 JSON 给前端。源码包没有强制约定异常处理但我在改造项目时一般会增加一个 Result 包装返回对象把微信错误代码、错误描述和业务错误分离方便运营人员直接定位是配置问题还是参数问题。4.2 Global.asax 中预加载支付配置的意义Global.asax 里的 Application_Start 是这个源码包初始化的核心。微信支付客户端证书、API Key、商户号等信息如果在每次请求时从配置文件读取配置中心一旦更新就会影响所有在途请求。推荐做法是在应用启动时把配置放到静态类里做一次性缓存后面所有代码从缓存读取。public class MvcApplication : HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); PaymentConfig.Initialize(ConfigurationManager.AppSettings); } protected void Application_BeginRequest(object sender, EventArgs e) { CallContext.LogicalSetData(trace_id, Guid.NewGuid().ToString(N)); } }Application_Start 里执行初始化方法把 Web.config 中 appSettings 下 WxAppId、WxMchId、WxApiKey、WxCertPath、WxCertPassword 等键加载进 PaymentConfig 的静态属性。CallContext 在 BeginRequest 中写入一个 trace_id后续日志可以通过 HttpContext.Current?.Items 或 AsyncLocal 继续传递这个请求标识。实际项目如果用的是 .NET Framework 4.xCallContext 在异步代码中可能沿逻辑上下文传播比 ThreadStatic 更稳定。Certificate 的加载Native 和 App 支付通常不需要客户端证书退款和企业付款到零钱接口才需要 apiclient_cert.p12。所以 Application_Start 里不要无条件加载证书否则证书过期或路径不存在时整个应用启动失败。正确做法是让 PaymentConfig 暴露一个 Lazy 类型的属性需要双向认证的接口第一次调用时才真正加载证书文件。4.3 Web.config 与 Web.Release.config 的配置隔离策略源码包里同时存在 Web.config、Web.Debug.config 和 Web.Release.config这是 Visual Studio 默认的配置转换机制。Web.config 放本地调试用的测试商户号Web.Release.config 在发布时对指定 key 做替换。转换文件只在发布时生效不会影响运行时的编译结果。配置键Debug 值示例Release 值示例转换方式WxAppIdwx-test-appidwx-prod-appid替换WxMchId19000001091900000111替换WxApiKeytest-api-keyprod-api-key替换WxNotifyUrlhttps://localhost:44301/notifyhttps://api.example.com/wxpay/notify替换WxCertPathC:\certs\test\apiclient_cert.p12D:\certs\prod\apiclient_cert.p12替换Web.Release.config 里的转换片段常见写法如下appSettings add keyWxAppId valuewx-prod-appid xdt:TransformSetAttributes xdt:LocatorMatch(key) / /appSettings这个 transform 的语义是在发布 Release 时找到 appSettings 里 key 为 WxAppId 的 add 节点替换 value 属性。Match(key) 是定位条件SetAttributes 是动作两者必须同时出现否则会被替换成空值。这里有一个容易忽略的坑如果 Web.config 中的某个配置项被加密或者在父级 configSource 中定义transform 会失效支付类配置尽量保持明文放在独立的配置节中通过权限控制保护。还要注意 Web.Release.config 无法直接删除 appSettings 里已存在的 key只能修改属性。想彻底清空某个密钥可以用 xdt:TransformRemove xdt:LocatorMatch(key)但发布后该 key 不存在又会让 ConfigurationManager.AppSettings[key] 返回 nullPaymentConfig.Initialize 里需要做 null 检查并抛出包含可读信息的异常否则上线后只能看到空引用错误排错成本会高很多。5. 微信支付签名不一致的定位技巧与本地模拟回调验证5.1 签名错误的三步定位方法签名问题是微信支付集成中最常见也最难直接看出原因的报错返回码SIGNERROR或签名错误时我一般按三步处理。第一步是开源码包里的原始报文日志把 WxPayAPI 发送请求前组装好的 XML 原样记录第二步是把报文中的参数复制到微信商户平台“签名校验工具”中手动比对注意工具要求的是 URL 解码后的键值对第三步是检查公钥和密钥的配置来源很多项目里 Web.config 同时存在旧商户号和新商户号配置Executor 读取的是某一份支付平台却用另一份。封装源码的 MakeSign 里有一个日志输出空位常见做法是加一个#if DEBUG分支把拼接后的 signContent 随请求报文一起输出。上线时这些敏感信息要打码密钥日志不落盘。string signContent sb.ToString(); System.Diagnostics.Debug.WriteLine($[WxPay] signContent{signContent});这段调试代码只会在 DEBUG 编译下执行因为 Debug.WriteLine 本身在 Release 下会被 JIT 忽略但仍不建议长期保留。更稳妥的方案是用条件编译器常量或 ILogger 注入在测试环境把签名原串写到独立日志文件生产环境只记录签名串的哈希值。5.2 本地模拟回调用 Postman 验证 Notify 接口不需要等真实订单也可以在本地完整验证回调逻辑。先在统一下单请求里拿到 code_url再找到统一下单响应的 XML 结构把 prepay_id、return_code、result_code 等字段替换成测试订单对应的值构造一条模拟支付通知。直接复制官方文档中的示例报文会导致验签失败因为示例报文的签名是用官方密钥生成的必须重新用本地配置的密钥和随机 nonce_str 算一次签名。模拟回调用 Postman 或 curl 发送 POST 请求Content-Type 设置为 application/xml。注意本地联调时 notify_url 不能是 localhost微信服务器访问不到测试环境可以借助内网穿透工具把本机端口暴露到公网。发送后观察返回值成功返回 SUCCESS失败返回 FAIL。通过这种方式可以在开发阶段就把验签、金额核对、幂等逻辑全部跑通而不是每次都要等真实支付完成。本质是熟悉这套源码包的接口封装后自动化和回归测试的收益会非常大。微信支付本身是外部依赖不能保证测试环境随时可用因此我始终会在支付模块里留一个“模拟回调”的开关仅限测试环境打开杜绝线上误用。本文还有配套的精品资源点击获取