Files
sundynix-agentix/sundynix-gateway/internal/handler/payment_admin.go
T
Blizzard 3a4e1d53a5 refactor(payment): 渠道抽象成 Channel 接口 —— 接新渠道=加 adapter 不动骨架
PAYMENT_DESIGN §3 承诺的 internal/payment/channel.go 适配器接口此前不存在,微信硬编码在
manager/handler 里。补齐抽象:

- channel.go:Channel 接口(Name/CreatePay/QueryOrder/VerifyCallback)+ 统一 PayIntent/PayResult
  + 渠道名常量。入参用基本类型不吃 *store.PaymentOrder,payment 包不反依赖 store。
- Wechat 实现 Channel(编译期断言 var _ Channel);QueryResult 归一为 PayResult;CreatePay 返回 PayIntent。
- Manager 从「持一个 *Wechat」改为渠道注册表:Get(name)/Available()/Status(name)/ReloadWechat;
  按渠道名持有已装配实例,热重载不变。
- 回调路由收敛 /billing/callback/wechat → /billing/callback/:channel 按名路由(旧 notify URL 仍匹配);
  查单/掉单补偿据 order.Channel 路由,不再写死微信。下单支持可选 channel(缺省 wechat)。
- 支付宝/Stripe 现在真·只差一个 adapter+注册。唯一未泛化:回调 ack 应答格式(现微信态,注释标明)。
- payment 包首个测试:Manager 注册表 4 用例(空/注册摘除/空配置/配置不全)。build/vet/test/lint 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:56:24 +08:00

115 lines
4.1 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 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"
)
// 微信支付配置控制面:配置进 DBsundynix_setting)、改完热生效不重启;
// APIv3 密钥入库前 AES-GCM 加密(与模型 API Key 同一把 SUNDYNIX_SECRET_KEY),
// 私钥文件留在服务器磁盘,库里只存路径。env 仍作兜底(DB 优先 → env → 隐藏)。
// SettingWechatPay 是 settings KV 里的键,值为 payment.Config 的 JSONapiv3_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})
}