1
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# 运行时数据目录(compose 挂载卷,不进构建上下文,workspace 含 1.3GB APK 上传)
|
||||
workspace/
|
||||
data/
|
||||
cache/
|
||||
training/
|
||||
@@ -21,23 +21,23 @@ var AnnotateRecord = &annotateRecordDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS annotate_record (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phone_num TEXT NOT NULL,
|
||||
task_id INTEGER NOT NULL,
|
||||
dataset_id INTEGER NOT NULL DEFAULT 0,
|
||||
image_id INTEGER NOT NULL,
|
||||
labels_json TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TEXT NOT NULL,
|
||||
submitted_at TEXT,
|
||||
reviewed_at TEXT,
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
phone_num VARCHAR(20) NOT NULL,
|
||||
task_id BIGINT NOT NULL,
|
||||
dataset_id BIGINT NOT NULL DEFAULT 0,
|
||||
image_id BIGINT NOT NULL,
|
||||
labels_json JSONB,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
submitted_at TIMESTAMP,
|
||||
reviewed_at TIMESTAMP,
|
||||
UNIQUE (phone_num, image_id)
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 存量表补列(领取时冗余数据集 id,管理端详情页按数据集过滤记录)
|
||||
common.EnsureColumn(ctx, consts.TableAnnotateRecord, "dataset_id", "dataset_id INTEGER NOT NULL DEFAULT 0")
|
||||
common.EnsureColumn(ctx, consts.TableAnnotateRecord, "dataset_id", "dataset_id BIGINT NOT NULL DEFAULT 0")
|
||||
// 领取锁:同一张图同时只允许一条 pending(partial unique index,SQLite 原生支持)
|
||||
if _, err := g.DB().Exec(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS idx_annotate_record_image_pending
|
||||
ON annotate_record (image_id) WHERE status = 'pending'`); err != nil {
|
||||
@@ -52,7 +52,7 @@ func (d *annotateRecordDao) InsertInTx(ctx context.Context, tx gdb.TX, m *entity
|
||||
"task_id": m.TaskId,
|
||||
"dataset_id": m.DatasetId,
|
||||
"image_id": m.ImageId,
|
||||
"labels_json": m.LabelsJson,
|
||||
"labels_json": common.NilIfEmpty(m.LabelsJson),
|
||||
"status": m.Status,
|
||||
"created_at": m.CreatedAt,
|
||||
}).Insert()
|
||||
|
||||
@@ -19,11 +19,11 @@ var AnnotateTask = &annotateTaskDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS annotate_task (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'published',
|
||||
created_at TEXT NOT NULL
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dataset_id BIGINT NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'published',
|
||||
created_at TIMESTAMP NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"observer-server/biz/consts"
|
||||
"observer-server/biz/model/entity"
|
||||
@@ -20,29 +19,22 @@ var AppVersion = &appVersionDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS app_version (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
version TEXT NOT NULL UNIQUE,
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
version VARCHAR(32) NOT NULL UNIQUE,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// v6 迁移:删除 url 列(下载地址改为固定文件 app.apkDir/observer-latest.apk,
|
||||
// 表内不再记录;新库建表已无此列直接跳过)
|
||||
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info(app_version)")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, col := range cols {
|
||||
if gconv.String(col["name"]) == "url" {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE app_version DROP COLUMN url"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
g.Log().Warningf(ctx, "存量表 app_version 已迁移:删除 url 列")
|
||||
break
|
||||
if common.HasColumn(ctx, "app_version", "url") {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE app_version DROP COLUMN url"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
g.Log().Warningf(ctx, "存量表 app_version 已迁移:删除 url 列")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-23
@@ -22,42 +22,42 @@ var Dataset = &datasetDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS dataset (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL UNIQUE,
|
||||
source VARCHAR(10) NOT NULL DEFAULT 'manual',
|
||||
image_count INTEGER NOT NULL DEFAULT 0,
|
||||
labeled_count INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'building',
|
||||
cover TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'building',
|
||||
cover VARCHAR(200),
|
||||
description TEXT,
|
||||
name_prefix TEXT NOT NULL DEFAULT '',
|
||||
name_prefix VARCHAR(50) NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
gen_species TEXT,
|
||||
gen_tone TEXT,
|
||||
gen_heights TEXT,
|
||||
gen_scenes TEXT,
|
||||
gen_actions TEXT,
|
||||
gen_occlusions TEXT,
|
||||
gen_classes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
gen_species VARCHAR(100),
|
||||
gen_tone VARCHAR(50),
|
||||
gen_heights VARCHAR(100),
|
||||
gen_scenes JSONB,
|
||||
gen_actions JSONB,
|
||||
gen_occlusions JSONB,
|
||||
gen_classes VARCHAR(200),
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 存量库迁移:生成图文件名前缀列(EnsureColumn 的 ddl 须自带列名)
|
||||
common.EnsureColumn(ctx, consts.TableDataset, "name_prefix", "name_prefix TEXT NOT NULL DEFAULT ''")
|
||||
common.EnsureColumn(ctx, consts.TableDataset, "name_prefix", "name_prefix VARCHAR(50) NOT NULL DEFAULT ''")
|
||||
// 存量库迁移:序号列(列表排序主键,升序;同号按创建时间倒序)
|
||||
common.EnsureColumn(ctx, consts.TableDataset, "sort_order", "sort_order INTEGER NOT NULL DEFAULT 0")
|
||||
// 存量库迁移:生成参数池列(VLM 自动生成,2026-08-28)
|
||||
for _, c := range []struct{ name, ddl string }{
|
||||
{"gen_species", "gen_species TEXT"},
|
||||
{"gen_tone", "gen_tone TEXT"},
|
||||
{"gen_heights", "gen_heights TEXT"},
|
||||
{"gen_scenes", "gen_scenes TEXT"},
|
||||
{"gen_actions", "gen_actions TEXT"},
|
||||
{"gen_occlusions", "gen_occlusions TEXT"},
|
||||
{"gen_classes", "gen_classes TEXT"},
|
||||
{"gen_species", "gen_species VARCHAR(100)"},
|
||||
{"gen_tone", "gen_tone VARCHAR(50)"},
|
||||
{"gen_heights", "gen_heights VARCHAR(100)"},
|
||||
{"gen_scenes", "gen_scenes JSONB"},
|
||||
{"gen_actions", "gen_actions JSONB"},
|
||||
{"gen_occlusions", "gen_occlusions JSONB"},
|
||||
{"gen_classes", "gen_classes VARCHAR(200)"},
|
||||
} {
|
||||
common.EnsureColumn(ctx, consts.TableDataset, c.name, c.ddl)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"observer-server/biz/consts"
|
||||
"observer-server/biz/model/entity"
|
||||
@@ -19,13 +18,13 @@ var DatasetImage = &datasetImageDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS dataset_image (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dataset_id BIGINT NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
source VARCHAR(10) NOT NULL DEFAULT 'manual',
|
||||
animal_count INTEGER NOT NULL DEFAULT 0,
|
||||
labels_json TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
labels_json JSONB,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
UNIQUE (dataset_id, filename)
|
||||
)`)
|
||||
if err != nil {
|
||||
@@ -33,32 +32,25 @@ func init() {
|
||||
}
|
||||
// 存量库迁移:标注列缺失时追加(新库建表已含列,幂等跳过);candidates_json 已随 v10 删除
|
||||
// 注意 EnsureColumn 的 ddl 参数须自带列名(拼接为 ALTER TABLE ADD COLUMN <ddl>)
|
||||
common.EnsureColumn(ctx, consts.TableDatasetImage, "labels_json", "labels_json TEXT")
|
||||
common.EnsureColumn(ctx, consts.TableDatasetImage, "labels_json", "labels_json JSONB")
|
||||
common.EnsureColumn(ctx, consts.TableDatasetImage, "animal_count", "animal_count INTEGER NOT NULL DEFAULT 0")
|
||||
common.EnsureColumn(ctx, consts.TableDatasetImage, "clean_excluded", "clean_excluded INTEGER NOT NULL DEFAULT 0")
|
||||
common.EnsureColumn(ctx, consts.TableDatasetImage, "review_status", "review_status INTEGER NOT NULL DEFAULT 0")
|
||||
common.EnsureColumn(ctx, consts.TableDatasetImage, "clean_excluded", "clean_excluded SMALLINT NOT NULL DEFAULT 0")
|
||||
common.EnsureColumn(ctx, consts.TableDatasetImage, "review_status", "review_status SMALLINT NOT NULL DEFAULT 0")
|
||||
// 存量库迁移:众包任务图片粒度下发占用标记(2026-09-07,0=未下发)
|
||||
common.EnsureColumn(ctx, consts.TableDatasetImage, "annotate_task_id", "annotate_task_id INTEGER NOT NULL DEFAULT 0")
|
||||
common.EnsureColumn(ctx, consts.TableDatasetImage, "annotate_task_id", "annotate_task_id BIGINT NOT NULL DEFAULT 0")
|
||||
// 存量迁移(幂等):加列后既有标注统一视为已审定稿(非空框→2);上线后不变式
|
||||
// review_status=0 ⟹ labels_json 无框 恒成立(拒绝清框、提交/预标置 1、审核置 2),
|
||||
// 故本 UPDATE 只会命中存量行,不会误改新数据
|
||||
if _, err := g.DB().Exec(ctx, `UPDATE dataset_image SET review_status = 2
|
||||
WHERE review_status = 0 AND labels_json IS NOT NULL AND labels_json != '' AND labels_json != '[]'`); err != nil {
|
||||
WHERE review_status = 0 AND labels_json IS NOT NULL AND labels_json != '[]'::jsonb`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 存量库迁移:删除 prompt 列(生成提示词不再存储,2026-09-02 决策)
|
||||
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info(dataset_image)")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, col := range cols {
|
||||
if gconv.String(col["name"]) == "prompt" {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE dataset_image DROP COLUMN prompt"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
g.Log().Warningf(ctx, "存量表 dataset_image 已迁移:删除 prompt 列")
|
||||
break
|
||||
if common.HasColumn(ctx, "dataset_image", "prompt") {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE dataset_image DROP COLUMN prompt"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
g.Log().Warningf(ctx, "存量表 dataset_image 已迁移:删除 prompt 列")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,12 +81,15 @@ func (d *datasetImageDao) GetById(ctx context.Context, id int64) (*entity.Datase
|
||||
|
||||
// ListByDataset 某数据集全部图片(按文件名编号倒序,详情页逐行展示用)。
|
||||
// 文件名形如 <前缀>_<编号>.jpg(无前缀为时间戳数字名),须提取下划线后的数字按数值倒序:
|
||||
// 字符串序会把 pheasant_101.jpg 排在 pheasant_2.jpg 之前('1'<'9'),导致最大编号落到底部
|
||||
// 字符串序会把 pheasant_101.jpg 排在 pheasant_2.jpg 之前('1'<'9'),导致最大编号落到底部。
|
||||
// pg 方言:regexp_match 提取「_ 后到扩展名前的数字段」,无下划线回退取前导数字
|
||||
// (对齐 SQLite CAST 的前导数字解析语义);无法提取时 NULLS LAST 排底(对齐 SQLite CAST 非
|
||||
// 数字返回 0 的排序效果)
|
||||
func (d *datasetImageDao) ListByDataset(ctx context.Context, datasetId int64) ([]*entity.DatasetImage, error) {
|
||||
var list []*entity.DatasetImage
|
||||
err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).
|
||||
Where("dataset_id", datasetId).
|
||||
Order("CAST(substr(filename, instr(filename, '_') + 1) AS INTEGER) DESC").Scan(&list)
|
||||
Order(`COALESCE((regexp_match(filename, '_([0-9]+)\.'))[1], (regexp_match(filename, '^([0-9]+)'))[1])::BIGINT DESC NULLS LAST`).Scan(&list)
|
||||
if err != nil {
|
||||
if common.IsNoRows(err) {
|
||||
return []*entity.DatasetImage{}, nil
|
||||
@@ -219,7 +214,7 @@ func (d *datasetImageDao) CountByDataset(ctx context.Context, datasetId int64) (
|
||||
// UpdateLabels 覆写标注 JSON(整图粒度;空串=清空标注;AI 自动标注与人工保存共用)
|
||||
func (d *datasetImageDao) UpdateLabels(ctx context.Context, id int64, labelsJson string) error {
|
||||
_, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Where("id", id).
|
||||
Data(g.Map{"labels_json": labelsJson}).Update()
|
||||
Data(g.Map{"labels_json": common.NilIfEmpty(labelsJson)}).Update()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -227,7 +222,7 @@ func (d *datasetImageDao) UpdateLabels(ctx context.Context, id int64, labelsJson
|
||||
// 预标检出/人工保存/App 提交 → 1;人工清空保存 → 0(回到未标注池)
|
||||
func (d *datasetImageDao) UpdateLabelsAndReview(ctx context.Context, id int64, labelsJson string, reviewStatus int) error {
|
||||
_, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Where("id", id).
|
||||
Data(g.Map{"labels_json": labelsJson, "review_status": reviewStatus}).Update()
|
||||
Data(g.Map{"labels_json": common.NilIfEmpty(labelsJson), "review_status": reviewStatus}).Update()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -20,23 +20,23 @@ var FalseTargetReport = &falseTargetReportDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS false_target_report (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phone_num TEXT NOT NULL,
|
||||
file TEXT NOT NULL,
|
||||
labels_json TEXT NOT NULL DEFAULT '',
|
||||
species TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- pending/approved(拒绝即删记录,无 rejected 存量)
|
||||
image_hash INTEGER NOT NULL DEFAULT 0, -- 整帧 dHash 64 位(上报查重;0=未算/存量回填前)
|
||||
suspect_real INTEGER NOT NULL DEFAULT 0, -- RF-DETR 高分检出疑似真目标预判标记(0/1)
|
||||
created_at TEXT NOT NULL,
|
||||
reviewed_at TEXT
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
phone_num VARCHAR(20) NOT NULL,
|
||||
file VARCHAR(255) NOT NULL,
|
||||
labels_json JSONB,
|
||||
species VARCHAR(50) NOT NULL DEFAULT '',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending/approved(拒绝即删记录,无 rejected 存量)
|
||||
image_hash BIGINT NOT NULL DEFAULT 0, -- 整帧 dHash 64 位(上报查重;0=未算/存量回填前)
|
||||
suspect_real SMALLINT NOT NULL DEFAULT 0, -- RF-DETR 高分检出疑似真目标预判标记(0/1)
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
reviewed_at TIMESTAMP
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 存量库迁移:查重/预判两列(2026-09-09;新库建表已含列,幂等跳过)
|
||||
common.EnsureColumn(ctx, consts.TableFalseTargetReport, "image_hash", "image_hash INTEGER NOT NULL DEFAULT 0")
|
||||
common.EnsureColumn(ctx, consts.TableFalseTargetReport, "suspect_real", "suspect_real INTEGER NOT NULL DEFAULT 0")
|
||||
common.EnsureColumn(ctx, consts.TableFalseTargetReport, "image_hash", "image_hash BIGINT NOT NULL DEFAULT 0")
|
||||
common.EnsureColumn(ctx, consts.TableFalseTargetReport, "suspect_real", "suspect_real SMALLINT NOT NULL DEFAULT 0")
|
||||
}
|
||||
|
||||
// Insert 插入上报记录并返回自增 id
|
||||
|
||||
@@ -19,15 +19,15 @@ var GenTask = &genTaskDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS gen_task (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dataset_id BIGINT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'running',
|
||||
total INTEGER NOT NULL DEFAULT 0,
|
||||
done INTEGER NOT NULL DEFAULT 0,
|
||||
rejected INTEGER NOT NULL DEFAULT 0,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
finished_at TEXT
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
finished_at TIMESTAMP
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -19,15 +19,15 @@ var LabelTask = &labelTaskDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS label_task (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dataset_id BIGINT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'running',
|
||||
total INTEGER NOT NULL DEFAULT 0,
|
||||
done INTEGER NOT NULL DEFAULT 0,
|
||||
filenames TEXT,
|
||||
filenames JSONB,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
finished_at TEXT
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
finished_at TIMESTAMP
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -41,7 +41,7 @@ func (d *labelTaskDao) Insert(ctx context.Context, m *entity.LabelTask) (int64,
|
||||
"status": m.Status,
|
||||
"total": m.Total,
|
||||
"done": m.Done,
|
||||
"filenames": m.Filenames,
|
||||
"filenames": common.NilIfEmpty(m.Filenames),
|
||||
"error": m.Error,
|
||||
"created_at": m.CreatedAt,
|
||||
"finished_at": m.FinishedAt,
|
||||
|
||||
@@ -21,20 +21,20 @@ func init() {
|
||||
ctx := context.Background()
|
||||
common.DropLegacyTableIfHasColumn(ctx, consts.TableLicense, "device_id")
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS license (
|
||||
phone_num TEXT PRIMARY KEY,
|
||||
password TEXT NOT NULL,
|
||||
expires_at TEXT,
|
||||
phone_num VARCHAR(20) PRIMARY KEY,
|
||||
password VARCHAR(100) NOT NULL,
|
||||
expires_at TIMESTAMP,
|
||||
remark TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
common.EnsureColumn(ctx, consts.TableLicense, "remark", "remark TEXT")
|
||||
// 标注众包(2026-09-04):冻结到期标记 + 通过比例统计基线(技术设计.md「App 标注众包与时长激励」)
|
||||
common.EnsureColumn(ctx, consts.TableLicense, "annotate_frozen_until", "annotate_frozen_until TEXT")
|
||||
common.EnsureColumn(ctx, consts.TableLicense, "annotate_stats_since", "annotate_stats_since TEXT")
|
||||
common.EnsureColumn(ctx, consts.TableLicense, "annotate_frozen_until", "annotate_frozen_until TIMESTAMP")
|
||||
common.EnsureColumn(ctx, consts.TableLicense, "annotate_stats_since", "annotate_stats_since TIMESTAMP")
|
||||
}
|
||||
|
||||
// GetByPhone 按手机号查询(走缓存,键含手机号)
|
||||
|
||||
@@ -21,24 +21,26 @@ var Training = &modelTrainingDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS model_training (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
variant TEXT NOT NULL DEFAULT 's',
|
||||
dataset_id INTEGER NOT NULL,
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'running',
|
||||
variant VARCHAR(5) NOT NULL DEFAULT 's',
|
||||
dataset_id BIGINT NOT NULL,
|
||||
kind VARCHAR(20) NOT NULL DEFAULT 'species',
|
||||
dataset_ids JSONB,
|
||||
imgsz INTEGER NOT NULL DEFAULT 1280,
|
||||
epochs INTEGER NOT NULL DEFAULT 150,
|
||||
batch INTEGER NOT NULL DEFAULT 16,
|
||||
device TEXT NOT NULL DEFAULT '0',
|
||||
device VARCHAR(20) NOT NULL DEFAULT '0',
|
||||
current_epoch INTEGER NOT NULL DEFAULT 0,
|
||||
total_epochs INTEGER NOT NULL DEFAULT 0,
|
||||
metrics TEXT,
|
||||
metrics JSONB,
|
||||
log_tail TEXT,
|
||||
pid INTEGER,
|
||||
error TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
finished_at TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
started_at TIMESTAMP NOT NULL,
|
||||
finished_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -53,14 +55,14 @@ func (d *modelTrainingDao) Insert(ctx context.Context, m *entity.ModelTraining)
|
||||
"variant": m.Variant,
|
||||
"dataset_id": m.DatasetId,
|
||||
"kind": m.Kind,
|
||||
"dataset_ids": m.DatasetIds,
|
||||
"dataset_ids": common.NilIfEmpty(m.DatasetIds),
|
||||
"imgsz": m.Imgsz,
|
||||
"epochs": m.Epochs,
|
||||
"batch": m.Batch,
|
||||
"device": m.Device,
|
||||
"current_epoch": m.CurrentEpoch,
|
||||
"total_epochs": m.TotalEpochs,
|
||||
"metrics": m.Metrics,
|
||||
"metrics": common.NilIfEmpty(m.Metrics),
|
||||
"log_tail": m.LogTail,
|
||||
"pid": m.Pid,
|
||||
"error": m.Error,
|
||||
|
||||
@@ -18,18 +18,20 @@ var ModelVersion = &modelVersionDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS model_version (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL,
|
||||
variant TEXT NOT NULL DEFAULT 's',
|
||||
version TEXT NOT NULL,
|
||||
training_id INTEGER,
|
||||
metrics TEXT,
|
||||
labels TEXT NOT NULL,
|
||||
sha256 TEXT NOT NULL,
|
||||
size_bytes INTEGER NOT NULL,
|
||||
is_latest INTEGER NOT NULL DEFAULT 0,
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dataset_id BIGINT NOT NULL,
|
||||
variant VARCHAR(5) NOT NULL DEFAULT 's',
|
||||
version VARCHAR(50) NOT NULL,
|
||||
training_id BIGINT,
|
||||
kind VARCHAR(20) NOT NULL DEFAULT 'species',
|
||||
dataset_ids JSONB,
|
||||
metrics JSONB,
|
||||
labels JSONB NOT NULL,
|
||||
sha256 VARCHAR(64) NOT NULL,
|
||||
size_bytes BIGINT NOT NULL,
|
||||
is_latest SMALLINT NOT NULL DEFAULT 0,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
UNIQUE (dataset_id, version)
|
||||
)`)
|
||||
if err != nil {
|
||||
@@ -42,11 +44,11 @@ func (d *modelVersionDao) Insert(ctx context.Context, m *entity.ModelVersion) (i
|
||||
res, err := g.DB().Model(consts.TableModelVersion).Ctx(ctx).Data(g.Map{
|
||||
"dataset_id": m.DatasetId,
|
||||
"kind": m.Kind,
|
||||
"dataset_ids": m.DatasetIds,
|
||||
"dataset_ids": common.NilIfEmpty(m.DatasetIds),
|
||||
"variant": m.Variant,
|
||||
"version": m.Version,
|
||||
"training_id": m.TrainingId,
|
||||
"metrics": m.Metrics,
|
||||
"metrics": common.NilIfEmpty(m.Metrics),
|
||||
"labels": m.Labels,
|
||||
"sha256": m.Sha256,
|
||||
"size_bytes": m.SizeBytes,
|
||||
|
||||
@@ -22,16 +22,16 @@ func init() {
|
||||
ctx := context.Background()
|
||||
common.DropLegacyTableIfHasColumn(ctx, consts.TablePaymentOrder, "device_id")
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS payment_order (
|
||||
order_id TEXT PRIMARY KEY,
|
||||
phone_num TEXT NOT NULL,
|
||||
plan_id TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
order_id VARCHAR(64) PRIMARY KEY,
|
||||
phone_num VARCHAR(20) NOT NULL,
|
||||
plan_id VARCHAR(20) NOT NULL,
|
||||
channel VARCHAR(20) NOT NULL,
|
||||
amount_cents INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'created',
|
||||
wx_trade_no TEXT UNIQUE,
|
||||
alipay_trade_no TEXT UNIQUE,
|
||||
created_at TEXT NOT NULL,
|
||||
paid_at TEXT
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'created',
|
||||
wx_trade_no VARCHAR(64) UNIQUE,
|
||||
alipay_trade_no VARCHAR(64) UNIQUE,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
paid_at TIMESTAMP
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -18,11 +18,11 @@ var RewardLog = &rewardLogDao{}
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS reward_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phone_num TEXT NOT NULL,
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
phone_num VARCHAR(20) NOT NULL,
|
||||
minutes INTEGER NOT NULL,
|
||||
task_id INTEGER NOT NULL DEFAULT 0,
|
||||
granted_at TEXT NOT NULL
|
||||
task_id BIGINT NOT NULL DEFAULT 0,
|
||||
granted_at TIMESTAMP NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
+26
-41
@@ -4,14 +4,10 @@ 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),
|
||||
@@ -52,17 +48,30 @@ func ClearCache(ctx context.Context, keys ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureColumn 存量库迁移:列缺失时 ALTER TABLE ADD COLUMN(SQLite 支持表尾追加)。
|
||||
// 新库由 CREATE TABLE 直接含列、已迁移库列已存在,均跳过;失败 panic(启动即暴露)。
|
||||
func EnsureColumn(ctx context.Context, table, column, ddl string) {
|
||||
res, err := g.DB().GetAll(ctx, "PRAGMA table_info("+table+")")
|
||||
// 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())
|
||||
}
|
||||
for _, r := range res {
|
||||
if gconv.String(r["name"]) == column {
|
||||
return
|
||||
}
|
||||
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())
|
||||
@@ -73,35 +82,11 @@ func EnsureColumn(ctx context.Context, table, column, ddl string) {
|
||||
// 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())
|
||||
if !HasColumn(ctx, table, column) {
|
||||
return
|
||||
}
|
||||
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())
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package common
|
||||
|
||||
import _ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||
@@ -1,5 +0,0 @@
|
||||
package common
|
||||
|
||||
// 全局统一注册 SQLite 驱动(main 与服务层白盒测试共用同一注册点,
|
||||
// 测试二进制不编译 main.go,驱动须在 common 层保证加载)
|
||||
import _ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
+7
-3
@@ -7,9 +7,13 @@ server:
|
||||
|
||||
database:
|
||||
default:
|
||||
type: sqlite
|
||||
# SQLite 数据文件(运行时数据,不提交 git)
|
||||
link: "sqlite::@file(./data/observer.db)"
|
||||
type: pgsql
|
||||
# PostgreSQL 连接(本机 Docker pg16 容器;分字段写法规避密码特殊字符的 URL 转义问题)
|
||||
host: "127.0.0.1"
|
||||
port: "5432"
|
||||
user: "259278618@qq.com"
|
||||
pass: "Tongli686^*^"
|
||||
name: "animal_spot"
|
||||
cache:
|
||||
# 查询缓存 TTL(秒)
|
||||
ttl: 30
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 视野后端单机部署:前后端一体单端口,./data(数据库)与 ./workspace(APK 上传)挂载持久化(容器重建不丢数据)
|
||||
name: redfuture-app
|
||||
name: redfuture
|
||||
|
||||
networks:
|
||||
redfuture-net:
|
||||
|
||||
@@ -3,6 +3,7 @@ module observer-server
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
github.com/smartwalle/alipay/v3 v3.2.31
|
||||
@@ -24,6 +25,7 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
||||
@@ -22,6 +22,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2 h1:u8EpP24GkprogROnJ7htMov9Fc66pTP1eVYrWxiCYOs=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2/go.mod h1:GmvM3r8GVByVMi4RD2+MCs5+CfxVXPMeT8mVDkAaAXE=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2 h1:KLS68SWS2W749x7e+eCCOO3UD2Sbw+bIbLEPR8o1FXw=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2/go.mod h1:uLcsu73PfpyhRc0Jq0gGAWQjN1tyGU9iBRrYgt/lu7g=
|
||||
github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
|
||||
@@ -40,6 +42,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
|
||||
+51
-137
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"observer-server/biz/controller"
|
||||
"observer-server/biz/service"
|
||||
@@ -71,54 +70,30 @@ func main() {
|
||||
s.Run()
|
||||
}
|
||||
|
||||
// initDatabase 数据初始化:存量库以 PRAGMA user_version 版本化迁移;
|
||||
// initDatabase 数据初始化:存量库以 schema_version 表版本化迁移(pg 无 PRAGMA,版本落表);
|
||||
// v2 = 删 license.plan_id 列(无业务语义);v3 = 删 plan 表(套餐已配置化,清残留表)。
|
||||
func initDatabase(ctx context.Context) {
|
||||
res, err := g.DB().GetAll(ctx, "PRAGMA user_version")
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "读取库版本失败: %+v", err)
|
||||
}
|
||||
version := 0
|
||||
if len(res) > 0 {
|
||||
version = gconv.Int(res[0]["user_version"])
|
||||
}
|
||||
version := dbVersion(ctx)
|
||||
if version < 2 {
|
||||
// v2:删除 license.plan_id(只留到期时间;PRAGMA table_info 检测列存在才 DROP,
|
||||
// v2:删除 license.plan_id(只留到期时间;HasColumn 检测列存在才 DROP,
|
||||
// 全新库建表已无此列、直接跳过,不丢账号数据)
|
||||
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info(license)")
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "读取 license 表结构失败: %+v", err)
|
||||
}
|
||||
hasPlanId := false
|
||||
for _, col := range cols {
|
||||
if gconv.String(col["name"]) == "plan_id" {
|
||||
hasPlanId = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasPlanId {
|
||||
if common.HasColumn(ctx, "license", "plan_id") {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE license DROP COLUMN plan_id"); err != nil {
|
||||
g.Log().Fatalf(ctx, "删除 license.plan_id 列失败: %+v", err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "PRAGMA user_version = 2"); err != nil {
|
||||
g.Log().Fatalf(ctx, "库版本写入失败: %+v", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "数据库初始化完成(version=2)")
|
||||
setDbVersion(ctx, 2)
|
||||
}
|
||||
if version < 3 {
|
||||
// v3:套餐改 config.yml 配置后清理残留 plan 表(订单快照 payment_order.plan_id 不受影响)
|
||||
if _, err := g.DB().Exec(ctx, "DROP TABLE IF EXISTS plan"); err != nil {
|
||||
g.Log().Fatalf(ctx, "删除残留 plan 表失败: %+v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "PRAGMA user_version = 3"); err != nil {
|
||||
g.Log().Fatalf(ctx, "库版本写入失败: %+v", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "数据库初始化完成(version=3)")
|
||||
setDbVersion(ctx, 3)
|
||||
}
|
||||
if version < 8 {
|
||||
// v8:dataset 加训练配置/展示列 + label_task 加 filenames(多选批量标注)。
|
||||
// PRAGMA table_info 逐列检测缺失才 ADD COLUMN,全新库建表自带全列直接跳过
|
||||
// HasColumn 逐列检测缺失才 ADD COLUMN,全新库建表自带全列直接跳过
|
||||
addCols := []struct{ table, col, ddl string }{
|
||||
{"dataset", "cover", "TEXT"},
|
||||
{"dataset", "description", "TEXT"},
|
||||
@@ -128,85 +103,43 @@ func initDatabase(ctx context.Context) {
|
||||
{"dataset", "train_user", "TEXT"},
|
||||
{"dataset", "train_password", "TEXT"},
|
||||
{"dataset", "train_key", "TEXT"},
|
||||
{"label_task", "filenames", "TEXT"},
|
||||
{"label_task", "filenames", "JSONB"},
|
||||
}
|
||||
for _, c := range addCols {
|
||||
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info("+c.table+")")
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "读取 %s 表结构失败: %+v", c.table, err)
|
||||
}
|
||||
exists := false
|
||||
for _, col := range cols {
|
||||
if gconv.String(col["name"]) == c.col {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
if common.HasColumn(ctx, c.table, c.col) {
|
||||
continue
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+c.table+" ADD COLUMN "+c.col+" "+c.ddl); err != nil {
|
||||
g.Log().Fatalf(ctx, "给 %s 加列 %s 失败: %+v", c.table, c.col, err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "PRAGMA user_version = 8"); err != nil {
|
||||
g.Log().Fatalf(ctx, "库版本写入失败: %+v", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "数据库初始化完成(version=8)")
|
||||
setDbVersion(ctx, 8)
|
||||
}
|
||||
if version < 10 {
|
||||
// v10:标注流程简化,撤销候选确认两阶段 —— dataset_image 删 candidates_json 列
|
||||
//(存量候选数据为空直接删;全新库建表已无此列、PRAGMA table_info 检测后跳过)
|
||||
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info(dataset_image)")
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "读取 dataset_image 表结构失败: %+v", err)
|
||||
}
|
||||
hasCandidates := false
|
||||
for _, col := range cols {
|
||||
if gconv.String(col["name"]) == "candidates_json" {
|
||||
hasCandidates = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasCandidates {
|
||||
//(存量候选数据为空直接删;全新库建表已无此列、HasColumn 检测后跳过)
|
||||
if common.HasColumn(ctx, "dataset_image", "candidates_json") {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE dataset_image DROP COLUMN candidates_json"); err != nil {
|
||||
g.Log().Fatalf(ctx, "删除 candidates_json 列失败: %+v", err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "PRAGMA user_version = 10"); err != nil {
|
||||
g.Log().Fatalf(ctx, "库版本写入失败: %+v", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "数据库初始化完成(version=10)")
|
||||
setDbVersion(ctx, 10)
|
||||
}
|
||||
if version < 11 {
|
||||
// v11:移除模型存档回退机制 —— model_version 删 model_file 列
|
||||
//(模型文件不落表:发布即写 latest.tflite,客户端固定下载;存量库删列、新库建表已无此列自动跳过)
|
||||
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info(model_version)")
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "读取 model_version 表结构失败: %+v", err)
|
||||
}
|
||||
hasModelFile := false
|
||||
for _, col := range cols {
|
||||
if gconv.String(col["name"]) == "model_file" {
|
||||
hasModelFile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasModelFile {
|
||||
if common.HasColumn(ctx, "model_version", "model_file") {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE model_version DROP COLUMN model_file"); err != nil {
|
||||
g.Log().Fatalf(ctx, "删除 model_file 列失败: %+v", err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "PRAGMA user_version = 11"); err != nil {
|
||||
g.Log().Fatalf(ctx, "库版本写入失败: %+v", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "数据库初始化完成(version=11)")
|
||||
setDbVersion(ctx, 11)
|
||||
}
|
||||
if version < 12 {
|
||||
// v12:清理孤儿字段 —— dataset 删 6 列(AI 标注/训练机 SSH 配置统一走 config.yml 的
|
||||
// localAi/training.ssh,零读写)+ model_version 删 artifact_file(zip 产物布局移除后无人写)
|
||||
// + label_task 删 boxes_file(标注已入库,候选框文件机制废弃)。
|
||||
// PRAGMA table_info 逐列检测存在才 DROP,新库建表已无此列直接跳过
|
||||
// HasColumn 逐列检测存在才 DROP,新库建表已无此列直接跳过
|
||||
dropCols := []struct{ table, col string }{
|
||||
{"dataset", "ai_endpoint"},
|
||||
{"dataset", "ai_model"},
|
||||
@@ -218,97 +151,78 @@ func initDatabase(ctx context.Context) {
|
||||
{"label_task", "boxes_file"},
|
||||
}
|
||||
for _, c := range dropCols {
|
||||
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info("+c.table+")")
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "读取 %s 表结构失败: %+v", c.table, err)
|
||||
}
|
||||
exists := false
|
||||
for _, col := range cols {
|
||||
if gconv.String(col["name"]) == c.col {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
if !common.HasColumn(ctx, c.table, c.col) {
|
||||
continue
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+c.table+" DROP COLUMN "+c.col); err != nil {
|
||||
g.Log().Fatalf(ctx, "删除 %s.%s 列失败: %+v", c.table, c.col, err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "PRAGMA user_version = 12"); err != nil {
|
||||
g.Log().Fatalf(ctx, "库版本写入失败: %+v", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "数据库初始化完成(version=12)")
|
||||
setDbVersion(ctx, 12)
|
||||
}
|
||||
if version < 13 {
|
||||
// v13:双档位训练 —— model_training + model_version 增 variant 列(s|n,默认 s 存量归 s 档)。
|
||||
// 全新库建表自带该列;存量库 ADD COLUMN(NOT NULL DEFAULT 's' 免回填)。
|
||||
// PRAGMA table_info 逐列检测缺失才 ADD COLUMN
|
||||
// HasColumn 逐列检测缺失才 ADD COLUMN
|
||||
addCols := []struct{ table, col string }{
|
||||
{"model_training", "variant"},
|
||||
{"model_version", "variant"},
|
||||
}
|
||||
for _, c := range addCols {
|
||||
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info("+c.table+")")
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "读取 %s 表结构失败: %+v", c.table, err)
|
||||
}
|
||||
exists := false
|
||||
for _, col := range cols {
|
||||
if gconv.String(col["name"]) == c.col {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
if common.HasColumn(ctx, c.table, c.col) {
|
||||
continue
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+c.table+" ADD COLUMN "+c.col+" TEXT NOT NULL DEFAULT 's'"); err != nil {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+c.table+" ADD COLUMN "+c.col+" VARCHAR(5) NOT NULL DEFAULT 's'"); err != nil {
|
||||
g.Log().Fatalf(ctx, "增加 %s.%s 列失败: %+v", c.table, c.col, err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "PRAGMA user_version = 13"); err != nil {
|
||||
g.Log().Fatalf(ctx, "库版本写入失败: %+v", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "数据库初始化完成(version=13)")
|
||||
setDbVersion(ctx, 13)
|
||||
}
|
||||
if version < 14 {
|
||||
// v14:综合训练(2026-09-09 多物种合并模型)——model_training / model_version
|
||||
// 加 kind(species/combined)+ dataset_ids(JSON 覆盖数据集列表)。
|
||||
// 存量行默认 species 零回填;全新库建表自带全列直接跳过
|
||||
addCols := []struct{ table, col, ddl string }{
|
||||
{"model_training", "kind", "TEXT NOT NULL DEFAULT 'species'"},
|
||||
{"model_training", "dataset_ids", "TEXT NOT NULL DEFAULT ''"},
|
||||
{"model_version", "kind", "TEXT NOT NULL DEFAULT 'species'"},
|
||||
{"model_version", "dataset_ids", "TEXT NOT NULL DEFAULT ''"},
|
||||
{"model_training", "kind", "VARCHAR(20) NOT NULL DEFAULT 'species'"},
|
||||
{"model_training", "dataset_ids", "JSONB"},
|
||||
{"model_version", "kind", "VARCHAR(20) NOT NULL DEFAULT 'species'"},
|
||||
{"model_version", "dataset_ids", "JSONB"},
|
||||
}
|
||||
for _, c := range addCols {
|
||||
cols, err := g.DB().GetAll(ctx, "PRAGMA table_info("+c.table+")")
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "读取 %s 表结构失败: %+v", c.table, err)
|
||||
}
|
||||
exists := false
|
||||
for _, col := range cols {
|
||||
if gconv.String(col["name"]) == c.col {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
if common.HasColumn(ctx, c.table, c.col) {
|
||||
continue
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+c.table+" ADD COLUMN "+c.col+" "+c.ddl); err != nil {
|
||||
g.Log().Fatalf(ctx, "给 %s 加列 %s 失败: %+v", c.table, c.col, err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "PRAGMA user_version = 14"); err != nil {
|
||||
g.Log().Fatalf(ctx, "库版本写入失败: %+v", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "数据库初始化完成(version=14)")
|
||||
setDbVersion(ctx, 14)
|
||||
}
|
||||
// 死表清理:app_config 全局训练配置表已撤销(配置走 config.yml),存量库残留表启动即删
|
||||
if _, err := g.DB().Exec(ctx, "DROP TABLE IF EXISTS app_config"); err != nil {
|
||||
g.Log().Fatalf(ctx, "删除残留 app_config 表失败: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// dbVersion 读取库版本(schema_version 单行表);首次运行建表并初始化 0。
|
||||
func dbVersion(ctx context.Context) int {
|
||||
if _, err := g.DB().Exec(ctx, "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)"); err != nil {
|
||||
g.Log().Fatalf(ctx, "创建 schema_version 表失败: %+v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO schema_version (version) SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM schema_version)"); err != nil {
|
||||
g.Log().Fatalf(ctx, "初始化 schema_version 失败: %+v", err)
|
||||
}
|
||||
res, err := g.DB().GetOne(ctx, "SELECT version FROM schema_version")
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "读取库版本失败: %+v", err)
|
||||
}
|
||||
return res["version"].Int()
|
||||
}
|
||||
|
||||
// setDbVersion 写入库版本。
|
||||
func setDbVersion(ctx context.Context, v int) {
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE schema_version SET version = ?", v); err != nil {
|
||||
g.Log().Fatalf(ctx, "库版本写入失败: %+v", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "数据库初始化完成(version=%d)", v)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user