Files
2026-09-16 18:06:18 +08:00

208 lines
5.0 KiB
Go
Raw Permalink 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
}
// 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,
}
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
}
// 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{}
}