diff --git a/.gitignore b/.gitignore index 11ddd8d..402f0b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules # Keep environment variables out of version control .env +prisma/database/data diff --git a/batchMigrate.js b/batchMigrate.js new file mode 100644 index 0000000..3668b84 --- /dev/null +++ b/batchMigrate.js @@ -0,0 +1,192 @@ +#!/usr/bin/env node +import fs from 'fs'; +import path from 'path'; +import { execSync } from 'child_process'; +import dotenv from 'dotenv'; + +// 加载环境变量 +dotenv.config(); + +const PRISMA_DIR = path.join(process.cwd(), 'prisma'); +const DATABASE_DIR = path.join(PRISMA_DIR, 'database'); +const MIGRATIONS_DIR = path.join(PRISMA_DIR, 'migrations'); + +// 数据库 URL 环境变量映射 +const DB_URL_VARS = { + mysql: 'MYSQL_DATABASE_URL', + postgres: 'PG_DATABASE_URL' +}; + +function copyDirectory(source, destination) { + // 如果目标目录不存在,创建它 + if (!fs.existsSync(destination)) { + fs.mkdirSync(destination, { recursive: true }); + } + + // 读取源目录中的所有内容 + const items = fs.readdirSync(source); + + for (const item of items) { + const sourcePath = path.join(source, item); + const destPath = path.join(destination, item); + + const stats = fs.statSync(sourcePath); + if (stats.isDirectory()) { + // 如果是目录,递归复制 + copyDirectory(sourcePath, destPath); + } else { + // 如果是文件,直接复制 + fs.copyFileSync(sourcePath, destPath); + } + } +} + +function deleteMigrationsDir() { + if (fs.existsSync(MIGRATIONS_DIR)) { + console.log('🗑️ 删除现有的 migrations 目录...'); + fs.rmSync(MIGRATIONS_DIR, { recursive: true, force: true }); + } +} + +// 修改 schema 文件中的数据库配置 +function updateSchemaConfig(schemaPath, dbType) { + console.log(`📝 更新 schema 文件配置...`); + + // 读取原始内容 + let content = fs.readFileSync(schemaPath, 'utf8'); + const originalContent = content; + + if (dbType === 'sqlite') { + // 修改 SQLite 数据库路径为 ../../data/db.db(用于迁移) + content = content.replace( + /url\s*=\s*"file:..\/data\/db.db"/, + 'url = "file:../../data/db.db"' + ); + } else { + // 获取对应的环境变量名 + const urlEnvVar = DB_URL_VARS[dbType]; + if (!urlEnvVar) { + throw new Error(`未找到 ${dbType} 的数据库 URL 环境变量映射`); + } + + // 替换 env("DATABASE_URL") 为对应的环境变量 + content = content.replace( + /env\s*\(\s*"DATABASE_URL"\s*\)/, + `env("${urlEnvVar}")` + ); + } + + // 写入修改后的内容 + fs.writeFileSync(schemaPath, content, 'utf8'); + + return originalContent; +} + +// 恢复 schema 文件的原始内容,对于 SQLite 恢复为 ../data/db.db +function restoreSchema(schemaPath, dbType, originalContent) { + if (originalContent) { + console.log(`📝 恢复 schema 文件的原始内容...`); + if (dbType === 'sqlite') { + // 确保恢复为 ../data/db.db + let content = originalContent; + if (content.includes('../../data/db.db')) { + content = content.replace( + /url\s*=\s*"file:..\/..\/data\/db.db"/, + 'url = "file:../data/db.db"' + ); + } + fs.writeFileSync(schemaPath, content, 'utf8'); + } else { + fs.writeFileSync(schemaPath, originalContent, 'utf8'); + } + } +} + +async function processDatabaseType(dbType) { + const schemaPath = path.join(DATABASE_DIR, dbType, 'schema.prisma'); + const dbMigrationsDir = path.join(DATABASE_DIR, dbType, 'migrations'); + + if (!fs.existsSync(schemaPath)) { + console.log(`⚠️ 跳过 ${dbType}: schema.prisma 文件不存在`); + return; + } + + let originalContent; + try { + console.log(`\n🔄 处理 ${dbType} 数据库迁移...`); + + // 删除旧的迁移目录 + deleteMigrationsDir(); + + // 修改 schema 文件配置 + originalContent = updateSchemaConfig(schemaPath, dbType); + + // 先尝试部署现有迁移 + console.log(`📦 部署现有迁移...`); + try { + execSync(`npx prisma migrate deploy --schema=${schemaPath}`, { + stdio: 'inherit' + }); + } catch (error) { + console.log(`⚠️ 部署现有迁移失败,将创建新迁移`); + } + + // 执行新迁移 + console.log(`📦 创建新迁移...`); + execSync(`npx prisma migrate dev --name ${new Date().toISOString().split('T')[0]} --schema=${schemaPath}`, { + stdio: 'inherit' + }); + + // 复制迁移文件到数据库特定目录 + if (fs.existsSync(MIGRATIONS_DIR)) { + console.log(`📋 复制迁移文件到 ${dbType} 目录...`); + copyDirectory(MIGRATIONS_DIR, dbMigrationsDir); + } + + console.log(`✅ ${dbType} 迁移完成`); + } catch (error) { + console.error(`❌ ${dbType} 迁移失败:`, error.message); + } finally { + // 确保无论成功还是失败都恢复原始内容,对于 SQLite 恢复为 ../data/db.db + restoreSchema(schemaPath, dbType, originalContent); + } +} + +async function main() { + try { + // 确保数据库目录存在 + if (!fs.existsSync(DATABASE_DIR)) { + console.error('❌ database 目录不存在'); + process.exit(1); + } + + // 获取所有数据库类型目录 + const dbTypes = fs.readdirSync(DATABASE_DIR).filter(item => { + const itemPath = path.join(DATABASE_DIR, item); + return fs.statSync(itemPath).isDirectory(); + }); + + console.log('📊 发现的数据库类型:', dbTypes.join(', ')); + console.log('🔑 数据库配置:'); + for (const [dbType, envVar] of Object.entries(DB_URL_VARS)) { + console.log(` - ${dbType}: 使用环境变量 ${envVar}`); + } + console.log(' - sqlite: 迁移时使用 ../../data/db.db,完成后恢复为 ../data/db.db'); + + // 依次处理每个数据库类型 + for (const dbType of dbTypes) { + await processDatabaseType(dbType); + } + + console.log('\n🎉 所有数据库迁移处理完成!'); + } catch (error) { + console.error('❌ 批量迁移失败:', error); + process.exit(1); + } +} + +// 执行主函数 +main().catch(error => { + console.error('❌ 程序执行失败:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/classworks.js b/classworks.js index d4371de..56746d6 100644 --- a/classworks.js +++ b/classworks.js @@ -46,11 +46,7 @@ function setupDatabase() { process.env.DATABASE_URL = DATABASE_URL; // 检查数据库表并执行必要的迁移 - execSync( - "npx prisma migrate dev --name update-" + - new Date().toISOString().split("T")[0], - { stdio: "inherit" } - ); + execSync("npx prisma migrate deploy", { stdio: "inherit" }); } catch (error) { console.error("❌ 数据库初始化失败:", error.message); process.exit(1); diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..383f836 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,69 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: classworks_mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-classworks} + MYSQL_DATABASE: ${MYSQL_DATABASE:-classworks} + MYSQL_USER: ${MYSQL_USER:-classworks} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-classworks} + TZ: Asia/Shanghai + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./mysql/conf.d:/etc/mysql/conf.d:ro + - ./mysql/initdb.d:/docker-entrypoint-initdb.d:ro + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --default-authentication-plugin=mysql_native_password + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u$$MYSQL_USER", "-p$$MYSQL_PASSWORD"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - classworks_net + + postgres: + image: postgres:15-alpine + container_name: classworks_postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-classworks} + POSTGRES_USER: ${POSTGRES_USER:-classworks} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-classworks} + TZ: Asia/Shanghai + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./postgres/initdb.d:/docker-entrypoint-initdb.d:ro + command: + - "postgres" + - "-c" + - "max_connections=100" + - "-c" + - "shared_buffers=128MB" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - classworks_net + +volumes: + mysql_data: + name: classworks_mysql_data + postgres_data: + name: classworks_postgres_data + +networks: + classworks_net: + name: classworks_network + driver: bridge \ No newline at end of file diff --git a/docker/mysql/conf.d/my.cnf b/docker/mysql/conf.d/my.cnf new file mode 100644 index 0000000..0384d42 --- /dev/null +++ b/docker/mysql/conf.d/my.cnf @@ -0,0 +1,33 @@ +[mysqld] +# 字符集设置 +character-set-server=utf8mb4 +collation-server=utf8mb4_unicode_ci + +# 连接设置 +max_connections=100 +max_allowed_packet=64M + +# InnoDB设置 +innodb_buffer_pool_size=256M +innodb_log_file_size=64M +innodb_flush_log_at_trx_commit=2 +innodb_flush_method=O_DIRECT + +# 优化设置 +query_cache_type=1 +query_cache_size=32M +sort_buffer_size=4M +read_buffer_size=2M +read_rnd_buffer_size=4M +join_buffer_size=2M + +# 日志设置 +slow_query_log=1 +slow_query_log_file=/var/log/mysql/slow.log +long_query_time=2 + +[client] +default-character-set=utf8mb4 + +[mysql] +default-character-set=utf8mb4 \ No newline at end of file diff --git a/docker/mysql/initdb.d/init.sql b/docker/mysql/initdb.d/init.sql new file mode 100644 index 0000000..f96ed98 --- /dev/null +++ b/docker/mysql/initdb.d/init.sql @@ -0,0 +1,12 @@ +-- 设置时区 +SET GLOBAL time_zone = '+8:00'; +SET time_zone = '+8:00'; + +-- 创建数据库(如果不存在) +CREATE DATABASE IF NOT EXISTS classworks + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +-- 设置权限 +GRANT ALL PRIVILEGES ON classworks.* TO 'classworks'@'%'; +FLUSH PRIVILEGES; \ No newline at end of file diff --git a/docker/postgres/initdb.d/init.sql b/docker/postgres/initdb.d/init.sql new file mode 100644 index 0000000..2507d7a --- /dev/null +++ b/docker/postgres/initdb.d/init.sql @@ -0,0 +1,10 @@ +-- 创建扩展 +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +-- 设置时区 +SET timezone = 'Asia/Shanghai'; + +-- 设置默认权限 +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO classworks; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO classworks; \ No newline at end of file diff --git a/prisma/data/db.db b/prisma/data/db.db new file mode 100644 index 0000000..b7a91ce Binary files /dev/null and b/prisma/data/db.db differ diff --git a/prisma/data/db.db-journal b/prisma/data/db.db-journal new file mode 100644 index 0000000..862e53b Binary files /dev/null and b/prisma/data/db.db-journal differ diff --git a/prisma/database/mysql/migrations/20250524123413_2025_05_24/migration.sql b/prisma/database/mysql/migrations/20250524123413_2025_05_24/migration.sql new file mode 100644 index 0000000..e18c7e6 --- /dev/null +++ b/prisma/database/mysql/migrations/20250524123413_2025_05_24/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE `KVStore` ( + `namespace` VARCHAR(191) NOT NULL, + `key` VARCHAR(191) NOT NULL, + `value` JSON NOT NULL, + `creatorIp` VARCHAR(191) NULL DEFAULT '', + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + PRIMARY KEY (`namespace`, `key`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Device` ( + `uuid` VARCHAR(191) NOT NULL, + `password` VARCHAR(191) NULL, + `passwordHint` VARCHAR(191) NULL, + `name` VARCHAR(191) NULL, + `accessType` ENUM('PUBLIC', 'PROTECTED', 'PRIVATE') NOT NULL DEFAULT 'PUBLIC', + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + PRIMARY KEY (`uuid`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/prisma/database/mysql/migrations/migration_lock.toml b/prisma/database/mysql/migrations/migration_lock.toml new file mode 100644 index 0000000..592fc0b --- /dev/null +++ b/prisma/database/mysql/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" diff --git a/prisma/database/postgres/migrations/20250524123225_init/migration.sql b/prisma/database/postgres/migrations/20250524123225_init/migration.sql new file mode 100644 index 0000000..93c7d9c --- /dev/null +++ b/prisma/database/postgres/migrations/20250524123225_init/migration.sql @@ -0,0 +1,27 @@ +-- CreateEnum +CREATE TYPE "AccessType" AS ENUM ('PUBLIC', 'PROTECTED', 'PRIVATE'); + +-- CreateTable +CREATE TABLE "KVStore" ( + "namespace" TEXT NOT NULL, + "key" TEXT NOT NULL, + "value" JSONB NOT NULL, + "creatorIp" TEXT DEFAULT '', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "KVStore_pkey" PRIMARY KEY ("namespace","key") +); + +-- CreateTable +CREATE TABLE "Device" ( + "uuid" TEXT NOT NULL, + "password" TEXT, + "passwordHint" TEXT, + "name" TEXT, + "accessType" "AccessType" NOT NULL DEFAULT 'PUBLIC', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Device_pkey" PRIMARY KEY ("uuid") +); diff --git a/prisma/database/postgres/migrations/migration_lock.toml b/prisma/database/postgres/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/database/postgres/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/database/sqlite/migrations/20250524124141_2025_05_24/migration.sql b/prisma/database/sqlite/migrations/20250524124141_2025_05_24/migration.sql new file mode 100644 index 0000000..5357006 --- /dev/null +++ b/prisma/database/sqlite/migrations/20250524124141_2025_05_24/migration.sql @@ -0,0 +1,22 @@ +-- CreateTable +CREATE TABLE "KVStore" ( + "namespace" TEXT NOT NULL, + "key" TEXT NOT NULL, + "value" JSONB NOT NULL, + "creatorIp" TEXT DEFAULT '', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + + PRIMARY KEY ("namespace", "key") +); + +-- CreateTable +CREATE TABLE "Device" ( + "uuid" TEXT NOT NULL PRIMARY KEY, + "password" TEXT, + "passwordHint" TEXT, + "name" TEXT, + "accessType" TEXT NOT NULL DEFAULT 'PUBLIC', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); diff --git a/prisma/database/sqlite/migrations/migration_lock.toml b/prisma/database/sqlite/migrations/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/database/sqlite/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite"