Files
2026-09-11 09:50:06 +08:00

93 lines
3.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 空串转 nilJSONB/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)
}