From e6f533b71e47ab8fda3f4c8ff672185d282b09fe Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Thu, 20 Aug 2026 10:08:45 +0800 Subject: [PATCH] fix(#48): add the missing version-local migration for syb_session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/05-common-changes.md | 19 +++++++- server/.gitignore | 5 ++- server/app/goauto/migrations/migrate_test.go | 26 +++++++++++ server/app/goauto/migrations/verify.go | 44 +++++++++++++++++++ .../1786700000000_goauto_schema.go | 25 +++++++++++ .../1786700100000_device_registration.go | 25 +++++++++++ .../1786700200000_device_heartbeat.go | 25 +++++++++++ .../1786700300000_collection_execution.go | 29 ++++++++++++ .../1786700800000_syb_session.go | 33 ++++++++++++++ server/cmd/migrate/server.go | 16 +++++-- 10 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 server/app/goauto/migrations/verify.go create mode 100644 server/cmd/migrate/migration/version-local/1786700000000_goauto_schema.go create mode 100644 server/cmd/migrate/migration/version-local/1786700100000_device_registration.go create mode 100644 server/cmd/migrate/migration/version-local/1786700200000_device_heartbeat.go create mode 100644 server/cmd/migrate/migration/version-local/1786700300000_collection_execution.go create mode 100644 server/cmd/migrate/migration/version-local/1786700800000_syb_session.go diff --git a/docs/05-common-changes.md b/docs/05-common-changes.md index a4b93a9..7186e2d 100644 --- a/docs/05-common-changes.md +++ b/docs/05-common-changes.md @@ -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 # 常见修改指南 @@ -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`。 diff --git a/server/.gitignore b/server/.gitignore index 49dfc2e..9ecd5ca 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -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 diff --git a/server/app/goauto/migrations/migrate_test.go b/server/app/goauto/migrations/migrate_test.go index 8de548d..3ef859b 100644 --- a/server/app/goauto/migrations/migrate_test.go +++ b/server/app/goauto/migrations/migrate_test.go @@ -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) + } +} diff --git a/server/app/goauto/migrations/verify.go b/server/app/goauto/migrations/verify.go new file mode 100644 index 0000000..bc6aeb9 --- /dev/null +++ b/server/app/goauto/migrations/verify.go @@ -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, "、")) +} diff --git a/server/cmd/migrate/migration/version-local/1786700000000_goauto_schema.go b/server/cmd/migrate/migration/version-local/1786700000000_goauto_schema.go new file mode 100644 index 0000000..a2e7592 --- /dev/null +++ b/server/cmd/migrate/migration/version-local/1786700000000_goauto_schema.go @@ -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 + }) +} diff --git a/server/cmd/migrate/migration/version-local/1786700100000_device_registration.go b/server/cmd/migrate/migration/version-local/1786700100000_device_registration.go new file mode 100644 index 0000000..f64eec0 --- /dev/null +++ b/server/cmd/migrate/migration/version-local/1786700100000_device_registration.go @@ -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 + }) +} diff --git a/server/cmd/migrate/migration/version-local/1786700200000_device_heartbeat.go b/server/cmd/migrate/migration/version-local/1786700200000_device_heartbeat.go new file mode 100644 index 0000000..cd41efe --- /dev/null +++ b/server/cmd/migrate/migration/version-local/1786700200000_device_heartbeat.go @@ -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 + }) +} diff --git a/server/cmd/migrate/migration/version-local/1786700300000_collection_execution.go b/server/cmd/migrate/migration/version-local/1786700300000_collection_execution.go new file mode 100644 index 0000000..a8f1f56 --- /dev/null +++ b/server/cmd/migrate/migration/version-local/1786700300000_collection_execution.go @@ -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 + }) +} diff --git a/server/cmd/migrate/migration/version-local/1786700800000_syb_session.go b/server/cmd/migrate/migration/version-local/1786700800000_syb_session.go new file mode 100644 index 0000000..9e1b8ea --- /dev/null +++ b/server/cmd/migrate/migration/version-local/1786700800000_syb_session.go @@ -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 + }) +} diff --git a/server/cmd/migrate/server.go b/server/cmd/migrate/server.go index 4f31e25..551c060 100644 --- a/server/cmd/migrate/server.go +++ b/server/cmd/migrate/server.go @@ -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(`数据库基础数据初始化成功`) }