befly 3.0.0 → 3.0.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 (60) hide show
  1. package/checks/conflict.ts +35 -114
  2. package/checks/table.ts +31 -63
  3. package/config/env.ts +3 -3
  4. package/config/fields.ts +55 -0
  5. package/config/regexAliases.ts +51 -0
  6. package/config/reserved.ts +1 -1
  7. package/main.ts +17 -71
  8. package/package.json +7 -28
  9. package/plugins/db.ts +11 -10
  10. package/plugins/redis.ts +5 -9
  11. package/scripts/syncDb/apply.ts +3 -3
  12. package/scripts/syncDb/constants.ts +2 -1
  13. package/scripts/syncDb/ddl.ts +15 -8
  14. package/scripts/syncDb/helpers.ts +3 -2
  15. package/scripts/syncDb/index.ts +23 -35
  16. package/scripts/syncDb/state.ts +8 -6
  17. package/scripts/syncDb/table.ts +32 -22
  18. package/scripts/syncDb/tableCreate.ts +9 -3
  19. package/scripts/syncDb/tests/constants.test.ts +2 -1
  20. package/scripts/syncDb.ts +10 -9
  21. package/types/addon.d.ts +53 -0
  22. package/types/api.d.ts +17 -14
  23. package/types/befly.d.ts +2 -6
  24. package/types/context.d.ts +7 -0
  25. package/types/database.d.ts +9 -14
  26. package/types/index.d.ts +442 -8
  27. package/types/index.ts +35 -56
  28. package/types/redis.d.ts +2 -0
  29. package/types/validator.d.ts +0 -2
  30. package/types/validator.ts +43 -0
  31. package/utils/colors.ts +117 -37
  32. package/utils/database.ts +348 -0
  33. package/utils/dbHelper.ts +687 -116
  34. package/utils/helper.ts +812 -0
  35. package/utils/index.ts +10 -23
  36. package/utils/logger.ts +78 -171
  37. package/utils/redisHelper.ts +135 -152
  38. package/{types/context.ts → utils/requestContext.ts} +3 -3
  39. package/utils/sqlBuilder.ts +142 -165
  40. package/utils/validate.ts +51 -9
  41. package/apis/health/info.ts +0 -64
  42. package/apis/tool/tokenCheck.ts +0 -51
  43. package/bin/befly.ts +0 -202
  44. package/bunfig.toml +0 -3
  45. package/plugins/tool.ts +0 -34
  46. package/scripts/syncDev.ts +0 -112
  47. package/system.ts +0 -149
  48. package/tables/_common.json +0 -21
  49. package/tables/admin.json +0 -10
  50. package/utils/addonHelper.ts +0 -60
  51. package/utils/api.ts +0 -23
  52. package/utils/datetime.ts +0 -51
  53. package/utils/errorHandler.ts +0 -68
  54. package/utils/objectHelper.ts +0 -68
  55. package/utils/pluginHelper.ts +0 -62
  56. package/utils/response.ts +0 -38
  57. package/utils/sqlHelper.ts +0 -447
  58. package/utils/tableHelper.ts +0 -167
  59. package/utils/tool.ts +0 -230
  60. package/utils/typeHelper.ts +0 -101
@@ -5,9 +5,9 @@
5
5
 
6
6
  import path from 'node:path';
7
7
  import { Logger } from '../utils/logger.js';
8
- import { scanAddons, getAddonDir, hasAddonDir } from '../utils/addonHelper.js';
9
- import { __dirtables, __dirapis, __dirplugins, getProjectDir } from '../system.js';
10
- import { isReservedTableName, isReservedRoute, isReservedPluginName, isReservedAddonName, getReservedTablePrefixes, getReservedRoutes, getReservedPlugins, getReservedAddonNames } from '../config/reserved.js';
8
+ import { paths } from '../paths.js';
9
+ import { scanAddons, getAddonDir, addonDirExists } from '../utils/helper.js';
10
+ import { isReservedTableName, isReservedPluginName, isReservedAddonName, getReservedTablePrefixes, getReservedPlugins, getReservedAddonNames } from '../config/reserved.js';
11
11
 
12
12
  /**
13
13
  * 资源注册表
@@ -26,73 +26,6 @@ interface ConflictResult {
26
26
  conflicts: string[];
27
27
  }
28
28
 
29
- /**
30
- * 收集核心表定义
31
- */
32
- async function collectCoreTables(registry: ResourceRegistry): Promise<void> {
33
- try {
34
- const glob = new Bun.Glob('*.json');
35
- for await (const file of glob.scan({
36
- cwd: __dirtables,
37
- onlyFiles: true,
38
- absolute: true
39
- })) {
40
- const fileName = path.basename(file, '.json');
41
- if (fileName.startsWith('_')) continue;
42
-
43
- try {
44
- const tableDefine = await Bun.file(file).json();
45
- const tableName = tableDefine.tableName || `sys_${fileName}`;
46
-
47
- if (registry.tables.has(tableName)) {
48
- Logger.error(`核心表 "${tableName}" 重复定义`);
49
- } else {
50
- registry.tables.set(tableName, 'core');
51
- }
52
- } catch (error: any) {
53
- // 表定义解析错误会在 table.ts 中处理,这里跳过
54
- }
55
- }
56
- } catch (error: any) {
57
- Logger.error('收集核心表定义时出错:', error);
58
- }
59
- }
60
-
61
- /**
62
- * 收集核心 API 路由
63
- */
64
- async function collectCoreApis(registry: ResourceRegistry): Promise<void> {
65
- try {
66
- const glob = new Bun.Glob('**/*.ts');
67
- for await (const file of glob.scan({
68
- cwd: __dirapis,
69
- onlyFiles: true,
70
- absolute: true
71
- })) {
72
- const apiPath = path
73
- .relative(__dirapis, file)
74
- .replace(/\.(js|ts)$/, '')
75
- .replace(/\\/g, '/');
76
- if (apiPath.indexOf('_') !== -1) continue;
77
-
78
- try {
79
- const api = (await import(file)).default;
80
- const route = `${(api.method || 'POST').toUpperCase()}/api/${apiPath}`;
81
-
82
- if (registry.routes.has(route)) {
83
- Logger.error(`核心路由 "${route}" 重复定义`);
84
- } else {
85
- registry.routes.set(route, 'core');
86
- }
87
- } catch (error: any) {
88
- // API 加载错误会在 loader.ts 中处理,这里跳过
89
- }
90
- }
91
- } catch (error: any) {
92
- Logger.error('收集核心 API 路由时出错:', error);
93
- }
94
- }
95
-
96
29
  /**
97
30
  * 收集核心插件
98
31
  */
@@ -100,15 +33,15 @@ async function collectCorePlugins(registry: ResourceRegistry): Promise<void> {
100
33
  try {
101
34
  const glob = new Bun.Glob('*.ts');
102
35
  for await (const file of glob.scan({
103
- cwd: __dirplugins,
36
+ cwd: paths.rootPluginDir,
104
37
  onlyFiles: true,
105
38
  absolute: true
106
39
  })) {
107
- const pluginName = path.basename(file).replace(/\.(js|ts)$/, '');
40
+ const pluginName = path.basename(file).replace(/\.ts$/, '');
108
41
  if (pluginName.startsWith('_')) continue;
109
42
 
110
43
  if (registry.plugins.has(pluginName)) {
111
- Logger.error(`核心插件 "${pluginName}" 重复定义`);
44
+ Logger.warn(`核心插件 "${pluginName}" 重复定义`);
112
45
  } else {
113
46
  registry.plugins.set(pluginName, 'core');
114
47
  }
@@ -126,12 +59,12 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
126
59
 
127
60
  // 检查 addon 名称是否使用保留名称
128
61
  if (isReservedAddonName(addonName)) {
129
- conflicts.push(`Addon 名称 "${addonName}" 使用了保留名称,保留名称包括: ${getReservedAddonNames().join(', ')}`);
62
+ conflicts.push(`组件名称 "${addonName}" 使用了保留名称,保留名称包括: ${getReservedAddonNames().join(', ')}`);
130
63
  return conflicts;
131
64
  }
132
65
 
133
66
  // 收集 addon 表定义
134
- if (hasAddonDir(addonName, 'tables')) {
67
+ if (addonDirExists(addonName, 'tables')) {
135
68
  const addonTablesDir = getAddonDir(addonName, 'tables');
136
69
  const glob = new Bun.Glob('*.json');
137
70
 
@@ -149,15 +82,15 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
149
82
 
150
83
  // 检查是否使用保留前缀
151
84
  if (isReservedTableName(tableName)) {
152
- conflicts.push(`Addon [${addonName}] 表 "${tableName}" 使用了保留前缀,保留前缀包括: ${getReservedTablePrefixes().join(', ')}`);
85
+ conflicts.push(`组件 ${addonName} 表 "${tableName}" 使用了保留前缀,保留前缀包括: ${getReservedTablePrefixes().join(', ')}`);
153
86
  continue;
154
87
  }
155
88
 
156
89
  // 检查是否与已有表冲突
157
90
  if (registry.tables.has(tableName)) {
158
- conflicts.push(`Addon [${addonName}] 表 "${tableName}" 与 ${registry.tables.get(tableName)} 冲突`);
91
+ conflicts.push(`组件 ${addonName} 表 "${tableName}" 与 ${registry.tables.get(tableName)} 冲突`);
159
92
  } else {
160
- registry.tables.set(tableName, `addon[${addonName}]`);
93
+ registry.tables.set(tableName, `组件${addonName}`);
161
94
  }
162
95
  } catch (error: any) {
163
96
  // 表定义解析错误会在 table.ts 中处理,这里跳过
@@ -166,7 +99,7 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
166
99
  }
167
100
 
168
101
  // 收集 addon API 路由
169
- if (hasAddonDir(addonName, 'apis')) {
102
+ if (addonDirExists(addonName, 'apis')) {
170
103
  const addonApisDir = getAddonDir(addonName, 'apis');
171
104
  const glob = new Bun.Glob('**/*.ts');
172
105
 
@@ -175,27 +108,24 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
175
108
  onlyFiles: true,
176
109
  absolute: true
177
110
  })) {
178
- const apiPath = path
179
- .relative(addonApisDir, file)
180
- .replace(/\.(js|ts)$/, '')
181
- .replace(/\\/g, '/');
111
+ const apiPath = path.relative(addonApisDir, file).replace(/\.ts$/, '').replace(/\\/g, '/');
182
112
  if (apiPath.indexOf('_') !== -1) continue;
183
113
 
184
114
  try {
185
115
  const api = (await import(file)).default;
186
116
  const route = `${(api.method || 'POST').toUpperCase()}/api/${addonName}/${apiPath}`;
187
117
 
188
- // 检查是否使用保留路由
189
- if (isReservedRoute(route)) {
190
- conflicts.push(`Addon [${addonName}] 路由 "${route}" 使用了保留路径,保留路径包括: ${getReservedRoutes().join(', ')}`);
118
+ // 检查是否使用保留路由前缀 /api
119
+ if (route.includes('/api/api/') || route.includes('/api/api')) {
120
+ conflicts.push(`组件 [${addonName}] 路由 "${route}" 使用了保留路径前缀 "/api"`);
191
121
  continue;
192
122
  }
193
123
 
194
124
  // 检查是否与已有路由冲突
195
125
  if (registry.routes.has(route)) {
196
- conflicts.push(`Addon [${addonName}] 路由 "${route}" 与 ${registry.routes.get(route)} 冲突`);
126
+ conflicts.push(`组件 [${addonName}] 路由 "${route}" 与 ${registry.routes.get(route)} 冲突`);
197
127
  } else {
198
- registry.routes.set(route, `addon[${addonName}]`);
128
+ registry.routes.set(route, `组件${addonName}`);
199
129
  }
200
130
  } catch (error: any) {
201
131
  // API 加载错误会在 loader.ts 中处理,这里跳过
@@ -204,7 +134,7 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
204
134
  }
205
135
 
206
136
  // 收集 addon 插件
207
- if (hasAddonDir(addonName, 'plugins')) {
137
+ if (addonDirExists(addonName, 'plugins')) {
208
138
  const addonPluginsDir = getAddonDir(addonName, 'plugins');
209
139
  const glob = new Bun.Glob('*.ts');
210
140
 
@@ -213,7 +143,7 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
213
143
  onlyFiles: true,
214
144
  absolute: true
215
145
  })) {
216
- const fileName = path.basename(file).replace(/\.(js|ts)$/, '');
146
+ const fileName = path.basename(file).replace(/\.ts$/, '');
217
147
  if (fileName.startsWith('_')) continue;
218
148
 
219
149
  // Addon 插件使用点号命名空间
@@ -221,15 +151,15 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
221
151
 
222
152
  // 检查是否使用保留名称
223
153
  if (isReservedPluginName(pluginName)) {
224
- conflicts.push(`Addon [${addonName}] 插件 "${pluginName}" 使用了保留名称,保留名称包括: ${getReservedPlugins().join(', ')}`);
154
+ conflicts.push(`组件 ${addonName} 插件 "${pluginName}" 使用了保留名称,保留名称包括: ${getReservedPlugins().join(', ')}`);
225
155
  continue;
226
156
  }
227
157
 
228
158
  // 检查是否与已有插件冲突
229
159
  if (registry.plugins.has(pluginName)) {
230
- conflicts.push(`Addon [${addonName}] 插件 "${pluginName}" 与 ${registry.plugins.get(pluginName)} 冲突`);
160
+ conflicts.push(`组件 [${addonName}] 插件 "${pluginName}" 与 ${registry.plugins.get(pluginName)} 冲突`);
231
161
  } else {
232
- registry.plugins.set(pluginName, `addon[${addonName}]`);
162
+ registry.plugins.set(pluginName, `组件${addonName}`);
233
163
  }
234
164
  }
235
165
  }
@@ -244,7 +174,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
244
174
  const conflicts: string[] = [];
245
175
 
246
176
  // 收集用户表定义
247
- const userTablesDir = getProjectDir('tables');
177
+ const userTablesDir = paths.projectTableDir;
248
178
  try {
249
179
  const glob = new Bun.Glob('*.json');
250
180
  for await (const file of glob.scan({
@@ -280,7 +210,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
280
210
  }
281
211
 
282
212
  // 收集用户 API 路由
283
- const userApisDir = getProjectDir('apis');
213
+ const userApisDir = paths.projectApiDir;
284
214
  try {
285
215
  const glob = new Bun.Glob('**/*.ts');
286
216
  for await (const file of glob.scan({
@@ -288,19 +218,16 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
288
218
  onlyFiles: true,
289
219
  absolute: true
290
220
  })) {
291
- const apiPath = path
292
- .relative(userApisDir, file)
293
- .replace(/\.(js|ts)$/, '')
294
- .replace(/\\/g, '/');
221
+ const apiPath = path.relative(userApisDir, file).replace(/\.ts$/, '').replace(/\\/g, '/');
295
222
  if (apiPath.indexOf('_') !== -1) continue;
296
223
 
297
224
  try {
298
225
  const api = (await import(file)).default;
299
226
  const route = `${(api.method || 'POST').toUpperCase()}/api/${apiPath}`;
300
227
 
301
- // 检查是否使用保留路由
302
- if (isReservedRoute(route)) {
303
- conflicts.push(`用户路由 "${route}" 使用了保留路径,保留路径包括: ${getReservedRoutes().join(', ')}`);
228
+ // 检查是否使用保留路由前缀 /api
229
+ if (apiPath.startsWith('api/') || apiPath === 'api') {
230
+ conflicts.push(`用户路由 "${route}" 使用了保留路径前缀 "/api"`);
304
231
  continue;
305
232
  }
306
233
 
@@ -319,7 +246,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
319
246
  }
320
247
 
321
248
  // 收集用户插件
322
- const userPluginsDir = getProjectDir('plugins');
249
+ const userPluginsDir = paths.projectPluginDir;
323
250
  try {
324
251
  const glob = new Bun.Glob('*.ts');
325
252
  for await (const file of glob.scan({
@@ -327,7 +254,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
327
254
  onlyFiles: true,
328
255
  absolute: true
329
256
  })) {
330
- const pluginName = path.basename(file).replace(/\.(js|ts)$/, '');
257
+ const pluginName = path.basename(file).replace(/\.ts$/, '');
331
258
  if (pluginName.startsWith('_')) continue;
332
259
 
333
260
  // 检查是否使用保留名称
@@ -364,9 +291,7 @@ export default async function checkConflict(): Promise<boolean> {
364
291
 
365
292
  const allConflicts: string[] = [];
366
293
 
367
- // 1. 收集核心资源
368
- await collectCoreTables(registry);
369
- await collectCoreApis(registry);
294
+ // 1. 收集核心插件
370
295
  await collectCorePlugins(registry);
371
296
 
372
297
  // 2. 收集 addon 资源
@@ -382,15 +307,11 @@ export default async function checkConflict(): Promise<boolean> {
382
307
 
383
308
  // 4. 报告冲突
384
309
  if (allConflicts.length > 0) {
385
- Logger.error('');
386
- Logger.error('❌ 检测到资源冲突:');
387
- Logger.error('');
310
+ Logger.warn('检测到资源冲突:');
388
311
  allConflicts.forEach((conflict, index) => {
389
- Logger.error(` ${index + 1}. ${conflict}`);
312
+ Logger.warn(` ${index + 1}. ${conflict}`);
390
313
  });
391
- Logger.error('');
392
- Logger.error('请解决以上冲突后再启动服务器');
393
- Logger.error('');
314
+ Logger.warn('请解决以上冲突后再启动服务器');
394
315
  return false;
395
316
  }
396
317
 
package/checks/table.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  import path from 'node:path';
7
7
  import { Logger } from '../utils/logger.js';
8
8
  import { parseRule } from '../utils/index.js';
9
- import { __dirtables, getProjectDir } from '../system.js';
10
- import { scanAddons, getAddonDir } from '../utils/addonHelper.js';
9
+ import { paths } from '../paths.js';
10
+ import { scanAddons, getAddonDir } from '../utils/helper.js';
11
11
 
12
12
  /**
13
13
  * 表文件信息接口
@@ -15,8 +15,8 @@ import { scanAddons, getAddonDir } from '../utils/addonHelper.js';
15
15
  interface TableFileInfo {
16
16
  /** 表文件路径 */
17
17
  file: string;
18
- /** 文件类型:core(核心)、project(项目)或 addon(组件) */
19
- type: 'core' | 'project' | 'addon';
18
+ /** 文件类型:project(项目)或 addon(组件) */
19
+ type: 'project' | 'addon';
20
20
  /** 如果是 addon 类型,记录 addon 名称 */
21
21
  addonName?: string;
22
22
  }
@@ -29,13 +29,13 @@ const RESERVED_FIELDS = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'
29
29
  /**
30
30
  * 允许的字段类型
31
31
  */
32
- const FIELD_TYPES = ['string', 'number', 'text', 'array'] as const;
32
+ const FIELD_TYPES = ['string', 'number', 'text', 'array_string', 'array_text'] as const;
33
33
 
34
34
  /**
35
35
  * 小驼峰命名正则
36
- * 可选:以下划线开头(用于特殊文件如 _common.json)
36
+ * 可选:以下划线开头(用于特殊文件,如通用字段定义)
37
37
  * 必须以小写字母开头,后续可包含小写/数字,或多个 [大写+小写/数字] 片段
38
- * 示例:userTable、testCustomers、common、_common
38
+ * 示例:userTable、testCustomers、common
39
39
  */
40
40
  const LOWER_CAMEL_CASE_REGEX = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
41
41
 
@@ -66,39 +66,17 @@ export default async function (): Promise<boolean> {
66
66
 
67
67
  // 收集所有表文件
68
68
  const allTableFiles: TableFileInfo[] = [];
69
- const coreTableNames = new Set<string>(); // 存储内核表文件名
70
69
 
71
- // 收集内核表字段定义文件
70
+ // 收集项目表字段定义文件
72
71
  for await (const file of tablesGlob.scan({
73
- cwd: __dirtables,
72
+ cwd: paths.projectTableDir,
74
73
  absolute: true,
75
74
  onlyFiles: true
76
75
  })) {
77
- const fileName = path.basename(file, '.json');
78
- coreTableNames.add(fileName);
79
- allTableFiles.push({ file, type: 'core' });
80
- }
81
-
82
- // 收集项目表字段定义文件,并检查是否与内核表同名
83
- for await (const file of tablesGlob.scan({
84
- cwd: getProjectDir('tables'),
85
- absolute: true,
86
- onlyFiles: true
87
- })) {
88
- const fileName = path.basename(file, '.json');
89
-
90
- // 检查项目表是否与内核表同名
91
- if (coreTableNames.has(fileName)) {
92
- Logger.error(`项目表 ${fileName}.json 与内核表同名,项目表不能与内核表定义文件同名`);
93
- invalidFiles++;
94
- totalFiles++;
95
- continue;
96
- }
97
-
98
76
  allTableFiles.push({ file, type: 'project' });
99
77
  }
100
78
 
101
- // 收集 addon 表字段定义文件,并检查是否与内核表同名
79
+ // 收集 addon 表字段定义文件
102
80
  const addons = scanAddons();
103
81
  for (const addonName of addons) {
104
82
  const addonTablesDir = getAddonDir(addonName, 'tables');
@@ -109,16 +87,6 @@ export default async function (): Promise<boolean> {
109
87
  absolute: true,
110
88
  onlyFiles: true
111
89
  })) {
112
- const fileName = path.basename(file, '.json');
113
-
114
- // 检查 addon 表是否与内核表同名
115
- if (coreTableNames.has(fileName)) {
116
- Logger.error(`组件${addonName}表 ${fileName}.json 与内核表同名,addon 表不能与内核表定义文件同名`);
117
- invalidFiles++;
118
- totalFiles++;
119
- continue;
120
- }
121
-
122
90
  allTableFiles.push({ file, type: 'addon', addonName });
123
91
  }
124
92
  } catch (error) {
@@ -131,12 +99,12 @@ export default async function (): Promise<boolean> {
131
99
  totalFiles++;
132
100
  const fileName = path.basename(file);
133
101
  const fileBaseName = path.basename(file, '.json');
134
- const fileType = type === 'core' ? '内核' : type === 'project' ? '项目' : `组件${addonName}`;
102
+ const fileType = type === 'project' ? '项目' : `组件${addonName}`;
135
103
 
136
104
  try {
137
105
  // 1) 文件名小驼峰校验
138
106
  if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
139
- Logger.error(`${fileType}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
107
+ Logger.warn(`${fileType}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
140
108
  // 命名不合规,记录错误并计为无效文件,继续下一个文件
141
109
  invalidFiles++;
142
110
  continue;
@@ -150,7 +118,7 @@ export default async function (): Promise<boolean> {
150
118
  // 检查 table 中的每个验证规则
151
119
  for (const [colKey, rule] of Object.entries(table)) {
152
120
  if (typeof rule !== 'string') {
153
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 规则必须为字符串`);
121
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 规则必须为字符串`);
154
122
  fileValid = false;
155
123
  continue;
156
124
  }
@@ -162,7 +130,7 @@ export default async function (): Promise<boolean> {
162
130
 
163
131
  // 检查是否使用了保留字段
164
132
  if (RESERVED_FIELDS.includes(colKey as any)) {
165
- Logger.error(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
133
+ Logger.warn(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
166
134
  fileValid = false;
167
135
  }
168
136
 
@@ -171,7 +139,7 @@ export default async function (): Promise<boolean> {
171
139
  try {
172
140
  parsed = parseRule(rule);
173
141
  } catch (error: any) {
174
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段规则解析失败:${error.message}`);
142
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段规则解析失败:${error.message}`);
175
143
  fileValid = false;
176
144
  continue;
177
145
  }
@@ -180,37 +148,37 @@ export default async function (): Promise<boolean> {
180
148
 
181
149
  // 第1个值:名称必须为中文、数字、字母、下划线、短横线、空格
182
150
  if (!FIELD_NAME_REGEX.test(fieldName)) {
183
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
151
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
184
152
  fileValid = false;
185
153
  }
186
154
 
187
- // 第2个值:字段类型必须为string,number,text,array之一
155
+ // 第2个值:字段类型必须为string,number,text,array_string,array_text之一
188
156
  if (!FIELD_TYPES.includes(fieldType as any)) {
189
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
157
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
190
158
  fileValid = false;
191
159
  }
192
160
 
193
161
  // 第3/4个值:需要是 null 或 数字
194
162
  if (!(fieldMin === null || typeof fieldMin === 'number')) {
195
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
163
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
196
164
  fileValid = false;
197
165
  }
198
166
  if (!(fieldMax === null || typeof fieldMax === 'number')) {
199
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
167
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
200
168
  fileValid = false;
201
169
  }
202
170
 
203
171
  // 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
204
172
  if (fieldMin !== null && fieldMax !== null) {
205
173
  if (fieldMin > fieldMax) {
206
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
174
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
207
175
  fileValid = false;
208
176
  }
209
177
  }
210
178
 
211
179
  // 第6个值:是否创建索引必须为0或1
212
180
  if (fieldIndex !== 0 && fieldIndex !== 1) {
213
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 索引标识 "${fieldIndex}" 格式错误,必须为0或1`);
181
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 索引标识 "${fieldIndex}" 格式错误,必须为0或1`);
214
182
  fileValid = false;
215
183
  }
216
184
 
@@ -221,28 +189,28 @@ export default async function (): Promise<boolean> {
221
189
  if (fieldType === 'text') {
222
190
  // text:min/max 必须为 null,默认值必须为 'null'
223
191
  if (fieldMin !== null) {
224
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
192
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
225
193
  fileValid = false;
226
194
  }
227
195
  if (fieldMax !== null) {
228
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
196
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
229
197
  fileValid = false;
230
198
  }
231
199
  if (fieldDefault !== 'null') {
232
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
200
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
233
201
  fileValid = false;
234
202
  }
235
203
  } else if (fieldType === 'string' || fieldType === 'array') {
236
204
  if (fieldMax === null || typeof fieldMax !== 'number') {
237
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
205
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
238
206
  fileValid = false;
239
207
  } else if (fieldMax > MAX_VARCHAR_LENGTH) {
240
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
208
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
241
209
  fileValid = false;
242
210
  }
243
211
  } else if (fieldType === 'number') {
244
212
  if (fieldDefault !== 'null' && typeof fieldDefault !== 'number') {
245
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或null,当前为 "${fieldDefault}"`);
213
+ Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或null,当前为 "${fieldDefault}"`);
246
214
  fileValid = false;
247
215
  }
248
216
  }
@@ -264,17 +232,17 @@ export default async function (): Promise<boolean> {
264
232
  }
265
233
 
266
234
  // 输出统计信息
267
- Logger.info(`\n表定义检查完成:`);
235
+ Logger.info(`表定义检查完成:`);
268
236
  Logger.info(` 总文件数: ${totalFiles}`);
269
237
  Logger.info(` 总规则数: ${totalRules}`);
270
238
  Logger.info(` 通过文件: ${validFiles}`);
271
239
  Logger.info(` 失败文件: ${invalidFiles}`);
272
240
 
273
241
  if (invalidFiles > 0) {
274
- Logger.error(`\n表定义检查失败,请修复上述错误后重试`);
242
+ Logger.warn(`表定义检查失败,请修复上述错误后重试`);
275
243
  return false;
276
244
  } else {
277
- Logger.info(`\n所有表定义检查通过 ✓`);
245
+ Logger.info(`所有表定义检查通过 ✓`);
278
246
  return true;
279
247
  }
280
248
  } catch (error: any) {
package/config/env.ts CHANGED
@@ -26,8 +26,8 @@ export interface EnvConfig {
26
26
  PARAMS_CHECK: string;
27
27
 
28
28
  // ========== 日志配置 ==========
29
- /** 日志等级:debug | info | warn | error */
30
- LOG_LEVEL: string;
29
+ /** debug日志开关:0 | 1 */
30
+ LOG_DEBUG: number;
31
31
  /** 日志排除字段(逗号分隔) */
32
32
  LOG_EXCLUDE_FIELDS: string;
33
33
  /** 日志目录 */
@@ -158,7 +158,7 @@ export const Env: EnvConfig = {
158
158
  PARAMS_CHECK: getEnv('PARAMS_CHECK', 'true'),
159
159
 
160
160
  // ========== 日志配置 ==========
161
- LOG_LEVEL: getEnv('LOG_LEVEL', 'info'),
161
+ LOG_DEBUG: getEnvNumber('LOG_DEBUG', 0),
162
162
  LOG_EXCLUDE_FIELDS: getEnv('LOG_EXCLUDE_FIELDS', 'password,token,secret'),
163
163
  LOG_DIR: getEnv('LOG_DIR', './logs'),
164
164
  LOG_TO_CONSOLE: getEnvNumber('LOG_TO_CONSOLE', 1),
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 通用字段定义
3
+ *
4
+ * 用于在 API 和表定义中复用常见字段规则
5
+ *
6
+ * 格式:`字段标签|数据类型|最小值|最大值|默认值|是否必填|正则表达式`
7
+ *
8
+ * 说明:
9
+ * - 字段标签:用于显示的中文名称
10
+ * - 数据类型:string、number、boolean、array_string、array_text 等
11
+ * - 最小值:string 类型表示最小长度,number 类型表示最小数值,array 类型表示最小元素个数
12
+ * - 最大值:string 类型表示最大长度,number 类型表示最大数值,array 类型表示最大元素个数
13
+ * - 默认值:字段的默认值,无默认值时填 null
14
+ * - 是否必填:0 表示非必填,1 表示必填
15
+ * - 正则表达式:用于验证字段值的正则表达式,无验证时填 null
16
+ *
17
+ * 类型说明:
18
+ * - array_string: 短数组,存储为 VARCHAR,建议设置 max 限制(如 0-100)
19
+ * - array_text: 长数组,存储为 MEDIUMTEXT,min/max 可设为 null 表示不限制
20
+ *
21
+ * 正则表达式别名:
22
+ * - 使用 @ 前缀可以引用内置正则表达式别名,例如:
23
+ * - @number: 纯数字
24
+ * - @alphanumeric: 字母+数字
25
+ * - @email: 邮箱格式
26
+ * - @phone: 中国手机号
27
+ * - @chinese: 纯中文
28
+ * - 完整别名列表见 config/regexAliases.ts
29
+ *
30
+ * 示例:
31
+ * - '用户ID|array_text|null|null|null|0|@number' - 数字数组
32
+ * - '标签|array_string|0|50|null|0|@alphanumeric' - 字母数字数组
33
+ */
34
+
35
+ export const Fields = {
36
+ _id: 'ID|number|1|null|null|1|null',
37
+ email: '邮箱|string|5|100|null|1|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
38
+ phone: '手机号|string|11|11|null|1|^1[3-9]\\d{9}$',
39
+ page: '页码|number|1|9999|1|0|null',
40
+ limit: '每页数量|number|1|100|10|0|null',
41
+ title: '标题|string|1|200|null|0|null',
42
+ description: '描述|string|0|500|null|0|null',
43
+ keyword: '关键词|string|1|50|null|1|null',
44
+ keywords: '关键词列表|array_string|0|50|null|0|null',
45
+ enabled: '启用状态|number|0|1|1|0|^(0|1)$',
46
+ date: '日期|string|10|10|null|0|^\\d{4}-\\d{2}-\\d{2}$',
47
+ datetime: '日期时间|string|19|25|null|0|^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}',
48
+ filename: '文件名|string|1|255|null|0|null',
49
+ url: '网址|string|5|500|null|0|^https?://',
50
+ tag: '标签|array_string|0|10|null|0|null',
51
+ startTime: '开始时间|number|0|9999999999999|null|0|null',
52
+ endTime: '结束时间|number|0|9999999999999|null|0|null'
53
+ } as const;
54
+
55
+ export default Fields;