281 lines
11 KiB
Go
281 lines
11 KiB
Go
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/<数据集名>.tflite(datasetDir 下 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)
|
||
})
|
||
// 支付回调组:不做统一包装,按渠道应答格式直接返回(微信 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_dist(server_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 {
|
||
// v8:dataset 加训练配置/展示列 + 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_file(zip 产物布局移除后无人写)
|
||
// + 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 COLUMN(NOT 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)")
|
||
}
|
||
// 死表清理: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)
|
||
}
|
||
}
|