befly 2.0.14 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/scripts/dbSync.js DELETED
@@ -1,714 +0,0 @@
1
- import path from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
- import { existsSync, readFileSync } from 'node:fs';
4
- import { Env } from '../config/env.js';
5
- import { Logger } from '../utils/logger.js';
6
- import { parseFieldRule } from '../utils/util.js';
7
- import { __dirtables, getProjectDir } from '../system.js';
8
- import tableCheck from '../checks/table.js';
9
-
10
- // 自动加载环境配置文件
11
- const loadEnvFile = () => {
12
- const envFiles = [path.join(process.cwd(), '.env.development'), path.join(process.cwd(), '.env.local'), path.join(process.cwd(), '.env')];
13
-
14
- for (const envFile of envFiles) {
15
- if (existsSync(envFile)) {
16
- console.log(`📄 加载环境配置文件: ${envFile}`);
17
- const envContent = readFileSync(envFile, 'utf8');
18
- const lines = envContent.split('\n');
19
-
20
- for (const line of lines) {
21
- const trimmed = line.trim();
22
- if (trimmed && !trimmed.startsWith('#')) {
23
- const [key, ...valueParts] = trimmed.split('=');
24
- if (key && valueParts.length > 0) {
25
- const value = valueParts.join('=').replace(/^["']|["']$/g, '');
26
- process.env[key] = value;
27
- }
28
- }
29
- }
30
- break;
31
- }
32
- }
33
- };
34
-
35
- // 初始化时加载环境配置
36
- loadEnvFile();
37
-
38
- // 数据类型映射到数据库字段类型
39
- const typeMapping = {
40
- number: 'BIGINT',
41
- string: 'VARCHAR',
42
- text: 'MEDIUMTEXT',
43
- array: 'VARCHAR' // 使用管道符连接元素存储
44
- };
45
-
46
- // 获取字段的SQL定义
47
- const getColumnDefinition = (fieldName, rule, withoutIndex = false) => {
48
- const ruleParts = parseFieldRule(rule);
49
- const [displayName, type, minStr, maxStr, defaultValue, hasIndex, spec] = ruleParts;
50
-
51
- let sqlType = typeMapping[type];
52
- if (!sqlType) {
53
- throw new Error(`不支持的数据类型: ${type}`);
54
- }
55
-
56
- // 处理字符串类型的长度
57
- if (type === 'string') {
58
- const maxLength = maxStr === 'null' ? 255 : parseInt(maxStr);
59
- sqlType = `VARCHAR(${maxLength})`;
60
- }
61
-
62
- // 处理数组类型的长度
63
- if (type === 'array') {
64
- const maxLength = maxStr === 'null' ? 1000 : parseInt(maxStr);
65
- sqlType = `VARCHAR(${maxLength})`;
66
- }
67
-
68
- // 构建完整的列定义
69
- let columnDef = `\`${fieldName}\` ${sqlType} NOT NULL`;
70
-
71
- // 添加默认值
72
- if (defaultValue && defaultValue !== 'null') {
73
- if (type === 'string') {
74
- columnDef += ` DEFAULT "${defaultValue.replace(/"/g, '\\"')}"`;
75
- } else if (type === 'number') {
76
- columnDef += ` DEFAULT ${defaultValue}`;
77
- } else if (type === 'array') {
78
- columnDef += ` DEFAULT "${defaultValue.replace(/"/g, '\\"')}"`;
79
- }
80
- // text 类型不添加默认值,因为MySQL不支持TEXT类型的默认值
81
- } else {
82
- // 根据字段类型设置合适的默认值,所有字段都不允许为NULL
83
- if (type === 'string' || type === 'array') {
84
- columnDef += ` DEFAULT ""`;
85
- } else if (type === 'number') {
86
- columnDef += ` DEFAULT 0`;
87
- }
88
- // text 类型不添加默认值,因为MySQL不支持TEXT类型的默认值
89
- }
90
-
91
- // 添加注释
92
- if (displayName && displayName !== 'null') {
93
- columnDef += ` COMMENT "${displayName.replace(/"/g, '\\"')}"`;
94
- }
95
-
96
- return columnDef;
97
- };
98
-
99
- // 创建数据库连接
100
- const createConnection = async () => {
101
- console.log(`🔍 检查 MySQL 配置...`);
102
- console.log(`MYSQL_ENABLE: ${process.env.MYSQL_ENABLE}`);
103
- console.log(`MYSQL_HOST: ${process.env.MYSQL_HOST}`);
104
- console.log(`MYSQL_PORT: ${process.env.MYSQL_PORT}`);
105
- console.log(`MYSQL_DB: ${process.env.MYSQL_DB}`);
106
- console.log(`MYSQL_USER: ${process.env.MYSQL_USER}`);
107
-
108
- if (Env.MYSQL_ENABLE !== 1) {
109
- throw new Error('MySQL 未启用,请在环境变量中设置 MYSQL_ENABLE=1');
110
- }
111
-
112
- console.log(`📦 导入 mariadb 驱动...`);
113
- const mariadb = await import('mariadb');
114
-
115
- const config = {
116
- host: Env.MYSQL_HOST || '127.0.0.1',
117
- port: Env.MYSQL_PORT || 3306,
118
- database: Env.MYSQL_DB || 'test',
119
- user: Env.MYSQL_USER || 'root',
120
- password: Env.MYSQL_PASSWORD || 'root',
121
- charset: 'utf8mb4',
122
- timezone: Env.TIMEZONE || 'local'
123
- };
124
-
125
- console.log(`🔌 尝试连接数据库...`);
126
- console.log(`连接配置: ${config.user}@${config.host}:${config.port}/${config.database}`);
127
-
128
- return await mariadb.createConnection(config);
129
- };
130
-
131
- // 检查表是否存在
132
- const tableExists = async (conn, tableName) => {
133
- const result = await conn.query('SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', [Env.MYSQL_DB || 'test', tableName]);
134
- return result[0].count > 0;
135
- };
136
-
137
- // 获取表的现有列信息
138
- const getTableColumns = async (conn, tableName) => {
139
- const result = await conn.query(
140
- `SELECT
141
- COLUMN_NAME,
142
- DATA_TYPE,
143
- CHARACTER_MAXIMUM_LENGTH,
144
- NUMERIC_PRECISION,
145
- NUMERIC_SCALE,
146
- IS_NULLABLE,
147
- COLUMN_DEFAULT,
148
- COLUMN_COMMENT,
149
- COLUMN_TYPE
150
- FROM information_schema.COLUMNS
151
- WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
152
- ORDER BY ORDINAL_POSITION`,
153
- [Env.MYSQL_DB || 'test', tableName]
154
- );
155
-
156
- const columns = {};
157
- result.forEach((row) => {
158
- columns[row.COLUMN_NAME] = {
159
- type: row.DATA_TYPE,
160
- columnType: row.COLUMN_TYPE, // 完整的类型定义,如 varchar(255)
161
- length: row.CHARACTER_MAXIMUM_LENGTH,
162
- precision: row.NUMERIC_PRECISION,
163
- scale: row.NUMERIC_SCALE,
164
- nullable: row.IS_NULLABLE === 'YES',
165
- defaultValue: row.COLUMN_DEFAULT,
166
- comment: row.COLUMN_COMMENT
167
- };
168
- });
169
- return columns;
170
- };
171
-
172
- // 获取表的现有索引信息
173
- const getTableIndexes = async (conn, tableName) => {
174
- const result = await conn.query(
175
- `SELECT INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX
176
- FROM information_schema.STATISTICS
177
- WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME != 'PRIMARY'
178
- ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
179
- [Env.MYSQL_DB || 'test', tableName]
180
- );
181
-
182
- const indexes = {};
183
- result.forEach((row) => {
184
- if (!indexes[row.INDEX_NAME]) {
185
- indexes[row.INDEX_NAME] = [];
186
- }
187
- indexes[row.INDEX_NAME].push(row.COLUMN_NAME);
188
- });
189
- return indexes;
190
- };
191
-
192
- // 创建索引
193
- const createIndex = async (conn, tableName, fieldName, dbInfo) => {
194
- const indexName = `idx_${fieldName}`;
195
- const createIndexSQL = `CREATE INDEX \`${indexName}\` ON \`${tableName}\` (\`${fieldName}\`)`;
196
-
197
- try {
198
- await conn.query(createIndexSQL);
199
- Logger.info(`表 ${tableName} 字段 ${fieldName} 索引创建成功`);
200
- } catch (error) {
201
- Logger.error(`创建索引失败: ${error.message}`);
202
- throw error;
203
- }
204
- };
205
-
206
- // 删除索引
207
- const dropIndex = async (conn, tableName, indexName) => {
208
- const dropIndexSQL = `DROP INDEX \`${indexName}\` ON \`${tableName}\``;
209
-
210
- try {
211
- await conn.query(dropIndexSQL);
212
- Logger.info(`表 ${tableName} 索引 ${indexName} 删除成功`);
213
- } catch (error) {
214
- Logger.error(`删除索引失败: ${error.message}`);
215
- throw error;
216
- }
217
- };
218
-
219
- // 创建表
220
- const createTable = async (conn, tableName, fields) => {
221
- const columns = [];
222
- const indexes = [];
223
-
224
- // 添加系统默认字段
225
- columns.push('`id` BIGINT PRIMARY KEY COMMENT "主键ID"');
226
- columns.push('`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT "创建时间"');
227
- columns.push('`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT "更新时间"');
228
- columns.push('`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT "删除时间"');
229
- columns.push('`state` BIGINT NOT NULL DEFAULT 0 COMMENT "状态字段"');
230
-
231
- // 添加系统字段的索引
232
- indexes.push('INDEX `idx_created_at` (`created_at`)');
233
- indexes.push('INDEX `idx_updated_at` (`updated_at`)');
234
- indexes.push('INDEX `idx_state` (`state`)');
235
-
236
- // 添加自定义字段
237
- for (const [fieldName, rule] of Object.entries(fields)) {
238
- const columnDef = getColumnDefinition(fieldName, rule);
239
- columns.push(columnDef);
240
-
241
- // 检查是否需要创建索引
242
- const ruleParts = parseFieldRule(rule);
243
- const hasIndex = ruleParts[5]; // 第6个参数是索引设置
244
-
245
- if (hasIndex && hasIndex !== 'null' && hasIndex !== '0' && hasIndex.toLowerCase() !== 'false') {
246
- indexes.push(`INDEX \`idx_${fieldName}\` (\`${fieldName}\`)`);
247
- console.log(`📊 为字段 ${tableName}.${fieldName} 创建索引`);
248
- }
249
- }
250
-
251
- const createTableSQL = `
252
- CREATE TABLE \`${tableName}\` (
253
- ${columns.join(',\n ')},
254
- ${indexes.join(',\n ')}
255
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT="${tableName} 表"
256
- `;
257
-
258
- await conn.query(createTableSQL);
259
- Logger.info(`表 ${tableName} 创建成功`);
260
- };
261
-
262
- // 比较字段定义是否有变化
263
- const compareFieldDefinition = (existingColumn, newRule) => {
264
- const ruleParts = parseFieldRule(newRule);
265
- const [displayName, type, minStr, maxStr, defaultValue, hasIndex, spec] = ruleParts;
266
- const changes = [];
267
-
268
- // 检查数据类型变化
269
- const expectedType = typeMapping[type];
270
-
271
- // 对于 string 类型,检查长度变化
272
- if (type === 'string') {
273
- const newMaxLength = maxStr === 'null' ? 255 : parseInt(maxStr);
274
- const currentLength = existingColumn.length;
275
-
276
- if (currentLength !== newMaxLength) {
277
- changes.push({
278
- type: 'length',
279
- current: currentLength,
280
- new: newMaxLength,
281
- field: 'CHARACTER_MAXIMUM_LENGTH'
282
- });
283
- }
284
- }
285
-
286
- // 检查注释变化
287
- if (displayName && displayName !== 'null') {
288
- const currentComment = existingColumn.comment || '';
289
- if (currentComment !== displayName) {
290
- changes.push({
291
- type: 'comment',
292
- current: currentComment,
293
- new: displayName,
294
- field: 'COLUMN_COMMENT'
295
- });
296
- }
297
- }
298
-
299
- // 检查默认值变化
300
- if (defaultValue && defaultValue !== 'null') {
301
- const currentDefault = existingColumn.defaultValue;
302
- let expectedDefault = defaultValue;
303
-
304
- // 根据类型格式化期望的默认值
305
- if (type === 'string' || type === 'text' || type === 'array') {
306
- expectedDefault = defaultValue;
307
- } else if (type === 'number') {
308
- expectedDefault = parseInt(defaultValue);
309
- }
310
-
311
- if (currentDefault !== expectedDefault) {
312
- changes.push({
313
- type: 'default',
314
- current: currentDefault,
315
- new: expectedDefault,
316
- field: 'COLUMN_DEFAULT'
317
- });
318
- }
319
- }
320
-
321
- // 检查基础数据类型变化
322
- const currentType = existingColumn.type.toLowerCase();
323
- let expectedDbType = '';
324
-
325
- switch (type) {
326
- case 'number':
327
- expectedDbType = 'bigint';
328
- break;
329
- case 'string':
330
- expectedDbType = 'varchar';
331
- break;
332
- case 'text':
333
- expectedDbType = 'mediumtext';
334
- break;
335
- case 'array':
336
- expectedDbType = 'varchar';
337
- break;
338
- }
339
-
340
- if (currentType !== expectedDbType) {
341
- changes.push({
342
- type: 'datatype',
343
- current: currentType,
344
- new: expectedDbType,
345
- field: 'DATA_TYPE'
346
- });
347
- }
348
-
349
- return {
350
- hasChanges: changes.length > 0,
351
- changes: changes,
352
- reason: changes.length > 0 ? `发现 ${changes.length} 个变化` : '无变化'
353
- };
354
- };
355
-
356
- // 生成ALTER语句来修改字段(使用MySQL 8 Online DDL)
357
- const generateAlterStatement = (tableName, fieldName, rule, changes) => {
358
- const columnDef = getColumnDefinition(fieldName, rule);
359
-
360
- // 使用 MySQL 8 的 Online DDL 语法
361
- // ALGORITHM=INSTANT: 立即执行,不复制数据
362
- // ALGORITHM=INPLACE: 就地执行,不阻塞DML操作
363
- // LOCK=NONE: 不锁定表,允许并发读写
364
- return `ALTER TABLE \`${tableName}\` MODIFY COLUMN ${columnDef}, ALGORITHM=INPLACE, LOCK=NONE`;
365
- };
366
-
367
- // 生成添加字段的ALTER语句(使用MySQL 8 Online DDL)
368
- const generateAddColumnStatement = (tableName, fieldName, rule) => {
369
- const columnDef = getColumnDefinition(fieldName, rule);
370
-
371
- // 使用 Online DDL 添加字段
372
- return `ALTER TABLE \`${tableName}\` ADD COLUMN ${columnDef}, ALGORITHM=INSTANT, LOCK=NONE`;
373
- };
374
-
375
- // 检查MySQL版本和Online DDL支持
376
- const checkMySQLVersion = async (conn) => {
377
- try {
378
- const result = await conn.query('SELECT VERSION() AS version');
379
- const version = result[0].version;
380
- Logger.info(`MySQL/MariaDB 版本: ${version}`);
381
-
382
- // 检查是否支持 Online DDL
383
- const versionParts = version.split('.');
384
- const majorVersion = parseInt(versionParts[0]);
385
- const minorVersion = parseInt(versionParts[1]);
386
-
387
- const isMySQL = version.toLowerCase().includes('mysql') || !version.toLowerCase().includes('mariadb');
388
- const isMariaDB = version.toLowerCase().includes('mariadb');
389
-
390
- // MySQL 5.6+ 支持 Online DDL,MySQL 8.0+ 支持更完善的 Online DDL
391
- const isMySQL56Plus = isMySQL && (majorVersion > 5 || (majorVersion === 5 && minorVersion >= 6));
392
- // MariaDB 10.0+ 支持 Online DDL
393
- const isMariaDB10Plus = isMariaDB && majorVersion >= 10;
394
-
395
- const supportsOnlineDDL = isMySQL56Plus || isMariaDB10Plus;
396
- Logger.info(`Online DDL 支持: ${supportsOnlineDDL ? '是' : '否'}`);
397
-
398
- if (supportsOnlineDDL) {
399
- Logger.info(`数据库类型: ${isMySQL ? 'MySQL' : 'MariaDB'} ${majorVersion}.${minorVersion}`);
400
- }
401
-
402
- return { version, supportsOnlineDDL };
403
- } catch (error) {
404
- Logger.warn('无法检测数据库版本,使用默认设置');
405
- return { version: 'unknown', supportsOnlineDDL: false };
406
- }
407
- };
408
-
409
- // 安全执行DDL语句
410
- const executeDDLSafely = async (conn, sql, fallbackSql = null) => {
411
- try {
412
- Logger.info(`执行SQL: ${sql}`);
413
- await conn.query(sql);
414
- return true;
415
- } catch (error) {
416
- Logger.warn(`Online DDL 执行失败: ${error.message}`);
417
-
418
- if (fallbackSql) {
419
- Logger.info(`尝试回退SQL: ${fallbackSql}`);
420
- try {
421
- await conn.query(fallbackSql);
422
- Logger.info('回退SQL执行成功');
423
- return true;
424
- } catch (fallbackError) {
425
- Logger.error(`回退SQL也执行失败: ${fallbackError.message}`);
426
- throw fallbackError;
427
- }
428
- } else {
429
- throw error;
430
- }
431
- }
432
- };
433
-
434
- // 同步表字段
435
- const syncTableFields = async (conn, tableName, fields, dbInfo) => {
436
- const existingColumns = await getTableColumns(conn, tableName);
437
- const systemFields = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'];
438
-
439
- Logger.info(`开始同步表 ${tableName} 的字段...`);
440
- Logger.info(`现有字段数量: ${Object.keys(existingColumns).length}`);
441
- Logger.info(`新定义字段数量: ${Object.keys(fields).length}`);
442
-
443
- for (const [fieldName, rule] of Object.entries(fields)) {
444
- if (existingColumns[fieldName]) {
445
- // 字段已存在,检查是否需要修改
446
- const comparison = compareFieldDefinition(existingColumns[fieldName], rule);
447
-
448
- if (comparison.hasChanges) {
449
- Logger.info(`字段 ${tableName}.${fieldName} 需要更新:`);
450
- comparison.changes.forEach((change) => {
451
- Logger.info(` - ${change.type}: ${change.current} → ${change.new}`);
452
- });
453
-
454
- // 生成Online DDL语句
455
- const onlineSQL = generateAlterStatement(tableName, fieldName, rule, comparison.changes);
456
- const fallbackSQL = `ALTER TABLE \`${tableName}\` MODIFY COLUMN ${getColumnDefinition(fieldName, rule)}`;
457
-
458
- // 安全执行DDL(总是提供回退方案)
459
- await executeDDLSafely(conn, onlineSQL, fallbackSQL);
460
- Logger.info(`表 ${tableName} 字段 ${fieldName} 更新成功`);
461
- } else {
462
- Logger.info(`字段 ${tableName}.${fieldName} 无变化,跳过`);
463
- }
464
- } else {
465
- // 添加新字段
466
- Logger.info(`字段 ${tableName}.${fieldName} 不存在,需要添加`);
467
-
468
- const onlineSQL = generateAddColumnStatement(tableName, fieldName, rule);
469
- const fallbackSQL = `ALTER TABLE \`${tableName}\` ADD COLUMN ${getColumnDefinition(fieldName, rule)}`;
470
-
471
- // 安全执行DDL(总是提供回退方案)
472
- await executeDDLSafely(conn, onlineSQL, fallbackSQL);
473
- Logger.info(`表 ${tableName} 添加字段 ${fieldName} 成功`);
474
-
475
- // 检查新字段是否需要创建索引
476
- const ruleParts = parseFieldRule(rule);
477
- const hasIndex = ruleParts[5]; // 第6个参数是索引设置
478
-
479
- if (hasIndex && hasIndex !== 'null' && hasIndex !== '0' && hasIndex.toLowerCase() !== 'false') {
480
- await createIndex(conn, tableName, fieldName, dbInfo);
481
- }
482
- }
483
- }
484
-
485
- // 同步索引
486
- Logger.info(`开始同步表 ${tableName} 的索引...`);
487
- await syncTableIndexes(conn, tableName, fields, dbInfo);
488
-
489
- Logger.info(`表 ${tableName} 字段和索引同步完成`);
490
- };
491
-
492
- // 同步表索引
493
- const syncTableIndexes = async (conn, tableName, fields, dbInfo) => {
494
- // 获取现有索引
495
- const existingIndexes = await getTableIndexes(conn, tableName);
496
-
497
- // 系统字段索引(这些索引在表创建时已经建立)
498
- const systemIndexes = ['idx_created_at', 'idx_updated_at', 'idx_state'];
499
-
500
- // 收集需要创建的索引
501
- const requiredIndexes = [];
502
-
503
- for (const [fieldName, rule] of Object.entries(fields)) {
504
- const ruleParts = parseFieldRule(rule);
505
- const hasIndex = ruleParts[5]; // 第6个参数是索引设置
506
-
507
- if (hasIndex && hasIndex !== 'null' && hasIndex !== '0' && hasIndex.toLowerCase() !== 'false') {
508
- const indexName = `idx_${fieldName}`;
509
- requiredIndexes.push({ fieldName, indexName });
510
- }
511
- }
512
-
513
- // 检查需要创建的索引
514
- for (const { fieldName, indexName } of requiredIndexes) {
515
- if (!existingIndexes[indexName]) {
516
- Logger.info(`字段 ${tableName}.${fieldName} 需要创建索引`);
517
- await createIndex(conn, tableName, fieldName, dbInfo);
518
- } else {
519
- Logger.info(`字段 ${tableName}.${fieldName} 索引已存在,跳过`);
520
- }
521
- }
522
-
523
- // 检查需要删除的索引(字段定义中不再需要索引的字段)
524
- for (const [indexName, columns] of Object.entries(existingIndexes)) {
525
- // 跳过系统索引
526
- if (systemIndexes.includes(indexName)) {
527
- continue;
528
- }
529
-
530
- // 检查是否为单字段索引且该字段在当前定义中不需要索引
531
- if (columns.length === 1) {
532
- const fieldName = columns[0];
533
-
534
- // 检查该字段是否在当前表定义中
535
- if (fields[fieldName]) {
536
- const ruleParts = parseFieldRule(fields[fieldName]);
537
- const hasIndex = ruleParts[5];
538
-
539
- // 如果字段定义中不需要索引,则删除现有索引
540
- if (!hasIndex || hasIndex === 'null' || hasIndex === '0' || hasIndex.toLowerCase() === 'false') {
541
- Logger.info(`字段 ${tableName}.${fieldName} 不再需要索引,删除索引 ${indexName}`);
542
- await dropIndex(conn, tableName, indexName);
543
- }
544
- } else {
545
- // 字段已被删除,但我们不处理字段删除,只记录
546
- Logger.info(`字段 ${tableName}.${fieldName} 不在当前定义中,保留索引 ${indexName}`);
547
- }
548
- }
549
- }
550
-
551
- Logger.info(`表 ${tableName} 索引同步完成`);
552
- };
553
-
554
- // 处理单个表文件
555
- const processTableFile = async (conn, filePath, dbInfo) => {
556
- const fileName = path.basename(filePath, '.json');
557
- const tableName = fileName;
558
-
559
- Logger.info(`处理表定义文件: ${fileName}`);
560
-
561
- // 读取表定义
562
- const tableDefinition = await Bun.file(filePath).json();
563
-
564
- // 检查表是否存在
565
- const exists = await tableExists(conn, tableName);
566
- Logger.info(`表 ${tableName} 存在状态: ${exists}`);
567
-
568
- if (exists) {
569
- Logger.info(`表 ${tableName} 已存在,检查字段变化并同步...`);
570
- await syncTableFields(conn, tableName, tableDefinition, dbInfo);
571
- } else {
572
- Logger.info(`表 ${tableName} 不存在,创建新表...`);
573
- await createTable(conn, tableName, tableDefinition);
574
- }
575
-
576
- Logger.info(`表 ${tableName} 处理完成`);
577
- };
578
-
579
- // 主同步函数
580
- const syncDatabase = async () => {
581
- let conn = null;
582
-
583
- try {
584
- Logger.info('开始数据库表结构同步...');
585
-
586
- // 首先执行表定义验证
587
- Logger.info('步骤 1/3: 验证表定义文件...');
588
- const tableValidationResult = await tableCheck();
589
-
590
- if (!tableValidationResult) {
591
- throw new Error('表定义验证失败,请检查表定义文件格式。同步操作已取消。');
592
- }
593
-
594
- Logger.info('✅ 表定义验证通过,继续执行数据库同步...');
595
-
596
- // 创建数据库连接
597
- Logger.info('步骤 2/3: 建立数据库连接...');
598
- conn = await createConnection();
599
- Logger.info('数据库连接成功');
600
-
601
- // 检查数据库版本和 Online DDL 支持
602
- const dbInfo = await checkMySQLVersion(conn);
603
- Logger.info(`数据库信息: ${dbInfo.version}`);
604
- Logger.info(`Online DDL 支持: ${dbInfo.supportsOnlineDDL ? '是' : '否'}`);
605
-
606
- // 扫描tables目录
607
- Logger.info('步骤 3/3: 同步数据库表结构...');
608
- const tablesGlob = new Bun.Glob('*.json');
609
- const coreTablesDir = __dirtables;
610
- const userTablesDir = getProjectDir('tables');
611
-
612
- let processedCount = 0;
613
- let createdTables = 0;
614
- let modifiedTables = 0;
615
-
616
- Logger.info('开始处理表定义文件...');
617
-
618
- // 处理核心表定义
619
- Logger.info(`扫描核心表目录: ${coreTablesDir}`);
620
- try {
621
- for await (const file of tablesGlob.scan({
622
- cwd: coreTablesDir,
623
- absolute: true,
624
- onlyFiles: true
625
- })) {
626
- const tableName = path.basename(file, '.json');
627
- const exists = await tableExists(conn, tableName);
628
-
629
- try {
630
- await processTableFile(conn, file, dbInfo);
631
- if (exists) {
632
- modifiedTables++;
633
- } else {
634
- createdTables++;
635
- }
636
- processedCount++;
637
- } catch (error) {
638
- Logger.error(`处理表文件 ${file} 时出错:`, error.message);
639
- console.error(`错误详情:`, error);
640
- throw error;
641
- }
642
- }
643
- } catch (error) {
644
- Logger.warn('核心表目录扫描出错:', error.message);
645
- }
646
-
647
- // 处理用户表定义
648
- Logger.info(`扫描用户表目录: ${userTablesDir}`);
649
- try {
650
- for await (const file of tablesGlob.scan({
651
- cwd: userTablesDir,
652
- absolute: true,
653
- onlyFiles: true
654
- })) {
655
- const tableName = path.basename(file, '.json');
656
- const exists = await tableExists(conn, tableName);
657
-
658
- try {
659
- await processTableFile(conn, file, dbInfo);
660
- if (exists) {
661
- modifiedTables++;
662
- } else {
663
- createdTables++;
664
- }
665
- processedCount++;
666
- } catch (error) {
667
- Logger.error(`处理表文件 ${file} 时出错:`, error.message);
668
- console.error(`错误详情:`, error);
669
- throw error;
670
- }
671
- }
672
- } catch (error) {
673
- Logger.warn('用户表目录扫描出错:', error.message);
674
- }
675
-
676
- // 显示同步统计信息
677
- Logger.info('='.repeat(50));
678
- Logger.info('数据库表结构同步完成');
679
- Logger.info('='.repeat(50));
680
- Logger.info(`总处理表数: ${processedCount}`);
681
- Logger.info(`新创建表数: ${createdTables}`);
682
- Logger.info(`修改表数: ${modifiedTables}`);
683
- Logger.info(`使用的DDL模式: ${dbInfo.supportsOnlineDDL ? 'Online DDL (无锁)' : '传统DDL'}`);
684
- Logger.info(`数据库版本: ${dbInfo.version}`);
685
- Logger.info('='.repeat(50));
686
-
687
- if (processedCount === 0) {
688
- Logger.warn('没有找到任何表定义文件,请检查 tables/ 目录');
689
- }
690
- } catch (error) {
691
- Logger.error('数据库同步失败:', error);
692
- process.exit(1);
693
- } finally {
694
- if (conn) {
695
- try {
696
- await conn.end();
697
- Logger.info('数据库连接已关闭');
698
- } catch (error) {
699
- Logger.warn('关闭数据库连接时出错:', error.message);
700
- }
701
- }
702
- }
703
- };
704
-
705
- // 如果直接运行此脚本或通过 CLI 调用
706
- if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('dbSync.js')) {
707
- console.log(`🚀 开始执行数据库同步脚本...`);
708
- syncDatabase().catch((error) => {
709
- console.error('❌ 数据库同步失败:', error);
710
- process.exit(1);
711
- });
712
- }
713
-
714
- export { syncDatabase };