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:
parent
4fba70467d
commit
9867b87594
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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位) + 时间戳 + 随机数
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user