diff --git a/server/HoneyBox/src/HoneyBox.Api/appsettings.json b/server/HoneyBox/src/HoneyBox.Api/appsettings.json index 5e5314fb..e35520ff 100644 --- a/server/HoneyBox/src/HoneyBox.Api/appsettings.json +++ b/server/HoneyBox/src/HoneyBox.Api/appsettings.json @@ -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" diff --git a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IWechatService.cs b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IWechatService.cs index b0a494c7..f5d908dc 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IWechatService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IWechatService.cs @@ -30,10 +30,10 @@ public interface IWechatService Task GetAccessTokenAsync(string? appId = null); /// - /// 创建支付订单(Web支付方式) + /// 创建支付订单(原生微信支付) /// /// 支付请求 - /// 支付结果 + /// 支付结果,包含前端调用 uni.requestPayment 所需的参数 Task CreatePayOrderAsync(CreatePayRequest request); } @@ -94,13 +94,49 @@ public class CreatePayResult public string OrderNo { get; set; } = string.Empty; /// - /// 支付参数(返回给前端) + /// 支付参数(返回给前端,用于调用 uni.requestPayment) /// - public WebPayParams? Res { get; set; } + public NativePayParams? Res { get; set; } } /// -/// Web支付参数 +/// 原生微信支付参数(用于 uni.requestPayment) +/// +public class NativePayParams +{ + /// + /// 小程序AppId + /// + public string AppId { get; set; } = string.Empty; + + /// + /// 时间戳(秒) + /// + public string TimeStamp { get; set; } = string.Empty; + + /// + /// 随机字符串 + /// + public string NonceStr { get; set; } = string.Empty; + + /// + /// 统一下单接口返回的 prepay_id(格式:prepay_id=xxx) + /// + public string Package { get; set; } = string.Empty; + + /// + /// 签名类型 + /// + public string SignType { get; set; } = "MD5"; + + /// + /// 签名 + /// + public string PaySign { get; set; } = string.Empty; +} + +/// +/// Web支付参数(客服消息支付,备用) /// public class WebPayParams { diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/ConfigService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/ConfigService.cs index f1c9feb1..d22de694 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Services/ConfigService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Services/ConfigService.cs @@ -105,28 +105,15 @@ public class ConfigService : IConfigService public Task 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); } diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/WarehouseService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/WarehouseService.cs index 4ef31810..53f6ad8c 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Services/WarehouseService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Services/WarehouseService.cs @@ -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 }; } diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/WechatService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/WechatService.cs index 2a6b3ea0..0ed2219b 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Services/WechatService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Services/WechatService.cs @@ -401,7 +401,7 @@ public class WechatService : IWechatService } /// - /// 创建支付订单(Web支付方式) + /// 创建支付订单(原生微信支付) /// public async Task 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 - { - { "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 { - 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 + { + { "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 } } + /// + /// 获取商户配置 + /// + private WechatPayMerchantConfig? GetMerchantConfig() + { + // 优先使用默认商户配置 + if (!string.IsNullOrEmpty(_wechatPaySettings.DefaultMerchant?.MchId)) + { + return _wechatPaySettings.DefaultMerchant; + } + + // 从商户列表中获取第一个 + return _wechatPaySettings.Merchants.FirstOrDefault(); + } + + /// + /// 生成随机字符串 + /// + 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()); + } + + /// + /// 生成回调通知URL + /// + 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}"; + } + + /// + /// 生成签名(MD5) + /// + private static string MakeSign(SortedDictionary 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(); + } + + /// + /// 字典转XML + /// + private static string DictToXml(SortedDictionary dict) + { + var sb = new System.Text.StringBuilder(); + sb.Append(""); + foreach (var kvp in dict) + { + sb.Append($"<{kvp.Key}>"); + } + sb.Append(""); + return sb.ToString(); + } + + /// + /// XML转字典 + /// + private static Dictionary XmlToDict(string xml) + { + var dict = new Dictionary(); + 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; + } + /// /// 生成订单号 /// 格式:前缀(3位) + 商户前缀(3位) + 项目前缀(2位) + 支付类型(3位) + 时间戳 + 随机数 diff --git a/server/HoneyBox/src/HoneyBox.Model/Models/Order/WarehouseModels.cs b/server/HoneyBox/src/HoneyBox.Model/Models/Order/WarehouseModels.cs index c7b5c75b..16e1f311 100644 --- a/server/HoneyBox/src/HoneyBox.Model/Models/Order/WarehouseModels.cs +++ b/server/HoneyBox/src/HoneyBox.Model/Models/Order/WarehouseModels.cs @@ -473,14 +473,56 @@ public class SendResultDto public string OrderNo { get; set; } = string.Empty; /// - /// 支付参数(需要支付运费时返回,支持Web支付和微信原生支付) + /// 支付参数(需要支付运费时返回,原生微信支付参数) /// [JsonPropertyName("res")] - public WebPayResultDto? Res { get; set; } + public NativePayResultDto? Res { get; set; } } /// -/// Web支付结果DTO(兼容前端支付流程) +/// 原生微信支付结果DTO(用于 uni.requestPayment) +/// +public class NativePayResultDto +{ + /// + /// 小程序AppId + /// + [JsonPropertyName("appId")] + public string AppId { get; set; } = string.Empty; + + /// + /// 时间戳(秒) + /// + [JsonPropertyName("timeStamp")] + public string TimeStamp { get; set; } = string.Empty; + + /// + /// 随机字符串 + /// + [JsonPropertyName("nonceStr")] + public string NonceStr { get; set; } = string.Empty; + + /// + /// 统一下单接口返回的 prepay_id(格式:prepay_id=xxx) + /// + [JsonPropertyName("package")] + public string Package { get; set; } = string.Empty; + + /// + /// 签名类型 + /// + [JsonPropertyName("signType")] + public string SignType { get; set; } = "MD5"; + + /// + /// 签名 + /// + [JsonPropertyName("paySign")] + public string PaySign { get; set; } = string.Empty; +} + +/// +/// Web支付结果DTO(兼容前端支付流程,备用) /// public class WebPayResultDto {