☰
C#.NET整合微信、支付宝、银联支付:统一支付服务层与回调避坑实践
2026/10/8 2:23:01 网站建设 项目流程

简介: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; } Task<PaymentPrepareResult> CreatePaymentAsync(UnifiedOrderRequest request); Task<NotifyHandleResult> HandleNotifyAsync( string headers, string body, Func<string, Task<UnifiedOrderRequest>> getOrderByOutTradeNo, Func<string, Task<bool>> existsPaidOrder); }

逻辑说明:HandleNotifyAsync内部先做渠道验签,验签通过后调用getOrderByOutTradeNo拿到内部订单,再检查existsPaidOrder是否已支付过。这里把“幂等查询”作为回调处理的第一步,重复通知直接返回成功,不再重复改状态。

2.3 统一回调入口:验签、幂等与事件解耦

三端的回调 URL 可以不同,但入口处理器只做三件事:验签、幂等、落库。验签不过直接返回失败文本;验签通过但订单已支付,返回成功文本但不更新数据库;只有未支付订单才推进状态。

[HttpPost] [Route("api/pay/notify/{channel}")] public async Task<string> 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 Task<PaymentPrepareResult> 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固定CNY;openid通过 OAuth 2.0 网页授权拿,不是自己拼的。

3.3 回调通知:解密 resource 与验签

微信 V3 回调的报文结构是:header 里有Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce,body 里是resource字段,其中ciphertext是 AES-256-GCM 加密的数据。先验签还是先解密,很多人会搞反,正确顺序是先验签,验签通过后解密ciphertext再转 JSON,这能在第一时间挡住伪造回调。

public async Task<NotifyHandleResult> HandleNotifyAsync( string headers, string body, Func<string, Task<UnifiedOrderRequest>> getOrderByNo, Func<string, Task<bool>> 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 风格接口,签名算法是 RSA2(SHA256withRSA)。生成密钥对推荐用 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 Task<PaymentPrepareResult> 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 SortedDictionary<string, 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固定RSA2;total_amount必须是字符串且保留两位小数,这里用0.00格式避免浮点数精度问题。BuildAutoSubmitForm生成<form>+<input>隐藏域并自动触发submit。

参数说明:product_code在 WAP 支付时固定是QUICK_WAP_WAY;quit_url表示用户中途退出时跳回的位置。biz_content是一次 JSON 序列化,内部字段无序不影响验签,因为整体作为字符串参与签名。

4.3 异步回调:先验签再改状态

支付宝的回调验签比微信简单,不需要平台证书,只需要支付宝公钥。表单提交过来的参数是平铺的key=value,验签时要把除sign和sign_type外的所有参数按字典序拼接,然后再做 SHA256withRSA 验签。

public async Task<NotifyHandleResult> HandleNotifyAsync( string headers, string body, Func<string, Task<UnifiedOrderRequest>> getOrderByNo, Func<string, Task<bool>> existsPaid) { var form = HttpUtility.ParseQueryString(body); var sign = form["sign"]; var tradeStatus = form["trade_status"]; var parameters = new SortedDictionary<string, 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 Task<PaymentPrepareResult> CreatePaymentAsync(UnifiedOrderRequest request) { var req = new Dictionary<string, 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; charset=utf-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 跑回调入口接口,观察返回文本;再连发两次相同通知,确认第二次不重复改状态;最后在数据库里核对订单状态流转和渠道流水号。从那以后,我每次接入新渠道都强制走一遍这个顺序,先验签、再幂等、后落库,验签不过的直接丢掉,重点看掉单和重复通知两条日志。三端整合的翻车大多不在密钥算法上,而在时序、单位和证书这类细处,这套流程帮我避掉了大半的返工,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询