package payment import ( "context" "errors" "log" "os" "sort" "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"` // 微信支付公钥验签体系(2024 起新注册商户只有这个;本项目商户 2025-09 开户,钉死此模式, // 老商户的平台证书模式不做)。 PublicKeyPath string `json:"public_key_path"` // 微信支付公钥文件路径(服务器磁盘) PublicKeyID string `json:"public_key_id"` // 公钥 ID(PUB_KEY_ID_ 开头) } // Empty 完全未配置(一个字段都没填)。 func (c Config) Empty() bool { return c.MchID == "" && c.CertSerial == "" && c.PrivateKeyPath == "" && c.APIv3Key == "" && c.AppID == "" && c.NotifyURL == "" && c.PublicKeyPath == "" && c.PublicKeyID == "" } // missing 返回缺失字段名(配置不全时给 admin 一句能看懂的原因)。八项全必填: // APIv3 密钥用于回调资源解密,公钥两项用于验签。 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}, {"public_key_path", c.PublicKeyPath}, {"public_key_id", c.PublicKeyID}, } { 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"), PublicKeyPath: os.Getenv("WECHAT_PUBLIC_KEY"), PublicKeyID: os.Getenv("WECHAT_PUBLIC_KEY_ID"), } } // Manager 是充值渠道注册表:按渠道名持有已装配的 Channel 实例,支持 admin 改配置后热重载 // (学 prompt 控制面:改完即生效,不重启 gateway)。业务路径经 Get(name) 按订单渠道取用—— // 取到 nil 即该渠道未启用/隐藏。接新渠道(支付宝/Stripe)= 加一个 ReloadXxx + 各自 Channel 实现。 type Manager struct { mu sync.RWMutex channels map[string]Channel // 渠道名 → 实例(仅含已成功装配的) reasons map[string]string // 渠道名 → 未启用原因(给 admin 状态面看,含未启用的) } func NewManager() *Manager { return &Manager{ channels: map[string]Channel{}, reasons: map[string]string{ChannelWechat: "未配置"}, } } // register 装配结果登记:成功则入表、失败则从表移除并记原因(内部用,调用方持有意图日志)。 func (m *Manager) register(name string, ch Channel, reason string) { m.mu.Lock() defer m.mu.Unlock() if ch != nil { m.channels[name] = ch } else { delete(m.channels, name) } m.reasons[name] = reason } // ReloadWechat 按给定配置重建微信渠道。失败只降级为隐藏并记录原因,绝不 panic/拖垮服务。 func (m *Manager) ReloadWechat(ctx context.Context, c Config) error { w, reason, err := buildWechat(ctx, c) var ch Channel if w != nil { ch = w } m.register(ChannelWechat, ch, reason) if w != nil { log.Printf("[payment] 微信支付 Native 渠道已启用 (mchid=%s)", c.MchID) } else if !c.Empty() { log.Printf("[payment] 微信支付渠道未启用: %s", reason) } return err } // Get 按渠道名取实例;nil = 未配置/配置失败(渠道隐藏)。 func (m *Manager) Get(name string) Channel { m.mu.RLock() defer m.mu.RUnlock() return m.channels[name] } // Available 返回已启用的渠道名(供前端展示可选渠道)。 func (m *Manager) Available() []string { m.mu.RLock() defer m.mu.RUnlock() out := make([]string, 0, len(m.channels)) for name := range m.channels { out = append(out, name) } sort.Strings(out) return out } // Status 给 admin 状态面:某渠道是否启用 + 未启用原因。 func (m *Manager) Status(name string) (bool, string) { m.mu.RLock() defer m.mu.RUnlock() _, ok := m.channels[name] return ok, m.reasons[name] } // buildWechat 装配微信渠道实例;返回 (实例, 未启用原因, 错误)。配置为空不算错。 func buildWechat(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 }