using System.Text.Json; using HoneyBox.Core.Interfaces; using HoneyBox.Model.Data; using HoneyBox.Model.Entities; using HoneyBox.Model.Models.Auth; using HoneyBox.Model.Models.Payment; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace HoneyBox.Core.Services; /// /// 微信服务实现 /// public class WechatService : IWechatService { private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly WechatSettings _wechatSettings; private readonly WechatPaySettings _wechatPaySettings; private readonly IRedisService _redisService; private readonly HoneyBoxDbContext _dbContext; // 微信API端点 private const string WechatCodeToSessionUrl = "https://api.weixin.qq.com/sns/jscode2session"; private const string WechatGetPhoneNumberUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"; private const string WechatGetAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token"; // Redis缓存键前缀 private const string AccessTokenCacheKeyPrefix = "wechat:access_token:"; public WechatService( HttpClient httpClient, ILogger logger, WechatSettings wechatSettings, IOptions wechatPaySettings, IRedisService redisService, HoneyBoxDbContext dbContext) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _wechatSettings = wechatSettings ?? throw new ArgumentNullException(nameof(wechatSettings)); _wechatPaySettings = wechatPaySettings?.Value ?? throw new ArgumentNullException(nameof(wechatPaySettings)); _redisService = redisService ?? throw new ArgumentNullException(nameof(redisService)); _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } /// /// 获取微信openid和unionid /// public async Task GetOpenIdAsync(string code) { _logger.LogInformation("[微信登录] 开始处理,code={Code}", code); if (string.IsNullOrWhiteSpace(code)) { _logger.LogWarning("[微信登录] code为空"); return new WechatAuthResult { Success = false, ErrorMessage = "授权code不能为空" }; } try { // 记录配置信息(脱敏) var maskedAppId = _wechatSettings.AppId?.Length > 8 ? $"{_wechatSettings.AppId.Substring(0, 4)}****{_wechatSettings.AppId.Substring(_wechatSettings.AppId.Length - 4)}" : "未配置"; var maskedSecret = string.IsNullOrEmpty(_wechatSettings.AppSecret) ? "未配置" : $"{_wechatSettings.AppSecret.Substring(0, 4)}****"; _logger.LogInformation("[微信登录] 配置信息: AppId={AppId}, AppSecret={AppSecret}", maskedAppId, maskedSecret); var url = $"{WechatCodeToSessionUrl}?appid={_wechatSettings.AppId}&secret={_wechatSettings.AppSecret}&js_code={code}&grant_type=authorization_code"; _logger.LogInformation("[微信登录] 调用微信API: {Url}", WechatCodeToSessionUrl); var response = await _httpClient.GetAsync(url); var content = await response.Content.ReadAsStringAsync(); _logger.LogInformation("[微信登录] 微信API响应状态码: {StatusCode}", response.StatusCode); _logger.LogInformation("[微信登录] 微信API响应内容: {Content}", content); if (!response.IsSuccessStatusCode) { _logger.LogError("[微信登录] 微信API返回HTTP错误 {StatusCode}: {Content}", response.StatusCode, content); return new WechatAuthResult { Success = false, ErrorMessage = "微信API调用失败" }; } using var jsonDoc = JsonDocument.Parse(content); var root = jsonDoc.RootElement; // 检查是否有错误 if (root.TryGetProperty("errcode", out var errCode) && errCode.GetInt32() != 0) { var errMsg = root.TryGetProperty("errmsg", out var msg) ? msg.GetString() : "未知错误"; _logger.LogWarning("[微信登录] 微信API返回业务错误: errcode={ErrorCode}, errmsg={ErrorMessage}", errCode.GetInt32(), errMsg); return new WechatAuthResult { Success = false, ErrorMessage = $"微信授权失败: {errMsg}" }; } // 提取openid和unionid var openId = root.TryGetProperty("openid", out var openIdProp) ? openIdProp.GetString() : null; var unionId = root.TryGetProperty("unionid", out var unionIdProp) ? unionIdProp.GetString() : null; var sessionKey = root.TryGetProperty("session_key", out var sessionKeyProp) ? sessionKeyProp.GetString() : null; _logger.LogInformation("[微信登录] 解析结果: openid={OpenId}, unionid={UnionId}, session_key={SessionKey}", openId ?? "null", unionId ?? "null", string.IsNullOrEmpty(sessionKey) ? "null" : "已获取"); if (string.IsNullOrEmpty(openId)) { _logger.LogError("[微信登录] 微信API响应中缺少openid"); return new WechatAuthResult { Success = false, ErrorMessage = "微信返回数据异常" }; } _logger.LogInformation("[微信登录] 成功获取openid: {OpenId}", openId); return new WechatAuthResult { Success = true, OpenId = openId, UnionId = unionId }; } catch (HttpRequestException ex) { _logger.LogError(ex, "[微信登录] HTTP请求异常: {Message}", ex.Message); return new WechatAuthResult { Success = false, ErrorMessage = "网络连接失败" }; } catch (JsonException ex) { _logger.LogError(ex, "[微信登录] JSON解析异常: {Message}", ex.Message); return new WechatAuthResult { Success = false, ErrorMessage = "响应数据格式错误" }; } catch (Exception ex) { _logger.LogError(ex, "[微信登录] 未知异常: {Message}", ex.Message); return new WechatAuthResult { Success = false, ErrorMessage = "系统错误" }; } } /// /// 获取微信授权的手机号 /// public async Task GetMobileAsync(string code) { if (string.IsNullOrWhiteSpace(code)) { _logger.LogWarning("GetMobileAsync called with empty code"); return new WechatMobileResult { Success = false, ErrorMessage = "授权code不能为空" }; } try { // 1. 先获取 access_token var accessToken = await GetAccessTokenAsync(); if (string.IsNullOrEmpty(accessToken)) { _logger.LogError("Failed to get access_token for phone number API"); return new WechatMobileResult { Success = false, ErrorMessage = "获取access_token失败" }; } // 2. 使用 access_token 作为 URL 参数,code 放在请求体中 var url = $"{WechatGetPhoneNumberUrl}?access_token={accessToken}"; var requestBody = new { code = code }; var jsonContent = new StringContent( JsonSerializer.Serialize(requestBody), System.Text.Encoding.UTF8, "application/json"); _logger.LogInformation("Calling WeChat API to get phone number with access_token"); var response = await _httpClient.PostAsync(url, jsonContent); var content = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { _logger.LogError("WeChat API returned error status {StatusCode}: {Content}", response.StatusCode, content); return new WechatMobileResult { Success = false, ErrorMessage = "微信API调用失败" }; } using var jsonDoc = JsonDocument.Parse(content); var root = jsonDoc.RootElement; // 检查是否有错误 if (root.TryGetProperty("errcode", out var errCode) && errCode.GetInt32() != 0) { var errMsg = root.TryGetProperty("errmsg", out var msg) ? msg.GetString() : "未知错误"; _logger.LogWarning("WeChat API returned error: {ErrorCode} - {ErrorMessage}", errCode.GetInt32(), errMsg); return new WechatMobileResult { Success = false, ErrorMessage = $"获取手机号失败: {errMsg}" }; } // 提取手机号 string? mobile = null; if (root.TryGetProperty("phone_info", out var phoneInfo)) { mobile = phoneInfo.TryGetProperty("phoneNumber", out var phoneNumber) ? phoneNumber.GetString() : null; } if (string.IsNullOrEmpty(mobile)) { _logger.LogError("WeChat API response missing phone number"); return new WechatMobileResult { Success = false, ErrorMessage = "微信返回数据异常" }; } _logger.LogInformation("Successfully retrieved phone number from WeChat API"); return new WechatMobileResult { Success = true, Mobile = mobile }; } catch (HttpRequestException ex) { _logger.LogError(ex, "HTTP request error when calling WeChat API"); return new WechatMobileResult { Success = false, ErrorMessage = "网络连接失败" }; } catch (JsonException ex) { _logger.LogError(ex, "JSON parsing error when processing WeChat API response"); return new WechatMobileResult { Success = false, ErrorMessage = "响应数据格式错误" }; } catch (Exception ex) { _logger.LogError(ex, "Unexpected error when calling WeChat API"); return new WechatMobileResult { Success = false, ErrorMessage = "系统错误" }; } } /// /// 获取小程序接口调用凭证(access_token) /// /// 小程序AppId(可选,不传则使用默认配置) /// access_token,失败返回null public async Task GetAccessTokenAsync(string? appId = null) { try { // 确定使用哪个AppId和AppSecret var targetAppId = appId ?? _wechatSettings.AppId; var targetAppSecret = GetAppSecretByAppId(targetAppId); if (string.IsNullOrEmpty(targetAppId) || string.IsNullOrEmpty(targetAppSecret)) { _logger.LogError("无法获取access_token:AppId或AppSecret为空"); return null; } // 尝试从Redis缓存获取 var cacheKey = $"{AccessTokenCacheKeyPrefix}{targetAppId}"; var cachedToken = await _redisService.GetStringAsync(cacheKey); if (!string.IsNullOrEmpty(cachedToken)) { _logger.LogDebug("从缓存获取access_token: AppId={AppId}", targetAppId); return cachedToken; } // 调用微信API获取新的access_token var url = $"{WechatGetAccessTokenUrl}?grant_type=client_credential&appid={targetAppId}&secret={targetAppSecret}"; _logger.LogInformation("调用微信API获取access_token: AppId={AppId}", targetAppId); var response = await _httpClient.GetAsync(url); var content = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { _logger.LogError("微信API返回错误状态 {StatusCode}: {Content}", response.StatusCode, content); return null; } using var jsonDoc = JsonDocument.Parse(content); var root = jsonDoc.RootElement; // 检查是否有错误 if (root.TryGetProperty("errcode", out var errCode) && errCode.GetInt32() != 0) { var errMsg = root.TryGetProperty("errmsg", out var msg) ? msg.GetString() : "未知错误"; _logger.LogWarning("微信API返回错误: {ErrorCode} - {ErrorMessage}", errCode.GetInt32(), errMsg); return null; } // 提取access_token和过期时间 var accessToken = root.TryGetProperty("access_token", out var tokenProp) ? tokenProp.GetString() : null; var expiresIn = root.TryGetProperty("expires_in", out var expiresProp) ? expiresProp.GetInt32() : 7200; if (string.IsNullOrEmpty(accessToken)) { _logger.LogError("微信API响应中缺少access_token"); return null; } // 缓存access_token(提前5分钟过期,避免边界问题) var cacheExpiry = TimeSpan.FromSeconds(Math.Max(expiresIn - 300, 60)); await _redisService.SetStringAsync(cacheKey, accessToken, cacheExpiry); _logger.LogInformation("成功获取access_token: AppId={AppId}, ExpiresIn={ExpiresIn}s", targetAppId, expiresIn); return accessToken; } catch (HttpRequestException ex) { _logger.LogError(ex, "获取access_token时HTTP请求错误"); return null; } catch (JsonException ex) { _logger.LogError(ex, "获取access_token时JSON解析错误"); return null; } catch (Exception ex) { _logger.LogError(ex, "获取access_token时发生未知错误"); return null; } } /// /// 根据AppId获取对应的AppSecret /// private string GetAppSecretByAppId(string appId) { // 首先检查默认配置 if (_wechatSettings.AppId == appId) { return _wechatSettings.AppSecret; } // 从小程序配置列表中查找 var miniprogram = _wechatPaySettings.Miniprograms.FirstOrDefault(m => m.AppId == appId); if (miniprogram != null) { return miniprogram.AppSecret; } // 如果都没找到,返回默认配置的AppSecret _logger.LogWarning("未找到AppId {AppId} 的配置,使用默认AppSecret", appId); return _wechatSettings.AppSecret; } /// /// 创建支付订单(原生微信支付) /// public async Task CreatePayOrderAsync(CreatePayRequest request) { try { _logger.LogInformation("[创建支付订单] 开始处理: UserId={UserId}, Price={Price}, Title={Title}, Attach={Attach}", request.UserId, request.Price, request.Title, request.Attach); // 如果金额为0,直接返回成功(免费订单) if (request.Price <= 0) { var freeOrderNo = GenerateOrderNo(request.Prefix, "MON", "YD", "MP0"); _logger.LogInformation("[创建支付订单] 免费订单,直接返回成功: OrderNo={OrderNo}", freeOrderNo); return new CreatePayResult { Status = 1, OrderNo = freeOrderNo, Res = null }; } // 获取商户配置 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 nonceStr = GenerateNonceStr(); var callbackNonceStr = GenerateNonceStr(); // 生成回调通知URL var notifyUrl = GenerateNotifyUrl(request.Attach, request.UserId, orderNo, callbackNonceStr); // 获取客户端IP var clientIp = "127.0.0.1"; // 实际应从请求中获取 // 构建统一下单参数 var unifiedOrderParams = new SortedDictionary { { "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 } }; // 生成签名 var sign = MakeSign(unifiedOrderParams, merchantConfig.Key); unifiedOrderParams.Add("sign", sign); // 转换为XML var xmlData = DictToXml(unifiedOrderParams); _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") { // 获取 prepay_id if (!result.TryGetValue("prepay_id", out var prepayId)) { _logger.LogError("[创建支付订单] 微信返回数据缺少 prepay_id"); return new CreatePayResult { Status = 0, Message = "微信返回数据异常", OrderNo = string.Empty }; } // 保存订单通知记录 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 { // 解析错误信息 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) { _logger.LogError(ex, "[创建支付订单] 创建失败: UserId={UserId}", request.UserId); return new CreatePayResult { Status = 0, Message = "创建支付订单失败", OrderNo = string.Empty }; } } /// /// 获取商户配置 /// 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位) + 时间戳 + 随机数 /// private string GenerateOrderNo(string prefix, string merchantPrefix, string projectPrefix, string payType) { var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss"); var random = new Random().Next(1000, 9999); return $"{prefix}{merchantPrefix}{projectPrefix}{payType}{timestamp}{random}"; } }