简介: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 证书 |
| 签名算法 | SHA256withRSA | SHA256withRSA | SHA256withRSA |
| 金额单位 | 分(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 跑回调入口接口,观察返回文本;再连发两次相同通知,确认第二次不重复改状态;最后在数据库里核对订单状态流转和渠道流水号。从那以后,我每次接入新渠道都强制走一遍这个顺序,先验签、再幂等、后落库,验签不过的直接丢掉,重点看掉单和重复通知两条日志。三端整合的翻车大多不在密钥算法上,而在时序、单位和证书这类细处,这套流程帮我避掉了大半的返工,希望帮到你。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_31188927/article/details/167180083



