feat(payment): 支持微信支付V3证书PEM内容存储到数据库
- WechatPayMerchantConfig 新增 PrivateKeyContent/WechatPublicKeyContent 字段 - WechatPayV3Service 新增 ResolvePrivateKey/ResolvePublicKey 优先读数据库内容 - 后台管理页面改为文本域粘贴PEM内容,路径作为备选 - 完全向后兼容,原文件路径方式依然可用 - 迁移服务器只需在后台重新配置即可,无需拷贝证书文件
This commit is contained in:
parent
f082f20fc8
commit
1bd6683cb8
|
|
@ -108,6 +108,12 @@ public class WeixinPayMerchant
|
|||
[JsonPropertyName("private_key_path")]
|
||||
public string? PrivateKeyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户私钥PEM内容(V3版本使用,优先级高于路径)
|
||||
/// </summary>
|
||||
[JsonPropertyName("private_key_content")]
|
||||
public string? PrivateKeyContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付公钥ID(V3版本使用)
|
||||
/// </summary>
|
||||
|
|
@ -119,6 +125,12 @@ public class WeixinPayMerchant
|
|||
/// </summary>
|
||||
[JsonPropertyName("wechat_public_key_path")]
|
||||
public string? WechatPublicKeyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付公钥PEM内容(V3版本使用,优先级高于路径)
|
||||
/// </summary>
|
||||
[JsonPropertyName("wechat_public_key_content")]
|
||||
public string? WechatPublicKeyContent { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@ public class WeixinPayMerchant
|
|||
[JsonPropertyName("private_key_path")]
|
||||
public string? PrivateKeyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户私钥PEM内容(优先级高于路径)
|
||||
/// </summary>
|
||||
[JsonPropertyName("private_key_content")]
|
||||
public string? PrivateKeyContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付公钥ID
|
||||
/// </summary>
|
||||
|
|
@ -73,6 +79,12 @@ public class WeixinPayMerchant
|
|||
[JsonPropertyName("wechat_public_key_path")]
|
||||
public string? WechatPublicKeyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付公钥PEM内容(优先级高于路径)
|
||||
/// </summary>
|
||||
[JsonPropertyName("wechat_public_key_content")]
|
||||
public string? WechatPublicKeyContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// API密钥(V2版本使用)
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -125,10 +125,14 @@ export interface WeixinPayMerchant {
|
|||
cert_serial_no?: string
|
||||
/** 商户私钥文件路径 */
|
||||
private_key_path?: string
|
||||
/** 商户私钥PEM内容(优先级高于路径) */
|
||||
private_key_content?: string
|
||||
/** 微信支付公钥ID */
|
||||
wechat_public_key_id?: string
|
||||
/** 微信支付公钥文件路径 */
|
||||
wechat_public_key_path?: string
|
||||
/** 微信支付公钥PEM内容(优先级高于路径) */
|
||||
wechat_public_key_content?: string
|
||||
/** API密钥(V2) */
|
||||
api_key?: string
|
||||
/** 证书路径(V2) */
|
||||
|
|
|
|||
|
|
@ -99,19 +99,43 @@
|
|||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商户私钥路径">
|
||||
<el-input v-model="merchant.private_key_path" placeholder="apiclient_key.pem 文件路径" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="微信支付公钥ID">
|
||||
<el-input v-model="merchant.wechat_public_key_id" placeholder="微信支付公钥ID" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">证书配置(二选一:粘贴内容 或 填写服务器路径)</el-divider>
|
||||
|
||||
<!-- 商户私钥 -->
|
||||
<el-form-item label="商户私钥内容">
|
||||
<el-input
|
||||
v-model="merchant.private_key_content"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="粘贴 apiclient_key.pem 文件内容(-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----)"
|
||||
/>
|
||||
<div class="form-item-tip">推荐方式:直接粘贴PEM文件内容,迁移服务器无需重新上传证书文件</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="商户私钥路径">
|
||||
<el-input v-model="merchant.private_key_path" placeholder="备选:apiclient_key.pem 服务器文件路径" clearable />
|
||||
<div class="form-item-tip">仅当未填写私钥内容时使用</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 微信支付公钥 -->
|
||||
<el-form-item label="微信支付公钥内容">
|
||||
<el-input
|
||||
v-model="merchant.wechat_public_key_content"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="粘贴 pub_key.pem 文件内容(-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----)"
|
||||
/>
|
||||
<div class="form-item-tip">推荐方式:直接粘贴PEM文件内容</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="微信支付公钥路径">
|
||||
<el-input v-model="merchant.wechat_public_key_path" placeholder="pub_key.pem 文件路径" clearable />
|
||||
<el-input v-model="merchant.wechat_public_key_path" placeholder="备选:pub_key.pem 服务器文件路径" clearable />
|
||||
<div class="form-item-tip">仅当未填写公钥内容时使用</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
|
|
@ -205,8 +229,10 @@ function addMerchant() {
|
|||
api_v3_key: '',
|
||||
cert_serial_no: '',
|
||||
private_key_path: '',
|
||||
private_key_content: '',
|
||||
wechat_public_key_id: '',
|
||||
wechat_public_key_path: '',
|
||||
wechat_public_key_content: '',
|
||||
api_key: '',
|
||||
cert_path: '',
|
||||
is_enabled: '1'
|
||||
|
|
|
|||
|
|
@ -71,8 +71,10 @@ public class WechatPayConfigService : IWechatPayConfigService
|
|||
ApiV3Key = m.ApiV3Key,
|
||||
CertSerialNo = m.CertSerialNo,
|
||||
PrivateKeyPath = m.PrivateKeyPath,
|
||||
PrivateKeyContent = m.PrivateKeyContent,
|
||||
WechatPublicKeyId = m.WechatPublicKeyId,
|
||||
WechatPublicKeyPath = m.WechatPublicKeyPath,
|
||||
WechatPublicKeyContent = m.WechatPublicKeyContent,
|
||||
NotifyUrl = !string.IsNullOrEmpty(m.NotifyUrl) ? m.NotifyUrl : "https://api.zfunbox.cn/api/notify"
|
||||
});
|
||||
}
|
||||
|
|
@ -215,8 +217,10 @@ public class WechatPayConfigService : IWechatPayConfigService
|
|||
ApiV3Key = selectedMerchant.ApiV3Key,
|
||||
CertSerialNo = selectedMerchant.CertSerialNo,
|
||||
PrivateKeyPath = selectedMerchant.PrivateKeyPath,
|
||||
PrivateKeyContent = selectedMerchant.PrivateKeyContent,
|
||||
WechatPublicKeyId = selectedMerchant.WechatPublicKeyId,
|
||||
WechatPublicKeyPath = selectedMerchant.WechatPublicKeyPath
|
||||
WechatPublicKeyPath = selectedMerchant.WechatPublicKeyPath,
|
||||
WechatPublicKeyContent = selectedMerchant.WechatPublicKeyContent
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -304,8 +308,10 @@ public class WechatPayConfigService : IWechatPayConfigService
|
|||
[JsonPropertyName("api_v3_key")] public string? ApiV3Key { get; set; }
|
||||
[JsonPropertyName("cert_serial_no")] public string? CertSerialNo { get; set; }
|
||||
[JsonPropertyName("private_key_path")] public string? PrivateKeyPath { get; set; }
|
||||
[JsonPropertyName("private_key_content")] public string? PrivateKeyContent { get; set; }
|
||||
[JsonPropertyName("wechat_public_key_id")] public string? WechatPublicKeyId { get; set; }
|
||||
[JsonPropertyName("wechat_public_key_path")] public string? WechatPublicKeyPath { get; set; }
|
||||
[JsonPropertyName("wechat_public_key_content")] public string? WechatPublicKeyContent { get; set; }
|
||||
[JsonPropertyName("notify_url")] public string? NotifyUrl { get; set; }
|
||||
}
|
||||
private class DbWeixinPayConfig
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public class WechatPayV3Service : IWechatPayV3Service
|
|||
// 验证 V3 配置
|
||||
if (string.IsNullOrEmpty(merchantConfig.ApiV3Key) ||
|
||||
string.IsNullOrEmpty(merchantConfig.CertSerialNo) ||
|
||||
string.IsNullOrEmpty(merchantConfig.PrivateKeyPath))
|
||||
(string.IsNullOrEmpty(merchantConfig.PrivateKeyContent) && string.IsNullOrEmpty(merchantConfig.PrivateKeyPath)))
|
||||
{
|
||||
_logger.LogError("V3 配置不完整: MchId={MchId}", merchantConfig.MchId);
|
||||
return new WechatPayResult { Status = 0, Msg = "V3 支付配置不完整" };
|
||||
|
|
@ -98,11 +98,11 @@ public class WechatPayV3Service : IWechatPayV3Service
|
|||
|
||||
_logger.LogDebug("使用 V3 商户配置: MchId={MchId}, AppId={AppId}", merchantConfig.MchId, merchantConfig.AppId);
|
||||
|
||||
// 3. 读取私钥
|
||||
var privateKey = ReadPrivateKey(merchantConfig.PrivateKeyPath);
|
||||
// 3. 读取私钥(优先使用数据库中的PEM内容)
|
||||
var privateKey = ResolvePrivateKey(merchantConfig);
|
||||
if (string.IsNullOrEmpty(privateKey))
|
||||
{
|
||||
_logger.LogError("读取私钥失败: Path={Path}", merchantConfig.PrivateKeyPath);
|
||||
_logger.LogError("读取私钥失败: MchId={MchId}", merchantConfig.MchId);
|
||||
return new WechatPayResult { Status = 0, Msg = "读取商户私钥失败" };
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ public class WechatPayV3Service : IWechatPayV3Service
|
|||
_logger.LogInformation("开始查询 V3 订单: OrderNo={OrderNo}", orderNo);
|
||||
|
||||
var merchantConfig = _configService.GetMerchantByOrderNo(orderNo);
|
||||
var privateKey = ReadPrivateKey(merchantConfig.PrivateKeyPath!);
|
||||
var privateKey = ResolvePrivateKey(merchantConfig);
|
||||
|
||||
var url = $"/v3/pay/transactions/out-trade-no/{orderNo}?mchid={merchantConfig.MchId}";
|
||||
var fullUrl = string.Format(V3_QUERY_URL, orderNo) + $"?mchid={merchantConfig.MchId}";
|
||||
|
|
@ -274,7 +274,7 @@ public class WechatPayV3Service : IWechatPayV3Service
|
|||
_logger.LogInformation("开始关闭 V3 订单: OrderNo={OrderNo}", orderNo);
|
||||
|
||||
var merchantConfig = _configService.GetMerchantByOrderNo(orderNo);
|
||||
var privateKey = ReadPrivateKey(merchantConfig.PrivateKeyPath!);
|
||||
var privateKey = ResolvePrivateKey(merchantConfig);
|
||||
|
||||
var url = $"/v3/pay/transactions/out-trade-no/{orderNo}/close";
|
||||
var fullUrl = string.Format(V3_CLOSE_URL, orderNo);
|
||||
|
|
@ -337,7 +337,7 @@ public class WechatPayV3Service : IWechatPayV3Service
|
|||
request.OrderNo, request.RefundNo, request.RefundAmount);
|
||||
|
||||
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
|
||||
var privateKey = ReadPrivateKey(merchantConfig.PrivateKeyPath!);
|
||||
var privateKey = ResolvePrivateKey(merchantConfig);
|
||||
|
||||
var apiRequest = new WechatPayV3RefundApiRequest
|
||||
{
|
||||
|
|
@ -451,9 +451,9 @@ public class WechatPayV3Service : IWechatPayV3Service
|
|||
// 获取商户配置
|
||||
var merchantConfig = _configService.GetDefaultConfig();
|
||||
|
||||
if (string.IsNullOrEmpty(merchantConfig.WechatPublicKeyPath))
|
||||
if (string.IsNullOrEmpty(merchantConfig.WechatPublicKeyContent) && string.IsNullOrEmpty(merchantConfig.WechatPublicKeyPath))
|
||||
{
|
||||
_logger.LogError("微信支付公钥路径未配置");
|
||||
_logger.LogError("微信支付公钥未配置(内容和路径均为空)");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -467,10 +467,10 @@ public class WechatPayV3Service : IWechatPayV3Service
|
|||
// 继续验证,因为可能是微信更换了公钥
|
||||
}
|
||||
|
||||
var publicKey = ReadPublicKey(merchantConfig.WechatPublicKeyPath);
|
||||
var publicKey = ResolvePublicKey(merchantConfig);
|
||||
if (string.IsNullOrEmpty(publicKey))
|
||||
{
|
||||
_logger.LogError("读取微信支付公钥失败: Path={Path}", merchantConfig.WechatPublicKeyPath);
|
||||
_logger.LogError("读取微信支付公钥失败: MchId={MchId}", merchantConfig.MchId);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -853,6 +853,54 @@ public class WechatPayV3Service : IWechatPayV3Service
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析商户私钥:优先使用数据库中的PEM内容,fallback到文件路径
|
||||
/// </summary>
|
||||
/// <param name="config">商户配置</param>
|
||||
/// <returns>私钥PEM内容</returns>
|
||||
private string ResolvePrivateKey(WechatPayMerchantConfig config)
|
||||
{
|
||||
// 优先使用数据库中存储的PEM内容
|
||||
if (!string.IsNullOrEmpty(config.PrivateKeyContent))
|
||||
{
|
||||
_logger.LogDebug("使用数据库中的商户私钥内容");
|
||||
return config.PrivateKeyContent;
|
||||
}
|
||||
|
||||
// fallback到文件路径
|
||||
if (!string.IsNullOrEmpty(config.PrivateKeyPath))
|
||||
{
|
||||
return ReadPrivateKey(config.PrivateKeyPath);
|
||||
}
|
||||
|
||||
_logger.LogError("商户私钥未配置(内容和路径均为空): MchId={MchId}", config.MchId);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析微信支付公钥:优先使用数据库中的PEM内容,fallback到文件路径
|
||||
/// </summary>
|
||||
/// <param name="config">商户配置</param>
|
||||
/// <returns>公钥PEM内容</returns>
|
||||
private string ResolvePublicKey(WechatPayMerchantConfig config)
|
||||
{
|
||||
// 优先使用数据库中存储的PEM内容
|
||||
if (!string.IsNullOrEmpty(config.WechatPublicKeyContent))
|
||||
{
|
||||
_logger.LogDebug("使用数据库中的微信支付公钥内容");
|
||||
return config.WechatPublicKeyContent;
|
||||
}
|
||||
|
||||
// fallback到文件路径
|
||||
if (!string.IsNullOrEmpty(config.WechatPublicKeyPath))
|
||||
{
|
||||
return ReadPublicKey(config.WechatPublicKeyPath);
|
||||
}
|
||||
|
||||
_logger.LogError("微信支付公钥未配置(内容和路径均为空): MchId={MchId}", config.MchId);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 截断商品描述(V3 限制最大 127 字符)
|
||||
/// </summary>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user