package handler import ( "context" "encoding/json" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/sundynix/sundynix-gateway/internal/payment" "github.com/sundynix/sundynix-shared/secrets" ) // 微信支付配置控制面:配置进 DB(sundynix_setting)、改完热生效不重启; // APIv3 密钥入库前 AES-GCM 加密(与模型 API Key 同一把 SUNDYNIX_SECRET_KEY), // 私钥文件留在服务器磁盘,库里只存路径。env 仍作兜底(DB 优先 → env → 隐藏)。 // SettingWechatPay 是 settings KV 里的键,值为 payment.Config 的 JSON(apiv3_key 为密文)。 const SettingWechatPay = "payment_wechat" // loadWechatConfig 读支付配置:DB 优先(解密 apiv3),空则回退 env。 func (h *Handler) loadWechatConfig(ctx context.Context) payment.Config { raw := h.db.GetSetting(ctx, SettingWechatPay) if raw == "" { return payment.ConfigFromEnv() } var c payment.Config if err := json.Unmarshal([]byte(raw), &c); err != nil { return payment.ConfigFromEnv() } if c.APIv3Key != "" { if plain, err := secrets.Decrypt(c.APIv3Key); err == nil { c.APIv3Key = plain } } return c } // InitWechat 启动时装配微信渠道(DB 优先 → env)。失败只降级隐藏,不阻断启动。 func (h *Handler) InitWechat(ctx context.Context) { _ = h.pay.ReloadWechat(ctx, h.loadWechatConfig(ctx)) } // AdminGetWechatPay: GET /api/v1/admin/payment/wechat —— 当前配置(密钥不回显)+ 渠道状态。 func (h *Handler) AdminGetWechatPay(c *gin.Context) { cfg := h.loadWechatConfig(c.Request.Context()) enabled, reason := h.pay.Status(payment.ChannelWechat) c.JSON(http.StatusOK, gin.H{ "config": gin.H{ "mchid": cfg.MchID, "cert_serial": cfg.CertSerial, "private_key_path": cfg.PrivateKeyPath, "public_key_path": cfg.PublicKeyPath, "public_key_id": cfg.PublicKeyID, "appid": cfg.AppID, "notify_url": cfg.NotifyURL, "has_apiv3_key": cfg.APIv3Key != "", // 密钥只报有无,明文永不回显 }, "enabled": enabled, "reason": reason, }) } // AdminSaveWechatPay: PUT /api/v1/admin/payment/wechat —— 保存配置并热重载渠道。 // apiv3_key 留空 = 沿用已存的(只写不回显的编辑语义,同模型 Key)。 func (h *Handler) AdminSaveWechatPay(c *gin.Context) { var b struct { MchID string `json:"mchid"` CertSerial string `json:"cert_serial"` PrivateKeyPath string `json:"private_key_path"` APIv3Key string `json:"apiv3_key"` AppID string `json:"appid"` NotifyURL string `json:"notify_url"` PublicKeyPath string `json:"public_key_path"` PublicKeyID string `json:"public_key_id"` } if err := c.ShouldBindJSON(&b); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) return } ctx := c.Request.Context() apiv3 := strings.TrimSpace(b.APIv3Key) if apiv3 == "" { apiv3 = h.loadWechatConfig(ctx).APIv3Key // 留空沿用旧密钥(此处为明文,稍后统一加密入库) } cfg := payment.Config{ MchID: strings.TrimSpace(b.MchID), CertSerial: strings.TrimSpace(b.CertSerial), PrivateKeyPath: strings.TrimSpace(b.PrivateKeyPath), APIv3Key: apiv3, AppID: strings.TrimSpace(b.AppID), NotifyURL: strings.TrimSpace(b.NotifyURL), PublicKeyPath: strings.TrimSpace(b.PublicKeyPath), PublicKeyID: strings.TrimSpace(b.PublicKeyID), } stored := cfg if stored.APIv3Key != "" { enc, err := secrets.Encrypt(stored.APIv3Key) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "密钥加密失败: " + err.Error()}) return } stored.APIv3Key = enc } raw, _ := json.Marshal(stored) if err := h.db.SetSetting(ctx, SettingWechatPay, string(raw)); err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } // 热重载:配置有毛病只会让渠道隐藏并给出原因,不影响其它功能。 _ = h.pay.ReloadWechat(ctx, cfg) enabled, reason := h.pay.Status(payment.ChannelWechat) c.JSON(http.StatusOK, gin.H{"status": "ok", "enabled": enabled, "reason": reason}) }