资讯动态

.NET Core 微信支付 V3 服务商模式接入实战:下单、分账、退款与回调全链路

发布时间:2026/10/9 3:01:57 来源:尧图企业网站定制
简介本资源面向.NET Core开发者聚焦微信支付V3服务商模式下的完整支付链路实现涵盖普通支付、支付回写、退款、分账给个人、服务商模式支付与回写、服务商分账给子商户以及V3支付退款等核心场景适合需要快速落地微信支付与分账业务的中高级开发者参考。压缩包共696个文件约34.16MB以383个dll、70个cs源码、62个pdb调试文件为主辅以json配置、xml文档、csproj工程文件与sln解决方案整体为可直接编译运行的源码工程结构。目前已有1369人学习下载说明该方案在实际项目中具备一定参考价值。读者可从中获取各支付场景的接口调用示例、分账逻辑组织方式与回写处理思路便于对照自身业务进行改造与排错减少从零搭建微信支付模块的时间成本。1. 服务商模式下的 .NET Core 微信支付 V3一条绕不开的接入路径如果你正在做一个多商户 SaaS 平台或者给连锁品牌做收银中台大概率会遇到这个场景平台自己不是收款方钱要直接进各个子商户的账户平台只拿分账抽成。这时候普通商户直连模式就不够用了必须走服务商模式。而微信支付 V3 接口相比老 V2 版本签名机制、证书体系、回调解密全部换了一套很多团队第一次接的时候会在验签和回调解密上卡好几天。这篇笔记围绕 .NET Core 环境下接入微信支付 V3 服务商模式把支付下单、分账、退款、支付回写这条完整链路拆开讲。适合两类人一是刚拿到服务商资质、准备从零接入的团队二是已经接了普通商户模式想迁移到服务商模式的开发者。核心不是讲微信支付有多复杂而是把每一步的代码、参数、踩坑点摆出来让你照着能跑通。2. 服务商模式的前置准备证书、密钥与商户号关系2.1 服务商模式到底和普通模式差在哪普通商户模式下你用自己的商户号下单钱进自己账户。服务商模式下你有两个身份服务商商户号sp_mchid和子商户号sub_mchid。下单时接口里要同时传这两个 ID微信会把钱结算到子商户账户服务商通过分账接口抽取佣金。这个区别直接影响了几个关键环节。第一签名用的私钥是服务商的不是子商户的。第二回调通知里的商户号字段是子商户的验签时要用服务商的平台证书。第三退款和分账的权限校验走的是服务商维度子商户不需要单独配置 API 密钥。很多人在第一步就搞混了用子商户的密钥去签名结果一直报签名错误。另一个容易忽略的点是子商户的绑定关系。子商户必须先通过服务商平台完成进件审核拿到 sub_mchid 之后才能下单。进件流程不在代码层面但如果你在测试环境用了一个没审核通过的 sub_mchid下单接口会直接返回PARAM_ERROR错误信息里不会明确告诉你子商户没绑定只会说参数不对。这个坑后面会细说。2.2 在 .NET Core 项目里配置证书和密钥微信支付 V3 需要三类密钥材料服务商 API 私钥apiclient_key.pem、服务商证书序列号、微信支付平台证书。前两个在服务商商户平台下载平台证书需要通过接口动态获取或者用工具下载。我一般会在项目里建一个WeChatPayOptions配置类从appsettings.json读取路径和序列号不把证书内容硬编码进代码。public class WeChatPayOptions { public string SpMchId { get; set; } // 服务商商户号 public string AppId { get; set; } // 服务商绑定的 AppId public string PrivateKeyPath { get; set; } // apiclient_key.pem 路径 public string MerchantSerialNo { get; set; } // 服务商证书序列号 public string ApiV3Key { get; set; } // APIv3 密钥用于回调解密 public string PlatformCertPath { get; set; } // 微信平台证书路径 }SpMchId和AppId必须匹配服务商模式下 AppId 是服务商自己申请的那个不是子商户的。ApiV3Key是在商户平台手动设置的 32 位字符串回调解密和敏感信息加密都用它。MerchantSerialNo是证书序列号不是证书内容在商户平台证书管理页面能看到。加载私钥时用X509Certificate2或者直接读 PEM 文件。.NET Core 里推荐用RSA.Create()配合ImportFromPem比老式的X509Certificate2更干净。public RSA LoadPrivateKey(string path) { var rsa RSA.Create(); var pem File.ReadAllText(path); rsa.ImportFromPem(pem); // .NET 5 支持直接读 PEM 格式 return rsa; }如果你用的是 .NET Core 3.1ImportFromPem不存在需要用BouncyCastle或者手动解析 PEM 的 Base64 内容再调ImportRSAPrivateKey。这是版本差异带来的第一个坑后面避坑章节会展开。平台证书的获取有两种方式一是用微信提供的工具下载二是调/v3/certificates接口动态获取。生产环境建议动态获取并缓存因为平台证书会定期轮换。缓存时注意记录证书的serial_no验签时要根据回调头里的Wechatpay-Serial找到对应的证书。3. 用 .NET Core 实现 V3 服务商下单与签名3.1 V3 签名机制的拆解与构造方法V3 的签名和 V2 的 MD5 完全不是一回事。它要求你把 HTTP 方法、URL 路径、时间戳、随机串、请求体拼成一个字符串然后用 SHA256withRSA 签名最后放到Authorization头里。拼串格式是HTTP方法\nURL路径\n时间戳\n随机串\n请求体\n注意 URL 路径要带 query string比如/v3/pay/partner/transactions/jsapi不带参数但查单接口/v3/pay/partner/transactions/out-trade-no/{out_trade_no}?sp_mchidxxx就要把 query 拼进去。请求体如果是 GET 请求留空但换行符不能少。签名串构造代码public string BuildSignatureString(string method, string urlPath, long timestamp, string nonceStr, string body) { // 每行末尾都要有 \n包括最后一行 return ${method}\n{urlPath}\n{timestamp}\n{nonceStr}\n{body}\n; }时间戳是秒级 Unix 时间戳不是毫秒。随机串用Guid.NewGuid().ToString(N)就行32 位以内。签名结果用 Base64 编码然后拼成WECHATPAY2-SHA256-RSA2048 mchid服务商商户号,nonce_str随机串,timestamp时间戳,serial_no证书序列号,signature签名值这里mchid填服务商商户号不是子商户号。serial_no是服务商证书序列号。这两个字段填错会直接返回 401。3.2 服务商 JSAPI 下单的完整请求构造服务商模式下单接口是/v3/pay/partner/transactions/jsapi。请求体里必须包含sp_appid、sp_mchid、sub_mchid、description、out_trade_no、notify_url、amount、payer。public async Taskstring CreatePartnerOrderAsync(string subMchId, string openId, string outTradeNo, int totalFee) { var urlPath /v3/pay/partner/transactions/jsapi; var body new { sp_appid _options.AppId, sp_mchid _options.SpMchId, sub_mchid subMchId, description 测试商品, out_trade_no outTradeNo, notify_url https://yourdomain.com/api/wxpay/notify, amount new { total totalFee, currency CNY }, payer new { openid openId } }; var json JsonSerializer.Serialize(body); var timestamp DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var nonce Guid.NewGuid().ToString(N); var signStr BuildSignatureString(POST, urlPath, timestamp, nonce, json); var signature SignWithRsa(signStr, _privateKey); var auth $WECHATPAY2-SHA256-RSA2048 mchid\{_options.SpMchId}\, $nonce_str\{nonce}\,timestamp\{timestamp}\, $serial_no\{_options.MerchantSerialNo}\,signature\{signature}\; // 用 HttpClient 发送请求带上 Authorization 和 Accept // ... }totalFee单位是分不是元。notify_url必须是 HTTPS不能带参数。openid是用户在服务商 AppId 下的 openid不是子商户 AppId 的。如果子商户有自己的 AppId需要走sub_appid字段但 openid 对应的主体也要跟着变。这个细节在跨主体场景下特别容易翻车。返回结果里会拿到prepay_id然后需要再签一次名生成小程序或 JSAPI 调起支付的参数。调起支付的签名串格式是appId\ntimeStamp\nnonceStr\npackage\n注意这里的appId是服务商的 AppIdpackage是prepay_idxxx。签名用同一个私钥但拼串格式和接口签名不同别搞混。3.3 支付回写的验签与解密处理支付成功后微信会异步通知到notify_url。回调请求头里有Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature、Wechatpay-Serial。验签流程是用平台证书公钥对时间戳\n随机串\n请求体\n做验签。public bool VerifyNotify(string timestamp, string nonce, string body, string signature, string serialNo) { var message ${timestamp}\n{nonce}\n{body}\n; var cert GetPlatformCert(serialNo); // 根据 serialNo 找证书 using var rsa cert.GetRSAPublicKey(); var data Encoding.UTF8.GetBytes(message); var sig Convert.FromBase64String(signature); return rsa.VerifyData(data, sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); }验签通过后回调体是 AES-256-GCM 加密的需要用ApiV3Key解密。解密时注意nonce和associated_data从resource对象里取不是请求头的 nonce。public string DecryptResource(string ciphertext, string nonce, string associatedData, string apiV3Key) { var key Encoding.UTF8.GetBytes(apiV3Key); var cipherBytes Convert.FromBase64String(ciphertext); var nonceBytes Encoding.UTF8.GetBytes(nonce); var tag cipherBytes[^16..]; // 最后 16 字节是 auth tag var data cipherBytes[..^16]; using var aes new AesGcm(key); var plain new byte[data.Length]; aes.Decrypt(nonceBytes, data, tag, plain, Encoding.UTF8.GetBytes(associatedData)); return Encoding.UTF8.GetString(plain); }解密后的 JSON 里out_trade_no、transaction_id、trade_state是核心字段。处理完业务逻辑后必须返回{code:SUCCESS,message:成功}否则微信会按策略重试。重试间隔是 15s、15s、30s、3m、10m、20m、30m、30m、30m、60m、3h、3h、3h、6h、6h、6h最多 24 小时。所以回调处理一定要做幂等用out_trade_no或transaction_id做唯一键。4. 分账与退款的接口调用与状态机4.1 分账接口的调用时机与参数配置分账不是下单就能调的必须等支付成功且订单进入可分账状态。微信规定支付成功后订单有 180 天的分账窗口但实际业务里一般支付回调处理完就发起分账。分账接口是/v3/profitsharing/orders。请求体关键字段appid、transaction_id、out_order_no、receivers、unfreeze_unsplit。receivers是一个数组每个元素包含type、account、amount、description。type可以是MERCHANT_ID或PERSONAL_OPENID分别对应分给商户或分给个人。var body new { appid _options.AppId, transaction_id transactionId, out_order_no $profit_{outTradeNo}, receivers new[] { new { type MERCHANT_ID, account subMchId, amount 100, description 分账给子商户 }, new { type MERCHANT_ID, account _options.SpMchId, amount 20, description 平台佣金 } }, unfreeze_unsplit true // 分账后剩余金额解冻 };amount单位是分所有 receiver 的 amount 之和不能超过订单总金额。out_order_no是服务商侧的分账单号必须唯一。unfreeze_unsplit设为 true 表示分账完成后剩余资金解冻给子商户如果设为 false剩余资金会继续冻结需要再调解冻接口。分账接口返回status字段常见值有PROCESSING、FINISHED、CLOSED。PROCESSING表示微信还在处理需要等分账回调或者主动查单。分账回调的event_type是PROFITSHARING.ORDER.FINISHED处理逻辑和支付回调类似验签解密后更新分账状态。4.2 退款接口在服务商模式下的差异退款接口是/v3/refund/domestic/refunds。服务商模式下请求体里要传sub_mchid而不是sp_mchid。退款金额不能超过订单剩余可退金额部分退款时要注意累计退款金额。var body new { sub_mchid subMchId, out_trade_no outTradeNo, out_refund_no $refund_{outTradeNo}_{DateTime.Now.Ticks}, reason 用户申请退款, notify_url https://yourdomain.com/api/wxpay/refund-notify, amount new { refund refundFee, total totalFee, currency CNY } };out_refund_no必须唯一重复提交同一个退款单号微信会返回原退款结果不会重复退款。退款回调的event_type是REFUND.SUCCESS或REFUND.ABNORMAL。退款成功后如果原订单有分账微信会自动从分账方扣回对应比例但前提是分账方账户余额充足。如果分账方余额不足退款会失败这个坑在分账后立即退款的场景里特别常见。退款状态机比支付复杂有SUCCESS、CLOSED、PROCESSING、ABNORMAL四种。ABNORMAL表示退款异常通常是收款方账户问题需要人工介入。PROCESSING状态要等回调或者主动查单不能直接当失败处理。4.3 分账与退款的组合场景处理实际业务里最常见的组合是用户支付 100 元平台分账 20 元给服务商80 元给子商户然后用户申请全额退款。这时候微信会先从子商户扣 80 元再从服务商扣 20 元。如果服务商账户余额不足退款会卡在ABNORMAL。处理这种场景我一般会在退款前先查分账状态确认分账已完成。如果分账还在PROCESSING先等分账回调再发起退款。另外退款金额要按分账比例回滚不能只退子商户那部分。微信的退款接口会自动处理分账回滚但前提是分账方余额充足。还有一个细节分账接口有 30 天的分账窗口限制超过 30 天的订单不能再分账。如果业务需要长周期分账要在支付成功后尽快发起或者用unfreeze_unsplitfalse先冻结资金后续再分账。5. 避坑与排查签名、回调、分账的 5 个血泪教训5.1 签名一直报 401 但不知道哪里错了现象调接口返回 401错误信息是SIGN_ERROR但检查了私钥、序列号、拼串格式都没问题。原因最常见的是 URL 路径拼串时漏了 query string或者 GET 请求的 body 留空但没加换行符。另一个高频原因是时间戳用了毫秒微信要求秒级。还有一个隐蔽原因是Authorization头里的mchid填了子商户号服务商模式下必须填服务商商户号。解决把拼串内容打印出来逐行对比。时间戳用DateTimeOffset.UtcNow.ToUnixTimeSeconds()不要用ToUnixTimeMilliseconds()。GET 请求的 body 传空字符串但拼串时\n不能省。mchid字段确认是sp_mchid。5.2 回调验签失败但平台证书是对的现象支付回调进来验签返回 false但平台证书是从微信官方渠道下载的。原因回调请求体在验签前被读取了一次导致流位置变了第二次读取时拿到空字符串。ASP.NET Core 里Request.Body默认只能读一次如果中间件里读过控制器里再读就是空的。解决在验签前用EnableBuffering()开启缓冲或者用StreamReader读取后把Position重置为 0。更稳妥的做法是在中间件里一次性读完 body存到HttpContext.Items里后续直接用。Request.EnableBuffering(); using var reader new StreamReader(Request.Body, Encoding.UTF8); var body await reader.ReadToEndAsync(); Request.Body.Position 0; // 重置位置方便后续读取5.3 分账接口返回 PARAM_ERROR 但参数看起来都对现象分账请求返回PARAM_ERROR提示receiver相关字段有问题。原因receivers数组里某个account填的是子商户号但type写成了PERSONAL_OPENID。或者amount之和超过了订单总金额。还有一种情况是分账接收方没有在服务商平台添加分账关系微信要求先调/v3/profitsharing/receivers/add添加接收方。解决检查每个 receiver 的type和account是否匹配。MERCHANT_ID对应商户号PERSONAL_OPENID对应 openid。分账前先调添加接收方接口确保关系已建立。金额之和用代码校验一遍别靠肉眼。5.4 退款成功但分账方余额被扣成负数现象退款回调显示成功但服务商账户余额变成负数后续分账接口全部失败。原因分账后立即退款微信从分账方扣回资金时如果分账方余额不足会先扣成负数然后限制后续分账和退款操作。解决退款前先查分账方余额确保足够覆盖退款金额。如果余额不足先让分账方充值或者调整分账比例留足退款缓冲。生产环境建议在分账时预留 10% 到 20% 的退款保证金不要全部分完。5.5 回调处理超时导致微信重复通知现象回调处理逻辑里查了数据库、调了其他服务耗时超过 5 秒微信判定超时开始重试导致同一笔订单被处理多次。原因微信回调的超时时间是 5 秒超过就会重试。如果业务逻辑里有慢查询或者外部接口调用很容易超时。解决回调里只做验签、解密、落库把耗时操作放到异步队列里处理。落库时用out_trade_no做唯一索引重复插入直接忽略。返回给微信的响应要快不要等业务逻辑全部跑完。6. 用在线调试工具验证签名与回调的实操技巧微信支付有个在线调试网站可以模拟请求和验签。我一般用它来验证签名串构造是否正确。把拼串内容、私钥、序列号填进去看生成的签名和代码里的是否一致。如果在线工具能通过代码里报 401那问题一定在 HTTP 请求构造上比如 header 拼写、Content-Type 设置。另一个技巧是用curl手动发一次请求把Authorization头完整打印出来和代码里生成的对比。curl命令里注意-d的 JSON 不要有换行和多余空格否则签名串会变。curl -X POST https://api.mch.weixin.qq.com/v3/pay/partner/transactions/jsapi \ -H Authorization: WECHATPAY2-SHA256-RSA2048 mchid\1900000001\,nonce_str\abc123\,timestamp\1700000000\,serial_no\ABC123\,signature\xxx\ \ -H Content-Type: application/json \ -H Accept: application/json \ -d {sp_appid:wx123,sp_mchid:1900000001,sub_mchid:1900000002,description:test,out_trade_no:test001,notify_url:https://yourdomain.com/notify,amount:{total:100,currency:CNY},payer:{openid:o123}}回调验证可以用微信提供的回调测试工具模拟加密回调体看你的解密逻辑能不能正确还原。我习惯在本地写一个单元测试把真实的回调体、nonce、associated_data 存成测试用例每次改代码跑一遍确保解密和验签逻辑没被改坏。最后一个习惯所有和微信支付相关的配置项在启动时做一次校验。私钥文件是否存在、序列号是否为空、ApiV3Key 是否 32 位这些检查放在Startup或Program里启动就报错别等到调接口才发现。这个习惯帮我省了很多次线上排查的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑