增加微信通知功能
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
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{}
|
||||
}
|
||||
Reference in New Issue
Block a user