新闻详情

新闻详情

首页 / 资讯中心 / 详情

.NET Core接入微信支付V3服务商模式:签名、下单、分账与退款实战

发布时间:2026/10/1 21:42:14来源:尧图网络
.NET Core接入微信支付V3服务商模式:签名、下单、分账与退款实战
简介面向.NET Core开发者的微信支付V3服务商模式完整接入示例覆盖普通支付、服务商模式支付、支付回写、退款、分账给个人及分账给子商户等高频难点。资源以源码工程方式组织共696个文件、约34.16MB其中dll为运行时依赖、cs为项目源码、json为配置与数据文件、config与props为环境设置同时包含sln工程文件和发布配置便于直接打开调试与按模块裁剪。已有1368人学习适合正在处理微信支付多商户分账或服务商模式回写的后台开发人员。代码按PayCommon、PayService、WechatPay等工程拆分清晰区分公共模型、业务逻辑与支付接口层能够帮助快速理解V3接口签名、回调验签、分账参数组装及退款流程并结合现有代码减少重复踩坑。1. 服务商模式不是普通商户换了个号先搞清楚这套账怎么走做平台类系统的 .NET Core 团队早晚要碰微信支付 V3 服务商模式。它解决的不是「谁能收钱」而是「谁的钱算谁的」平台用一个服务商商户号管理所有入驻商户支付成功后资金先进子商户平台通过分账把属于自己那部分拿回来退款再原路还回去。很多人第一次接时以为只是把直连商户号换成服务商商户号结果卡在签名、sub_mchid 传递和回调解密三座山上。下面的内容把你需要动手的事按顺序拆开证书与签名、JSAPI 下单与支付回写、分账与退款、以及我踩过的那些报错到底在说什么。适合正在做多商户电商、SaaS 收银、B2B2C 分润系统的开发者也适合已经接完直连模式想平移过来的团队。2. 先把 V3 服务商模式的钥匙配齐证书、密钥与签名封装2.1 服务商模式要配齐哪四样证书序列号、APIv3 密钥、商户私钥、平台证书V3 和 V2 最大区别是它不用 MD5 签名而是「商户私钥签名 平台公钥验签」的双向认证。所以配置不是填一个 key 就完事而是四样东西配置项获取位置用途服务商商户号 sp_mchid商户平台首页请求体与签名头里的商户标识子商户号 sub_mchid服务商平台-商户进件审核通过后标识每笔交易资金归属APIv3 密钥商户平台-API安全-APIv3密钥AES-256-GCM 解密回调、生成敏感字段商户 API 证书/私钥商户平台-API安全-申请证书请求签名SHA256-RSA2048平台证书或微信支付公钥商户平台-API安全 下载/配置验证微信回调签名关于证书加载.NET Core 最常见的做法是直接加载 p12// p12 密码是商户号Exportable 才能取出私钥 var cert new X509Certificate2(apiclient_cert.p12, 1900000001, X509KeyStorageFlags.Exportable); RSA privateKey cert.GetRSAPrivateKey(); string serialNo cert.SerialNumber;这段代码跑起来之前有两点要先确认。第一serialNo是签名字段里要用的证书序列号虽然 .NET 返回的 SerialNumber 可以直接用但我更建议从商户平台「API安全」页面复制因为部分旧证书导出的 SerialNumber 大小写和平台展示不一致而微信是按字符串比对这个字段的。第二如果证书加载报CryptographicException多数是 p12 在生成时用了非标准加密或者你拿到的其实是 .pem。这时候用 openssl 先转一遍openssl pkcs12 -in apiclient_cert.p12 -nodes -out apiclient_cert.pem然后RSA.Create()配合ImportFromPem也能拿到私钥。一句话总结这个环节证书文件、序列号、APIv3 密钥是三个不同的东西别混。实际项目里我会把这些配置放进 appsettings.json 的 WechatPay 节点私钥读取时注意部署在容器里时 p12 文件要作为 secret 挂载而不是打进镜像序列号从配置中心读不要运行时从证书里取因为国密证书和 RSA 证书序列号的展示格式可能不一样。2.2 写一个不被微信拒掉的签名请求签名字符串的拼接顺序V3 的签名算法是被问得最多的但它其实没有玄学就是把五个字段按固定顺序拼起来最后补一个换行// 按微信 V3 规范构造待签名字符串 public static string BuildMessage(string method, string url, long timestamp, string nonce, string body) { // 五个字段HTTP方法、URL、时间戳、随机串、请求体 // 每个字段后面都要跟 \n请求体为空时也要保留这个空行 return ${method}\n{url}\n{timestamp}\n{nonce}\n{body}\n; } public static string SignMessage(string message, RSA privateKey) { byte[] data Encoding.UTF8.GetBytes(message); byte[] signature privateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); return Convert.ToBase64String(signature); }这里有个 90% 的人第一次都会写错的点url是指请求的路径部分比如下单接口的/v3/pay/partner/transactions/jsapi不带域名也不带协议头。如果 URL 里带 query 参数比如查询退款时/v3/refund/domestic/refunds/REFUND001?sub_mchid1900000002那 query 原样保留而且顺序不能乱。我吃过一次亏用HttpRequestMessage.RequestUri.PathAndQuery拼 URL结果被网关跳转了一次PathAndQuery 里多了一段路由前缀签名就过不了。所以现在一律用写死的常量接口路径去拼签名不从请求对象里现取。签完名之后组装请求头public static string BuildAuthHeader(string mchId, string serialNo, long timestamp, string nonce, string signature) { return $WECHATPAY2-SHA256-RSA2048 $mchid\{mchId}\, $nonce_str\{nonce}\, $timestamp\{timestamp}\, $serial_no\{serialNo}\, $signature\{signature}\; }mchid这个字段在服务商模式下必须填服务商商户号不管请求体里带不带 sub_mchid。后面避坑章节我会再展开Authorization 头的 mchid 填错是「签名错误」最高频的原因。把这三段拼到一起一个能正常发出 V3 请求的 HttpClient 封装就有了雏形。至于随机串 nonce用Guid.NewGuid().ToString(N)就够不用引额外的库。时间戳用DateTimeOffset.UtcNow.ToUnixTimeSeconds()注意是秒不是毫秒。签名耗时通常在 1ms 内性能不是瓶颈不用做缓存。2.3 服务商接口的 URL 与请求头差异mchid 和 sub_mchid 各就各位服务商模式和直连商户模式在接口形态上有两个明显差异提前看清楚能省不少调试时间。第一个差异在接口路径。支付类接口服务商版固定多一段partner比如直连下单是/v3/pay/transactions/jsapi服务商下单是/v3/pay/partner/transactions/jsapi。退款和分账接口路径则和直连共用比如/v3/refund/domestic/refunds、/v3/profitsharing/orders区别在于请求体里必须带sub_mchid。第二个差异在参数归属。直连模式你只有一个 mchid怎么传都对服务商模式下每个请求都可能同时出现sp_mchid、sub_mchid、appid、sub_appid四个标识。我的经验是记一句话签名头里只认 sp_mchid请求体里按接口文档放业务参数查询接口的 query string 里如果要求 sub_mchid那它必须和下单时一致漏传或错传会直接报「商户号不存在」或者让你查到一个根本不存在的数据。场景签名头 mchid请求体/查询参数JSAPI 下单sp_mchidsp_appid, sp_mchid, sub_mchid, payer.sub_openid查询分账sp_mchidquery: sub_mchid, out_order_no发起退款sp_mchidsub_mchid, out_trade_no/transaction_id, out_refund_no我在项目里会单独封装一个WechatPayClient把 sp_mchid 和 sub_mchid 分开传签名头永远从配置里取 sp_mchid请求体参数由业务层组装。这样能避免在接口调用处随手写错一个 ID 导致线上资金归属跑偏。服务商模式的另一个差异是获取 openid 的渠道。JSAPI 下单要求的 sub_openid 必须来自服务商 AppID 下的登录态。如果子商户有自己的小程序那么下单参数里还要带上 sub_appid 和对应的一套 openid 体系。先想清楚谁的小程序发起支付再决定填哪个 openid否则必报 openid 与 appid 不匹配。到这里证书能加载、签名能产生、请求头能拼、接口地址的分工也清楚了下一步就是真正下一单。3. 支付下单到回写落库JSAPI 参数与异步通知解密3.1 JSAPI 下单的必传参数sp_appid、sub_mchid、sub_openid 缺一不可服务商模式的 JSAPI 下单请求体长这样var requestBody new { sp_appid wx1234567890abcdef, // 服务商的小程序 AppID sp_mchid 1900000001, // 服务商商户号 sub_mchid 1900000002, // 子商户号资金进这个号 description 订单20250101001, // 商品描述会展示给用户 out_trade_no ORDER2025010100001, // 商户订单号自己生成全局唯一 time_expire 2025-01-01T23:59:5908:00, // 超时时间格式固定 attach uid1001, // 透传字段回调会原样带回来 notify_url https://api.example.com/pay/notify, amount new { total 100, // 金额单位是分100 就是 1 元 currency CNY }, payer new { sub_openid oUpF8uMuAJO_M2pxb1Q9zNjWeS6o // 用户在小程序里的 openid } };这段代码里有几个参数值得专门说明。sub_mchid是子商户号如果你做的是多商户平台这里不能写平台自己的号payer.sub_openid必须是小程序真实拿到的 openid不能从前端传上来拼否则下单会报「用户与appid不匹配」。notify_url必须是公网可访问的 https 地址微信只认 443 端口。下单成功返回的是prepay_id这个值不能直接丢给前端还需要用它生成调起支付参数// 用 prepay_id 构造调起 JSAPI 支付的参数 var payParams new Dictionarystring, string { [appId] wx1234567890abcdef, [timeStamp] timestamp.ToString(), // 秒字符串 [nonceStr] nonce, [package] $prepay_id{prepayId}, // 注意整个值不是只有 prepay_id 后面那段 [signType] RSA }; // paySign 的签名串是 appId、timeStamp、nonceStr、package 四段 string payMessage ${payParams[appId]}\n{payParams[timeStamp]}\n{payParams[nonceStr]}\n{payParams[package]}\n; string paySign SignMessage(payMessage, privateKey);提示前端如果报「用户态签名signature错误」先查三件事timeStamp 是不是字符串类型的秒、nonceStr 和签名时用的是不是同一个值、package 里有没有写全prepay_id前缀。我见过最多的情况是把签名串里的 package 写成了${prepayId}少了前缀。这个用户态签名错误在微信开发者工具里几乎天天有人问根因就一句话调起支付时前端参与的字段必须是后端签名时用到的同一个字段任何一端少拼或多拼一个字符都会翻车。实际项目里我一般把 prepay_id、paySign、timeStamp、nonceStr 一起返回给前端前端原样传给wx.requestPayment不做任何拼接。另外提醒一句有同事问过 uni-app 打包成 App 后和小程序端支付参数是不是一样。答案是不一样。App 端走的是 App 支付/v3/pay/transactions/app调起方式也从wx.requestPayment变成 App SDK 的 provider 参数只有小程序端用的才是 JSAPI 这一套。如果你的业务同时有两个端后端要按支付场景分别下单别复用 JSAPI 下单结果去调 App 支付。3.2 异步通知处理的完整链路验签、解密 resource、转业务对象支付成功后微信会往notify_url发 POST 通知真正的业务数据在resource字段里且是加密的。处理链路分两步先用平台公钥验签再用 APIv3 密钥解密。验签代码// 回调验签注意验签串里没有 URL只有时间戳、随机串、请求体 public static bool VerifyNotify(string timestamp, string nonce, string body, string signature, RSA platformPublicKey) { string message ${timestamp}\n{nonce}\n{body}\n; return platformPublicKey.VerifyData( Encoding.UTF8.GetBytes(message), Convert.FromBase64String(signature), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); }微信回调头里的Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature、Wechatpay-Serial四个头都要拿到。Wechatpay-Serial用来告诉你是哪张平台证书发的签名如果你用的是微信支付公钥模式这个值对应PUB_KEY_ID_...。平台公钥从哪来要么下载平台证书文件要么用微信支付公钥模式在商户平台配置好后把公钥内容存进配置中心。验签通过后解密 resource// 解密 V3 回调的 resource 字段.NET 6 使用 AesGcm public static string DecryptResource(string ciphertext, string nonce, string associatedData, string apiV3Key) { byte[] key Encoding.UTF8.GetBytes(apiV3Key); // APIv3 密钥32 字节 byte[] nonceBytes Encoding.UTF8.GetBytes(nonce); // 注意用 UTF8不是 ASCII byte[] associatedBytes Encoding.UTF8.GetBytes(associatedData ?? ); byte[] fullCipher Convert.FromBase64String(ciphertext); // 微信的密文格式实际密文 尾部 16 字节 GCM 认证标签 byte[] encrypted fullCipher[..^16]; byte[] tag fullCipher[^16..]; var aes new AesGcm(key, 16); byte[] plaintext new byte[encrypted.Length]; aes.Decrypt(nonceBytes, encrypted, tag, plaintext, associatedBytes); return Encoding.UTF8.GetString(plaintext); }这段代码每次都要解释的细节是微信返回的ciphertext是一整段 base64它内部已经包含了 GCM 的 tag而且 tag 固定是最后 16 字节。很多人直接拿整段去解密抛AuthenticationTagMismatchException后开始怀疑人生其实就是没做拆分。nonce在报文里是明文字符串转成 byte 直接用 UTF8 编码不要做 Hex 解码。解密后你会得到一段 JSON核心字段是out_trade_no、transaction_id、trade_state、amount.total。trade_state等于SUCCESS才说明支付真的成功了其他的都别信。回写时把transaction_id存下来它是微信侧唯一的支付单号后续退款要用。注意验签和解密失败时微信会认为你没处理成功然后按频率重试。所以不要在验签前就返回 SUCCESS也别把解密异常吞掉直接回 200。如果验签失败先看 Wechatpay-Serial 对应的证书是不是当前正在用的那张平台证书轮换期间最容易出现新旧证书混用。3.3 支付回写落库的幂等同一个 notify 收到两次不能插两条流水微信的异步通知不是只发一次失败重试、网络重发都会导致同一个支付成功事件到达多次。所以回写落库的幂等是上线前必须解决的问题。我的做法是两层保险。第一层用数据库唯一索引兜底表结构里把out_trade_no和transaction_id建联合唯一索引第二层在代码里先查后插// 回写落库先查是否已存在存在则直接返回成功 var exists db.PayFlows.Any(f f.OutTradeNo outTradeNo f.TransactionId transactionId); if (exists) { // 已经处理过幂等返回不再更新业务状态 return JsonResult(new { code SUCCESS, message 成功 }); } var flow new PayFlow { OutTradeNo outTradeNo, TransactionId transactionId, SubMchid subMchid, TotalAmount amountTotal, // 单位分落库统一用分避免精度问题 Status PAID, PaidAt successTime }; db.PayFlows.Add(flow); db.SaveChanges();先查再插在高并发下会有竞态两条线程同时查到不存在就会各插一条所以唯一索引不是可选项。注意全文中金额我坚持用分存储数据库字段用 int 或 bigint不做 decimal因为微信的所有金额字段都是整数分回传时不需要小数。前端展示再除 100避免浮点误差。这是一个很小的习惯但能省掉后面账单对不上的麻烦。到这里「下单 → 回调 → 解密 → 落库」这半边链路已经走通下面进入真正的平台级功能分账和退款。4. 分账与退款服务商模式下的资金操作与回写4.1 分账的调用顺序请求分账、查询分账、完结分账分账是服务商模式里最不平滑的一环因为微信的分账是「账变」而不是「钱动」接口返回成功不代表余额就到了。先记住完整调用顺序请求分账 → 查询分账轮询或等通知 → 订单完结后调用完结分账。请求分账的常见写法// 服务商模式请求分账把子商户的钱分给另一个商户 var requestBody new { sub_mchid 1900000002, // 资金从哪个子商户出 appid wx1234567890abcdef, // 下单时的 appid out_order_no PROFIT202501010001, // 分账单号自己生成 receivers new[] { new { type MERCHANT_ID, // 接收方类型商户号或个人 openid account 1900000003, // 接收方商户号 amount 30, // 分给接收方 0.3 元单位分 description 平台技术服务费 } } };这里要注意几个约束。receivers是数组单次最多 50 个所有接收方金额加起来不能超过订单实付金额。服务商默认分账比例上限是 30%具体以商户平台展示为准想调高需要联系微信支付。type MERCHANT_ID的接收方必须已经追加到该子商户的分账接收方列表里否则会报「分账接收方不存在」。追加接收方接口是POST /v3/profitsharing/receivers/add上线前要把所有参与分润的商户号先加一遍。分账模式在商户平台可以设成自动分账或手动分账。自动分账适合每笔订单分润规则固定的场景支付成功微信按预设比例直接分手动分账适合按订单金额事后算钱的场景。我的项目里绝大多数用自动分账加按订单维度查询因为免去了漏调完结分账的风险但自动分账的缺点是分账比例是全局的没法针对单个子商户差异化配置这时候手动分账更灵活。请求分账发出去后接口会同步返回分账结果但微信内部真正把资金划过去是有延迟的。正确做法是接着查分账// 查询分账结果GET 请求参数在 query string 里 // 路径示例示意不直接请求 GET /v3/profitsharing/orders/PROFIT202501010001?sub_mchid1900000002查询结果里每个 receiver 有自己的result字段SUCCESS才是分账成功PENDING表示处理中。我一般写一个定时任务对状态为处理中的分账单每 30 秒查一次最多查 10 次仍然没到账就发告警让运营介入。把查询当作主手段而不是依赖回调是因为分账通知需要单独配置且部分场景下并不可靠这是我多次踩坑后的体感。最后一个动作是完结分账订单退款期结束、不会再产生售后时调用POST /v3/profitsharing/orders/{out_order_no}/finish把剩余资金全部归子商户。不调用完结分账的话这笔订单的分账状态会一直挂在「待完结」影响后续资金操作。要注意支付成功后的分账有效期是有限的订单超过时限没分账会失去分账资格完结分账后也不能再对该订单发起分账。4.2 分账通知与支付通知怎么区分event_type 不同处理链路别混分账结果通知是可以配置的微信会往你设置的回调地址推送分账结果。但很多团队图省事把分账通知和支付通知配成同一个 URL然后发现解密后的 JSON 结构对不上。两者的event_type完全不同支付通知是TRANSACTION.SUCCESS分账通知是分账成功或退回相关类型。如果共用一个入口第一步不是解密而是先看顶层event_type然后分发到不同处理函数// 统一回调入口根据 event_type 分发处理链路 public async TaskIActionResult HandleNotify() { string body await new StreamReader(Request.Body).ReadToEndAsync(); // 先验签再解析 event_type var notify JsonSerializer.DeserializeNotifyRoot(body); if (notify.EventType.StartsWith(TRANSACTION)) { return ProcessPaymentNotify(body); // 支付回写 } if (notify.EventType.Contains(PROFIT)) { return ProcessProfitSharingNotify(body); // 分账回写 } return JsonResult(new { code SUCCESS, message 成功 }); }分账通知解密后的业务 JSON 里没有out_trade_no有的是order_id微信分账单号和out_order_no你的分账单号以及接收方数组。所以不要用支付通知的处理函数去解析分账报文字段错位时很容易拿到空的out_trade_no然后白报一轮错。我个人的项目里还是坚持把分账通知和支付通知配到不同的路径/pay/notify和/profit/notify分开省得在公共代码里加一堆 if 判断。只有当子商户需要自己接收通知时才共用入口那时才做分发。4.3 退款下单与退款异步通知out_refund_no 与 refund_status 的对应退款接口和直连模式路径相同但服务商模式下 body 里要带sub_mchid而且退款优先传transaction_id// 服务商模式退款优先用微信支付单号不使用商户订单号 var requestBody new { sub_mchid 1900000002, transaction_id 420000123420250101000001, // 支付回调里落库的 transaction_id out_refund_no REFUND202501010001, // 退款单号自己生成 reason 用户申请退款, notify_url https://api.example.com/pay/refund-notify, amount new { refund 100, // 退款金额单位分 total 100, // 原订单金额单位分 currency CNY } };为什么优先用transaction_id而不是out_trade_no因为out_trade_no是商户侧订单号如果订单系统里同一个业务订单被重新支付过比如支付超时后用户又发起一单用商户订单号退款可能退到错误的支付单上。transaction_id是微信支付单号一单一号绝对不会串。另外reason字段是必填的虽然长度限制比较宽松但别写「随便退」这种话售后审计时会很难看写清楚「用户七天无理由退货」这类描述。退款的结果也是异步的。下单接口返回的是受理成功真正退没退成功要看退款单状态或回调。退款状态机就四个PROCESSING处理中、SUCCESS成功、CLOSED关闭、ABNORMAL异常。其中CLOSED不等于失败通常是全额退款被关闭比如原路退回失败且用户换了支付方式这时候要转线下处理。退款回调的结构和支付回调一样需要验签、解密但event_type是REFUND.SUCCESS这类解密后的 JSON 核心字段是out_refund_no、refund_id、refund_status。回写时要注意refund_status等于SUCCESS时再更新订单为已退款PROCESSING状态可以落库但不要改订单状态否则用户看到退款成功钱还在路上客诉就来了。退款单里传的notify_url只对该退款单生效如果没传就会用商户平台配置的默认退款回调地址。如果 30 分钟还没收到回调用GET /v3/refund/domestic/refunds/{out_refund_no}?sub_mchid...查一次兜底。5. 避坑指南V3 服务商模式最容易翻车的 5 个细节5.1 现象报错「签名错误」但同一套签名在直连商户模式能过这是服务商模式接入时被问得最多的报错。现象很迷惑代码在直连模式下跑得好好的换成服务商商户号和子商户号就报签名错误。原因基本是 Authorization 头里的mchid写成了子商户号。签名串本身用的是商户私钥私钥属于服务商但头里的 mchid 也必须和服务商商户号一致两个字段任何一个对不上微信都会返回签名错误。解决方法是把mchid、serial_no、私钥三者放到同一个配置对象里从同一个来源读取不要分别在两个地方填。第二个可能的原因是你用了 GET 请求去调退款查询但签名串里的 URL 把 query string 的顺序拼错了。微信对 URL 的比对是精确的?sub_mchidxxxout_refund_noyyy和?out_refund_noyyysub_mchidxxx是两个字符串签名自然不同。解决办法是用文档给的参数顺序别用字典自动拼接。5.2 现象回调能收到但 resource 解密失败回调收到了验签也过了解密时抛AuthenticationTagMismatchException。这个异常十有八九不是密钥错了而是ciphertext没拆分微信给的密文 实际密文 16 字节 tag必须拆开再调用 AesGcm.Decrypt。APIv3 密钥错误的话也是这个异常但概率低得多因为 APIv3 密钥是自己配的复制粘贴时不会错而密文格式问题是代码层普遍漏洞。还有一个安静的小坑nonce字段在报文里是字符串有的实现会对它做 Hex 解码结果对不上。正确做法是Encoding.UTF8.GetBytes(resource.Nonce)不要用 Convert.FromHexString。把这两点写进代码注释里团队里后来维护的人就不会再踩。5.3 现象分账查询显示 SUCCESS 但子商户余额没变分账查询接口返回 SUCCESS子商户在商户平台看到的余额却没变化。先别怀疑接口去商户平台「资金管理-分账明细」里看这笔分账。常见情况分账成功进的是子商户的「可用余额」而你看的是「待结算余额」或者页面默认展示的其他账户。第二个情况是 receiver 的 account 填了一个从未添加的分账接收方接口返回的是「受理成功」实际结算时才发现接收方未签约资金挂起。所以分账前一定要先调用POST /v3/profitsharing/receivers/add把接收方加好并且检查返回里的state是ACTIVE。另外如果分账金额超过了默认 30% 比例请求分账接口本身可能返回成功但资金不会流动查询时能看到异常状态。这类「接口成功、资金没动」的黑匣子问题只能靠分账明细去核对别依赖接口返回值。5.4 现象退款回调先到查询接口反而查不到退款回调先于查询接口返回这在微信 V3 里是真实存在的时序问题。回调通知发出时退款事务已经提交但查询接口可能因为多数据中心同步延迟短暂查不到这条退款单。如果代码里用查询结果覆盖回调结果就会出现「回调显示成功、查询显示不存在、然后被改成失败」的翻车现场。我的处理策略是回调到达先落库状态标记为SUCCESS并记录success_time查询接口的结果只用于补充超时未回调的订单永远不反向覆盖已经成功的回调。如果两份数据冲突以回调数据为准但触发告警人工核对。另一个相关建议是退款回写不要直接改订单状态而是先更新退款单状态再由事件或定时任务推进订单状态这样退款单和订单两个状态机解耦排查问题时少一半干扰。5.5 现象平台证书过期或者收到国密改造相关的验签失败平台证书是有有效期的到期后微信会用新证书签名回调你的验签代码如果还在用旧证书回调验签会失败但请求签名不受影响。解决方法是换成微信支付公钥模式公钥通过商户平台下载回调头里的Wechatpay-Serial会携带公钥 ID你只需要定期替换公钥内容。如果你懒得维护证书轮换直接用公钥模式是省心选项。还有一类新情况微信支付在推进国密算法部分新申请商户拿到的证书是国密证书serial_no、签名算法不再是 RSA。这种情况下 .NET Core 自带的 RSA 验签不适用需要引入支持 SM2/SM3/SM4 的密码库比如 BouncyCastle 的 .NET 版用 SM2 实现同样的签名和验签流程。你在检索里看到的「netcore 国密 sm2 解密」指的就是这个场景。我的建议是如果商户平台提示证书类型为国密先把 RSA 模式下的验签、解密逻辑做好参数化抽象后面替换算法时只换 Provider不换业务代码。这一章写的五个问题覆盖了签名、解密、分账、退款、证书五个环节基本是我在服务商模式项目里最常被问的。下面用最后一章把整个链路变成可以自己验证的流程。6. 用一笔测试单把全链路跑通验证签名、回写、分账、退款6.1 本地自测把真实回调报文喂给解密函数接入阶段因为回调地址必须是公网 https很多团队卡在「本地没法收回调」。我的做法是在本地控制台工程里直接模拟回调报文从商户平台「支付通知」日志里复制一条真实的回调原文然后跑验签和解密// 本地自测用一条真实回调报文验证验签与解密逻辑 string body File.ReadAllText(notify_sample.json); // 从商户平台日志复制的回调正文 string timestamp 1700000000; string nonce xxxxx; string signature xxxxx; // 微信回调头里的 Wechatpay-Signature RSA platformPublicKey LoadPlatformPublicKey(); // 从平台证书或公钥文件加载 bool verified VerifyNotify(timestamp, nonce, body, signature, platformPublicKey); if (verified) { var root JsonSerializer.DeserializeNotifyRoot(body); string businessJson DecryptResource( root.Resource.Ciphertext, root.Resource.Nonce, root.Resource.AssociatedData, apiV3Key); Console.WriteLine(businessJson); }这段代码的价值在于把「网络链路」和「密码学逻辑」分开测试。本地跑通了解密和验签再部署到服务器联调回调地址定位问题时就只剩下网络和配置两个变量。如果连一条真实回调报文都没有可以用微信支付官方文档里的示例报文构造一个但要注意示例里的密文不一定是可解密的最靠谱的还是从商户平台复制真实数据。6.2 全链路核对表从下单到退款一张表盯完我在每个新接入的商户上线前都会跑一笔 0.01 元的测试单按下面这张表逐项打勾步骤操作期望结果核对重点1. 下单JSAPI 下单接口返回 prepay_id请求体 sub_mchid 是否正确2. 支付小程序调起支付支付成功paySign 不报用户态签名错误3. 回写支付回调处理落库一条 PAID 流水trade_stateSUCCESS金额一致4. 分账请求分账查询接收方账户到账receiver.resultSUCCESS5. 完结完结分账分账单 FINISHED剩余资金归子商户6. 退款退款下单回调 refund_statusSUCCESS退款金额与原订单对得上这六步每一步都可能暴露一类问题第 1 步错在参数第 2 步错在前端签名第 3 步错在解密第 4 步错在接收方配置第 5 步错在调用时机第 6 步错在金额对不上。我在项目里把这个清单直接写进接入文档每次新商户进件完成运维照着跑一遍再放量。还有一个我坚持的小习惯所有金额字段在日志里以分打印测试单金额写成 1 分核对日志时直接看整数不用心算小数点。微信支付 V3 的所有金额、退款、分账字段都是整数分日志和数据库统一用分能少一半对账烦恼。希望这份从签名到分账再到退款的路径能帮你把服务商模式的每个环节都变成可验证的环节毕竟资金操作最怕的就是「接口返回成功但钱没到该到的地方」。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

端侧Agent本地部署指南:从模型量化到Ollama实战 2026/10/2 0:41:35

端侧Agent本地部署指南:从模型量化到Ollama实战

这两年端侧 Agent 的热度一直没降,和以往那种“云上大脑”的做法不同,现在越来越多人想把整个链路压到一块本地设备上。我自己也花了很长时间折腾各种开发板和推理框架,最后发现真正决定体验的往往不是哪家模型跑分多高,而是部署时…

阅读更多 →
极限存在判断:7种存在与21种不存在的完整框架 2026/10/2 0:39:52

极限存在判断:7种存在与21种不存在的完整框架

听过太多人第一次看到“∀ε>0,∃δ>0”就头皮发麻。极限这个概念,从牛顿时代就开始用,但“无限接近”这四个字含糊了两百年,最后才被一套严格的不等式语言锤实。这“锤实”的工具,就是用 ε、δ、X、N、x、n、∀…

阅读更多 →
Windows 10中文版安装日语支持的底层原理与DISM实战 2026/10/2 0:39:52

Windows 10中文版安装日语支持的底层原理与DISM实战

1. 为什么“安装日语支持”在中文版Windows 10里不是点几下就能完事?你刚打开“设置 > 时间和语言 > 语言”,把“日语”加进首选语言列表,点击“选项”,再点“下载语言包”——然后卡在99%,或者弹出“无法下载此…

阅读更多 →
智能体从能跑到能落地:工程化与业务落地的关键实践 2026/10/2 0:39:33

智能体从能跑到能落地:工程化与业务落地的关键实践

1. 从这期周报里我看到的真正信号:智能体不再只是"能跑通"这周我把 GitHub Trending 上跟智能体相关的项目从头到尾翻了一遍,最大的感受不是"又出了多少新框架",而是整个赛道的重心明显在往两个方向沉:工程化…

阅读更多 →
基于S7-200和组态王的游泳池水处理PLC控制系统设计 2026/10/2 0:38:14

基于S7-200和组态王的游泳池水处理PLC控制系统设计

做自动化工程项目这些年,游泳池水处理系统是我认为非常适合作为PLC入门到进阶的完整案例。它规模不大,但麻雀虽小五脏俱全:开关量控制、模拟量采集、顺序逻辑、上位机监控全都涉及,而且和日常生活贴近,理解起来没有门槛…

阅读更多 →
海康萤石云接入全链路:accessToken、设备归属与直播播放 2026/10/2 0:37:49

海康萤石云接入全链路:accessToken、设备归属与直播播放

上周接了个电话,做智慧工地的一位老哥,八台海康球机在萤石云APP里看得清清楚楚,他想把这几个画面嵌进自己项目的后台管理页,结果接口调了三天,accessToken一直报10002,把人整得没脾气。这种事我遇得太多了——海康萤石云接入这件事,表面上看就是"拿token、调接…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉