befly 2.0.12 → 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.
@@ -0,0 +1,367 @@
1
+ /**
2
+ * 数据库表结构同步脚本 - 仅支持 MySQL 8.0+
3
+ */
4
+
5
+ import path from 'node:path';
6
+ import { SQL } from 'bun';
7
+ import { Env } from '../config/env.js';
8
+ import { Logger } from '../utils/logger.js';
9
+ import { parseFieldRule } from '../utils/util.js';
10
+ import { __dirtables, getProjectDir } from '../system.js';
11
+ import tableCheck from '../checks/table.js';
12
+
13
+ const typeMapping = {
14
+ number: 'BIGINT',
15
+ string: 'VARCHAR',
16
+ text: 'MEDIUMTEXT',
17
+ array: 'VARCHAR'
18
+ };
19
+
20
+ // 获取字段的SQL定义
21
+ const getColumnDefinition = (fieldName, rule) => {
22
+ const [fieldDisplayName, fieldType, fieldMin, fieldMaxLength, fieldDefaultValue, fieldHasIndex] = parseFieldRule(rule);
23
+
24
+ let sqlType = typeMapping[fieldType];
25
+ if (!sqlType) throw new Error(`不支持的数据类型: ${fieldType}`);
26
+
27
+ // 根据字段类型设置SQL类型和长度
28
+ if (fieldType === 'string' || fieldType === 'array') {
29
+ const maxLength = parseInt(fieldMaxLength);
30
+ sqlType = `VARCHAR(${maxLength})`;
31
+ }
32
+
33
+ let columnDef = `\`${fieldName}\` ${sqlType} NOT NULL`;
34
+
35
+ // 设置默认值:如果第5个属性为null或字段类型为text,则不设置默认值
36
+ if (fieldDefaultValue && fieldDefaultValue !== 'null' && fieldType !== 'text') {
37
+ if (fieldType === 'number') {
38
+ columnDef += ` DEFAULT ${fieldDefaultValue}`;
39
+ } else {
40
+ columnDef += ` DEFAULT "${fieldDefaultValue.replace(/"/g, '\\"')}"`;
41
+ }
42
+ } else if (fieldType === 'string' || fieldType === 'array') {
43
+ columnDef += ` DEFAULT ""`;
44
+ } else if (fieldType === 'number') {
45
+ columnDef += ` DEFAULT 0`;
46
+ }
47
+ // text类型不设置默认值
48
+
49
+ // 添加字段注释(使用第1个属性作为字段显示名称)
50
+ if (fieldDisplayName && fieldDisplayName !== 'null') {
51
+ columnDef += ` COMMENT "${fieldDisplayName.replace(/"/g, '\\"')}"`;
52
+ }
53
+
54
+ return columnDef;
55
+ };
56
+
57
+ // 通用执行器:将 '?' 占位符转换为 Bun SQL 的 $1, $2 并执行
58
+ const toDollarParams = (query, params) => {
59
+ if (!params || params.length === 0) return query;
60
+ let i = 0;
61
+ return query.replace(/\?/g, () => `$${++i}`);
62
+ };
63
+
64
+ const exec = async (client, query, params = []) => {
65
+ if (params.length > 0) {
66
+ return await client.unsafe(toDollarParams(query, params), params);
67
+ }
68
+ return await client.unsafe(query);
69
+ };
70
+
71
+ // 获取表的现有列信息
72
+ const getTableColumns = async (client, tableName) => {
73
+ const result = await exec(
74
+ client,
75
+ `SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT, COLUMN_TYPE
76
+ FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
77
+ [Env.MYSQL_DB || 'test', tableName]
78
+ );
79
+
80
+ const columns = {};
81
+ result.forEach((row) => {
82
+ columns[row.COLUMN_NAME] = {
83
+ type: row.DATA_TYPE,
84
+ columnType: row.COLUMN_TYPE,
85
+ length: row.CHARACTER_MAXIMUM_LENGTH,
86
+ nullable: row.IS_NULLABLE === 'YES',
87
+ defaultValue: row.COLUMN_DEFAULT,
88
+ comment: row.COLUMN_COMMENT
89
+ };
90
+ });
91
+ return columns;
92
+ };
93
+
94
+ // 获取表的现有索引信息
95
+ const getTableIndexes = async (client, tableName) => {
96
+ const result = await exec(
97
+ client,
98
+ `SELECT INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS
99
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME != 'PRIMARY' ORDER BY INDEX_NAME`,
100
+ [Env.MYSQL_DB || 'test', tableName]
101
+ );
102
+
103
+ const indexes = {};
104
+ result.forEach((row) => {
105
+ if (!indexes[row.INDEX_NAME]) indexes[row.INDEX_NAME] = [];
106
+ indexes[row.INDEX_NAME].push(row.COLUMN_NAME);
107
+ });
108
+ return indexes;
109
+ };
110
+
111
+ // 管理索引
112
+ const manageIndex = async (client, tableName, indexName, fieldName, action) => {
113
+ const sql = action === 'create' ? `CREATE INDEX \`${indexName}\` ON \`${tableName}\` (\`${fieldName}\`)` : `DROP INDEX \`${indexName}\` ON \`${tableName}\``;
114
+
115
+ try {
116
+ await exec(client, sql);
117
+ Logger.info(`表 ${tableName} 索引 ${indexName} ${action === 'create' ? '创建' : '删除'}成功`);
118
+ } catch (error) {
119
+ Logger.error(`${action === 'create' ? '创建' : '删除'}索引失败: ${error.message}`);
120
+ throw error;
121
+ }
122
+ };
123
+
124
+ // 创建表
125
+ const createTable = async (client, tableName, fields) => {
126
+ const columns = [
127
+ //
128
+ '`id` BIGINT PRIMARY KEY COMMENT "主键ID"',
129
+ '`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT "创建时间"',
130
+ '`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT "更新时间"',
131
+ '`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT "删除时间"',
132
+ '`state` BIGINT NOT NULL DEFAULT 0 COMMENT "状态字段"'
133
+ ];
134
+
135
+ const indexes = [
136
+ //
137
+ 'INDEX `idx_created_at` (`created_at`)',
138
+ 'INDEX `idx_updated_at` (`updated_at`)',
139
+ 'INDEX `idx_state` (`state`)'
140
+ ];
141
+
142
+ // 添加自定义字段和索引
143
+ for (const [fieldName, rule] of Object.entries(fields)) {
144
+ columns.push(getColumnDefinition(fieldName, rule));
145
+
146
+ // 使用第6个属性判断是否设置索引
147
+ const ruleParts = parseFieldRule(rule);
148
+ const fieldHasIndex = ruleParts[5]; // 第6个属性
149
+ if (fieldHasIndex === '1') {
150
+ indexes.push(`INDEX \`idx_${fieldName}\` (\`${fieldName}\`)`);
151
+ }
152
+ }
153
+
154
+ const createTableSQL = `
155
+ CREATE TABLE \`${tableName}\` (
156
+ ${columns.join(',\n ')},
157
+ ${indexes.join(',\n ')}
158
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
159
+ `;
160
+
161
+ await exec(client, createTableSQL);
162
+ Logger.info(`表 ${tableName} 创建成功`);
163
+ };
164
+
165
+ // 比较字段定义变化
166
+ const compareFieldDefinition = (existingColumn, newRule, fieldName) => {
167
+ const ruleParts = parseFieldRule(newRule);
168
+ const [fieldDisplayName, fieldType, fieldMin, fieldMaxLength, fieldDefaultValue] = ruleParts;
169
+ const changes = [];
170
+
171
+ // 检查长度变化(string和array类型)
172
+ if (fieldType === 'string' || fieldType === 'array') {
173
+ if (fieldMaxLength === 'null') {
174
+ throw new Error(`string/array 类型字段的最大长度未设置,必须指定最大长度`);
175
+ }
176
+ const newMaxLength = parseInt(fieldMaxLength);
177
+ if (existingColumn.length !== newMaxLength) {
178
+ changes.push({ type: 'length', current: existingColumn.length, new: newMaxLength });
179
+ }
180
+ }
181
+
182
+ // 检查注释变化(使用第1个属性作为字段显示名称)
183
+ if (fieldDisplayName && fieldDisplayName !== 'null') {
184
+ const currentComment = existingColumn.comment || '';
185
+ if (currentComment !== fieldDisplayName) {
186
+ changes.push({ type: 'comment', current: currentComment, new: fieldDisplayName });
187
+ }
188
+ }
189
+
190
+ // 检查数据类型变化
191
+ const expectedDbType = {
192
+ number: 'bigint',
193
+ string: 'varchar',
194
+ text: 'mediumtext',
195
+ array: 'varchar'
196
+ }[fieldType];
197
+
198
+ if (existingColumn.type.toLowerCase() !== expectedDbType) {
199
+ changes.push({ type: 'datatype', current: existingColumn.type, new: expectedDbType });
200
+ }
201
+
202
+ return { hasChanges: changes.length > 0, changes };
203
+ };
204
+
205
+ // 生成DDL语句
206
+ const generateDDL = (tableName, fieldName, rule, isAdd = false) => {
207
+ const columnDef = getColumnDefinition(fieldName, rule);
208
+ const operation = isAdd ? 'ADD COLUMN' : 'MODIFY COLUMN';
209
+ return `ALTER TABLE \`${tableName}\` ${operation} ${columnDef}, ALGORITHM=INSTANT, LOCK=NONE`;
210
+ };
211
+
212
+ // 安全执行DDL语句
213
+ const executeDDLSafely = async (client, sql) => {
214
+ try {
215
+ await exec(client, sql);
216
+ return true;
217
+ } catch (error) {
218
+ // INSTANT失败时尝试INPLACE
219
+ if (sql.includes('ALGORITHM=INSTANT')) {
220
+ const inplaceSql = sql.replace('ALGORITHM=INSTANT', 'ALGORITHM=INPLACE');
221
+ try {
222
+ await exec(client, inplaceSql);
223
+ return true;
224
+ } catch (inplaceError) {
225
+ // 最后尝试传统DDL
226
+ const traditionSql = sql.split(',')[0]; // 移除ALGORITHM和LOCK参数
227
+ await exec(client, traditionSql);
228
+ return true;
229
+ }
230
+ } else {
231
+ throw error;
232
+ }
233
+ }
234
+ };
235
+
236
+ // 同步表结构
237
+ const syncTable = async (client, tableName, fields) => {
238
+ const existingColumns = await getTableColumns(client, tableName);
239
+ const existingIndexes = await getTableIndexes(client, tableName);
240
+ const systemFields = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'];
241
+
242
+ // 同步字段
243
+ for (const [fieldName, rule] of Object.entries(fields)) {
244
+ if (existingColumns[fieldName]) {
245
+ const comparison = compareFieldDefinition(existingColumns[fieldName], rule, fieldName);
246
+ if (comparison.hasChanges) {
247
+ const sql = generateDDL(tableName, fieldName, rule);
248
+ await executeDDLSafely(client, sql);
249
+ Logger.info(`字段 ${tableName}.${fieldName} 更新成功`);
250
+ }
251
+ } else {
252
+ const sql = generateDDL(tableName, fieldName, rule, true);
253
+ await executeDDLSafely(client, sql);
254
+ Logger.info(`字段 ${tableName}.${fieldName} 添加成功`);
255
+ }
256
+ }
257
+
258
+ // 同步索引
259
+ for (const [fieldName, rule] of Object.entries(fields)) {
260
+ const ruleParts = parseFieldRule(rule);
261
+ const fieldHasIndex = ruleParts[5]; // 使用第6个属性判断是否设置索引
262
+ const indexName = `idx_${fieldName}`;
263
+
264
+ if (fieldHasIndex === '1' && !existingIndexes[indexName]) {
265
+ await manageIndex(client, tableName, indexName, fieldName, 'create');
266
+ } else if (fieldHasIndex !== '1' && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
267
+ await manageIndex(client, tableName, indexName, fieldName, 'drop');
268
+ }
269
+ }
270
+ };
271
+
272
+ // 主同步函数
273
+ const SyncDb = async () => {
274
+ let client = null;
275
+
276
+ try {
277
+ Logger.info('开始数据库表结构同步...');
278
+
279
+ // 验证表定义文件
280
+ const tableValidationResult = await tableCheck();
281
+ if (!tableValidationResult) {
282
+ throw new Error('表定义验证失败');
283
+ }
284
+
285
+ // 建立数据库连接并检查版本(Bun SQL)
286
+ const url = `mysql://${encodeURIComponent(Env.MYSQL_USER || 'root')}:${encodeURIComponent(Env.MYSQL_PASSWORD || 'root')}@${Env.MYSQL_HOST || '127.0.0.1'}:${Env.MYSQL_PORT || 3306}/${Env.MYSQL_DB || 'test'}`;
287
+ client = new SQL({ url, max: 1, bigint: true });
288
+ const result = await client`SELECT VERSION() AS version`;
289
+ const version = result[0].version;
290
+
291
+ if (version.toLowerCase().includes('mariadb')) {
292
+ throw new Error('此脚本仅支持 MySQL 8.0+,不支持 MariaDB');
293
+ }
294
+
295
+ const majorVersion = parseInt(version.split('.')[0]);
296
+ if (majorVersion < 8) {
297
+ throw new Error(`此脚本仅支持 MySQL 8.0+,当前版本: ${version}`);
298
+ }
299
+
300
+ Logger.info(`MySQL 版本检查通过: ${version}`);
301
+
302
+ // 扫描并处理表文件
303
+ const tablesGlob = new Bun.Glob('*.json');
304
+ const directories = [__dirtables, getProjectDir('tables')];
305
+ let processedCount = 0;
306
+ let createdTables = 0;
307
+ let modifiedTables = 0;
308
+
309
+ for (const dir of directories) {
310
+ try {
311
+ for await (const file of tablesGlob.scan({ cwd: dir, absolute: true, onlyFiles: true })) {
312
+ const tableName = path.basename(file, '.json');
313
+ const tableDefinition = await Bun.file(file).json();
314
+ const result = await exec(client, 'SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', [Env.MYSQL_DB || 'test', tableName]);
315
+ const exists = result[0].count > 0;
316
+
317
+ if (exists) {
318
+ await syncTable(client, tableName, tableDefinition);
319
+ } else {
320
+ await createTable(client, tableName, tableDefinition);
321
+ }
322
+
323
+ Logger.info(`表 ${tableName} 处理完成`);
324
+ exists ? modifiedTables++ : createdTables++;
325
+ processedCount++;
326
+ }
327
+ } catch (error) {
328
+ Logger.warn(`扫描目录 ${dir} 出错: ${error.message}`);
329
+ }
330
+ }
331
+
332
+ // 显示统计信息
333
+ Logger.info(`同步完成 - 总计: ${processedCount}, 新建: ${createdTables}, 修改: ${modifiedTables}`);
334
+
335
+ if (processedCount === 0) {
336
+ Logger.warn('没有找到任何表定义文件');
337
+ }
338
+ } catch (error) {
339
+ Logger.error(`数据库同步失败: ${error.message}`);
340
+ Logger.error(`错误详情: ${error.stack}`);
341
+ if (error.code) {
342
+ Logger.error(`错误代码: ${error.code}`);
343
+ }
344
+ if (error.errno) {
345
+ Logger.error(`错误编号: ${error.errno}`);
346
+ }
347
+ process.exit(1);
348
+ } finally {
349
+ if (client) {
350
+ try {
351
+ await client.close();
352
+ } catch (error) {
353
+ Logger.warn('关闭数据库连接时出错:', error.message);
354
+ }
355
+ }
356
+ }
357
+ };
358
+
359
+ // 如果直接运行此脚本
360
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('dbSync.js')) {
361
+ SyncDb().catch((error) => {
362
+ console.error('❌ 数据库同步失败:', error);
363
+ process.exit(1);
364
+ });
365
+ }
366
+
367
+ export { SyncDb };
package/system.js ADDED
@@ -0,0 +1,118 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { dirname, join, resolve, relative } from 'node:path';
3
+
4
+ /**
5
+ * Befly 框架系统路径定义
6
+ * 提供统一的路径变量,供整个框架使用
7
+ */
8
+
9
+ // 当前文件的路径信息
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ // Befly 框架根目录
14
+ const __dirroot = __dirname;
15
+
16
+ // 各个重要目录的路径
17
+ const __dirscript = join(__dirroot, 'scripts');
18
+ const __dirbin = join(__dirroot, 'bin');
19
+ const __dirutils = join(__dirroot, 'utils');
20
+ const __dirconfig = join(__dirroot, 'config');
21
+ const __dirtables = join(__dirroot, 'tables');
22
+ const __dirchecks = join(__dirroot, 'checks');
23
+ const __dirapis = join(__dirroot, 'apis');
24
+ const __dirplugins = join(__dirroot, 'plugins');
25
+ const __dirlibs = join(__dirroot, 'libs');
26
+ const __dirtests = join(__dirroot, 'tests');
27
+
28
+ // 获取项目根目录(befly 框架的使用方项目)
29
+ const getProjectRoot = () => {
30
+ return process.cwd();
31
+ };
32
+
33
+ // 获取项目中的特定目录
34
+ const getProjectDir = (subdir = '') => {
35
+ return subdir ? join(getProjectRoot(), subdir) : getProjectRoot();
36
+ };
37
+
38
+ // 创建路径解析器,基于 befly 根目录
39
+ const resolveBeflyPath = (...paths) => {
40
+ return resolve(__dirroot, ...paths);
41
+ };
42
+
43
+ // 创建路径解析器,基于项目根目录
44
+ const resolveProjectPath = (...paths) => {
45
+ return resolve(getProjectRoot(), ...paths);
46
+ };
47
+
48
+ // 获取相对于 befly 根目录的相对路径
49
+ const getRelativeBeflyPath = (targetPath) => {
50
+ return relative(__dirroot, targetPath);
51
+ };
52
+
53
+ // 获取相对于项目根目录的相对路径
54
+ const getRelativeProjectPath = (targetPath) => {
55
+ return relative(getProjectRoot(), targetPath);
56
+ };
57
+
58
+ // 导出所有路径变量和工具函数
59
+ export {
60
+ // 基础路径变量
61
+ __filename,
62
+ __dirname,
63
+ __dirroot,
64
+
65
+ // Befly 框架目录
66
+ __dirscript,
67
+ __dirbin,
68
+ __dirutils,
69
+ __dirconfig,
70
+ __dirtables,
71
+ __dirchecks,
72
+ __dirapis,
73
+ __dirplugins,
74
+ __dirlibs,
75
+ __dirtests,
76
+
77
+ // 项目路径工具函数
78
+ getProjectRoot,
79
+ getProjectDir,
80
+
81
+ // 路径解析工具函数
82
+ resolveBeflyPath,
83
+ resolveProjectPath,
84
+ getRelativeBeflyPath,
85
+ getRelativeProjectPath
86
+ };
87
+
88
+ // 默认导出包含所有路径信息的对象
89
+ export default {
90
+ // 基础路径变量
91
+ __filename,
92
+ __dirname,
93
+ __dirroot,
94
+
95
+ // Befly 框架目录
96
+ paths: {
97
+ script: __dirscript,
98
+ bin: __dirbin,
99
+ utils: __dirutils,
100
+ config: __dirconfig,
101
+ tables: __dirtables,
102
+ checks: __dirchecks,
103
+ apis: __dirapis,
104
+ plugins: __dirplugins,
105
+ libs: __dirlibs,
106
+ tests: __dirtests
107
+ },
108
+
109
+ // 工具函数
110
+ utils: {
111
+ getProjectRoot,
112
+ getProjectDir,
113
+ resolveBeflyPath,
114
+ resolveProjectPath,
115
+ getRelativeBeflyPath,
116
+ getRelativeProjectPath
117
+ }
118
+ };
@@ -0,0 +1,16 @@
1
+ {
2
+ "email": "邮箱⚡string⚡5⚡100⚡null⚡1⚡^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
3
+ "phone": "手机号⚡string⚡11⚡11⚡null⚡1⚡^1[3-9]\\d{9}$",
4
+ "page": "页码⚡number⚡1⚡9999⚡1⚡0⚡null",
5
+ "limit": "每页数量⚡number⚡1⚡100⚡10⚡0⚡null",
6
+ "title": "标题⚡string⚡1⚡200⚡null⚡0⚡null",
7
+ "description": "描述⚡string⚡0⚡500⚡null⚡0⚡null",
8
+ "keyword": "关键词⚡string⚡1⚡50⚡null⚡1⚡null",
9
+ "status": "状态⚡string⚡1⚡20⚡active⚡1⚡^(active|inactive|pending|suspended)$",
10
+ "enabled": "启用状态⚡number⚡0⚡1⚡1⚡0⚡^(0|1)$",
11
+ "date": "日期⚡string⚡10⚡10⚡null⚡0⚡^\\d{4}-\\d{2}-\\d{2}$",
12
+ "datetime": "日期时间⚡string⚡19⚡25⚡null⚡0⚡^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
13
+ "filename": "文件名⚡string⚡1⚡255⚡null⚡0⚡null",
14
+ "url": "网址⚡string⚡5⚡500⚡null⚡0⚡^https?://",
15
+ "tag": "标签⚡array⚡0⚡10⚡[]⚡0⚡null"
16
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "filename": "文件名⚡string⚡1⚡255⚡null⚡1⚡null",
3
+ "content": "内容⚡string⚡0⚡1000000⚡null⚡0⚡null",
4
+ "type": "类型⚡string⚡0⚡50⚡file⚡1⚡^(file|folder|link)$",
5
+ "path": "路径⚡string⚡1⚡500⚡null⚡0⚡null"
6
+ }
package/utils/util.js CHANGED
@@ -1,5 +1,19 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import path from 'node:path';
3
+ import { Env } from '../config/env.js';
4
+
5
+ export const setCorsOptions = (req) => {
6
+ return {
7
+ headers: {
8
+ 'Access-Control-Allow-Origin': Env.ALLOWED_ORIGIN || req.headers.get('origin') || '*',
9
+ 'Access-Control-Allow-Methods': Env.ALLOWED_METHODS || 'GET, POST, PUT, DELETE, OPTIONS',
10
+ 'Access-Control-Allow-Headers': Env.ALLOWED_HEADERS || 'Content-Type, Authorization, authorization, token',
11
+ 'Access-Control-Expose-Headers': Env.EXPOSE_HEADERS || 'Content-Range, X-Content-Range, Authorization, authorization, token',
12
+ 'Access-Control-Max-Age': Env.MAX_AGE || 86400,
13
+ 'Access-Control-Allow-Credentials': Env.ALLOW_CREDENTIALS || 'true'
14
+ }
15
+ };
16
+ };
3
17
 
4
18
  export const sortPlugins = (plugins) => {
5
19
  const result = [];
@@ -53,6 +67,23 @@ export const formatDate = (date = new Date(), format = 'YYYY-MM-DD HH:mm:ss') =>
53
67
  return format.replace('YYYY', year).replace('MM', month).replace('DD', day).replace('HH', hour).replace('mm', minute).replace('ss', second);
54
68
  };
55
69
 
70
+ /**
71
+ * 计算时间差并返回带单位的字符串
72
+ * @param {number} startTime - 开始时间(Bun.nanoseconds()返回值)
73
+ * @param {number} endTime - 结束时间(可选,默认为当前时间)
74
+ * @returns {string} 时间差(如果小于1秒返回"xx 毫秒",否则返回"xx 秒")
75
+ */
76
+ export const calculateElapsedTime = (startTime, endTime = Bun.nanoseconds()) => {
77
+ const elapsedMs = (endTime - startTime) / 1_000_000;
78
+
79
+ if (elapsedMs < 1000) {
80
+ return `${elapsedMs.toFixed(2)} 毫秒`;
81
+ } else {
82
+ const elapsedSeconds = elapsedMs / 1000;
83
+ return `${elapsedSeconds.toFixed(2)} 秒`;
84
+ }
85
+ };
86
+
56
87
  // 类型判断
57
88
  export const isType = (value, type) => {
58
89
  const actualType = Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
@@ -191,3 +222,95 @@ export const filterLogFields = (body, excludeFields = '') => {
191
222
  }
192
223
  return filtered;
193
224
  };
225
+
226
+ // 验证字段名称是否为中文、数字、字母
227
+ const validateFieldName = (name) => {
228
+ const nameRegex = /^[\u4e00-\u9fa5a-zA-Z0-9]+$/;
229
+ return nameRegex.test(name);
230
+ };
231
+
232
+ // 验证字段类型是否为指定的四种类型之一
233
+ const validateFieldType = (type) => {
234
+ const validTypes = ['string', 'number', 'text', 'array'];
235
+ return validTypes.includes(type);
236
+ };
237
+
238
+ // 验证最小值/最大值是否为null或数字
239
+ const validateMinMax = (value) => {
240
+ return value === 'null' || (!isNaN(parseFloat(value)) && isFinite(parseFloat(value)));
241
+ };
242
+
243
+ // 验证默认值是否为null、字符串或数字
244
+ const validateDefaultValue = (value) => {
245
+ if (value === 'null') return true;
246
+ // 检查是否为数字
247
+ if (!isNaN(parseFloat(value)) && isFinite(parseFloat(value))) return true;
248
+ // 其他情况视为字符串,都是有效的
249
+ return true;
250
+ };
251
+
252
+ // 验证索引标识是否为0或1
253
+ const validateIndex = (value) => {
254
+ return value === '0' || value === '1';
255
+ };
256
+
257
+ // 验证正则表达式是否有效
258
+ const validateRegex = (value) => {
259
+ if (value === 'null') return true;
260
+ try {
261
+ new RegExp(value);
262
+ return true;
263
+ } catch (e) {
264
+ return false;
265
+ }
266
+ };
267
+
268
+ // 专门用于处理⚡分隔的字段规则
269
+ export const parseFieldRule = (rule) => {
270
+ const allParts = rule.split('⚡');
271
+
272
+ // 必须包含7个部分:显示名⚡类型⚡最小值⚡最大值⚡默认值⚡是否索引⚡正则约束
273
+ if (allParts.length !== 7) {
274
+ throw new Error(`字段规则格式错误,必须包含7个部分,当前包含${allParts.length}个部分`);
275
+ }
276
+
277
+ // 验证各个部分的格式
278
+ const [name, type, minValue, maxValue, defaultValue, isIndex, regexConstraint] = allParts;
279
+
280
+ // 第1个值:名称必须为中文、数字、字母
281
+ if (!validateFieldName(name)) {
282
+ throw new Error(`字段名称 "${name}" 格式错误,必须为中文、数字、字母`);
283
+ }
284
+
285
+ // 第2个值:字段类型必须为string,number,text,array之一
286
+ if (!validateFieldType(type)) {
287
+ throw new Error(`字段类型 "${type}" 格式错误,必须为string、number、text、array之一`);
288
+ }
289
+
290
+ // 第3个值:最小值必须为null或数字
291
+ if (!validateMinMax(minValue)) {
292
+ throw new Error(`最小值 "${minValue}" 格式错误,必须为null或数字`);
293
+ }
294
+
295
+ // 第4个值:最大值必须为null或数字
296
+ if (!validateMinMax(maxValue)) {
297
+ throw new Error(`最大值 "${maxValue}" 格式错误,必须为null或数字`);
298
+ }
299
+
300
+ // 第5个值:默认值必须为null、字符串或数字
301
+ if (!validateDefaultValue(defaultValue)) {
302
+ throw new Error(`默认值 "${defaultValue}" 格式错误,必须为null、字符串或数字`);
303
+ }
304
+
305
+ // 第6个值:是否创建索引必须为0或1
306
+ if (!validateIndex(isIndex)) {
307
+ throw new Error(`索引标识 "${isIndex}" 格式错误,必须为0或1`);
308
+ }
309
+
310
+ // 第7个值:必须为null或正则表达式
311
+ if (!validateRegex(regexConstraint)) {
312
+ throw new Error(`正则约束 "${regexConstraint}" 格式错误,必须为null或有效的正则表达式`);
313
+ }
314
+
315
+ return allParts;
316
+ };