
简介这份资源面向使用C#.NET进行支付功能开发的工程师聚焦微信、支付宝、银联三方支付的整合方案帮助开发者在一个项目中统一接入多种主流支付渠道适用于电商、移动应用及在线服务等需要在线收款的业务场景。压缩包为zip格式整体约52.91MB其中Payment.Api等模块封装了与三方支付平台的交互逻辑对外提供统一支付接口内部实现各支付方式的具体调用便于应用代码保持简洁与模块化。资源围绕SDK引入与使用、支付接口调用、异步回调通知处理、查询退款撤销等操作展开并涉及敏感信息加密、错误处理机制、沙箱测试与合规性等关键要点可帮助读者理解预支付订单生成、支付结果验证及交易状态更新的完整链路。目前已有1492人学习下载适合希望系统掌握.NET支付集成思路、构建稳定可靠支付系统的开发者参考借鉴。1. 支付聚合这件事难的不是接口调用而是状态对齐做过 C#.NET 聚合支付的人大多有个共同体会把微信、支付宝、银联三家的 SDK 分别跑通可能只花两天但让三家的订单状态在同一个系统里始终对得上往往要花两个月。标题里说的「整合」真正的技术含量不在「调通接口」而在「统一抽象 状态机对齐 回调幂等」这三件事上。这套方案适合谁适合手里有 ASP.NET Core 后端、需要同时接多家支付渠道、又不想被某一家 SDK 绑死的中小团队。读完你能拿到一套可复现的分层结构、关键参数配置以及我在真实项目里踩过的坑。下面从抽象层怎么设计讲起一路落到对账和排错。2. 先定抽象层三家支付渠道怎么收敛成一套接口聚合支付最容易翻车的地方是一开始就按渠道写业务代码结果if (channel wechat)散落在几十个文件里。正确顺序是先定抽象再填实现。这一章讲清楚抽象层怎么设计、参数怎么统一、签名差异怎么隔离。2.1 统一支付请求模型与渠道枚举核心思路是业务层只认一个PaymentRequest渠道差异全部下沉到各自的IPaymentChannel实现里。先定义统一模型。// 统一支付请求业务层只构造这个对象不关心底层是哪家 public class PaymentRequest { public string OutTradeNo { get; set; } // 商户侧订单号全局唯一三家都用它做幂等键 public decimal Amount { get; set; } // 单位元保留两位小数 public string Subject { get; set; } // 商品标题微信/支付宝有长度限制 public string NotifyUrl { get; set; } // 异步回调地址必须公网可达 public string ReturnUrl { get; set; } // 同步跳转银联和支付宝网页支付才用 public PayChannel Channel { get; set; } // 渠道枚举 public string OpenId { get; set; } // 微信 JSAPI 必填其他渠道忽略 public string ClientIp { get; set; } // 银联部分产品要求上送 } public enum PayChannel { Wechat, Alipay, UnionPay } // 所有渠道实现这个接口业务层面向接口编程 public interface IPaymentChannel { TaskUnifiedPayResult CreateOrderAsync(PaymentRequest request); TaskUnifiedNotifyResult VerifyNotifyAsync(HttpRequest request); TaskUnifiedQueryResult QueryOrderAsync(string outTradeNo); }逻辑说明OutTradeNo是整个聚合系统的锚点三家渠道的下单、查询、退款、对账全部围绕它展开所以它必须在进入渠道之前就生成好而不是让渠道 SDK 自己生成。参数说明Amount用decimal而不是double因为浮点误差在金额场景是致命的NotifyUrl三家都要求公网可达且不能带自定义参数微信会校验所以渠道标识要通过路径区分比如/notify/wechat、/notify/alipay。2.2 签名与验签的差异隔离三家签名算法完全不同微信 V3 用 SHA256-RSA支付宝用 RSA2银联老接口用 MD5 或 SHA1 再叠 RSA。如果把这些散在业务里改一次密钥就是灾难。常见做法是每个渠道实现内部各自封装签名器对外只暴露VerifyNotifyAsync。// 微信 V3 验签核心用平台证书对回调的签名串做 RSA 验证 public async TaskUnifiedNotifyResult VerifyNotifyAsync(HttpRequest request) { // 1. 读取原始 body注意不能先被 ModelBinding 消费掉 request.EnableBuffering(); using var reader new StreamReader(request.Body, leaveOpen: true); var body await reader.ReadToEndAsync(); request.Body.Position 0; // 2. 从请求头取签名相关字段 var timestamp request.Headers[Wechatpay-Timestamp].ToString(); var nonce request.Headers[Wechatpay-Nonce].ToString(); var signature request.Headers[Wechatpay-Signature].ToString(); // 3. 拼接待验签串时间戳\n随机串\n报文主体\n缺一不可 var message ${timestamp}\n{nonce}\n{body}\n; // 4. 用平台证书公钥验签失败直接拒绝不要继续解析业务 if (!RsaVerify(message, signature, _platformCertPublicKey)) return UnifiedNotifyResult.Fail(sign verify failed); // 5. 验签通过后再解密 resource 字段拿到真实订单数据 return ParseWechatNotify(body); }逻辑说明微信 V3 的回调是「先验签、再解密」顺序不能反否则等于信任了未验证的报文。参数说明EnableBuffering是必须的因为 ASP.NET Core 默认的请求体是只读流读一次就没了不开启缓冲会导致后续中间件拿不到 body。这里有个血泪经验如果你在全局中间件里读过 body 却没重置Position验签必然失败而且报错信息只会说「签名不匹配」排查起来很玄学。2.3 渠道工厂与配置注入抽象定好后需要一个工厂根据PayChannel拿到对应实现。用依赖注入注册避免new到处飞。// 注册把三个渠道实现都注册进容器 builder.Services.AddScopedIPaymentChannel, WechatPayChannel(); builder.Services.AddScopedIPaymentChannel, AlipayChannel(); builder.Services.AddScopedIPaymentChannel, UnionPayChannel(); builder.Services.AddScopedIPaymentChannelFactory, PaymentChannelFactory(); // 工厂按枚举解析注意用 keyed service 或字典缓存 public class PaymentChannelFactory : IPaymentChannelFactory { private readonly IServiceProvider _sp; public PaymentChannelFactory(IServiceProvider sp) _sp sp; public IPaymentChannel Resolve(PayChannel channel) channel switch { PayChannel.Wechat _sp.GetServicesIPaymentChannel().OfTypeWechatPayChannel().First(), PayChannel.Alipay _sp.GetServicesIPaymentChannel().OfTypeAlipayChannel().First(), PayChannel.UnionPay _sp.GetServicesIPaymentChannel().OfTypeUnionPayChannel().First(), _ throw new NotSupportedException($channel {channel} not supported) }; }逻辑说明用OfType从注册集合里挑具体类型比维护一个Dictionary更省心新增渠道只要加一行注册。参数说明生命周期用Scoped而不是Singleton因为渠道实现里通常持有HttpClient和每次请求的配置快照单例容易串数据。如果你的渠道实现是无状态的可以改Singleton提升性能但要确认内部没有可变字段。3. 下单、回调、对账三条链路怎么串起来抽象层只是骨架真正决定系统稳不稳的是三条链路下单要能拿到支付参数、回调要能幂等处理、对账要能兜底。这一章按链路顺序讲每步都给可抄的代码和参数。3.1 统一下单把三家的返回收敛成支付参数下单接口的职责是把业务订单转成渠道订单并返回前端能直接用的支付参数。三家的返回结构差异很大微信返回prepay_id支付宝返回表单 HTML 或trade_no银联返回tn。统一结果模型如下。public class UnifiedPayResult { public bool Success { get; set; } public string ChannelOrderNo { get; set; } // 渠道侧订单号对账用 public string PayParams { get; set; } // 前端直接用的 JSON 或表单 public string ErrorMsg { get; set; } } // 微信 JSAPI 下单后前端需要的是二次签名后的参数 public async TaskUnifiedPayResult CreateOrderAsync(PaymentRequest req) { var prepayId await CallWechatUnifiedOrder(req); // 调微信统一下单 // 二次签名前端 wx.chooseWXPay 需要 paySign不能用商户私钥直接签 var payParams new { appId _options.AppId, timeStamp DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), nonceStr Guid.NewGuid().ToString(N), package $prepay_id{prepayId}, signType RSA, paySign SignFrontendParams(...) // 用商户私钥签注意字段顺序 }; return new UnifiedPayResult { Success true, ChannelOrderNo prepayId, PayParams JsonSerializer.Serialize(payParams) }; }逻辑说明微信 JSAPI 的paySign是前端调起支付用的和下单时的签名是两回事很多人第一次做会混。参数说明timeStamp必须是秒级字符串nonceStr长度建议 32 位以内package格式固定为prepay_idxxx字段顺序在签名时必须和文档一致否则验签失败。支付宝这边如果是网页支付返回的是一段自动提交的表单 HTML直接塞给前端document.write即可但要注意表单里的sign已经由 SDK 处理好不要自己再签一遍。3.2 回调处理幂等是唯一不能省的一步回调是聚合支付里最容易出问题的地方。三家都会重复推送网络抖动、超时重试都会导致同一笔订单回调多次。如果回调里直接改订单状态就会出现重复发货、重复加积分。正确做法是用OutTradeNo做幂等键先查状态再处理。public async TaskUnifiedNotifyResult HandleNotifyAsync(PayChannel channel, HttpRequest request) { var channelImpl _factory.Resolve(channel); var notify await channelImpl.VerifyNotifyAsync(request); if (!notify.Success) return UnifiedNotifyResult.Fail(verify failed); // 幂等用数据库唯一索引 状态判断双重保险 var order await _db.Orders.FirstOrDefaultAsync(o o.OutTradeNo notify.OutTradeNo); if (order null) return UnifiedNotifyResult.Fail(order not found); // 已经是终态直接返回成功让渠道停止重推 if (order.Status OrderStatus.Paid) return UnifiedNotifyResult.Success(); // 金额校验防止渠道金额和本地不一致极少见但必须防 if (order.Amount ! notify.Amount) return UnifiedNotifyResult.Fail(amount mismatch); order.Status OrderStatus.Paid; order.ChannelOrderNo notify.ChannelOrderNo; order.PaidAt notify.PaidAt; await _db.SaveChangesAsync(); return UnifiedNotifyResult.Success(); }逻辑说明幂等判断要在事务内做order.Status Paid的检查必须和更新在同一个事务否则并发回调会同时通过检查。参数说明返回给渠道的响应体格式各家不同微信要{code:SUCCESS}支付宝要纯文本success银联要ok这些细节在UnifiedNotifyResult里按渠道序列化。注意回调处理时间要控制在 5 秒内微信超时会重推如果你在回调里做耗时操作比如发短信、调外部接口应该丢到队列异步处理回调只负责改状态。3.3 主动查询与对账兜底回调丢失回调不是 100% 可靠网络分区、服务器重启都可能丢。所以必须有主动查询和对账兜底。常见做法是下单后 5 分钟未支付查一次30 分钟再查一次每天凌晨跑一次全量对账。// 定时任务扫描超过 5 分钟仍是待支付的订单主动查询渠道 public async Task ReconcilePendingOrdersAsync() { var pending await _db.Orders .Where(o o.Status OrderStatus.Pending o.CreatedAt DateTime.UtcNow.AddMinutes(-5)) .Take(200) // 分批避免一次拉太多 .ToListAsync(); foreach (var order in pending) { var channel _factory.Resolve(order.Channel); var result await channel.QueryOrderAsync(order.OutTradeNo); if (result.Success result.Paid) { // 复用回调的处理逻辑保证状态流转一致 await MarkOrderPaidAsync(order, result); } } }逻辑说明主动查询的结果要和回调走同一套状态更新逻辑否则两条路径可能写出不一致的状态。参数说明Take(200)是保护数据库和渠道接口渠道查询接口通常有频率限制微信单商户查询 QPS 有限批量太大容易被限流。对账文件方面微信和支付宝都提供 T1 的对账单下载银联的对账文件格式更复杂建议先做微信和支付宝的对账银联用查询接口兜底。4. 避坑指南聚合支付里那些让人半夜爬起来的问题这一章是我在几个项目里真实踩过的坑每条按「现象 → 原因 → 解决」写能帮你省下不少通宵。4.1 回调验签一直失败但报文看起来没问题现象微信 V3 回调验签始终返回失败日志里报文和签名都在手动验签却通过。原因ASP.NET Core 的请求体被上游中间件比如日志中间件、模型绑定提前读过request.Body.Position没有重置到 0导致验签时读到空字符串。解决在读取 body 前调用EnableBuffering()读完立即Position 0并且把验签逻辑放在所有可能读 body 的中间件之前。更稳妥的做法是给回调接口单独开一条不经过全局中间件的路由。4.2 同一笔订单被发了两次货现象用户只付了一次但系统发了两次货查日志发现回调进来了两次间隔 3 秒。原因回调处理没有幂等两次回调都通过了Status ! Paid的检查因为第一次更新还没提交第二次就读到了旧状态。解决给OutTradeNo加数据库唯一索引状态更新用乐观锁UPDATE ... WHERE Status Pending判断受影响行数只有 1 行才继续后续业务。光靠代码里的if判断在并发下不可靠。4.3 金额对不上差了一分钱现象本地订单金额 99.99渠道回调金额 99.98对账时发现差异。原因金额在传输过程中被double转换过或者渠道侧对金额做了四舍五入。微信和支付宝的金额单位是「分」本地用「元」转换时(int)(amount * 100)在浮点误差下可能少一分。解决全程用decimal转分时用decimal.Round(amount * 100, 0, MidpointRounding.AwayFromZero)并且在下单和回调两处都做金额校验不一致直接告警而不是静默处理。4.4 银联回调收不到其他两家正常现象微信支付宝回调都正常银联回调一直不来。原因银联部分产品的异步通知地址要求是 80 或 443 端口且不支持带路径参数的 URL如果你的回调地址是https://domain.com/api/pay/unionpay/notify可能被银联网关拒绝。解决给银联单独配一个根路径回调比如https://domain.com/unionpay-notify并且在商户后台确认通知地址已生效。另外银联的验签证书有有效期过期后回调会静默失败要提前续期。4.5 本地调试收不到回调只能靠日志猜现象开发环境没有公网地址渠道回调打不进来只能看日志。原因回调本质是渠道主动请求你的服务器本地localhost不可达。解决常见做法是用内网穿透工具把本地端口映射到公网注意合规使用或者搭一个测试环境部署到有公网 IP 的服务器。更省事的做法是写一个「模拟回调」的接口把渠道的报文样本存下来本地直接 POST 这个样本走完整验签和状态更新流程能覆盖 90% 的逻辑问题。5. 进阶把支付状态机做成可观测、可回放的黑匣子前面讲的都是「能跑通」这一章讲「跑得稳、查得快」。聚合支付系统最怕的不是报错而是「状态不对但没人知道」。我的习惯是给每笔订单建一条状态流转记录把渠道交互的原始报文都存下来出问题时能回放。5.1 状态流转表设计不要只存订单当前状态要存每一次状态变更。表结构大致如下字段类型说明Idbigint自增主键OutTradeNovarchar(64)商户订单号索引FromStatusvarchar(20)变更前状态ToStatusvarchar(20)变更后状态Channelvarchar(20)渠道标识RawPayloadtext渠道原始报文脱敏后CreatedAtdatetime变更时间有了这张表任何「订单状态为什么是这样」的问题都能回答。比如用户说付了钱但订单没变你查这张表就能看到是回调没进来还是进来了但验签失败还是验签通过但金额校验没过。5.2 用原始报文做本地回放把渠道回调的原始报文存下来后可以写一个回放工具把历史报文重新灌进处理逻辑验证代码改动是否影响已有订单。这在改验签逻辑或状态机时特别有用。// 回放从状态流转表读出原始报文重新走一遍处理逻辑 public async Task ReplayAsync(string outTradeNo) { var records await _db.StatusLogs .Where(l l.OutTradeNo outTradeNo l.RawPayload ! null) .OrderBy(l l.CreatedAt) .ToListAsync(); foreach (var record in records) { // 构造一个模拟 HttpRequest把 RawPayload 塞进去 var fakeRequest BuildFakeRequest(record.RawPayload, record.Channel); var result await HandleNotifyAsync(record.Channel, fakeRequest); _logger.LogInformation(replay {OutTradeNo} result {Result}, outTradeNo, result.Success); } }逻辑说明回放不修改真实订单状态只验证处理逻辑是否还能正确解析历史报文。参数说明RawPayload存之前要脱敏把用户手机号、身份证等字段替换掉但签名相关的字段必须原样保留否则回放时验签会失败。这个工具帮我定位过一次线上问题渠道升级了签名算法新报文验签失败但历史报文回放正常一下就锁定了是渠道侧变更而不是我们的代码问题。5.3 监控指标三个必须告警的信号支付系统不需要花哨的监控但三个指标必须告警回调失败率验签失败 处理异常、待支付订单积压量超过阈值说明回调大面积丢失、对账差异笔数不为零就要人工介入。我一般用Interlocked计数器在内存里统计定时推送到监控系统比每次写日志再聚合要轻量。回调失败率超过 1% 就告警因为正常情况这个值应该接近 0。最后说个我自己的教训早期做聚合支付时我总觉得「回调处理逻辑简单不用写测试」结果一次渠道升级导致验签失败线上挂了 40 分钟才发现。后来我强制自己给每个渠道的回调处理写至少三个测试用例正常报文、重复报文、篡改报文。这三个用例能挡住绝大多数回归问题。希望帮到你。本文还有配套的精品资源点击获取