package payment import ( "context" "errors" "log" "os" "strings" "sync" ) // Config 微信支付渠道配置。来源两级:DB 设置(admin 控制面,热生效)优先,env 兜底 // ——与 TokensPerCredit 的「DB 优先 → env → 默认」同一约定。 // APIv3Key 在库里是 AES-GCM 密文(与模型 API Key 同一把 SUNDYNIX_SECRET_KEY), // 这里拿到的是解密后的明文;PrivateKeyPath 只是服务器磁盘路径,私钥文件本身不进库。 type Config 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"` } // Empty 完全未配置(一个字段都没填)。 func (c Config) Empty() bool { return c.MchID == "" && c.CertSerial == "" && c.PrivateKeyPath == "" && c.APIv3Key == "" && c.AppID == "" && c.NotifyURL == "" } // missing 返回缺失字段名(配置不全时给 admin 一句能看懂的原因)。 func (c Config) missing() []string { var out []string for _, f := range []struct{ k, v string }{ {"mchid", c.MchID}, {"cert_serial", c.CertSerial}, {"private_key_path", c.PrivateKeyPath}, {"apiv3_key", c.APIv3Key}, {"appid", c.AppID}, {"notify_url", c.NotifyURL}, } { if strings.TrimSpace(f.v) == "" { out = append(out, f.k) } } return out } // ConfigFromEnv 从环境变量读配置(DB 未配置时的兜底,兼容 P5.2 的纯 env 用法)。 func ConfigFromEnv() Config { return Config{ MchID: os.Getenv("WECHAT_MCHID"), CertSerial: os.Getenv("WECHAT_MCH_CERT_SERIAL"), PrivateKeyPath: os.Getenv("WECHAT_MCH_PRIVATE_KEY"), APIv3Key: os.Getenv("WECHAT_APIV3_KEY"), AppID: os.Getenv("WECHAT_APPID"), NotifyURL: os.Getenv("WECHAT_NOTIFY_URL"), } } // Manager 持有当前微信渠道实例,支持 admin 改配置后热重载(学 prompt 控制面:改完即生效, // 不重启 gateway)。所有业务路径经 Current() 取用——nil 即渠道隐藏。 type Manager struct { mu sync.RWMutex w *Wechat reason string // 未启用原因(给 admin 状态面看) } func NewManager() *Manager { return &Manager{reason: "未配置"} } // Reload 按给定配置重建渠道实例。失败只降级为隐藏并记录原因,绝不 panic/拖垮服务。 func (m *Manager) Reload(ctx context.Context, c Config) error { w, reason, err := build(ctx, c) m.mu.Lock() m.w = w m.reason = reason m.mu.Unlock() if w != nil { log.Printf("[payment] 微信支付 Native 渠道已启用 (mchid=%s)", c.MchID) } else if !c.Empty() { log.Printf("[payment] 微信支付渠道未启用: %s", reason) } return err } // Current 当前渠道实例;nil = 未配置/配置失败(渠道隐藏)。 func (m *Manager) Current() *Wechat { m.mu.RLock() defer m.mu.RUnlock() return m.w } // Status 给 admin 状态面:是否启用 + 未启用原因。 func (m *Manager) Status() (bool, string) { m.mu.RLock() defer m.mu.RUnlock() return m.w != nil, m.reason } // build 装配渠道实例;返回 (实例, 未启用原因, 错误)。配置为空不算错。 func build(ctx context.Context, c Config) (*Wechat, string, error) { if c.Empty() { return nil, "未配置", nil } if miss := c.missing(); len(miss) > 0 { reason := "配置不全,缺: " + strings.Join(miss, ", ") return nil, reason, errors.New(reason) } w, err := New(ctx, c) if err != nil { return nil, "初始化失败: " + err.Error(), err } return w, "", nil }