Files
opsx-licence/backend/internal/controller/payment/config_controller.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

106 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/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}})
}