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.
141 lines
5.3 KiB
141 lines
5.3 KiB
package console
|
|
|
|
/*
|
|
设备管理域(厂家 / 产品 / 版本)只读缓存。
|
|
|
|
console 主库(supabase)在海外、直连查询慢;这些是「后台维护、低频变更、高频读取」的引用数据,
|
|
后台多个页面(产品/版本/出货等)每次打开都要全量读,直连 DB 就「卡」。本组件把它们缓存到
|
|
与服务同区域的 Redis,使后台读请求恒命中 Redis:
|
|
|
|
- 预热:Start() 异步全量写入 Redis(避免海外慢查询阻塞模块启动)
|
|
- 定时刷新:内部 ticker 每 10 分钟全量刷新(console 服务未装 cron,用 ticker 自驱)
|
|
- 写后失效:增删改 handler 成功后调用对应 refreshXxx() 同步重写缓存,使紧随其后的列表读到最新
|
|
|
|
复用 sys/cache(基于 Redis Hash 的全量数据集);DB 回退用 consoleDeviceConn()(=postgres.GetSys())。
|
|
读方法(GetXxx)缓存优先、未命中或异常时回退查 DB,保证 Redis 异常时功能不受影响。
|
|
*/
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/lego/core"
|
|
"yunyan/lego/core/cbase"
|
|
"yunyan/lego/sys/log"
|
|
"yunyan/pb"
|
|
"yunyan/sys/cache"
|
|
)
|
|
|
|
// 兜底 TTL:正常由写后刷新 + 定时刷新覆盖,TTL 仅作异常情况下的过期保护
|
|
const deviceCacheTTL = time.Hour * 24
|
|
|
|
type deviceCacheComp struct {
|
|
cbase.ModuleCompBase
|
|
module *Console
|
|
}
|
|
|
|
func (this *deviceCacheComp) 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.(*Console)
|
|
return
|
|
}
|
|
|
|
func (this *deviceCacheComp) Start() (err error) {
|
|
if err = this.ModuleCompBase.Start(); err != nil {
|
|
return
|
|
}
|
|
go this.Refresh() // 异步预热
|
|
go this.loopRefresh() // 每 10 分钟兜底全量刷新
|
|
return
|
|
}
|
|
|
|
// loopRefresh console 服务未初始化 cron 子系统,这里用 ticker 自驱定时刷新。
|
|
func (this *deviceCacheComp) loopRefresh() {
|
|
t := time.NewTicker(time.Minute * 10)
|
|
defer t.Stop()
|
|
for range t.C {
|
|
this.Refresh()
|
|
}
|
|
}
|
|
|
|
// Refresh 全量刷新所有数据集。best-effort:单个数据集失败仅记日志,不影响其它。
|
|
func (this *deviceCacheComp) Refresh() {
|
|
this.refreshProducts()
|
|
this.refreshProductVersions()
|
|
}
|
|
|
|
// ---- 产品 ----
|
|
|
|
func (this *deviceCacheComp) refreshProducts() {
|
|
models, err := dvProducts(consoleDeviceConn())
|
|
if err != nil {
|
|
this.module.Error("deviceCache refreshProducts load", 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_Product, items, deviceCacheTTL); err != nil {
|
|
this.module.Error("deviceCache refreshProducts write", log.Field{Key: "err", Value: err.Error()})
|
|
}
|
|
}
|
|
|
|
// GetProducts 读全部产品:缓存优先,回退 DB。
|
|
// 缓存底层是 Redis Hash(HGetAll 无序),统一按产品 id 升序,保证列表顺序稳定。
|
|
// (2026-09-12 品牌商下线前这里先按 brandid 排,那一维现在恒为 0,已去掉。)
|
|
func (this *deviceCacheComp) GetProducts() (models []*pb.DBProduct, err error) {
|
|
if list, cerr := cache.GetAll[pb.DBProduct](context.Background(), comm.Cache_Product); cerr == nil && len(list) > 0 {
|
|
models = list
|
|
} else if models, err = dvProducts(consoleDeviceConn()); err != nil {
|
|
return
|
|
}
|
|
sort.Slice(models, func(i, j int) bool { return models[i].Id < models[j].Id })
|
|
return models, nil
|
|
}
|
|
|
|
// GetProduct 按 id 读产品:缓存优先,回退 DB。
|
|
func (this *deviceCacheComp) GetProduct(id uint32) (model *pb.DBProduct, err error) {
|
|
if v, found, cerr := cache.GetOne[pb.DBProduct](context.Background(), comm.Cache_Product, fmt.Sprintf("%d", id)); cerr == nil && found {
|
|
return v, nil
|
|
}
|
|
return dvProduct(consoleDeviceConn(), id)
|
|
}
|
|
|
|
// ---- 产品版本 ----
|
|
// 全部版本存一个 Hash(field=版本id),按 productid 在内存过滤。版本数据量小,整表缓存足够。
|
|
|
|
func (this *deviceCacheComp) refreshProductVersions() {
|
|
models, err := dvAllProductVersions(consoleDeviceConn())
|
|
if err != nil {
|
|
this.module.Error("deviceCache refreshProductVersions load", 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_ProductVersion, items, deviceCacheTTL); err != nil {
|
|
this.module.Error("deviceCache refreshProductVersions write", log.Field{Key: "err", Value: err.Error()})
|
|
}
|
|
}
|
|
|
|
// GetProductVersions 读某产品的版本:缓存命中则内存过滤 productid,未命中回退 DB(仅查该产品)。
|
|
// 同样按版本 id 升序,避免 Hash 无序导致版本列表顺序漂移。
|
|
func (this *deviceCacheComp) GetProductVersions(pid uint32) (models []*pb.DBProductVersion, err error) {
|
|
if list, cerr := cache.GetAll[pb.DBProductVersion](context.Background(), comm.Cache_ProductVersion); cerr == nil && len(list) > 0 {
|
|
models = make([]*pb.DBProductVersion, 0, len(list))
|
|
for _, v := range list {
|
|
if v.Productid == pid {
|
|
models = append(models, v)
|
|
}
|
|
}
|
|
} else if models, err = dvProductVersions(consoleDeviceConn(), pid); err != nil {
|
|
return
|
|
}
|
|
sort.Slice(models, func(i, j int) bool { return models[i].Id < models[j].Id })
|
|
return models, nil
|
|
}
|
|
|