120 lines
2.9 KiB
Go
120 lines
2.9 KiB
Go
package sms
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// 场景配置启用状态。
|
|
const sceneStatusEnabled = 1
|
|
|
|
// SceneConfig 是「业务场景 -> 短信模板」的配置,来源于数据库表 t_sms_template_config。
|
|
// 运营侧修改该表后,短信服务在缓存过期后即按新配置发送,无需改代码或重启。
|
|
type SceneConfig struct {
|
|
Scene string
|
|
ServiceType string
|
|
SignName string
|
|
TemplateID string
|
|
MsgContentTemplate string
|
|
Status int
|
|
}
|
|
|
|
// SceneProvider 提供按场景码查询模板配置的能力。
|
|
type SceneProvider interface {
|
|
Lookup(ctx context.Context, scene string) (*SceneConfig, error)
|
|
}
|
|
|
|
const sceneQuery = "SELECT `scene`, `service_type`, `sign_name`, `template_id`, `msg_content_template`, `status` " +
|
|
"FROM `t_sms_template_config` WHERE `scene` = ? LIMIT 1"
|
|
|
|
type sceneCacheEntry struct {
|
|
config *SceneConfig
|
|
expired time.Time
|
|
}
|
|
|
|
// SceneStore 从数据库读取场景模板配置,并做短 TTL 内存缓存:
|
|
// 改库后最多 ttl 时间即生效,同时避免每次发送都查库。
|
|
type SceneStore struct {
|
|
db *sql.DB
|
|
ttl time.Duration
|
|
mu sync.RWMutex
|
|
cache map[string]sceneCacheEntry
|
|
}
|
|
|
|
// NewSceneStore 创建一个基于数据库的场景配置存储。ttl<=0 时使用默认 60s。
|
|
func NewSceneStore(db *sql.DB, ttl time.Duration) *SceneStore {
|
|
if ttl <= 0 {
|
|
ttl = 60 * time.Second
|
|
}
|
|
return &SceneStore{
|
|
db: db,
|
|
ttl: ttl,
|
|
cache: make(map[string]sceneCacheEntry),
|
|
}
|
|
}
|
|
|
|
// Close 关闭底层数据库连接。
|
|
func (s *SceneStore) Close() error {
|
|
if s == nil || s.db == nil {
|
|
return nil
|
|
}
|
|
return s.db.Close()
|
|
}
|
|
|
|
// Lookup 返回指定场景的模板配置;场景未配置时返回 (nil, nil)。
|
|
// 命中与未命中都会缓存 ttl,避免对不存在的场景反复查库。
|
|
func (s *SceneStore) Lookup(ctx context.Context, scene string) (*SceneConfig, error) {
|
|
if s == nil || s.db == nil {
|
|
return nil, nil
|
|
}
|
|
scene = strings.TrimSpace(scene)
|
|
if scene == "" {
|
|
return nil, nil
|
|
}
|
|
if cfg, ok := s.cacheGet(scene); ok {
|
|
return cfg, nil
|
|
}
|
|
|
|
var item SceneConfig
|
|
row := s.db.QueryRowContext(ctx, sceneQuery, scene)
|
|
err := row.Scan(
|
|
&item.Scene,
|
|
&item.ServiceType,
|
|
&item.SignName,
|
|
&item.TemplateID,
|
|
&item.MsgContentTemplate,
|
|
&item.Status,
|
|
)
|
|
switch {
|
|
case err == nil:
|
|
result := item
|
|
s.cacheSet(scene, &result)
|
|
return &result, nil
|
|
case err == sql.ErrNoRows:
|
|
s.cacheSet(scene, nil)
|
|
return nil, nil
|
|
default:
|
|
// 查询失败不缓存,下次请求重试。
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
func (s *SceneStore) cacheGet(scene string) (*SceneConfig, bool) {
|
|
s.mu.RLock()
|
|
entry, ok := s.cache[scene]
|
|
s.mu.RUnlock()
|
|
if !ok || time.Now().After(entry.expired) {
|
|
return nil, false
|
|
}
|
|
return entry.config, true
|
|
}
|
|
|
|
func (s *SceneStore) cacheSet(scene string, cfg *SceneConfig) {
|
|
s.mu.Lock()
|
|
s.cache[scene] = sceneCacheEntry{config: cfg, expired: time.Now().Add(s.ttl)}
|
|
s.mu.Unlock()
|
|
}
|