.netcore 微信小程序调用微信支付JSAPI下单

微信支付JSAPI下单文档

目录

  1. 概述
  2. 涉及对象
  3. 核心配置
  4. 完整代码
  5. DTO定义
  6. 处理流程图
  7. 小程序前端调起支付

概述

微信支付 V3 版本 JSAPI 下单流程:

  1. 商户后端调用微信支付统一下单API,传入订单信息和用户openid
  2. 微信返回 prepay_id(预支付交易会话标识)
  3. 商户后端使用私钥对支付参数进行签名
  4. 小程序前端使用签名后的参数调起微信支付
  5. 用户支付成功后,微信回调通知商户

涉及对象

对象 说明
HomeService 服务层,负责调用微信支付API
PaymentEntity 支付参数实体,返回给小程序前端
WeChatPayErrorResponse 微信支付错误响应DTO
h_bd_order 订单实体

核心配置

csharp 复制代码
// 微信支付配置
public readonly static string _appid = "wx1234567890abcdef";           // 小程序AppID
public readonly static string _secret = "1234567890abcdef1234567890abcdef";  // 小程序AppSecret
public readonly static string _mch_id = "1234567890";                 // 商户号
public readonly static string serial_no = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";  // API证书序列号
public readonly static string _apiV3Key = "1234567890abcdef1234567890abcdef";  // APIv3密钥(32字符)
public readonly static string privateKey = @"-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END PRIVATE KEY-----";  // 商户API私钥(PKCS#8格式)

完整代码

Service层下单方法

csharp 复制代码
/// <summary>
/// 微信统一下单获取prepay_id & 再次签名返回数据
/// </summary>
/// <param name="attach">商品描述</param>
/// <param name="openid">用户openid</param>
/// <param name="bookingNo">商户订单号</param>
/// <param name="total_fee">订单金额(单位:分)</param>
/// <returns></returns>
public async Task<APIResult> Getprepay_id(string attach, string openid, string bookingNo, int total_fee)
{
    // 1. 微信统一下单API地址
    var url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
    // 支付成功回调地址
    var notify_url = "https://your-domain.com/api/Home/HandleNotify";

    // 2. 构建请求数据
    var requestData = new
    {
        appid = _appid,           // 小程序AppID
        mchid = _mch_id,          // 商户号
        description = attach,     // 商品描述
        out_trade_no = bookingNo, // 商户订单号
        notify_url = notify_url,  // 回调地址
        amount = new
        {
            total = total_fee,    // 订单金额(分)
            currency = "CNY"     // 货币类型
        },
        payer = new
        {
            openid = openid       // 用户openid
        }
    };

    // 3. 构造Authorization头
    string timestamp = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
    string nonce = Guid.NewGuid().ToString("N");
    string method = "POST";
    var json = JsonConvert.SerializeObject(requestData);

    // 4. 生成签名
    string signature = GenerateSignature(method, url, timestamp, nonce, json);

    // 5. 构建Authorization头
    string authorization = $"WECHATPAY2-SHA256-RSA2048 " +
              $"mchid=\"{_mch_id}\"," +
              $"nonce_str=\"{nonce}\"," +
              $"timestamp=\"{timestamp}\"," +
              $"serial_no=\"{serial_no}\"," +
              $"signature=\"{signature}\"";

    // 6. 发送请求到微信
    var getdata = await sendPost(url, json, authorization);

    // 7. 解析响应
    var error = System.Text.Json.JsonSerializer.Deserialize<WeChatPayErrorResponse>(getdata);
    if (!StringExtension.IsNullOrEmpty(error.ErrorMessage))
    {
        return APIResult.Error(error.ErrorMessage);
    }
    string prepayId = error.prepay_id;

    // 8. 生成小程序调起支付的paySign
    string paySign = GenerateMiniProgramPaySign(
        _appid,
        timestamp,
        nonce,
        prepayId,
        privateKey);

    // 9. 构建返回给小程序的数据
    PaymentEntity en = new PaymentEntity();
    en.timeStamp = timestamp;
    en.nonceStr = nonce;
    en.package = "prepay_id=" + prepayId;
    en.paySign = paySign;
    en.signType = "RSA";
    en.parorderid = bookingNo;

    return new APIResult()
    {
        Data = en
    };
}

签名相关方法

csharp 复制代码
/// <summary>
/// 生成签名
/// </summary>
/// <param name="method">HTTP方法(GET/POST)</param>
/// <param name="url">请求URL</param>
/// <param name="timestamp">时间戳</param>
/// <param name="nonce">随机字符串</param>
/// <param name="body">请求体(可为null)</param>
/// <returns>签名字符串(Base64)</returns>
public string GenerateSignature(string method, string url, string timestamp, string nonce, string body = null)
{
    // 1. 构造签名字符串
    var message = BuildSignMessage(method, url, timestamp, nonce, body);
    // 2. 使用私钥对消息进行签名
    var signature = SignWithPrivateKey(message);
    return signature;
}

/// <summary>
/// 构建签名字符串
/// </summary>
private string BuildSignMessage(string method, string url, string timestamp, string nonce, string body)
{
    // 处理URL路径(去掉域名部分)
    var uri = new Uri(url, UriKind.RelativeOrAbsolute);
    var path = uri.IsAbsoluteUri ? uri.PathAndQuery : url;

    // 构造签名字符串
    // 格式: HTTP方法\n请求路径\n时间戳\n随机字符串\n请求体\n
    var builder = new StringBuilder();
    builder.Append(method.ToUpper()).Append('\n');  // HTTP方法
    builder.Append(path).Append('\n');             // 请求路径
    builder.Append(timestamp).Append('\n');        // 时间戳
    builder.Append(nonce).Append('\n');            // 随机字符串

    if (!string.IsNullOrEmpty(body))
    {
        builder.Append(body).Append('\n');         // 请求体
    }
    else
    {
        builder.Append('\n');                      // 空行
    }

    return builder.ToString();
}

/// <summary>
/// 使用私钥签名
/// </summary>
private string SignWithPrivateKey(string message)
{
    // 加载私钥
    using var rsa = RSA.Create();
    rsa.ImportFromPem(privateKey);

    // 计算签名
    var data = Encoding.UTF8.GetBytes(message);
    var signature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

    return Convert.ToBase64String(signature);
}

/// <summary>
/// 生成小程序调起支付的paySign
/// </summary>
/// <param name="appId">小程序AppID</param>
/// <param name="timeStamp">时间戳</param>
/// <param name="nonceStr">随机字符串</param>
/// <param name="prepayId">预支付会话标识</param>
/// <param name="privateKeyPem">私钥(PEM格式)</param>
/// <returns>签名字符串(Base64)</returns>
public string GenerateMiniProgramPaySign(string appId, string timeStamp, string nonceStr, string prepayId, string privateKeyPem)
{
    // 构造签名字符串
    // 格式: AppId\n时间戳\n随机字符串\nprepay_id=xxx\n
    var builder = new StringBuilder();
    builder.Append(appId).Append('\n');
    builder.Append(timeStamp).Append('\n');
    builder.Append(nonceStr).Append('\n');
    builder.Append($"prepay_id={prepayId}").Append('\n');

    // SHA256-RSA签名
    using var rsa = RSA.Create();
    rsa.ImportFromPem(privateKeyPem); // 加载PKCS#8格式私钥
    byte[] signature = rsa.SignData(
        Encoding.UTF8.GetBytes(builder.ToString()),
        HashAlgorithmName.SHA256,
        RSASignaturePadding.Pkcs1
    );

    return Convert.ToBase64String(signature);
}

Http请求方法 - HttpHelper.cs

完整的 HTTP 请求工具类,用于发送 GET/POST 请求到微信支付API:

csharp 复制代码
using Hyzx.Cxy.Common.Extensions;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System.Text;

namespace Hyzx.Cxy.Common.Helper
{
    /// <summary>
    /// HTTP请求帮助类
    /// </summary>
    public static class HttpHelper
    {
        #region 获取所有请求的参数(包括get参数和post参数)
        /// <summary>
        /// 获取所有请求的参数(包括get参数和post参数)
        /// </summary>
        /// <param name="context">请求上下文</param>
        /// <returns></returns>
        public static async Task<Dictionary<string, object>> GetAllRequestParams(HttpContext context)
        {
            Dictionary<string, object> allParams = new Dictionary<string, object>();
            var request = context.Request;
            List<string> paramKeys = new List<string>();
            var getParams = request.Query.Keys.ToList();
            var postParams = new List<string>();
            string contentType = request.ContentType?.ToLower() ?? "";

            // 若为POST的application/json
            if (contentType.Contains("application/json"))
            {
                var stream = request.Body;
                string str = await stream.ReadToString(Encoding.UTF8);
                using var reader = new StreamReader(request.Body, Encoding.UTF8);
                string body = await reader.ReadToEndAsync();

                if (!str.IsNullOrEmpty())
                {
                    var obj = str.ToJObject();
                    foreach (var aProperty in obj)
                    {
                        allParams[aProperty.Key] = aProperty.Value;
                    }
                }
            }
            else
            {
                try
                {
                    if (request.Method.ToLower() != "get")
                        postParams = request.Form.Keys.ToList();
                }
                catch { }
            }
            paramKeys.AddRange(getParams);
            paramKeys.AddRange(postParams);

            paramKeys.ForEach(aParam =>
            {
                object value = null;
                if (request.Query.ContainsKey(aParam))
                    value = request.Query[aParam].ToString();
                else if (request.Form.ContainsKey(aParam))
                    value = request.Form[aParam].ToString();

                if (aParam == "Password")
                    allParams.Add("Password", "********");
                else
                    allParams.Add(aParam, value);
            });

            return allParams;
        }
        #endregion

        #region 发起POST同步请求
        /// <summary>
        /// 发起POST同步请求
        /// </summary>
        /// <param name="url">请求地址</param>
        /// <param name="postData">POST数据</param>
        /// <param name="contentType">内容类型(application/xml、application/json、application/text、application/x-www-form-urlencoded)</param>
        /// <param name="timeOut">超时时间(秒)</param>
        /// <param name="headers">请求头</param>
        /// <returns></returns>
        public static string HttpPost(string url, string postData = null, string contentType = null, int timeOut = 30, Dictionary<string, string> headers = null)
        {
            postData ??= "";
            using HttpClient client = new HttpClient();
            client.Timeout = new TimeSpan(0, 0, timeOut);
            if (headers != null)
            {
                foreach (var header in headers)
                    client.DefaultRequestHeaders.Add(header.Key, header.Value);
            }
            using HttpContent httpContent = new StringContent(postData, Encoding.UTF8);
            if (contentType != null)
                httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);

            HttpResponseMessage response = client.PostAsync(url, httpContent).Result;
            return response.Content.ReadAsStringAsync().Result;
        }

        /// <summary>
        /// 发起POST异步请求(微信支付专用)
        /// </summary>
        /// <param name="url">请求地址</param>
        /// <param name="postData">POST数据</param>
        /// <param name="contentType">内容类型</param>
        /// <param name="timeOut">超时时间(秒)</param>
        /// <param name="headers">请求头</param>
        /// <returns></returns>
        public static async Task<string> WxHttpPost(string url, string postData = null, string contentType = null, int timeOut = 30, Dictionary<string, string> headers = null)
        {
            postData ??= "";
            using HttpClient client = new HttpClient();
            client.Timeout = new TimeSpan(0, 0, timeOut);
            if (headers != null)
            {
                foreach (var header in headers)
                    client.DefaultRequestHeaders.Add(header.Key, header.Value);
            }
            using HttpContent httpContent = new StringContent(postData, Encoding.UTF8);
            if (contentType != null)
                httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);

            HttpResponseMessage response = await client.PostAsync(url, httpContent);
            return await response.Content.ReadAsStringAsync();
        }

        /// <summary>
        /// 发起POST请求并返回流
        /// </summary>
        public static Stream HttpPostStream(string url, string postData = null, string contentType = null, int timeOut = 30, Dictionary<string, string> headers = null)
        {
            postData ??= "";
            using HttpClient client = new HttpClient();
            client.Timeout = new TimeSpan(0, 0, timeOut);
            if (headers != null)
            {
                foreach (var header in headers)
                    client.DefaultRequestHeaders.Add(header.Key, header.Value);
            }
            using HttpContent httpContent = new StringContent(postData, Encoding.UTF8);
            if (contentType != null)
                httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);

            HttpResponseMessage response = client.PostAsync(url, httpContent).Result;
            return response.Content.ReadAsStreamAsync().Result;
        }
        #endregion

        #region 发起POST异步请求
        /// <summary>
        /// 发起POST异步请求
        /// </summary>
        public static async Task<string> HttpPostAsync(string url, string postData = null, string contentType = null, int timeOut = 30, Dictionary<string, string> headers = null)
        {
            postData ??= "";
            using HttpClient client = new HttpClient();
            client.Timeout = new TimeSpan(0, 0, timeOut);
            if (headers != null)
            {
                foreach (var header in headers)
                    client.DefaultRequestHeaders.Add(header.Key, header.Value);
            }
            using HttpContent httpContent = new StringContent(postData, Encoding.UTF8);
            if (contentType != null)
                httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);

            HttpResponseMessage response = await client.PostAsync(url, httpContent);
            return await response.Content.ReadAsStringAsync();
        }
        #endregion

        #region 发起GET同步请求
        /// <summary>
        /// 发起GET同步请求
        /// </summary>
        public static string HttpGet(string url, Dictionary<string, string> headers = null)
        {
            using HttpClient client = new HttpClient();
            if (headers != null)
            {
                foreach (var header in headers)
                    client.DefaultRequestHeaders.Add(header.Key, header.Value);
            }
            else
            {
                client.DefaultRequestHeaders.Add("ContentType", "application/x-www-form-urlencoded");
                client.DefaultRequestHeaders.Add("UserAgent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)");
            }
            try
            {
                HttpResponseMessage response = client.GetAsync(url).Result;
                return response.Content.ReadAsStringAsync().Result;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[Http请求出错]{url}|{ex.Message}");
            }
            return "";
        }
        #endregion

        #region 发起GET异步请求
        /// <summary>
        /// 发起GET异步请求
        /// </summary>
        public static async Task<string> HttpGetAsync(string url, Dictionary<string, string> headers = null)
        {
            using HttpClient client = new HttpClient();
            if (headers != null)
            {
                foreach (var header in headers)
                    client.DefaultRequestHeaders.Add(header.Key, header.Value);
            }
            HttpResponseMessage response = await client.GetAsync(url);
            return await response.Content.ReadAsStringAsync();
        }
        #endregion
    }
}

Service中调用的sendPost封装

csharp 复制代码
/// <summary>
/// 发送POST请求到微信支付API
/// </summary>
/// <param name="URL">请求地址</param>
/// <param name="urlArgs">请求参数(JSON字符串)</param>
/// <param name="authorization">Authorization头</param>
/// <returns>响应JSON字符串</returns>
private static async Task<string> sendPost(string URL, string urlArgs, string authorization)
{
    Dictionary<string, string> headers = new Dictionary<string, string>
    {
        ["Accept"] = "application/json",
        ["User-Agent"] = "WeChatPay Aos/1.0",
        ["Authorization"] = authorization
    };

    var str = await HttpHelper.WxHttpPost(URL, urlArgs, "application/json", 30, headers);
    return str;
}

DTO定义

PaymentEntity - 支付参数实体

csharp 复制代码
namespace Hyzx.Cxy.Entity.VO
{
    public class PaymentEntity
    {
        /// <summary>
        /// 时间戳
        /// </summary>
        public string timeStamp { get; set; }
        
        /// <summary>
        /// 随机字符串
        /// </summary>
        public string nonceStr { get; set; }
        
        /// <summary>
        /// 订单详情扩展字符串(格式: prepay_id=xxx)
        /// </summary>
        public string package { get; set; }
        
        /// <summary>
        /// 签名(使用RSA加密)
        /// </summary>
        public string paySign { get; set; }
        
        /// <summary>
        /// 签名类型(RSA)
        /// </summary>
        public string signType { get; set; }
        
        /// <summary>
        /// 商户订单号
        /// </summary>
        public string parorderid { get; set; }
    }
}

WeChatPayErrorResponse - 微信支付响应DTO

csharp 复制代码
namespace Hyzx.Cxy.Entity.DTO
{
    public class WeChatPayErrorResponse
    {
        /// <summary>
        /// 错误码(有错误时返回)
        /// </summary>
        [JsonPropertyName("code")]
        public string ErrorCode { get; set; }

        /// <summary>
        /// 错误信息(有错误时返回)
        /// </summary>
        [JsonPropertyName("message")]
        public string ErrorMessage { get; set; }

        /// <summary>
        /// 错误详情
        /// </summary>
        [JsonPropertyName("detail")]
        public ErrorDetail Detail { get; set; }

        /// <summary>
        /// 预支付会话标识(成功时返回)
        /// </summary>
        [JsonPropertyName("prepay_id")]
        public string prepay_id { get; set; }
        
        public class ErrorDetail
        {
            [JsonPropertyName("field")]
            public string Field { get; set; }

            [JsonPropertyName("value")]
            public string InvalidValue { get; set; }

            [JsonPropertyName("issue")]
            public string Issue { get; set; }

            [JsonPropertyName("location")]
            public string Location { get; set; }
        }
    }
}

处理流程图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    商户后端 Getprepay_id()                       │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  1. 构建请求数据                                                 │
│     - appid, mchid, description                                  │
│     - out_trade_no(商户订单号)                                   │
│     - notify_url(回调地址)                                       │
│     - amount(金额), payer(openid)                                │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  2. 生成签名 Authorization                                      │
│     - timestamp: 时间戳                                          │
│     - nonce: 随机字符串                                           │
│     - signature: 使用私钥对请求签名                               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  3. 调用微信统一下单API                                           │
│     POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi│
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  4. 接收响应                                                      │
│     - 成功: { prepay_id: "wx..." }                               │
│     - 失败: { code: "ERROR", message: "..." }                    │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  5. 生成小程序调起支付签名                                        │
│     - appId + timeStamp + nonceStr + prepay_id                 │
│     - 使用私钥签名                                               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  6. 返回 PaymentEntity                                           │
│     { timeStamp, nonceStr, package, paySign, signType }         │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    小程序前端 wx.requestPayment()                │
└─────────────────────────────────────────────────────────────────┘

小程序前端调起支付

商户后端返回 PaymentEntity 后,小程序前端调用微信支付:

javascript 复制代码
// 1. 调用后端接口获取支付参数
wx.request({
  url: 'https://your-domain.com/api/Home/GetWxPay',
  method: 'GET',
  data: {
    openid: '用户openid',
    amount: 100,  // 金额(分)
    orderid: '商户订单号'
  },
  success: function(res) {
    // 2. 获取到支付参数
    var paymentData = res.data.Data;
    
    // 3. 调起微信支付
    wx.requestPayment({
      timeStamp: paymentData.timeStamp,      // 时间戳
      nonceStr: paymentData.nonceStr,         // 随机字符串
      package: paymentData.package,           // prepay_id=xxx
      signType: paymentData.signType,         // 签名类型(RSA)
      paySign: paymentData.paySign,           // 签名
      success: function(res) {
        console.log('支付成功');
        // 支付成功后,微信会回调 notify_url
      },
      fail: function(res) {
        console.log('支付失败', res);
      }
    });
  }
});

合单下单(多次支付)

如果需要一次支付多个订单,可以使用合单下单接口:

csharp 复制代码
/// <summary>
/// JSAPI合单下单
/// </summary>
/// <param name="attach">商品描述</param>
/// <param name="openid">用户openid</param>
/// <param name="orderid">多个订单ID(逗号分隔)</param>
/// <returns></returns>
public async Task<APIResult> CombineGetprepay_id(string attach, string openid, string orderid)
{
    var url = "https://api.mch.weixin.qq.com/v3/combine-transactions/jsapi";
    var notify_url = "https://your-domain.com/api/Home/HandleNotify";
    var orderids = orderid.Split(',', StringSplitOptions.RemoveEmptyEntries);
    var orederlist = await db.Queryable<h_bd_order>().Where(v => SqlFunc.ContainsArray(orderids, v.id)).ToListAsync();

    var parorderid = IdHelper.GetId();  // 生成合单父订单号

    // 构建请求数据
    var requestData = new
    {
        combine_appid = _appid,
        combine_mchid = _mch_id,
        combine_out_trade_no = parorderid,      // 合单订单号
        combine_payer_info = new
        {
            openid = openid
        },
        sub_orders = orederlist.Select(order => new
        {
            mchid = _mch_id,
            out_trade_no = order.id.ToString(),  // 子订单号
            amount = new
            {
                total_amount = Convert.ToInt32(order.amount * 100),  // 金额(分)
                currency = "CNY"
            },
            description = "小程序订单",
            attach = order.ordernumber ?? "",
        }).ToArray(),
        notify_url = notify_url
    };

    // 后续步骤同 Getprepay_id()...
}

注意事项

  1. 金额单位 :微信支付金额单位是,不是元

  2. 私钥格式:必须使用 PKCS#8 格式的私钥

  3. 回调地址:必须是 HTTPS 协议的公网可访问地址

  4. 签名串格式

    • 下单API签名:{method}\n{path}\n{timestamp}\n{nonce}\n{body}\n
    • 小程序调起支付签名:{appId}\n{timeStamp}\n{nonceStr}\nprepay_id={prepayId}\n
  5. prepay_id有效期:prepay_id 的有效期为2小时,过期需要重新下单