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、回调地址
- 菜单归入「审计与设置 → 支付配置」
This commit is contained in:
Logan.Li committed 2026-11-01 05:59:51 +08:00
1 parent 630f2819cb
commit 7d53c6ed3c
21 files changed
+3090 -6

No files matched your search

@@ -0,0 +1,105 @@
package payment
import (
"backend/config"
"backend/internal/dto"
paymentservice "backend/internal/service/payment"
"io"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// PaymentConfigController 支付配置管理控制器(仅管理员,挂在 JWT 保护组下) // Logan.Li
type PaymentConfigController struct {
svc *paymentservice.ConfigService
}
// NewPaymentConfigController 创建控制器 // Logan.Li
func NewPaymentConfigController(db *gorm.DB, cfg *config.Config) *PaymentConfigController {
return &PaymentConfigController{svc: paymentservice.NewConfigService(db, cfg)}
}
// GetConfig 获取支付配置(敏感字段脱敏) // Logan.Li
func (ctrl *PaymentConfigController) GetConfig(c *gin.Context) {
data, err := ctrl.svc.Get()
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": data})
}
// UpdateConfig 保存支付配置 // Logan.Li
func (ctrl *PaymentConfigController) UpdateConfig(c *gin.Context) {
var req dto.PaymentConfigUpdate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误:" + err.Error()})
return
}
data, err := ctrl.svc.Update(req)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "支付配置已保存", "data": data})
}
// Upload 上传证书/私钥(multipart:kind + file) // Logan.Li
func (ctrl *PaymentConfigController) Upload(c *gin.Context) {
kind := strings.TrimSpace(c.PostForm("kind"))
if kind == "" {
kind = strings.TrimSpace(c.Query("kind"))
}
fileHeader, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "未收到文件(字段名应为 file)"})
return
}
f, err := fileHeader.Open()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "读取上传文件失败"})
return
}
defer f.Close()
content, err := io.ReadAll(io.LimitReader(f, 256*1024))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "读取上传文件失败"})
return
}
data, err := ctrl.svc.Upload(kind, fileHeader.Filename, content)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": data.Message, "data": data})
}
// Test 用已保存凭据做一次最小连通性校验 // Logan.Li
func (ctrl *PaymentConfigController) Test(c *gin.Context) {
var req dto.PaymentTestRequest
if err := c.ShouldBindJSON(&req); err != nil {
// 允许空 body,默认测试全部渠道 // Logan.Li
req = dto.PaymentTestRequest{}
}
data := ctrl.svc.Test(req.Channel)
// 业务失败也返回 200 + code=0,结果在 results[].status 中体现,避免前端弹 500 // Logan.Li
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "测试完成", "data": data})
}
// FetchWechatPlatformCerts 自动获取微信支付平台证书 // Logan.Li
func (ctrl *PaymentConfigController) FetchWechatPlatformCerts(c *gin.Context) {
items, msg, err := ctrl.svc.FetchWechatPlatformCerts()
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
if items == nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": msg, "data": gin.H{"items": []any{}}})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": msg, "data": gin.H{"items": items}})
}