ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C#.NET整合微信、支付宝、银联支付:统一支付服务层与回调避坑实践

2026/10/8 2:23:02 拓冰建站 浏览量
C#.NET整合微信、支付宝、银联支付:统一支付服务层与回调避坑实践 简介C#.NET整合微信、支付宝和银联支付是一份面向.NET开发者的三方支付集成方案资料包适合需要快速掌握移动支付、在线支付与统一支付接口设计的中高级开发人员。资源围绕微信WxPaySDK、支付宝Alipay.Aop SDK、银联Unipay SDK的接入方法覆盖JSAPI、Native、H5、APP、网页支付、扫码支付等常见场景并重点讲解预支付订单生成、异步回调校验、订单查询/退款/撤销、错误处理与沙箱测试等关键环节资料中提及的Payment.Api层也有助于理解如何用统一API封装三方支付差异、降低业务耦合度。压缩包约52.91MB文件数量与类型明细暂未标注。已有1492人学习下载适合正在搭建或重构支付模块的团队作为参考。1. 微信、支付宝、银联支付整合先想清楚这不是 SDK 堆叠做传统行业管理系统或者接外包时最常见的需求就是订单页面同时出现微信、支付宝、银联三个图标。C#.NET整合微信、支付宝、银联支付第一反应往往是各找一个官方 SDK 塞进项目里结果边调边骂微信的证书体系一套支付宝的密钥体系另一套银联更直接甩给你一堆 .pfx 证书文件。三个渠道的下单、回调、验签、掉单补单逻辑各不相同直接堆代码的话踩坑速度远大于写码速度。这份资源的核心是把三端支付收拢到一个支付服务层里统一订单状态、统一回调入口、统一签名校验时序。适合正在做订单系统、想一次把三个支付渠道接干净而不是今天调通一个明天返工另一个的 C#.NET 开发者和外包工程师。2. 支付服务层先行渠道适配器、订单状态机与统一回调入口2.1 三端时序差异同步返回、异步通知与掉单在做统一服务层之前先看三端在时序上的差异。微信支付的同步返回只能告诉你“下单成功”真正的支付结果完全靠服务器异步通知且通知可能延迟几分钟支付宝除了服务器异步通知还有同步跳转回业务页的 GET 参数里面也有 trade_status但那是给用户看结果用的不能作为改单据的最终依据银联则是表单跳转到银行网关用户完成支付后先回前台页面再触发后台通知两套通知都可能到也可能只到一套。这就引出一个掉单的根源三端通知时机不一致且各自的幂等要求不一样。如果每个渠道各写一套回调处理逻辑状态流转会很快失控。我一般会先画一张订单状态机把状态限定为Pending、Paid、Failed、Closed所有渠道的异步通知都只做一件事把外部流水号映射到内部订单按状态机规则向前推进状态。这张状态机能挡住八成以上的重复通知和乱序通知问题。2.2 渠道适配器接口与订单状态机统一服务层的第一步是定义渠道适配器。不要在业务代码里直接调微信 SDK 或者支付宝 SDK而是让业务只依赖一个下单接口具体渠道通过枚举和配置切换后续加新渠道时业务代码不用动。public enum PaymentChannelType { WechatPay 1, Alipay 2, UnionPay 3 } public enum OrderStatus { Pending 0, Paid 1, Failed 2, Closed 3 } public class UnifiedOrderRequest { public string OrderNo { get; set; } // 内部订单号唯一 public long AmountFen { get; set; } // 统一以“分”为单位 public string Subject { get; set; } // 支付标题 public PaymentChannelType Channel { get; set; } public string OpenId { get; set; } // 微信 JSAPI 需要 public string ReturnUrl { get; set; } // 支付宝/银联同步跳转地址 }参数说明这里把金额统一为long型分避免微信以分为单位、支付宝以元为单位带来的换算混乱OrderNo由业务系统生成三端都用订单号作为幂等键。适配器接口只暴露CreatePaymentAsync和HandleNotifyAsync两个方法前者返回前端调起支付所需的数据后者在回调入口统一调用。public interface IPaymentChannelAdapter { PaymentChannelType Channel { get; } TaskPaymentPrepareResult CreatePaymentAsync(UnifiedOrderRequest request); TaskNotifyHandleResult HandleNotifyAsync( string headers, string body, Funcstring, TaskUnifiedOrderRequest getOrderByOutTradeNo, Funcstring, Taskbool existsPaidOrder); }逻辑说明HandleNotifyAsync内部先做渠道验签验签通过后调用getOrderByOutTradeNo拿到内部订单再检查existsPaidOrder是否已支付过。这里把“幂等查询”作为回调处理的第一步重复通知直接返回成功不再重复改状态。2.3 统一回调入口验签、幂等与事件解耦三端的回调 URL 可以不同但入口处理器只做三件事验签、幂等、落库。验签不过直接返回失败文本验签通过但订单已支付返回成功文本但不更新数据库只有未支付订单才推进状态。[HttpPost] [Route(api/pay/notify/{channel})] public async Taskstring HandleNotify(PaymentChannelType channel) { var adapter _adapterResolver.Resolve(channel); var headers Request.Headers.ToDictionary(k k.Key, v v.Value.ToString()); var body await new StreamReader(Request.Body).ReadToEndAsync(); var result await adapter.HandleNotifyAsync( headers, body, outTradeNo _orderService.GetByOutTradeNoAsync(outTradeNo), outTradeNo _orderService.ExistsPaidAsync(outTradeNo)); if (!result.IsValid) return channel PaymentChannelType.WechatPay ? FAIL : fail; if (result.ShouldUpdateOrder) await _orderService.MarkPaidAsync(result.OrderNo, result.TransactionId); else if (result.IsDuplicate) _logger.LogWarning(重复通知{OrderNo}, result.OrderNo); return channel PaymentChannelType.WechatPay ? SUCCESS : success; }这段代码把三端差异压在一个channel参数里。微信通知要求返回大写的SUCCESS/FAIL支付宝和银联是小写success/fail顺序不能搞错。ShouldUpdateOrder为真时才写库重复通知只记日志状态机保证Paid不会被Pending覆盖。3. 微信支付接入V3 接口、JSAPI 调起与回调验签3.1 微信支付的凭证体系与验签规则微信支付 V3 的凭证比支付宝复杂商户号、AppID、API 证书apiclient_key.pem、APIv3 密钥、微信支付平台证书五个缺一不可。其中apiclient_key.pem是你自己用来给请求签名微信支付平台证书是你验证微信回调签名用的两者不能混。凭证用途丢失/过期影响商户号下单接口主体无法发起下单AppID关联公众号/小程序JSAPI 调起失败apiclient_key.pem请求签名下单接口 401 签名错误APIv3 密钥解密回调 resource回调内容解不开平台证书验签回调回调解密后验签失败初始化时我习惯把 API 证书和平台证书路径写进配置不放进项目目录避免把私钥带上 Git。3.2 JSAPI 下单与页面调起参数拼接微信 JSAPI 下单接口是/v3/pay/transactions/jsapi请求体里金额单位是分description不能超过 127 字节这些都容易在联调时翻车。关键点是想清楚返回的prepay_id需要二次签名才能给前端调起支付。public async TaskPaymentPrepareResult CreatePaymentAsync(UnifiedOrderRequest request) { var payload new { appid _wechatOptions.AppId, mchid _wechatOptions.MerchantId, description request.Subject, out_trade_no request.OrderNo, notify_url _wechatOptions.NotifyUrl, amount new { total request.AmountFen, currency CNY }, payer new { openid request.OpenId } }; var response await _wechatClient.PostAsJsonAsync( https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi, payload); var prepayId response[prepay_id]; var timeStamp DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); var nonceStr Guid.NewGuid().ToString(N); var payMessage ${_wechatOptions.AppId}\n{timeStamp}\n{nonceStr}\nprepay_id{prepayId}\n; var paySign _wechatSigner.Sign(payMessage); return new PaymentPrepareResult { Channel PaymentChannelType.WechatPay, InvokeParams new { timeStamp, nonceStr, package $prepay_id{prepayId}, signType RSA, paySign } }; }逻辑说明先请求微信统一下单拿prepay_id再用支付签名规则拼字符串并做 SHA256withRSA 签名最后返回给前端。前端拿到paySign后调用wx.chooseWXPay调起支付。注意这里的payMessage拼接内容是固定的四项换行符是\n顺序错一位签名就失败。参数说明total传的是分接口要求int或long不需要转字符串currency固定CNYopenid通过 OAuth 2.0 网页授权拿不是自己拼的。3.3 回调通知解密 resource 与验签微信 V3 回调的报文结构是header 里有Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Noncebody 里是resource字段其中ciphertext是 AES-256-GCM 加密的数据。先验签还是先解密很多人会搞反正确顺序是先验签验签通过后解密ciphertext再转 JSON这能在第一时间挡住伪造回调。public async TaskNotifyHandleResult HandleNotifyAsync( string headers, string body, Funcstring, TaskUnifiedOrderRequest getOrderByNo, Funcstring, Taskbool existsPaid) { var timestamp headers[Wechatpay-Timestamp]; var nonce headers[Wechatpay-Nonce]; var signature headers[Wechatpay-Signature]; string message ${timestamp}\n{nonce}\n{body}\n; bool verified _platformCertificate.Verify(message, signature); if (!verified) return NotifyHandleResult.Invalid(signature invalid); var resourceJson _apiV3Key.DecryptAesGcm(resource); var tradeState resourceJson[trade_state]?.ToString(); if (tradeState ! SUCCESS) return NotifyHandleResult.NoNeedProcess(); var outTradeNo resourceJson[out_trade_no].ToString(); var order await getOrderByNo(outTradeNo); if (await existsPaid(outTradeNo)) return NotifyHandleResult.Duplicate(order.OrderNo); return NotifyHandleResult.Success(order.OrderNo, resourceJson[transaction_id].ToString()); }逻辑说明微信回调要求 5 秒内响应且必须包含明文SUCCESS或FAIL。先验签再解密既防伪造也避免把解密时间浪费在无效请求上。解密用的是 APIv3 密钥和取证时的平台证书是两套东西。参数说明message的拼接顺序是timestamp 换行 nonce 换行 body 换行这是 V3 验签的固定格式body 必须是原始请求文本不能重新序列化否则签名一定对不上。4. 支付宝接入RSA2 密钥体系、WAP 下单与异步通知4.1 支付宝 RSA2 密钥体系生成、上传与验签公钥支付宝是 RPC 风格接口签名算法是 RSA2SHA256withRSA。生成密钥对推荐用 OpenSSL 命令openssl genrsa -out app_private_key.pem 2048 openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem openssl pkcs8 -topk8 -nocrypt -in app_private_key.pem -out app_private_key_pkcs8.pem生成后把app_public_key.pem的内容上传到支付宝开放平台换成支付宝公钥alipay_public_key.pem而应用公钥不能用于验签。我见过有人直接用应用公钥验签结果每次回调都报签名验证失败。应用私钥签名、支付宝公钥验签这个对应关系是支付宝整套验签的基础。4.2 WAP 下单构建参数、签名与自动提交表单支付宝 WAP 支付接口叫alipay.trade.wap.pay它不返回 JSON而是返回一段会自动提交的 HTML 表单。服务端要做的是组装公共参数、业务参数、按字典序拼接后签名再输出到页面。public async TaskPaymentPrepareResult CreatePaymentAsync(UnifiedOrderRequest request) { var bizContent new { out_trade_no request.OrderNo, total_amount (request.AmountFen / 100.0).ToString(0.00), subject request.Subject, product_code QUICK_WAP_WAY, quit_url request.ReturnUrl }; var parameters new SortedDictionarystring, string { [app_id] _alipayOptions.AppId, [method] alipay.trade.wap.pay, [charset] utf-8, [sign_type] RSA2, [timestamp] DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss), [version] 1.0, [notify_url] _alipayOptions.NotifyUrl, [return_url] request.ReturnUrl, [biz_content] JsonConvert.SerializeObject(bizContent) }; var signSource string.Join(, parameters.Select(kvp ${kvp.Key}{kvp.Value})); var sign _alipaySigner.SignWithRsa2(signSource); var formHtml BuildAutoSubmitForm(parameters, sign); return new PaymentPrepareResult { Channel PaymentChannelType.Alipay, RedirectHtml formHtml }; }逻辑说明SortedDictionary保证参数按字典序排列这是支付宝签名规则要求sign_type固定RSA2total_amount必须是字符串且保留两位小数这里用0.00格式避免浮点数精度问题。BuildAutoSubmitForm生成forminput隐藏域并自动触发submit。参数说明product_code在 WAP 支付时固定是QUICK_WAP_WAYquit_url表示用户中途退出时跳回的位置。biz_content是一次 JSON 序列化内部字段无序不影响验签因为整体作为字符串参与签名。4.3 异步回调先验签再改状态支付宝的回调验签比微信简单不需要平台证书只需要支付宝公钥。表单提交过来的参数是平铺的keyvalue验签时要把除sign和sign_type外的所有参数按字典序拼接然后再做 SHA256withRSA 验签。public async TaskNotifyHandleResult HandleNotifyAsync( string headers, string body, Funcstring, TaskUnifiedOrderRequest getOrderByNo, Funcstring, Taskbool existsPaid) { var form HttpUtility.ParseQueryString(body); var sign form[sign]; var tradeStatus form[trade_status]; var parameters new SortedDictionarystring, string(); foreach (string key in form.AllKeys) { if (key sign || key sign_type) continue; parameters[key] form[key]; } var signSource string.Join(, parameters.Select(kvp ${kvp.Key}{kvp.Value})); if (!_alipayPublicKey.VerifyRsa2(signSource, sign)) return NotifyHandleResult.Invalid(sign invalid); if (tradeStatus ! TRADE_SUCCESS) return NotifyHandleResult.NoNeedProcess(); var outTradeNo form[out_trade_no]; if (await existsPaid(outTradeNo)) return NotifyHandleResult.Duplicate(outTradeNo); return NotifyHandleResult.Success(outTradeNo, form[trade_no]); }逻辑说明支付宝回调验签的核心是“原生参数 SortedDictionary 拼接 RSA2 验签”。需要注意trade_status有WAIT_BUYER_PAY、TRADE_SUCCESS两个常见值只有TRADE_SUCCESS才代表支付成功。trade_no是支付宝交易号存库时可以作为渠道流水号。参数说明body里的编码是表单application/x-www-form-urlencoded直接ParseQueryString解析不要用 JSON 反序列化去解析因为支付宝回调不是 JSON 格式。5. 银联接入证书链加载、表单跳转与三端签名差异5.1 银联证书体系签名证书、验签证书与密码银联全渠道网关是三种支付方式里最“老派”的给你两个证书文件加一个密码。签名证书是.pfx生产环境叫acp_sign.pfx验签证书是.cer还有一套测试环境的同名文件。网上能找到的银联 C#.NET SDK 包多数自带一套测试证书作为示例接生产环境时必须换成银联商户后台下载的正式证书和配套密码。配置上一般这样写{ UnionPay: { SignCertPath: /certs/acp_sign.pfx, SignCertPassword: your-password, VerifyCertPath: /certs/acp_verify_sign.cer, GatewayUrl: https://gateway.95516.com/gateway/api/frontTransReq.do, MerId: 700000000000001 } }银联签名原理是请求参数按字典序拼接用签名证书私钥做 SHA256withRSA验签用verifyCert。比微信、支付宝都多了一个环节——证书本身要加载到 X509Certificate2 里且 Windows 和 Linux 在证书路径处理上有明显差异这块坑在后一章里专门说。5.2 表单跳转下单与前台/后台通知落地银联下单没有 JSON 接口SDK 的做法是把所有业务参数放入Dictionary签名后拼成 HTML 表单 POST 到网关网关返回一段带交易信息的 HTML 页面。下面是一个简化版本public async TaskPaymentPrepareResult CreatePaymentAsync(UnifiedOrderRequest request) { var req new Dictionarystring, string { [version] 5.1.0, [encoding] utf-8, [signMethod] 01, [txnType] 01, [txnSubType] 01, [bizType] 000201, [channelType] 07, [merId] _unionPayOptions.MerId, [orderId] request.OrderNo, [txnTime] DateTime.Now.ToString(yyyyMMddHHmmss), [txnAmt] request.AmountFen.ToString(), [currencyCode] 156, [frontUrl] request.ReturnUrl, [backUrl] _unionPayOptions.NotifyUrl }; req[signature] _unionPaySigner.Sign(req, _unionPayOptions.SignCertPath, _unionPayOptions.SignCertPassword); return new PaymentPrepareResult { Channel PaymentChannelType.UnionPay, RedirectHtml BuildAutoSubmitForm(req) }; }逻辑说明银联参数里txnAmt单位是分但必须是整数形式的字符串与微信的amount.total一致currencyCode是156表示人民币channelType在商户自研 PC 网关场景通常填07或08具体值按商户开通的场景填。signature字段是自签字段不参与后续支付网关的逻辑。参数说明version、signMethod、txnType这些固定值不能随意改SDK 在验签时会检查整个过程。正式环境的merId是 15 位数字测试环境的号码格式不同拿混了会出现“商户不存在”的报错。5.3 三端签名与通知时序对比把三端放在一起对比最容易看出来的是微信和支付宝的签名都基于“密钥串”银联基于证书文件微信和银联的同步结果都不能作为完成支付的依据支付宝的同步跳转参数里虽然有trade_status但也不能直接信赖。维度微信支付 V3支付宝银联全渠道下单方式JSON POST请求拼接 签名表单 POST同步返回prepay_id跳转 URL前台通知页面异步通知JSON AES 加密表单 URL 编码表单 URL 编码验签材料微信平台证书支付宝公钥验签 .cer 证书签名算法SHA256withRSASHA256withRSASHA256withRSA金额单位分int元字符串分字符串这张表在接银联时尤其有用因为银联和微信都是分但一个用 JSON 一个用表单支付宝用元但字符串格式化时如果直接用double会有浮点误差风险。三端支付整合里的黑匣子往往就是这些单位换算和字段类型差异而不是算法本身。6. 三端整合避坑记录与沙箱自测掉单、幂等与验签顺序6.1 微信回调 Content-Type 不一致导致验签失败现象联调时手动用 Postman 模拟微信回调把 header 的Content-Type设为application/json验签总失败。原因微信支付要求回调通知的Content-Type是application/json; charsetutf-8且必须带原始 body 原文Postman 或 HttpClient 自动添加的charset或内容重排都会改变 body。解决用真实回调日志里的完整 header 与 body 做复现不要把 body 反序列化后再拼接验签的字符串只能是原始请求文本。6.2 支付宝回调重复通知引发的状态回退现象用户支付成功后订单状态偶尔变回Pending。原因支付宝的通知机制本身会多次重复通知并且可能先到TRADE_SUCCESS再到WAIT_BUYER_PAY乱序业务代码没有做幂等校验就执行订单状态机更新。解决回调处理统一走existsPaid判断已支付订单直接返回成功绝不让旧状态覆盖新状态状态机只允许Pending → Paid正向流转。6.3 银联证书在 Linux 中文路径下加载失败现象把 Windows 上能正常跑的银联 SDK 部署到 Linux启动后报“证书不存在”或“Private Key not found”。原因.pfx证书的存储路径里有中文目录Windows 的X509Certificate2能容忍Linux 下路径编码不一致直接抛错。解决证书文件放到纯英文路径密码用配置中心管理生产环境把签名证书文件放到/opt/certs/这类固定目录并做权限收敛不要在代码里硬编码证书路径。6.4 金额单位搞混分、元与 long现象微信和银联传了1000显示支付 1000 元支付宝传了10.00显示支付 10 元同一笔订单金额不一致。原因微信、银联要求“分”支付宝要求“元”开发时两个渠道的金额字段没统一换算。解决统一支付服务层里UnifiedOrderRequest.AmountFen一律以分存储支付宝适配器内部做/100.0转成字符串保留两位小数微信、银联直接用long分。加一个单元测试锁住这个换算关系所有渠道都走同一个入口。6.5 沙箱自测先跑验签再跑状态机的硬规矩我最后一次接三端支付时把沙箱自测流程固定成了四个命令先拿到三端的真实回调样例存成文件然后跑一个验签工具确认验签通过接着用同一份 body 跑回调入口接口观察返回文本再连发两次相同通知确认第二次不重复改状态最后在数据库里核对订单状态流转和渠道流水号。从那以后我每次接入新渠道都强制走一遍这个顺序先验签、再幂等、后落库验签不过的直接丢掉重点看掉单和重复通知两条日志。三端整合的翻车大多不在密钥算法上而在时序、单位和证书这类细处这套流程帮我避掉了大半的返工希望帮到你。本文还有配套的精品资源点击获取