Files
yunzerwebsiteallinone/go/services/wechatmp/config.go
T
2026-09-30 00:57:36 +08:00

241 lines
6.0 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 wechatmp
import (
"errors"
"strings"
"sync"
"time"
"server/models"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// =============================================================
// 微信公众号(服务号)配置与 access_token
// =============================================================
// 消息加解密方式
const (
EncryptModePlain = "plain" // 明文模式
EncryptModeCompatible = "compatible" // 兼容模式
EncryptModeSafe = "safe" // 安全模式
)
// Config 服务号运行配置(敏感字段已解密)
type Config struct {
ID uint64
AppID string
AppSecret string // 明文
Token string
AESKey string // 明文(43 位 EncodingAESKey)
EncryptMode string
TemplateID string
Verified bool
Enabled bool
Remark string
FollowQrcode string // 关注二维码(data URL),绑定页展示供扫码关注
}
// ErrNotConfigured 未配置或未启用
var ErrNotConfigured = errors.New("微信公众号未配置或未启用")
// LoadConfig 读取服务号配置(单行,敏感字段解密);未配置返回 ErrNotConfigured
func LoadConfig() (*Config, error) {
var row models.WechatMpConfig
err := models.Orm.QueryTable(new(models.WechatMpConfig)).OrderBy("id").One(&row)
if err != nil {
if err == orm.ErrNoRows {
return nil, ErrNotConfigured
}
return nil, err
}
cfg := &Config{
ID: row.ID,
AppID: strings.TrimSpace(row.AppID),
Token: strings.TrimSpace(row.Token),
EncryptMode: strings.TrimSpace(row.EncryptMode),
TemplateID: strings.TrimSpace(row.TemplateID),
Verified: row.Verified == 1,
Enabled: row.Enabled == 1,
Remark: row.Remark,
FollowQrcode: strings.TrimSpace(row.FollowQrcode),
}
if cfg.EncryptMode == "" {
cfg.EncryptMode = EncryptModePlain
}
if row.AppSecret != "" {
if plain, derr := DecryptSecret(row.AppSecret); derr == nil {
cfg.AppSecret = plain
} else {
return nil, derr
}
}
if row.AESKey != "" {
if plain, derr := DecryptSecret(row.AESKey); derr == nil {
cfg.AESKey = plain
} else {
return nil, derr
}
}
return cfg, nil
}
// LoadEnabledConfig 读取并校验启用状态
func LoadEnabledConfig() (*Config, error) {
cfg, err := LoadConfig()
if err != nil {
return nil, err
}
if !cfg.Enabled || cfg.AppID == "" || cfg.AppSecret == "" {
return nil, ErrNotConfigured
}
return cfg, nil
}
// SaveInput 保存配置入参(敏感字段支持掩码表示不改)
type SaveInput struct {
AppID string
AppSecret string // 掩码 / 空串 = 保持不变
Token string
AESKey string // 掩码 / 空串 = 保持不变
EncryptMode string
TemplateID string
Verified bool
Enabled bool
Remark string
}
// SaveConfig 保存服务号配置(不存在则创建)
func SaveConfig(in SaveInput) error {
if strings.TrimSpace(in.AppID) == "" {
return errors.New("AppID 不能为空")
}
mode := strings.TrimSpace(in.EncryptMode)
if mode == "" {
mode = EncryptModePlain
}
var row models.WechatMpConfig
err := models.Orm.QueryTable(new(models.WechatMpConfig)).OrderBy("id").One(&row)
now := time.Now()
isNew := false
if err != nil {
if err != orm.ErrNoRows {
return err
}
isNew = true
row = models.WechatMpConfig{}
}
// 敏感字段:掩码/空串表示保持原值
secret := strings.TrimSpace(in.AppSecret)
if secret != "" && !IsMasked(secret) {
enc, eerr := EncryptSecret(secret)
if eerr != nil {
return eerr
}
row.AppSecret = enc
}
aesKey := strings.TrimSpace(in.AESKey)
if aesKey != "" && !IsMasked(aesKey) {
enc, eerr := EncryptSecret(aesKey)
if eerr != nil {
return eerr
}
row.AESKey = enc
}
row.AppID = strings.TrimSpace(in.AppID)
row.Token = strings.TrimSpace(in.Token)
row.EncryptMode = mode
row.TemplateID = strings.TrimSpace(in.TemplateID)
row.Remark = strings.TrimSpace(in.Remark)
if in.Verified {
row.Verified = 1
} else {
row.Verified = 0
}
if in.Enabled {
row.Enabled = 1
} else {
row.Enabled = 0
}
row.UpdateTime = &now
if isNew {
row.CreateTime = now
_, err = models.Orm.Insert(&row)
return err
}
_, err = models.Orm.Update(&row)
return err
}
// SaveFollowQrcode 保存/清空关注二维码(data URL,空串表示删除)
func SaveFollowQrcode(dataURL string) error {
dataURL = strings.TrimSpace(dataURL)
now := time.Now()
var row models.WechatMpConfig
err := models.Orm.QueryTable(new(models.WechatMpConfig)).OrderBy("id").One(&row)
if err != nil {
if err == orm.ErrNoRows {
// 尚无配置行:仅存二维码,其余字段留空
row = models.WechatMpConfig{FollowQrcode: dataURL, EncryptMode: EncryptModePlain, CreateTime: now}
_, err = models.Orm.Insert(&row)
return err
}
return err
}
_, err = models.Orm.QueryTable(new(models.WechatMpConfig)).
Filter("id", row.ID).
Update(map[string]interface{}{"follow_qrcode": dataURL, "update_time": now})
return err
}
// FollowQrcodeURL 读取关注二维码(未配置时返回空串)
func FollowQrcodeURL() string {
cfg, err := LoadConfig()
if err != nil {
return ""
}
return cfg.FollowQrcode
}
// CallbackURL 微信服务器回调地址(供公众号后台「服务器配置」填写)。
// 优先读取 app.conf 的 wechat_mp_callback_base,其次回落 payment_callback_base(同为对外 API 域名)。
func CallbackURL() string {
base, _ := beego.AppConfig.String("wechat_mp_callback_base")
if strings.TrimSpace(base) == "" {
base, _ = beego.AppConfig.String("payment_callback_base")
}
base = strings.TrimRight(strings.TrimSpace(base), "/")
if base == "" {
return "/api/wechat/mp/callback"
}
return base + "/api/wechat/mp/callback"
}
// ============================ access_token 缓存 ============================
type tokenEntry struct {
appID string
token string
expiresAt time.Time
}
var (
tokenMu sync.Mutex
tokenCache tokenEntry
tokenMinTTL = 5 * time.Minute
)
// resetTokenCacheForTest 仅供测试清理缓存
func resetTokenCacheForTest() {
tokenMu.Lock()
defer tokenMu.Unlock()
tokenCache = tokenEntry{}
}