Files
iotlcm/internal/sms/scene_store.go
T
2026-10-29 14:17:14 +08:00

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()
}