225 lines
6.0 KiB
Go
225 lines
6.0 KiB
Go
package database
|
||
|
||
import (
|
||
"database/sql"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
_ "github.com/mattn/go-sqlite3"
|
||
)
|
||
|
||
func Init(dbPath string) (*sql.DB, error) {
|
||
// 确保目录存在
|
||
dir := filepath.Dir(dbPath)
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if err := migrate(db); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return db, nil
|
||
}
|
||
|
||
func migrate(db *sql.DB) error {
|
||
schema := `
|
||
-- 用户表
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
username TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
nickname TEXT NOT NULL,
|
||
avatar_url TEXT DEFAULT '',
|
||
bio TEXT DEFAULT '',
|
||
is_admin INTEGER DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- 系统配置表
|
||
CREATE TABLE IF NOT EXISTS settings (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
);
|
||
|
||
-- 版本表
|
||
CREATE TABLE IF NOT EXISTS app_version (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
version_code INTEGER NOT NULL,
|
||
version_name TEXT NOT NULL,
|
||
download_url TEXT NOT NULL,
|
||
update_log TEXT DEFAULT '',
|
||
force_update INTEGER DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- 减重纪元表
|
||
CREATE TABLE IF NOT EXISTS weight_epochs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
name TEXT DEFAULT '',
|
||
initial_weight REAL NOT NULL,
|
||
target_weight REAL NOT NULL,
|
||
start_date DATE NOT NULL,
|
||
end_date DATE NOT NULL,
|
||
final_weight REAL,
|
||
is_active INTEGER DEFAULT 1,
|
||
is_completed INTEGER DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
);
|
||
|
||
-- 每周计划表
|
||
CREATE TABLE IF NOT EXISTS weekly_plans (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
epoch_id INTEGER NOT NULL,
|
||
user_id INTEGER NOT NULL,
|
||
year INTEGER NOT NULL,
|
||
week INTEGER NOT NULL,
|
||
start_date DATE NOT NULL,
|
||
end_date DATE NOT NULL,
|
||
initial_weight REAL,
|
||
target_weight REAL,
|
||
final_weight REAL,
|
||
note TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE(epoch_id, year, week),
|
||
FOREIGN KEY (epoch_id) REFERENCES weight_epochs(id),
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
);
|
||
|
||
-- 体重目标表 (保留兼容)
|
||
CREATE TABLE IF NOT EXISTS weight_goals (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
target_weight REAL NOT NULL,
|
||
start_date DATE NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
);
|
||
|
||
-- 体重记录表 (每周一条)
|
||
CREATE TABLE IF NOT EXISTS weight_records (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
weight REAL NOT NULL,
|
||
year INTEGER NOT NULL,
|
||
week INTEGER NOT NULL,
|
||
record_date DATE NOT NULL,
|
||
note TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE(user_id, year, week),
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
);
|
||
|
||
-- 运动打卡表
|
||
CREATE TABLE IF NOT EXISTS exercise_checkins (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
checkin_date DATE NOT NULL,
|
||
exercise_type TEXT NOT NULL,
|
||
body_part TEXT DEFAULT '',
|
||
duration INTEGER DEFAULT 0,
|
||
note TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
);
|
||
|
||
-- 索引
|
||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||
CREATE INDEX IF NOT EXISTS idx_weight_epochs_user ON weight_epochs(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_weekly_plans_epoch ON weekly_plans(epoch_id);
|
||
CREATE INDEX IF NOT EXISTS idx_weight_records_user ON weight_records(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_weight_records_week ON weight_records(year, week);
|
||
CREATE INDEX IF NOT EXISTS idx_exercise_checkins_user ON exercise_checkins(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_exercise_checkins_date ON exercise_checkins(checkin_date);
|
||
|
||
-- 初始化默认设置
|
||
INSERT OR IGNORE INTO settings (key, value) VALUES ('allow_register', 'true');
|
||
`
|
||
_, err := db.Exec(schema)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 迁移:移除 exercise_checkins 表的 UNIQUE 约束(如果存在)
|
||
// 检查是否有旧的 UNIQUE 约束,如果有则重建表
|
||
migrateExerciseCheckins(db)
|
||
|
||
return nil
|
||
}
|
||
|
||
// migrateExerciseCheckins 移除 exercise_checkins 表的 UNIQUE 约束并更新表结构
|
||
func migrateExerciseCheckins(db *sql.DB) {
|
||
// 检查表结构
|
||
var tableSql string
|
||
err := db.QueryRow("SELECT sql FROM sqlite_master WHERE type='table' AND name='exercise_checkins'").Scan(&tableSql)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 如果表定义中包含 image_url 或 UNIQUE,需要重建表
|
||
needsMigration := strings.Contains(tableSql, "image_url") || strings.Contains(tableSql, "UNIQUE")
|
||
if !needsMigration {
|
||
return
|
||
}
|
||
|
||
// 重建表以更新结构
|
||
tx, err := db.Begin()
|
||
if err != nil {
|
||
return
|
||
}
|
||
defer tx.Rollback()
|
||
|
||
// 1. 重命名旧表
|
||
_, err = tx.Exec("ALTER TABLE exercise_checkins RENAME TO exercise_checkins_old")
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 2. 创建新表(新结构)
|
||
_, err = tx.Exec(`
|
||
CREATE TABLE exercise_checkins (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
checkin_date DATE NOT NULL,
|
||
exercise_type TEXT NOT NULL DEFAULT 'aerobic',
|
||
body_part TEXT DEFAULT '',
|
||
duration INTEGER DEFAULT 0,
|
||
note TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
)
|
||
`)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 3. 复制数据(旧数据默认为有氧运动)
|
||
_, err = tx.Exec(`
|
||
INSERT INTO exercise_checkins (id, user_id, checkin_date, exercise_type, note, created_at)
|
||
SELECT id, user_id, checkin_date, 'aerobic', note, created_at FROM exercise_checkins_old
|
||
`)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 4. 删除旧表
|
||
_, err = tx.Exec("DROP TABLE exercise_checkins_old")
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 5. 重建索引
|
||
tx.Exec("CREATE INDEX IF NOT EXISTS idx_exercise_checkins_user ON exercise_checkins(user_id)")
|
||
tx.Exec("CREATE INDEX IF NOT EXISTS idx_exercise_checkins_date ON exercise_checkins(checkin_date)")
|
||
|
||
tx.Commit()
|
||
}
|