forked from wangziqi/gongxue-base
feat: Docker Compose deployment + auto migration on startup
- docker-compose.yml: 移除 MySQL 容器,连宿主机 MySQL - main.ts: 启动前通过独立 DataSource 执行 runMigrations - migration-runner.ts: 独立 migration 执行器 - app.module.ts: TypeORM 配置增加 migrations - 修复 InitialSchema 中 room_expenses 和 financial_operations 重复索引
This commit is contained in:
@@ -50,6 +50,8 @@ import {
|
||||
FinancialOperation,
|
||||
} from './entities';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { InitialSchema1784520727860 } from './migrations/1784520727860-InitialSchema';
|
||||
const allMigrations = [InitialSchema1784520727860];
|
||||
import { AuthorizationModule } from './authorization';
|
||||
import { RbacModule } from './rbac/rbac.module';
|
||||
import { StudentsModule } from './students/students.module';
|
||||
@@ -158,6 +160,7 @@ import { IntegrationConfigModule } from './integration/config/config.module';
|
||||
password: config.get<string>('DB_PASSWORD', ''),
|
||||
database: config.get<string>('DB_DATABASE', 'dorm_billing'),
|
||||
entities: allEntities,
|
||||
migrations: allMigrations,
|
||||
synchronize: config.get('DB_SYNCHRONIZE', 'true') !== 'false',
|
||||
charset: 'utf8mb4',
|
||||
};
|
||||
@@ -165,6 +168,7 @@ import { IntegrationConfigModule } from './integration/config/config.module';
|
||||
return {
|
||||
type: 'better-sqlite3' as const,
|
||||
database: config.get<string>('DB_DATABASE', 'dorm_billing.db'),
|
||||
migrations: allMigrations,
|
||||
entities: allEntities,
|
||||
synchronize: config.get('DB_SYNCHRONIZE', 'true') !== 'false',
|
||||
};
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { runMigrationsOnStartup } from './migration-runner';
|
||||
|
||||
async function bootstrap() {
|
||||
await runMigrationsOnStartup();
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('api');
|
||||
app.enableCors();
|
||||
const dataSource = app.get(DataSource);
|
||||
await dataSource.runMigrations();
|
||||
console.log('Migrations OK');
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
console.log(`Server running on http://localhost:${process.env.PORT ?? 3000}`);
|
||||
}
|
||||
|
||||
24
apps/server/src/migration-runner.ts
Normal file
24
apps/server/src/migration-runner.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { InitialSchema1784520727860 } from './migrations/1784520727860-InitialSchema';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
config();
|
||||
|
||||
const isMySQL = (process.env.DB_TYPE || 'sqlite') === 'mysql';
|
||||
|
||||
export async function runMigrationsOnStartup(): Promise<void> {
|
||||
const ds = new DataSource({
|
||||
type: isMySQL ? 'mysql' : 'better-sqlite3',
|
||||
host: isMySQL ? (process.env.DB_HOST || 'localhost') : undefined,
|
||||
port: isMySQL ? Number(process.env.DB_PORT || 3306) : undefined,
|
||||
username: isMySQL ? (process.env.DB_USERNAME || 'root') : undefined,
|
||||
password: isMySQL ? (process.env.DB_PASSWORD || '') : undefined,
|
||||
database: process.env.DB_DATABASE || (isMySQL ? 'dorm_billing' : 'dorm_billing.db'),
|
||||
charset: isMySQL ? 'utf8mb4' : undefined,
|
||||
migrations: [InitialSchema1784520727860],
|
||||
});
|
||||
|
||||
await ds.initialize();
|
||||
await ds.runMigrations();
|
||||
await ds.destroy();
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export class InitialSchema1784520727860 implements MigrationInterface {
|
||||
await queryRunner.query(`CREATE TABLE \`users\` (\`id\` int NOT NULL AUTO_INCREMENT, \`username\` varchar(50) NOT NULL, \`password_hash\` varchar(255) NOT NULL, \`name\` varchar(50) NULL, \`is_active\` tinyint NOT NULL DEFAULT 1, \`last_login_at\` datetime NULL, \`is_archived\` tinyint NOT NULL DEFAULT 0, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`profile\` text NULL, UNIQUE INDEX \`IDX_fe0bb3f6520ee0469504521e71\` (\`username\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`sync_state\` (\`platform\` varchar(20) NOT NULL, \`last_sync_at\` datetime NULL, \`run_id\` varchar(64) NULL, \`running_since\` datetime NULL, PRIMARY KEY (\`platform\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`sync_logs\` (\`id\` int NOT NULL AUTO_INCREMENT, \`platform\` varchar(20) NOT NULL, \`sync_type\` varchar(20) NOT NULL, \`status\` varchar(20) NOT NULL, \`records_count\` int NOT NULL DEFAULT '0', \`error_message\` text NULL, \`started_at\` datetime NULL, \`finished_at\` datetime NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`room_expenses\` (\`id\` int NOT NULL AUTO_INCREMENT, \`room_id\` int NOT NULL, \`expense_type\` varchar(20) NOT NULL, \`amount\` decimal(10,2) NOT NULL, \`period_start\` date NOT NULL, \`period_end\` date NOT NULL, \`description\` text NULL, \`recorded_by\` int NULL, \`import_key\` varchar(120) NULL, \`status\` varchar(20) NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_050485162d4fe47bd3cfd7dedb\` (\`import_key\`), UNIQUE INDEX \`IDX_050485162d4fe47bd3cfd7dedb\` (\`import_key\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`room_expenses\` (\`id\` int NOT NULL AUTO_INCREMENT, \`room_id\` int NOT NULL, \`expense_type\` varchar(20) NOT NULL, \`amount\` decimal(10,2) NOT NULL, \`period_start\` date NOT NULL, \`period_end\` date NOT NULL, \`description\` text NULL, \`recorded_by\` int NULL, \`import_key\` varchar(120) NULL, \`status\` varchar(20) NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_050485162d4fe47bd3cfd7dedb\` (\`import_key\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`rooms\` (\`id\` int NOT NULL AUTO_INCREMENT, \`room_number\` varchar(20) NOT NULL, \`building\` varchar(50) NULL, \`floor\` int NULL, \`capacity\` int NOT NULL, \`status\` varchar(20) NOT NULL DEFAULT 'available', \`room_type\` varchar(20) NULL, \`rental_category\` varchar(10) NOT NULL DEFAULT 'short', \`monthly_rate\` decimal(10,2) NOT NULL DEFAULT '0.00', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_8f7c6fa4c469bab1a06fe3e49f\` (\`room_number\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`organizations\` (\`id\` int NOT NULL AUTO_INCREMENT, \`public_id\` varchar(36) NOT NULL, \`code\` varchar(50) NOT NULL, \`name\` varchar(100) NOT NULL, \`is_host\` tinyint NOT NULL DEFAULT 0, \`contact_name\` varchar(50) NULL, \`phone\` varchar(30) NULL, \`color\` varchar(20) NULL, \`notes\` text NULL, \`status\` varchar(20) NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_0db5eda192bf60a02bd41931f8\` (\`public_id\`), UNIQUE INDEX \`IDX_7e27c3b62c681fbe3e2322535f\` (\`code\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`beds\` (\`id\` int NOT NULL AUTO_INCREMENT, \`room_id\` int NOT NULL, \`bed_number\` varchar(20) NOT NULL, \`status\` varchar(20) NOT NULL DEFAULT 'available', \`notes\` text NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_95e9ba0a907346ef7b0d5ca488\` (\`room_id\`, \`bed_number\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
@@ -28,7 +28,7 @@ export class InitialSchema1784520727860 implements MigrationInterface {
|
||||
await queryRunner.query(`CREATE TABLE \`operation_logs\` (\`id\` int NOT NULL AUTO_INCREMENT, \`user_id\` int NULL, \`username\` varchar(50) NULL, \`module\` varchar(50) NOT NULL, \`action\` varchar(50) NOT NULL, \`target_id\` int NULL, \`target_type\` varchar(50) NULL, \`detail\` text NULL, \`ip_address\` varchar(50) NULL, \`user_agent\` varchar(500) NULL, \`status\` varchar(20) NULL DEFAULT 'success', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`notifications\` (\`id\` int NOT NULL AUTO_INCREMENT, \`recipient_id\` int NOT NULL, \`type\` varchar(30) NOT NULL, \`title\` varchar(200) NOT NULL, \`content\` text NULL, \`link\` varchar(500) NULL, \`is_read\` tinyint NOT NULL DEFAULT 0, \`read_at\` datetime NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`learning_records\` (\`id\` int NOT NULL AUTO_INCREMENT, \`student_id\` int NOT NULL, \`record_date\` date NULL, \`record_type\` varchar(50) NULL, \`content\` text NULL, \`follow_up_method\` varchar(50) NULL, \`next_step\` text NULL, \`status\` varchar(20) NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`financial_operations\` (\`id\` int NOT NULL AUTO_INCREMENT, \`operation_id\` varchar(64) NOT NULL, \`type\` varchar(64) NOT NULL, \`status\` varchar(20) NOT NULL DEFAULT 'running', \`result_json\` text NULL, \`error_message\` varchar(500) NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_f15aa4c89aa57366fdba8a84db\` (\`operation_id\`), UNIQUE INDEX \`IDX_f15aa4c89aa57366fdba8a84db\` (\`operation_id\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`financial_operations\` (\`id\` int NOT NULL AUTO_INCREMENT, \`operation_id\` varchar(64) NOT NULL, \`type\` varchar(64) NOT NULL, \`status\` varchar(20) NOT NULL DEFAULT 'running', \`result_json\` text NULL, \`error_message\` varchar(500) NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_f15aa4c89aa57366fdba8a84db\` (\`operation_id\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`expense_types\` (\`id\` int NOT NULL AUTO_INCREMENT, \`code\` varchar(30) NOT NULL, \`name\` varchar(30) NOT NULL, \`category\` varchar(20) NOT NULL DEFAULT 'room', \`sort_order\` int NOT NULL DEFAULT '0', \`enabled\` tinyint NOT NULL DEFAULT 1, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_36eda3eb0f6740ecf2ba906012\` (\`code\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`exam_scores\` (\`id\` int NOT NULL AUTO_INCREMENT, \`student_id\` int NOT NULL, \`enrollment_id\` int NULL, \`exam_type\` varchar(50) NULL, \`exam_name\` varchar(100) NULL, \`subject\` varchar(50) NULL, \`score\` decimal(5,2) NULL, \`class_avg\` decimal(5,2) NULL, \`rank\` int NULL, \`exam_date\` date NULL, \`status\` varchar(20) NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`CREATE TABLE \`ding_attendance_raw\` (\`id\` int NOT NULL AUTO_INCREMENT, \`ding_user_id\` varchar(100) NOT NULL, \`user_name\` varchar(100) NOT NULL, \`attendance_date\` date NOT NULL, \`ding_id\` varchar(100) NOT NULL, \`check_in_time\` datetime NULL, \`check_out_time\` datetime NULL, \`attendance_type\` varchar(20) NOT NULL, \`time_result\` varchar(20) NOT NULL, \`location_result\` varchar(20) NULL, \`punch_source\` varchar(40) NULL, \`punch_device_name\` varchar(100) NULL, \`punch_device_id\` varchar(100) NULL, \`match_status\` varchar(20) NOT NULL DEFAULT 'unmatched', \`matched_student_id\` int NULL, \`raw_data\` text NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), INDEX \`IDX_d6680d5150f0bfa47dbc1fe96f\` (\`match_status\`), INDEX \`IDX_088de070f537b15ab7da255e5b\` (\`attendance_date\`), UNIQUE INDEX \`IDX_997849bb04ff69149fcf00a8d3\` (\`ding_id\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
@@ -193,7 +193,6 @@ export class InitialSchema1784520727860 implements MigrationInterface {
|
||||
await queryRunner.query(`DROP INDEX \`IDX_36eda3eb0f6740ecf2ba906012\` ON \`expense_types\``);
|
||||
await queryRunner.query(`DROP TABLE \`expense_types\``);
|
||||
await queryRunner.query(`DROP INDEX \`IDX_f15aa4c89aa57366fdba8a84db\` ON \`financial_operations\``);
|
||||
await queryRunner.query(`DROP INDEX \`IDX_f15aa4c89aa57366fdba8a84db\` ON \`financial_operations\``);
|
||||
await queryRunner.query(`DROP TABLE \`financial_operations\``);
|
||||
await queryRunner.query(`DROP TABLE \`learning_records\``);
|
||||
await queryRunner.query(`DROP TABLE \`notifications\``);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
|
||||
Reference in New Issue
Block a user