Files
healthflow/server/internal/database/database.go
2026-01-17 18:21:40 +08:00

225 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}