fix(#48): add the missing version-local migration for syb_session
Adding SYBSession to migrations.Migrate was not enough. Schema reaches an
existing database only through a version-local file; with the previous
version already recorded in sys_migration, Migrate never re-ran and the
table simply never appeared. The unit tests build a fresh database every
time, so they stayed green while the real database was missing a table —
which surfaced as "Error 1146: Table 'goauto.syb_session' doesn't exist"
on the first import attempt.
server/.gitignore was hiding these files. go-admin ignores version-local
because it is where generated local migrations land, but every GoAuto
migration belongs in version control; the existing ones had been forced
in with `git add -f`. Un-ignoring *.go there also recovers four migrations
that were never committed at all — 1786700000000 through 1786700300000,
covering the base schema, device registration, heartbeat and collection
execution. A fresh clone could not have built a working database.
Guard the class of mistake rather than just this instance:
- migrations.VerifyTables checks every model's table after migrating and
names what is missing along with the fix.
- The migrate command runs it, so the failure lands at migrate time
instead of at the first request that needs the table.
- initDB no longer discards migrateModel's error. Upstream had
`_ = migrateModel()` followed by an unconditional "初始化成功", so a
failed migration reported success and the launcher believed it.
Also records the two-step rule in Common-Changes: a new model needs both
the model registration and a new version file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Common-Changes
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Common-Changes.-
|
||||
wiki_revision: bba4d9558ccf9c6b5953ac99b958e81dc52a0637
|
||||
synchronized_at: 2026-08-19T01:22:30Z
|
||||
wiki_revision: 20cd4cd33e0cc7573e16b1de1442650e68fe472c
|
||||
synchronized_at: 2026-08-20T02:07:25Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 常见修改指南
|
||||
@@ -36,6 +36,21 @@ synchronized_at: 2026-08-19T01:22:30Z
|
||||
|
||||
停止条件:规则需要创建订单、支付、任意脚本、OCR/VLM 或点击不唯一候选。
|
||||
|
||||
## 新增数据库模型或字段
|
||||
|
||||
`[必须]` **两步都要做,缺一不可:**
|
||||
|
||||
1. 把模型加进 `server/app/goauto/models/schema.go`,并登记到 `migrations.MigratedModels()`;
|
||||
2. 在 `server/cmd/migrate/migration/version-local/` 下**新建一个版本文件**(时间戳递增,照抄同目录已有文件的写法)。
|
||||
|
||||
只做第 1 步对**全新数据库**有效,对**已有数据库无效**:旧版本号已经记在 `sys_migration` 里,`Migrate()` 不会再次执行,表就是不会出现。而单元测试每次都用全新数据库,所以照样全绿——这个缺口只会在真实环境里暴露成一句 `Error 1146: Table ... doesn't exist`(见 [#48](https://git.ilapage.cn/OPC/goauto/issues/48))。
|
||||
|
||||
`[必须]` 模型必须显式声明 `TableName()` 返回单数表名。漏写时 gorm 会静默使用复数,迁移照样成功。
|
||||
|
||||
迁移命令跑完会调用 `migrations.VerifyTables` 核对所有表是否都在,缺表时直接以非零码退出并报出表名。
|
||||
|
||||
停止条件:修改或删除已有列、需要数据回填、涉及唯一键语义变化。
|
||||
|
||||
## 增加错误码
|
||||
|
||||
错误码必须包含稳定代码、用户可读消息、是否可重试和建议处理。同步更新 Android、服务端、管理端和 `docs/08-agent-api-contract.md`。
|
||||
|
||||
+4
-1
@@ -14,8 +14,11 @@ config/settings.dev.*.yml.log
|
||||
temp/logs
|
||||
config/settings.dev.yml.log
|
||||
config/settings.b.dev.yml
|
||||
# go-admin 默认忽略这个目录(它原本放本地生成的迁移)。GoAuto 的迁移全部要
|
||||
# 跟踪:漏提交一个版本文件,别人的数据库就少一张表,而且要到运行时才炸。
|
||||
# 之前的版本文件是靠 git add -f 进来的,这条否定规则让它不再需要。
|
||||
cmd/migrate/migration/version-local/*
|
||||
!cmd/migrate/migration/version-local/doc.go
|
||||
!cmd/migrate/migration/version-local/*.go
|
||||
|
||||
config/settings.deva.yml
|
||||
go-admin-server
|
||||
|
||||
@@ -376,3 +376,29 @@ func TestEveryModelDeclaresSingularTableName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTablesPassesAfterMigrate(t *testing.T) {
|
||||
db := openDatabase(t)
|
||||
if err := migrations.VerifyTables(db); err != nil {
|
||||
t.Fatalf("迁移后不应有缺表: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 缺表时必须报出表名,并说清楚该怎么修——这正是漏建 version-local 迁移文件
|
||||
// 时的现场,之前它只会在运行时变成一句 "Error 1146"。
|
||||
func TestVerifyTablesNamesMissingTablesAndTheFix(t *testing.T) {
|
||||
db := openDatabase(t)
|
||||
if err := db.Migrator().DropTable("syb_session"); err != nil {
|
||||
t.Fatalf("删表失败: %v", err)
|
||||
}
|
||||
err := migrations.VerifyTables(db)
|
||||
if err == nil {
|
||||
t.Fatal("缺表时应报错")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "syb_session") {
|
||||
t.Fatalf("错误信息应指出缺哪张表: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "version-local") {
|
||||
t.Fatalf("错误信息应说明修法: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// VerifyTables reports which tables in the GoAuto schema are missing.
|
||||
//
|
||||
// `[必须]` This exists because adding a model to Migrate is not enough on its
|
||||
// own. Schema changes reach an existing database only through a new
|
||||
// version-local migration file; without one, the already-recorded version is
|
||||
// skipped, Migrate never runs again, and the new table simply never appears.
|
||||
// Nothing fails at migrate time — the gap surfaces much later as a bare
|
||||
// "Error 1146: Table ... doesn't exist" from whatever feature needed it.
|
||||
//
|
||||
// Running this right after the migrations turns that into an immediate,
|
||||
// named failure.
|
||||
func VerifyTables(db *gorm.DB) error {
|
||||
type tableNamer interface{ TableName() string }
|
||||
|
||||
missing := make([]string, 0)
|
||||
for _, model := range MigratedModels() {
|
||||
namer, ok := model.(tableNamer)
|
||||
if !ok {
|
||||
return fmt.Errorf("模型 %T 没有声明 TableName()", model)
|
||||
}
|
||||
if !db.Migrator().HasTable(namer.TableName()) {
|
||||
missing = append(missing, namer.TableName())
|
||||
}
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(missing)
|
||||
return fmt.Errorf(
|
||||
"迁移后仍缺少表:%s。新增模型必须同时在 cmd/migrate/migration/version-local/ "+
|
||||
"下新建一个版本文件;只把模型加进 migrations.Migrate 对已有数据库无效,"+
|
||||
"因为旧版本号已记录在 sys_migration 中,不会再次执行",
|
||||
strings.Join(missing, "、"))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package version_local
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
goautomigrations "go-admin/app/goauto/migrations"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
common "go-admin/common/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_, fileName, _, _ := runtime.Caller(0)
|
||||
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateGoAutoSchema)
|
||||
}
|
||||
|
||||
func migrateGoAutoSchema(db *gorm.DB, version string) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := goautomigrations.Migrate(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&common.Migration{Version: version}).Error
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package version_local
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
goautomodels "go-admin/app/goauto/models"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
common "go-admin/common/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_, fileName, _, _ := runtime.Caller(0)
|
||||
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateDeviceRegistration)
|
||||
}
|
||||
|
||||
func migrateDeviceRegistration(db *gorm.DB, version string) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&goautomodels.AgentDevice{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&common.Migration{Version: version}).Error
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package version_local
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
goautomodels "go-admin/app/goauto/models"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
common "go-admin/common/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_, fileName, _, _ := runtime.Caller(0)
|
||||
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateDeviceHeartbeat)
|
||||
}
|
||||
|
||||
func migrateDeviceHeartbeat(db *gorm.DB, version string) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&goautomodels.AgentDevice{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&common.Migration{Version: version}).Error
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package version_local
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
goautomigrations "go-admin/app/goauto/migrations"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
common "go-admin/common/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// This separate version is required for databases that already applied the
|
||||
// initial GoAuto schema before task execution, result, reset and delete fields
|
||||
// were introduced. Re-running AutoMigrate under a new version is additive and
|
||||
// keeps existing task snapshots and results intact.
|
||||
func init() {
|
||||
_, fileName, _, _ := runtime.Caller(0)
|
||||
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateCollectionExecution)
|
||||
}
|
||||
|
||||
func migrateCollectionExecution(db *gorm.DB, version string) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := goautomigrations.Migrate(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&common.Migration{Version: version}).Error
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package version_local
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
goautomigrations "go-admin/app/goauto/migrations"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
common "go-admin/common/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// This migration adds the syb_session table, which caches one SYB ERP login so
|
||||
// a server restart does not force a fresh captcha round-trip (#48). It is
|
||||
// additive: no existing table is modified.
|
||||
//
|
||||
// `[必须]` A new version file is required for every model added to
|
||||
// goautomigrations.Migrate. Adding the model alone only affects databases
|
||||
// created from scratch — an existing database has already recorded the previous
|
||||
// version and will never re-run it, so the table silently never appears.
|
||||
func init() {
|
||||
_, fileName, _, _ := runtime.Caller(0)
|
||||
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateSYBSession)
|
||||
}
|
||||
|
||||
func migrateSYBSession(db *gorm.DB, version string) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := goautomigrations.Migrate(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&common.Migration{Version: version}).Error
|
||||
})
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
goautomigrations "go-admin/app/goauto/migrations"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
_ "go-admin/cmd/migrate/migration/version"
|
||||
_ "go-admin/cmd/migrate/migration/version-local"
|
||||
@@ -52,7 +54,7 @@ func run() {
|
||||
config.Setup(
|
||||
file.NewSource(file.WithPath(configYml)),
|
||||
ext.ApplyLocalConfig,
|
||||
ext.ApplyEnvironment,
|
||||
ext.ApplyEnvironment,
|
||||
initDB,
|
||||
)
|
||||
} else {
|
||||
@@ -88,14 +90,22 @@ func migrateModel() error {
|
||||
}
|
||||
migration.Migrate.SetDb(db.Debug())
|
||||
migration.Migrate.Migrate()
|
||||
return err
|
||||
// GoAuto 改动(#48):迁移跑完后核对 GoAuto 的表是否都在。
|
||||
// 新增模型时若忘了加 version-local 版本文件,迁移会「成功」但表不会建出来,
|
||||
// 直到运行时才炸成一句 Error 1146。这里让它在迁移阶段就报出表名。
|
||||
return goautomigrations.VerifyTables(db)
|
||||
}
|
||||
func initDB() {
|
||||
//3. 初始化数据库链接
|
||||
database.Setup()
|
||||
//4. 数据库迁移
|
||||
fmt.Println("数据库迁移开始")
|
||||
_ = migrateModel()
|
||||
// GoAuto 改动(#48):上游这里是 `_ = migrateModel()`,丢掉错误后无条件
|
||||
// 打印「成功」——迁移失败也会报成功,启动脚本据此认为一切正常。
|
||||
if err := migrateModel(); err != nil {
|
||||
fmt.Println("数据库迁移失败:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(`数据库基础数据初始化成功`)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user