资讯详情 C#微信支付Demo:统一下单、回调验签与企业付款到零钱全链路
📅 2026/10/23 5:09:36
简介面向C#开发者的微信支付集成参考代码包覆盖微信支付、微信企业支付到个人及企业付款到零钱等核心场景并提供可直接调试的ASP.NET WebForm示例项目适合需要快速接入支付或理解企业转账流程的中高级开发人员。压缩包共93个文件约2.13MB以47个cs源码文件、13个aspx页面为主辅以dll库、config配置、js脚本及sln工程文件便于直接打开项目并对照接口调用与业务实现。方案涵盖JSAPI支付、刷卡/扫码支付、企业付款到零钱、订单退款与查询、账单下载等关键接口集成CS_sdk_v3.0.9封装可帮助开发者理清统一下单、签名验证、回调处理等环节减少重复排查时间。目前已有1575人学习下载是一份贴近实战、可运行调试的微信支付DEMO参考。1. 一个C#微信支付Demo从下单到企业付款到零钱的完整链路你接过微信支付但大概率没有完整做过用户下单支付→回调验签→企业付款到零钱转账给个人这一整套流程。公众号里能搜到一堆微信支付接入的片段但真正把收款和付款到个人两个方向同时跑通的Demo并不多。这个C#代码DEMO的价值就在这它把统一下单、支付回调、企业转账到个人微信企业付款到零钱从请求构造、签名、证书加载到结果查询全部串了起来直接对着商户平台配置就能改跑。适合两类人一是刚在商户平台开通了JSAPI支付和小程序支付想用C#快速打通收款闭环的开发者二是已经能收款、但被企业付款到零钱的证书和签名规则折磨到想放弃的开发者——后者在这个Demo里能找到完整的可运行代码而不是网上那种缺一半的伪代码。2. 项目全貌与技术选型从API版本到工程目录结构2.1 玩法概述收款与付款是两套签名逻辑很多人第一次接触微信支付会以为付款给用户就是退款接口的变体实际上完全不是一回事。退款走的是secapi_pay_refund接口需要APIv3密钥和平台证书而企业付款到零钱走的是mmpaymkttransfers/promotion/transfers接口用的是老版v2的MD5签名但额外要求双向TLS客户端必须加载商户p12证书。这就造成了一个很尴尬的情况——你刚把v3的RSA验签模式理清转头发现v2转账要的是另一套规则。这个Demo把所有接口统一到一套配置类下AppId、MchId、ApiKey、P12证书签名方法内部区分普通接口和证书接口调用方不需要关心底层差异这是项目设计上最实用的一个点。微信支付的C#实现其实一直有个尴尬官方SDK更新慢社区封装版本又杂。这个Demo的做法是不用任何第三方微信支付SDK直接用HttpClient XmlDocument X509Certificate2裸调接口。这样做的最大好处是没有黑匣子——每个请求长什么样、签名怎么拼、证书怎么挂全部摊开在你面前。缺点是代码量会多一些但作为二次开发的底子这种看得见每一处细节的方式反而更安全也更容易迁移到不同的.NET版本。2.2 工程结构一眼扫完的配置与调用链解压Demo后目录结构大致如下WeChatPayDemo/ ├─ Config/ │ └─ WxPayConfig.cs # 微信支付配置AppId、MchId、ApiKey、证书路径 ├─ Common/ │ ├─ WxPayUtil.cs # 签名、随机数、XML转换、时间戳 │ └─ HttpHelper.cs # HTTP请求封装支持客户端证书 ├─ Service/ │ ├─ OrderService.cs # 统一下单、查询订单 │ ├─ TransferService.cs # 企业付款到零钱、查询转账 │ └─ NotifyService.cs # 支付回调验签与业务处理 ├─ Model/ │ ├─ OrderInfo.cs # 订单参数金额单位分元转分在构造时完成 │ ├─ TransferRequest.cs # 转账请求模型 │ └─ WxPayResult.cs # 接口统一返回模型 ├─ Demo/ │ └─ Program.cs # 控制台入口演示调用顺序 └─ cert/ └─ apiclient_cert.p12 # 商户证书从商户平台下载每个Service类都只暴露一个公开方法参数用强类型模型传入返回结果统一包装成WxPayResult里面包含ReturnCode、ResultCode、ErrCodeDes和原始XML。这种设计很朴素但足够支撑起后期的业务扩展比如把OrderService替换成你自己的订单系统只留微信交互部分。2.3 三个核心接口的参数差异对比统一下单、支付回调、企业付款到零钱三者的参数规则差异很大我把Demo里涉及的接口参数整理成了下表方便你对照着看接口请求地址签名方式证书要求金额字段统一下单/pay/unifiedorderMD5ApiKey无普通HTTPS即可total_fee分支付回调由微信POST到NotifyUrlRSA验签平台证书公钥无total_fee分企业付款到零钱/mmpaymkttransfers/promotion/transfersMD5ApiKey必须加载p12客户端证书amount分查询转账/mmpaymkttransfers/gettransferinfoMD5ApiKey无但建议开启服务端IP白名单不需要金额注意上表中企业付款到零钱的amount字段文档上标注单位是分但实际调试时最容易翻车的也正是这里——有人把元直接填进去转了10元却只到了0.1元后面避坑章节我会专门说。查询转账接口虽然不需要客户端证书但商户平台的安全设置里通常会要求绑定服务器IP白名单否则直接返回IP_NOT_ALLOWED。3. 核心链路拆解统一下单、支付回调与验签实现3.1 统一下单先解决签名再谈请求微信支付v2接口的签名规则是参数名按ASCII字典序排序→拼接键值对→末尾追加key→MD5后转大写。这个逻辑在所有v2接口里完全一致所以WxPayUtil.BuildSign是全局复用的基础方法。看OrderService里的统一下单实现// Service/OrderService.cs - 统一下单核心代码 public async TaskWxPayResult UnifiedOrder(OrderInfo order) { // 1. 构造参与签名的参数字典SortedDictionary会自动按键名ASCII排序 var dict new SortedDictionarystring, string { { appid, _config.AppId }, { mch_id, _config.MchId }, { body, order.Body }, // 商品描述不超过127字符 { nonce_str, Guid.NewGuid().ToString(N).ToUpper() }, // 随机串32位以内 { notify_url, _config.NotifyUrl }, // 回调地址必须是公网HTTPS { out_trade_no, order.OutTradeNo }, // 商户订单号同一商户号下唯一 { total_fee, order.TotalFee.ToString() }, // 金额单位是分不是元 { trade_type, order.TradeType }, // JSAPI / APP / NATIVE { openid, order.OpenId } // 仅JSAPI必填 }; // 2. 生成MD5签名 string sign WxPayUtil.BuildSign(dict, _config.ApiKey); dict.Add(sign, sign); // 3. 拼接XML并POST到统一下单地址 string xml WxPayUtil.BuildXml(dict); string respXml await _httpHelper.PostXmlAsync( https://api.mch.weixin.qq.com/pay/unifiedorder, xml); // 4. 解析响应XML返回code_url或prepay_id var result WxPayUtil.ParseXml(respXml); var payResult new WxPayResult { ReturnCode result[return_code], ResultCode result[result_code], ErrCodeDes result.ContainsKey(err_code_des) ? result[err_code_des] : , PrepayId result.ContainsKey(prepay_id) ? result[prepay_id] : , CodeUrl result.ContainsKey(code_url) ? result[code_url] : // NATIVE支付用 }; return payResult; }这段代码里有几个容易被忽略的细节SortedDictionary帮你省去了手动排序的步骤但必须保证所有参数名与微信文档一致写错一个字母签名必挂。nonce_str要求32位以内、区分大小写使用Guid.NewGuid().ToString(N)可以稳定生成32位小写字符串属于安全用法。total_fee在OrderInfo构造函数里强制做了元转分的换算这样业务层传进来的是10.5元到微信这层已经是1050分能有效避免金额单位混淆。如果返回的ReturnCode是FAIL先不要怀疑签名大概率是参数漏填或appid与mch_id未绑定。如果ReturnCode是SUCCESS但ResultCode是FAIL重点看ErrCodeDes例如APPID_MCH_ID_NOT_MATCH就说明公众号AppId和商户号没有绑定关系。3.2 支付回调验签不是可选项是必选项支付成功后微信会异步POST一条XML到notify_url里面包含out_trade_no、transaction_id、total_fee等字段。很多Demo的处理方式是解析XML拿字段直接改订单状态但这是极大的安全隐患——任何人都能伪造一条POST到你服务器上所以NotifyService里的验签环节不能省// Service/NotifyService.cs - 支付回调验签与业务处理 public async Taskstring HandleNotify(string requestBody) { // 1. 解析回调XML var dict WxPayUtil.ParseXml(requestBody); if (dict[return_code] ! SUCCESS) { return WxPayUtil.BuildFailResponse(FAIL); // 告诉微信不要重试 } // 2. 从商户平台下载的API证书中加载微信支付平台公钥用于验签 // 注意这里是微信平台证书不是商户自己的p12证书两者别混淆 bool isValid WxPayUtil.VerifyRsaSign(dict, _config.PlatformCertPath); if (!isValid) { return WxPayUtil.BuildFailResponse(SIGN_INVALID); // 验签失败记录日志 } // 3. 二次校验金额与商户订单号防止报文被篡改 string outTradeNo dict[out_trade_no]; string totalFee dict[total_fee]; // 这里需要查本地数据库把入参金额与total_fee比较不一致则拒绝 bool orderMatched await _orderRepository.CheckAmount(outTradeNo, totalFee); if (!orderMatched) { return WxPayUtil.BuildFailResponse(AMOUNT_MISMATCH); } // 4. 更新订单状态为已支付标记第三方支付流水号 await _orderRepository.MarkPaid(outTradeNo, dict[transaction_id]); // 5. 返回SUCCESS给微信通知停止重试 return WxPayUtil.BuildSuccessResponse(); }这里的验签逻辑和统一下单完全不同统一下单是拿自己的key拼MD5回调验签是用微信平台证书的公钥验RSA签名。如果配错了证书VerifyRsaSign会一直返回false。我之前在某个项目里踩过这个坑当时拿着商户p12证书去验签在本地怎么测都对部署到服务器就挂——后来才发现回调验签用的是微信支付平台证书需要在商户平台API安全-平台证书处下载且证书会定期轮换上线前要确认有效期。回调返回的字符串也有讲究微信要求返回xmlreturn_code![CDATA[SUCCESS]]/return_code/xml不能多一个空格不能带BOM头。BuildSuccessResponse内部用XmlDocument保存格式后再输出能避免手拼字符串引发的编码问题。3.3 查询订单给前端轮询兜底支付结果以回调为准但有些场景比如用户支付后进程崩溃回调解析失败需要主动查询兜底。OrderService里还实现了一个QueryOrder方法向/pay/orderquery发送out_trade_no获取订单状态。它的签名方式和统一下单一致只是XML里少了body、notify_url等字段多了out_trade_no。这个接口适合放在定时任务里每分钟把支付中的订单扫一遍把状态推进到已支付。注意查询接口有频率限制官方建议间隔不小于15分钟不然容易触发风控。4. 企业付款到零钱转账接口的签名、证书与查询实现4.1 转账前的三个硬性条件钱包、权限、证书真正让我觉得这个Demo值钱的地方在TransferService。企业付款到零钱商户把资金从商户余额转到用户微信零钱的接口比收款苛刻得多第一商户号和用户openid必须在同一个公众号下不能拿公众号A的openid配公众号B绑定的商户号第二商户平台需要单独开通企业付款到零钱权限有些商户号开通后还有单笔限额常见的是单笔上限2万单日上限按商户号等级第三请求必须携带商户p12证书p12证书从商户平台下载后会有一个密码默认通常是商户号MchId本身下载后如果改过密码要用改过的。这三个条件任意一个不满足接口都会返回FAIL而且错误码各不相同有些错误码在微信文档里都搜不到确切解释只能靠经验判断属实的玄学现场。下面把TransferService里的转账实现完整拆开看。4.2 转账核心方法从签名到HttpClient挂证书// Service/TransferService.cs - 企业付款到零钱核心代码 public async TaskWxPayResult TransferToBalance(TransferRequest req) { // 1. 构造签名参数字段名与统一下单完全不同 var dict new SortedDictionarystring, string { { mch_appid, _config.AppId }, // 注意是mch_appid不是appid { mchid, _config.MchId }, // 注意是mchid不是mch_id { nonce_str, Guid.NewGuid().ToString(N).ToUpper() }, { partner_trade_no, req.PartnerTradeNo }, // 商户转账单号需唯一 { openid, req.OpenId }, // 收款用户openid { check_name, req.CheckName }, // NO_CHECK / OPTION_CHECK / FORCE_CHECK { re_user_name, req.UserName ?? }, // FORCE_CHECK时必须填真实姓名 { amount, req.Amount.ToString() }, // 金额单位分 { desc, req.Desc }, // 转账备注会展示给用户 { spbill_create_ip, req.SpbillCreateIp } // 发起转账的服务器IP }; // 2. 与统一下单同款MD5签名 string sign WxPayUtil.BuildSign(dict, _config.ApiKey); dict.Add(sign, sign); // 3. 核心区别HttpClient必须挂上p12客户端证书 var handler new HttpClientHandler(); handler.ClientCertificates.Add(_config.TransferCertificate); // TransferCertificate 由 X509Certificate2 加载 cert/apiclient_cert.p12 // 密码在WxPayConfig中配置一般为商户号或自定义证书密码 using var client new HttpClient(handler); client.Timeout TimeSpan.FromSeconds(30); // 4. 发送XML请求到转账接口 string xml WxPayUtil.BuildXml(dict); string respXml await WxPayUtil.PostXmlAsync( client, https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers, xml); // 5. 解析返回结果并判断 var result WxPayUtil.ParseXml(respXml); var payResult new WxPayResult { ReturnCode result[return_code], ResultCode result[result_code], ErrCodeDes result.ContainsKey(err_code_des) ? result[err_code_des] : , PaymentNo result.ContainsKey(payment_no) ? result[payment_no] : , }; // 注意ResultCode为SUCCESS只代表请求受理不代表到账成功 return payResult; }这段代码的第一个记忆点是字段名和统一下单不一样。mch_appid、mchid少一个下划线多一个appid前缀拼错直接报PARAM_ERROR。第二个记忆点是check_name字段如果是FORCE_CHECK必须传re_user_name且与用户实名一致否则转账失败这个字段适合对公转账、报销打款场景如果是内部测试用NO_CHECK最省事。第三个记忆点是amount还是分这已经是第二个接口在金额单位上坑你了后面改造时建议统一封装。HttpClientHandler.ClientCertificates.Add这行代码是转账接口的钥匙。_config.TransferCertificate在配置中是通过new X509Certificate2(certPath, certPassword)加载的如果加载时报密码不正确或找不到指定的X509证书先回到商户平台重新下载p12或者确认当前服务器Windows/Unix的证书权限是否允许读取文件。4.3 查询转账结果区分受理成功和到账成功转账接口返回SUCCESS不代表钱已经到用户零钱。资金流转需要时间网络环境差时甚至会出现接口超时但后台已转账的情况所以Demo里配套实现了查询接口。代码如下// Service/TransferService.cs - 查询企业付款到零钱结果 public async TaskTransferStatus QueryTransfer(string partnerTradeNo) { var dict new SortedDictionarystring, string { { mch_appid, _config.AppId }, { mchid, _config.MchId }, { nonce_str, Guid.NewGuid().ToString(N).ToUpper() }, { partner_trade_no, partnerTradeNo } }; string sign WxPayUtil.BuildSign(dict, _config.ApiKey); dict.Add(sign, sign); // 普通HTTPS请求即可不需要客户端证书 string xml WxPayUtil.BuildXml(dict); string respXml await _httpHelper.PostXmlAsync( https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo, xml); var result WxPayUtil.ParseXml(respXml); // 关键字段status - PROCESSING / SUCCESS / FAILED // PROCESSING 表示转账中SUCCESS 表示已到账FAILED 表示转账失败 string status result.ContainsKey(status) ? result[status] : ; string reason result.ContainsKey(reason) ? result[reason] : ; // 失败原因 return new TransferStatus { Status status, Reason reason, PaymentTime result.ContainsKey(payment_time) ? result[payment_time] : }; }建议把处理中状态理解成转账的人已收到指令但还没到账这时候不能立刻把订单标记为失败也不能无限制地重复查询。合理做法是把PROCESSING状态的转账单放进一张待查表每5分钟扫一次最多查24小时超过时间的转入人工处理。由于转账单号对应的是真金白银这种兜底机制在生产环境里必须有——宁可查到FAILED再去退款也不能把处理中当成已到账。5. 避坑指南调试这个Demo时容易翻车的五个典型坑5.1 报错商户号该产品权限未开通权限不是默认有的现象统一下单接口返回return_codeFAILerr_code_des提示商户号该产品权限未开通。原因不是每个商户号都默认拥有JSAPI支付权限。你必须在商户平台的产品中心里面自助申请开通JSAPI支付、并完成APPID绑定。另一个隐蔽情况是商户号主体和公众号主体不一致导致绑定失败。解决进入商户平台-产品中心-JSAPI支付-产品设置检查已关联AppID是否包含你配置的那个AppId如果没有先完成公众号与商户号的关联再重新跑Demo。如果主体不一致需要走微信的授权流程把公众号授权给商户号的主体使用这一步在商户平台有引导。5.2 回调验签永远失败平台证书与商户证书分不清现象支付成功微信回调到NotifyUrl但VerifyRsaSign一直返回false日志里全是验签失败。原因最常见的是拿商户p12证书去验签。回调验签要求的是微信支付平台证书的公钥这个证书从商户平台账户中心-API安全-平台证书管理下载和商户p12不是同一个文件。还有一个坑是回调请求体的编码问题——某些框架会自动做URL解码或转义把原始XML改变了一两个字符导致RSA验签失败。解决下载最新的wechatpay_platform.pem文件替换PlatformCertPath在回调入口处直接接收Request.Body的原始字节流避免中间层做任何格式化如果验签还是失败把收到的XML和证书公钥打印出来做离线比对。5.3 转账报openid与mch_appid不匹配openid有归属现象调用TransferToBalance时报openid校验不通过。原因每个openid只属于一个AppId。用户在公众号A授权后拿到的openid不能提供给绑定了公众号B的商户号来转账。项目里如果同时跑着公众号A和公众号B两套系统最容易出现这个错误。解决检查_config.AppId和req.OpenId的来源是否为同一公众号如果用户是通过小程序支付后就来提现注意小程序的openid体系和公众号的openid体系是隔离的必须用小程序对应的AppId发起转账否则在商户平台上绑定小程序的AppId后再调。5.4 p12证书加载失败密码、路径、权限三位一体现象运行到TransferToBalance时new X509Certificate2(certPath, password)抛异常或者接口返回certificate verify failed。原因证书密码错误是第一位默认密码是商户号但很多人在商户平台下载时自定义过密码第二位是相对路径问题bin目录下运行时./cert/路径找不到文件第三位是Linux服务器上证书文件权限不足。解决统一用绝对路径读取p12或者把证书复制到项目根目录再写Path.Combine(AppContext.BaseDirectory, cert, apiclient_cert.p12)避免环境不同导致路径失效。加载后用cert.HasPrivateKey判断是否真的含私钥——如果只是挂着证书壳子没有私钥转账请求必失败。5.5 金额单位搞混你以为转了10元用户收到0.1元现象转账接口返回成功用户反馈收到的钱不对。查日志发现amount字段明明填了10但用户只到账0.1元。原因微信所有资金类接口的金额都以分为单位。你填10微信读到的就是10分也就是一角钱。这个问题在统一下单时同理——把10元写成10分订单创建成功但支付页金额变成0.1元等用户点完支付你才发现已经晚了。解决模型层强制转换。在OrderInfo和TransferRequest的构造函数里写this.AmountInFen Decimal.ToInt32(decimal.Round(amountYuan * 100))业务层只传元模型层负责转分杜绝散落各处的*100逻辑。同时可以在调用微信接口前断言if (amountInFen 0) throw new InvalidOperationException(金额必须大于0且单位为分)。6. 从Demo到生产二次开发的三个改造落点拿到这个Demo直接跑通只是第一步想在真实项目里用上至少要完成三件事。6.1 把配置外置支持多商户隔离WxPayConfig目前是单例所有接口都读同一组AppId和MchId。实际上很多公司的业务线有多套主体公众号入口不同商户号也各自独立。改造时把WxPayConfig改成可配置对象用IServiceProvider注入或者做一个WxPayContext字典来存多套配置。调用时按业务线选择对应上下文。签名、证书这些基础方法全部复用唯一要小心的是回调验签的PlatformCertPath也必须跟着上下文走不能共用一套平台证书——不同商户号之间证书是不同的。6.2 给转账单号加上幂等保护partner_trade_no在微信侧要求唯一同一个单号重复转账两次会被拒绝但并发快但你本地数据库还没commit时两次请求都落到数据库查询不存在然后同时调微信一单成功一单失败。生产环境要这样做partner_trade_no生成规则统一为TR yyyyMMddHHmmss 6位随机数并在数据库字段上加唯一索引转账前先INSERT这条记录主键冲突就说明请求重复直接返回重复请求。这个防重逻辑放在TransferService入口处和纯Demo的区别就在这里。6.3 完整记录回调与转账日志别只记result_code微信支付接口返回的所有字段都应该落库保存尤其是退款和转账的payment_no、status、reason。我见过不少项目只在出错时记日志结果对账时缺了一笔转账查无可查。建议从Demo改造开始就是设计一张wechat_wallet_log表里面存接口类型、请求报文、响应报文、错误码、耗时毫秒、涉事订单号。后续排查问题时这张表能帮你把到底微信没收到请求还是微信返回了但我们没处理这类问题从概率变成确定。从那以后我每接一个新的支付渠道都强制自己先走一遍最小闭环→异常日志→幂等校验→对账兜底四步流程这套习惯帮我避开了不少线上故障。希望帮到你。本文还有配套的精品资源点击获取