Files
observer/server/main.go
T
admin 87ab06221c 综合训练(多物种合并模型)全链路:合并打包/独立版本序列/App 覆盖互斥 + 管理端发布入口
- 后端:/admin/trainings/combined 发起(≥2 数据集、类别重映射、防重名、负样本单份);
  model_training/model_version 加 kind+dataset_ids(迁移 v14),综合任务 dataset_id=0、
  文件基名 combined(_n)、版本序列独立;训练列表补 published 标记
- 管理端:数据训练页工具栏发起综合训练;横幅常驻进行中任务 + 每档最近一条已结束任务,
  成功未发布给「发布模型」入口(可关闭收起)
- App:目录解析 kind/datasetIds、激活覆盖互斥、自动更新退场改目标档待办横幅手动一键下载
2026-09-09 15:45:51 +08:00

315 lines
12 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 main
import (
"context"
"os"
"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"
"observer-server/common"
)
func main() {
ctx := context.Background()
initDatabase(ctx)
//// 封面存量迁移:历史固定命名 cover* → UUID jpg(幂等,新库空跑)
//if err := service.Dataset.MigrateLegacyCovers(ctx); err != nil {
// g.Log().Errorf(ctx, "封面存量迁移失败: %+v", err)
//}
//// 封面存量尺寸统一:非 1248x704 的压缩覆盖写(幂等,新库空跑)
//if err := service.Dataset.CompressExistingCovers(ctx); err != nil {
// g.Log().Errorf(ctx, "封面存量压缩失败: %+v", err)
//}
s := g.Server()
// Android APK 下载静态托管:app.apkDir 目录下固定文件 observer-latest.apk
// URL 固定 /download/observer-latest.apk(绕过统一响应包装,纯二进制流);
// 模型热更新文件同根 /download/trainings/<数据集名>.tflitedatasetDir 下 trainings 目录)
if err := os.MkdirAll(common.ApkDir(ctx), 0o755); err != nil {
g.Log().Fatalf(ctx, "创建 APK 目录失败: %+v", err)
}
s.AddStaticPath("/download", common.ApkDir(ctx))
// 客户端接口组:统一响应包装 {"code":0,"message":"ok","data":...}
// 账号组公开(注册/登录),业务组(订单/授权/模型目录)需登录态(Authorization: Bearer token
s.Group("/api/v1", func(group *ghttp.RouterGroup) {
group.Middleware(common.UnifiedResponse)
common.BindController(group, controller.Auth, controller.AppVersion)
})
s.Group("/api/v1", func(group *ghttp.RouterGroup) {
group.Middleware(common.UnifiedResponse, common.AuthRequired)
common.BindController(group, controller.Order, controller.License, controller.ModelCatalog, controller.Annotate, controller.FalseTarget)
})
// 支付回调组:不做统一包装,按渠道应答格式直接返回(微信 SUCCESS/FAIL JSON、支付宝 success/failure 文本)
s.Group("/api/v1/payment", func(group *ghttp.RouterGroup) {
common.BindController(group, controller.Payment)
})
// 管理端接口组:统一响应包装 + 静态 token 鉴权
s.Group("/api/v1/admin", func(group *ghttp.RouterGroup) {
group.Middleware(common.UnifiedResponse, common.AdminAuth)
common.BindController(group, controller.Admin)
})
// APK 下载引导页(h5/ 源码目录):微信内打开提示用手机浏览器,非微信直显下载按钮
if stat, err := os.Stat("./h5"); err == nil && stat.IsDir() {
s.AddStaticPath("/download-page", "./h5")
} else {
g.Log().Warningf(ctx, "h5 目录不存在,跳过下载引导页托管")
}
// 管理端静态页面托管:构建产物输出到 admin_distserver_admin/ 构建),SPA history 路由回退 index.html
// 目录不存在(尚未构建前端)时跳过,不影响 API 启动
if stat, err := os.Stat("./admin_dist"); err == nil && stat.IsDir() {
s.AddStaticPath("/admin", "./admin_dist")
s.BindMiddlewareDefault(common.SpaFallback("./admin_dist", "/admin"))
} else {
g.Log().Warningf(ctx, "admin_dist 不存在,跳过管理端静态托管(cd server_admin && npm run build")
}
// 后台协程:训练进度轮询 + 孤儿预标注任务恢复
service.Training.StartBackgroundJobs(ctx)
s.Run()
}
// initDatabase 数据初始化:存量库以 PRAGMA user_version 版本化迁移;
// 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"])
}
if version < 2 {
// v2:删除 license.plan_id(只留到期时间;PRAGMA table_info 检测列存在才 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 _, 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)")
}
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)")
}
if version < 8 {
// v8dataset 加训练配置/展示列 + label_task 加 filenames(多选批量标注)。
// PRAGMA table_info 逐列检测缺失才 ADD COLUMN,全新库建表自带全列直接跳过
addCols := []struct{ table, col, ddl string }{
{"dataset", "cover", "TEXT"},
{"dataset", "description", "TEXT"},
{"dataset", "ai_endpoint", "TEXT"},
{"dataset", "ai_model", "TEXT"},
{"dataset", "train_host", "TEXT"},
{"dataset", "train_user", "TEXT"},
{"dataset", "train_password", "TEXT"},
{"dataset", "train_key", "TEXT"},
{"label_task", "filenames", "TEXT"},
}
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 {
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)")
}
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 {
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)")
}
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 _, 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)")
}
if version < 12 {
// v12:清理孤儿字段 —— dataset 删 6 列(AI 标注/训练机 SSH 配置统一走 config.yml 的
// localAi/training.ssh,零读写)+ model_version 删 artifact_filezip 产物布局移除后无人写)
// + label_task 删 boxes_file(标注已入库,候选框文件机制废弃)。
// PRAGMA table_info 逐列检测存在才 DROP,新库建表已无此列直接跳过
dropCols := []struct{ table, col string }{
{"dataset", "ai_endpoint"},
{"dataset", "ai_model"},
{"dataset", "train_host"},
{"dataset", "train_user"},
{"dataset", "train_password"},
{"dataset", "train_key"},
{"model_version", "artifact_file"},
{"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 {
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)")
}
if version < 13 {
// v13:双档位训练 —— model_training + model_version 增 variant 列(s|n,默认 s 存量归 s 档)。
// 全新库建表自带该列;存量库 ADD COLUMNNOT NULL DEFAULT 's' 免回填)。
// PRAGMA table_info 逐列检测缺失才 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 {
continue
}
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+c.table+" ADD COLUMN "+c.col+" TEXT 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)")
}
if version < 14 {
// v14:综合训练(2026-09-09 多物种合并模型)——model_training / model_version
// 加 kindspecies/combined+ dataset_idsJSON 覆盖数据集列表)。
// 存量行默认 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 ''"},
}
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 {
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)")
}
// 死表清理: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)
}
}