93 lines
3.6 KiB
Go
93 lines
3.6 KiB
Go
package common
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/database/gdb"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
)
|
||
|
||
// IsNoRows 判断 Scan 空结果(结构体 Scan 无行时返回 sql.ErrNoRows),
|
||
// dao 查询方法据此返回 nil 实体而非错误。
|
||
func IsNoRows(err error) bool {
|
||
return err != nil && errors.Is(err, sql.ErrNoRows)
|
||
}
|
||
|
||
// CacheTTL 查询缓存 TTL(秒),来自 config.yml database.cache.ttl,缺失或非法回退默认值。
|
||
func CacheTTL(ctx context.Context) time.Duration {
|
||
ttl := g.Cfg().MustGet(ctx, "database.cache.ttl", 30).Int()
|
||
if ttl <= 0 {
|
||
ttl = 30
|
||
}
|
||
return time.Duration(ttl) * time.Second
|
||
}
|
||
|
||
// CacheOption 构造带显式缓存键的查询缓存选项:键须含业务参数(如 license:设备号),
|
||
// 写操作后必须按同键调用 ClearCache,否则「库里已改、查询还是旧值」。
|
||
func CacheOption(ctx context.Context, key string) gdb.CacheOption {
|
||
return gdb.CacheOption{Duration: CacheTTL(ctx), Force: false, Name: key}
|
||
}
|
||
|
||
// gdb 的查询缓存键 = 固定前缀 + 自定义 Name(见 gdb.genSelectCacheKey,前缀常量未导出),
|
||
// 清除时必须拼上同一前缀,否则键对不上、缓存永远清不掉。
|
||
const selectCachePrefix = "SelectCache:"
|
||
|
||
// ClearCache 清除查询缓存(写操作后必须调用)。清除失败仅记录日志:缓存 TTL 自愈兜底,
|
||
// 不阻断业务主链路。
|
||
func ClearCache(ctx context.Context, keys ...string) {
|
||
if len(keys) == 0 {
|
||
return
|
||
}
|
||
for _, key := range keys {
|
||
if _, err := g.DB().GetCache().Remove(ctx, selectCachePrefix+key); err != nil {
|
||
g.Log().Warningf(ctx, "清除查询缓存 %q 失败: %+v", key, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// NilIfEmpty 空串转 nil(JSONB/TIMESTAMP 列拒收空串,dao 写入可能为空的此类列时经此转换落 NULL;
|
||
// 读回 NULL 时 gf 转 string 为空串,业务侧 =="" 判断不受影响)。
|
||
func NilIfEmpty(s string) any {
|
||
if s == "" {
|
||
return nil
|
||
}
|
||
return s
|
||
}
|
||
|
||
// HasColumn 判断当前 schema 下表是否存在某列(迁移逻辑用,information_schema 通用)。
|
||
func HasColumn(ctx context.Context, table, column string) bool {
|
||
res, err := g.DB().GetOne(ctx, `SELECT COUNT(1) AS n FROM information_schema.columns
|
||
WHERE table_schema = current_schema() AND table_name = ? AND column_name = ?`, table, column)
|
||
if err != nil {
|
||
panic("检查表结构失败 " + table + ": " + err.Error())
|
||
}
|
||
return res != nil && res["n"].Int() > 0
|
||
}
|
||
|
||
// EnsureColumn 存量库迁移:列缺失时 ALTER TABLE ADD COLUMN(表尾追加)。
|
||
// 新库由 CREATE TABLE 直接含列、已迁移库列已存在,均跳过;失败 panic(启动即暴露)。
|
||
func EnsureColumn(ctx context.Context, table, column, ddl string) {
|
||
if HasColumn(ctx, table, column) {
|
||
return
|
||
}
|
||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+table+" ADD COLUMN "+ddl); err != nil {
|
||
panic("迁移加列失败 " + table + "." + column + ": " + err.Error())
|
||
}
|
||
g.Log().Warningf(ctx, "存量表 %s 已迁移:新增列 %s", table, column)
|
||
}
|
||
|
||
// DropLegacyTableIfHasColumn 存量库迁移:表存在旧版本废弃列(账号体系上线前的 device_id)
|
||
// 时 DROP 整表(存量数据作废,用户决策),由 dao init 以新结构重建。
|
||
func DropLegacyTableIfHasColumn(ctx context.Context, table, column string) {
|
||
if !HasColumn(ctx, table, column) {
|
||
return
|
||
}
|
||
if _, err := g.DB().Exec(ctx, "DROP TABLE "+table); err != nil {
|
||
panic("DROP 旧表失败 " + table + ": " + err.Error())
|
||
}
|
||
g.Log().Warningf(ctx, "存量表 %s 为旧结构(列 %s),已 DROP 由新结构重建(存量数据作废)", table, column)
|
||
}
|