接入合作伙伴模式的微信支付

2 阅读10分钟

背景

近期在做的项目,要接入微信支付的模块,趁此机会了解了一下接入微信支付的整体流程。

本篇以“合作伙伴”模式的微信支付方式为背景聊一下接入过程,支付宝类似。

如果是普通的商家平台,自研支付模块,建议考虑Paylink等轻量级sdk,高效,无侵入。

准备阶段

服务商模式(Partner Mode)与普通商户模式最大的区别在于:由服务商(SpMch)发起请求,代子商户(SubMch)进行收款。

开始接入之前,要在微信支付平台获取以下几个核心参数

  • 服务商商户号,下定义为SpMchId
  • 服务商的 AppID,下定义为SpAppID
  • 特邀商户商户号,下定义为SubMchId
  • 服务商证书私钥,apiclient_key.pem
  • 证书序列号,下定义为CertSerialNo
  • API v3密钥,下定义为APIV3Key

关于以上参数的说明可参考:pay.weixin.qq.com/doc/v3/part…

构造签名

在微信支付APIv3的所有请求应答场景、接口回调场景、调起支付场景,开发者都需要进行签名验签。

这一部分官方文档也有详细的说明:pay.weixin.qq.com/doc/v3/part…

我这里不再赘述,直接来聊一下构造签名的关键点。V3版本的微信支付采用的是SHA256 with RSA来签名。构造的时候必须严格按照官方的要求,换行符用\n拼接

HTTP方法\n
URL路径(含QueryString,不含域名)\n
时间戳(Unix Timestamp)\n
随机字符串(Nonce Str)\n
请求报文主体(JSON Body)\n

注意: 最后一行必须带 \n。如果 Body 为空(如 GET 请求),最后一行直接留空但保留换行。

以native下单为例,生成一个下单二维码请求签名的部分代码

var bodyObj = new
{
    sp_mchid = spMchId,
    sub_mchid = subMchId,
    sp_appid = spAppId,
    sub_appid = subAppId,
    out_trade_no = outTradeNo,
    description = "Image形象店-深圳腾大-QQ公仔-测试0.01元",
    notify_url = "https://www.weixin.qq.com",
    amount = new { total = 1, currency = "CNY" }, 
    
};

// 序列化JSON 
string bodyJson = JsonSerializer.Serialize(bodyObj, new JsonSerializerOptions { WriteIndented = false });

string method = "POST";
string url = "/v3/pay/partner/transactions/native";
// 按规则构造签名串
string signString = $"{method}\n{url}\n{timestamp}\n{nonceStr}\n{bodyJson}\n";

// 加载RSA私钥
string pemKey = File.ReadAllText(privateKeyPath);
using RSA rsa = RSA.Create();
rsa.ImportFromPem(pemKey);

// 生成签名
byte[] dataBytes = Encoding.UTF8.GetBytes(signString);
byte[] signatureBytes = rsa.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
string signature = Convert.ToBase64String(signatureBytes);

// 构造Authorization Header
string authHeader = $"WECHATPAY2-SHA256-RSA2048 mchid=\"{spMchId}\",nonce_str=\"{nonceStr}\",signature=\"{signature}\",timestamp=\"{timestamp}\",serial_no=\"{certSerialNo}\"";

上面这段代码得到的authHeader,就是后续请求微信api时都需要携带的一个固定格式的请求头

发送请求

准备工作完成后我们就可以尝试发送请求真正得到一个扫描二维码了。这部分就简单了,常规的网络操作

using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", "Magic-Declaration-WechatPay-1.0");
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
httpClient.DefaultRequestHeaders.Add("Authorization", authHeader);

var content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
HttpResponseMessage response = await httpClient.PostAsync("https://api.mch.weixin.qq.com" + url, content);

string result = await response.Content.ReadAsStringAsync();

需要注意的是,请求头里要有User-Agent,Accept,Content-Type等参数,这都是官方的要求,正常添加即可。

解析一下响应值,正常情况下就可以得到一个支付二维码链接,再通过一些二维码的操作库,转换一下即可。

{
  "code_url" : "weixin://wxpay/bizpayurl/up?pr=NwY5Mz9&groupid=00"
}

封装服务

上面的过程,实际就是根据微信支付文档的请求案例,使用dotnet 10的脚本模式快速跑通,验证我们的参数配置是否都正常。

到这里,我们就可以根据官方文档手搓一份完全适配我们自己系统的轻量sdk。封装的代码,我这里就不灌了,大概聊一下基本结构,

  1. 准备配置文件
{
  "WeChatPay": {
    "SpAppId": "wx123...",
    "SpMchId": "1900...",
    "ApiV3Key": "your_api_v3_key",
    "NotifyUrl": "https://api.domain.com/api/wechat/notify",
    "PrivateKeyPath": "Configs/Certs/apiclient_key.pem",
    "CertSerialNo": "1D234...", 
    "MerchantPublicKeyId": "PUB_KEY_ID_...",
    "WeChatPayPublicKeyPath": "Configs/Certs/wechatpay_pub.pem"
  }
}
  1. 统一的通信层

由于微信接口所有的请求都需要加入授权请求头,所有我们可以封装一个统一的通信层

public async Task<WeChatPayResult<TResponse>> SendAsync<TResponse>(HttpMethod method, string uri, object? body = null)
{
    try
    {
        string bodyJson = body == null ? "" : JsonSerializer.Serialize(body, _jsonOptions);

        // 1. 构造签名
        long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        string nonceStr = Guid.NewGuid().ToString("N");
        string signString = $"{method}\n{uri}\n{timestamp}\n{nonceStr}\n{bodyJson}\n";
        string signature = Sign(signString);
        string authHeader = $"WECHATPAY2-SHA256-RSA2048 mchid=\"{_options.SpMchId}\",nonce_str=\"{nonceStr}\",signature=\"{signature}\",timestamp=\"{timestamp}\",serial_no=\"{_options.CertSerialNo}\"";

        
        // 2. 发起请求
        using var request = new HttpRequestMessage(method, uri);
        request.Headers.TryAddWithoutValidation("Authorization", authHeader);

        if (body != null)
        {
            request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
        }

        var response = await _httpClient.SendAsync(request);
        // 微信V3接口关闭订单成功返回 204,没有请求体,这里要特殊处理下
        if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
        {
            return WeChatPayResult<TResponse>.Success(default!);
        }
        string responseContent = await response.Content.ReadAsStringAsync();

        // 3. 处理响应
        // ...略
    }
    catch (Exception ex)
    {
        // 捕获网络异常等底层错误
        return WeChatPayResult<TResponse>.Fail("SYSTEM_EXCEPTION", ex.Message);
    }
}
  1. 构造业务层

基本注意两点就好

  • 我们可以把官方文档里的参数说明,直接生成强类型的请求模型和响应模型,比如订单查询接口的请求参数和相应参数,后续用起来会很方便
/// <summary>
/// 订单查询模型
/// </summary>
public class OrderQueryRequest
{
    public required string SubMchId { get; set; }
    public required string OutTradeNo { get; set; }
}

/// <summary>
/// 订单查询响应
/// </summary>
public class OrderQueryResponse
{
    [JsonPropertyName("sp_appid")] public string SpAppId { get; set; } = string.Empty;
    [JsonPropertyName("sp_mchid")] public string SpMchId { get; set; } = string.Empty;
    [JsonPropertyName("sub_appid")] public string? SubAppId { get; set; }
    [JsonPropertyName("sub_mchid")] public string SubMchId { get; set; } = string.Empty;
    [JsonPropertyName("out_trade_no")] public string OutTradeNo { get; set; } = string.Empty;
    [JsonPropertyName("transaction_id")] public string? TransactionId { get; set; }
    [JsonPropertyName("trade_type")] public string? TradeType { get; set; } // JSAPI, NATIVE, APP...
    [JsonPropertyName("trade_state")] public string TradeState { get; set; } = string.Empty; // SUCCESS, REFUND, NOTPAY...
    [JsonPropertyName("trade_state_desc")] public string TradeStateDesc { get; set; } = string.Empty;
    [JsonPropertyName("bank_type")] public string? BankType { get; set; }
    [JsonPropertyName("attach")] public string? Attach { get; set; }
    [JsonPropertyName("success_time")] public string? SuccessTime { get; set; }

    [JsonPropertyName("amount")] public OrderAmount? Amount { get; set; }
    [JsonPropertyName("payer")] public OrderPayer? Payer { get; set; }

    public class OrderAmount
    {
        [JsonPropertyName("total")] public int Total { get; set; }
        [JsonPropertyName("payer_total")] public int PayerTotal { get; set; }
        [JsonPropertyName("currency")] public string Currency { get; set; } = "CNY";
        [JsonPropertyName("payer_currency")] public string PayerCurrency { get; set; } = "CNY";
    }

    public class OrderPayer
    {
        [JsonPropertyName("sp_openid")] public string? SpOpenId { get; set; }
        [JsonPropertyName("sub_openid")] public string? SubOpenId { get; set; }
    }
}
  • 对每个接口动作单独封装,比如查询订单的接口,可以封装为
/// <summary>
/// 订单查询 
/// </summary>
public async Task<WeChatPayResult<OrderQueryResponse>> QueryOrderAsync(OrderQueryRequest req)
{
    var uri = $"/v3/pay/partner/transactions/out-trade-no/{req.OutTradeNo}?sp_mchid={_options.SpMchId}&sub_mchid={req.SubMchId}";
    return await _client.SendAsync<OrderQueryResponse>(HttpMethod.Get, uri);
}
  1. 注册服务,上面3个过程,说的简单,实际上封装成好用的模块也需要花点功夫,封装完成后,就可以在系统里注入这些服务,然后在业务模块里调用即可
private static void ConfigurePayment(this IServiceCollection services, IConfiguration configuration)
{
    services.Configure<WeChatProviderOptions>(configuration.GetSection("WeChatPay"));

    services.AddHttpClient("WeChatPayClient", client => {
        client.BaseAddress = new Uri("https://api.mch.weixin.qq.com/");
    });

    services.AddScoped<WeChatPayClient>(sp => {
        var factory = sp.GetRequiredService<IHttpClientFactory>();
        var opts = sp.GetRequiredService<IOptions<WeChatProviderOptions>>();
        return new WeChatPayClient(factory.CreateClient("WeChatPayClient"), opts);
    });

    services.AddScoped<WeChatNativePayService>();
}
  1. 业务调用,这部分就是业务代码了,我这里不在灌代码。基本2条线
  • 花钱的线:下单-->(关闭订单)-->支付回调-->手动对账-->业务处理
  • 退钱的线:退款-->退款回调-->手动对账-->业务处理

*小坑集锦

  1. 我在接入支付模块的时候,最开始的测试代码就是完全按照文档的参数进行请求,但一直返回一个NO_AUTH的错误,提示商家收款功能被限制,这个我相信也会有小伙伴遇到。这个不是代码的问题,就是商家平台可能因为长期没有订单流水被微信临时冻结了,登录【微信支付商家助手】小程序,进入风险处理->违约处理记录,查看具体限制原因并尝试解冻一下即可。

  1. 合作伙伴模式的退款api是需要单独开通的,即向特邀商户发送授权链接,再到授权商户的后台授权即可。

  1. 在一个就是,支付回调的模块,必须是可以公开访问的地址,而且要放开系统本身的权限验证,我们需要通过支付平台的公钥对回调请求验签,确保请求来源合法,以此替代系统本来的限制。

  1. 支付模块的集成不能投机取巧,比如
  • 下单或退款的结果,不能仅依赖平台回调,还要做手动的对账验证来兜底;
  • 取消订单一定要先保证支付平台的订单关闭在处理本地业务,外部状态要先于内部。
  1. 密钥文件别搞混,更别外泄,这个都是常识了,我这里也顺嘴提一下。

总结

好了,基本就是这些,总的来说,接入支付模块现在几乎没有什么技术上的难点,难的就是配置参数的准备,以及对于首次接入在线支付的小伙伴来说,还是要多翻翻文档,多验证。