C#.NET聚合支付整合包:微信、支付宝、银联三渠道统一封装实战
简介这份资源面向在.NET平台下开发支付功能的C#工程师与项目团队聚焦微信、支付宝、银联三方支付的整合方案帮助开发者解决多支付渠道接入时接口分散、回调处理复杂、安全合规难把控等实际问题。压缩包为zip格式整体约52.91MB其中Payment.Api文件承担支付服务API接口层角色封装与三方支付平台的交互逻辑对外提供统一支付接口内部实现各支付方式的具体调用便于应用代码保持简洁与模块化。目前已有1492人学习下载适合需要落地线上支付业务的中高级开发者参考。资源围绕SDK引入与使用、预支付订单生成、支付链接与二维码调用、异步回调验证、查询退款撤销操作以及敏感信息加密等关键环节展开并强调沙箱测试与合规性要求可帮助读者理清三方支付整合的整体思路构建稳定可靠的支付系统。1. 从一份 C#.NET 支付整合包说起它到底能省掉哪些重复劳动做过聚合支付的人都知道最磨人的不是写业务逻辑而是把微信、支付宝、银联三家的签名规则、回调格式、退款接口各啃一遍。微信 V3 用 SHA256-RSA 加平台证书支付宝走 RSA2 加应用私钥银联全渠道又是另一套证书体系和报文规范。三套东西放在同一个 .NET 项目里命名空间、HttpClient 复用、异步回调验签、订单号幂等随便一个环节没处理好就是线上事故。这份 C#.NET 整合微信、支付宝、银联支付的资源包解决的就是这个场景把三家渠道的统一下单、支付回调、订单查询、退款、对账这几条主链路封装成一套可复用的服务层让业务代码只面对一个IPaymentService接口。它适合正在做 .NET 商城、SaaS 收银台、或者需要把支付模块从老项目里剥离重写的后端同学。下面我按自己拆包复现的顺序把选型理由、配置步骤和踩过的坑一条条讲清楚。2. 三渠道 SDK 选型与项目骨架为什么不用官方 NuGet 直接拼2.1 官方 SDK 的边界在哪微信支付有官方Senparc.Weixin和微信支付 V3 的WeChatPay-API-v3社区库支付宝有Alipay.AopSdk.Core银联这边官方 .NET SDK 更新慢、文档散。直接拼这三家的问题在于三套 HttpClient 各自管理连接池回调验签逻辑分散在三个 Controller 里订单状态机没法统一。我一般会保留官方 SDK 做底层签名和报文组装但在上面加一层PaymentGateway抽象把「下单→回调→查单→退款」四步统一成同一组 DTO。这样业务层不感知渠道差异新增渠道只加一个实现类。2.2 项目分层与目录结构资源包常见的分层是Payment.Core接口与模型、Payment.WeChat、Payment.Alipay、Payment.UnionPay、Payment.Api对外 Web API。核心接口大致长这样// Payment.Core/IPaymentGateway.cs public interface IPaymentGateway { // 渠道标识WeChat / Alipay / UnionPay string Channel { get; } // 统一下单返回渠道预支付参数如微信 prepay_id、支付宝 form 表单 TaskPrepayResult PrepayAsync(PrepayRequest request); // 回调验签并解析为统一通知模型 TaskPaymentNotification ParseNotificationAsync(HttpRequest request); // 主动查单用于回调丢失时的补偿 TaskQueryResult QueryAsync(string outTradeNo); // 退款 TaskRefundResult RefundAsync(RefundRequest request); }逻辑说明PrepayAsync只负责拿到渠道侧的预支付凭证不碰业务订单状态ParseNotificationAsync把三家的回调报文统一成PaymentNotification字段包括OutTradeNo、TransactionId、TotalAmount、TradeStatus。参数上PrepayRequest里我习惯放OutTradeNo商户订单号全局唯一、Amount单位统一用分避免浮点、Subject、NotifyUrl、Attach透传业务参数。金额统一用int存分是血泪经验早期用decimal元在三家之间转换对账时差过几分钱。2.3 依赖注入与配置绑定在Program.cs里按渠道注册配置从appsettings.json读// Payment.Api/Program.cs builder.Services.ConfigureWeChatPayOptions( builder.Configuration.GetSection(Payment:WeChat)); builder.Services.ConfigureAlipayOptions( builder.Configuration.GetSection(Payment:Alipay)); builder.Services.ConfigureUnionPayOptions( builder.Configuration.GetSection(Payment:UnionPay)); // 按渠道注册业务层通过 IPaymentGatewayResolver 取 builder.Services.AddSingletonIPaymentGateway, WeChatPayGateway(); builder.Services.AddSingletonIPaymentGateway, AlipayGateway(); builder.Services.AddSingletonIPaymentGateway, UnionPayGateway(); builder.Services.AddSingletonIPaymentGatewayResolver, PaymentGatewayResolver();参数说明WeChatPayOptions至少要有AppId、MchId、ApiV3Key、MerchantSerialNo、MerchantPrivateKeyPathAlipayOptions要有AppId、PrivateKey、AlipayPublicKey、GatewayUrlUnionPayOptions要有MerchantId、CertPath、CertPassword、BackUrl。私钥和证书路径不要硬编码走配置或密钥管理服务。PaymentGatewayResolver内部用IEnumerableIPaymentGateway按Channel建字典业务层传渠道名就能拿到对应实现。3. 微信支付 V3 接入签名、回调验签与平台证书轮换3.1 V3 签名机制拆解微信 V3 的请求签名串是「HTTP 方法\nURL\n时间戳\n随机串\n请求体\n」用商户私钥做 SHA256-RSA 签名放进Authorization头。回调验签则反过来用微信平台证书公钥验Wechatpay-Signature。这里最容易翻车的是 URL 必须带 query string且请求体要做规范化。构造签名串的代码// Payment.WeChat/WeChatSigner.cs public string BuildSignature(string method, string url, string body) { var timestamp DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); var nonce Guid.NewGuid().ToString(N); // 注意url 必须包含 path 和 query不含域名 var message ${method}\n{url}\n{timestamp}\n{nonce}\n{body}\n; var signature RsaSign(message, _merchantPrivateKey); return $WECHATPAY2-SHA256-RSA2048 mchid\{_mchId}\, $nonce_str\{nonce}\,timestamp\{timestamp}\, $serial_no\{_merchantSerialNo}\,signature\{signature}\; }逻辑说明RsaSign用RSA.SignData配SHA256私钥从 PKCS#8 文件加载。参数上serial_no是商户证书序列号不是平台证书序列号这两个搞混会一直报 401。nonce_str每次请求都要新生成别复用。3.2 回调验签与平台证书下载回调进来先验签再解密resource。平台证书要通过GET /v3/certificates下载并缓存微信会定期轮换所以不能只下一次。常见做法是启动时拉一次之后每天定时刷新验签时按Wechatpay-Serial头匹配对应证书。// Payment.WeChat/WeChatPayGateway.cs public async TaskPaymentNotification ParseNotificationAsync(HttpRequest request) { // 1. 读原始 body不能先被模型绑定消费 request.EnableBuffering(); var body await new StreamReader(request.Body).ReadToEndAsync(); request.Body.Position 0; // 2. 验签用 Wechatpay-Serial 找平台证书 var serial request.Headers[Wechatpay-Serial].ToString(); var cert _certManager.GetBySerial(serial); var message ${request.Headers[Wechatpay-Timestamp]}\n ${request.Headers[Wechatpay-Nonce]}\n{body}\n; if (!RsaVerify(message, request.Headers[Wechatpay-Signature], cert.PublicKey)) throw new PaymentException(微信回调验签失败); // 3. 解密 resourceAES-256-GCMkey 是 ApiV3Key var notify DecryptResource(body, _options.ApiV3Key); return MapToNotification(notify); }参数说明EnableBuffering必须开否则 body 读完就没了。ApiV3Key是 32 位字符串AES-GCM 的 nonce 和 associated_data 都在报文里。解密后拿到的out_trade_no、transaction_id、trade_state才是可信数据。3.3 平台证书轮换的坑微信平台证书有效期和轮换周期不固定缓存过期会导致验签全挂。我一般会在验签失败时触发一次强制刷新再重试一次而不是直接抛异常。另外Wechatpay-Serial可能对应新旧两张证书缓存里要保留最近两张。4. 支付宝与银联接入RSA2 签名、证书体系与报文差异4.1 支付宝 RSA2 与应用私钥支付宝用应用私钥签名、支付宝公钥验签签名算法 RSA2SHA256WithRSA。请求参数按 key 字典序拼接去掉空值和sign再拼私钥签名。回调验签同理用支付宝公钥验sign字段。// Payment.Alipay/AlipaySigner.cs public string Sign(IDictionarystring, string parameters) { // 1. 过滤空值和 sign按 key 升序 var filtered parameters .Where(p p.Key ! sign !string.IsNullOrEmpty(p.Value)) .OrderBy(p p.Key, StringComparer.Ordinal); // 2. 拼接待签串 var content string.Join(, filtered.Select(p ${p.Key}{p.Value})); // 3. RSA2 签名 return RsaSign(content, _appPrivateKey, SHA256); }逻辑说明StringComparer.Ordinal保证字典序和支付宝一致用默认比较器在含特殊字符时会不一致。参数上app_id、method、charset、sign_type、timestamp、version是公共参数业务参数放biz_contentJSON 字符串。回调里trade_status为TRADE_SUCCESS或TRADE_FINISHED才算成功注意TRADE_FINISHED在退款场景下的语义差异。4.2 银联全渠道证书与报文银联全渠道用 PFX 证书做签名和验签报文是 keyvalue 的 form 形式签名前要按规则排序并做 URL 编码。银联的坑在于编码签名用 UTF-8但传输时部分字段要 URL encode顺序错了验签必挂。// Payment.UnionPay/UnionPaySigner.cs public string Sign(IDictionarystring, string parameters) { var filtered parameters .Where(p p.Key ! signature !string.IsNullOrEmpty(p.Value)) .OrderBy(p p.Key, StringComparer.Ordinal); var content string.Join(, filtered.Select(p ${p.Key}{UrlEncode(p.Value)})); // 用 PFX 证书私钥签名 var cert new X509Certificate2(_options.CertPath, _options.CertPassword); return RsaSign(content, cert.GetRSAPrivateKey(), SHA256); }参数说明UrlEncode用Uri.EscapeDataString不要用HttpUtility.UrlEncode后者对空格编码成会导致验签失败。银联的merId、orderId、txnAmt单位分、txnTimeyyyyMMddHHmmss是必填。回调验签用银联公钥证书注意证书序列号匹配。4.3 三渠道统一订单模型三家字段名不同统一模型是关键。下面是对照表统一字段微信 V3支付宝银联商户订单号out_trade_noout_trade_noorderId渠道流水号transaction_idtrade_noqueryId金额分amount.totaltotal_amount元txnAmt分交易状态trade_statetrade_statusrespCode回调地址notify_urlnotify_urlbackUrl注意支付宝total_amount是元转分要乘 100 并四舍五入银联和微信都是分。这个差异在对账时最容易出问题。5. 避坑与排查回调、幂等、金额这三处最容易翻车5.1 回调重复通知导致重复发货现象用户付一次款订单被标记多次已支付库存扣了两次。原因三家回调都会重试微信重试多次、支付宝 25 小时内多次、银联也会补发业务层没做幂等。解决用out_trade_no做唯一索引回调处理放在数据库事务里先INSERT一条通知记录唯一键冲突就说明处理过再更新订单状态。别用「先查再更新」并发下会漏。5.2 验签失败但报文看起来正常现象回调验签一直失败日志里报文和文档一致。原因读 body 时被模型绑定或中间件消费了或者 URL 编码方式不对。解决回调接口参数用HttpRequest手动读原始 bodyEnableBuffering打开银联的 URL 编码统一用Uri.EscapeDataString微信验签串末尾的\n不能少。5.3 金额精度与单位混乱现象对账时总差几分钱。原因支付宝用元、微信银联用分decimal转int时四舍五入方式不一致。解决入口统一转分用Math.Round(amount * 100, MidpointRounding.AwayFromZero)全链路只用int或long存分展示时再除 100。5.4 平台证书/公钥过期导致验签全挂现象某天开始所有微信回调验签失败。原因平台证书轮换缓存里还是旧证书。解决验签失败时强制刷新证书再重试一次证书缓存保留最近两张加定时刷新任务。5.5 退款回调与异步通知丢失现象退款成功但业务侧没收到通知。原因退款也有异步回调很多人只处理了支付回调。解决退款回调单独一个接口同样做验签和幂等同时加定时查单补偿对超过一定时间未终态的订单主动查询。6. 进阶把三渠道收口成一个收银台与对账补偿6.1 统一收银台的下单流程业务层只调一个方法// Payment.Api/CheckoutController.cs [HttpPost(checkout)] public async TaskIActionResult Checkout([FromBody] CheckoutRequest req) { // 1. 生成商户订单号落库为待支付 var outTradeNo $ORD{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}; await _orderService.CreatePendingAsync(outTradeNo, req.Amount, req.Channel); // 2. 取对应渠道网关下单 var gateway _resolver.Resolve(req.Channel); var result await gateway.PrepayAsync(new PrepayRequest { OutTradeNo outTradeNo, Amount req.Amount, // 单位分 Subject req.Subject, NotifyUrl _options.NotifyUrl, Attach req.UserId }); // 3. 返回渠道预支付参数给前端 return Ok(result); }逻辑说明订单先落库再下单避免渠道下单成功但本地没记录。Attach透传UserId回调时能直接关联业务。参数上Amount全程用分NotifyUrl按渠道不同可能要区分路径。6.2 对账与补偿任务三家都提供对账单下载常见做法是每天凌晨拉前一天账单和本地订单逐笔比对。差异分三类本地成功渠道未成功可能掉单、渠道成功本地未成功回调丢失、金额不一致。前两类走补偿查单第三类人工介入。补偿任务用后台服务跑// Payment.Api/ReconcileWorker.cs protected override async Task ExecuteAsync(CancellationToken token) { while (!token.IsCancellationRequested) { // 查超过 5 分钟仍是待支付的订单主动查单 var pendings await _orderService.GetPendingOlderThanAsync( TimeSpan.FromMinutes(5)); foreach (var order in pendings) { var gateway _resolver.Resolve(order.Channel); var result await gateway.QueryAsync(order.OutTradeNo); if (result.TradeStatus TradeStatus.Success) await _orderService.MarkPaidAsync(order.OutTradeNo, result.TransactionId); } await Task.Delay(TimeSpan.FromMinutes(1), token); } }参数说明GetPendingOlderThanAsync的时间阈值别设太短渠道侧状态同步有延迟5 分钟比较稳。查单频率别太高微信和支付宝都有频率限制。6.3 一个验证技巧用沙箱环境跑通全链路微信和支付宝都有沙箱银联有测试环境。上线前我会用沙箱把「下单→支付→回调→查单→退款→退款回调」整条链路跑一遍重点验证验签和幂等。沙箱的坑是金额和账号有限制回调地址要公网可达本地调试用内网穿透工具把回调打到本地。从那以后我每次接新渠道都强制先把回调验签和幂等这两段单独写测试用例跑通再动业务代码。希望帮到你。本文还有配套的精品资源点击获取