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