Spaces:
Sleeping
Sleeping
| const sqlite3 = require('sqlite3').verbose(); | |
| const { createClient } = require('@libsql/client'); | |
| const path = require('path'); | |
| // 检查是否使用 Turso(云端数据库) | |
| const USE_TURSO = !!(process.env.TURSO_DATABASE_URL && process.env.TURSO_AUTH_TOKEN); | |
| const DB_PATH = process.env.DB_PATH || './database/fitjourney.db'; | |
| let db = null; | |
| let tursoClient = null; | |
| const initDatabase = () => { | |
| return new Promise((resolve, reject) => { | |
| if (USE_TURSO) { | |
| // 使用 Turso 云端数据库 | |
| try { | |
| tursoClient = createClient({ | |
| url: process.env.TURSO_DATABASE_URL, | |
| authToken: process.env.TURSO_AUTH_TOKEN, | |
| }); | |
| console.log('✅ Connected to Turso database'); | |
| // 创建表 | |
| createTables() | |
| .then(() => resolve()) | |
| .catch(reject); | |
| } catch (error) { | |
| console.error('❌ Error connecting to Turso:', error.message); | |
| reject(error); | |
| } | |
| } else { | |
| // 使用本地 SQLite 数据库 | |
| const dbDir = path.dirname(DB_PATH); | |
| const fs = require('fs'); | |
| if (!fs.existsSync(dbDir)) { | |
| fs.mkdirSync(dbDir, { recursive: true }); | |
| } | |
| db = new sqlite3.Database(DB_PATH, (err) => { | |
| if (err) { | |
| console.error('❌ Error opening database:', err.message); | |
| reject(err); | |
| return; | |
| } | |
| console.log('✅ Connected to local SQLite database'); | |
| // 创建用户表 | |
| createTables() | |
| .then(() => resolve()) | |
| .catch(reject); | |
| }); | |
| } | |
| }); | |
| }; | |
| const createTables = () => { | |
| return new Promise((resolve, reject) => { | |
| const createUsersTable = ` | |
| CREATE TABLE IF NOT EXISTS users ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| email TEXT UNIQUE NOT NULL, | |
| password_hash TEXT NOT NULL, | |
| is_verified INTEGER DEFAULT 0, | |
| verification_code TEXT, | |
| verification_expires INTEGER, | |
| reset_token TEXT, | |
| reset_expires INTEGER, | |
| is_activated INTEGER DEFAULT 0, | |
| activation_expires INTEGER, | |
| created_at INTEGER DEFAULT (strftime('%s', 'now')), | |
| updated_at INTEGER DEFAULT (strftime('%s', 'now')) | |
| ) | |
| `; | |
| const createTokensTable = ` | |
| CREATE TABLE IF NOT EXISTS user_tokens ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| user_id INTEGER NOT NULL, | |
| token_hash TEXT NOT NULL, | |
| expires_at INTEGER NOT NULL, | |
| created_at INTEGER DEFAULT (strftime('%s', 'now')), | |
| FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE | |
| ) | |
| `; | |
| const createActivationCodesTable = ` | |
| CREATE TABLE IF NOT EXISTS activation_codes ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| code TEXT UNIQUE NOT NULL, | |
| days INTEGER NOT NULL DEFAULT 30, | |
| is_used INTEGER DEFAULT 0, | |
| used_by INTEGER, | |
| used_at INTEGER, | |
| created_at INTEGER DEFAULT (strftime('%s', 'now')), | |
| FOREIGN KEY (used_by) REFERENCES users (id) ON DELETE SET NULL | |
| ) | |
| `; | |
| // 教练表 | |
| const createCoachesTable = ` | |
| CREATE TABLE IF NOT EXISTS coaches ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| username TEXT UNIQUE NOT NULL, | |
| password_hash TEXT NOT NULL, | |
| password_plain TEXT, | |
| name TEXT, | |
| phone TEXT, | |
| is_active INTEGER DEFAULT 1, | |
| created_at DATETIME DEFAULT (datetime('now', 'localtime')), | |
| updated_at DATETIME DEFAULT (datetime('now', 'localtime')) | |
| ) | |
| `; | |
| // 教练邀请码表 | |
| const createInviteCodesTable = ` | |
| CREATE TABLE IF NOT EXISTS invite_codes ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| coach_id INTEGER NOT NULL, | |
| code TEXT UNIQUE NOT NULL, | |
| is_active INTEGER DEFAULT 1, | |
| created_at INTEGER DEFAULT (strftime('%s', 'now')), | |
| FOREIGN KEY (coach_id) REFERENCES coaches (id) ON DELETE CASCADE | |
| ) | |
| `; | |
| // 教练-学员关系表 | |
| const createTrainerStudentsTable = ` | |
| CREATE TABLE IF NOT EXISTS trainer_students ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| coach_id INTEGER NOT NULL, | |
| user_id INTEGER NOT NULL, | |
| student_name TEXT, | |
| joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, | |
| FOREIGN KEY (coach_id) REFERENCES coaches (id) ON DELETE CASCADE, | |
| FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, | |
| UNIQUE(coach_id, user_id) | |
| ) | |
| `; | |
| // 用户数据表(用于教练查看学员数据) | |
| const createUserDataTable = ` | |
| CREATE TABLE IF NOT EXISTS user_data ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| user_id INTEGER UNIQUE NOT NULL, | |
| diet_data TEXT, | |
| sleep_data TEXT, | |
| exercise_data TEXT, | |
| status_data TEXT, | |
| personal_data TEXT, | |
| updated_at DATETIME DEFAULT (datetime('now', 'localtime')), | |
| FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE | |
| ) | |
| `; | |
| // 迁移脚本:为现有的users表添加新列 | |
| const migrateUsersTable = () => { | |
| return new Promise((migrateResolve, migrateReject) => { | |
| // 检查是否存在is_activated列 | |
| db.all("PRAGMA table_info(users)", (err, columns) => { | |
| if (err) { | |
| console.error('❌ Error checking table info:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| const hasIsActivated = columns.some(col => col.name === 'is_activated'); | |
| const hasActivationExpires = columns.some(col => col.name === 'activation_expires'); | |
| let migrationCount = 0; | |
| let totalMigrations = 0; | |
| const checkMigrationComplete = () => { | |
| migrationCount++; | |
| if (migrationCount === totalMigrations) { | |
| migrateResolve(); | |
| } | |
| }; | |
| // 添加is_activated列 | |
| if (!hasIsActivated) { | |
| totalMigrations++; | |
| db.run("ALTER TABLE users ADD COLUMN is_activated INTEGER DEFAULT 0", (err) => { | |
| if (err) { | |
| console.error('❌ Error adding is_activated column:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| console.log('✅ Added is_activated column to users table'); | |
| checkMigrationComplete(); | |
| }); | |
| } | |
| // 添加activation_expires列 | |
| if (!hasActivationExpires) { | |
| totalMigrations++; | |
| db.run("ALTER TABLE users ADD COLUMN activation_expires INTEGER", (err) => { | |
| if (err) { | |
| console.error('❌ Error adding activation_expires column:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| console.log('✅ Added activation_expires column to users table'); | |
| checkMigrationComplete(); | |
| }); | |
| } | |
| // 如果没有需要迁移的列,直接完成 | |
| if (totalMigrations === 0) { | |
| console.log('✅ Users table migration not needed'); | |
| migrateResolve(); | |
| } | |
| }); | |
| }); | |
| }; | |
| // 迁移脚本:为教练表添加password_plain列 | |
| const migrateCoachesTable = () => { | |
| return new Promise((migrateResolve, migrateReject) => { | |
| db.all("PRAGMA table_info(coaches)", (err, columns) => { | |
| if (err) { | |
| console.error('❌ Error checking coaches table info:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| const hasPasswordPlain = columns.some(col => col.name === 'password_plain'); | |
| if (!hasPasswordPlain) { | |
| db.run("ALTER TABLE coaches ADD COLUMN password_plain TEXT", (err) => { | |
| if (err) { | |
| console.error('❌ Error adding password_plain column:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| console.log('✅ Added password_plain column to coaches table'); | |
| migrateResolve(); | |
| }); | |
| } else { | |
| console.log('✅ Coaches table migration not needed'); | |
| migrateResolve(); | |
| } | |
| }); | |
| }); | |
| }; | |
| // 迁移脚本:为trainer_students表添加student_name列 | |
| const migrateTrainerStudentsTable = () => { | |
| return new Promise((migrateResolve, migrateReject) => { | |
| db.all("PRAGMA table_info(trainer_students)", (err, columns) => { | |
| if (err) { | |
| console.error('❌ Error checking trainer_students table info:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| const hasStudentName = columns.some(col => col.name === 'student_name'); | |
| if (!hasStudentName) { | |
| db.run("ALTER TABLE trainer_students ADD COLUMN student_name TEXT", (err) => { | |
| if (err) { | |
| console.error('❌ Error adding student_name column:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| console.log('✅ Added student_name column to trainer_students table'); | |
| migrateResolve(); | |
| }); | |
| } else { | |
| console.log('✅ Trainer_students table migration not needed'); | |
| migrateResolve(); | |
| } | |
| }); | |
| }); | |
| }; | |
| const migrateUserDataTable = () => { | |
| return new Promise((migrateResolve, migrateReject) => { | |
| db.all("PRAGMA table_info(user_data)", (err, columns) => { | |
| if (err) { | |
| console.error('❌ Error checking user_data table info:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| const hasStatusData = columns.some(col => col.name === 'status_data'); | |
| const hasPersonalData = columns.some(col => col.name === 'personal_data'); | |
| let migrationCount = 0; | |
| let totalMigrations = 0; | |
| const checkMigrationComplete = () => { | |
| migrationCount++; | |
| if (migrationCount === totalMigrations) { | |
| migrateResolve(); | |
| } | |
| }; | |
| if (!hasStatusData) { | |
| totalMigrations++; | |
| } | |
| if (!hasPersonalData) { | |
| totalMigrations++; | |
| } | |
| if (totalMigrations === 0) { | |
| console.log('✅ User_data table migration not needed'); | |
| migrateResolve(); | |
| return; | |
| } | |
| if (!hasStatusData) { | |
| db.run("ALTER TABLE user_data ADD COLUMN status_data TEXT", (err) => { | |
| if (err) { | |
| console.error('❌ Error adding status_data column:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| console.log('✅ Added status_data column to user_data table'); | |
| checkMigrationComplete(); | |
| }); | |
| } | |
| if (!hasPersonalData) { | |
| db.run("ALTER TABLE user_data ADD COLUMN personal_data TEXT", (err) => { | |
| if (err) { | |
| console.error('❌ Error adding personal_data column:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| console.log('✅ Added personal_data column to user_data table'); | |
| checkMigrationComplete(); | |
| }); | |
| } | |
| }); | |
| }); | |
| }; | |
| // 迁移 user_data 表的时间戳从UTC转换为本地时间(东八区) | |
| const migrateUserDataTimestamp = () => { | |
| return new Promise((migrateResolve, migrateReject) => { | |
| // 更新所有现有记录的 updated_at,将UTC时间转换为东八区时间 (+8小时) | |
| db.run( | |
| `UPDATE user_data | |
| SET updated_at = datetime(updated_at, '+8 hours') | |
| WHERE updated_at IS NOT NULL | |
| AND updated_at NOT LIKE '%+%'`, | |
| (err) => { | |
| if (err) { | |
| console.error('❌ Error migrating user_data timestamps:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| console.log('✅ Migrated user_data timestamps to local time (UTC+8)'); | |
| migrateResolve(); | |
| } | |
| ); | |
| }); | |
| }; | |
| // 修正 coaches 表的时间戳(如果之前加了 12 小时,现在改为只加 8 小时) | |
| const fixCoachesTimezone = () => { | |
| return new Promise((migrateResolve, migrateReject) => { | |
| // 检查是否有 DATETIME 格式的数据(已经被迁移过的) | |
| db.get( | |
| `SELECT created_at FROM coaches WHERE created_at > '2025-01-01' LIMIT 1`, | |
| (err, row) => { | |
| if (err) { | |
| console.error('❌ Error checking coaches timestamps:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| if (row && row.created_at) { | |
| // 如果时间看起来比正常时间快(即多加了4小时),则修正 | |
| // 这里我们减去4小时,将 UTC+12 改为 UTC+8 | |
| db.run( | |
| `UPDATE coaches | |
| SET created_at = datetime(created_at, '-4 hours'), | |
| updated_at = datetime(updated_at, '-4 hours') | |
| WHERE created_at > datetime('now', 'localtime', '+1 hour')`, | |
| (err) => { | |
| if (err) { | |
| console.error('❌ Error fixing coaches timezone:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| console.log('✅ Fixed coaches timezone (UTC+12 -> UTC+8)'); | |
| migrateResolve(); | |
| } | |
| ); | |
| } else { | |
| console.log('✅ Coaches timezone fix not needed'); | |
| migrateResolve(); | |
| } | |
| } | |
| ); | |
| }); | |
| }; | |
| const migrateCoachesTimestamp = () => { | |
| return new Promise((migrateResolve, migrateReject) => { | |
| // 检查coaches表的created_at字段类型 | |
| db.all("PRAGMA table_info(coaches)", (err, columns) => { | |
| if (err) { | |
| console.error('❌ Error checking coaches table info:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| const createdAtCol = columns.find(col => col.name === 'created_at'); | |
| // 如果created_at是INTEGER类型,需要转换 | |
| if (createdAtCol && createdAtCol.type === 'INTEGER') { | |
| console.log('🔄 Migrating coaches table timestamp fields from INTEGER to DATETIME...'); | |
| // SQLite不支持直接修改列类型,需要重建表 | |
| db.serialize(() => { | |
| // 1. 创建新表 | |
| db.run(` | |
| CREATE TABLE coaches_new ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| username TEXT UNIQUE NOT NULL, | |
| password_hash TEXT NOT NULL, | |
| password_plain TEXT, | |
| name TEXT, | |
| phone TEXT, | |
| is_active INTEGER DEFAULT 1, | |
| created_at DATETIME DEFAULT CURRENT_TIMESTAMP, | |
| updated_at DATETIME DEFAULT CURRENT_TIMESTAMP | |
| ) | |
| `, (err) => { | |
| if (err) { | |
| console.error('❌ Error creating new coaches table:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| // 2. 复制数据,将INTEGER时间戳转换为DATETIME(服务器已在东八区,使用localtime即可) | |
| db.run(` | |
| INSERT INTO coaches_new (id, username, password_hash, password_plain, name, phone, is_active, created_at, updated_at) | |
| SELECT id, username, password_hash, password_plain, name, phone, is_active, | |
| datetime(created_at, 'unixepoch', 'localtime'), | |
| datetime(updated_at, 'unixepoch', 'localtime') | |
| FROM coaches | |
| `, (err) => { | |
| if (err) { | |
| console.error('❌ Error copying data to new coaches table:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| // 3. 删除旧表 | |
| db.run('DROP TABLE coaches', (err) => { | |
| if (err) { | |
| console.error('❌ Error dropping old coaches table:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| // 4. 重命名新表 | |
| db.run('ALTER TABLE coaches_new RENAME TO coaches', (err) => { | |
| if (err) { | |
| console.error('❌ Error renaming new coaches table:', err.message); | |
| migrateReject(err); | |
| return; | |
| } | |
| console.log('✅ Coaches table timestamp migration completed'); | |
| migrateResolve(); | |
| }); | |
| }); | |
| }); | |
| }); | |
| }); | |
| } else { | |
| console.log('✅ Coaches table timestamp migration not needed'); | |
| migrateResolve(); | |
| } | |
| }); | |
| }); | |
| }; | |
| // 使用统一的方式执行SQL(支持 Turso 和 SQLite) | |
| const executeSQL = async (sql, errorMsg, successMsg) => { | |
| try { | |
| await runQuery(sql); | |
| console.log(successMsg); | |
| } catch (err) { | |
| console.error(errorMsg, err.message); | |
| throw err; | |
| } | |
| }; | |
| // 按顺序创建所有表 | |
| (async () => { | |
| try { | |
| await executeSQL(createUsersTable, '❌ Error creating users table:', '✅ Users table ready'); | |
| await executeSQL(createTokensTable, '❌ Error creating tokens table:', '✅ Tokens table ready'); | |
| await executeSQL(createActivationCodesTable, '❌ Error creating activation codes table:', '✅ Activation codes table ready'); | |
| await executeSQL(createCoachesTable, '❌ Error creating coaches table:', '✅ Coaches table ready'); | |
| await executeSQL(createInviteCodesTable, '❌ Error creating invite codes table:', '✅ Invite codes table ready'); | |
| await executeSQL(createTrainerStudentsTable, '❌ Error creating trainer_students table:', '✅ Trainer students table ready'); | |
| await executeSQL(createUserDataTable, '❌ Error creating user_data table:', '✅ User data table ready'); | |
| // 只在本地 SQLite 时执行迁移(Turso 不需要) | |
| if (!USE_TURSO) { | |
| await migrateUsersTable(); | |
| await migrateCoachesTable(); | |
| await migrateTrainerStudentsTable(); | |
| await migrateUserDataTable(); | |
| await migrateUserDataTimestamp(); | |
| await migrateCoachesTimestamp(); | |
| await fixCoachesTimezone(); | |
| } | |
| console.log('✅ Database migration completed'); | |
| resolve(); | |
| } catch (error) { | |
| reject(error); | |
| } | |
| })(); | |
| }); | |
| }; | |
| const getDatabase = () => { | |
| if (USE_TURSO) { | |
| if (!tursoClient) { | |
| throw new Error('Turso database not initialized. Call initDatabase() first.'); | |
| } | |
| return tursoClient; | |
| } else { | |
| if (!db) { | |
| throw new Error('Database not initialized. Call initDatabase() first.'); | |
| } | |
| return db; | |
| } | |
| }; | |
| // 辅助函数:执行SQL查询(支持 Turso 和 SQLite) | |
| const runQuery = async (sql, params = []) => { | |
| if (USE_TURSO) { | |
| try { | |
| const result = await tursoClient.execute({ sql, args: params }); | |
| return { | |
| id: result.lastInsertRowid ? Number(result.lastInsertRowid) : null, | |
| changes: result.rowsAffected | |
| }; | |
| } catch (error) { | |
| throw error; | |
| } | |
| } else { | |
| return new Promise((resolve, reject) => { | |
| db.run(sql, params, function(err) { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve({ id: this.lastID, changes: this.changes }); | |
| } | |
| }); | |
| }); | |
| } | |
| }; | |
| const getQuery = async (sql, params = []) => { | |
| if (USE_TURSO) { | |
| try { | |
| const result = await tursoClient.execute({ sql, args: params }); | |
| return result.rows[0] || undefined; | |
| } catch (error) { | |
| throw error; | |
| } | |
| } else { | |
| return new Promise((resolve, reject) => { | |
| db.get(sql, params, (err, row) => { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(row); | |
| } | |
| }); | |
| }); | |
| } | |
| }; | |
| const allQuery = async (sql, params = []) => { | |
| if (USE_TURSO) { | |
| try { | |
| const result = await tursoClient.execute({ sql, args: params }); | |
| return result.rows || []; | |
| } catch (error) { | |
| throw error; | |
| } | |
| } else { | |
| return new Promise((resolve, reject) => { | |
| db.all(sql, params, (err, rows) => { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| resolve(rows); | |
| } | |
| }); | |
| }); | |
| } | |
| }; | |
| module.exports = { | |
| initDatabase, | |
| getDatabase, | |
| runQuery, | |
| getQuery, | |
| allQuery | |
| }; | |