feat: 保存并展示 PDD 店铺与价格采样信息 (#31)
This commit is contained in:
+72
-11
@@ -264,7 +264,16 @@ var migrations = [][]string{
|
||||
// 背景见 #20:v1 曾经被原地改写而不是新增版本,导致已经建过库的机器
|
||||
// (user_version 已经越过 v1)永远不会重跑改写后的语句,程序拿着一个
|
||||
// 和代码对不上的库静默启动。
|
||||
const schemaVersion = 3
|
||||
const schemaVersion = 4
|
||||
|
||||
// migrationV4 给 PDD 商品增加店铺名。
|
||||
//
|
||||
// v3 是特殊的 Go 迁移,不能塞进上面的纯 SQL migrations。v4 必须等 v3
|
||||
// 建好 pdd_products 后再执行,所以单独放在这里。已经发布的 v1/v2 原文
|
||||
// 保持不动,老库才能可靠地逐版升级。
|
||||
var migrationV4 = []string{
|
||||
`ALTER TABLE pdd_products ADD COLUMN shop_name TEXT;`,
|
||||
}
|
||||
|
||||
// Migrate 把数据库升到最新版本。
|
||||
// 已经是最新的就什么都不做,可以重复调用。
|
||||
@@ -309,15 +318,46 @@ func Migrate(db *sql.DB) error {
|
||||
// sku_mappings 结构),再由这一步收敛成最终结构——
|
||||
// 这样"全新库"和"老库升级"最终跑的是完全相同的 v3 代码,
|
||||
// 不需要分别维护两条路径。
|
||||
if reached < schemaVersion {
|
||||
if reached < 3 {
|
||||
if err := migrateV3(db); err != nil {
|
||||
return err
|
||||
}
|
||||
reached = 3
|
||||
}
|
||||
|
||||
// v4 是普通的追加列迁移,但必须排在特殊 v3 后面执行。
|
||||
if reached < 4 {
|
||||
if err := runSQLMigration(db, 4, migrationV4); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSQLMigration 在一个事务里执行指定版本的 SQL,并最后更新 user_version。
|
||||
// 它只接收本文件中写死的版本号和 SQL,不接收外部输入。
|
||||
func runSQLMigration(db *sql.DB, version int, statements []string) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始迁移 v%d 失败: %w", version, err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
for i, stmt := range statements {
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
return fmt.Errorf("执行迁移 v%d 第 %d 条语句失败: %w", version, i+1, err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", version)); err != nil {
|
||||
return fmt.Errorf("更新 user_version 到 %d 失败: %w", version, err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("提交迁移 v%d 失败: %w", version, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateV3 把库收敛成当前结构,对应工单 #20。
|
||||
//
|
||||
// # 起点不止一种,不能用 user_version 推断结构
|
||||
@@ -752,6 +792,13 @@ var requiredTables = []string{
|
||||
"idempotency_keys", "task_claims",
|
||||
}
|
||||
|
||||
// requiredColumns 只列出不能靠“表存在”发现的关键追加列。
|
||||
// shop_name 是 v4 新增列;缺少它时查询 PDD 页面会直接失败,因此启动时
|
||||
// 就应给出明确错误,而不是等操作员点到页面才暴露。
|
||||
var requiredColumns = map[string][]string{
|
||||
"pdd_products": {"shop_name"},
|
||||
}
|
||||
|
||||
// CheckSchema 在 Migrate 成功后调用,确认代码依赖的表都在。
|
||||
//
|
||||
// [必须] 缺表就返回错误,调用方要**拒绝启动**,不是打个警告继续跑。
|
||||
@@ -759,8 +806,8 @@ var requiredTables = []string{
|
||||
// 错误要等操作员点到那个页面才暴露——如果那是个写操作页面,
|
||||
// 暴露出来的就不是报错而是写坏数据。
|
||||
//
|
||||
// [建议] 只查表名,不逐列校验:够抓住"迁移没跑到、表没建出来"这一类问题,
|
||||
// 代价也低。真出了列级别的不一致,业务 SQL 跑起来自然会报错。
|
||||
// 表名全部检查;关键的追加列也检查,防止 user_version 已更新但迁移未完整
|
||||
// 落地时,程序拿着缺列的库继续启动。
|
||||
func CheckSchema(db *sql.DB) error {
|
||||
rows, err := db.Query(`SELECT name FROM sqlite_master WHERE type = 'table'`)
|
||||
if err != nil {
|
||||
@@ -786,13 +833,27 @@ func CheckSchema(db *sql.DB) error {
|
||||
missing = append(missing, t)
|
||||
}
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf(
|
||||
"数据库结构与本程序不匹配:缺少表 %s。\n"+
|
||||
"这通常是数据库比程序旧、而迁移没有覆盖到。\n"+
|
||||
"请备份 data/admin.db 后删除它让程序重建,或联系维护者。",
|
||||
strings.Join(missing, "、"))
|
||||
}
|
||||
|
||||
return fmt.Errorf(
|
||||
"数据库结构与本程序不匹配:缺少表 %s。\n"+
|
||||
"这通常是数据库比程序旧、而迁移没有覆盖到。\n"+
|
||||
"请备份 data/admin.db 后删除它让程序重建,或联系维护者。",
|
||||
strings.Join(missing, "、"))
|
||||
for table, columns := range requiredColumns {
|
||||
existingColumns, err := tableColumnSet(db, table)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查数据表 %s 的列失败: %w", table, err)
|
||||
}
|
||||
for _, column := range columns {
|
||||
if !existingColumns[column] {
|
||||
return fmt.Errorf(
|
||||
"数据库结构与本程序不匹配:数据表 %s 缺少列 %s。\n"+
|
||||
"这通常是数据库迁移没有完整执行,请备份 data/admin.db 后联系维护者。",
|
||||
table, column)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user