fix(goauto): create syb_product_filter on existing databases #269

Three defects that only a real MySQL run could show.

The table had no version-local migration. Adding the model to
migrations.Migrate covers databases built from scratch, but every existing
one already has the older versions recorded in sys_migration, so that list
never runs again there. cmd/migrate's post-migration table check caught it:
"迁移后仍缺少表:syb_product_filter". The new version file creates the table
and seeds the same eight rules with FirstOrCreate on the normalized key, so
running both paths against one database cannot duplicate them.

Keyword and NormalizedKeyword were typed text, which MySQL refuses to put in
a key specification without a prefix length (Error 1170), so the unique index
could not be built at all. They are sized 200 now, matching how SYBShop sizes
its own matching key. SQLite accepts text in an index, which is why the whole
test suite passed while the real migration failed.

The 只看启用 filter is removed rather than kept: hiding a disabled structural
rule would conceal the one state on this page that matters most, since a
disabled "#" means roughly 65% of 档口 rows start entering the system again.
Keyword search stays.

Verified against the local MySQL database: migration applies, eight rules
present with traditional Chinese preserved (印花大學T normalizes to 印花大學t),
menu row created under 采采管理.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
This commit is contained in:
QiuSW
2026-09-11 17:00:11 +08:00
co-authored by Claude Opus 5
parent cad9f6d185
commit 503b3cbbc5
3 changed files with 74 additions and 11 deletions
+8 -4
View File
@@ -605,10 +605,14 @@ func (SYBShop) TableName() string { return "syb_shop" }
// rows are operator-maintained. DeletedFlag keeps the composite unique key
// reusable after a keyword is soft-deleted.
type SYBProductFilter struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
Kind string `json:"kind" gorm:"size:16;not null;index;uniqueIndex:ux_syb_product_filter_key,priority:1;check:ck_syb_product_filter_kind,kind IN ('char','keyword')"`
Keyword string `json:"keyword" gorm:"type:text;not null"`
NormalizedKeyword string `json:"normalizedKeyword" gorm:"type:text;not null;uniqueIndex:ux_syb_product_filter_key,priority:2"`
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
Kind string `json:"kind" gorm:"size:16;not null;index;uniqueIndex:ux_syb_product_filter_key,priority:1;check:ck_syb_product_filter_kind,kind IN ('char','keyword')"`
// Keyword and NormalizedKeyword are sized, not text: MySQL refuses a
// BLOB/TEXT column in a key specification without a prefix length, so the
// unique index below cannot be built on a text column (Error 1170). SQLite
// accepts either, which is why only a real MySQL migration surfaces this.
Keyword string `json:"keyword" gorm:"size:200;not null"`
NormalizedKeyword string `json:"normalizedKeyword" gorm:"size:200;not null;uniqueIndex:ux_syb_product_filter_key,priority:2"`
Enabled bool `json:"enabled" gorm:"not null;default:true"`
LastHitAt *time.Time `json:"lastHitAt"`
LastHitCount *int `json:"lastHitCount"`
@@ -0,0 +1,63 @@
package version_local
import (
"runtime"
"go-admin/app/goauto/models"
"go-admin/app/goauto/sybshop"
"go-admin/cmd/migrate/migration"
common "go-admin/common/models"
"gorm.io/gorm"
)
func init() {
_, file, _, _ := runtime.Caller(0)
migration.Migrate.SetVersion(migration.GetFilename(file), migrateSYBProductFilterTable)
}
// migrateSYBProductFilterTable creates syb_product_filter and seeds its rules
// for #269.
//
// `[必须]` Adding the model to migrations.Migrate only covers databases created
// from scratch. Every existing database already has the older version numbers
// recorded in sys_migration, so that list never runs again there — a new model
// needs its own version file, which is what the post-migration table check in
// cmd/migrate enforces.
//
// The seed is deliberately the same set the fresh-database path writes, created
// with FirstOrCreate on the normalized key so running both paths against one
// database cannot produce duplicates.
func migrateSYBProductFilterTable(db *gorm.DB, version string) error {
if err := db.AutoMigrate(&models.SYBProductFilter{}); err != nil {
return err
}
return db.Transaction(func(tx *gorm.DB) error {
// 结构判据 is what defines a 档口 product; the keywords are the forward
// safety net for the day those codes stop carrying "#" (#269).
seed := []struct{ kind, keyword string }{
{"char", "-"},
{"char", "#"},
{"keyword", "CVC短袖220斤"},
{"keyword", "180克純棉圓領短袖"},
{"keyword", "拼色純棉短袖"},
{"keyword", "300斤牛奶絲圓領"},
{"keyword", "印花大學T"},
{"keyword", "印花帽T"},
}
for _, item := range seed {
norm := sybshop.Normalize(item.keyword)
row := models.SYBProductFilter{
Kind: item.kind,
Keyword: item.keyword,
NormalizedKeyword: norm,
Enabled: true,
}
if err := tx.Where("kind = ? AND normalized_keyword = ?", item.kind, norm).
FirstOrCreate(&row).Error; err != nil {
return err
}
}
return tx.Create(&common.Migration{Version: version}).Error
})
}
@@ -6,9 +6,6 @@
<el-form-item label="搜索">
<el-input v-model="query.keyword" clearable placeholder="字符或关键词" @keyup.enter="load" />
</el-form-item>
<el-form-item label="只看启用">
<el-switch v-model="query.enabledOnly" @change="load" />
</el-form-item>
<el-form-item>
<el-button @click="reset">重置</el-button>
</el-form-item>
@@ -163,7 +160,7 @@ export default {
loading: false,
chars: [],
keywords: [],
query: { keyword: '', enabledOnly: false },
query: { keyword: '' },
editor: { open: false, saving: false, keyword: '', note: '' }
}
},
@@ -182,8 +179,7 @@ export default {
const res = await listSybProductFilters({
page: 1,
pageSize: 200,
keyword: this.query.keyword || undefined,
enabledOnly: this.query.enabledOnly || undefined
keyword: this.query.keyword || undefined
})
const rows = (res.data.items || []).map(row => ({ ...row, updating: false }))
this.chars = rows.filter(row => row.kind === 'char')
@@ -193,7 +189,7 @@ export default {
}
},
reset() {
this.query = { keyword: '', enabledOnly: false }
this.query = { keyword: '' }
this.load()
},
formatTime(value) {