mi-assessment/server/MiAssessment/src/MiAssessment.Core/Services/WechatPayService.cs
2026-03-18 00:18:14 +08:00

919 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Xml;
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using MiAssessment.Model.Models.Auth;
using MiAssessment.Model.Models.Payment;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace MiAssessment.Core.Services;
/// <summary>
/// 微信支付服务实现
/// </summary>
public class WechatPayService : IWechatPayService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly HttpClient _httpClient;
private readonly ILogger<WechatPayService> _logger;
private readonly IWechatPayConfigService _configService;
private readonly IWechatService _wechatService;
private readonly IRedisService _redisService;
private readonly WechatPaySettings _settings;
private readonly AppSettings _appSettings;
private readonly Lazy<IWechatPayV3Service>? _v3ServiceLazy;
/// <summary>
/// 微信统一下单API地址
/// </summary>
private const string UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/// <summary>
/// 微信发货通知API地址
/// </summary>
private const string SHIPPING_NOTIFY_URL = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info";
/// <summary>
/// 发货失败订单Redis键前缀
/// </summary>
private const string FAILED_SHIPPING_KEY_PREFIX = "post_order:";
/// <summary>
/// 发货失败订单Redis过期时间3天
/// </summary>
private static readonly TimeSpan FAILED_SHIPPING_EXPIRY = TimeSpan.FromDays(3);
/// <summary>
/// 测试用户支付金额(分)
/// </summary>
private const int TEST_USER_PAY_AMOUNT = 1; // 0.01元 = 1分
public WechatPayService(
MiAssessmentDbContext dbContext,
HttpClient httpClient,
ILogger<WechatPayService> logger,
IWechatPayConfigService configService,
IWechatService wechatService,
IRedisService redisService,
IOptions<WechatPaySettings> settings,
AppSettings appSettings,
Lazy<IWechatPayV3Service>? v3ServiceLazy = null)
{
_dbContext = dbContext;
_httpClient = httpClient;
_logger = logger;
_configService = configService;
_wechatService = wechatService;
_redisService = redisService;
_settings = settings.Value;
_appSettings = appSettings;
_v3ServiceLazy = v3ServiceLazy;
}
/// <inheritdoc />
public async Task<WechatPayResult> CreatePaymentAsync(WechatPayRequest request)
{
try
{
_logger.LogInformation("开始创建微信支付订单: OrderNo={OrderNo}, UserId={UserId}, Amount={Amount}",
request.OrderNo, request.UserId, request.Amount);
// 1. 根据订单号获取商户配置,判断支付版本
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
// 2. 版本路由:如果配置为 V3 且 V3 服务可用,则使用 V3 流程
if (merchantConfig.PayVersion == "V3" && _v3ServiceLazy != null)
{
_logger.LogInformation("商户配置为 V3 版本,路由到 V3 流程: MchId={MchId}", merchantConfig.MchId);
return await _v3ServiceLazy.Value.CreateJsapiOrderAsync(request);
}
// 3. 使用 V2 流程
_logger.LogDebug("使用 V2 支付流程: MchId={MchId}, PayVersion={PayVersion}",
merchantConfig.MchId, merchantConfig.PayVersion);
// 4. 获取用户信息和OpenId
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == request.UserId);
if (user == null)
{
_logger.LogWarning("用户不存在: UserId={UserId}", request.UserId);
return new WechatPayResult
{
Status = 0,
Msg = "用户不存在"
};
}
var openId = string.IsNullOrEmpty(request.OpenId) ? user.OpenId : request.OpenId;
if (string.IsNullOrEmpty(openId))
{
_logger.LogWarning("用户OpenId为空: UserId={UserId}", request.UserId);
return new WechatPayResult
{
Status = 0,
Msg = "用户OpenId不存在"
};
}
// 5. 使用已获取的商户配置
var appId = merchantConfig.AppId;
var mchId = merchantConfig.MchId;
var merchantKey = merchantConfig.Key;
_logger.LogDebug("使用商户配置: MchId={MchId}, AppId={AppId}", mchId, appId);
// 6. 生成随机字符串
var nonceStr = GenerateNonceStr();
var callbackNonceStr = GenerateNonceStr();
// 7. 生成回调通知URL
var notifyUrl = GenerateNotifyUrl(request.Attach, request.UserId, request.OrderNo, callbackNonceStr);
// 验证回调通知URL
if (string.IsNullOrEmpty(notifyUrl))
{
_logger.LogError("支付回调URL未配置NotifyBaseUrl为空请在运营管理-支付配置中设置回调URL");
return new WechatPayResult { Status = 0, Msg = "支付回调URL未配置请联系管理员在后台配置" };
}
// 8. 保存通知记录order_notify
await SaveOrderNotifyAsync(request.OrderNo, notifyUrl, callbackNonceStr, request.Amount, request.Attach, openId);
// 9. 构建统一下单参数
var body = TruncateBody(request.Body, 30);
var totalFee = (int)Math.Round(request.Amount * 100); // 转换为分
// 测试环境下IsTest=2 的用户支付金额为 0.01 元
if (_appSettings.IsTestEnvironment && user.IsTest == 2)
{
_logger.LogInformation("测试用户支付金额调整: UserId={UserId}, 原金额={OriginalAmount}分, 调整为={TestAmount}分",
request.UserId, totalFee, TEST_USER_PAY_AMOUNT);
totalFee = TEST_USER_PAY_AMOUNT;
}
var unifiedOrderParams = new Dictionary<string, string>
{
{ "appid", appId },
{ "mch_id", mchId },
{ "nonce_str", nonceStr },
{ "body", body },
{ "attach", request.Attach },
{ "out_trade_no", request.OrderNo },
{ "notify_url", notifyUrl },
{ "total_fee", totalFee.ToString() },
{ "spbill_create_ip", GetClientIp() },
{ "trade_type", "JSAPI" },
{ "openid", openId }
};
// 10. 生成签名
unifiedOrderParams["sign"] = MakeSign(unifiedOrderParams, merchantKey);
// 11. 转换为XML并调用微信API
var requestXml = DictionaryToXml(unifiedOrderParams);
_logger.LogDebug("统一下单请求XML: {Xml}", requestXml);
var responseXml = await PostXmlAsync(UNIFIED_ORDER_URL, requestXml);
_logger.LogDebug("统一下单响应XML: {Xml}", responseXml);
// 12. 解析响应
var responseData = XmlToDictionary(responseXml);
if (responseData == null)
{
_logger.LogError("解析微信响应失败");
return new WechatPayResult
{
Status = 0,
Msg = "网络故障,请稍后重试(解析响应失败)"
};
}
// 13. 检查返回结果
if (!responseData.TryGetValue("return_code", out var returnCode) || returnCode != "SUCCESS")
{
var returnMsg = responseData.GetValueOrDefault("return_msg", "未知错误");
_logger.LogWarning("统一下单失败: return_code={ReturnCode}, return_msg={ReturnMsg}", returnCode, returnMsg);
return new WechatPayResult
{
Status = 0,
Msg = $"网络故障,请稍后重试({returnMsg})"
};
}
if (!responseData.TryGetValue("result_code", out var resultCode) || resultCode != "SUCCESS")
{
var errCode = responseData.GetValueOrDefault("err_code", "");
var errCodeDes = responseData.GetValueOrDefault("err_code_des", "未知错误");
_logger.LogWarning("统一下单业务失败: err_code={ErrCode}, err_code_des={ErrCodeDes}", errCode, errCodeDes);
return new WechatPayResult
{
Status = 0,
Msg = $"支付失败({GetErrorMessage(errCode, errCodeDes)})"
};
}
// 14. 获取prepay_id
if (!responseData.TryGetValue("prepay_id", out var prepayId) || string.IsNullOrEmpty(prepayId))
{
_logger.LogError("统一下单成功但prepay_id为空");
return new WechatPayResult
{
Status = 0,
Msg = "网络故障,请稍后重试(prepay_id为空)"
};
}
// 15. 构建返回给前端的支付参数
var timeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var payNonceStr = GenerateNonceStr();
var payParams = new Dictionary<string, string>
{
{ "appId", appId },
{ "timeStamp", timeStamp },
{ "nonceStr", payNonceStr },
{ "package", $"prepay_id={prepayId}" },
{ "signType", "MD5" }
};
// 16. 生成支付签名
var paySign = MakeSign(payParams, merchantKey);
_logger.LogInformation("微信支付订单创建成功: OrderNo={OrderNo}, PrepayId={PrepayId}", request.OrderNo, prepayId);
return new WechatPayResult
{
Status = 1,
Msg = "success",
Data = new WechatPayData
{
AppId = appId,
TimeStamp = timeStamp,
NonceStr = payNonceStr,
Package = $"prepay_id={prepayId}",
SignType = "MD5",
PaySign = paySign,
IsWeixin = 1
}
};
}
catch (Exception ex)
{
_logger.LogError(ex, "创建微信支付订单异常: OrderNo={OrderNo}", request.OrderNo);
return new WechatPayResult
{
Status = 0,
Msg = "系统错误,请稍后重试"
};
}
}
/// <summary>
/// 生成回调通知URL
/// </summary>
private string GenerateNotifyUrl(string attach, long userId, string orderNo, string nonceStr)
{
var baseUrl = _settings.NotifyBaseUrl;
if (string.IsNullOrEmpty(baseUrl))
{
return string.Empty;
}
return $"{baseUrl.TrimEnd('/')}/api/notify/{attach}/{userId}/{orderNo}/{nonceStr}";
}
/// <summary>
/// 保存订单通知记录
/// </summary>
private async Task SaveOrderNotifyAsync(string orderNo, string notifyUrl, string nonceStr, decimal amount, string attach, string openId)
{
var orderNotify = new OrderNotify
{
OrderNo = orderNo,
NotifyUrl = notifyUrl,
NonceStr = nonceStr,
PayTime = DateTime.Now,
PayAmount = amount,
Status = 0,
RetryCount = 0,
Attach = attach,
OpenId = openId,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
_dbContext.OrderNotifies.Add(orderNotify);
await _dbContext.SaveChangesAsync();
_logger.LogDebug("保存订单通知记录: OrderNo={OrderNo}, NotifyUrl={NotifyUrl}", orderNo, notifyUrl);
}
/// <summary>
/// 截断商品描述(微信有最大长度限制)
/// </summary>
private static string TruncateBody(string body, int maxLength)
{
if (string.IsNullOrEmpty(body))
{
return "商品购买";
}
if (body.Length <= maxLength)
{
return body;
}
return body.Substring(0, maxLength);
}
/// <summary>
/// 生成32位随机字符串
/// </summary>
private static string GenerateNonceStr(int length = 32)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var result = new char[length];
for (int i = 0; i < length; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
/// <summary>
/// 获取客户端IP
/// </summary>
private static string GetClientIp()
{
// 在实际环境中应该从HttpContext获取
// 这里返回默认值实际使用时需要通过依赖注入获取IHttpContextAccessor
return "127.0.0.1";
}
/// <summary>
/// 将字典转换为XML
/// </summary>
private static string DictionaryToXml(Dictionary<string, string> parameters)
{
var sb = new StringBuilder();
sb.Append("<xml>");
foreach (var kvp in parameters)
{
if (string.IsNullOrEmpty(kvp.Value))
{
continue;
}
// 数字类型不需要CDATA
if (int.TryParse(kvp.Value, out _) || decimal.TryParse(kvp.Value, out _))
{
sb.Append($"<{kvp.Key}>{kvp.Value}</{kvp.Key}>");
}
else
{
sb.Append($"<{kvp.Key}><![CDATA[{kvp.Value}]]></{kvp.Key}>");
}
}
sb.Append("</xml>");
return sb.ToString();
}
/// <summary>
/// 将XML转换为字典
/// </summary>
private static Dictionary<string, string>? XmlToDictionary(string xml)
{
if (string.IsNullOrEmpty(xml))
{
return null;
}
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
var root = doc.DocumentElement;
if (root == null)
{
return null;
}
var result = new Dictionary<string, string>();
foreach (XmlNode node in root.ChildNodes)
{
if (node.NodeType == XmlNodeType.Element)
{
result[node.Name] = node.InnerText;
}
}
return result;
}
catch
{
return null;
}
}
/// <summary>
/// 发送XML POST请求
/// </summary>
private async Task<string> PostXmlAsync(string url, string xml, int timeout = 30)
{
try
{
var content = new StringContent(xml, Encoding.UTF8, "application/xml");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout));
var response = await _httpClient.PostAsync(url, content, cts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "发送XML请求失败: Url={Url}", url);
throw;
}
}
/// <summary>
/// 获取错误信息
/// </summary>
private static string GetErrorMessage(string errCode, string errCodeDes)
{
var errorMessages = new Dictionary<string, string>
{
{ "NOAUTH", "商户未开通此接口权限" },
{ "NOTENOUGH", "用户账号余额不足" },
{ "ORDERNOTEXIST", "订单号不存在" },
{ "ORDERPAID", "商户订单已支付,请勿重复提交" },
{ "ORDERCLOSED", "当前订单已关闭,无法支付" },
{ "SYSTEMERROR", "系统错误!系统超时" },
{ "APPID_NOT_EXIST", "参数中缺少APPID" },
{ "MCHID_NOT_EXIST", "参数中缺少MCHID" },
{ "APPID_MCHID_NOT_MATCH", "appid和mch_id不匹配" },
{ "LACK_PARAMS", "缺少必要的请求参数" },
{ "OUT_TRADE_NO_USED", "同一笔交易不能多次提交" },
{ "SIGNERROR", "参数签名结果不正确" },
{ "XML_FORMAT_ERROR", "XML格式错误" },
{ "REQUIRE_POST_METHOD", "未使用post传递参数" },
{ "POST_DATA_EMPTY", "post数据不能为空" },
{ "NOT_UTF8", "未使用指定编码格式" }
};
if (!string.IsNullOrEmpty(errCode) && errorMessages.TryGetValue(errCode, out var message))
{
return message;
}
return errCodeDes;
}
/// <inheritdoc />
public bool VerifySign(Dictionary<string, string> parameters, string sign, string? merchantKey = null)
{
if (string.IsNullOrEmpty(sign))
{
_logger.LogWarning("签名验证失败:签名为空");
return false;
}
var calculatedSign = MakeSign(parameters, merchantKey);
var isValid = string.Equals(calculatedSign, sign, StringComparison.OrdinalIgnoreCase);
if (!isValid)
{
_logger.LogWarning("签名验证失败:计算签名={CalculatedSign},传入签名={Sign}", calculatedSign, sign);
}
return isValid;
}
/// <inheritdoc />
public string MakeSign(Dictionary<string, string> parameters, string? merchantKey = null)
{
// 获取商户密钥
var key = merchantKey ?? _settings.DefaultMerchant.Key;
// 签名步骤一将字典按参数名ASCII码从小到大排序
// 过滤掉空值和sign字段
var sortedParams = parameters
.Where(p => !string.IsNullOrEmpty(p.Value) && !string.Equals(p.Key, "sign", StringComparison.OrdinalIgnoreCase))
.OrderBy(p => p.Key, StringComparer.Ordinal)
.ToList();
// 签名步骤二将参数拼接为URL格式 key=value&key=value
var urlParams = ToUrlParams(sortedParams);
// 签名步骤三在string后面加上KEY
var signString = $"{urlParams}&key={key}";
// 签名步骤四MD5加密
using var md5 = MD5.Create();
var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(signString));
// 签名步骤五:所有字符转为大写
return BitConverter.ToString(hashBytes).Replace("-", "").ToUpper();
}
/// <summary>
/// 将参数拼接为URL格式: key=value&key=value
/// </summary>
/// <param name="parameters">排序后的参数列表</param>
/// <returns>URL格式字符串</returns>
private static string ToUrlParams(IEnumerable<KeyValuePair<string, string>> parameters)
{
var parts = parameters.Select(p => $"{p.Key}={p.Value}");
return string.Join("&", parts);
}
/// <summary>
/// 根据订单号获取商户密钥
/// </summary>
/// <param name="orderNo">订单号</param>
/// <returns>商户密钥</returns>
public string GetMerchantKeyByOrderNo(string orderNo)
{
var merchant = GetMerchantByOrderNo(orderNo);
return merchant.Key;
}
/// <summary>
/// 验证微信回调签名
/// </summary>
/// <param name="notifyData">回调数据</param>
/// <returns>是否验证通过</returns>
public bool VerifyNotifySign(WechatNotifyData notifyData)
{
// 从回调数据构建参数字典
var parameters = new Dictionary<string, string>
{
{ "return_code", notifyData.ReturnCode },
{ "return_msg", notifyData.ReturnMsg },
{ "result_code", notifyData.ResultCode },
{ "appid", notifyData.AppId },
{ "mch_id", notifyData.MchId },
{ "nonce_str", notifyData.NonceStr },
{ "openid", notifyData.OpenId },
{ "trade_type", notifyData.TradeType },
{ "bank_type", notifyData.BankType },
{ "total_fee", notifyData.TotalFee.ToString() },
{ "fee_type", notifyData.FeeType },
{ "cash_fee", notifyData.CashFee.ToString() },
{ "transaction_id", notifyData.TransactionId },
{ "out_trade_no", notifyData.OutTradeNo },
{ "attach", notifyData.Attach },
{ "time_end", notifyData.TimeEnd }
};
// 添加可选字段
if (!string.IsNullOrEmpty(notifyData.ErrCode))
parameters["err_code"] = notifyData.ErrCode;
if (!string.IsNullOrEmpty(notifyData.ErrCodeDes))
parameters["err_code_des"] = notifyData.ErrCodeDes;
if (!string.IsNullOrEmpty(notifyData.SignType))
parameters["sign_type"] = notifyData.SignType;
// 根据商户号获取对应的密钥
var merchantKey = GetMerchantKeyByMchId(notifyData.MchId);
return VerifySign(parameters, notifyData.Sign, merchantKey);
}
/// <summary>
/// 根据商户号获取商户密钥
/// </summary>
/// <param name="mchId">商户号</param>
/// <returns>商户密钥</returns>
private string GetMerchantKeyByMchId(string mchId)
{
// 先从配置的商户列表中查找
var merchant = _settings.Merchants.FirstOrDefault(m => m.MchId == mchId);
if (merchant != null)
{
return merchant.Key;
}
// 如果没找到,检查默认商户
if (_settings.DefaultMerchant.MchId == mchId)
{
return _settings.DefaultMerchant.Key;
}
// 返回默认商户密钥
_logger.LogWarning("未找到商户号 {MchId} 的配置,使用默认商户密钥", mchId);
return _settings.DefaultMerchant.Key;
}
/// <inheritdoc />
public async Task<OrderShippingNotifyResult> PostOrderShippingAsync(OrderShippingNotifyRequest request)
{
try
{
_logger.LogInformation("开始发送订单发货通知: OrderNo={OrderNo}, OpenId={OpenId}",
request.OrderNo, request.OpenId);
// 1. 根据订单号获取商户配置
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
var mchId = merchantConfig.MchId;
var appId = merchantConfig.AppId;
_logger.LogDebug("使用商户配置: MchId={MchId}, AppId={AppId}", mchId, appId);
// 2. 获取access_token
var accessToken = await _wechatService.GetAccessTokenAsync(appId);
if (string.IsNullOrEmpty(accessToken))
{
_logger.LogError("获取access_token失败: AppId={AppId}", appId);
// 保存到自动重试队列
await SaveFailedShippingOrderAsync(request, merchantConfig, -1, "获取access_token失败");
return new OrderShippingNotifyResult
{
Success = false,
ErrCode = -1,
ErrMsg = "获取access_token失败",
QueuedForRetry = true
};
}
// 3. 构建发货通知消息
var itemDesc = GetShippingItemDesc(request);
// 4. 构建请求参数
var uploadTime = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00";
var requestBody = new
{
order_key = new
{
order_number_type = 1, // 使用商户订单号
mchid = mchId,
out_trade_no = request.OrderNo
},
logistics_type = request.LogisticsType, // 物流类型4=虚拟商品
delivery_mode = request.DeliveryMode, // 发货模式1=统一发货
shipping_list = new[]
{
new
{
item_desc = itemDesc
}
},
upload_time = uploadTime,
payer = new
{
openid = request.OpenId
}
};
var requestJson = JsonSerializer.Serialize(requestBody);
// 5. 记录请求日志
_logger.LogDebug("发货通知请求: OrderNo={OrderNo}, MchId={MchId}, Request={Request}",
request.OrderNo, mchId, requestJson);
// 6. 调用微信API
var requestUrl = $"{SHIPPING_NOTIFY_URL}?access_token={accessToken}";
var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(requestUrl, content);
var responseContent = await response.Content.ReadAsStringAsync();
// 7. 记录响应日志
_logger.LogDebug("发货通知响应: OrderNo={OrderNo}, Response={Response}",
request.OrderNo, responseContent);
// 8. 解析响应
using var jsonDoc = JsonDocument.Parse(responseContent);
var root = jsonDoc.RootElement;
var errCode = root.TryGetProperty("errcode", out var errCodeProp) ? errCodeProp.GetInt32() : -1;
var errMsg = root.TryGetProperty("errmsg", out var errMsgProp) ? errMsgProp.GetString() ?? "unknown" : "unknown";
// 9. 判断结果
if (errCode == 0 && errMsg == "ok")
{
_logger.LogInformation("发货通知成功: OrderNo={OrderNo}", request.OrderNo);
return new OrderShippingNotifyResult
{
Success = true,
ErrCode = 0,
ErrMsg = "ok"
};
}
else
{
_logger.LogWarning("发货通知失败: OrderNo={OrderNo}, ErrCode={ErrCode}, ErrMsg={ErrMsg}",
request.OrderNo, errCode, errMsg);
// 保存到自动重试队列
await SaveFailedShippingOrderAsync(request, merchantConfig, errCode, errMsg);
return new OrderShippingNotifyResult
{
Success = false,
ErrCode = errCode,
ErrMsg = errMsg,
QueuedForRetry = true
};
}
}
catch (Exception ex)
{
_logger.LogError(ex, "发货通知异常: OrderNo={OrderNo}", request.OrderNo);
// 尝试保存到自动重试队列
try
{
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
await SaveFailedShippingOrderAsync(request, merchantConfig, -1, ex.Message);
}
catch (Exception saveEx)
{
_logger.LogError(saveEx, "保存失败订单到自动重试队列时发生错误: OrderNo={OrderNo}", request.OrderNo);
}
return new OrderShippingNotifyResult
{
Success = false,
ErrCode = -1,
ErrMsg = ex.Message,
QueuedForRetry = true
};
}
}
/// <summary>
/// 获取发货商品描述
/// </summary>
private static string GetShippingItemDesc(OrderShippingNotifyRequest request)
{
// 如果请求中指定了描述,直接使用
if (!string.IsNullOrEmpty(request.ItemDesc))
{
return request.ItemDesc;
}
// 根据订单前缀判断消息内容
if (request.OrderNo.StartsWith("FH_"))
{
// 发货订单
return "您购买的实物商品正在处理中,请联系客服获取物流信息";
}
else
{
// 虚拟商品(测评服务等)
return "您购买的测评服务已开通,请在小程序中查看";
}
}
/// <summary>
/// 保存发货失败订单到Redis自动重试队列
/// </summary>
/// <param name="request">发货通知请求</param>
/// <param name="merchantConfig">商户配置</param>
/// <param name="errCode">错误码</param>
/// <param name="errMsg">错误信息</param>
private async Task SaveFailedShippingOrderAsync(
OrderShippingNotifyRequest request,
WechatPayMerchantConfig merchantConfig,
int errCode,
string errMsg)
{
try
{
var itemDesc = GetShippingItemDesc(request);
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var failedOrder = new FailedShippingOrderData
{
OpenId = request.OpenId,
AppId = merchantConfig.AppId,
OrderNo = request.OrderNo,
MchId = merchantConfig.MchId,
ItemDesc = itemDesc,
ErrorCode = errCode,
ErrorMsg = errMsg,
RetryCount = 0,
LastRetryTime = now,
CreateTime = now
};
var redisKey = $"{FAILED_SHIPPING_KEY_PREFIX}{request.OrderNo}";
var json = JsonSerializer.Serialize(failedOrder);
await _redisService.SetStringAsync(redisKey, json, FAILED_SHIPPING_EXPIRY);
_logger.LogInformation("已保存发货失败订单到重试队列: OrderNo={OrderNo}, ErrCode={ErrCode}, ErrMsg={ErrMsg}",
request.OrderNo, errCode, errMsg);
}
catch (Exception ex)
{
_logger.LogError(ex, "保存发货失败订单到Redis异常: OrderNo={OrderNo}", request.OrderNo);
}
}
/// <inheritdoc />
public WechatPayMerchantConfig GetMerchantByOrderNo(string orderNo)
{
return _configService.GetMerchantByOrderNo(orderNo);
}
/// <inheritdoc />
public WechatNotifyData ParseNotifyXml(string xmlData)
{
var notifyData = new WechatNotifyData();
if (string.IsNullOrEmpty(xmlData))
{
_logger.LogWarning("回调XML数据为空");
return notifyData;
}
try
{
var doc = new XmlDocument();
doc.LoadXml(xmlData);
var root = doc.DocumentElement;
if (root == null)
{
_logger.LogWarning("回调XML根节点为空");
return notifyData;
}
notifyData.ReturnCode = GetXmlNodeValue(root, "return_code");
notifyData.ReturnMsg = GetXmlNodeValue(root, "return_msg");
notifyData.ResultCode = GetXmlNodeValue(root, "result_code");
notifyData.ErrCode = GetXmlNodeValue(root, "err_code");
notifyData.ErrCodeDes = GetXmlNodeValue(root, "err_code_des");
notifyData.AppId = GetXmlNodeValue(root, "appid");
notifyData.MchId = GetXmlNodeValue(root, "mch_id");
notifyData.NonceStr = GetXmlNodeValue(root, "nonce_str");
notifyData.Sign = GetXmlNodeValue(root, "sign");
notifyData.SignType = GetXmlNodeValue(root, "sign_type");
notifyData.OpenId = GetXmlNodeValue(root, "openid");
notifyData.TradeType = GetXmlNodeValue(root, "trade_type");
notifyData.BankType = GetXmlNodeValue(root, "bank_type");
notifyData.FeeType = GetXmlNodeValue(root, "fee_type");
notifyData.TransactionId = GetXmlNodeValue(root, "transaction_id");
notifyData.OutTradeNo = GetXmlNodeValue(root, "out_trade_no");
notifyData.Attach = GetXmlNodeValue(root, "attach");
notifyData.TimeEnd = GetXmlNodeValue(root, "time_end");
// 解析金额字段
var totalFeeStr = GetXmlNodeValue(root, "total_fee");
if (int.TryParse(totalFeeStr, out var totalFee))
{
notifyData.TotalFee = totalFee;
}
var cashFeeStr = GetXmlNodeValue(root, "cash_fee");
if (int.TryParse(cashFeeStr, out var cashFee))
{
notifyData.CashFee = cashFee;
}
_logger.LogDebug("解析回调XML成功: OutTradeNo={OutTradeNo}, TransactionId={TransactionId}",
notifyData.OutTradeNo, notifyData.TransactionId);
}
catch (Exception ex)
{
_logger.LogError(ex, "解析回调XML异常");
}
return notifyData;
}
/// <inheritdoc />
public string GenerateNotifyResponseXml(string returnCode, string returnMsg)
{
return $"<xml><return_code><![CDATA[{returnCode}]]></return_code><return_msg><![CDATA[{returnMsg}]]></return_msg></xml>";
}
/// <summary>
/// 获取XML节点值
/// </summary>
/// <param name="root">XML根节点</param>
/// <param name="nodeName">节点名称</param>
/// <returns>节点值,不存在则返回空字符串</returns>
private static string GetXmlNodeValue(XmlElement root, string nodeName)
{
var node = root.SelectSingleNode(nodeName);
return node?.InnerText ?? string.Empty;
}
}