周恰恰头像

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; }

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

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_31188927/article/details/167180083

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--