befly 3.2.0 → 3.3.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.
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 +308 -0
  9. package/commands/start.ts +80 -0
  10. package/commands/syncApi.ts +328 -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 +301 -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,368 @@
1
+ /**
2
+ * SyncMenu 命令 - 同步菜单数据到数据库
3
+ * 说明:根据 menu.json 配置文件增量同步菜单数据(最多2级:父级和子级)
4
+ *
5
+ * 流程:
6
+ * 1. 读取 core 和 tpl 两个配置文件,core 优先覆盖 tpl
7
+ * 2. 合并菜单配置(根据 path 匹配,core 覆盖 tpl)
8
+ * 3. 子级菜单自动追加父级路径作为前缀
9
+ * 4. 根据菜单的 path 字段检查是否存在
10
+ * 5. 存在则更新其他字段(name、icon、sort、type、pid)
11
+ * 6. 不存在则新增菜单记录
12
+ * 7. 强制删除配置中不存在的菜单记录
13
+ * 注:state 字段由框架自动管理(1=正常,2=禁用,0=删除)
14
+ */
15
+
16
+ import { Logger } from '../lib/logger.js';
17
+ import { Database } from '../lib/database.js';
18
+ import { join } from 'pathe';
19
+ import { existsSync } from 'node:fs';
20
+ import { paths } from '../paths.js';
21
+
22
+ interface SyncMenuOptions {
23
+ plan?: boolean;
24
+ }
25
+
26
+ interface MenuConfig {
27
+ name: string;
28
+ path: string;
29
+ icon?: string;
30
+ sort?: number;
31
+ type?: number;
32
+ children?: MenuConfig[];
33
+ }
34
+
35
+ /**
36
+ * 读取菜单配置文件
37
+ */
38
+ async function readMenuConfig(filePath: string): Promise<MenuConfig[]> {
39
+ try {
40
+ if (!existsSync(filePath)) {
41
+ return [];
42
+ }
43
+ const file = Bun.file(filePath);
44
+ return await file.json();
45
+ } catch (error: any) {
46
+ Logger.warn(`读取菜单配置失败: ${filePath}`, error.message);
47
+ return [];
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 合并菜单配置(core 优先覆盖 tpl)
53
+ * 支持二级菜单结构:父级和子级
54
+ */
55
+ function mergeMenuConfigs(tplMenus: MenuConfig[], coreMenus: MenuConfig[]): MenuConfig[] {
56
+ const menuMap = new Map<string, MenuConfig>();
57
+
58
+ // 1. 先添加 tpl 菜单
59
+ for (const menu of tplMenus) {
60
+ if (menu.path) {
61
+ menuMap.set(menu.path, { ...menu });
62
+ }
63
+ }
64
+
65
+ // 2. core 菜单覆盖同 path 的 tpl 菜单
66
+ for (const menu of coreMenus) {
67
+ if (menu.path) {
68
+ menuMap.set(menu.path, { ...menu });
69
+ }
70
+ }
71
+
72
+ // 3. 转换为数组并处理子菜单
73
+ const result: MenuConfig[] = [];
74
+ for (const menu of menuMap.values()) {
75
+ const mergedMenu = { ...menu };
76
+
77
+ // 处理子菜单合并
78
+ if (menu.children && menu.children.length > 0) {
79
+ const childMap = new Map<string, MenuConfig>();
80
+
81
+ // 先添加 tpl 的子菜单
82
+ const tplMenu = tplMenus.find((m) => m.path === menu.path);
83
+ if (tplMenu?.children) {
84
+ for (const child of tplMenu.children) {
85
+ if (child.path) {
86
+ childMap.set(child.path, { ...child });
87
+ }
88
+ }
89
+ }
90
+
91
+ // core 子菜单覆盖
92
+ const coreMenu = coreMenus.find((m) => m.path === menu.path);
93
+ if (coreMenu?.children) {
94
+ for (const child of coreMenu.children) {
95
+ if (child.path) {
96
+ childMap.set(child.path, { ...child });
97
+ }
98
+ }
99
+ }
100
+
101
+ mergedMenu.children = Array.from(childMap.values());
102
+ }
103
+
104
+ result.push(mergedMenu);
105
+ }
106
+
107
+ return result;
108
+ }
109
+
110
+ /**
111
+ * 收集配置文件中所有菜单的 path(最多2级)
112
+ * 子级菜单自动追加父级路径前缀
113
+ */
114
+ function collectPaths(menus: MenuConfig[]): Set<string> {
115
+ const paths = new Set<string>();
116
+
117
+ for (const menu of menus) {
118
+ if (menu.path) {
119
+ paths.add(menu.path);
120
+ }
121
+ if (menu.children && menu.children.length > 0) {
122
+ for (const child of menu.children) {
123
+ if (child.path) {
124
+ // 子级菜单追加父级路径前缀
125
+ const fullPath = menu.path + child.path;
126
+ paths.add(fullPath);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ return paths;
133
+ }
134
+
135
+ /**
136
+ * 同步菜单(两层结构:父级和子级)
137
+ * 子级菜单路径自动追加父级路径前缀
138
+ */
139
+ async function syncMenus(helper: any, menus: MenuConfig[]): Promise<{ created: number; updated: number }> {
140
+ const stats = { created: 0, updated: 0 };
141
+
142
+ for (const menu of menus) {
143
+ try {
144
+ // 1. 同步父级菜单
145
+ const existingParent = await helper.getOne({
146
+ table: 'core_menu',
147
+ where: { path: menu.path || '' }
148
+ });
149
+
150
+ let parentId: number;
151
+
152
+ if (existingParent) {
153
+ await helper.updData({
154
+ table: 'core_menu',
155
+ where: { id: existingParent.id },
156
+ data: {
157
+ pid: 0,
158
+ name: menu.name,
159
+ icon: menu.icon || '',
160
+ sort: menu.sort || 0,
161
+ type: menu.type || 1
162
+ }
163
+ });
164
+ parentId = existingParent.id;
165
+ stats.updated++;
166
+ Logger.info(` └ 更新父级菜单: ${menu.name} (ID: ${parentId}, Path: ${menu.path})`);
167
+ } else {
168
+ parentId = await helper.insData({
169
+ table: 'core_menu',
170
+ data: {
171
+ pid: 0,
172
+ name: menu.name,
173
+ path: menu.path || '',
174
+ icon: menu.icon || '',
175
+ sort: menu.sort || 0,
176
+ type: menu.type || 1
177
+ }
178
+ });
179
+ stats.created++;
180
+ Logger.info(` └ 新增父级菜单: ${menu.name} (ID: ${parentId}, Path: ${menu.path})`);
181
+ }
182
+
183
+ // 2. 同步子级菜单(自动追加父级路径前缀)
184
+ if (menu.children && menu.children.length > 0) {
185
+ for (const child of menu.children) {
186
+ // 子级菜单完整路径 = 父级路径 + 子级路径
187
+ const childFullPath = menu.path + child.path;
188
+
189
+ const existingChild = await helper.getOne({
190
+ table: 'core_menu',
191
+ where: { path: childFullPath }
192
+ });
193
+
194
+ if (existingChild) {
195
+ await helper.updData({
196
+ table: 'core_menu',
197
+ where: { id: existingChild.id },
198
+ data: {
199
+ pid: parentId,
200
+ name: child.name,
201
+ icon: child.icon || '',
202
+ sort: child.sort || 0,
203
+ type: child.type || 1
204
+ }
205
+ });
206
+ stats.updated++;
207
+ Logger.info(` └ 更新子级菜单: ${child.name} (ID: ${existingChild.id}, PID: ${parentId}, Path: ${childFullPath})`);
208
+ } else {
209
+ const childId = await helper.insData({
210
+ table: 'core_menu',
211
+ data: {
212
+ pid: parentId,
213
+ name: child.name,
214
+ path: childFullPath,
215
+ icon: child.icon || '',
216
+ sort: child.sort || 0,
217
+ type: child.type || 1
218
+ }
219
+ });
220
+ stats.created++;
221
+ Logger.info(` └ 新增子级菜单: ${child.name} (ID: ${childId}, PID: ${parentId}, Path: ${childFullPath})`);
222
+ }
223
+ }
224
+ }
225
+ } catch (error: any) {
226
+ Logger.error(`同步菜单 "${menu.name}" 失败:`, error.message || String(error));
227
+ throw error;
228
+ }
229
+ }
230
+
231
+ return stats;
232
+ }
233
+
234
+ /**
235
+ * 删除配置中不存在的菜单(强制删除)
236
+ */
237
+ async function deleteObsoleteRecords(helper: any, configPaths: Set<string>): Promise<number> {
238
+ Logger.info(`\n=== 删除配置中不存在的记录 ===`);
239
+
240
+ const allRecords = await helper.getAll({
241
+ table: 'core_menu',
242
+ fields: ['id', 'path', 'name'],
243
+ where: { state$gte: 0 } // 查询所有状态(包括软删除的 state=0)
244
+ });
245
+
246
+ let deletedCount = 0;
247
+ for (const record of allRecords) {
248
+ if (record.path && !configPaths.has(record.path)) {
249
+ await helper.delForce({
250
+ table: 'core_menu',
251
+ where: { id: record.id }
252
+ });
253
+ deletedCount++;
254
+ Logger.info(` └ 删除记录: ${record.name} (ID: ${record.id}, path: ${record.path})`);
255
+ }
256
+ }
257
+
258
+ if (deletedCount === 0) {
259
+ Logger.info(' ✅ 无需删除的记录');
260
+ }
261
+
262
+ return deletedCount;
263
+ }
264
+
265
+ /**
266
+ * SyncMenu 命令主函数
267
+ */
268
+ export async function syncMenuCommand(options: SyncMenuOptions = {}) {
269
+ try {
270
+ if (options.plan) {
271
+ Logger.info('[计划] 同步菜单配置到数据库(plan 模式不执行)');
272
+ Logger.info('[计划] 1. 读取 core 和 tpl 两个配置文件');
273
+ Logger.info('[计划] 2. 合并菜单配置(core 优先覆盖 tpl)');
274
+ Logger.info('[计划] 3. 子级菜单自动追加父级路径前缀');
275
+ Logger.info('[计划] 4. 根据 path 检查菜单是否存在');
276
+ Logger.info('[计划] 5. 存在则更新,不存在则新增');
277
+ Logger.info('[计划] 6. 强制删除配置中不存在的菜单');
278
+ Logger.info('[计划] 7. 显示菜单结构预览');
279
+ return;
280
+ }
281
+
282
+ Logger.info('开始同步菜单配置到数据库...\n');
283
+
284
+ // 1. 读取两个配置文件
285
+ Logger.info('=== 步骤 1: 读取菜单配置文件 ===');
286
+ const tplMenuPath = join(paths.projectConfigDir, 'menu.json');
287
+ const coreMenuPath = join(paths.rootConfigDir, 'menu.json');
288
+
289
+ Logger.info(` tpl 路径: ${tplMenuPath}`);
290
+ Logger.info(` core 路径: ${coreMenuPath}`);
291
+
292
+ const tplMenus = await readMenuConfig(tplMenuPath);
293
+ const coreMenus = await readMenuConfig(coreMenuPath);
294
+
295
+ Logger.info(`✅ tpl 配置: ${tplMenus.length} 个父级菜单`);
296
+ Logger.info(`✅ core 配置: ${coreMenus.length} 个父级菜单`);
297
+
298
+ // 2. 合并菜单配置
299
+ Logger.info('\n=== 步骤 2: 合并菜单配置(core 优先覆盖 tpl) ===');
300
+ const mergedMenus = mergeMenuConfigs(tplMenus, coreMenus);
301
+ Logger.info(`✅ 合并后共有 ${mergedMenus.length} 个父级菜单`);
302
+
303
+ // 打印合并后的菜单结构
304
+ for (const menu of mergedMenus) {
305
+ const childCount = menu.children?.length || 0;
306
+ Logger.info(` └ ${menu.name} (${menu.path}) - ${childCount} 个子菜单`);
307
+ }
308
+
309
+ // 连接数据库(SQL + Redis)
310
+ await Database.connect();
311
+
312
+ const helper = Database.getDbHelper();
313
+
314
+ // 3. 检查表是否存在
315
+ Logger.info('\n=== 步骤 3: 检查数据表 ===');
316
+ const exists = await helper.tableExists('core_menu');
317
+
318
+ if (!exists) {
319
+ Logger.error(`❌ 表 core_menu 不存在,请先运行 befly syncDb 同步数据库`);
320
+ process.exit(1);
321
+ }
322
+
323
+ Logger.info(`✅ 表 core_menu 存在`);
324
+
325
+ // 4. 收集配置文件中所有菜单的 path
326
+ Logger.info('\n=== 步骤 4: 收集配置菜单路径 ===');
327
+ const configPaths = collectPaths(mergedMenus);
328
+ Logger.info(`✅ 配置文件中共有 ${configPaths.size} 个菜单路径`);
329
+
330
+ // 5. 同步菜单
331
+ Logger.info('\n=== 步骤 5: 同步菜单数据(新增/更新) ===');
332
+ const stats = await syncMenus(helper, mergedMenus);
333
+
334
+ // 6. 删除文件中不存在的菜单(强制删除)
335
+ const deletedCount = await deleteObsoleteRecords(helper, configPaths);
336
+
337
+ // 7. 构建树形结构预览
338
+ Logger.info('\n=== 步骤 7: 菜单结构预览 ===');
339
+ const allMenus = await helper.getAll({
340
+ table: 'core_menu',
341
+ fields: ['id', 'pid', 'name', 'path', 'type'],
342
+ orderBy: ['pid#ASC', 'sort#ASC', 'id#ASC']
343
+ });
344
+
345
+ const parentMenus = allMenus.filter((m: any) => m.pid === 0);
346
+ for (const parent of parentMenus) {
347
+ const children = allMenus.filter((m: any) => m.pid === parent.id);
348
+ Logger.info(` └ ${parent.name} (${parent.path})`);
349
+ for (const child of children) {
350
+ Logger.info(` └ ${child.name} (${child.path})`);
351
+ }
352
+ }
353
+
354
+ // 8. 输出统计信息
355
+ Logger.info(`\n=== 菜单同步完成 ===`);
356
+ Logger.info(`新增菜单: ${stats.created} 个`);
357
+ Logger.info(`更新菜单: ${stats.updated} 个`);
358
+ Logger.info(`删除菜单: ${deletedCount} 个`);
359
+ Logger.info(`当前父级菜单: ${allMenus.filter((m: any) => m.pid === 0).length} 个`);
360
+ Logger.info(`当前子级菜单: ${allMenus.filter((m: any) => m.pid !== 0).length} 个`);
361
+ Logger.info('提示: 菜单缓存将在服务器启动时自动完成');
362
+ } catch (error: any) {
363
+ Logger.error('菜单同步失败:', error);
364
+ process.exit(1);
365
+ } finally {
366
+ await Database?.disconnect();
367
+ }
368
+ }
package/config/env.ts CHANGED
@@ -12,13 +12,13 @@ export interface EnvConfig {
12
12
  NODE_ENV: string;
13
13
  /** 应用名称 */
14
14
  APP_NAME: string;
15
- /** MD5 加密盐 */
16
- MD5_SALT: string;
17
15
  /** 监听端口 */
18
16
  APP_PORT: number;
19
17
  /** 监听主机 */
20
18
  APP_HOST: string;
21
- /** 超级管理员密码 */
19
+ /** 开发管理员邮箱 */
20
+ DEV_EMAIL: string;
21
+ /** 开发管理员密码 */
22
22
  DEV_PASSWORD: string;
23
23
  /** 请求体大小限制(字节) */
24
24
  BODY_LIMIT: number;
@@ -150,9 +150,9 @@ export const Env: EnvConfig = {
150
150
  // ========== 项目配置 ==========
151
151
  NODE_ENV: getEnv('NODE_ENV', 'development'),
152
152
  APP_NAME: getEnv('APP_NAME', 'befly'),
153
- MD5_SALT: getEnv('MD5_SALT', 'befly'),
154
153
  APP_PORT: getEnvNumber('APP_PORT', 3000),
155
154
  APP_HOST: getEnv('APP_HOST', '0.0.0.0'),
155
+ DEV_EMAIL: getEnv('DEV_EMAIL', 'dev@qq.com'),
156
156
  DEV_PASSWORD: getEnv('DEV_PASSWORD', ''),
157
157
  BODY_LIMIT: getEnvNumber('BODY_LIMIT', 10485760), // 10MB
158
158
  PARAMS_CHECK: getEnv('PARAMS_CHECK', 'true'),
@@ -0,0 +1,67 @@
1
+ [
2
+ {
3
+ "name": "首页",
4
+ "path": "/",
5
+ "icon": "Home",
6
+ "sort": 1,
7
+ "type": 1
8
+ },
9
+ {
10
+ "name": "管理员管理",
11
+ "path": "/admin",
12
+ "icon": "Users",
13
+ "sort": 2,
14
+ "type": 1
15
+ },
16
+ {
17
+ "name": "新闻管理",
18
+ "path": "/news",
19
+ "icon": "Newspaper",
20
+ "sort": 3,
21
+ "type": 1
22
+ },
23
+ {
24
+ "name": "菜单管理",
25
+ "path": "/menu",
26
+ "icon": "Menu",
27
+ "sort": 4,
28
+ "type": 1
29
+ },
30
+ {
31
+ "name": "角色管理",
32
+ "path": "/role",
33
+ "icon": "Users",
34
+ "sort": 5,
35
+ "type": 1
36
+ },
37
+ {
38
+ "name": "字典管理",
39
+ "path": "/dict",
40
+ "icon": "BookOpen",
41
+ "sort": 6,
42
+ "type": 1
43
+ },
44
+ {
45
+ "name": "用户管理",
46
+ "path": "/user",
47
+ "icon": "UserCog",
48
+ "sort": 7,
49
+ "type": 1,
50
+ "children": [
51
+ {
52
+ "name": "用户列表",
53
+ "path": "/list",
54
+ "icon": "Minus",
55
+ "sort": 1,
56
+ "type": 1
57
+ },
58
+ {
59
+ "name": "用户权限",
60
+ "path": "/permission",
61
+ "icon": "Minus",
62
+ "sort": 2,
63
+ "type": 1
64
+ }
65
+ ]
66
+ }
67
+ ]
@@ -9,7 +9,7 @@ import type { EncodingType, HashAlgorithm, PasswordHashOptions } from '../types/
9
9
  /**
10
10
  * 加密工具类
11
11
  */
12
- export class Crypto2 {
12
+ export class Cipher {
13
13
  /**
14
14
  * MD5 哈希
15
15
  * @param data - 要哈希的数据
@@ -178,23 +178,18 @@ export class Crypto2 {
178
178
  }
179
179
 
180
180
  /**
181
- * 创建流式哈希器
182
- * @param algorithm - 算法名称
183
- * @param key - 可选的 HMAC 密钥
184
- * @returns 流式哈希器实例
185
- */
186
- static createHasher(algorithm: HashAlgorithm, key: string | Uint8Array | null = null): StreamHasher {
187
- return new StreamHasher(algorithm, key);
188
- }
189
-
190
- /**
191
- * 密码哈希 (使用 Argon2)
181
+ * 密码哈希(使用 bcrypt 算法)
192
182
  * @param password - 密码
193
- * @param options - 选项
194
- * @returns 哈希后的密码
183
+ * @param options - 选项(可选,支持自定义 cost 参数)
184
+ * @returns 哈希后的密码(固定 60 字符)
185
+ * @example
186
+ * // 默认配置(推荐)
187
+ * const hash = await Cipher.hashPassword('123456');
188
+ *
189
+ * // 自定义强度(可选)
190
+ * const hash = await Cipher.hashPassword('123456', { cost: 12 });
195
191
  */
196
192
  static async hashPassword(password: string, options: PasswordHashOptions = {}): Promise<string> {
197
- // 设置默认算法为 bcrypt
198
193
  const finalOptions = {
199
194
  algorithm: 'bcrypt',
200
195
  ...options
@@ -205,8 +200,13 @@ export class Crypto2 {
205
200
  /**
206
201
  * 验证密码
207
202
  * @param password - 原始密码
208
- * @param hash - 哈希值
203
+ * @param hash - 存储的哈希值(自动识别算法和提取盐值)
209
204
  * @returns 验证结果
205
+ * @example
206
+ * const isValid = await Cipher.verifyPassword('123456', storedHash);
207
+ * if (isValid) {
208
+ * // 密码正确
209
+ * }
210
210
  */
211
211
  static async verifyPassword(password: string, hash: string): Promise<boolean> {
212
212
  return await Bun.password.verify(password, hash);
@@ -255,54 +255,3 @@ export class Crypto2 {
255
255
  return Bun.hash(data, seed);
256
256
  }
257
257
  }
258
-
259
- /**
260
- * 流式哈希器类
261
- */
262
- export class StreamHasher {
263
- private hasher: any;
264
- private finalized: boolean = false;
265
-
266
- constructor(algorithm: HashAlgorithm, key: string | Uint8Array | null = null) {
267
- this.hasher = new Bun.CryptoHasher(algorithm, key);
268
- }
269
-
270
- /**
271
- * 更新数据
272
- * @param data - 数据
273
- * @returns 支持链式调用
274
- */
275
- update(data: string | Uint8Array): this {
276
- if (this.finalized) {
277
- throw new Error('哈希器已经完成,不能再更新数据');
278
- }
279
- this.hasher.update(data);
280
- return this;
281
- }
282
-
283
- /**
284
- * 生成最终哈希值
285
- * @param encoding - 输出编码
286
- * @returns 哈希值
287
- */
288
- digest(encoding: EncodingType = 'hex'): string {
289
- if (this.finalized) {
290
- throw new Error('哈希器已经完成');
291
- }
292
- this.finalized = true;
293
- return this.hasher.digest(encoding);
294
- }
295
-
296
- /**
297
- * 复制哈希器
298
- * @returns 新的哈希器实例
299
- */
300
- copy(): StreamHasher {
301
- if (this.finalized) {
302
- throw new Error('不能复制已完成的哈希器');
303
- }
304
- const newHasher = new StreamHasher('md5'); // 临时算法
305
- newHasher.hasher = this.hasher.copy();
306
- return newHasher;
307
- }
308
- }