微信支付JSAPI下单文档
目录
概述
微信支付 V3 版本 JSAPI 下单流程:
- 商户后端调用微信支付统一下单API,传入订单信息和用户openid
- 微信返回
prepay_id(预支付交易会话标识) - 商户后端使用私钥对支付参数进行签名
- 小程序前端使用签名后的参数调起微信支付
- 用户支付成功后,微信回调通知商户
涉及对象
| 对象 | 说明 |
|---|---|
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()...
}
注意事项
-
金额单位 :微信支付金额单位是分,不是元
-
私钥格式:必须使用 PKCS#8 格式的私钥
-
回调地址:必须是 HTTPS 协议的公网可访问地址
-
签名串格式:
- 下单API签名:
{method}\n{path}\n{timestamp}\n{nonce}\n{body}\n - 小程序调起支付签名:
{appId}\n{timeStamp}\n{nonceStr}\nprepay_id={prepayId}\n
- 下单API签名:
-
prepay_id有效期:prepay_id 的有效期为2小时,过期需要重新下单