849 lines
33 KiB
C#
849 lines
33 KiB
C#
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.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
|
||
namespace HoneyBox.Core.Services;
|
||
|
||
/// <summary>
|
||
/// 微信服务实现
|
||
/// </summary>
|
||
public class WechatService : IWechatService
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
private readonly ILogger<WechatService> _logger;
|
||
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<WechatService> logger,
|
||
IOptions<WechatPaySettings> wechatPaySettings,
|
||
IRedisService redisService,
|
||
HoneyBoxDbContext dbContext)
|
||
{
|
||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||
_wechatPaySettings = wechatPaySettings?.Value ?? throw new ArgumentNullException(nameof(wechatPaySettings));
|
||
_redisService = redisService ?? throw new ArgumentNullException(nameof(redisService));
|
||
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取微信openid和unionid
|
||
/// </summary>
|
||
public async Task<WechatAuthResult> GetOpenIdAsync(string code)
|
||
{
|
||
_logger.LogInformation("[微信登录] 开始处理,code={Code}", code);
|
||
|
||
if (string.IsNullOrWhiteSpace(code))
|
||
{
|
||
_logger.LogWarning("[微信登录] code为空");
|
||
return new WechatAuthResult
|
||
{
|
||
Success = false,
|
||
ErrorMessage = "授权code不能为空"
|
||
};
|
||
}
|
||
|
||
try
|
||
{
|
||
// 从数据库获取微信配置
|
||
var wechatConfig = await GetWechatSettingFromDbAsync();
|
||
if (wechatConfig == null)
|
||
{
|
||
_logger.LogError("[微信登录] 未找到小程序配置,请在后台管理系统中配置 miniprogram_setting");
|
||
return new WechatAuthResult
|
||
{
|
||
Success = false,
|
||
ErrorMessage = "小程序配置未设置,请联系管理员"
|
||
};
|
||
}
|
||
|
||
var appId = wechatConfig.AppId;
|
||
var appSecret = wechatConfig.AppSecret;
|
||
|
||
// 记录配置信息(脱敏)
|
||
var maskedAppId = appId?.Length > 8
|
||
? $"{appId.Substring(0, 4)}****{appId.Substring(appId.Length - 4)}"
|
||
: "未配置";
|
||
var maskedSecret = string.IsNullOrEmpty(appSecret)
|
||
? "未配置"
|
||
: $"{appSecret.Substring(0, 4)}****";
|
||
_logger.LogInformation("[微信登录] 配置信息: AppId={AppId}, AppSecret={AppSecret}, 来源=数据库",
|
||
maskedAppId, maskedSecret);
|
||
|
||
var url = $"{WechatCodeToSessionUrl}?appid={appId}&secret={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 = "系统错误"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取微信授权的手机号
|
||
/// </summary>
|
||
public async Task<WechatMobileResult> 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 = "系统错误"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取小程序接口调用凭证(access_token)
|
||
/// </summary>
|
||
/// <param name="appId">小程序AppId(可选,不传则使用数据库默认配置)</param>
|
||
/// <returns>access_token,失败返回null</returns>
|
||
public async Task<string?> GetAccessTokenAsync(string? appId = null)
|
||
{
|
||
try
|
||
{
|
||
// 从数据库获取配置
|
||
var wechatConfig = await GetWechatSettingFromDbAsync();
|
||
if (wechatConfig == null)
|
||
{
|
||
_logger.LogError("无法获取access_token:未找到小程序配置,请在后台管理系统中配置 miniprogram_setting");
|
||
return null;
|
||
}
|
||
|
||
// 确定使用哪个AppId和AppSecret
|
||
var targetAppId = appId ?? wechatConfig.AppId;
|
||
var targetAppSecret = await GetAppSecretByAppIdAsync(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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据AppId获取对应的AppSecret(异步版本,从数据库读取)
|
||
/// </summary>
|
||
private async Task<string> GetAppSecretByAppIdAsync(string appId)
|
||
{
|
||
// 从数据库获取配置
|
||
var wechatConfig = await GetWechatSettingFromDbAsync();
|
||
if (wechatConfig != null && wechatConfig.AppId == appId)
|
||
{
|
||
return wechatConfig.AppSecret;
|
||
}
|
||
|
||
// 从小程序配置列表中查找
|
||
var miniprogram = _wechatPaySettings.Miniprograms.FirstOrDefault(m => m.AppId == appId);
|
||
if (miniprogram != null)
|
||
{
|
||
return miniprogram.AppSecret;
|
||
}
|
||
|
||
// 如果都没找到,返回数据库默认配置的AppSecret
|
||
_logger.LogWarning("未找到AppId {AppId} 的配置,使用数据库默认配置", appId);
|
||
return wechatConfig?.AppSecret ?? string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建支付订单(原生微信支付)
|
||
/// </summary>
|
||
public async Task<CreatePayResult> 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 = await GetMerchantConfigAsync();
|
||
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<string, string>
|
||
{
|
||
{ "appid", merchantConfig.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<string, string>
|
||
{
|
||
{ "appId", merchantConfig.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 = merchantConfig.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
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从数据库获取微信小程序配置
|
||
/// 仅从 miniprogram_setting 读取,无配置则返回 null
|
||
/// </summary>
|
||
private async Task<WechatSettings?> GetWechatSettingFromDbAsync()
|
||
{
|
||
try
|
||
{
|
||
// 从 miniprogram_setting 读取小程序配置
|
||
var miniprogramConfig = await _dbContext.Configs
|
||
.Where(c => c.ConfigKey == "miniprogram_setting")
|
||
.Select(c => c.ConfigValue)
|
||
.FirstOrDefaultAsync();
|
||
|
||
if (string.IsNullOrEmpty(miniprogramConfig))
|
||
{
|
||
_logger.LogWarning("[微信配置] 数据库中未找到 miniprogram_setting 配置");
|
||
return null;
|
||
}
|
||
|
||
var config = JsonSerializer.Deserialize<JsonElement>(miniprogramConfig);
|
||
|
||
if (!config.TryGetProperty("miniprograms", out var miniprograms) || miniprograms.ValueKind != JsonValueKind.Array)
|
||
{
|
||
_logger.LogWarning("[微信配置] miniprogram_setting 配置格式错误,缺少 miniprograms 数组");
|
||
return null;
|
||
}
|
||
|
||
// 查找默认小程序配置 (is_default = 1)
|
||
foreach (var mp in miniprograms.EnumerateArray())
|
||
{
|
||
var isDefault = mp.TryGetProperty("is_default", out var isDefaultProp) &&
|
||
(isDefaultProp.ValueKind == JsonValueKind.Number ? isDefaultProp.GetInt32() == 1 : isDefaultProp.GetString() == "1");
|
||
|
||
if (isDefault)
|
||
{
|
||
var appId = mp.TryGetProperty("appid", out var appIdProp) ? appIdProp.GetString() : null;
|
||
var appSecret = mp.TryGetProperty("appsecret", out var appSecretProp) ? appSecretProp.GetString() : null;
|
||
|
||
if (!string.IsNullOrEmpty(appId) && !string.IsNullOrEmpty(appSecret))
|
||
{
|
||
_logger.LogDebug("[微信配置] 从 miniprogram_setting 读取默认小程序配置: AppId={AppId}",
|
||
appId.Length > 8 ? $"{appId.Substring(0, 4)}****{appId.Substring(appId.Length - 4)}" : appId);
|
||
return new WechatSettings
|
||
{
|
||
AppId = appId,
|
||
AppSecret = appSecret
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果没有默认配置,使用第一个小程序配置
|
||
var firstMp = miniprograms.EnumerateArray().FirstOrDefault();
|
||
if (firstMp.ValueKind == JsonValueKind.Object)
|
||
{
|
||
var appId = firstMp.TryGetProperty("appid", out var appIdProp) ? appIdProp.GetString() : null;
|
||
var appSecret = firstMp.TryGetProperty("appsecret", out var appSecretProp) ? appSecretProp.GetString() : null;
|
||
|
||
if (!string.IsNullOrEmpty(appId) && !string.IsNullOrEmpty(appSecret))
|
||
{
|
||
_logger.LogDebug("[微信配置] 从 miniprogram_setting 读取第一个小程序配置: AppId={AppId}",
|
||
appId.Length > 8 ? $"{appId.Substring(0, 4)}****{appId.Substring(appId.Length - 4)}" : appId);
|
||
return new WechatSettings
|
||
{
|
||
AppId = appId,
|
||
AppSecret = appSecret
|
||
};
|
||
}
|
||
}
|
||
|
||
_logger.LogWarning("[微信配置] miniprogram_setting 中未找到有效的小程序配置");
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "[微信配置] 从数据库读取配置失败");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取商户配置(从数据库读取)
|
||
/// </summary>
|
||
private async Task<WechatPayMerchantConfig?> GetMerchantConfigAsync()
|
||
{
|
||
try
|
||
{
|
||
// 从数据库读取 weixinpay 配置
|
||
var weixinpayConfig = await _dbContext.Configs
|
||
.Where(c => c.ConfigKey == "weixinpay")
|
||
.Select(c => c.ConfigValue)
|
||
.FirstOrDefaultAsync();
|
||
|
||
if (string.IsNullOrEmpty(weixinpayConfig))
|
||
{
|
||
_logger.LogError("[微信支付] 数据库中未找到 weixinpay 配置");
|
||
return null;
|
||
}
|
||
|
||
var config = JsonSerializer.Deserialize<JsonElement>(weixinpayConfig);
|
||
|
||
var mchId = config.TryGetProperty("mch_id", out var mchIdProp) ? mchIdProp.GetString() : null;
|
||
var appId = config.TryGetProperty("appid", out var appIdProp) ? appIdProp.GetString() : null;
|
||
var key = config.TryGetProperty("keys", out var keysProp) ? keysProp.GetString() : null;
|
||
|
||
if (string.IsNullOrEmpty(mchId) || string.IsNullOrEmpty(key))
|
||
{
|
||
_logger.LogError("[微信支付] weixinpay 配置不完整,缺少 mch_id 或 keys");
|
||
return null;
|
||
}
|
||
|
||
// 如果 weixinpay 中没有 appid,从 miniprogram_setting 获取
|
||
if (string.IsNullOrEmpty(appId))
|
||
{
|
||
var wechatConfig = await GetWechatSettingFromDbAsync();
|
||
appId = wechatConfig?.AppId;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(appId))
|
||
{
|
||
_logger.LogError("[微信支付] 未找到有效的 AppId 配置");
|
||
return null;
|
||
}
|
||
|
||
_logger.LogInformation("[微信支付] 从数据库读取配置: MchId={MchId}, AppId={AppId}", mchId, appId);
|
||
return new WechatPayMerchantConfig
|
||
{
|
||
Name = "数据库配置",
|
||
MchId = mchId,
|
||
AppId = appId,
|
||
Key = key,
|
||
OrderPrefix = "MYH",
|
||
Weight = 1
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "[微信支付] 从数据库读取配置失败");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <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位) + 时间戳 + 随机数
|
||
/// </summary>
|
||
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}";
|
||
}
|
||
}
|