feat: 实现原生微信支付功能

- 修改 ConfigService.GetPlatformConfigAsync 返回 isWebPay=false,关闭 Web 支付
- 重写 WechatService.CreatePayOrderAsync 实现原生微信支付:
  - 调用微信统一下单 API 获取 prepay_id
  - 生成 MD5 签名
  - 返回前端 uni.requestPayment 所需的参数(appId, timeStamp, nonceStr, package, signType, paySign)
- 更新 IWechatService 接口,添加 NativePayParams 类型
- 更新 WarehouseModels.cs,SendResultDto 使用 NativePayResultDto
- 更新 WarehouseService.SendPrizesAsync 使用新的支付参数格式
- 更新 appsettings.json 配置微信支付商户信息
This commit is contained in:
gpu 2026-01-24 13:15:12 +08:00
parent 4fba70467d
commit 9867b87594
6 changed files with 338 additions and 78 deletions

View File

@ -4,15 +4,15 @@
"Redis": "192.168.195.15:6379,abortConnect=false,connectTimeout=5000"
},
"WechatSettings": {
"AppId": "wxa17265f5fe8374b1",
"AppSecret": "af99a9c8f1b986ded540d317879cc799"
"AppId": "wx595ec949c6efd72b",
"AppSecret": "1677345a20450146cf4610a41b49794d"
},
"WechatPaySettings": {
"DefaultMerchant": {
"Name": "默认商户",
"MchId": "",
"AppId": "wxa17265f5fe8374b1",
"Key": "",
"MchId": "1680394019",
"AppId": "wx595ec949c6efd72b",
"Key": "bad162066cbc9c34bb457e6997b7255b",
"OrderPrefix": "MYH",
"Weight": 1,
"NotifyUrl": ""
@ -21,7 +21,7 @@
"Miniprograms": [],
"UnifiedOrderUrl": "https://api.mch.weixin.qq.com/pay/unifiedorder",
"ShippingNotifyUrl": "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info",
"NotifyBaseUrl": ""
"NotifyBaseUrl": "https://api.zfunbox.cn"
},
"AmapSettings": {
"ApiKey": "6a46ad822120e393956e89d498e8c40b"

View File

@ -30,10 +30,10 @@ public interface IWechatService
Task<string?> GetAccessTokenAsync(string? appId = null);
/// <summary>
/// 创建支付订单(Web支付方式
/// 创建支付订单(原生微信支付
/// </summary>
/// <param name="request">支付请求</param>
/// <returns>支付结果</returns>
/// <returns>支付结果,包含前端调用 uni.requestPayment 所需的参数</returns>
Task<CreatePayResult> CreatePayOrderAsync(CreatePayRequest request);
}
@ -94,13 +94,49 @@ public class CreatePayResult
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 支付参数(返回给前端
/// 支付参数(返回给前端,用于调用 uni.requestPayment
/// </summary>
public WebPayParams? Res { get; set; }
public NativePayParams? Res { get; set; }
}
/// <summary>
/// Web支付参数
/// 原生微信支付参数(用于 uni.requestPayment
/// </summary>
public class NativePayParams
{
/// <summary>
/// 小程序AppId
/// </summary>
public string AppId { get; set; } = string.Empty;
/// <summary>
/// 时间戳(秒)
/// </summary>
public string TimeStamp { get; set; } = string.Empty;
/// <summary>
/// 随机字符串
/// </summary>
public string NonceStr { get; set; } = string.Empty;
/// <summary>
/// 统一下单接口返回的 prepay_id格式prepay_id=xxx
/// </summary>
public string Package { get; set; } = string.Empty;
/// <summary>
/// 签名类型
/// </summary>
public string SignType { get; set; } = "MD5";
/// <summary>
/// 签名
/// </summary>
public string PaySign { get; set; } = string.Empty;
}
/// <summary>
/// Web支付参数客服消息支付备用
/// </summary>
public class WebPayParams
{

View File

@ -105,28 +105,15 @@ public class ConfigService : IConfigService
public Task<PlatformConfigDto> GetPlatformConfigAsync(string? platform)
{
// 根据平台返回不同配置
// 目前小程序和H5都返回 isWebPay = true
// isWebPay = false 表示直接拉起微信支付,true 表示走客服消息支付
var config = new PlatformConfigDto
{
IsWebPay = true
IsWebPay = false // 默认关闭 Web 支付,使用原生微信支付
};
// 可以根据 platform 参数返回不同配置
// 例如: MP-WEIXIN, WEB_H5, APP_ANDROID 等
if (!string.IsNullOrEmpty(platform))
{
switch (platform.ToUpper())
{
case "WEB_H5":
case "WEB_APP":
config.IsWebPay = false;
break;
case "MP-WEIXIN":
default:
config.IsWebPay = true;
break;
}
}
// 目前所有平台都使用原生支付
return Task.FromResult(config);
}

View File

@ -1142,14 +1142,14 @@ public class WarehouseService : IWarehouseService
{
Status = 1, // 需要支付
OrderNo = payResult.OrderNo,
Res = payResult.Res != null ? new WebPayResultDto
Res = payResult.Res != null ? new NativePayResultDto
{
Data = new WebPayDataDto
{
OrderNum = payResult.OrderNo
},
RequestPay = payResult.Res.RequestPay,
Tips = payResult.Res.Tips
AppId = payResult.Res.AppId,
TimeStamp = payResult.Res.TimeStamp,
NonceStr = payResult.Res.NonceStr,
Package = payResult.Res.Package,
SignType = payResult.Res.SignType,
PaySign = payResult.Res.PaySign
} : null
};
}

View File

@ -401,7 +401,7 @@ public class WechatService : IWechatService
}
/// <summary>
/// 创建支付订单(Web支付方式
/// 创建支付订单(原生微信支付
/// </summary>
public async Task<CreatePayResult> CreatePayOrderAsync(CreatePayRequest request)
{
@ -413,7 +413,7 @@ public class WechatService : IWechatService
// 如果金额为0直接返回成功免费订单
if (request.Price <= 0)
{
var freeOrderNo = GenerateOrderNo(request.Prefix, "MON", "H5", "ZFB");
var freeOrderNo = GenerateOrderNo(request.Prefix, "MON", "YD", "MP0");
_logger.LogInformation("[创建支付订单] 免费订单,直接返回成功: OrderNo={OrderNo}", freeOrderNo);
return new CreatePayResult
{
@ -423,59 +423,159 @@ public class WechatService : IWechatService
};
}
// 生成订单号
var orderNo = GenerateOrderNo(request.Prefix, "ZFA", "H5", "ZFB");
// 获取商户配置
var merchantConfig = GetMerchantConfig();
if (merchantConfig == null)
{
_logger.LogError("[创建支付订单] 未找到商户配置");
return new CreatePayResult
{
Status = 0,
Message = "支付配置错误",
OrderNo = string.Empty
};
}
// 生成订单号:前缀 + 商户前缀 + 小程序前缀 + 支付类型 + 时间戳 + 随机数
var orderNo = GenerateOrderNo(request.Prefix, merchantConfig.OrderPrefix, "YD", "MP0");
// 截取标题最多30个字符
var title = request.Title.Length > 30 ? request.Title.Substring(0, 30) : request.Title;
// 构建扩展数据
var extend = new Dictionary<string, string>
{
{ "orderType", request.Attach }
};
var extendStr = JsonSerializer.Serialize(extend);
// 生成随机字符串
var nonceStr = GenerateNonceStr();
var callbackNonceStr = GenerateNonceStr();
// 创建支付通知记录
var orderNotify = new OrderNotify
// 生成回调通知URL
var notifyUrl = GenerateNotifyUrl(request.Attach, request.UserId, orderNo, callbackNonceStr);
// 获取客户端IP
var clientIp = "127.0.0.1"; // 实际应从请求中获取
// 构建统一下单参数
var unifiedOrderParams = new SortedDictionary<string, string>
{
OrderNo = orderNo,
NotifyUrl = string.Empty,
NonceStr = string.Empty,
PayTime = DateTime.UtcNow,
PayAmount = request.Price,
Status = 0, // 待处理
RetryCount = 0,
Attach = request.Attach,
OpenId = request.OpenId,
Extend = extendStr,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
{ "appid", _wechatSettings.AppId },
{ "mch_id", merchantConfig.MchId },
{ "nonce_str", nonceStr },
{ "body", title },
{ "attach", request.Attach },
{ "out_trade_no", orderNo },
{ "notify_url", notifyUrl },
{ "total_fee", ((int)(request.Price * 100)).ToString() }, // 转换为分
{ "spbill_create_ip", clientIp },
{ "trade_type", "JSAPI" },
{ "openid", request.OpenId }
};
_dbContext.OrderNotifies.Add(orderNotify);
await _dbContext.SaveChangesAsync();
// 生成签名
var sign = MakeSign(unifiedOrderParams, merchantConfig.Key);
unifiedOrderParams.Add("sign", sign);
_logger.LogInformation("[创建支付订单] 订单记录已创建: OrderNo={OrderNo}, NotifyId={NotifyId}",
orderNo, orderNotify.Id);
// 转换为XML
var xmlData = DictToXml(unifiedOrderParams);
// 构建Web支付参数
var webPayParams = new WebPayParams
_logger.LogInformation("[创建支付订单] 调用微信统一下单API: OrderNo={OrderNo}", orderNo);
// 调用微信统一下单API
var content = new StringContent(xmlData, System.Text.Encoding.UTF8, "application/xml");
var response = await _httpClient.PostAsync(_wechatPaySettings.UnifiedOrderUrl, content);
var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogInformation("[创建支付订单] 微信统一下单响应: {Response}", responseContent);
// 解析响应
var result = XmlToDict(responseContent);
if (result.TryGetValue("return_code", out var returnCode) && returnCode == "SUCCESS" &&
result.TryGetValue("result_code", out var resultCode) && resultCode == "SUCCESS")
{
Data = new WebPayData
// 获取 prepay_id
if (!result.TryGetValue("prepay_id", out var prepayId))
{
OrderNum = orderNo
},
RequestPay = "/api/send_web_pay_order",
Tips = "您即将进入客服聊天界面完成支付也可前往「我的」页面下载官方APP享受更便捷的购物及充值服务。"
};
_logger.LogError("[创建支付订单] 微信返回数据缺少 prepay_id");
return new CreatePayResult
{
Status = 0,
Message = "微信返回数据异常",
OrderNo = string.Empty
};
}
return new CreatePayResult
// 保存订单通知记录
var orderNotify = new OrderNotify
{
OrderNo = orderNo,
NotifyUrl = notifyUrl,
NonceStr = callbackNonceStr,
PayTime = DateTime.UtcNow,
PayAmount = request.Price,
Status = 0, // 待支付
RetryCount = 0,
Attach = request.Attach,
OpenId = request.OpenId,
Extend = JsonSerializer.Serialize(new { orderType = request.Attach, title = title }),
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_dbContext.OrderNotifies.Add(orderNotify);
await _dbContext.SaveChangesAsync();
// 生成前端支付参数
var timeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var payNonceStr = GenerateNonceStr();
var payParams = new SortedDictionary<string, string>
{
{ "appId", _wechatSettings.AppId },
{ "timeStamp", timeStamp },
{ "nonceStr", payNonceStr },
{ "package", $"prepay_id={prepayId}" },
{ "signType", "MD5" }
};
var paySign = MakeSign(payParams, merchantConfig.Key);
_logger.LogInformation("[创建支付订单] 订单创建成功: OrderNo={OrderNo}, PrepayId={PrepayId}", orderNo, prepayId);
return new CreatePayResult
{
Status = 1,
OrderNo = orderNo,
Res = new NativePayParams
{
AppId = _wechatSettings.AppId,
TimeStamp = timeStamp,
NonceStr = payNonceStr,
Package = $"prepay_id={prepayId}",
SignType = "MD5",
PaySign = paySign
}
};
}
else
{
Status = 1,
OrderNo = orderNo,
Res = webPayParams
};
// 解析错误信息
var errorMsg = "微信支付接口返回异常";
if (result.TryGetValue("return_msg", out var returnMsg))
{
errorMsg = returnMsg;
}
else if (result.TryGetValue("err_code_des", out var errCodeDes))
{
errorMsg = errCodeDes;
}
_logger.LogError("[创建支付订单] 微信统一下单失败: {Error}, Response: {Response}", errorMsg, responseContent);
return new CreatePayResult
{
Status = 0,
Message = errorMsg,
OrderNo = string.Empty
};
}
}
catch (Exception ex)
{
@ -489,6 +589,101 @@ public class WechatService : IWechatService
}
}
/// <summary>
/// 获取商户配置
/// </summary>
private WechatPayMerchantConfig? GetMerchantConfig()
{
// 优先使用默认商户配置
if (!string.IsNullOrEmpty(_wechatPaySettings.DefaultMerchant?.MchId))
{
return _wechatPaySettings.DefaultMerchant;
}
// 从商户列表中获取第一个
return _wechatPaySettings.Merchants.FirstOrDefault();
}
/// <summary>
/// 生成随机字符串
/// </summary>
private static string GenerateNonceStr(int length = 32)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
/// <summary>
/// 生成回调通知URL
/// </summary>
private string GenerateNotifyUrl(string orderType, int userId, string orderNo, string nonceStr)
{
var baseUrl = _wechatPaySettings.NotifyBaseUrl.TrimEnd('/');
return $"{baseUrl}/api/pay/notify?payment_type=wxpay&order_type={orderType}&user_id={userId}&order_no={orderNo}&nonce_str={nonceStr}";
}
/// <summary>
/// 生成签名MD5
/// </summary>
private static string MakeSign(SortedDictionary<string, string> parameters, string key)
{
var sb = new System.Text.StringBuilder();
foreach (var kvp in parameters)
{
if (!string.IsNullOrEmpty(kvp.Value) && kvp.Key != "sign")
{
sb.Append($"{kvp.Key}={kvp.Value}&");
}
}
sb.Append($"key={key}");
using var md5 = System.Security.Cryptography.MD5.Create();
var inputBytes = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
var hashBytes = md5.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToUpper();
}
/// <summary>
/// 字典转XML
/// </summary>
private static string DictToXml(SortedDictionary<string, string> dict)
{
var sb = new System.Text.StringBuilder();
sb.Append("<xml>");
foreach (var kvp in dict)
{
sb.Append($"<{kvp.Key}><![CDATA[{kvp.Value}]]></{kvp.Key}>");
}
sb.Append("</xml>");
return sb.ToString();
}
/// <summary>
/// XML转字典
/// </summary>
private static Dictionary<string, string> XmlToDict(string xml)
{
var dict = new Dictionary<string, string>();
try
{
var doc = System.Xml.Linq.XDocument.Parse(xml);
if (doc.Root != null)
{
foreach (var element in doc.Root.Elements())
{
dict[element.Name.LocalName] = element.Value;
}
}
}
catch
{
// 解析失败返回空字典
}
return dict;
}
/// <summary>
/// 生成订单号
/// 格式:前缀(3位) + 商户前缀(3位) + 项目前缀(2位) + 支付类型(3位) + 时间戳 + 随机数

View File

@ -473,14 +473,56 @@ public class SendResultDto
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 支付参数(需要支付运费时返回,支持Web支付和微信原生支付
/// 支付参数(需要支付运费时返回,原生微信支付参数
/// </summary>
[JsonPropertyName("res")]
public WebPayResultDto? Res { get; set; }
public NativePayResultDto? Res { get; set; }
}
/// <summary>
/// Web支付结果DTO兼容前端支付流程
/// 原生微信支付结果DTO用于 uni.requestPayment
/// </summary>
public class NativePayResultDto
{
/// <summary>
/// 小程序AppId
/// </summary>
[JsonPropertyName("appId")]
public string AppId { get; set; } = string.Empty;
/// <summary>
/// 时间戳(秒)
/// </summary>
[JsonPropertyName("timeStamp")]
public string TimeStamp { get; set; } = string.Empty;
/// <summary>
/// 随机字符串
/// </summary>
[JsonPropertyName("nonceStr")]
public string NonceStr { get; set; } = string.Empty;
/// <summary>
/// 统一下单接口返回的 prepay_id格式prepay_id=xxx
/// </summary>
[JsonPropertyName("package")]
public string Package { get; set; } = string.Empty;
/// <summary>
/// 签名类型
/// </summary>
[JsonPropertyName("signType")]
public string SignType { get; set; } = "MD5";
/// <summary>
/// 签名
/// </summary>
[JsonPropertyName("paySign")]
public string PaySign { get; set; } = string.Empty;
}
/// <summary>
/// Web支付结果DTO兼容前端支付流程备用
/// </summary>
public class WebPayResultDto
{