mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
publish: 1.0.0 snapshot (bad2ec1)
channel: master version: 1.0.0 source-ref: master published-at-utc: 2026-03-13T15:19:28Z
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// DB 数据库连接
|
||||
type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
// migration 单个版本迁移
|
||||
type migration struct {
|
||||
version int // 版本号,单调递增,永不修改
|
||||
desc string // 描述,便于日志追踪
|
||||
stmts []string
|
||||
}
|
||||
|
||||
// migrations 所有版本迁移,按 version 升序排列
|
||||
// 规则:
|
||||
// - 只能追加新版本,绝对不能修改已有版本
|
||||
// - version 从 1 开始,每次发布新版本时递增
|
||||
// - 每个 version 对应一批幂等的 DDL 语句
|
||||
var migrations = []migration{
|
||||
{
|
||||
version: 1,
|
||||
desc: "初始化核心表结构",
|
||||
stmts: []string{
|
||||
`CREATE TABLE IF NOT EXISTS launch_codes (
|
||||
profile_id TEXT PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_launch_codes_code ON launch_codes(code)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS browser_profiles (
|
||||
profile_id TEXT PRIMARY KEY,
|
||||
profile_name TEXT NOT NULL,
|
||||
user_data_dir TEXT NOT NULL DEFAULT '',
|
||||
core_id TEXT NOT NULL DEFAULT '',
|
||||
fingerprint_args TEXT NOT NULL DEFAULT '[]',
|
||||
proxy_id TEXT NOT NULL DEFAULT '',
|
||||
proxy_config TEXT NOT NULL DEFAULT '',
|
||||
launch_args TEXT NOT NULL DEFAULT '[]',
|
||||
tags TEXT NOT NULL DEFAULT '[]',
|
||||
keywords TEXT NOT NULL DEFAULT '[]',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_browser_profiles_created_at ON browser_profiles(created_at)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS browser_proxies (
|
||||
proxy_id TEXT PRIMARY KEY,
|
||||
proxy_name TEXT NOT NULL,
|
||||
proxy_config TEXT NOT NULL,
|
||||
dns_servers TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS browser_cores (
|
||||
core_id TEXT PRIMARY KEY,
|
||||
core_name TEXT NOT NULL,
|
||||
core_path TEXT NOT NULL,
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS browser_bookmarks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL UNIQUE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 2,
|
||||
desc: "添加实例分组支持",
|
||||
stmts: []string{
|
||||
`CREATE TABLE IF NOT EXISTS browser_groups (
|
||||
group_id TEXT PRIMARY KEY,
|
||||
group_name TEXT NOT NULL,
|
||||
parent_id TEXT DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_browser_groups_parent_id ON browser_groups(parent_id)`,
|
||||
`ALTER TABLE browser_profiles ADD COLUMN group_id TEXT DEFAULT ''`,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 3,
|
||||
desc: "代理表添加分组和测速字段",
|
||||
stmts: []string{
|
||||
`ALTER TABLE browser_proxies ADD COLUMN group_name TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE browser_proxies ADD COLUMN last_latency_ms INTEGER NOT NULL DEFAULT -1`,
|
||||
`ALTER TABLE browser_proxies ADD COLUMN last_test_ok INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE browser_proxies ADD COLUMN last_tested_at TEXT NOT NULL DEFAULT ''`,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 4,
|
||||
desc: "代理表添加 IP 健康结果字段",
|
||||
stmts: []string{
|
||||
`ALTER TABLE browser_proxies ADD COLUMN last_ip_health_json TEXT NOT NULL DEFAULT ''`,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 5,
|
||||
desc: "代理表添加 URL 来源与自动刷新字段",
|
||||
stmts: []string{
|
||||
`ALTER TABLE browser_proxies ADD COLUMN source_id TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE browser_proxies ADD COLUMN source_url TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE browser_proxies ADD COLUMN source_name_prefix TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE browser_proxies ADD COLUMN source_auto_refresh INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE browser_proxies ADD COLUMN source_refresh_interval_m INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE browser_proxies ADD COLUMN source_last_refresh_at TEXT NOT NULL DEFAULT ''`,
|
||||
},
|
||||
},
|
||||
// ── 新版本在此追加,格式:
|
||||
// {
|
||||
// version: 4,
|
||||
// desc: "描述本次变更",
|
||||
// stmts: []string{
|
||||
// `ALTER TABLE xxx ADD COLUMN yyy TEXT NOT NULL DEFAULT ''`,
|
||||
// },
|
||||
// },
|
||||
}
|
||||
|
||||
// NewDB 创建新的数据库连接
|
||||
func NewDB(dbPath string) (*DB, error) {
|
||||
conn, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开数据库失败: %w", err)
|
||||
}
|
||||
|
||||
conn.SetMaxOpenConns(1)
|
||||
conn.SetMaxIdleConns(1)
|
||||
|
||||
if err := conn.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("连接数据库失败: %w", err)
|
||||
}
|
||||
|
||||
// WAL 模式:写不阻塞读
|
||||
if _, err := conn.Exec(`PRAGMA journal_mode=WAL`); err != nil {
|
||||
return nil, fmt.Errorf("设置 WAL 模式失败: %w", err)
|
||||
}
|
||||
// 开启外键约束
|
||||
if _, err := conn.Exec(`PRAGMA foreign_keys=ON`); err != nil {
|
||||
return nil, fmt.Errorf("开启外键约束失败: %w", err)
|
||||
}
|
||||
|
||||
return &DB{conn: conn}, nil
|
||||
}
|
||||
|
||||
// GetConn 获取数据库连接
|
||||
func (db *DB) GetConn() *sql.DB {
|
||||
return db.conn
|
||||
}
|
||||
|
||||
// Close 关闭数据库连接
|
||||
func (db *DB) Close() error {
|
||||
if db.conn != nil {
|
||||
return db.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate 执行版本化迁移
|
||||
// 原理:维护 schema_migrations 表记录已执行版本,每次启动只执行未执行的版本
|
||||
func (db *DB) Migrate() error {
|
||||
// 确保版本记录表存在
|
||||
if _, err := db.conn.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
desc TEXT NOT NULL DEFAULT '',
|
||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`); err != nil {
|
||||
return fmt.Errorf("创建 schema_migrations 表失败: %w", err)
|
||||
}
|
||||
|
||||
// 查询已执行的最大版本号
|
||||
var currentVersion int
|
||||
row := db.conn.QueryRow(`SELECT COALESCE(MAX(version), 0) FROM schema_migrations`)
|
||||
if err := row.Scan(¤tVersion); err != nil {
|
||||
return fmt.Errorf("查询当前 schema 版本失败: %w", err)
|
||||
}
|
||||
|
||||
// 按版本顺序执行未执行的迁移
|
||||
for _, m := range migrations {
|
||||
if m.version <= currentVersion {
|
||||
continue // 已执行,跳过
|
||||
}
|
||||
|
||||
// 每个版本在事务内执行,保证原子性
|
||||
if err := db.applyMigration(m); err != nil {
|
||||
return fmt.Errorf("迁移版本 %d (%s) 失败: %w", m.version, m.desc, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyMigration 在事务内执行单个版本的所有语句,并记录版本号
|
||||
func (db *DB) applyMigration(m migration) error {
|
||||
tx, err := db.conn.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开启事务失败: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
for _, stmt := range m.stmts {
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
// ALTER TABLE 添加已存在列时忽略(兼容从旧版本直接升级的情况)
|
||||
if isColumnExistsError(err) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("执行语句失败 [%s]: %w", truncate(stmt, 60), err)
|
||||
}
|
||||
}
|
||||
|
||||
// 记录版本号
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO schema_migrations (version, desc) VALUES (?, ?)`,
|
||||
m.version, m.desc,
|
||||
); err != nil {
|
||||
return fmt.Errorf("记录迁移版本失败: %w", err)
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// isColumnExistsError 检查是否是列已存在的错误(SQLite 错误信息)
|
||||
func isColumnExistsError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
s := err.Error()
|
||||
return strings.Contains(s, "duplicate column") || strings.Contains(s, "already exists")
|
||||
}
|
||||
|
||||
// truncate 截断字符串用于日志展示
|
||||
func truncate(s string, n int) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user