Files
observer/server/common/base_dao.go
T
admin a0b115d954 训练体系整合与标注单阶段化
- 标注:AI 预标注直写 labels_json(去候选确认两阶段);重叠去重(minIoU);全量标注按钮
- 训练:脚本迁移入 server/training/(Go 化 prepare_yolo/analyze_rfdetr,保留 train_server.py);tflite 产物自检并入训练流程(check_tflite)
- 数据目录/权重不进 git;.gitignore 迁移至仓库根
2026-08-26 18:22:56 +08:00

108 lines
3.9 KiB
Go
Raw 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"
"os"
"path/filepath"
"regexp"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
// 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)
}
}
}
// EnsureColumn 存量库迁移:列缺失时 ALTER TABLE ADD COLUMNSQLite 支持表尾追加)。
// 新库由 CREATE TABLE 直接含列、已迁移库列已存在,均跳过;失败 panic(启动即暴露)。
func EnsureColumn(ctx context.Context, table, column, ddl string) {
res, err := g.DB().GetAll(ctx, "PRAGMA table_info("+table+")")
if err != nil {
panic("检查表结构失败 " + table + ": " + err.Error())
}
for _, r := range res {
if gconv.String(r["name"]) == 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) {
res, err := g.DB().GetAll(ctx, "PRAGMA table_info("+table+")")
if err != nil {
panic("检查表结构失败 " + table + ": " + err.Error())
}
for _, r := range res {
if gconv.String(r["name"]) == column {
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)
return
}
}
}
var fileLinkRegexp = regexp.MustCompile(`@file\(([^)]+)\)`)
// init 确保 SQLite 数据文件所在目录存在(驱动打开文件前必须已创建,否则报 unable to open)。
// 必须在任何 g.DB() 调用(含 dao 包 init 建表)之前执行。
func init() {
ctx := context.Background()
link := g.Cfg().MustGet(ctx, "database.default.link", "").String()
file := link
if m := fileLinkRegexp.FindStringSubmatch(link); len(m) == 2 {
file = m[1]
}
if dir := filepath.Dir(file); dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
panic("创建数据目录失败: " + err.Error())
}
}
}