This commit is contained in:
zpc 2026-03-18 00:08:28 +08:00
parent b39b218743
commit b7c5fe1bab
8 changed files with 195 additions and 157 deletions

View File

@ -131,6 +131,12 @@ public class WeixinPayMerchant
/// </summary> /// </summary>
[JsonPropertyName("wechat_public_key_content")] [JsonPropertyName("wechat_public_key_content")]
public string? WechatPublicKeyContent { get; set; } public string? WechatPublicKeyContent { get; set; }
/// <summary>
/// 支付回调通知URL
/// </summary>
[JsonPropertyName("notify_url")]
public string? NotifyUrl { get; set; }
} }

View File

@ -102,4 +102,10 @@ public class WeixinPayMerchant
/// </summary> /// </summary>
[JsonPropertyName("is_enabled")] [JsonPropertyName("is_enabled")]
public string? IsEnabled { get; set; } = "1"; public string? IsEnabled { get; set; } = "1";
/// <summary>
/// 支付回调通知URL
/// </summary>
[JsonPropertyName("notify_url")]
public string? NotifyUrl { get; set; }
} }

View File

@ -139,6 +139,8 @@ export interface WeixinPayMerchant {
cert_path?: string cert_path?: string
/** 是否启用 */ /** 是否启用 */
is_enabled?: string is_enabled?: string
/** 支付回调通知URL */
notify_url?: string
} }
/** /**

View File

@ -76,6 +76,14 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="支付回调URL" required>
<el-input v-model="merchant.notify_url" placeholder="例如https://your-domain.com/api/notify/order_notify" clearable />
<div class="form-item-tip">微信支付结果回调通知地址必须为外网可访问的HTTPS地址</div>
</el-form-item>
</el-col>
</el-row>
<!-- V3 配置 --> <!-- V3 配置 -->
<template v-if="merchant.pay_version === 'V3'"> <template v-if="merchant.pay_version === 'V3'">
@ -235,7 +243,8 @@ function addMerchant() {
wechat_public_key_content: '', wechat_public_key_content: '',
api_key: '', api_key: '',
cert_path: '', cert_path: '',
is_enabled: '1' is_enabled: '1',
notify_url: ''
}) })
} }
@ -269,6 +278,10 @@ async function handleSave() {
ElMessage.warning('请填写所有商户的商户号') ElMessage.warning('请填写所有商户的商户号')
return return
} }
if (!merchant.notify_url?.trim()) {
ElMessage.warning('请填写所有商户的支付回调URL')
return
}
} }
state.saving = true state.saving = true

View File

@ -75,7 +75,7 @@ public class WechatPayConfigService : IWechatPayConfigService
WechatPublicKeyId = m.WechatPublicKeyId, WechatPublicKeyId = m.WechatPublicKeyId,
WechatPublicKeyPath = m.WechatPublicKeyPath, WechatPublicKeyPath = m.WechatPublicKeyPath,
WechatPublicKeyContent = m.WechatPublicKeyContent, WechatPublicKeyContent = m.WechatPublicKeyContent,
NotifyUrl = !string.IsNullOrEmpty(m.NotifyUrl) ? m.NotifyUrl : "https://api.zfunbox.cn/api/notify" NotifyUrl = m.NotifyUrl ?? ""
}); });
} }
} }
@ -99,7 +99,7 @@ public class WechatPayConfigService : IWechatPayConfigService
Key = config.Keys ?? "", Key = config.Keys ?? "",
OrderPrefix = "MYH", OrderPrefix = "MYH",
PayVersion = "V2", PayVersion = "V2",
NotifyUrl = "https://api.zfunbox.cn/api/notify" NotifyUrl = ""
}); });
} }
} }

View File

@ -14,7 +14,7 @@ using Microsoft.Extensions.Options;
namespace MiAssessment.Core.Services; namespace MiAssessment.Core.Services;
/// <summary> /// <summary>
/// 微信支付服务实现 /// ΢<EFBFBD><EFBFBD>֧<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD>
/// </summary> /// </summary>
public class WechatPayService : IWechatPayService public class WechatPayService : IWechatPayService
{ {
@ -29,29 +29,29 @@ public class WechatPayService : IWechatPayService
private readonly Lazy<IWechatPayV3Service>? _v3ServiceLazy; private readonly Lazy<IWechatPayV3Service>? _v3ServiceLazy;
/// <summary> /// <summary>
/// 微信统一下单API地址 /// ΢<EFBFBD><EFBFBD>ͳһ<EFBFBD>µ<EFBFBD>API<EFBFBD><EFBFBD>ַ
/// </summary> /// </summary>
private const string UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"; private const string UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/// <summary> /// <summary>
/// 微信发货通知API地址 /// ΢<EFBFBD>ŷ<EFBFBD><EFBFBD><EFBFBD>֪ͨAPI<EFBFBD><EFBFBD>ַ
/// </summary> /// </summary>
private const string SHIPPING_NOTIFY_URL = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info"; private const string SHIPPING_NOTIFY_URL = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info";
/// <summary> /// <summary>
/// 发货失败订单Redis键前缀 /// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܶ<EFBFBD><EFBFBD><EFBFBD>Redis<EFBFBD><EFBFBD>ǰ׺
/// </summary> /// </summary>
private const string FAILED_SHIPPING_KEY_PREFIX = "post_order:"; private const string FAILED_SHIPPING_KEY_PREFIX = "post_order:";
/// <summary> /// <summary>
/// 发货失败订单Redis过期时间3天 /// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܶ<EFBFBD><EFBFBD><EFBFBD>Redis<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD>䣨3<EFBFBD>
/// </summary> /// </summary>
private static readonly TimeSpan FAILED_SHIPPING_EXPIRY = TimeSpan.FromDays(3); private static readonly TimeSpan FAILED_SHIPPING_EXPIRY = TimeSpan.FromDays(3);
/// <summary> /// <summary>
/// 测试用户支付金额(分) /// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>֧<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֣<EFBFBD>
/// </summary> /// </summary>
private const int TEST_USER_PAY_AMOUNT = 1; // 0.01元 = 1分 private const int TEST_USER_PAY_AMOUNT = 1; // 0.01Ԫ = 1<><31>
public WechatPayService( public WechatPayService(
MiAssessmentDbContext dbContext, MiAssessmentDbContext dbContext,
@ -80,71 +80,78 @@ public class WechatPayService : IWechatPayService
{ {
try try
{ {
_logger.LogInformation("开始创建微信支付订单: OrderNo={OrderNo}, UserId={UserId}, Amount={Amount}", _logger.LogInformation("<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>֧<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: OrderNo={OrderNo}, UserId={UserId}, Amount={Amount}",
request.OrderNo, request.UserId, request.Amount); request.OrderNo, request.UserId, request.Amount);
// 1. 根据订单号获取商户配置,检查支付版本 // 1. <EFBFBD><EFBFBD><EFBFBD>ݶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ż<EFBFBD>ȡ<EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֧<EFBFBD><EFBFBD><EFBFBD>
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo); var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
// 2. 版本路由:如果配置为 V3 且 V3 服务可用,则使用 V3 服务 // 2. <EFBFBD>汾·<EFBFBD>ɣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ V3 <20><> V3 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD><C3A3><EFBFBD>ʹ<EFBFBD><CAB9> V3 <20><><EFBFBD><EFBFBD>
if (merchantConfig.PayVersion == "V3" && _v3ServiceLazy != null) if (merchantConfig.PayVersion == "V3" && _v3ServiceLazy != null)
{ {
_logger.LogInformation("商户配置为 V3 版本,路由到 V3 服务: MchId={MchId}", merchantConfig.MchId); _logger.LogInformation("<EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ V3 <20><EFBFBD><E6B1BE>·<EFBFBD>ɵ<EFBFBD> V3 <20><><EFBFBD><EFBFBD>: MchId={MchId}", merchantConfig.MchId);
return await _v3ServiceLazy.Value.CreateJsapiOrderAsync(request); return await _v3ServiceLazy.Value.CreateJsapiOrderAsync(request);
} }
// 3. 使用 V2 流程 // 3. ʹ<EFBFBD><EFBFBD> V2 <20><><EFBFBD><EFBFBD>
_logger.LogDebug("使用 V2 支付流程: MchId={MchId}, PayVersion={PayVersion}", _logger.LogDebug("ʹ<EFBFBD><EFBFBD> V2 ֧<><D6A7><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: MchId={MchId}, PayVersion={PayVersion}",
merchantConfig.MchId, merchantConfig.PayVersion); merchantConfig.MchId, merchantConfig.PayVersion);
// 4. 获取用户信息和OpenId // 4. <EFBFBD><EFBFBD>ȡ<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>Ϣ<EFBFBD><EFBFBD>OpenId
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == request.UserId); var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == request.UserId);
if (user == null) if (user == null)
{ {
_logger.LogWarning("用户不存在: UserId={UserId}", request.UserId); _logger.LogWarning("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: UserId={UserId}", request.UserId);
return new WechatPayResult return new WechatPayResult
{ {
Status = 0, Status = 0,
Msg = "用户不存在" Msg = "<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
}; };
} }
var openId = string.IsNullOrEmpty(request.OpenId) ? user.OpenId : request.OpenId; var openId = string.IsNullOrEmpty(request.OpenId) ? user.OpenId : request.OpenId;
if (string.IsNullOrEmpty(openId)) if (string.IsNullOrEmpty(openId))
{ {
_logger.LogWarning("用户OpenId为空: UserId={UserId}", request.UserId); _logger.LogWarning("<EFBFBD>û<EFBFBD>OpenIdΪ<EFBFBD><EFBFBD>: UserId={UserId}", request.UserId);
return new WechatPayResult return new WechatPayResult
{ {
Status = 0, Status = 0,
Msg = "用户OpenId不存在" Msg = "<EFBFBD>û<EFBFBD>OpenId<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
}; };
} }
// 5. 使用已获取的商户配置 // 5. ʹ<EFBFBD><EFBFBD><EFBFBD>ѻ<EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var appId = merchantConfig.AppId; var appId = merchantConfig.AppId;
var mchId = merchantConfig.MchId; var mchId = merchantConfig.MchId;
var merchantKey = merchantConfig.Key; var merchantKey = merchantConfig.Key;
_logger.LogDebug("使用商户配置: MchId={MchId}, AppId={AppId}", mchId, appId); _logger.LogDebug("ʹ<EFBFBD><EFBFBD><EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: MchId={MchId}, AppId={AppId}", mchId, appId);
// 3. 生成随机字符串 // 3. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD>
var nonceStr = GenerateNonceStr(); var nonceStr = GenerateNonceStr();
var callbackNonceStr = GenerateNonceStr(); var callbackNonceStr = GenerateNonceStr();
// 4. 生成回调通知URL // 4. <EFBFBD><EFBFBD><EFBFBD>ɻص<EFBFBD>֪ͨURL
var notifyUrl = GenerateNotifyUrl(request.Attach, request.UserId, request.OrderNo, callbackNonceStr); var notifyUrl = GenerateNotifyUrl(request.Attach, request.UserId, request.OrderNo, callbackNonceStr);
// 5. 保存通知记录到order_notify表 // 验证回调通知URL
if (string.IsNullOrEmpty(notifyUrl))
{
_logger.LogError("支付回调URL未配置NotifyBaseUrl为空请在运营管理-支付配置中设置回调URL");
return new WechatPayResult { Status = 0, Msg = "支付回调URL未配置请联系管理员在后台配置" };
}
// 5. <20><><EFBFBD><EFBFBD>֪ͨ<CDA8><D6AA>¼<EFBFBD><C2BC>order_notify<66><79>
await SaveOrderNotifyAsync(request.OrderNo, notifyUrl, callbackNonceStr, request.Amount, request.Attach, openId); await SaveOrderNotifyAsync(request.OrderNo, notifyUrl, callbackNonceStr, request.Amount, request.Attach, openId);
// 6. 构建统一下单参数 // 6. <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͳһ<EFBFBD>µ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var body = TruncateBody(request.Body, 30); var body = TruncateBody(request.Body, 30);
var totalFee = (int)Math.Round(request.Amount * 100); // 转换为分 var totalFee = (int)Math.Round(request.Amount * 100); // ת<EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>
// 测试环境下IsTest=2 的用户支付金额改为 0.01 元 // <EFBFBD><EFBFBD><EFBFBD>Ի<EFBFBD><EFBFBD><EFBFBD><EFBFBD>£<EFBFBD>IsTest=2 <20><><EFBFBD>û<EFBFBD>֧<EFBFBD><D6A7><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ 0.01 Ԫ
if (_appSettings.IsTestEnvironment && user.IsTest == 2) if (_appSettings.IsTestEnvironment && user.IsTest == 2)
{ {
_logger.LogInformation("测试用户支付金额调整: UserId={UserId}, 原金额={OriginalAmount}分, 调整为={TestAmount}分", _logger.LogInformation("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>֧<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: UserId={UserId}, ԭ<><D4AD><EFBFBD>={OriginalAmount}<7D><>, <20><><EFBFBD><EFBFBD>Ϊ={TestAmount}<7D><>",
request.UserId, totalFee, TEST_USER_PAY_AMOUNT); request.UserId, totalFee, TEST_USER_PAY_AMOUNT);
totalFee = TEST_USER_PAY_AMOUNT; totalFee = TEST_USER_PAY_AMOUNT;
} }
@ -164,64 +171,64 @@ public class WechatPayService : IWechatPayService
{ "openid", openId } { "openid", openId }
}; };
// 7. 生成签名 // 7. <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǩ<EFBFBD><EFBFBD>
unifiedOrderParams["sign"] = MakeSign(unifiedOrderParams, merchantKey); unifiedOrderParams["sign"] = MakeSign(unifiedOrderParams, merchantKey);
// 8. 转换为XML并调用微信API // 8. ת<EFBFBD><EFBFBD>ΪXML<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>API
var requestXml = DictionaryToXml(unifiedOrderParams); var requestXml = DictionaryToXml(unifiedOrderParams);
_logger.LogDebug("统一下单请求XML: {Xml}", requestXml); _logger.LogDebug("ͳһ<EFBFBD>µ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>XML: {Xml}", requestXml);
var responseXml = await PostXmlAsync(UNIFIED_ORDER_URL, requestXml); var responseXml = await PostXmlAsync(UNIFIED_ORDER_URL, requestXml);
_logger.LogDebug("统一下单响应XML: {Xml}", responseXml); _logger.LogDebug("ͳһ<EFBFBD>µ<EFBFBD><EFBFBD><EFBFBD>ӦXML: {Xml}", responseXml);
// 9. 解析响应 // 9. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ
var responseData = XmlToDictionary(responseXml); var responseData = XmlToDictionary(responseXml);
if (responseData == null) if (responseData == null)
{ {
_logger.LogError("解析微信响应失败"); _logger.LogError("<EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧʧ<EFBFBD><EFBFBD>");
return new WechatPayResult return new WechatPayResult
{ {
Status = 0, Status = 0,
Msg = "网络故障,请稍后重试(解析响应失败)" Msg = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>(<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧʧ<D3A6><CAA7>)"
}; };
} }
// 10. 检查返回结果 // 10. <EFBFBD><EFBFBD><EFBFBD>ؽ<EFBFBD><EFBFBD>
if (!responseData.TryGetValue("return_code", out var returnCode) || returnCode != "SUCCESS") if (!responseData.TryGetValue("return_code", out var returnCode) || returnCode != "SUCCESS")
{ {
var returnMsg = responseData.GetValueOrDefault("return_msg", "未知错误"); var returnMsg = responseData.GetValueOrDefault("return_msg", "δ֪<EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
_logger.LogWarning("统一下单失败: return_code={ReturnCode}, return_msg={ReturnMsg}", returnCode, returnMsg); _logger.LogWarning("ͳһ<EFBFBD>µ<EFBFBD>ʧ<EFBFBD><EFBFBD>: return_code={ReturnCode}, return_msg={ReturnMsg}", returnCode, returnMsg);
return new WechatPayResult return new WechatPayResult
{ {
Status = 0, Status = 0,
Msg = $"网络故障,请稍后重试({returnMsg})" Msg = $"<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>({returnMsg})"
}; };
} }
if (!responseData.TryGetValue("result_code", out var resultCode) || resultCode != "SUCCESS") if (!responseData.TryGetValue("result_code", out var resultCode) || resultCode != "SUCCESS")
{ {
var errCode = responseData.GetValueOrDefault("err_code", ""); var errCode = responseData.GetValueOrDefault("err_code", "");
var errCodeDes = responseData.GetValueOrDefault("err_code_des", "未知错误"); var errCodeDes = responseData.GetValueOrDefault("err_code_des", "δ֪<EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
_logger.LogWarning("统一下单业务失败: err_code={ErrCode}, err_code_des={ErrCodeDes}", errCode, errCodeDes); _logger.LogWarning("ͳһ<EFBFBD>µ<EFBFBD>ҵ<EFBFBD><EFBFBD>ʧ<EFBFBD><EFBFBD>: err_code={ErrCode}, err_code_des={ErrCodeDes}", errCode, errCodeDes);
return new WechatPayResult return new WechatPayResult
{ {
Status = 0, Status = 0,
Msg = $"支付失败({GetErrorMessage(errCode, errCodeDes)})" Msg = $"֧<EFBFBD><EFBFBD>ʧ<EFBFBD><EFBFBD>({GetErrorMessage(errCode, errCodeDes)})"
}; };
} }
// 11. 获取prepay_id // 11. <EFBFBD><EFBFBD>ȡprepay_id
if (!responseData.TryGetValue("prepay_id", out var prepayId) || string.IsNullOrEmpty(prepayId)) if (!responseData.TryGetValue("prepay_id", out var prepayId) || string.IsNullOrEmpty(prepayId))
{ {
_logger.LogError("统一下单成功但prepay_id为空"); _logger.LogError("ͳһ<EFBFBD>µ<EFBFBD><EFBFBD>ɹ<EFBFBD><EFBFBD><EFBFBD>prepay_idΪ<EFBFBD><EFBFBD>");
return new WechatPayResult return new WechatPayResult
{ {
Status = 0, Status = 0,
Msg = "网络故障,请稍后重试(prepay_id为空)" Msg = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>(prepay_idΪ<64><CEAA>)"
}; };
} }
// 12. 构建返回给前端的支付参数 // 12. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD>ǰ<EFBFBD>˵<EFBFBD>֧<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var timeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); var timeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var payNonceStr = GenerateNonceStr(); var payNonceStr = GenerateNonceStr();
@ -234,10 +241,10 @@ public class WechatPayService : IWechatPayService
{ "signType", "MD5" } { "signType", "MD5" }
}; };
// 13. 生成支付签名 // 13. <EFBFBD><EFBFBD><EFBFBD><EFBFBD>֧<EFBFBD><EFBFBD>ǩ<EFBFBD><EFBFBD>
var paySign = MakeSign(payParams, merchantKey); var paySign = MakeSign(payParams, merchantKey);
_logger.LogInformation("微信支付订单创建成功: OrderNo={OrderNo}, PrepayId={PrepayId}", request.OrderNo, prepayId); _logger.LogInformation("΢<EFBFBD><EFBFBD>֧<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD>: OrderNo={OrderNo}, PrepayId={PrepayId}", request.OrderNo, prepayId);
return new WechatPayResult return new WechatPayResult
{ {
@ -257,34 +264,31 @@ public class WechatPayService : IWechatPayService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "创建微信支付订单异常: OrderNo={OrderNo}", request.OrderNo); _logger.LogError(ex, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>֧<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: OrderNo={OrderNo}", request.OrderNo);
return new WechatPayResult return new WechatPayResult
{ {
Status = 0, Status = 0,
Msg = "系统错误,请稍后重试" Msg = "ϵͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
}; };
} }
} }
/// <summary> /// <summary>
/// 生成回调通知URL /// <EFBFBD><EFBFBD><EFBFBD>ɻص<EFBFBD>֪ͨURL
/// </summary> /// </summary>
private string GenerateNotifyUrl(string attach, long userId, string orderNo, string nonceStr) private string GenerateNotifyUrl(string attach, long userId, string orderNo, string nonceStr)
{ {
// 使用配置的基础URL如果没有配置则使用默认格式
var baseUrl = _settings.NotifyBaseUrl; var baseUrl = _settings.NotifyBaseUrl;
if (string.IsNullOrEmpty(baseUrl)) if (string.IsNullOrEmpty(baseUrl))
{ {
// 默认回调URL格式 return string.Empty;
return $"/api/notify/order_notify";
} }
// 生成带参数的回调URL与PHP保持一致
return $"{baseUrl.TrimEnd('/')}/api/notify/{attach}/{userId}/{orderNo}/{nonceStr}"; return $"{baseUrl.TrimEnd('/')}/api/notify/{attach}/{userId}/{orderNo}/{nonceStr}";
} }
/// <summary> /// <summary>
/// 保存订单通知记录 /// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ<EFBFBD><EFBFBD>¼
/// </summary> /// </summary>
private async Task SaveOrderNotifyAsync(string orderNo, string notifyUrl, string nonceStr, decimal amount, string attach, string openId) private async Task SaveOrderNotifyAsync(string orderNo, string notifyUrl, string nonceStr, decimal amount, string attach, string openId)
{ {
@ -306,20 +310,20 @@ public class WechatPayService : IWechatPayService
_dbContext.OrderNotifies.Add(orderNotify); _dbContext.OrderNotifies.Add(orderNotify);
await _dbContext.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
_logger.LogDebug("保存订单通知记录: OrderNo={OrderNo}, NotifyUrl={NotifyUrl}", orderNo, notifyUrl); _logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ<EFBFBD><EFBFBD>¼: OrderNo={OrderNo}, NotifyUrl={NotifyUrl}", orderNo, notifyUrl);
} }
/// <summary> /// <summary>
/// 截断商品描述(微信限制最大长度) /// <EFBFBD>ض<EFBFBD><EFBFBD><EFBFBD>Ʒ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>󳤶ȣ<EFBFBD>
/// </summary> /// </summary>
private static string TruncateBody(string body, int maxLength) private static string TruncateBody(string body, int maxLength)
{ {
if (string.IsNullOrEmpty(body)) if (string.IsNullOrEmpty(body))
{ {
return "商品购买"; return "<EFBFBD><EFBFBD>Ʒ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>";
} }
// 使用字符数而不是字节数 // ʹ<EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֽ<EFBFBD><EFBFBD><EFBFBD>
if (body.Length <= maxLength) if (body.Length <= maxLength)
{ {
return body; return body;
@ -329,7 +333,7 @@ public class WechatPayService : IWechatPayService
} }
/// <summary> /// <summary>
/// 生成32位随机字符串 /// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>32λ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD>
/// </summary> /// </summary>
private static string GenerateNonceStr(int length = 32) private static string GenerateNonceStr(int length = 32)
{ {
@ -346,17 +350,17 @@ public class WechatPayService : IWechatPayService
} }
/// <summary> /// <summary>
/// 获取客户端IP /// <EFBFBD><EFBFBD>ȡ<EFBFBD>ͻ<EFBFBD><EFBFBD><EFBFBD>IP
/// </summary> /// </summary>
private static string GetClientIp() private static string GetClientIp()
{ {
// 在实际环境中应该从HttpContext获取 // <EFBFBD><EFBFBD>ʵ<EFBFBD>ʻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ<EFBFBD>ô<EFBFBD>HttpContext<EFBFBD><EFBFBD>ȡ
// 这里返回默认值,实际使用时需要通过依赖注入获取IHttpContextAccessor // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD>ֵ<EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>Ҫͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD><EFBFBD>ȡIHttpContextAccessor
return "127.0.0.1"; return "127.0.0.1";
} }
/// <summary> /// <summary>
/// 将字典转换为XML /// <EFBFBD><EFBFBD><EFBFBD>ֵ<EFBFBD>ת<EFBFBD><EFBFBD>ΪXML
/// </summary> /// </summary>
private static string DictionaryToXml(Dictionary<string, string> parameters) private static string DictionaryToXml(Dictionary<string, string> parameters)
{ {
@ -370,7 +374,7 @@ public class WechatPayService : IWechatPayService
continue; continue;
} }
// 数字类型不需要CDATA // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ͳ<EFBFBD><EFBFBD><EFBFBD>ҪCDATA
if (int.TryParse(kvp.Value, out _) || decimal.TryParse(kvp.Value, out _)) if (int.TryParse(kvp.Value, out _) || decimal.TryParse(kvp.Value, out _))
{ {
sb.Append($"<{kvp.Key}>{kvp.Value}</{kvp.Key}>"); sb.Append($"<{kvp.Key}>{kvp.Value}</{kvp.Key}>");
@ -386,7 +390,7 @@ public class WechatPayService : IWechatPayService
} }
/// <summary> /// <summary>
/// 将XML转换为字典 /// <EFBFBD><EFBFBD>XMLת<EFBFBD><EFBFBD>Ϊ<EFBFBD>ֵ<EFBFBD>
/// </summary> /// </summary>
private static Dictionary<string, string>? XmlToDictionary(string xml) private static Dictionary<string, string>? XmlToDictionary(string xml)
{ {
@ -424,7 +428,7 @@ public class WechatPayService : IWechatPayService
} }
/// <summary> /// <summary>
/// 发送XML POST请求 /// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>XML POST<53><54><EFBFBD><EFBFBD>
/// </summary> /// </summary>
private async Task<string> PostXmlAsync(string url, string xml, int timeout = 30) private async Task<string> PostXmlAsync(string url, string xml, int timeout = 30)
{ {
@ -440,34 +444,34 @@ public class WechatPayService : IWechatPayService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "发送XML请求失败: Url={Url}", url); _logger.LogError(ex, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>XML<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><EFBFBD>: Url={Url}", url);
throw; throw;
} }
} }
/// <summary> /// <summary>
/// 获取错误消息 /// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
/// </summary> /// </summary>
private static string GetErrorMessage(string errCode, string errCodeDes) private static string GetErrorMessage(string errCode, string errCodeDes)
{ {
var errorMessages = new Dictionary<string, string> var errorMessages = new Dictionary<string, string>
{ {
{ "NOAUTH", "商户未开通此接口权限" }, { "NOAUTH", "<EFBFBD>̻<EFBFBD>δ<EFBFBD><EFBFBD>ͨ<EFBFBD>˽ӿ<EFBFBD>Ȩ<EFBFBD><EFBFBD>" },
{ "NOTENOUGH", "用户帐号余额不足" }, { "NOTENOUGH", "<EFBFBD>û<EFBFBD><EFBFBD>ʺ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" },
{ "ORDERNOTEXIST", "订单号不存在" }, { "ORDERNOTEXIST", "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ų<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" },
{ "ORDERPAID", "商户订单已支付,无需重复操作" }, { "ORDERPAID", "<EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֧<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" },
{ "ORDERCLOSED", "当前订单已关闭,无法支付" }, { "ORDERCLOSED", "<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹرգ<EFBFBD><EFBFBD>޷<EFBFBD>֧<EFBFBD><EFBFBD>" },
{ "SYSTEMERROR", "系统错误!系统超时" }, { "SYSTEMERROR", "ϵͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>!ϵͳ<CFB5><CDB3>ʱ" },
{ "APPID_NOT_EXIST", "参数中缺少APPID" }, { "APPID_NOT_EXIST", "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȱ<EFBFBD><EFBFBD>APPID" },
{ "MCHID_NOT_EXIST", "参数中缺少MCHID" }, { "MCHID_NOT_EXIST", "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȱ<EFBFBD><EFBFBD>MCHID" },
{ "APPID_MCHID_NOT_MATCH", "appid和mch_id不匹配" }, { "APPID_MCHID_NOT_MATCH", "appid<EFBFBD><EFBFBD>mch_id<EFBFBD><EFBFBD>ƥ<EFBFBD><EFBFBD>" },
{ "LACK_PARAMS", "缺少必要的请求参数" }, { "LACK_PARAMS", "ȱ<EFBFBD>ٱ<EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" },
{ "OUT_TRADE_NO_USED", "同一笔交易不能多次提交" }, { "OUT_TRADE_NO_USED", "ͬһ<EFBFBD>ʽ<EFBFBD><EFBFBD>ײ<EFBFBD><EFBFBD>ܶ<EFBFBD><EFBFBD><EFBFBD>" },
{ "SIGNERROR", "参数签名结果不正确" }, { "SIGNERROR", "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǩ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȷ" },
{ "XML_FORMAT_ERROR", "XML格式错误" }, { "XML_FORMAT_ERROR", "XML<EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>" },
{ "REQUIRE_POST_METHOD", "未使用post传递参数" }, { "REQUIRE_POST_METHOD", "δʹ<EFBFBD><EFBFBD>post<EFBFBD><EFBFBD><EFBFBD>ݲ<EFBFBD><EFBFBD><EFBFBD>" },
{ "POST_DATA_EMPTY", "post数据不能为空" }, { "POST_DATA_EMPTY", "post<EFBFBD><EFBFBD><EFBFBD>ݲ<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>" },
{ "NOT_UTF8", "未使用指定编码格式" } { "NOT_UTF8", "δʹ<EFBFBD><EFBFBD>ָ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ" }
}; };
if (!string.IsNullOrEmpty(errCode) && errorMessages.TryGetValue(errCode, out var message)) if (!string.IsNullOrEmpty(errCode) && errorMessages.TryGetValue(errCode, out var message))
@ -483,7 +487,7 @@ public class WechatPayService : IWechatPayService
{ {
if (string.IsNullOrEmpty(sign)) if (string.IsNullOrEmpty(sign))
{ {
_logger.LogWarning("签名验证失败:签名为空"); _logger.LogWarning("ǩ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤ʧ<EFBFBD>ܣ<EFBFBD>ǩ<EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>");
return false; return false;
} }
@ -492,7 +496,7 @@ public class WechatPayService : IWechatPayService
if (!isValid) if (!isValid)
{ {
_logger.LogWarning("签名验证失败:计算签名={CalculatedSign},传入签名={Sign}", calculatedSign, sign); _logger.LogWarning("ǩ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤ʧ<EFBFBD>ܣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǩ<EFBFBD><EFBFBD>={CalculatedSign}<7D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǩ<EFBFBD><C7A9>={Sign}", calculatedSign, sign);
} }
return isValid; return isValid;
@ -501,35 +505,35 @@ public class WechatPayService : IWechatPayService
/// <inheritdoc /> /// <inheritdoc />
public string MakeSign(Dictionary<string, string> parameters, string? merchantKey = null) public string MakeSign(Dictionary<string, string> parameters, string? merchantKey = null)
{ {
// 获取商户密钥 // <EFBFBD><EFBFBD>ȡ<EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD>Կ
var key = merchantKey ?? _settings.DefaultMerchant.Key; var key = merchantKey ?? _settings.DefaultMerchant.Key;
// 签名步骤一按字典序排序数组参数ASCII码从小到大 // ǩ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ASCII<EFBFBD><EFBFBD><EFBFBD>С<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// 过滤掉空值和sign字段 // <EFBFBD><EFBFBD><EFBFBD>˵<EFBFBD><EFBFBD><EFBFBD>ֵ<EFBFBD><EFBFBD>sign<EFBFBD>ֶ<EFBFBD>
var sortedParams = parameters var sortedParams = parameters
.Where(p => !string.IsNullOrEmpty(p.Value) && !string.Equals(p.Key, "sign", StringComparison.OrdinalIgnoreCase)) .Where(p => !string.IsNullOrEmpty(p.Value) && !string.Equals(p.Key, "sign", StringComparison.OrdinalIgnoreCase))
.OrderBy(p => p.Key, StringComparer.Ordinal) .OrderBy(p => p.Key, StringComparer.Ordinal)
.ToList(); .ToList();
// 签名步骤二将参数拼接为URL格式 key=value&key=value // ǩ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƴ<EFBFBD><EFBFBD>ΪURL<EFBFBD><EFBFBD>ʽ key=value&key=value
var urlParams = ToUrlParams(sortedParams); var urlParams = ToUrlParams(sortedParams);
// 签名步骤三在string后加入KEY // ǩ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>string<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>KEY
var signString = $"{urlParams}&key={key}"; var signString = $"{urlParams}&key={key}";
// 签名步骤四MD5加密 // ǩ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģ<EFBFBD>MD5<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
using var md5 = MD5.Create(); using var md5 = MD5.Create();
var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(signString)); var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(signString));
// 签名步骤五:所有字符转为大写 // ǩ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD>תΪ<EFBFBD><EFBFBD>д
return BitConverter.ToString(hashBytes).Replace("-", "").ToUpper(); return BitConverter.ToString(hashBytes).Replace("-", "").ToUpper();
} }
/// <summary> /// <summary>
/// 将参数拼接为URL格式: key=value&key=value /// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƴ<EFBFBD><EFBFBD>ΪURL<EFBFBD><EFBFBD>ʽ: key=value&key=value
/// </summary> /// </summary>
/// <param name="parameters">已排序的参数列表</param> /// <param name="parameters"><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>IJ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD></param>
/// <returns>URL格式字符串</returns> /// <returns>URL<EFBFBD><EFBFBD>ʽ<EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD></returns>
private static string ToUrlParams(IEnumerable<KeyValuePair<string, string>> parameters) private static string ToUrlParams(IEnumerable<KeyValuePair<string, string>> parameters)
{ {
var parts = parameters.Select(p => $"{p.Key}={p.Value}"); var parts = parameters.Select(p => $"{p.Key}={p.Value}");
@ -537,10 +541,10 @@ public class WechatPayService : IWechatPayService
} }
/// <summary> /// <summary>
/// 根据订单号获取商户密钥 /// <EFBFBD><EFBFBD><EFBFBD>ݶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ż<EFBFBD>ȡ<EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD>Կ
/// </summary> /// </summary>
/// <param name="orderNo">订单号</param> /// <param name="orderNo"><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></param>
/// <returns>商户密钥</returns> /// <returns><EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD>Կ</returns>
public string GetMerchantKeyByOrderNo(string orderNo) public string GetMerchantKeyByOrderNo(string orderNo)
{ {
var merchant = GetMerchantByOrderNo(orderNo); var merchant = GetMerchantByOrderNo(orderNo);
@ -548,13 +552,13 @@ public class WechatPayService : IWechatPayService
} }
/// <summary> /// <summary>
/// 验证微信回调签名 /// <EFBFBD><EFBFBD>֤΢<EFBFBD>Żص<EFBFBD>ǩ<EFBFBD><EFBFBD>
/// </summary> /// </summary>
/// <param name="notifyData">回调数据</param> /// <param name="notifyData"><EFBFBD>ص<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></param>
/// <returns>是否验证通过</returns> /// <returns><EFBFBD>Ƿ<EFBFBD><EFBFBD><EFBFBD>֤ͨ<EFBFBD><EFBFBD></returns>
public bool VerifyNotifySign(WechatNotifyData notifyData) public bool VerifyNotifySign(WechatNotifyData notifyData)
{ {
// 从回调数据构建参数字典 // <EFBFBD>ӻص<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ<EFBFBD>
var parameters = new Dictionary<string, string> var parameters = new Dictionary<string, string>
{ {
{ "return_code", notifyData.ReturnCode }, { "return_code", notifyData.ReturnCode },
@ -575,7 +579,7 @@ public class WechatPayService : IWechatPayService
{ "time_end", notifyData.TimeEnd } { "time_end", notifyData.TimeEnd }
}; };
// 添加可选字段 // <EFBFBD><EFBFBD><EFBFBD>ӿ<EFBFBD>ѡ<EFBFBD>ֶ<EFBFBD>
if (!string.IsNullOrEmpty(notifyData.ErrCode)) if (!string.IsNullOrEmpty(notifyData.ErrCode))
parameters["err_code"] = notifyData.ErrCode; parameters["err_code"] = notifyData.ErrCode;
if (!string.IsNullOrEmpty(notifyData.ErrCodeDes)) if (!string.IsNullOrEmpty(notifyData.ErrCodeDes))
@ -583,34 +587,34 @@ public class WechatPayService : IWechatPayService
if (!string.IsNullOrEmpty(notifyData.SignType)) if (!string.IsNullOrEmpty(notifyData.SignType))
parameters["sign_type"] = notifyData.SignType; parameters["sign_type"] = notifyData.SignType;
// 根据商户号获取对应的密钥 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̻<EFBFBD><EFBFBD>Ż<EFBFBD>ȡ<EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Կ
var merchantKey = GetMerchantKeyByMchId(notifyData.MchId); var merchantKey = GetMerchantKeyByMchId(notifyData.MchId);
return VerifySign(parameters, notifyData.Sign, merchantKey); return VerifySign(parameters, notifyData.Sign, merchantKey);
} }
/// <summary> /// <summary>
/// 根据商户号获取商户密钥 /// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̻<EFBFBD><EFBFBD>Ż<EFBFBD>ȡ<EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD>Կ
/// </summary> /// </summary>
/// <param name="mchId">商户号</param> /// <param name="mchId"><EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD></param>
/// <returns>商户密钥</returns> /// <returns><EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD>Կ</returns>
private string GetMerchantKeyByMchId(string mchId) private string GetMerchantKeyByMchId(string mchId)
{ {
// 先从配置的商户列表中查找 // <EFBFBD>ȴ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>õ<EFBFBD><EFBFBD>̻<EFBFBD><EFBFBD>б<EFBFBD><EFBFBD>в<EFBFBD><EFBFBD><EFBFBD>
var merchant = _settings.Merchants.FirstOrDefault(m => m.MchId == mchId); var merchant = _settings.Merchants.FirstOrDefault(m => m.MchId == mchId);
if (merchant != null) if (merchant != null)
{ {
return merchant.Key; return merchant.Key;
} }
// 如果没找到,检查默认商户 // <EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>ҵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD><EFBFBD>̻<EFBFBD>
if (_settings.DefaultMerchant.MchId == mchId) if (_settings.DefaultMerchant.MchId == mchId)
{ {
return _settings.DefaultMerchant.Key; return _settings.DefaultMerchant.Key;
} }
// 返回默认商户密钥 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD><EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD>Կ
_logger.LogWarning("未找到商户号 {MchId} 的配置,使用默认商户密钥", mchId); _logger.LogWarning("δ<EFBFBD>ҵ<EFBFBD><EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD> {MchId} <20><><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD>ʹ<EFBFBD><CAB9>Ĭ<EFBFBD><C4AC><EFBFBD>̻<EFBFBD><CCBB><EFBFBD>Կ", mchId);
return _settings.DefaultMerchant.Key; return _settings.DefaultMerchant.Key;
} }
@ -619,49 +623,49 @@ public class WechatPayService : IWechatPayService
{ {
try try
{ {
_logger.LogInformation("开始发送订单发货通知: OrderNo={OrderNo}, OpenId={OpenId}", _logger.LogInformation("<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD>Ͷ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ: OrderNo={OrderNo}, OpenId={OpenId}",
request.OrderNo, request.OpenId); request.OrderNo, request.OpenId);
// 1. 根据订单号获取商户配置 // 1. <EFBFBD><EFBFBD><EFBFBD>ݶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ż<EFBFBD>ȡ<EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo); var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
var mchId = merchantConfig.MchId; var mchId = merchantConfig.MchId;
var appId = merchantConfig.AppId; var appId = merchantConfig.AppId;
_logger.LogDebug("使用商户配置: MchId={MchId}, AppId={AppId}", mchId, appId); _logger.LogDebug("ʹ<EFBFBD><EFBFBD><EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: MchId={MchId}, AppId={AppId}", mchId, appId);
// 2. 获取access_token // 2. <EFBFBD><EFBFBD>ȡaccess_token
var accessToken = await _wechatService.GetAccessTokenAsync(appId); var accessToken = await _wechatService.GetAccessTokenAsync(appId);
if (string.IsNullOrEmpty(accessToken)) if (string.IsNullOrEmpty(accessToken))
{ {
_logger.LogError("获取access_token失败: AppId={AppId}", appId); _logger.LogError("<EFBFBD><EFBFBD>ȡaccess_tokenʧ<EFBFBD><EFBFBD>: AppId={AppId}", appId);
// 存入重试队列 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><EFBFBD><EFBFBD>
await SaveFailedShippingOrderAsync(request, merchantConfig, -1, "获取access_token失败"); await SaveFailedShippingOrderAsync(request, merchantConfig, -1, "<EFBFBD><EFBFBD>ȡaccess_tokenʧ<EFBFBD><EFBFBD>");
return new OrderShippingNotifyResult return new OrderShippingNotifyResult
{ {
Success = false, Success = false,
ErrCode = -1, ErrCode = -1,
ErrMsg = "获取access_token失败", ErrMsg = "<EFBFBD><EFBFBD>ȡaccess_tokenʧ<EFBFBD><EFBFBD>",
QueuedForRetry = true QueuedForRetry = true
}; };
} }
// 3. 构建发货通知消息 // 3. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ<EFBFBD><EFBFBD>Ϣ
var itemDesc = GetShippingItemDesc(request); var itemDesc = GetShippingItemDesc(request);
// 4. 构建请求参数 // 4. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var uploadTime = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00"; var uploadTime = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00";
var requestBody = new var requestBody = new
{ {
order_key = new order_key = new
{ {
order_number_type = 1, // 使用商户订单号 order_number_type = 1, // ʹ<EFBFBD><EFBFBD><EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
mchid = mchId, mchid = mchId,
out_trade_no = request.OrderNo out_trade_no = request.OrderNo
}, },
logistics_type = request.LogisticsType, // 物流类型4=虚拟商品 logistics_type = request.LogisticsType, // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͣ<EFBFBD>4=<3D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ
delivery_mode = request.DeliveryMode, // 发货模式1=统一发货 delivery_mode = request.DeliveryMode, // <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģʽ<EFBFBD><EFBFBD>1=ͳһ<CDB3><D2BB><EFBFBD><EFBFBD>
shipping_list = new[] shipping_list = new[]
{ {
new new
@ -678,32 +682,32 @@ public class WechatPayService : IWechatPayService
var requestJson = JsonSerializer.Serialize(requestBody); var requestJson = JsonSerializer.Serialize(requestBody);
// 5. 记录请求日志 // 5. <EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־
_logger.LogDebug("发货通知请求: OrderNo={OrderNo}, MchId={MchId}, Request={Request}", _logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: OrderNo={OrderNo}, MchId={MchId}, Request={Request}",
request.OrderNo, mchId, requestJson); request.OrderNo, mchId, requestJson);
// 6. 调用微信API // 6. <EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>API
var requestUrl = $"{SHIPPING_NOTIFY_URL}?access_token={accessToken}"; var requestUrl = $"{SHIPPING_NOTIFY_URL}?access_token={accessToken}";
var content = new StringContent(requestJson, Encoding.UTF8, "application/json"); var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(requestUrl, content); var response = await _httpClient.PostAsync(requestUrl, content);
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
// 7. 记录响应日志 // 7. <EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD>־
_logger.LogDebug("发货通知响应: OrderNo={OrderNo}, Response={Response}", _logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ<EFBFBD><EFBFBD>Ӧ: OrderNo={OrderNo}, Response={Response}",
request.OrderNo, responseContent); request.OrderNo, responseContent);
// 8. 解析响应 // 8. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ
using var jsonDoc = JsonDocument.Parse(responseContent); using var jsonDoc = JsonDocument.Parse(responseContent);
var root = jsonDoc.RootElement; var root = jsonDoc.RootElement;
var errCode = root.TryGetProperty("errcode", out var errCodeProp) ? errCodeProp.GetInt32() : -1; var errCode = root.TryGetProperty("errcode", out var errCodeProp) ? errCodeProp.GetInt32() : -1;
var errMsg = root.TryGetProperty("errmsg", out var errMsgProp) ? errMsgProp.GetString() ?? "unknown" : "unknown"; var errMsg = root.TryGetProperty("errmsg", out var errMsgProp) ? errMsgProp.GetString() ?? "unknown" : "unknown";
// 9. 判断结果 // 9. <EFBFBD>жϽ<EFBFBD><EFBFBD>
if (errCode == 0 && errMsg == "ok") if (errCode == 0 && errMsg == "ok")
{ {
_logger.LogInformation("发货通知成功: OrderNo={OrderNo}", request.OrderNo); _logger.LogInformation("<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ<EFBFBD>ɹ<EFBFBD>: OrderNo={OrderNo}", request.OrderNo);
return new OrderShippingNotifyResult return new OrderShippingNotifyResult
{ {
Success = true, Success = true,
@ -713,10 +717,10 @@ public class WechatPayService : IWechatPayService
} }
else else
{ {
_logger.LogWarning("发货通知失败: OrderNo={OrderNo}, ErrCode={ErrCode}, ErrMsg={ErrMsg}", _logger.LogWarning("<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨʧ<EFBFBD><EFBFBD>: OrderNo={OrderNo}, ErrCode={ErrCode}, ErrMsg={ErrMsg}",
request.OrderNo, errCode, errMsg); request.OrderNo, errCode, errMsg);
// 存入重试队列 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><EFBFBD><EFBFBD>
await SaveFailedShippingOrderAsync(request, merchantConfig, errCode, errMsg); await SaveFailedShippingOrderAsync(request, merchantConfig, errCode, errMsg);
return new OrderShippingNotifyResult return new OrderShippingNotifyResult
@ -730,9 +734,9 @@ public class WechatPayService : IWechatPayService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "发货通知异常: OrderNo={OrderNo}", request.OrderNo); _logger.LogError(ex, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ<EFBFBD>: OrderNo={OrderNo}", request.OrderNo);
// 尝试存入重试队列 // <EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><EFBFBD><EFBFBD>
try try
{ {
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo); var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
@ -740,7 +744,7 @@ public class WechatPayService : IWechatPayService
} }
catch (Exception saveEx) catch (Exception saveEx)
{ {
_logger.LogError(saveEx, "保存失败订单到重试队列时发生错误: OrderNo={OrderNo}", request.OrderNo); _logger.LogError(saveEx, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: OrderNo={OrderNo}", request.OrderNo);
} }
return new OrderShippingNotifyResult return new OrderShippingNotifyResult
@ -754,31 +758,31 @@ public class WechatPayService : IWechatPayService
} }
/// <summary> /// <summary>
/// 获取发货商品描述 /// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary> /// </summary>
private static string GetShippingItemDesc(OrderShippingNotifyRequest request) private static string GetShippingItemDesc(OrderShippingNotifyRequest request)
{ {
// 如果请求中指定了描述,直接使用 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ָ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֱ<EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD>
if (!string.IsNullOrEmpty(request.ItemDesc)) if (!string.IsNullOrEmpty(request.ItemDesc))
{ {
return request.ItemDesc; return request.ItemDesc;
} }
// 根据订单前缀判断消息内容 // <EFBFBD><EFBFBD><EFBFBD>ݶ<EFBFBD><EFBFBD><EFBFBD>ǰ׺<EFBFBD>ж<EFBFBD><EFBFBD><EFBFBD>Ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
if (request.OrderNo.StartsWith("FH_")) if (request.OrderNo.StartsWith("FH_"))
{ {
// 发货订单 // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
return "本单购买的商品正在打包,请联系客服获取物流信息"; return "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ<EFBFBD><EFBFBD><EFBFBD>ڴ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ<EFBFBD>ͷ<EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ";
} }
else else
{ {
// 虚拟商品(测评服务等) // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȣ<EFBFBD>
return "本单购买的测评服务已开通,请在小程序中查看"; return "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>IJ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѿ<EFBFBD>ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>С<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>в鿴";
} }
} }
/// <summary> /// <summary>
/// 保存发货失败的订单到Redis重试队列 /// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܵĶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Redis<EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><EFBFBD><EFBFBD>
/// </summary> /// </summary>
private async Task SaveFailedShippingOrderAsync( private async Task SaveFailedShippingOrderAsync(
OrderShippingNotifyRequest request, OrderShippingNotifyRequest request,
@ -808,12 +812,12 @@ public class WechatPayService : IWechatPayService
var json = JsonSerializer.Serialize(failedOrder); var json = JsonSerializer.Serialize(failedOrder);
await _redisService.SetStringAsync(key, json, FAILED_SHIPPING_EXPIRY); await _redisService.SetStringAsync(key, json, FAILED_SHIPPING_EXPIRY);
_logger.LogInformation("已将发货失败订单存入重试队列: OrderNo={OrderNo}, Key={Key}", _logger.LogInformation("<EFBFBD>ѽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><EFBFBD><EFBFBD>: OrderNo={OrderNo}, Key={Key}",
request.OrderNo, key); request.OrderNo, key);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "保存发货失败订单到Redis时发生错误: OrderNo={OrderNo}", request.OrderNo); _logger.LogError(ex, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Redisʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: OrderNo={OrderNo}", request.OrderNo);
throw; throw;
} }
} }
@ -821,7 +825,7 @@ public class WechatPayService : IWechatPayService
/// <inheritdoc /> /// <inheritdoc />
public WechatPayMerchantConfig GetMerchantByOrderNo(string orderNo) public WechatPayMerchantConfig GetMerchantByOrderNo(string orderNo)
{ {
// 使用配置服务获取商户配置 // ʹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>÷<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD>̻<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
return _configService.GetMerchantByOrderNo(orderNo); return _configService.GetMerchantByOrderNo(orderNo);
} }
@ -857,7 +861,7 @@ public class WechatPayService : IWechatPayService
result.Attach = GetXmlNodeValue(root, "attach"); result.Attach = GetXmlNodeValue(root, "attach");
result.TimeEnd = GetXmlNodeValue(root, "time_end"); result.TimeEnd = GetXmlNodeValue(root, "time_end");
// 解析金额(单位:分) // <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>λ<EFBFBD><EFBFBD><EFBFBD>֣<EFBFBD>
if (int.TryParse(GetXmlNodeValue(root, "total_fee"), out var totalFee)) if (int.TryParse(GetXmlNodeValue(root, "total_fee"), out var totalFee))
{ {
result.TotalFee = totalFee; result.TotalFee = totalFee;
@ -869,7 +873,7 @@ public class WechatPayService : IWechatPayService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "解析微信回调XML失败: {XmlData}", xmlData); _logger.LogError(ex, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD>Żص<EFBFBD>XMLʧ<EFBFBD><EFBFBD>: {XmlData}", xmlData);
} }
return result; return result;

View File

@ -96,6 +96,13 @@ public class WechatPayV3Service : IWechatPayV3Service
return new WechatPayResult { Status = 0, Msg = "V3 支付配置不完整" }; return new WechatPayResult { Status = 0, Msg = "V3 支付配置不完整" };
} }
// 验证回调通知URL
if (string.IsNullOrEmpty(merchantConfig.NotifyUrl))
{
_logger.LogError("支付回调URL未配置: MchId={MchId},请在运营管理-支付配置中设置回调URL", merchantConfig.MchId);
return new WechatPayResult { Status = 0, Msg = "支付回调URL未配置请联系管理员在后台配置" };
}
_logger.LogDebug("使用 V3 商户配置: MchId={MchId}, AppId={AppId}", merchantConfig.MchId, merchantConfig.AppId); _logger.LogDebug("使用 V3 商户配置: MchId={MchId}, AppId={AppId}", merchantConfig.MchId, merchantConfig.AppId);
// 3. 读取私钥优先使用数据库中的PEM内容 // 3. 读取私钥优先使用数据库中的PEM内容

View File

@ -5,8 +5,8 @@
const ENV = { const ENV = {
development: { development: {
API_BASE_URL: 'http://api.nxt.shhmkjgs.cn/api', API_BASE_URL: 'https://api.nxt.shhmkjgs.cn/api',
STATIC_BASE_URL: 'http://api.nxt.shhmkjgs.cn', STATIC_BASE_URL: 'https://api.nxt.shhmkjgs.cn',
SIGNALR_URL: 'ws://api.nxt.shhmkjgs.cn/hubs/chat' SIGNALR_URL: 'ws://api.nxt.shhmkjgs.cn/hubs/chat'
}, },
production: { production: {