feat: 迭代
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 数据库迁移执行脚本
|
||||
* 用法: node database/migrate.js [migration_file]
|
||||
* 示例: node database/migrate.js 002_add_merchant_cover_image.sql
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m',
|
||||
bold: '\x1b[1m',
|
||||
};
|
||||
|
||||
function log(msg, color = '') {
|
||||
console.log(color ? `${color}${msg}${colors.reset}` : msg);
|
||||
}
|
||||
|
||||
function loadEnv() {
|
||||
const envPaths = [
|
||||
path.resolve(__dirname, '../apps/server/.env.local'),
|
||||
path.resolve(__dirname, '../apps/server/.env'),
|
||||
];
|
||||
|
||||
let envFile = null;
|
||||
for (const p of envPaths) {
|
||||
if (fs.existsSync(p)) { envFile = p; break; }
|
||||
}
|
||||
|
||||
if (!envFile) {
|
||||
log('错误: 未找到 .env.local 或 .env 文件', colors.red);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const env = {};
|
||||
for (const line of fs.readFileSync(envFile, 'utf-8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const i = trimmed.indexOf('=');
|
||||
if (i === -1) continue;
|
||||
env[trimmed.slice(0, i).trim()] = trimmed.slice(i + 1).trim();
|
||||
}
|
||||
|
||||
return {
|
||||
host: env.DB_HOST || 'localhost',
|
||||
port: parseInt(env.DB_PORT || '3306', 10),
|
||||
user: env.DB_USERNAME || 'root',
|
||||
password: env.DB_PASSWORD || '',
|
||||
database: env.DB_DATABASE || 'rent_platform',
|
||||
};
|
||||
}
|
||||
|
||||
async function runSqlFile(conn, filePath) {
|
||||
let raw = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// 移除注释行
|
||||
raw = raw.replace(/--.*$/gm, '');
|
||||
// 移除 USE 语句(已在代码中处理)
|
||||
raw = raw.replace(/USE\s+\S+\s*;/gi, '');
|
||||
|
||||
// 按 ; 分割,过滤空语句
|
||||
const stmts = raw
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
|
||||
let ok = 0;
|
||||
let skip = 0;
|
||||
let errors = [];
|
||||
|
||||
for (const stmt of stmts) {
|
||||
try {
|
||||
await conn.execute(stmt);
|
||||
ok++;
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_FIELDNAME' || err.code === 'ER_DUP_KEYNAME') {
|
||||
skip++;
|
||||
log(` ⚠ 跳过: ${err.message.slice(0, 80)}`, colors.yellow);
|
||||
} else {
|
||||
errors.push({ code: err.code, message: err.message, stmt: stmt.slice(0, 100) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ok, skip, errors };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
log('');
|
||||
log('用法: node database/migrate.js <migration_file>', colors.cyan);
|
||||
log('示例: node database/migrate.js 002_add_merchant_cover_image.sql', colors.cyan);
|
||||
log('');
|
||||
log('可用的迁移文件:', colors.yellow);
|
||||
const migrationsDir = path.resolve(__dirname, 'migrations');
|
||||
const files = fs.readdirSync(migrationsDir).filter(f => f.endsWith('.sql'));
|
||||
files.forEach(f => log(` - ${f}`, colors.cyan));
|
||||
log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const migrationFile = args[0];
|
||||
const migrationPath = path.resolve(__dirname, 'migrations', migrationFile);
|
||||
|
||||
if (!fs.existsSync(migrationPath)) {
|
||||
log(`错误: 迁移文件不存在: ${migrationPath}`, colors.red);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = loadEnv();
|
||||
|
||||
log('');
|
||||
log('==========================================', colors.cyan);
|
||||
log(' 数据库迁移执行', colors.bold + colors.cyan);
|
||||
log('==========================================', colors.cyan);
|
||||
log(` 主机: ${config.host}:${config.port}`);
|
||||
log(` 用户: ${config.user}`);
|
||||
log(` 数据库: ${config.database}`);
|
||||
log(` 迁移文件: ${migrationFile}`);
|
||||
log('==========================================', colors.cyan);
|
||||
log('');
|
||||
|
||||
let conn;
|
||||
try {
|
||||
conn = await mysql.createConnection({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
});
|
||||
log('✓ 数据库连接成功', colors.green);
|
||||
} catch (err) {
|
||||
log(`连接失败: ${err.message}`, colors.red);
|
||||
log('请检查 apps/server/.env.local 中的数据库配置', colors.red);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log('');
|
||||
log('执行迁移...', colors.yellow);
|
||||
|
||||
const result = await runSqlFile(conn, migrationPath);
|
||||
|
||||
log('');
|
||||
if (result.errors.length > 0) {
|
||||
log('==========================================', colors.red);
|
||||
log(` ✗ 迁移失败`, colors.bold + colors.red);
|
||||
log('==========================================', colors.red);
|
||||
log('');
|
||||
log('错误详情:', colors.red);
|
||||
result.errors.forEach((err, i) => {
|
||||
log(` ${i + 1}. [${err.code}] ${err.message}`, colors.red);
|
||||
log(` SQL: ${err.stmt}...`, colors.yellow);
|
||||
});
|
||||
log('');
|
||||
} else {
|
||||
log('==========================================', colors.green);
|
||||
log(' ✓ 迁移完成!', colors.bold + colors.green);
|
||||
log('==========================================', colors.green);
|
||||
log('');
|
||||
log(` 成功: ${result.ok} 条语句`, colors.green);
|
||||
if (result.skip > 0) {
|
||||
log(` 跳过: ${result.skip} 条语句 (已存在)`, colors.yellow);
|
||||
}
|
||||
log('');
|
||||
}
|
||||
|
||||
await conn.end();
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(`\n迁移失败: ${err.message}`, colors.red);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
-- ============================================================
|
||||
-- 迁移: 为商家表添加封面图片字段
|
||||
-- 版本: 002
|
||||
-- 日期: 2024-05-11
|
||||
-- ============================================================
|
||||
|
||||
USE `rent_platform`;
|
||||
|
||||
-- 为 merchants 表添加 cover_image 字段
|
||||
ALTER TABLE `merchants`
|
||||
ADD COLUMN `cover_image` VARCHAR(500) DEFAULT '' COMMENT '店铺封面图片' AFTER `logo`;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 为 merchants 表添加 sales_count 字段
|
||||
ALTER TABLE merchants ADD COLUMN sales_count INT UNSIGNED DEFAULT 0 COMMENT '销量统计';
|
||||
|
||||
-- 为 sales_count 字段添加索引,方便按销量排序
|
||||
CREATE INDEX idx_merchants_sales_count ON merchants(sales_count);
|
||||
Reference in New Issue
Block a user