初次提交
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
// 颜色输出函数
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
cyan: '\x1b[36m'
|
||||
}
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function logSuccess(message) {
|
||||
log(`✅ ${message}`, 'green')
|
||||
}
|
||||
|
||||
function logWarning(message) {
|
||||
log(`⚠️ ${message}`, 'yellow')
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
log(`❌ ${message}`, 'red')
|
||||
}
|
||||
|
||||
function logInfo(message) {
|
||||
log(`ℹ️ ${message}`, 'blue')
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
function fileExists(filePath) {
|
||||
try {
|
||||
return fs.existsSync(filePath)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目录大小
|
||||
function getDirectorySize(dirPath) {
|
||||
try {
|
||||
const stats = fs.statSync(dirPath)
|
||||
if (stats.isFile()) {
|
||||
return stats.size
|
||||
} else if (stats.isDirectory()) {
|
||||
let totalSize = 0
|
||||
const files = fs.readdirSync(dirPath)
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file)
|
||||
totalSize += getDirectorySize(filePath)
|
||||
}
|
||||
return totalSize
|
||||
}
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 检查 Drizzle 配置
|
||||
function checkDrizzleConfig() {
|
||||
log('🔍 检查 Drizzle 配置...', 'cyan')
|
||||
|
||||
const drizzleConfigPath = 'drizzle.config.ts'
|
||||
const drizzleSchemaPath = 'app/drizzle/schema.ts'
|
||||
|
||||
if (!fileExists(drizzleConfigPath)) {
|
||||
logError('Drizzle 配置文件不存在')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!fileExists(drizzleSchemaPath)) {
|
||||
logError('Drizzle schema 文件不存在')
|
||||
return false
|
||||
}
|
||||
|
||||
logSuccess('Drizzle 配置检查通过')
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查环境变量
|
||||
function checkEnvironmentVariables() {
|
||||
log('🔍 检查环境变量...', 'cyan')
|
||||
|
||||
const requiredVars = ['DATABASE_URL', 'JWT_SECRET']
|
||||
|
||||
const optionalVars = ['NODE_ENV']
|
||||
|
||||
let allRequired = true
|
||||
|
||||
// 检查必需的环境变量
|
||||
requiredVars.forEach((varName) => {
|
||||
if (process.env[varName]) {
|
||||
logSuccess(`${varName} 已设置`)
|
||||
} else {
|
||||
logError(`缺少必需的环境变量: ${varName}`)
|
||||
allRequired = false
|
||||
}
|
||||
})
|
||||
|
||||
// 检查可选的环境变量
|
||||
optionalVars.forEach((varName) => {
|
||||
if (process.env[varName]) {
|
||||
logInfo(`${varName} 已设置`)
|
||||
} else {
|
||||
logWarning(`可选环境变量未设置: ${varName}`)
|
||||
}
|
||||
})
|
||||
|
||||
return allRequired
|
||||
}
|
||||
|
||||
// 检查数据库连接
|
||||
async function checkDatabaseConnection() {
|
||||
log('🔍 检查数据库连接...', 'cyan')
|
||||
|
||||
try {
|
||||
// 检查数据库连接文件是否存在
|
||||
const dbPath = 'app/drizzle/db.ts'
|
||||
if (!fileExists(dbPath)) {
|
||||
logError('数据库连接文件不存在')
|
||||
return false
|
||||
}
|
||||
|
||||
logSuccess('数据库配置检查通过')
|
||||
return true
|
||||
} catch (error) {
|
||||
logWarning('数据库连接测试失败(这在某些部署环境中是正常的)')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查关键文件
|
||||
function checkCriticalFiles() {
|
||||
log('🔍 检查关键文件...', 'cyan')
|
||||
|
||||
const criticalFiles = [
|
||||
'nuxt.config.ts',
|
||||
'drizzle.config.ts',
|
||||
'app/drizzle/schema.ts',
|
||||
'package.json'
|
||||
]
|
||||
|
||||
let allExists = true
|
||||
|
||||
criticalFiles.forEach((file) => {
|
||||
if (fileExists(file)) {
|
||||
logSuccess(`${file} 存在`)
|
||||
} else {
|
||||
logError(`关键文件缺失: ${file}`)
|
||||
allExists = false
|
||||
}
|
||||
})
|
||||
|
||||
return allExists
|
||||
}
|
||||
|
||||
// 生成部署报告
|
||||
function generateDeploymentReport(checks) {
|
||||
log('\n📊 部署检查报告', 'bright')
|
||||
log('='.repeat(50), 'cyan')
|
||||
|
||||
const passed = checks.filter((check) => check.passed).length
|
||||
const total = checks.length
|
||||
|
||||
checks.forEach((check) => {
|
||||
const status = check.passed ? '✅' : '❌'
|
||||
const color = check.passed ? 'green' : 'red'
|
||||
log(`${status} ${check.name}`, color)
|
||||
if (check.details) {
|
||||
log(` ${check.details}`, 'reset')
|
||||
}
|
||||
})
|
||||
|
||||
log(`\n总计: ${passed}/${total} 项检查通过`, passed === total ? 'green' : 'yellow')
|
||||
|
||||
if (passed === total) {
|
||||
log('🎉 所有检查都通过了!部署应该可以正常工作。', 'green')
|
||||
} else {
|
||||
log('⚠️ 部分检查未通过,可能会影响应用运行。', 'yellow')
|
||||
}
|
||||
}
|
||||
|
||||
// 主检查函数
|
||||
async function checkDeployment() {
|
||||
log('🔍 开始部署后检查...', 'bright')
|
||||
|
||||
const checks = [
|
||||
{
|
||||
name: '关键文件检查',
|
||||
passed: checkCriticalFiles(),
|
||||
details: null
|
||||
},
|
||||
{
|
||||
name: 'Drizzle 配置检查',
|
||||
passed: checkDrizzleConfig(),
|
||||
details: null
|
||||
},
|
||||
{
|
||||
name: '环境变量检查',
|
||||
passed: checkEnvironmentVariables(),
|
||||
details: null
|
||||
}
|
||||
]
|
||||
|
||||
// 数据库连接检查(异步)
|
||||
const dbConnected = await checkDatabaseConnection()
|
||||
checks.push({
|
||||
name: '数据库连接检查',
|
||||
passed: dbConnected,
|
||||
details: dbConnected ? null : '在某些部署环境中这是正常的'
|
||||
})
|
||||
|
||||
generateDeploymentReport(checks)
|
||||
|
||||
const criticalFailed = checks.filter(
|
||||
(check) => !check.passed && !check.name.includes('数据库连接')
|
||||
).length
|
||||
|
||||
if (criticalFailed > 0) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行检查
|
||||
checkDeployment().catch((error) => {
|
||||
logError(`检查过程中发生错误: ${error.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 数据库清空脚本
|
||||
* 这个脚本会清空所有表的数据,但保留表结构
|
||||
*/
|
||||
import {
|
||||
apiKeyPermissions,
|
||||
apiKeys,
|
||||
apiLogs,
|
||||
db,
|
||||
emailTemplates,
|
||||
notifications,
|
||||
notificationSettings,
|
||||
playTimes,
|
||||
schedules,
|
||||
semesters,
|
||||
songBlacklists,
|
||||
songs,
|
||||
systemSettings,
|
||||
users,
|
||||
userStatusLogs,
|
||||
votes
|
||||
} from '../app/drizzle/db.ts'
|
||||
import bcrypt from 'bcrypt'
|
||||
|
||||
// 重置所有表的自增序列
|
||||
async function resetAutoIncrementSequences() {
|
||||
const tables = [
|
||||
'User',
|
||||
'Song',
|
||||
'Vote',
|
||||
'Schedule',
|
||||
'Notification',
|
||||
'NotificationSettings',
|
||||
'PlayTime',
|
||||
'Semester',
|
||||
'SystemSettings',
|
||||
'SongBlacklist',
|
||||
'EmailTemplate',
|
||||
'user_status_logs'
|
||||
]
|
||||
|
||||
console.log('重置自增序列...')
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
// PostgreSQL 重置序列的 SQL 命令
|
||||
const sequenceName = `"${table}_id_seq"`
|
||||
await db.execute(`ALTER SEQUENCE ${sequenceName} RESTART WITH 1`)
|
||||
} catch (error) {
|
||||
console.warn(`重置 ${table} 表序列失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('开始清空数据库...')
|
||||
|
||||
// 清空所有表数据
|
||||
try {
|
||||
// 按照关联关系顺序删除数据
|
||||
await db.delete(notifications)
|
||||
await db.delete(notificationSettings)
|
||||
await db.delete(schedules)
|
||||
await db.delete(votes)
|
||||
await db.delete(songs)
|
||||
await db.delete(playTimes)
|
||||
await db.delete(semesters)
|
||||
await db.delete(systemSettings)
|
||||
await db.delete(songBlacklists)
|
||||
await db.delete(apiLogs)
|
||||
await db.delete(apiKeyPermissions)
|
||||
await db.delete(apiKeys)
|
||||
await db.delete(userStatusLogs)
|
||||
await db.delete(emailTemplates)
|
||||
await db.delete(users)
|
||||
|
||||
console.log('数据库已清空,开始重置自增序列...')
|
||||
|
||||
// 先重置所有自增序列
|
||||
await resetAutoIncrementSequences()
|
||||
|
||||
console.log('开始创建默认超级管理员账户...')
|
||||
|
||||
// 创建默认管理员账户
|
||||
const hashedPassword = await bcrypt.hash('admin123', 10)
|
||||
|
||||
// 创建超级管理员用户
|
||||
const [admin] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
name: '超级管理员',
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
role: 'SUPER_ADMIN',
|
||||
forcePasswordChange: false
|
||||
})
|
||||
.returning()
|
||||
|
||||
console.log('默认超级管理员账户已创建:')
|
||||
console.log('账号名: admin')
|
||||
console.log('密码: admin123')
|
||||
console.log(`管理员ID: ${admin.id}`)
|
||||
|
||||
// 调整User表的自增序列,确保下一个用户从admin.id + 1开始
|
||||
try {
|
||||
const nextId = admin.id + 1
|
||||
await db.execute(`ALTER SEQUENCE "User_id_seq" RESTART WITH ${nextId}`)
|
||||
} catch (error) {
|
||||
console.warn(`调整User表序列失败: ${error.message}`)
|
||||
}
|
||||
|
||||
console.log('🎉 数据库清空、初始化和序列重置全部完成!')
|
||||
} catch (error) {
|
||||
console.error('清空数据库时出错:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { config } from 'dotenv'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { client, db, notificationSettings, systemSettings, users } from '../app/drizzle/db.ts'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
config({ path: path.resolve(process.cwd(), '.env') })
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ DATABASE_URL 环境变量未设置')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 检查是否已有超级管理员
|
||||
const existingSuperAdmin = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.role, 'SUPER_ADMIN'))
|
||||
.limit(1)
|
||||
|
||||
if (existingSuperAdmin.length > 0) {
|
||||
console.log('✅ 管理员已存在')
|
||||
return existingSuperAdmin[0]
|
||||
}
|
||||
|
||||
// 创建管理员
|
||||
const hashedPassword = await bcrypt.hash('admin123', 10)
|
||||
const [admin] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
username: 'admin',
|
||||
name: '超级管理员',
|
||||
password: hashedPassword,
|
||||
role: 'SUPER_ADMIN',
|
||||
forcePasswordChange: false
|
||||
})
|
||||
.returning()
|
||||
|
||||
// 创建通知设置
|
||||
await db
|
||||
.insert(notificationSettings)
|
||||
.values({
|
||||
userId: admin.id,
|
||||
enabled: true,
|
||||
songRequestEnabled: true,
|
||||
songVotedEnabled: true,
|
||||
songPlayedEnabled: true,
|
||||
refreshInterval: 60,
|
||||
songVotedThreshold: 1
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
|
||||
// 创建系统设置
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
id: 1,
|
||||
enablePlayTimeSelection: false
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
|
||||
console.log('✅ 管理员创建成功 (admin/admin123)')
|
||||
return admin
|
||||
} catch (error) {
|
||||
console.error('❌ 创建管理员失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await client.end()
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error('❌ 初始化失败:', error)
|
||||
await client.end()
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { config } from 'dotenv'
|
||||
config({ path: path.resolve(process.cwd(), '.env') })
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m'
|
||||
}
|
||||
const log = (msg, color = 'reset') => console.log(`${colors[color]}${msg}${colors.reset}`)
|
||||
const ok = (msg) => log(`✅ ${msg}`, 'green')
|
||||
const warn = (msg) => log(`⚠️ ${msg}`, 'yellow')
|
||||
const err = (msg) => log(`❌ ${msg}`, 'red')
|
||||
|
||||
const NON_INTERACTIVE_ENV = {
|
||||
...process.env,
|
||||
CI: 'true',
|
||||
DRIZZLE_KIT_FORCE: 'true',
|
||||
DRIZZLE_KIT_NON_INTERACTIVE: 'true',
|
||||
NODE_ENV: process.env.NODE_ENV || 'production'
|
||||
}
|
||||
|
||||
function safeExec(command, options = {}) {
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit', ...options })
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function fileExists(p) {
|
||||
try {
|
||||
return fs.existsSync(p)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDrizzleFiles() {
|
||||
if (!fileExists('drizzle.config.ts')) throw new Error('Drizzle 配置文件不存在')
|
||||
if (!fileExists('app/drizzle/schema.ts')) throw new Error('Schema 文件不存在')
|
||||
}
|
||||
|
||||
function isEmptyDatabase() {
|
||||
try {
|
||||
const output = execSync('pnpm exec drizzle-kit introspect --config=drizzle.config.ts', {
|
||||
stdio: 'pipe',
|
||||
env: NON_INTERACTIVE_ENV,
|
||||
encoding: 'utf8'
|
||||
})
|
||||
const tablesMatch = output.match(/(\d+)\s+tables/i)
|
||||
const hasTablesCount = tablesMatch && Number(tablesMatch[1]) > 0
|
||||
const listsTables = /\bcolumns\b|\bindexes\b|\bfks\b/i.test(output)
|
||||
return !(hasTablesCount || listsTables)
|
||||
} catch {
|
||||
warn('无法检测数据库状态,按非空库处理')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查数据库schema是否与代码中的schema一致
|
||||
function checkSchemaConsistency() {
|
||||
try {
|
||||
// 使用 drizzle-kit introspect 获取当前数据库schema
|
||||
const output = execSync('pnpm exec drizzle-kit introspect --config=drizzle.config.ts', {
|
||||
stdio: 'pipe',
|
||||
env: NON_INTERACTIVE_ENV,
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
// 检查关键表和字段是否存在
|
||||
const checks = [
|
||||
{ pattern: /"status".*user_status/i, name: 'user_status enum type' },
|
||||
{ pattern: /"api_keys"/i, name: 'api_keys table' }
|
||||
]
|
||||
|
||||
const missing = []
|
||||
for (const check of checks) {
|
||||
if (!check.pattern.test(output)) {
|
||||
missing.push(check.name)
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
warn(`检测到数据库schema不完整,缺少: ${missing.join(', ')}`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (e) {
|
||||
warn('无法检测数据库schema一致性')
|
||||
return true // 出错时不阻止继续
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
log('🔄 数据库同步', 'cyan')
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
warn('未设置 DATABASE_URL')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
ensureDrizzleFiles()
|
||||
|
||||
const emptyDb = isEmptyDatabase()
|
||||
if (emptyDb) {
|
||||
log('🆕 检测到空库,执行迁移 (migrate)...', 'cyan')
|
||||
if (!safeExec('pnpm run db:migrate', { env: NON_INTERACTIVE_ENV })) {
|
||||
err('数据库迁移失败')
|
||||
process.exit(1)
|
||||
}
|
||||
ok('空库迁移完成')
|
||||
} else {
|
||||
log('🔁 检测到非空库,检查schema一致性...', 'cyan')
|
||||
|
||||
// 检查数据库schema是否与代码一致
|
||||
const schemaConsistent = checkSchemaConsistency()
|
||||
|
||||
if (!schemaConsistent) {
|
||||
warn('数据库schema不完整,尝试使用 push --force 进行修复...', 'cyan')
|
||||
const pushCommand = 'pnpm exec drizzle-kit push --force --config=drizzle.config.ts'
|
||||
if (
|
||||
!safeExec(pushCommand, {
|
||||
env: { ...NON_INTERACTIVE_ENV, DRIZZLE_KIT_NON_INTERACTIVE: 'true' }
|
||||
})
|
||||
) {
|
||||
err('数据库schema修复失败')
|
||||
process.exit(1)
|
||||
}
|
||||
ok('schema修复成功')
|
||||
} else {
|
||||
log('🔁 数据库schema一致,尝试执行 migrate 同步...', 'cyan')
|
||||
|
||||
// 尝试执行 migrate
|
||||
const migrateSuccess = safeExec('pnpm run db:migrate', {
|
||||
env: { ...NON_INTERACTIVE_ENV, DRIZZLE_KIT_NON_INTERACTIVE: 'true' }
|
||||
})
|
||||
|
||||
if (migrateSuccess) {
|
||||
ok('migrate 同步成功')
|
||||
} else {
|
||||
warn('migrate 同步失败,可能是由于数据库结构与迁移记录不一致。')
|
||||
log('🔄 尝试使用 push --force 进行强制同步...', 'cyan')
|
||||
|
||||
// 在 CI 环境下,push 命令如果遇到重命名等歧义可能会弹出交互式提示
|
||||
// 确保 DRIZZLE_KIT_NON_INTERACTIVE 已设置
|
||||
const pushCommand = 'pnpm exec drizzle-kit push --force --config=drizzle.config.ts'
|
||||
if (
|
||||
!safeExec(pushCommand, {
|
||||
env: { ...NON_INTERACTIVE_ENV, DRIZZLE_KIT_NON_INTERACTIVE: 'true' }
|
||||
})
|
||||
) {
|
||||
err('数据库同步完全失败。请检查数据库连接或手动运行 pnpm exec drizzle-kit push 以解决歧义。')
|
||||
process.exit(1)
|
||||
}
|
||||
ok('强制同步 (push) 成功')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ok('数据库同步流程完成')
|
||||
}
|
||||
|
||||
try {
|
||||
main()
|
||||
} catch (e) {
|
||||
err(`同步异常: ${e.message || e}`)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import { config } from 'dotenv'
|
||||
import path from 'path'
|
||||
|
||||
// 加载环境变量(从项目根目录)
|
||||
config({ path: path.resolve(process.cwd(), '.env') })
|
||||
|
||||
// 如果设置了 PREBUILT=true,则自动跳过安装和构建
|
||||
if (process.env.PREBUILT === 'true') {
|
||||
process.env.SKIP_INSTALL = 'true'
|
||||
process.env.SKIP_BUILD = 'true'
|
||||
}
|
||||
|
||||
// 颜色输出函数
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m'
|
||||
}
|
||||
const BUILD_MEMORY_MB = 6144
|
||||
const DEFAULT_NODE_OPTIONS = `--max-old-space-size=${BUILD_MEMORY_MB}`
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function logStep(step, message) {
|
||||
log(`${step} ${message}`, 'cyan')
|
||||
}
|
||||
|
||||
function logSuccess(message) {
|
||||
log(`✅ ${message}`, 'green')
|
||||
}
|
||||
|
||||
function logWarning(message) {
|
||||
log(`⚠️ ${message}`, 'yellow')
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
log(`❌ ${message}`, 'red')
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
function fileExists(filePath) {
|
||||
try {
|
||||
return fs.existsSync(filePath)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 安全执行命令
|
||||
function safeExec(command, options = {}) {
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit', ...options })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查环境变量
|
||||
function checkEnvironment() {
|
||||
logStep('🔍', '检查环境配置...')
|
||||
|
||||
const requiredEnvVars = ['DATABASE_URL']
|
||||
const missingVars = []
|
||||
|
||||
requiredEnvVars.forEach((varName) => {
|
||||
if (!process.env[varName]) {
|
||||
missingVars.push(varName)
|
||||
}
|
||||
})
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
logError(`缺少必需的环境变量: ${missingVars.join(', ')}`)
|
||||
logError('请通过以下方式之一设置环境变量:')
|
||||
logError('1. 创建 .env 文件并配置 DATABASE_URL')
|
||||
logError('2. 使用 docker run -e DATABASE_URL=xxx 传递环境变量')
|
||||
logError('3. 在 docker-compose.yml 中配置 environment')
|
||||
throw new Error('环境变量配置不完整')
|
||||
} else {
|
||||
logSuccess('环境变量检查通过')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 主部署流程
|
||||
async function deploy() {
|
||||
log('🚀 开始部署...', 'cyan')
|
||||
|
||||
try {
|
||||
// 0. 检查环境(必须通过)
|
||||
checkEnvironment()
|
||||
|
||||
// 1. 安装依赖
|
||||
if (process.env.SKIP_INSTALL === 'true') {
|
||||
logStep('📦', '跳过依赖安装 (SKIP_INSTALL=true)...')
|
||||
} else {
|
||||
logStep('📦', '安装依赖...')
|
||||
let installed = false
|
||||
|
||||
// 优先尝试 pnpm install --frozen-lockfile
|
||||
if (fileExists('pnpm-lock.yaml')) {
|
||||
log('尝试使用 pnpm install --frozen-lockfile 安装...', 'cyan')
|
||||
if (safeExec('pnpm install --frozen-lockfile')) {
|
||||
installed = true
|
||||
logSuccess('依赖安装完成 (pnpm install --frozen-lockfile)')
|
||||
} else {
|
||||
logWarning('pnpm install --frozen-lockfile 安装失败,准备回退到 pnpm install...')
|
||||
}
|
||||
} else {
|
||||
logWarning('未检测到 pnpm-lock.yaml,跳过冻结锁文件安装,直接使用 pnpm install...')
|
||||
}
|
||||
|
||||
// 如果冻结锁文件安装没运行或失败,使用 pnpm install
|
||||
if (!installed) {
|
||||
log('正在使用 pnpm install 安装...', 'cyan')
|
||||
if (!safeExec('pnpm install')) {
|
||||
throw new Error('依赖安装失败')
|
||||
}
|
||||
logSuccess('依赖安装完成 (pnpm install)')
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查 Drizzle 配置
|
||||
if (
|
||||
!fileExists('drizzle.config.ts') ||
|
||||
!fileExists('app/drizzle/schema.ts') ||
|
||||
!fileExists('app/drizzle/db.ts')
|
||||
) {
|
||||
throw new Error('Drizzle 配置文件不完整')
|
||||
}
|
||||
|
||||
// 2.1. 确保迁移目录存在
|
||||
if (!fileExists('app/drizzle/migrations')) {
|
||||
fs.mkdirSync('app/drizzle/migrations', { recursive: true })
|
||||
}
|
||||
|
||||
// 3. 数据库同步
|
||||
logStep('🗄️', '同步数据库...')
|
||||
let dbSyncSuccess = false
|
||||
if (process.env.DATABASE_URL) {
|
||||
const nonInteractiveEnv = {
|
||||
...process.env,
|
||||
DRIZZLE_KIT_FORCE: 'true',
|
||||
CI: 'true',
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
if (safeExec('node scripts/db-sync.js', { env: nonInteractiveEnv })) {
|
||||
logSuccess('数据库同步成功')
|
||||
dbSyncSuccess = true
|
||||
} else {
|
||||
logWarning('数据库同步失败')
|
||||
}
|
||||
} else {
|
||||
logWarning('未设置 DATABASE_URL,跳过数据库迁移')
|
||||
}
|
||||
|
||||
// 4. 创建管理员账户
|
||||
if (fileExists('scripts/create-admin.js') && dbSyncSuccess) {
|
||||
logStep('👤', '检查管理员账户...')
|
||||
safeExec('pnpm run create-admin')
|
||||
}
|
||||
|
||||
// 5. 构建应用
|
||||
if (process.env.SKIP_BUILD === 'true') {
|
||||
logStep('🔨', '跳过应用构建 (SKIP_BUILD=true)...')
|
||||
} else {
|
||||
logStep('🔨', '构建应用...')
|
||||
const buildEnv = {
|
||||
...process.env,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS || DEFAULT_NODE_OPTIONS
|
||||
}
|
||||
if (!safeExec('pnpm exec nuxt build', { env: buildEnv })) {
|
||||
throw new Error('应用构建失败')
|
||||
}
|
||||
logSuccess('应用构建完成')
|
||||
}
|
||||
|
||||
log('🎉 部署完成!', 'green')
|
||||
} catch (error) {
|
||||
logError(`部署失败: ${error.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行部署
|
||||
deploy().catch((error) => {
|
||||
logError(`未预期的错误: ${error.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
{ "version": "7", "dialect": "postgresql", "entries": [] }
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import fs from 'fs'
|
||||
|
||||
// 颜色输出
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m'
|
||||
}
|
||||
const BUILD_MEMORY_MB = 6144
|
||||
const DEFAULT_NODE_OPTIONS = `--max-old-space-size=${BUILD_MEMORY_MB}`
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function logStep(step, message) {
|
||||
log(`${step} ${message}`, 'cyan')
|
||||
}
|
||||
|
||||
function logSuccess(message) {
|
||||
log(`✅ ${message}`, 'green')
|
||||
}
|
||||
|
||||
function logWarning(message) {
|
||||
log(`⚠️ ${message}`, 'yellow')
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
log(`❌ ${message}`, 'red')
|
||||
}
|
||||
|
||||
// 安全执行命令
|
||||
function safeExec(command, options = {}) {
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit', ...options })
|
||||
return true
|
||||
} catch (error) {
|
||||
logError(`命令执行失败: ${command}`)
|
||||
logError(error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
function fileExists(filePath) {
|
||||
try {
|
||||
return fs.existsSync(filePath)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Netlify 构建流程
|
||||
async function netlifyBuild() {
|
||||
log('🚀 Netlify 构建', 'cyan')
|
||||
|
||||
try {
|
||||
// 1. 设置环境变量
|
||||
process.env.NETLIFY = 'true'
|
||||
process.env.NITRO_PRESET = 'netlify'
|
||||
|
||||
// 2. 清理构建目录
|
||||
logStep('🧹', '清理构建目录...')
|
||||
if (fileExists('dist')) safeExec('rm -rf dist')
|
||||
if (fileExists('.netlify')) safeExec('rm -rf .netlify')
|
||||
if (fileExists('.nuxt')) safeExec('rm -rf .nuxt')
|
||||
logSuccess('清理完成')
|
||||
|
||||
// 3. 安装依赖
|
||||
logStep('📦', '安装依赖...')
|
||||
let installed = false
|
||||
if (fileExists('node_modules')) {
|
||||
safeExec('rm -rf node_modules')
|
||||
}
|
||||
|
||||
if (fileExists('pnpm-lock.yaml')) {
|
||||
if (safeExec('pnpm install --frozen-lockfile')) {
|
||||
installed = true
|
||||
logSuccess('依赖安装完成 (pnpm install --frozen-lockfile)')
|
||||
} else {
|
||||
logWarning('pnpm install --frozen-lockfile 安装失败,准备回退到 pnpm install...')
|
||||
}
|
||||
} else {
|
||||
logWarning('未检测到 pnpm-lock.yaml,跳过冻结锁文件安装,直接使用 pnpm install...')
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
if (!safeExec('pnpm install')) {
|
||||
throw new Error('依赖安装失败')
|
||||
}
|
||||
logSuccess('依赖安装完成 (pnpm install)')
|
||||
}
|
||||
|
||||
// 验证 Drizzle 依赖
|
||||
if (!safeExec('pnpm list drizzle-orm drizzle-kit')) {
|
||||
if (!safeExec('pnpm add drizzle-orm drizzle-kit')) {
|
||||
throw new Error('Drizzle 依赖安装失败')
|
||||
}
|
||||
}
|
||||
// 4. 检查 Drizzle 配置
|
||||
if (!fileExists('drizzle.config.ts') || !fileExists('app/drizzle/schema.ts')) {
|
||||
throw new Error('Drizzle 配置文件不完整')
|
||||
}
|
||||
|
||||
// 5. 确保迁移目录存在
|
||||
if (!fileExists('app/drizzle/migrations')) {
|
||||
fs.mkdirSync('app/drizzle/migrations', { recursive: true })
|
||||
}
|
||||
|
||||
// 6. 数据库同步
|
||||
if (process.env.DATABASE_URL) {
|
||||
logStep('�️', '同步数据库...')
|
||||
const env = { ...process.env, CI: 'true', DRIZZLE_KIT_FORCE: 'true', NODE_ENV: 'production' }
|
||||
if (safeExec('node scripts/db-sync.js', { env })) {
|
||||
logSuccess('数据库同步成功')
|
||||
} else {
|
||||
logWarning('数据库同步失败')
|
||||
}
|
||||
|
||||
// 检查管理员账户
|
||||
if (fileExists('scripts/create-admin.js')) {
|
||||
logStep('👤', '检查管理员账户...')
|
||||
safeExec('pnpm run create-admin', { env })
|
||||
}
|
||||
} else {
|
||||
logWarning('未设置 DATABASE_URL')
|
||||
}
|
||||
|
||||
// 7. 构建应用
|
||||
logStep('🔨', '构建应用...')
|
||||
const buildEnv = {
|
||||
...process.env,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS || DEFAULT_NODE_OPTIONS
|
||||
}
|
||||
if (!safeExec('pnpm exec nuxt build', { env: buildEnv })) {
|
||||
throw new Error('构建失败')
|
||||
}
|
||||
logSuccess('构建完成')
|
||||
|
||||
// 8. 验证构建输出
|
||||
const hasNetlifyFunctions = fileExists('.netlify/functions-internal/server')
|
||||
const hasOutputPublic = fileExists('.output/public')
|
||||
|
||||
if (hasNetlifyFunctions) {
|
||||
logSuccess('Netlify Functions 生成成功')
|
||||
}
|
||||
|
||||
if (hasOutputPublic) {
|
||||
logSuccess('静态资源生成成功')
|
||||
}
|
||||
|
||||
if (!hasNetlifyFunctions && !hasOutputPublic) {
|
||||
throw new Error('构建输出目录不存在')
|
||||
}
|
||||
|
||||
log('🎉 构建完成!', 'green')
|
||||
} catch (error) {
|
||||
logError(`构建失败: ${error.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行构建
|
||||
netlifyBuild().catch((error) => {
|
||||
logError(`未预期的错误: ${error.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "voicehub-scripts",
|
||||
"version": "1.0.0",
|
||||
"description": "VoiceHub管理脚本",
|
||||
"packageManager": "pnpm@10.29.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"create-admin": "pnpm exec tsx create-admin.js",
|
||||
"clear-db": "pnpm exec tsx clear-database.js",
|
||||
"safe-migrate": "pnpm exec tsx safe-migrate.js",
|
||||
"db:generate": "cd .. && pnpm exec drizzle-kit generate --config=drizzle.config.ts",
|
||||
"db:migrate": "cd .. && pnpm exec drizzle-kit migrate --config=drizzle.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"chalk": "^5.3.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"prompts": "^2.4.2",
|
||||
"drizzle-orm": "^0.36.4",
|
||||
"postgres": "^3.4.5",
|
||||
"tsx": "^4.7.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs'
|
||||
|
||||
const isCI = process.env.CI || process.env.NETLIFY || process.env.VERCEL
|
||||
|
||||
if (isCI) {
|
||||
console.log('🔧 CI 环境检测')
|
||||
|
||||
try {
|
||||
if (fs.existsSync('drizzle.config.ts') && fs.existsSync('app/drizzle/schema.ts')) {
|
||||
console.log('✅ Drizzle 配置正常')
|
||||
} else {
|
||||
console.warn('⚠️ Drizzle 配置不完整')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 配置检查失败:', error.message)
|
||||
}
|
||||
} else {
|
||||
console.log('📝 本地开发环境')
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { config } from 'dotenv'
|
||||
|
||||
// 加载环境变量
|
||||
config({ path: path.resolve(process.cwd(), '.env') })
|
||||
|
||||
// 颜色输出函数
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m'
|
||||
}
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function logSuccess(message) {
|
||||
log(`✅ ${message}`, 'green')
|
||||
}
|
||||
|
||||
function logWarning(message) {
|
||||
log(`⚠️ ${message}`, 'yellow')
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
log(`❌ ${message}`, 'red')
|
||||
}
|
||||
|
||||
// 检查环境变量
|
||||
if (!process.env.DATABASE_URL) {
|
||||
logError('DATABASE_URL 环境变量未设置')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 安全执行命令
|
||||
function safeExec(command, options = {}) {
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit', ...options })
|
||||
return true
|
||||
} catch (error) {
|
||||
logError(`命令执行失败: ${command}`)
|
||||
logError(error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
function fileExists(filePath) {
|
||||
try {
|
||||
return fs.existsSync(filePath)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理数据冲突的函数
|
||||
async function handleDataConflicts() {
|
||||
try {
|
||||
// 在Vercel环境中,我们主要关注schema同步而不是数据冲突处理
|
||||
log('检查数据库连接...', 'cyan')
|
||||
|
||||
// 简单的连接测试
|
||||
if (!safeExec('cd .. && pnpm exec drizzle-kit check --config=drizzle.config.ts', { stdio: 'pipe' })) {
|
||||
logWarning('数据库schema检查失败,将尝试强制同步')
|
||||
}
|
||||
|
||||
logSuccess('数据冲突检查完成')
|
||||
} catch (error) {
|
||||
logWarning(`数据冲突处理警告: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function safeMigrate() {
|
||||
log('🔄 开始安全数据库迁移流程...', 'bright')
|
||||
|
||||
try {
|
||||
// 获取项目根目录路径
|
||||
const projectRoot = path.resolve(process.cwd(), '..')
|
||||
const drizzleConfigPath = path.join(projectRoot, 'drizzle.config.ts')
|
||||
const schemaPath = path.join(projectRoot, 'app/drizzle/schema.ts')
|
||||
const migrationsPath = path.join(projectRoot, 'app/drizzle/migrations')
|
||||
|
||||
// 1. 确保drizzle配置存在
|
||||
if (!fileExists(drizzleConfigPath)) {
|
||||
throw new Error(`drizzle.config.ts 配置文件不存在: ${drizzleConfigPath}`)
|
||||
}
|
||||
|
||||
// 2. 检查schema文件
|
||||
if (!fileExists(schemaPath)) {
|
||||
throw new Error(`app/drizzle/schema.ts 文件不存在: ${schemaPath}`)
|
||||
}
|
||||
|
||||
// 3. 创建迁移目录(如果不存在)
|
||||
if (!fileExists(migrationsPath)) {
|
||||
log('创建迁移目录...', 'cyan')
|
||||
fs.mkdirSync(migrationsPath, { recursive: true })
|
||||
}
|
||||
|
||||
// 自动执行 generate 命令并处理交互
|
||||
async function runGenerateWithAutoConfirm(env) {
|
||||
return new Promise((resolve) => {
|
||||
const { spawn } = require('child_process')
|
||||
const child = spawn('pnpm', ['run', 'db:generate'], {
|
||||
env,
|
||||
shell: true
|
||||
})
|
||||
|
||||
// 监听输出,透传给用户,并自动响应提示
|
||||
child.stdout.on('data', (data) => {
|
||||
process.stdout.write(data)
|
||||
const str = data.toString()
|
||||
// 如果检测到交互提示(通常包含问号或选项),自动发送回车
|
||||
if (str.includes('?') || str.includes('renamed') || str.includes('created')) {
|
||||
try {
|
||||
// 发送回车以选择默认选项(通常是 Created)
|
||||
child.stdin.write('\n')
|
||||
} catch (e) {
|
||||
// 忽略写入错误
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
process.stderr.write(data)
|
||||
})
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve(code === 0)
|
||||
})
|
||||
|
||||
child.on('error', () => {
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 4. 生成迁移文件(如果需要)
|
||||
log('生成数据库迁移文件...', 'cyan')
|
||||
|
||||
// 设置非交互式环境变量
|
||||
const nonInteractiveEnv = {
|
||||
...process.env,
|
||||
DRIZZLE_KIT_FORCE: 'true',
|
||||
CI: 'true',
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
|
||||
// 使用新的自动确认函数
|
||||
const generateSuccess = await runGenerateWithAutoConfirm(nonInteractiveEnv)
|
||||
|
||||
if (!generateSuccess) {
|
||||
logWarning('迁移文件生成可能遇到问题,尝试继续...')
|
||||
} else {
|
||||
logSuccess('迁移文件生成完成')
|
||||
}
|
||||
|
||||
// 5. 预处理数据冲突
|
||||
log('🔍 检查并处理数据冲突...', 'cyan')
|
||||
await handleDataConflicts()
|
||||
|
||||
// 6. 检查是否为全新部署(数据库为空)
|
||||
log('📋 检查数据库状态...', 'cyan')
|
||||
|
||||
// 设置非交互式环境变量
|
||||
const env = {
|
||||
...process.env,
|
||||
DRIZZLE_KIT_FORCE: 'true',
|
||||
CI: 'true',
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
|
||||
// 检查数据库是否有任何表(基于 introspect 输出更稳健的判断)
|
||||
let isEmptyDatabase = false
|
||||
try {
|
||||
const checkResult = execSync(
|
||||
'cd .. && pnpm exec drizzle-kit introspect --config=drizzle.config.ts',
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env,
|
||||
encoding: 'utf8'
|
||||
}
|
||||
)
|
||||
|
||||
// 优先解析 "<n> tables" 提示,其次检查是否包含 columns 索引等摘要
|
||||
const tablesMatch = checkResult.match(/(\d+)\s+tables/i)
|
||||
const hasTablesCount = tablesMatch && Number(tablesMatch[1]) > 0
|
||||
const listsTables = /\bcolumns\b|\bindexes\b|\bfks\b/i.test(checkResult)
|
||||
|
||||
// 数据库为空当且仅当:统计为0且没有任何表摘要
|
||||
isEmptyDatabase = !(hasTablesCount || listsTables)
|
||||
} catch (error) {
|
||||
// introspect 失败时不轻易判定为空库,改为保守:视为非空库,走 push 路径
|
||||
logWarning('数据库状态检查失败,按非空库处理以避免重复建表')
|
||||
isEmptyDatabase = false
|
||||
}
|
||||
|
||||
if (isEmptyDatabase) {
|
||||
log('🆕 检测到全新部署,执行标准迁移...', 'cyan')
|
||||
// 对于全新部署,直接使用migrate避免交互式提示
|
||||
if (!safeExec('cd .. && pnpm run db:migrate', { env })) {
|
||||
throw new Error('数据库迁移失败')
|
||||
}
|
||||
logSuccess('全新数据库迁移成功')
|
||||
} else {
|
||||
log('🔄 检测到现有数据库,执行schema同步...', 'cyan')
|
||||
// 对于现有数据库,使用push进行增量更新
|
||||
if (safeExec('cd .. && pnpm exec drizzle-kit push --force --config=drizzle.config.ts', { env })) {
|
||||
logSuccess('数据库schema同步成功')
|
||||
} else {
|
||||
logWarning('schema同步失败,尝试标准迁移...')
|
||||
|
||||
// 7. 执行迁移(作为后备)
|
||||
if (!safeExec('cd .. && pnpm run db:migrate', { env })) {
|
||||
throw new Error('数据库迁移完全失败')
|
||||
}
|
||||
logSuccess('数据库迁移成功')
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 验证迁移结果
|
||||
log('✅ 数据库迁移流程完成!', 'green')
|
||||
} catch (error) {
|
||||
logError(`迁移失败: ${error.message}`)
|
||||
logError('请检查数据库连接和迁移文件')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行迁移
|
||||
safeMigrate().catch((error) => {
|
||||
logError(`未预期的错误: ${error.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user