ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

netCore微信支付源码实战:V3签名、服务商模式、分账与退款避坑指南

netCore微信支付源码实战:V3签名、服务商模式、分账与退款避坑指南 简介本资源面向.NET Core开发者聚焦微信支付V3服务商模式下的完整支付链路实现涵盖普通支付、支付回写、退款、分账给个人、服务商模式支付与回写、服务商分账给子商户以及V3退款等核心场景适合需要对接微信支付分账体系的中高级后端工程师参考。压缩包共696个文件约34.16MB以383个dll、70个cs源码、62个pdb调试文件、45个json配置及若干csproj、sln工程文件为主附带cache、config、xml等构建与配置产物构成可直接编译运行的完整解决方案。目前已有1369人学习下载。源码按WechatPay、PayService、SugarHelper等模块拆分读者可从中获取V3接口签名与证书管理、服务商与子商户分账参数组装、支付回调验签与幂等处理、退款流程编排等可复用实现并借助工程结构快速定位支付、分账、回写各环节代码减少从零搭建支付模块的试错成本。1. 从一堆缓存文件说起这套 netCore 微信支付源码到底能跑通什么拿到一个源码包解压后第一眼看到的不是Startup.cs也不是appsettings.json而是一堆*.csprojAssemblyReference.cache、*.assets.cache——WechatPay.csprojAssemblyReference.cache、PayCommon.csprojAssemblyReference.cache、SugarHelper.assets.cache、PayService.assets.cache反复出现。很多人到这一步就犯嘀咕这到底是能直接编译的工程还是别人bin/obj目录没清干净就打包发出来的半成品先说结论这些缓存文件本身是 MSBuild 和 NuGet 还原时生成的中间产物它们的出现恰恰说明这个包是从一个真实编译过的 netCore 解决方案里直接拷出来的WechatPay、PayCommon、PayService、SugarHelper这几个工程名已经把模块边界交代得很清楚——支付主逻辑、公共封装、业务服务层、ORM 辅助各占一块。这套源码要解决的是 netCore 环境下接微信支付时最容易被卡住的那几段普通支付、微信支付 V3 接口、服务商模式支付、支付结果回写、退款以及分账包括分账给个人、服务商模式分账给子商户。适合谁适合已经能用 netCore 起一个 Web API、但一碰到微信 V3 的签名验签、平台证书、服务商sub_mchid参数就头大的一线后端。它不教你 C# 语法也不讲微信支付的产品政策它给的是能对着改、能跑通回调的工程骨架。下面按「先搞懂它怎么组织 → 再动手把支付和回写跑起来 → 最后把分账和退款这些坑填掉」的顺序拆。2. 工程结构与 V3 签名机制先弄明白钱是怎么被安全传出去的2.1 四个工程各管什么别一上来就全局搜索WechatPay是核心V3 的 HTTP 客户端、签名器、验签器、证书管理器基本都落在这里PayCommon放的是跨支付渠道的公共模型和工具比如统一下单的请求/响应实体、金额转换、订单号生成PayService是业务编排层把「下单 → 回写 → 退款 → 分账」串成可调用的服务方法SugarHelper从命名看是配合 SqlSugar 做数据访问的辅助封装订单表、退款流水、分账记录的落库大概率走它。这种分层的好处是你换 ORM 只动SugarHelper换支付渠道只动PayCommon的抽象WechatPay里的签名逻辑几乎不用碰。我一般接手这类源码第一步不是急着dotnet run而是先确认目标框架和依赖版本因为 V3 接口对System.Text.Json和HttpClient的行为比较敏感。# 先看解决方案里各工程的目标框架确认是 net6/net7 还是 netcoreapp3.1 dotnet --list-sdks # 还原依赖观察有没有版本冲突 dotnet restore WechatPay.sln # 只编译不运行先把编译错误暴露出来 dotnet build WechatPay.sln -c Debugdotnet restore会重新生成那些.assets.cache所以原包里带的缓存文件其实可有可无真正决定能不能编译的是.csproj里写的PackageReference。如果还原时报某个微信 SDK 版本找不到优先检查 NuGet 源而不是去删缓存。dotnet build通过之后再去看appsettings.json里预留了哪些配置项——通常会有mchid、appid、serialNo、privateKeyPath、apiV3Key这几项服务商模式还会多出sub_mchid、sub_appid。2.2 V3 签名和验签整个支付链路里最不能想当然的一环微信支付 V3 和 V2 最大的区别是V2 用 MD5/HMAC 拼签名串V3 改用 SHA256-RSA并且请求和应答都要验。签名串的构造规则是「HTTP 方法\nURL路径\n时间戳\n随机串\n请求体\n」每一行以\n结尾最后一行请求体也要带\nGET 请求请求体为空但仍保留换行。这个细节错一个字符返回就是 401 或签名错误。// 构造 V3 请求签名串的核心逻辑示意字段名以工程内实际实现为准 var method POST; var urlPath /v3/pay/transactions/jsapi; // 注意带 query 时要连 query 一起拼 var timestamp DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); var nonce Guid.NewGuid().ToString(N); var body JsonSerializer.Serialize(requestModel); // 签名串每行以 \n 结尾最后一行也要有 \n var message ${method}\n{urlPath}\n{timestamp}\n{nonce}\n{body}\n; // 用商户私钥做 SHA256withRSA 签名再 Base64 var signature RsaSign(message, merchantPrivateKey); // 组装 Authorization 头 var auth $WECHATPAY2-SHA256-RSA2048 mchid\{mchid}\, $nonce_str\{nonce}\,timestamp\{timestamp}\, $serial_no\{serialNo}\,signature\{signature}\;urlPath这里有个高频翻车点如果请求带查询参数签名串里的路径必须包含?后面的完整 query且顺序要和实际发送一致。serialNo是商户证书序列号不是平台证书序列号两者混用会直接验签失败。应答验签则要用微信平台证书公钥而平台证书本身需要通过/v3/certificates接口下载并用apiV3Key做 AES-256-GCM 解密——这一步在WechatPay工程里通常有独立的证书管理器建议做成定时刷新并缓存别每次请求都去拉。提示私钥文件不要提交进 Git配置里存路径运行时用File.ReadAllText读取apiV3Key是 32 位长度不对解密平台证书必失败。3. 普通支付与服务商模式支付下单、回写、验签一条龙3.1 普通 JSAPI/APP 下单与支付结果回写普通支付的下单接口是/v3/pay/transactions/jsapi小程序/公众号或/v3/pay/transactions/app请求体里out_trade_no、amount.total单位分、payer.openid是必填。下单成功后拿到prepay_id再按小程序或 APP 的规则做二次签名返回给前端唤起支付。真正容易出问题的是回写微信会把支付结果 POST 到你的notify_url这个回调也必须验签验的是请求头里的Wechatpay-Signature用的是平台证书。// 支付结果回写处理示意先验签再解密再幂等落库 [HttpPost(notify)] public async TaskIActionResult Notify() { // 1. 读取原始请求体注意不能先被模型绑定消费掉 Request.EnableBuffering(); using var reader new StreamReader(Request.Body, Encoding.UTF8); var body await reader.ReadToEndAsync(); Request.Body.Position 0; // 2. 用平台证书验签失败直接返回失败别继续处理 var ok _verifier.Verify(Request.Headers, body); if (!ok) return BadRequest(new { code FAIL, message 验签失败 }); // 3. 解密 resource 里的密文AES-256-GCMkey 为 apiV3Key var notify _decryptor.Decrypt(body); // 4. 幂等用 out_trade_no 查订单已处理过就直接返回成功 if (await _orderService.IsPaid(notify.out_trade_no)) return Ok(new { code SUCCESS, message 成功 }); // 5. 更新订单状态、写支付流水 await _orderService.MarkPaid(notify); return Ok(new { code SUCCESS, message 成功 }); }EnableBuffering这行是血泪经验默认情况下请求体只能读一次如果你在验签前先让 MVC 做了模型绑定body就是空的验签必然失败。返回给微信的报文必须是{code:SUCCESS,message:成功}这种结构返回非 200 或 code 不为 SUCCESS微信会按策略重试重试次数多了会触发告警。幂等判断一定要在解密之后、落库之前做否则重复回调会把订单状态和流水写乱。3.2 服务商模式多出来的 sub_mchid 和 sub_appid服务商模式也叫 Partner 模式和普通直连最大的差别是请求里要带sp_mchid服务商商户号和sub_mchid子商户号下单接口路径变成/v3/pay/transactions/jsapi但 body 里多一层sp_appid/sub_appid的区分。签名用的仍然是服务商的私钥和证书序列号但回写验签、退款、分账都要在服务商身份下发起。很多人在这一步翻车是因为把子商户的sub_mchid填到了mchid位置或者sp_appid和sub_appid用反导致返回「appid 与 mchid 不匹配」。// 服务商模式 JSAPI 下单请求体关键字段示意 var req new { sp_appid 服务商appid, sp_mchid 服务商商户号, sub_appid 子商户appid, // 子商户有独立 appid 时填 sub_mchid 子商户号, description 测试订单, out_trade_no orderNo, notify_url https://your.domain/notify, amount new { total 1, currency CNY }, payer new { sub_openid userOpenId } // 服务商模式下用 sub_openid };payer字段在服务商模式下要用sub_openid子商户 appid 下的 openid直连模式才是openid。这个字段名错了返回的是「openid 与 appid 不匹配」但错误信息不会直接告诉你该用哪个只能对着文档逐字核。服务商模式的回写和直连一样要验签解密区别在于解密后的sub_mchid要和你库里的子商户绑定关系对上别拿着 A 子商户的回调去更新 B 子商户的订单。3.3 退款V3 退款接口和状态查询V3 退款走/v3/refund/domestic/refunds请求体里out_trade_no或transaction_id二选一out_refund_no是退款单号自己生成要唯一amount里refund是退款金额、total是原订单金额单位都是分。服务商模式退款还要带sub_mchid。退款是异步的提交成功只代表受理最终结果要等退款回写或主动查/v3/refund/domestic/refunds/{out_refund_no}。// 发起退款示意 var refundReq new { sub_mchid 子商户号, // 服务商模式必填 out_trade_no orderNo, out_refund_no refundNo, reason 用户申请退款, amount new { refund 1, total 1, currency CNY }, notify_url https://your.domain/refund-notify }; // POST /v3/refund/domestic/refunds退款回写同样要验签解密refund_status为SUCCESS才算真正到账。常见坑是退款金额大于原订单可退金额、同一out_refund_no重复提交、退款回写没做幂等导致重复更新退款流水。我一般会在退款表上对out_refund_no建唯一索引从数据库层面兜住重复。注意退款和支付的notify_url建议分开退款回写的报文结构和支付回写不同混在一个接口里解析容易出错。4. 分账给个人与服务商分账给子商户最容易踩坑的一段4.1 分账的前置条件先确认订单真的能分分账不是下单后随时能分的。微信要求订单支付成功、且商户号已开通分账权限同时分账要在订单支付成功后的约定时间内发起具体时限以当前接口文档为准。分账接收方要先通过/v3/profitsharing/receivers/add添加接收方类型分MERCHANT_ID商户号和PERSONAL_OPENID个人 openid。分账给个人用的就是PERSONAL_OPENID并且要传对应的openid和type。// 添加分账接收方分账给个人示意 var receiver new { appid 服务商或商户appid, type PERSONAL_OPENID, account 接收人openid, relation_type USER // 个人接收方常见关系类型 }; // POST /v3/profitsharing/receivers/addtype和account必须匹配填PERSONAL_OPENID时account是 openid填MERCHANT_ID时account是商户号。填错会返回「接收方类型与账号不匹配」。添加成功后接收方信息会绑定到该商户号下后续分账直接用。4.2 发起分账与服务商分账给子商户分账请求走/v3/profitsharing/orders请求体里transaction_id是原支付单号out_order_no是分账单号receivers是接收方列表每个接收方带type、account、amount、description。服务商模式分账给子商户时接收方type用MERCHANT_IDaccount填子商户号同时请求要带sub_mchid。// 服务商模式分账给子商户示意 var profitReq new { sub_mchid 子商户号, appid 服务商appid, transaction_id 微信支付订单号, out_order_no 分账单号, receivers new[] { new { type MERCHANT_ID, account 子商户号, amount 1, // 单位分 description 分账给子商户 } }, unfreeze_unsplit true // 不分账的剩余资金是否解冻按业务决定 }; // POST /v3/profitsharing/orders分账金额之和不能超过订单可分金额且单笔分账有最低金额限制。unfreeze_unsplit这个参数很关键如果设为false剩余未分账资金会一直冻结后续想再分或解冻要走额外接口设为true则分账完成后剩余资金自动解冻。我一般建议业务上想清楚再设别默认false把资金冻住。分账结果也是异步的要查/v3/profitsharing/orders/{out_order_no}或等回写。4.3 分账回退与查询分账之后如果要退回走/v3/profitsharing/return-orders请求体里带out_order_no原分账单号、out_return_no回退单号、return_mchid回退方商户号、amount。回退同样有金额和时限约束。查询分账结果用/v3/profitsharing/orders/{out_order_no}返回里status为FINISHED才算成功。这一段没有太多花哨核心就是单号唯一、金额对得上、状态查得到。提示分账相关的单号分账单号、回退单号建议和支付单号一样做全局唯一并落库出问题时能靠单号把整条链路串起来。5. 避坑与排查这几处不提前处理上线必翻车5.1 验签失败但不知道错在哪现象回写接口一直返回验签失败日志里只有「签名错误」。原因签名串拼接时 URL 路径没带 query、时间戳和请求头不一致、或者用了错误的证书序列号。解决把签名串原文打印出来注意脱敏逐行核对换行符确认serial_no是商户证书序列号确认平台证书已正确下载并解密缓存。我一般会在验签失败时把「期望签名串」和「实际签名串」的差异打到日志比盲猜快得多。5.2 回写重复导致订单状态错乱现象同一笔订单被更新多次流水表出现重复记录。原因微信回调有重试机制而回写处理没有做幂等。解决在解密后、落库前用out_trade_no或transaction_id查一次订单状态已支付就直接返回成功数据库层对支付流水加唯一约束兜底。5.3 服务商模式参数填错位置现象返回「appid 与 mchid 不匹配」或「openid 与 appid 不匹配」。原因sp_appid/sub_appid用反或payer里用了openid而不是sub_openid。解决服务商模式下服务商自己的 appid 填sp_appid子商户的填sub_appid用户 openid 用sub_openid。对着文档把字段名逐个核一遍别凭记忆。5.4 分账金额或接收方类型不对现象分账接口返回「接收方不存在」或「分账金额超限」。原因接收方没先添加或type与account不匹配或分账总额超过可分金额。解决先调接收方添加接口并确认成功再发起分账分账前查一次订单可分金额别硬编码。5.5 私钥和 apiV3Key 配置错误现象启动就报解密失败或签名失败。原因私钥文件路径不对、私钥格式不是 PKCS#8、apiV3Key长度不是 32 位。解决确认私钥是-----BEGIN PRIVATE KEY-----开头的 PKCS#8 格式apiV3Key严格 32 位配置项从环境变量或配置中心读取别写死在代码里。6. 进阶把回写和分账做成可验证、可回放的闭环这套源码真正值钱的地方不是它帮你省了几行签名代码而是它把「下单 → 回写 → 退款 → 分账」串成了一条能落库、能查状态的链路。但上线前我强烈建议做一件事把回写和分账做成可回放。具体做法是在回写处理里加一个「原始报文落库」的步骤把验签通过后的解密报文原样存一张pay_notify_log表字段至少包括out_trade_no、event_type、raw_body、created_at。这样一旦线上出现状态不一致你可以拿原始报文在测试环境重放而不是对着日志猜。// 回写报文落库便于回放排查示意 await _notifyLogRepo.InsertAsync(new PayNotifyLog { OutTradeNo notify.out_trade_no, EventType notify.event_type, // TRANSACTION.SUCCESS / REFUND.SUCCESS 等 RawBody body, // 解密后的明文 CreatedAt DateTime.UtcNow });验证方法上我一般会走三步第一步用微信支付提供的调试能力或沙箱环境发一笔最小金额订单确认下单和回写通第二步手动构造一笔退款确认退款回写和状态查询一致第三步用一笔已支付订单发起分账给个人再查分账结果确认status为FINISHED。这三步走完基本能覆盖 80% 的线上问题。验证项关键观察点失败时先看哪下单返回 prepay_id无签名错误签名串、serial_no支付回写验签通过、订单状态更新请求体是否被提前消费、幂等退款受理成功、回写 refund_status退款金额、out_refund_no 唯一性分账给个人接收方添加成功、分账 FINISHEDtype/account 匹配、可分金额服务商分账sub_mchid 正确、分账成功sp/sub 参数位置从那以后我每次接微信支付都会先把「原始报文落库 幂等 单号唯一」这三件事做完再写业务逻辑因为支付这条链路上后悔药基本买不到。希望帮到你。本文还有配套的精品资源点击获取
返回列表