后端: - 新增 payment_configs 单行配置表;APIv3 密钥、应用私钥/公钥以 AES-256-GCM 密文入库(enc:v1 前缀,主密钥取 PAYMENT_SECRET_KEY,缺省回退 JWT_SECRET 派生) - 证书/私钥落 PAYMENT_CERT_DIR(默认 /data/payment-certs),目录 0700 / 文件 0600,不经任何静态路由暴露 - 接口:GET/PUT /api/admin/payment/config、POST /upload(PEM 校验 + 微信证书序列号自动回填)、POST /test(真打官方接口:微信 /v3/certificates 签名校验、支付宝 alipay.trade.query 签名校验)、POST /wechat/platform-certs(APIv3 密钥解密平台证书并落盘) - 未配置凭据时测试返回 skipped 而不是 500;敏感字段在返回体里一律脱敏 前端: - /payments 改为「通用设置 + 微信支付 / 支付宝 / Waffo 海外支付」页签(原 Waffo 页整页迁到 views/payments/Waffo.vue) - 微信:mchid、AppID、APIv3 密钥(密码框 + 掩码回显 + 更换/清除)、证书序列号、支付方式多选、三项证书上传、自动获取平台证书 - 支付宝:app_id、应用私钥(粘贴/上传)、支付宝公钥、网关、RSA2、回调地址 - 菜单归入「审计与设置 → 支付配置」
102 lines
3.5 KiB
Go
102 lines
3.5 KiB
Go
package payment
|
||
|
||
import (
|
||
"backend/internal/dto"
|
||
"errors"
|
||
"strings"
|
||
)
|
||
|
||
// 证书 / 私钥上传处理:校验 PEM 可解析 → 0600 落地到仓库外目录 → 更新配置行。 // Logan.Li
|
||
|
||
// maxUploadBytes 单个上传文件大小上限(PEM 文本,64KB 足够) // Logan.Li
|
||
const maxUploadBytes = 64 * 1024
|
||
|
||
// Upload 处理一次证书/密钥上传 // Logan.Li
|
||
func (s *ConfigService) Upload(kind, filename string, content []byte) (*dto.PaymentUploadResponse, error) {
|
||
if !IsValidUploadKind(kind) {
|
||
return nil, errors.New("不支持的上传类型(可选:wechat_cert / wechat_key / wechat_platform_cert / alipay_private_key / alipay_public_key)")
|
||
}
|
||
if len(content) == 0 {
|
||
return nil, errors.New("上传文件为空")
|
||
}
|
||
if len(content) > maxUploadBytes {
|
||
return nil, errors.New("文件过大(上限 64KB)")
|
||
}
|
||
|
||
row, err := s.loadRow()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var certSerial, note string
|
||
// 1) 内容校验(解析失败直接拒绝入库) // Logan.Li
|
||
switch kind {
|
||
case UploadKindWechatCert, UploadKindWechatPlatformCert:
|
||
cert, perr := ParseCertificatePEM(content)
|
||
if perr != nil {
|
||
return nil, errors.New("证书文件校验失败:" + perr.Error())
|
||
}
|
||
certSerial = CertSerialHex(cert)
|
||
case UploadKindWechatKey:
|
||
normalized := NormalizePEM(string(content), "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----")
|
||
if _, perr := ParseRSAPrivateKey([]byte(normalized)); perr != nil {
|
||
return nil, errors.New("商户 API 私钥校验失败:" + perr.Error())
|
||
}
|
||
content = []byte(normalized)
|
||
case UploadKindAlipayPrivateKey:
|
||
normalized := NormalizePEM(string(content), "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----")
|
||
if _, perr := ParseRSAPrivateKey([]byte(normalized)); perr != nil {
|
||
return nil, errors.New("应用私钥校验失败:" + perr.Error())
|
||
}
|
||
content = []byte(normalized)
|
||
case UploadKindAlipayPublicKey:
|
||
normalized := NormalizePEM(string(content), "-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----")
|
||
if _, perr := ParseRSAPublicKey([]byte(normalized)); perr != nil {
|
||
return nil, errors.New("支付宝公钥校验失败:" + perr.Error())
|
||
}
|
||
content = []byte(normalized)
|
||
}
|
||
|
||
// 2) 落地文件(0600,仓库外目录) // Logan.Li
|
||
path, err := WriteSecretFile(kind, content)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 3) 更新配置行指向新文件 // Logan.Li
|
||
switch kind {
|
||
case UploadKindWechatCert:
|
||
row.WechatCertPath = path
|
||
if strings.TrimSpace(row.WechatSerialNo) == "" && certSerial != "" {
|
||
row.WechatSerialNo = certSerial
|
||
note = "已自动回填证书序列号"
|
||
} else if certSerial != "" && !strings.EqualFold(strings.TrimSpace(row.WechatSerialNo), certSerial) {
|
||
note = "注意:填写的序列号与证书实际序列号不一致(证书:" + certSerial + "),请核对"
|
||
}
|
||
case UploadKindWechatKey:
|
||
row.WechatKeyPath = path
|
||
case UploadKindWechatPlatformCert:
|
||
row.WechatPlatformCertPath = path
|
||
case UploadKindAlipayPrivateKey:
|
||
row.AlipayPrivateKeyPath = path
|
||
row.AlipayPrivateKeyEnc = "" // 上传后清掉旧的粘贴值,避免来源歧义 // Logan.Li
|
||
case UploadKindAlipayPublicKey:
|
||
row.AlipayPublicKeyPath = path
|
||
row.AlipayPublicKeyEnc = ""
|
||
}
|
||
|
||
if err := s.db.Save(row).Error; err != nil {
|
||
return nil, errors.New("保存上传记录失败")
|
||
}
|
||
|
||
msg := "上传成功:" + filename
|
||
if note != "" {
|
||
msg += "(" + note + ")"
|
||
}
|
||
return &dto.PaymentUploadResponse{
|
||
Kind: kind,
|
||
File: BuildFileInfo(path, IsCertKind(kind)),
|
||
Message: msg,
|
||
}, nil
|
||
}
|