befly 3.2.1 → 3.3.1

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.
Files changed (73) hide show
  1. package/bin/index.ts +138 -0
  2. package/checks/conflict.ts +35 -25
  3. package/checks/table.ts +6 -6
  4. package/commands/addon.ts +57 -0
  5. package/commands/build.ts +74 -0
  6. package/commands/dev.ts +94 -0
  7. package/commands/index.ts +252 -0
  8. package/commands/script.ts +303 -0
  9. package/commands/start.ts +80 -0
  10. package/commands/syncApi.ts +327 -0
  11. package/{scripts → commands}/syncDb/apply.ts +2 -2
  12. package/{scripts → commands}/syncDb/constants.ts +13 -7
  13. package/{scripts → commands}/syncDb/ddl.ts +7 -5
  14. package/{scripts → commands}/syncDb/helpers.ts +18 -18
  15. package/{scripts → commands}/syncDb/index.ts +37 -23
  16. package/{scripts → commands}/syncDb/sqlite.ts +1 -1
  17. package/{scripts → commands}/syncDb/state.ts +10 -4
  18. package/{scripts → commands}/syncDb/table.ts +7 -7
  19. package/{scripts → commands}/syncDb/tableCreate.ts +7 -6
  20. package/{scripts → commands}/syncDb/types.ts +5 -5
  21. package/{scripts → commands}/syncDb/version.ts +1 -1
  22. package/commands/syncDb.ts +35 -0
  23. package/commands/syncDev.ts +174 -0
  24. package/commands/syncMenu.ts +368 -0
  25. package/config/env.ts +4 -4
  26. package/config/menu.json +67 -0
  27. package/{utils/crypto.ts → lib/cipher.ts} +16 -67
  28. package/lib/database.ts +296 -0
  29. package/{utils → lib}/dbHelper.ts +102 -56
  30. package/{utils → lib}/jwt.ts +124 -151
  31. package/{utils → lib}/logger.ts +47 -24
  32. package/lib/middleware.ts +271 -0
  33. package/{utils → lib}/redisHelper.ts +4 -4
  34. package/{utils/validate.ts → lib/validator.ts} +101 -78
  35. package/lifecycle/bootstrap.ts +63 -0
  36. package/lifecycle/checker.ts +165 -0
  37. package/lifecycle/cluster.ts +241 -0
  38. package/lifecycle/lifecycle.ts +139 -0
  39. package/lifecycle/loader.ts +513 -0
  40. package/main.ts +14 -12
  41. package/package.json +21 -9
  42. package/paths.ts +34 -0
  43. package/plugins/cache.ts +187 -0
  44. package/plugins/db.ts +4 -4
  45. package/plugins/logger.ts +1 -1
  46. package/plugins/redis.ts +4 -4
  47. package/router/api.ts +155 -0
  48. package/router/root.ts +53 -0
  49. package/router/static.ts +76 -0
  50. package/types/api.d.ts +0 -36
  51. package/types/befly.d.ts +8 -6
  52. package/types/common.d.ts +1 -1
  53. package/types/context.d.ts +3 -3
  54. package/types/util.d.ts +45 -0
  55. package/util.ts +299 -0
  56. package/config/fields.ts +0 -55
  57. package/config/regexAliases.ts +0 -51
  58. package/config/reserved.ts +0 -96
  59. package/scripts/syncDb/tests/constants.test.ts +0 -105
  60. package/scripts/syncDb/tests/ddl.test.ts +0 -134
  61. package/scripts/syncDb/tests/helpers.test.ts +0 -70
  62. package/scripts/syncDb.ts +0 -10
  63. package/types/index.d.ts +0 -450
  64. package/types/index.ts +0 -438
  65. package/types/validator.ts +0 -43
  66. package/utils/colors.ts +0 -221
  67. package/utils/database.ts +0 -348
  68. package/utils/helper.ts +0 -812
  69. package/utils/index.ts +0 -33
  70. package/utils/requestContext.ts +0 -167
  71. /package/{scripts → commands}/syncDb/schema.ts +0 -0
  72. /package/{utils → lib}/sqlBuilder.ts +0 -0
  73. /package/{utils → lib}/xml.ts +0 -0
@@ -0,0 +1,327 @@
1
+ /**
2
+ * SyncApi 命令 - 同步 API 接口数据到数据库
3
+ * 说明:遍历所有 API 文件,收集接口路由信息并同步到 core_api 表
4
+ *
5
+ * 流程:
6
+ * 1. 扫描 core/apis 目录下所有 Core API 文件
7
+ * 2. 扫描项目 apis 目录下所有项目 API 文件
8
+ * 3. 扫描 node_modules/@befly-addon/* 目录下所有组件 API 文件
9
+ * 4. 提取每个 API 的 name、method、auth 等信息
10
+ * 5. 根据接口路径检查是否存在
11
+ * 6. 存在则更新,不存在则新增
12
+ * 7. 删除配置中不存在的接口记录
13
+ */
14
+
15
+ import { Logger } from '../lib/logger.js';
16
+ import { Database } from '../lib/database.js';
17
+ import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
18
+ import { readdirSync, statSync } from 'node:fs';
19
+ import { join, dirname, relative, basename } from 'pathe';
20
+
21
+ interface SyncApiOptions {
22
+ plan?: boolean;
23
+ }
24
+
25
+ interface ApiInfo {
26
+ name: string;
27
+ path: string;
28
+ method: string;
29
+ description: string;
30
+ addonName: string;
31
+ addonTitle: string;
32
+ }
33
+
34
+ /**
35
+ * 递归扫描目录下的所有 .ts 文件
36
+ */
37
+ function scanTsFiles(dir: string, fileList: string[] = []): string[] {
38
+ try {
39
+ const files = readdirSync(dir);
40
+
41
+ for (const file of files) {
42
+ const filePath = join(dir, file);
43
+ const stat = statSync(filePath);
44
+
45
+ if (stat.isDirectory()) {
46
+ scanTsFiles(filePath, fileList);
47
+ } else if (file.endsWith('.ts') && !file.endsWith('.d.ts')) {
48
+ fileList.push(filePath);
49
+ }
50
+ }
51
+ } catch (error: any) {
52
+ Logger.warn(`扫描目录失败: ${dir}`, error.message);
53
+ }
54
+
55
+ return fileList;
56
+ }
57
+
58
+ /**
59
+ * 从 API 文件中提取接口信息
60
+ */
61
+ async function extractApiInfo(filePath: string, apiRoot: string, type: 'core' | 'app' | 'addon', addonName: string = '', addonTitle: string = ''): Promise<ApiInfo | null> {
62
+ try {
63
+ const apiModule = await import(filePath);
64
+ const apiConfig = apiModule.default;
65
+
66
+ if (!apiConfig || !apiConfig.name) {
67
+ return null;
68
+ }
69
+
70
+ let apiPath = '';
71
+
72
+ if (type === 'core') {
73
+ // Core 接口:保留完整目录层级
74
+ // 例: apis/menu/all.ts → /api/core/menu/all
75
+ const relativePath = relative(apiRoot, filePath);
76
+ const pathWithoutExt = relativePath.replace(/\.ts$/, '');
77
+ apiPath = `/api/core/${pathWithoutExt}`;
78
+ } else if (type === 'addon') {
79
+ // Addon 接口:保留完整目录层级
80
+ // 例: apis/menu/list.ts → /api/addon/admin/menu/list
81
+ const relativePath = relative(apiRoot, filePath);
82
+ const pathWithoutExt = relativePath.replace(/\.ts$/, '');
83
+ apiPath = `/api/addon/${addonName}/${pathWithoutExt}`;
84
+ } else {
85
+ // 项目接口:保留完整目录层级
86
+ // 例: apis/user/list.ts → /api/user/list
87
+ const relativePath = relative(apiRoot, filePath);
88
+ const pathWithoutExt = relativePath.replace(/\.ts$/, '');
89
+ apiPath = `/api/${pathWithoutExt}`;
90
+ }
91
+
92
+ return {
93
+ name: apiConfig.name || '',
94
+ path: apiPath,
95
+ method: apiConfig.method || 'POST',
96
+ description: apiConfig.description || '',
97
+ addonName: type === 'core' ? 'core' : addonName,
98
+ addonTitle: type === 'core' ? '核心接口' : addonTitle || addonName
99
+ };
100
+ } catch (error: any) {
101
+ Logger.error(`解析 API 文件失败: ${filePath}`, error);
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * 扫描所有 API 文件
108
+ */
109
+ async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
110
+ const apis: ApiInfo[] = [];
111
+
112
+ // 1. 扫描 Core 框架 API
113
+ Logger.info('=== 扫描 Core 框架 API (core/apis) ===');
114
+ const coreApisDir = join(dirname(projectRoot), 'core', 'apis');
115
+ try {
116
+ const coreApiFiles = scanTsFiles(coreApisDir);
117
+ Logger.info(` 找到 ${coreApiFiles.length} 个核心 API 文件`);
118
+
119
+ for (const filePath of coreApiFiles) {
120
+ const apiInfo = await extractApiInfo(filePath, coreApisDir, 'core', '', '核心接口');
121
+ if (apiInfo) {
122
+ apis.push(apiInfo);
123
+ Logger.info(` └ ${apiInfo.path} - ${apiInfo.name}`);
124
+ }
125
+ }
126
+
127
+ // 2. 扫描项目 API
128
+ Logger.info('\n=== 扫描项目 API (apis) ===');
129
+ const projectApisDir = join(projectRoot, 'apis');
130
+ const projectApiFiles = scanTsFiles(projectApisDir);
131
+ Logger.info(` 找到 ${projectApiFiles.length} 个项目 API 文件`);
132
+
133
+ for (const filePath of projectApiFiles) {
134
+ const apiInfo = await extractApiInfo(filePath, projectApisDir, 'app', '', '项目接口');
135
+ if (apiInfo) {
136
+ apis.push(apiInfo);
137
+ Logger.info(` └ ${apiInfo.path} - ${apiInfo.name}`);
138
+ }
139
+ }
140
+
141
+ // 3. 扫描组件 API (node_modules/@befly-addon/*)
142
+ Logger.info('\n=== 扫描组件 API (node_modules/@befly-addon/*) ===');
143
+ const addonNames = scanAddons();
144
+
145
+ for (const addonName of addonNames) {
146
+ // addonName 格式: admin, demo 等
147
+
148
+ // 检查 apis 子目录是否存在
149
+ if (!addonDirExists(addonName, 'apis')) {
150
+ Logger.info(` [${addonName}] 无 apis 目录,跳过`);
151
+ continue;
152
+ }
153
+
154
+ const addonApisDir = getAddonDir(addonName, 'apis');
155
+
156
+ // 读取 addon 配置
157
+ const addonConfigPath = getAddonDir(addonName, 'addon.config.json');
158
+ let addonTitle = addonName;
159
+ try {
160
+ const configFile = Bun.file(addonConfigPath);
161
+ const config = await configFile.json();
162
+ addonTitle = config.title || addonName;
163
+ } catch (error) {
164
+ Logger.debug(` [${addonName}] 无法读取配置文件,使用默认标题`);
165
+ }
166
+
167
+ const addonApiFiles = scanTsFiles(addonApisDir);
168
+ Logger.info(` [${addonName}] 找到 ${addonApiFiles.length} 个 API 文件`);
169
+
170
+ for (const filePath of addonApiFiles) {
171
+ const apiInfo = await extractApiInfo(filePath, addonApisDir, 'addon', addonName, addonTitle);
172
+ if (apiInfo) {
173
+ apis.push(apiInfo);
174
+ Logger.info(` └ ${apiInfo.path} - ${apiInfo.name}`);
175
+ }
176
+ }
177
+ }
178
+
179
+ return apis;
180
+ } catch (error: any) {
181
+ Logger.error(`接口扫描失败:`, error);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * 同步 API 数据到数据库
187
+ */
188
+ async function syncApis(helper: any, apis: ApiInfo[]): Promise<{ created: number; updated: number }> {
189
+ const stats = { created: 0, updated: 0 };
190
+
191
+ for (const api of apis) {
192
+ try {
193
+ const existing = await helper.getOne({
194
+ table: 'core_api',
195
+ where: { path: api.path }
196
+ });
197
+
198
+ if (existing) {
199
+ await helper.updData({
200
+ table: 'core_api',
201
+ where: { id: existing.id },
202
+ data: {
203
+ name: api.name,
204
+ method: api.method,
205
+ description: api.description,
206
+ addonName: api.addonName,
207
+ addonTitle: api.addonTitle
208
+ }
209
+ });
210
+ stats.updated++;
211
+ Logger.info(` └ 更新接口: ${api.name} (ID: ${existing.id}, Path: ${api.path})`);
212
+ } else {
213
+ const id = await helper.insData({
214
+ table: 'core_api',
215
+ data: {
216
+ name: api.name,
217
+ path: api.path,
218
+ method: api.method,
219
+ description: api.description,
220
+ addonName: api.addonName,
221
+ addonTitle: api.addonTitle
222
+ }
223
+ });
224
+ stats.created++;
225
+ Logger.info(` └ 新增接口: ${api.name} (ID: ${id}, Path: ${api.path})`);
226
+ }
227
+ } catch (error: any) {
228
+ Logger.error(`同步接口 "${api.name}" 失败:`, error);
229
+ }
230
+ }
231
+
232
+ return stats;
233
+ }
234
+
235
+ /**
236
+ * 删除配置中不存在的记录
237
+ */
238
+ async function deleteObsoleteRecords(helper: any, apiPaths: Set<string>): Promise<number> {
239
+ Logger.info(`\n=== 删除配置中不存在的记录 ===`);
240
+
241
+ const allRecords = await helper.getAll({
242
+ table: 'core_api',
243
+ fields: ['id', 'path', 'name'],
244
+ where: { state$gte: 0 } // 查询所有状态(包括软删除的 state=0)
245
+ });
246
+
247
+ let deletedCount = 0;
248
+ for (const record of allRecords) {
249
+ if (record.path && !apiPaths.has(record.path)) {
250
+ await helper.delForce({
251
+ table: 'core_api',
252
+ where: { id: record.id }
253
+ });
254
+ deletedCount++;
255
+ Logger.info(` └ 删除记录: ${record.name} (ID: ${record.id}, path: ${record.path})`);
256
+ }
257
+ }
258
+
259
+ if (deletedCount === 0) {
260
+ Logger.info(' ✅ 无需删除的记录');
261
+ }
262
+
263
+ return deletedCount;
264
+ }
265
+
266
+ /**
267
+ * SyncApi 命令主函数
268
+ */
269
+ export async function syncApiCommand(options: SyncApiOptions = {}) {
270
+ try {
271
+ if (options.plan) {
272
+ Logger.info('[计划] 同步 API 接口到数据库(plan 模式不执行)');
273
+ Logger.info('[计划] 1. 扫描 apis 和 addons/*/apis 目录');
274
+ Logger.info('[计划] 2. 提取每个 API 的配置信息');
275
+ Logger.info('[计划] 3. 根据 path 检查接口是否存在');
276
+ Logger.info('[计划] 4. 存在则更新,不存在则新增');
277
+ Logger.info('[计划] 5. 删除文件中不存在的接口记录');
278
+ return;
279
+ }
280
+
281
+ Logger.info('开始同步 API 接口到数据库...\n');
282
+
283
+ const projectRoot = process.cwd();
284
+
285
+ // 连接数据库(SQL + Redis)
286
+ await Database.connect();
287
+
288
+ const helper = Database.getDbHelper();
289
+
290
+ // 1. 检查表是否存在
291
+ Logger.info('=== 检查数据表 ===');
292
+ const exists = await helper.tableExists('core_api');
293
+
294
+ if (!exists) {
295
+ Logger.error(`❌ 表 core_api 不存在,请先运行 befly syncDb 同步数据库`);
296
+ process.exit(1);
297
+ }
298
+
299
+ Logger.info(`✅ 表 core_api 存在\n`);
300
+
301
+ // 2. 扫描所有 API 文件
302
+ Logger.info('=== 步骤 2: 扫描 API 文件 ===');
303
+ const apis = await scanAllApis(projectRoot);
304
+ const apiPaths = new Set(apis.map((api) => api.path));
305
+ Logger.info(`\n✅ 共扫描到 ${apis.length} 个 API 接口\n`);
306
+
307
+ // 3. 同步 API 数据
308
+ Logger.info('=== 步骤 3: 同步 API 数据(新增/更新) ===');
309
+ const stats = await syncApis(helper, apis);
310
+
311
+ // 4. 删除文件中不存在的接口
312
+ const deletedCount = await deleteObsoleteRecords(helper, apiPaths);
313
+
314
+ // 5. 输出统计信息
315
+ Logger.info(`\n=== 接口同步完成 ===`);
316
+ Logger.info(`新增接口: ${stats.created} 个`);
317
+ Logger.info(`更新接口: ${stats.updated} 个`);
318
+ Logger.info(`删除接口: ${deletedCount} 个`);
319
+ Logger.info(`当前总接口数: ${apis.length} 个`);
320
+ Logger.info('提示: 接口缓存将在服务器启动时自动完成');
321
+ } catch (error: any) {
322
+ Logger.error('API 同步失败:', error);
323
+ process.exit(1);
324
+ } finally {
325
+ await Database?.disconnect();
326
+ }
327
+ }
@@ -6,8 +6,8 @@
6
6
  * - 应用表结构变更计划
7
7
  */
8
8
 
9
- import { Logger } from '../../utils/logger.js';
10
- import { parseRule } from '../../utils/helper.js';
9
+ import { Logger } from '../../lib/logger.js';
10
+ import { parseRule } from '../../util.js';
11
11
  import { IS_MYSQL, IS_PG, IS_SQLITE, CHANGE_TYPE_LABELS, typeMapping } from './constants.js';
12
12
  import { logFieldChange, resolveDefaultValue, isStringOrArrayType } from './helpers.js';
13
13
  import { executeDDLSafely, buildIndexSQL } from './ddl.js';
@@ -2,13 +2,14 @@
2
2
  * syncDb 常量定义模块
3
3
  *
4
4
  * 包含:
5
- * - 数据库版本要求
5
+ * - 数据库类型判断
6
+ * - 版本要求
7
+ * - 数据类型映射
6
8
  * - 系统字段定义
7
- * - 索引字段配置
8
- * - 字段变更类型标签
9
9
  */
10
10
 
11
11
  import { Env } from '../../config/env.js';
12
+ import type { Dialect } from './types.js';
12
13
 
13
14
  /**
14
15
  * 数据库版本要求
@@ -47,12 +48,17 @@ export const CHANGE_TYPE_LABELS = {
47
48
  } as const;
48
49
 
49
50
  /**
50
- * MySQL 表配置(支持环境变量自定义)
51
+ * MySQL 表配置
52
+ *
53
+ * 固定配置说明:
54
+ * - ENGINE: InnoDB(支持事务、外键)
55
+ * - CHARSET: utf8mb4(完整 Unicode 支持,包括 Emoji)
56
+ * - COLLATE: utf8mb4_0900_ai_ci(MySQL 8.0 推荐,不区分重音和大小写)
51
57
  */
52
58
  export const MYSQL_TABLE_CONFIG = {
53
- ENGINE: Env.MYSQL_ENGINE || 'InnoDB',
54
- CHARSET: Env.MYSQL_CHARSET || 'utf8mb4',
55
- COLLATE: Env.MYSQL_COLLATE || 'utf8mb4_0900_as_cs'
59
+ ENGINE: 'InnoDB',
60
+ CHARSET: 'utf8mb4',
61
+ COLLATE: 'utf8mb4_0900_ai_ci'
56
62
  } as const;
57
63
 
58
64
  // 数据库类型判断
@@ -8,12 +8,14 @@
8
8
  * - 构建系统列和业务列定义
9
9
  */
10
10
 
11
- import { Logger } from '../../utils/logger.js';
12
- import { parseRule, toSnakeCase } from '../../utils/helper.js';
13
- import type { ParsedFieldRule, AnyObject } from '../../types/common.js';
11
+ import { snakeCase } from 'es-toolkit/string';
12
+ import { parseRule } from '../../util.js';
13
+ import { Logger } from '../../lib/logger.js';
14
14
  import { IS_MYSQL, IS_PG, typeMapping } from './constants.js';
15
15
  import { quoteIdentifier, resolveDefaultValue, generateDefaultSql, getSqlType, escapeComment } from './helpers.js';
16
+
16
17
  import type { SQL } from 'bun';
18
+ import type { ParsedFieldRule, AnyObject } from 'befly/types/common.js';
17
19
 
18
20
  /**
19
21
  * 构建索引操作 SQL(统一使用在线策略)
@@ -80,7 +82,7 @@ export function buildBusinessColumnDefs(fields: Record<string, string>): string[
80
82
 
81
83
  for (const [fieldKey, fieldRule] of Object.entries(fields)) {
82
84
  // 转换字段名为下划线格式
83
- const dbFieldName = toSnakeCase(fieldKey);
85
+ const dbFieldName = snakeCase(fieldKey);
84
86
 
85
87
  const parsed = parseRule(fieldRule);
86
88
  const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
@@ -110,7 +112,7 @@ export function buildBusinessColumnDefs(fields: Record<string, string>): string[
110
112
  */
111
113
  export function generateDDLClause(fieldKey: string, fieldRule: string, isAdd: boolean = false): string {
112
114
  // 转换字段名为下划线格式
113
- const dbFieldName = toSnakeCase(fieldKey);
115
+ const dbFieldName = snakeCase(fieldKey);
114
116
 
115
117
  const parsed = parseRule(fieldRule);
116
118
  const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
@@ -1,16 +1,14 @@
1
1
  /**
2
- * syncDb 辅助函数模块
2
+ * syncDb 辅助工具模块
3
3
  *
4
4
  * 包含:
5
- * - SQL 标识符引用
6
- * - 日志格式化
7
- * - 类型判断工具
5
+ * - 标识符引用(反引号/双引号转义)
8
6
  * - 默认值处理
7
+ * - 日志输出格式化
9
8
  */
10
9
 
11
10
  import { IS_MYSQL, IS_PG, typeMapping } from './constants.js';
12
- import { isType } from '../../utils/helper.js';
13
- import { Logger } from '../../utils/logger.js';
11
+ import { Logger } from '../../lib/logger.js';
14
12
 
15
13
  /**
16
14
  * 根据数据库类型引用标识符
@@ -86,7 +84,7 @@ export function generateDefaultSql(actualDefault: any, fieldType: 'number' | 'st
86
84
 
87
85
  // 仅 number/string/array 类型设置默认值
88
86
  if (fieldType === 'number' || fieldType === 'string' || fieldType === 'array') {
89
- if (isType(actualDefault, 'number')) {
87
+ if (typeof actualDefault === 'number' && !Number.isNaN(actualDefault)) {
90
88
  return ` DEFAULT ${actualDefault}`;
91
89
  } else {
92
90
  // 字符串需要转义单引号:' -> ''
@@ -99,39 +97,41 @@ export function generateDefaultSql(actualDefault: any, fieldType: 'number' | 'st
99
97
  }
100
98
 
101
99
  /**
102
- * 判断是否为字符串或数组类型(需要长度限制的类型)
100
+ * 判断是否为字符串或数组类型(需要长度参数)
103
101
  *
104
102
  * @param fieldType - 字段类型
105
- * @returns 是否为 string 或 array
103
+ * @returns 是否为字符串或数组类型
106
104
  *
107
105
  * @example
108
106
  * isStringOrArrayType('string') // => true
109
- * isStringOrArrayType('array') // => true
107
+ * isStringOrArrayType('array_string') // => true
108
+ * isStringOrArrayType('array_text') // => false
110
109
  * isStringOrArrayType('number') // => false
111
110
  * isStringOrArrayType('text') // => false
112
111
  */
113
112
  export function isStringOrArrayType(fieldType: string): boolean {
114
- return fieldType === 'string' || fieldType === 'array';
113
+ return fieldType === 'string' || fieldType === 'array_string';
115
114
  }
116
115
 
117
116
  /**
118
117
  * 获取 SQL 数据类型
119
118
  *
120
- * @param fieldType - 字段类型(number/string/text/array
121
- * @param fieldMax - 最大长度(string/array 类型需要)
119
+ * @param fieldType - 字段类型(number/string/text/array_string/array_text
120
+ * @param fieldMax - 最大长度(string/array_string 类型需要)
122
121
  * @returns SQL 类型字符串
123
122
  *
124
123
  * @example
125
124
  * getSqlType('string', 100) // => 'VARCHAR(100)'
126
125
  * getSqlType('number', null) // => 'BIGINT'
127
- * getSqlType('text', null) // => 'TEXT'
128
- * getSqlType('array', 500) // => 'VARCHAR(500)'
126
+ * getSqlType('text', null) // => 'MEDIUMTEXT'
127
+ * getSqlType('array_string', 500) // => 'VARCHAR(500)'
128
+ * getSqlType('array_text', null) // => 'MEDIUMTEXT'
129
129
  */
130
130
  export function getSqlType(fieldType: string, fieldMax: number | null): string {
131
131
  if (isStringOrArrayType(fieldType)) {
132
132
  return `${typeMapping[fieldType]}(${fieldMax})`;
133
133
  }
134
- return typeMapping[fieldType];
134
+ return typeMapping[fieldType] || 'TEXT';
135
135
  }
136
136
 
137
137
  /**
@@ -149,7 +149,7 @@ export function escapeComment(str: string): string {
149
149
  }
150
150
 
151
151
  /**
152
- * 记录字段变更信息(带缩进和格式化)
152
+ * 记录字段变更信息(紧凑格式)
153
153
  *
154
154
  * @param tableName - 表名
155
155
  * @param fieldName - 字段名
@@ -159,7 +159,7 @@ export function escapeComment(str: string): string {
159
159
  * @param changeLabel - 变更类型的中文标签
160
160
  */
161
161
  export function logFieldChange(tableName: string, fieldName: string, changeType: string, oldValue: any, newValue: any, changeLabel: string): void {
162
- Logger.info(` 修改表 ${tableName} 的字段 ${fieldName} 的${changeLabel}: ${oldValue} -> ${newValue}`);
162
+ Logger.info(` ~ 修改 ${fieldName} ${changeLabel}: ${oldValue} -> ${newValue}`);
163
163
  }
164
164
 
165
165
  /**
@@ -7,14 +7,14 @@
7
7
  * - 提供统计信息和错误处理
8
8
  */
9
9
 
10
- import path from 'node:path';
11
- import { Logger } from '../../utils/logger.js';
10
+ import { basename } from 'pathe';
11
+ import { snakeCase } from 'es-toolkit/string';
12
+ import { Logger } from '../../lib/logger.js';
12
13
  import { Env } from '../../config/env.js';
13
- import { createSqlClient } from '../../utils/database.js';
14
- import { toSnakeCase } from '../../utils/helper.js';
14
+ import { scanAddons, addonDirExists, getAddonDir } from '../../util.js';
15
+ import { Database } from '../../lib/database.js';
15
16
  import checkTable from '../../checks/table.js';
16
17
  import { paths } from '../../paths.js';
17
- import { scanAddons, getAddonDir, addonDirExists } from '../../utils/helper.js';
18
18
 
19
19
  // 导入模块化的功能
20
20
  import { ensureDbVersion } from './version.js';
@@ -72,22 +72,27 @@ export const SyncDb = async (): Promise<void> => {
72
72
 
73
73
  // 阶段2:建立数据库连接并检查版本
74
74
  perfTracker.markPhase('数据库连接');
75
- sql = await createSqlClient({ max: 1 });
75
+ sql = await Database.connectSql({ max: 1 });
76
76
  await ensureDbVersion(sql);
77
77
  Logger.info(`✓ 数据库连接建立,耗时: ${perfTracker.getPhaseTime('数据库连接')}`);
78
78
 
79
79
  // 阶段3:扫描表定义文件
80
80
  perfTracker.markPhase('扫描表文件');
81
81
  const tablesGlob = new Bun.Glob('*.json');
82
- const directories: Array<{ path: string; isCore: boolean; addonName?: string }> = [{ path: paths.projectTableDir, isCore: false }];
83
-
84
- // 添加所有 addon tables 目录
82
+ const directories: Array<{ path: string; type: 'core' | 'app' | 'addon'; addonName?: string }> = [
83
+ // 1. core 框架表(core_ 前缀)
84
+ { path: paths.coreTableDir, type: 'core' },
85
+ // 2. 项目表(无前缀)
86
+ { path: paths.projectTableDir, type: 'app' }
87
+ ];
88
+
89
+ // 添加所有 addon 的 tables 目录(addon_{name}_ 前缀)
85
90
  const addons = scanAddons();
86
91
  for (const addon of addons) {
87
92
  if (addonDirExists(addon, 'tables')) {
88
93
  directories.push({
89
94
  path: getAddonDir(addon, 'tables'),
90
- isCore: false,
95
+ type: 'addon',
91
96
  addonName: addon
92
97
  });
93
98
  }
@@ -101,7 +106,7 @@ export const SyncDb = async (): Promise<void> => {
101
106
  absolute: true,
102
107
  onlyFiles: true
103
108
  })) {
104
- const fileName = path.basename(file, '.json');
109
+ const fileName = basename(file, '.json');
105
110
  if (!fileName.startsWith('_')) {
106
111
  totalTables++;
107
112
  }
@@ -115,15 +120,15 @@ export const SyncDb = async (): Promise<void> => {
115
120
  let processedCount = 0;
116
121
 
117
122
  for (const dirConfig of directories) {
118
- const { path: dir, isCore, addonName } = dirConfig;
119
- const dirType = addonName ? `组件${addonName}` : '项目';
123
+ const { path: dir, type, addonName } = dirConfig;
124
+ const dirType = type === 'core' ? '核心' : type === 'addon' ? `组件${addonName}` : '项目';
120
125
 
121
126
  for await (const file of tablesGlob.scan({
122
127
  cwd: dir,
123
128
  absolute: true,
124
129
  onlyFiles: true
125
130
  })) {
126
- const fileName = path.basename(file, '.json');
131
+ const fileName = basename(file, '.json');
127
132
 
128
133
  // 跳过以下划线开头的文件(这些是公共字段规则,不是表定义)
129
134
  if (fileName.startsWith('_')) {
@@ -131,17 +136,26 @@ export const SyncDb = async (): Promise<void> => {
131
136
  continue;
132
137
  }
133
138
 
134
- // 确定表名前缀:
135
- // - addon 表:addon_{addonName}_ 前缀
136
- // - 项目表:无前缀
137
- let tableName = toSnakeCase(fileName);
138
- if (addonName) {
139
- tableName = `addon_${addonName}_${tableName}`;
139
+ // 确定表名:
140
+ // - core 表:core_{表名}
141
+ // 例如:user.json core_user
142
+ // - addon 表:{addonName}_{表名}
143
+ // 例如:admin addon 的 user.json → admin_user
144
+ // - 项目表:{表名}
145
+ // 例如:user.json → user
146
+ let tableName = snakeCase(fileName);
147
+ if (type === 'core') {
148
+ // core 框架表,添加 core_ 前缀
149
+ tableName = `core_${tableName}`;
150
+ } else if (type === 'addon') {
151
+ // addon 表,添加 {addonName}_ 前缀
152
+ // 使用 snakeCase 统一转换(admin → admin)
153
+ const addonNameSnake = snakeCase(addonName!);
154
+ tableName = `${addonNameSnake}_${tableName}`;
140
155
  }
141
156
 
142
157
  processedCount++;
143
- progressLogger.logTableProgress(processedCount, totalTables, tableName);
144
- Logger.info(` 类型: ${dirType}`);
158
+ progressLogger.logTableProgress(processedCount, totalTables, tableName, dirType);
145
159
 
146
160
  const tableDefinition = await Bun.file(file).json();
147
161
  const existsTable = await tableExists(sql!, tableName);
@@ -186,7 +200,7 @@ export const SyncDb = async (): Promise<void> => {
186
200
  } finally {
187
201
  if (sql) {
188
202
  try {
189
- await sql.close();
203
+ await Database.disconnectSql();
190
204
  } catch (error: any) {
191
205
  Logger.warn('关闭数据库连接时出错:', error.message);
192
206
  }
@@ -5,7 +5,7 @@
5
5
  * - SQLite 重建表迁移(处理列修改等不支持的操作)
6
6
  */
7
7
 
8
- import { Logger } from '../../utils/logger.js';
8
+ import { Logger } from '../../lib/logger.js';
9
9
  import { createTable } from './tableCreate.js';
10
10
  import type { SQL } from 'bun';
11
11
 
@@ -6,7 +6,8 @@
6
6
  * - 进度信息记录
7
7
  */
8
8
 
9
- import { Logger } from '../../utils/logger.js';
9
+ import type { SQL } from 'bun';
10
+ import { Logger } from '../../lib/logger.js';
10
11
 
11
12
  /**
12
13
  * 阶段统计信息
@@ -84,10 +85,15 @@ export class PerformanceTracker {
84
85
  */
85
86
  export class ProgressLogger {
86
87
  /**
87
- * 记录表处理进度
88
+ * 记录表处理进度(紧凑格式)
89
+ * @param current 当前进度
90
+ * @param total 总数
91
+ * @param tableName 表名
92
+ * @param dirType 目录类型(核心/项目/组件名)
88
93
  */
89
- logTableProgress(current: number, total: number, tableName: string): void {
90
- Logger.info(`\n[${current}/${total}] 处理表: ${tableName}`);
94
+ logTableProgress(current: number, total: number, tableName: string, dirType?: string): void {
95
+ const typeInfo = dirType ? ` (${dirType})` : '';
96
+ Logger.info(`[${current}/${total}] ${tableName}${typeInfo}`);
91
97
  }
92
98
 
93
99
  /**