You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

178 lines
7.2 KiB

package echomeet
/*
缓存组件:把高频访问的 public 公共模板加载到 Redis,并对外提供读取接口。
public 模板存于 admin MySQL(海外),所有用户共享、每次 GetTemplates 都要读取,是慢查询热点。
本组件采用"全量预热 + 定时刷新 + 事件驱动刷新"策略,使读请求恒命中 Redis:
- 预热:Start() 时全量写入 Redis
- 定时刷新:内部 cron 每 10 分钟全量刷新
- 事件驱动:后台增删改 public 模板触发 Rpc_ModifyEchomeetTemplate,由本组件监听并刷新
用户私有模板(ServiceDB,高基数、专属)不预热,相关读取在缓存未命中时回退查 DB。
*/
import (
"context"
"yunyan/comm"
"yunyan/lego/base"
"yunyan/lego/core"
"yunyan/lego/core/cbase"
"yunyan/lego/sys/cron"
"yunyan/lego/sys/log"
"yunyan/pb"
"yunyan/sys/cache"
natssys "yunyan/sys/nats"
"encoding/json"
"fmt"
"time"
gonats "github.com/nats-io/nats.go"
)
// 缓存兜底 TTL:正常由定时/事件全量刷新覆盖,TTL 仅作异常情况下的过期保护
const cacheTTL = time.Hour * 24
type modelCacheComp struct {
cbase.ModuleCompBase
module *Echomeet
service base.IRPCXService
}
func (this *modelCacheComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) {
this.ModuleCompBase.Init(service, module, comp, opt)
this.module = module.(*Echomeet)
this.service = service.(base.IRPCXService)
return
}
func (this *modelCacheComp) Start() (err error) {
if err = this.ModuleCompBase.Start(); err != nil {
return
}
this.service.Register(string(comm.Rpc_ModifyEchomeetTemplate), this.Rpc_ModifyEchomeetTemplate) //事件驱动刷新
go this.Refresh() //启动预热(异步,避免海外 DB 慢查询阻塞模块启动)
cron.AddFunc("0 */10 * * * ?", this.Refresh) //每 10 分钟全量刷新
this.subscribeConfigChanged() //订阅 console 配置变更广播
return
}
// subscribeConfigChanged 订阅 console 的「配置已变更」广播(core NATS pub/sub,fanout)。
// 收到 template 类即全量刷新 public 模板缓存(与 Rpc_ModifyEchomeetTemplate 同效);
// 其余类型本模块不关心。NATS 未就绪则跳过——仍有 10 分钟定时刷新兜底。
func (this *modelCacheComp) subscribeConfigChanged() {
conn := natssys.Conn()
if conn == nil {
this.module.Warn("echomeet: NATS 未就绪,配置变更广播订阅未启用(定时刷新兜底)")
return
}
if _, err := conn.Subscribe(comm.Nats_ConfigChangedSubject, func(msg *gonats.Msg) {
var ev comm.ConfigChangedEvent
if e := json.Unmarshal(msg.Data, &ev); e != nil {
return
}
if ev.Kind == comm.ConfigKindTemplate {
this.Refresh()
this.module.Infof("echomeet: 按配置变更广播刷新模板缓存 action=%s", ev.Action)
}
}); err != nil {
this.module.Error("echomeet: 订阅配置变更广播失败", log.Field{Key: "err", Value: err.Error()})
}
}
// 事件驱动:后台增删改 public 模板后刷新 Redis 缓存
func (this *modelCacheComp) Rpc_ModifyEchomeetTemplate(ctx context.Context, args *pb.Rpc_EmptyReq, reply *pb.Rpc_EmptyResp) (err error) {
this.Refresh()
return
}
// Refresh 全量刷新缓存数据集到 Redis。best-effort:失败仅记日志
func (this *modelCacheComp) Refresh() {
models, err := this.module.model.getcommtemplates("")
if err != nil {
this.module.Error("refresh echomeet template load db", log.Field{Key: "err", Value: err.Error()})
return
}
items := make(map[string]any, len(models))
for _, m := range models {
items[fmt.Sprintf("%d", m.Id)] = m
}
if err = cache.ReplaceAll(context.Background(), comm.Cache_EchomeetTemplate, items, cacheTTL); err != nil {
this.module.Error("refresh echomeet template write redis", log.Field{Key: "err", Value: err.Error()})
}
}
// ---- 对外读取接口(缓存优先 + DB 回退)-----------------------------------------
// GetCommTemplates 读取 public 公共模板:缓存优先,language 为空返回全部,否则按语言过滤
func (this *modelCacheComp) GetCommTemplates(language string) (models []*pb.DBEchoMeetTemplate, err error) {
list, cerr := cache.GetAll[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate)
if cerr != nil || len(list) == 0 {
return this.module.model.getcommtemplates(language) //回退查 DB
}
models = make([]*pb.DBEchoMeetTemplate, 0, len(list))
for _, m := range list {
if language == "" || m.Language == language {
models = append(models, m)
}
}
if len(models) > 0 || language == "" {
return // 精确命中(或不限语言)
}
// 回退:该区域 locale 无专属模板时,取同基础语言变体(如 es-MX→es/es-ES),按 tid 去重保留 id 最小一条
if base := baseLang(language); base != "" && base != language {
models = pickBaseLangVariants(list, base)
}
return
}
// GetTemplateForId 按 id 读取模板:公共模板(id<起点)先查 public 缓存,未命中回退 DB;
// 私有模板(id>=起点)不在公共缓存内,直接查 DB(避免与公共模板 id 误命中)
func (this *modelCacheComp) GetTemplateForId(id uint64) (template *pb.DBEchoMeetTemplate, err error) {
if id < templatePrivateIdStart {
if v, found, cerr := cache.GetOne[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate, fmt.Sprintf("%d", id)); cerr == nil && found {
return v, nil
}
}
return this.module.model.gettemplateforid(id)
}
// GetTemplateForTid 按 tid+language 读取模板:先扫 public 缓存命中即返回,否则回退 DB。
// 精确匹配不到时,回退到同一基础语言的任一变体(如 es-MX→es-ES、zh-TW→zh-CN),
// 避免区域 locale 没有专属模板时解析失败;候选取 id 最小的一条保证结果稳定。
func (this *modelCacheComp) GetTemplateForTid(tid string, language string) (template *pb.DBEchoMeetTemplate, err error) {
if list, cerr := cache.GetAll[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate); cerr == nil && len(list) > 0 {
base := baseLang(language)
var fallback *pb.DBEchoMeetTemplate
for _, m := range list {
if m.Tid != tid {
continue
}
if m.Language == language {
return m, nil // 精确命中
}
if base != "" && base != language && baseLang(m.Language) == base {
if fallback == nil || m.Id < fallback.Id {
fallback = m
}
}
}
if fallback != nil {
return fallback, nil
}
}
return this.module.model.gettemplatefortid(tid, language)
}
// ResolveTemplate 解析模板(与 model.resolvetemplate 行为一致):
// - 私有模板(templateid>=起点):单条、无多语言版本,按主键直取,忽略 tid/language;
// - 公共模板:tid 非空按 tid+language 取对应语言版本,否则按主键。
func (this *modelCacheComp) ResolveTemplate(tid string, templateid uint64, language string) (template *pb.DBEchoMeetTemplate, err error) {
if templateid >= templatePrivateIdStart {
return this.GetTemplateForId(templateid)
}
if tid != "" {
return this.GetTemplateForTid(tid, language)
}
return this.GetTemplateForId(templateid)
}