Files
opsx-licence/backend/internal/service/payment/config_upload.go
T
Logan.Li 7d53c6ed3c feat(payment): 后台「支付配置」支持微信支付与支付宝凭据管理
后端:
- 新增 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、回调地址
- 菜单归入「审计与设置 → 支付配置」
2026-11-01 05:59:51 +08:00

102 lines
3.5 KiB
Go
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.
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
}