后端: - 新增 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、回调地址 - 菜单归入「审计与设置 → 支付配置」
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
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}})
|
||
}
|