befly 3.8.1 → 3.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/check.ts +57 -24
- package/lib/cacheHelper.ts +169 -0
- package/lib/redisHelper.ts +49 -65
- package/lib/validator.ts +29 -32
- package/lifecycle/bootstrap.ts +5 -19
- package/lifecycle/lifecycle.ts +16 -59
- package/lifecycle/loadApis.ts +164 -0
- package/lifecycle/loadPlugins.ts +244 -0
- package/main.ts +12 -15
- package/menu.json +13 -13
- package/package.json +2 -2
- package/plugins/cache.ts +3 -167
- package/types/common.d.ts +28 -7
- package/util.ts +4 -48
- package/lifecycle/loader.ts +0 -427
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件加载器
|
|
3
|
+
* 负责扫描和初始化所有插件(核心、组件、用户)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { basename } from 'pathe';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { camelCase } from 'es-toolkit/string';
|
|
9
|
+
import { Logger } from '../lib/logger.js';
|
|
10
|
+
import { calcPerfTime } from '../util.js';
|
|
11
|
+
import { corePluginDir, projectPluginDir } from '../paths.js';
|
|
12
|
+
import { Addon } from '../lib/addon.js';
|
|
13
|
+
import type { Plugin } from '../types/plugin.js';
|
|
14
|
+
import type { BeflyContext } from '../types/befly.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 排序插件(根据依赖关系)
|
|
18
|
+
*/
|
|
19
|
+
function sortPlugins(plugins: Plugin[]): Plugin[] | false {
|
|
20
|
+
const result: Plugin[] = [];
|
|
21
|
+
const visited = new Set<string>();
|
|
22
|
+
const visiting = new Set<string>();
|
|
23
|
+
const pluginMap: Record<string, Plugin> = Object.fromEntries(plugins.map((p) => [p.pluginName || p.name, p]));
|
|
24
|
+
let isPass = true;
|
|
25
|
+
|
|
26
|
+
const visit = (name: string): void => {
|
|
27
|
+
if (visited.has(name)) return;
|
|
28
|
+
if (visiting.has(name)) {
|
|
29
|
+
isPass = false;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const plugin = pluginMap[name];
|
|
34
|
+
if (!plugin) return;
|
|
35
|
+
|
|
36
|
+
visiting.add(name);
|
|
37
|
+
(plugin.after || []).forEach(visit);
|
|
38
|
+
visiting.delete(name);
|
|
39
|
+
visited.add(name);
|
|
40
|
+
result.push(plugin);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
plugins.forEach((p) => visit(p.pluginName || p.name));
|
|
44
|
+
return isPass ? result : false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 扫描核心插件
|
|
49
|
+
*/
|
|
50
|
+
async function scanCorePlugins(loadedPluginNames: Set<string>): Promise<Plugin[]> {
|
|
51
|
+
const plugins: Plugin[] = [];
|
|
52
|
+
const glob = new Bun.Glob('*.ts');
|
|
53
|
+
|
|
54
|
+
for await (const file of glob.scan({
|
|
55
|
+
cwd: corePluginDir,
|
|
56
|
+
onlyFiles: true,
|
|
57
|
+
absolute: true
|
|
58
|
+
})) {
|
|
59
|
+
const fileName = basename(file).replace(/\.ts$/, '');
|
|
60
|
+
if (fileName.startsWith('_')) continue;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const pluginImport = await import(file);
|
|
64
|
+
const plugin = pluginImport.default;
|
|
65
|
+
plugin.pluginName = fileName;
|
|
66
|
+
plugins.push(plugin);
|
|
67
|
+
loadedPluginNames.add(fileName);
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
Logger.error(`核心插件 ${fileName} 导入失败`, err);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return plugins;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 扫描组件插件
|
|
79
|
+
*/
|
|
80
|
+
async function scanAddonPlugins(loadedPluginNames: Set<string>): Promise<Plugin[]> {
|
|
81
|
+
const plugins: Plugin[] = [];
|
|
82
|
+
const glob = new Bun.Glob('*.ts');
|
|
83
|
+
const addons = Addon.scan();
|
|
84
|
+
|
|
85
|
+
for (const addon of addons) {
|
|
86
|
+
if (!Addon.dirExists(addon, 'plugins')) continue;
|
|
87
|
+
|
|
88
|
+
const addonPluginsDir = Addon.getDir(addon, 'plugins');
|
|
89
|
+
for await (const file of glob.scan({
|
|
90
|
+
cwd: addonPluginsDir,
|
|
91
|
+
onlyFiles: true,
|
|
92
|
+
absolute: true
|
|
93
|
+
})) {
|
|
94
|
+
const fileName = basename(file).replace(/\.ts$/, '');
|
|
95
|
+
if (fileName.startsWith('_')) continue;
|
|
96
|
+
|
|
97
|
+
const addonCamelCase = camelCase(addon);
|
|
98
|
+
const fileNameCamelCase = camelCase(fileName);
|
|
99
|
+
const pluginFullName = `addon${addonCamelCase.charAt(0).toUpperCase() + addonCamelCase.slice(1)}_${fileNameCamelCase}`;
|
|
100
|
+
|
|
101
|
+
if (loadedPluginNames.has(pluginFullName)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const plugin = require(file);
|
|
107
|
+
const pluginInstance = plugin.default;
|
|
108
|
+
pluginInstance.pluginName = pluginFullName;
|
|
109
|
+
plugins.push(pluginInstance);
|
|
110
|
+
loadedPluginNames.add(pluginFullName);
|
|
111
|
+
} catch (err: any) {
|
|
112
|
+
Logger.error(`组件${addon} ${fileName} 导入失败`, err);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return plugins;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 扫描用户插件
|
|
123
|
+
*/
|
|
124
|
+
async function scanUserPlugins(loadedPluginNames: Set<string>): Promise<Plugin[]> {
|
|
125
|
+
const plugins: Plugin[] = [];
|
|
126
|
+
|
|
127
|
+
if (!existsSync(projectPluginDir)) {
|
|
128
|
+
return plugins;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const glob = new Bun.Glob('*.ts');
|
|
132
|
+
for await (const file of glob.scan({
|
|
133
|
+
cwd: projectPluginDir,
|
|
134
|
+
onlyFiles: true,
|
|
135
|
+
absolute: true
|
|
136
|
+
})) {
|
|
137
|
+
const fileName = basename(file).replace(/\.ts$/, '');
|
|
138
|
+
if (fileName.startsWith('_')) continue;
|
|
139
|
+
|
|
140
|
+
const fileNameCamelCase = camelCase(fileName);
|
|
141
|
+
const pluginFullName = `app${fileNameCamelCase.charAt(0).toUpperCase() + fileNameCamelCase.slice(1)}`;
|
|
142
|
+
|
|
143
|
+
if (loadedPluginNames.has(pluginFullName)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const plugin = require(file);
|
|
149
|
+
const pluginInstance = plugin.default;
|
|
150
|
+
pluginInstance.pluginName = pluginFullName;
|
|
151
|
+
plugins.push(pluginInstance);
|
|
152
|
+
loadedPluginNames.add(pluginFullName);
|
|
153
|
+
} catch (err: any) {
|
|
154
|
+
Logger.error(`用户插件 ${fileName} 导入失败`, err);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return plugins;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 初始化单个插件
|
|
164
|
+
*/
|
|
165
|
+
async function initPlugin(befly: { pluginLists: Plugin[]; appContext: BeflyContext }, plugin: Plugin): Promise<void> {
|
|
166
|
+
befly.pluginLists.push(plugin);
|
|
167
|
+
|
|
168
|
+
if (typeof plugin?.onInit === 'function') {
|
|
169
|
+
befly.appContext[plugin.pluginName] = await plugin?.onInit(befly.appContext);
|
|
170
|
+
} else {
|
|
171
|
+
befly.appContext[plugin.pluginName] = {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 加载所有插件
|
|
177
|
+
* @param befly - Befly实例(需要访问 pluginLists 和 appContext)
|
|
178
|
+
*/
|
|
179
|
+
export async function loadPlugins(befly: { pluginLists: Plugin[]; appContext: BeflyContext }): Promise<void> {
|
|
180
|
+
try {
|
|
181
|
+
const loadStartTime = Bun.nanoseconds();
|
|
182
|
+
const loadedPluginNames = new Set<string>();
|
|
183
|
+
|
|
184
|
+
// 阶段1:扫描所有插件
|
|
185
|
+
const corePlugins = await scanCorePlugins(loadedPluginNames);
|
|
186
|
+
const addonPlugins = await scanAddonPlugins(loadedPluginNames);
|
|
187
|
+
const userPlugins = await scanUserPlugins(loadedPluginNames);
|
|
188
|
+
|
|
189
|
+
// 阶段2:分层排序插件
|
|
190
|
+
const sortedCorePlugins = sortPlugins(corePlugins);
|
|
191
|
+
if (sortedCorePlugins === false) {
|
|
192
|
+
Logger.error('核心插件依赖关系错误,请检查插件的 after 属性');
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const sortedAddonPlugins = sortPlugins(addonPlugins);
|
|
197
|
+
if (sortedAddonPlugins === false) {
|
|
198
|
+
Logger.error('组件插件依赖关系错误,请检查插件的 after 属性');
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sortedUserPlugins = sortPlugins(userPlugins);
|
|
203
|
+
if (sortedUserPlugins === false) {
|
|
204
|
+
Logger.error('用户插件依赖关系错误,请检查插件的 after 属性');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 阶段3:分层初始化插件(核心 → 组件 → 用户)
|
|
209
|
+
// 3.1 初始化核心插件
|
|
210
|
+
for (const plugin of sortedCorePlugins) {
|
|
211
|
+
try {
|
|
212
|
+
await initPlugin(befly, plugin);
|
|
213
|
+
} catch (error: any) {
|
|
214
|
+
Logger.error(`核心插件 ${plugin.pluginName} 初始化失败`, error);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 3.2 初始化组件插件
|
|
220
|
+
for (const plugin of sortedAddonPlugins) {
|
|
221
|
+
try {
|
|
222
|
+
await initPlugin(befly, plugin);
|
|
223
|
+
} catch (error: any) {
|
|
224
|
+
Logger.error(`组件插件 ${plugin.pluginName} 初始化失败`, error);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 3.3 初始化用户插件
|
|
230
|
+
for (const plugin of sortedUserPlugins) {
|
|
231
|
+
try {
|
|
232
|
+
await initPlugin(befly, plugin);
|
|
233
|
+
} catch (error: any) {
|
|
234
|
+
Logger.error(`用户插件 ${plugin.pluginName} 初始化失败`, error);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const totalLoadTime = calcPerfTime(loadStartTime);
|
|
240
|
+
} catch (error: any) {
|
|
241
|
+
Logger.error('加载插件时发生错误', error);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
package/main.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Env } from './env.js';
|
|
7
|
-
import { Yes, No } from './util.js';
|
|
7
|
+
import { Yes, No, keysToSnake, keysToCamel, arrayKeysToCamel, pickFields, fieldClear, calcPerfTime } from './util.js';
|
|
8
8
|
import { Logger } from './lib/logger.js';
|
|
9
9
|
import { Cipher } from './lib/cipher.js';
|
|
10
10
|
import { Jwt } from './lib/jwt.js';
|
|
@@ -15,10 +15,9 @@ import { DbHelper } from './lib/dbHelper.js';
|
|
|
15
15
|
import { RedisHelper } from './lib/redisHelper.js';
|
|
16
16
|
import { Addon } from './lib/addon.js';
|
|
17
17
|
import { checkDefault } from './check.js';
|
|
18
|
-
import * as utilFunctions from './util.js';
|
|
19
18
|
|
|
20
19
|
import type { Server } from 'bun';
|
|
21
|
-
import type { BeflyContext
|
|
20
|
+
import type { BeflyContext } from './types/befly.js';
|
|
22
21
|
/**
|
|
23
22
|
* Befly 框架核心类
|
|
24
23
|
* 职责:管理应用上下文和生命周期
|
|
@@ -30,17 +29,16 @@ export class Befly {
|
|
|
30
29
|
/** 应用上下文 */
|
|
31
30
|
public appContext: BeflyContext;
|
|
32
31
|
|
|
33
|
-
constructor(
|
|
34
|
-
this.lifecycle = new Lifecycle(
|
|
32
|
+
constructor() {
|
|
33
|
+
this.lifecycle = new Lifecycle();
|
|
35
34
|
this.appContext = {};
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
/**
|
|
39
38
|
* 启动服务器并注册优雅关闭处理
|
|
40
|
-
* @param callback - 启动完成后的回调函数
|
|
41
39
|
*/
|
|
42
|
-
async listen(
|
|
43
|
-
const server = await this.lifecycle.start(this.appContext
|
|
40
|
+
async listen(): Promise<Server> {
|
|
41
|
+
const server = await this.lifecycle.start(this.appContext);
|
|
44
42
|
|
|
45
43
|
const gracefulShutdown = async (signal: string) => {
|
|
46
44
|
// 1. 停止接收新请求
|
|
@@ -85,11 +83,10 @@ export {
|
|
|
85
83
|
|
|
86
84
|
// 工具函数命名空间导出
|
|
87
85
|
export const utils = {
|
|
88
|
-
keysToSnake:
|
|
89
|
-
keysToCamel:
|
|
90
|
-
arrayKeysToCamel:
|
|
91
|
-
pickFields:
|
|
92
|
-
fieldClear:
|
|
93
|
-
calcPerfTime:
|
|
94
|
-
parseRule: utilFunctions.parseRule
|
|
86
|
+
keysToSnake: keysToSnake,
|
|
87
|
+
keysToCamel: keysToCamel,
|
|
88
|
+
arrayKeysToCamel: arrayKeysToCamel,
|
|
89
|
+
pickFields: pickFields,
|
|
90
|
+
fieldClear: fieldClear,
|
|
91
|
+
calcPerfTime: calcPerfTime
|
|
95
92
|
};
|
package/menu.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"name": "首页",
|
|
4
|
-
"path": "/",
|
|
5
|
-
"icon": "
|
|
4
|
+
"path": "/internal/index",
|
|
5
|
+
"icon": "",
|
|
6
6
|
"sort": 1,
|
|
7
7
|
"type": 1
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"name": "人员管理",
|
|
11
|
-
"path": "
|
|
12
|
-
"icon": "
|
|
11
|
+
"path": "_people",
|
|
12
|
+
"icon": "",
|
|
13
13
|
"sort": 2,
|
|
14
14
|
"type": 1,
|
|
15
15
|
"children": [
|
|
16
16
|
{
|
|
17
17
|
"name": "管理员管理",
|
|
18
|
-
"path": "/admin",
|
|
19
|
-
"icon": "
|
|
18
|
+
"path": "/internal/admin",
|
|
19
|
+
"icon": "",
|
|
20
20
|
"sort": 2,
|
|
21
21
|
"type": 1
|
|
22
22
|
}
|
|
@@ -24,22 +24,22 @@
|
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"name": "权限设置",
|
|
27
|
-
"path": "
|
|
28
|
-
"icon": "
|
|
27
|
+
"path": "_permission",
|
|
28
|
+
"icon": "",
|
|
29
29
|
"sort": 3,
|
|
30
30
|
"type": 1,
|
|
31
31
|
"children": [
|
|
32
32
|
{
|
|
33
33
|
"name": "菜单管理",
|
|
34
|
-
"path": "/menu",
|
|
35
|
-
"icon": "
|
|
34
|
+
"path": "/internal/menu",
|
|
35
|
+
"icon": "",
|
|
36
36
|
"sort": 4,
|
|
37
37
|
"type": 1
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"name": "角色管理",
|
|
41
|
-
"path": "/role",
|
|
42
|
-
"icon": "
|
|
41
|
+
"path": "/internal/role",
|
|
42
|
+
"icon": "",
|
|
43
43
|
"sort": 5,
|
|
44
44
|
"type": 1
|
|
45
45
|
}
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
{
|
|
49
49
|
"name": "字典管理",
|
|
50
50
|
"path": "/internal/dict",
|
|
51
|
-
"icon": "
|
|
51
|
+
"icon": "",
|
|
52
52
|
"sort": 6,
|
|
53
53
|
"type": 1
|
|
54
54
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.2",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"es-toolkit": "^1.41.0",
|
|
70
70
|
"pathe": "^2.0.3"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "c0844203f8fee734d2a8742f3b77b67bf0874681"
|
|
73
73
|
}
|
package/plugins/cache.ts
CHANGED
|
@@ -3,168 +3,9 @@
|
|
|
3
3
|
* 负责在服务器启动时缓存接口、菜单和角色权限到 Redis
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { CacheHelper } from '../lib/cacheHelper.js';
|
|
7
7
|
import type { Plugin } from '../types/plugin.js';
|
|
8
8
|
import type { BeflyContext } from '../types/befly.js';
|
|
9
|
-
import type { ApiRoute } from '../types/api.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 缓存管理器类
|
|
13
|
-
*/
|
|
14
|
-
class CacheManager {
|
|
15
|
-
private appContext: BeflyContext;
|
|
16
|
-
|
|
17
|
-
constructor(appContext: BeflyContext) {
|
|
18
|
-
this.appContext = appContext;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 缓存所有接口到 Redis
|
|
23
|
-
*/
|
|
24
|
-
async cacheApis(): Promise<void> {
|
|
25
|
-
try {
|
|
26
|
-
// 检查表是否存在
|
|
27
|
-
const tableExists = await this.appContext.db.tableExists('core_api');
|
|
28
|
-
if (!tableExists) {
|
|
29
|
-
Logger.warn('⚠️ 接口表不存在,跳过接口缓存');
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 从数据库查询所有接口(与 apiAll.ts 保持一致)
|
|
34
|
-
const apiList = await this.appContext.db.getAll({
|
|
35
|
-
table: 'core_api',
|
|
36
|
-
fields: ['id', 'name', 'path', 'method', 'description', 'addonName', 'addonTitle'],
|
|
37
|
-
orderBy: ['addonName#ASC', 'path#ASC']
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// 缓存到 Redis
|
|
41
|
-
const result = await this.appContext.redis.setObject('apis:all', apiList);
|
|
42
|
-
|
|
43
|
-
if (result === null) {
|
|
44
|
-
Logger.warn('⚠️ 接口缓存失败');
|
|
45
|
-
} else {
|
|
46
|
-
Logger.info(`✅ 已缓存 ${apiList.length} 个接口到 Redis (Key: apis:all)`);
|
|
47
|
-
}
|
|
48
|
-
} catch (error: any) {
|
|
49
|
-
Logger.error('⚠️ 接口缓存异常:', error);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 缓存所有菜单到 Redis(从数据库读取)
|
|
55
|
-
*/
|
|
56
|
-
async cacheMenus(): Promise<void> {
|
|
57
|
-
try {
|
|
58
|
-
// 检查表是否存在
|
|
59
|
-
const tableExists = await this.appContext.db.tableExists('core_menu');
|
|
60
|
-
if (!tableExists) {
|
|
61
|
-
Logger.warn('⚠️ 菜单表不存在,跳过菜单缓存');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 从数据库查询所有菜单
|
|
66
|
-
const menus = await this.appContext.db.getAll({
|
|
67
|
-
table: 'core_menu',
|
|
68
|
-
fields: ['id', 'pid', 'name', 'path', 'icon', 'type', 'sort'],
|
|
69
|
-
orderBy: ['sort#ASC', 'id#ASC']
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// 缓存到 Redis
|
|
73
|
-
const result = await this.appContext.redis.setObject('menus:all', menus);
|
|
74
|
-
|
|
75
|
-
if (result === null) {
|
|
76
|
-
Logger.warn('⚠️ 菜单缓存失败');
|
|
77
|
-
} else {
|
|
78
|
-
Logger.info(`✅ 已缓存 ${menus.length} 个菜单到 Redis (Key: menus:all)`);
|
|
79
|
-
}
|
|
80
|
-
} catch (error: any) {
|
|
81
|
-
const errorMessage = error?.message || error?.toString?.() || String(error);
|
|
82
|
-
Logger.warn('⚠️ 菜单缓存异常:', errorMessage);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* 缓存所有角色的接口权限到 Redis
|
|
88
|
-
*/
|
|
89
|
-
async cacheRolePermissions(): Promise<void> {
|
|
90
|
-
try {
|
|
91
|
-
// 检查表是否存在
|
|
92
|
-
const apiTableExists = await this.appContext.db.tableExists('core_api');
|
|
93
|
-
const roleTableExists = await this.appContext.db.tableExists('core_role');
|
|
94
|
-
|
|
95
|
-
if (!apiTableExists || !roleTableExists) {
|
|
96
|
-
Logger.warn('⚠️ 接口或角色表不存在,跳过角色权限缓存');
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 查询所有角色
|
|
101
|
-
const roles = await this.appContext.db.getAll({
|
|
102
|
-
table: 'core_role',
|
|
103
|
-
fields: ['id', 'code', 'apis']
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// 查询所有接口(用于权限映射)
|
|
107
|
-
const allApis = await this.appContext.db.getAll({
|
|
108
|
-
table: 'core_api',
|
|
109
|
-
fields: ['id', 'name', 'path', 'method', 'description', 'addonName']
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// 为每个角色缓存接口权限
|
|
113
|
-
let cachedRoles = 0;
|
|
114
|
-
for (const role of roles) {
|
|
115
|
-
if (!role.apis) continue;
|
|
116
|
-
|
|
117
|
-
// 解析角色的接口 ID 列表
|
|
118
|
-
const apiIds = role.apis
|
|
119
|
-
.split(',')
|
|
120
|
-
.map((id: string) => parseInt(id.trim()))
|
|
121
|
-
.filter((id: number) => !isNaN(id));
|
|
122
|
-
|
|
123
|
-
// 根据 ID 过滤出接口路径
|
|
124
|
-
const roleApiPaths = allApis.filter((api: any) => apiIds.includes(api.id)).map((api: any) => `${api.method}${api.path}`);
|
|
125
|
-
|
|
126
|
-
if (roleApiPaths.length === 0) continue;
|
|
127
|
-
|
|
128
|
-
// 使用 Redis Set 缓存角色权限(性能优化:SADD + SISMEMBER)
|
|
129
|
-
const redisKey = `role:apis:${role.code}`;
|
|
130
|
-
|
|
131
|
-
// 先删除旧数据
|
|
132
|
-
await this.appContext.redis.del(redisKey);
|
|
133
|
-
|
|
134
|
-
// 批量添加到 Set
|
|
135
|
-
const result = await this.appContext.redis.sadd(redisKey, roleApiPaths);
|
|
136
|
-
|
|
137
|
-
if (result > 0) {
|
|
138
|
-
cachedRoles++;
|
|
139
|
-
Logger.debug(` └ 角色 ${role.code}: ${result} 个接口`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
Logger.info(`✅ 已缓存 ${cachedRoles} 个角色的接口权限`);
|
|
144
|
-
} catch (error: any) {
|
|
145
|
-
const errorMessage = error?.message || error?.toString?.() || String(error);
|
|
146
|
-
Logger.warn('⚠️ 角色权限缓存异常:', errorMessage);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* 缓存所有数据
|
|
152
|
-
*/
|
|
153
|
-
async cacheAll(): Promise<void> {
|
|
154
|
-
Logger.info('========== 开始缓存数据到 Redis ==========');
|
|
155
|
-
|
|
156
|
-
// 1. 缓存接口
|
|
157
|
-
await this.cacheApis();
|
|
158
|
-
|
|
159
|
-
// 2. 缓存菜单
|
|
160
|
-
await this.cacheMenus();
|
|
161
|
-
|
|
162
|
-
// 3. 缓存角色权限
|
|
163
|
-
await this.cacheRolePermissions();
|
|
164
|
-
|
|
165
|
-
Logger.info('========== 数据缓存完成 ==========\n');
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
9
|
|
|
169
10
|
/**
|
|
170
11
|
* 缓存插件
|
|
@@ -173,13 +14,8 @@ const cachePlugin: Plugin = {
|
|
|
173
14
|
name: '_cache',
|
|
174
15
|
after: ['_db', '_redis'],
|
|
175
16
|
|
|
176
|
-
async onInit(befly: BeflyContext): Promise<
|
|
177
|
-
|
|
178
|
-
const cacheManager = new CacheManager(befly);
|
|
179
|
-
return cacheManager;
|
|
180
|
-
} catch (error: any) {
|
|
181
|
-
throw error;
|
|
182
|
-
}
|
|
17
|
+
async onInit(befly: BeflyContext): Promise<CacheHelper> {
|
|
18
|
+
return new CacheHelper(befly);
|
|
183
19
|
}
|
|
184
20
|
};
|
|
185
21
|
|
package/types/common.d.ts
CHANGED
|
@@ -21,9 +21,33 @@ export interface ValidationResult {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* 字段定义类型(对象格式)
|
|
25
|
+
*/
|
|
26
|
+
export interface FieldDefinition {
|
|
27
|
+
name: string; // 字段标签/描述
|
|
28
|
+
detail: string; // 字段详细说明
|
|
29
|
+
type: 'string' | 'number' | 'text' | 'array_string' | 'array_text';
|
|
30
|
+
min: number | null; // 最小值/最小长度
|
|
31
|
+
max: number | null; // 最大值/最大长度
|
|
32
|
+
default: any; // 默认值
|
|
33
|
+
index: boolean; // 是否创建索引
|
|
34
|
+
unique: boolean; // 是否唯一
|
|
35
|
+
comment: string; // 字段注释
|
|
36
|
+
nullable: boolean; // 是否允许为空
|
|
37
|
+
unsigned: boolean; // 是否无符号(仅number类型)
|
|
38
|
+
regexp: string | null; // 正则验证
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 表定义类型(对象格式)
|
|
43
|
+
*/
|
|
44
|
+
export type TableDefinition = Record<string, FieldDefinition>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 字段规则字符串(已废弃,保留用于兼容)
|
|
25
48
|
* 格式: "字段名|类型|最小值|最大值|默认值|是否索引|正则约束"
|
|
26
49
|
*
|
|
50
|
+
* @deprecated 请使用 FieldDefinition 对象格式
|
|
27
51
|
* @example
|
|
28
52
|
* "用户名|string|2|50|null|1|^[a-zA-Z0-9_]+$"
|
|
29
53
|
* "年龄|number|0|150|18|0|null"
|
|
@@ -31,12 +55,9 @@ export interface ValidationResult {
|
|
|
31
55
|
export type FieldRule = string;
|
|
32
56
|
|
|
33
57
|
/**
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 解析后的字段规则
|
|
58
|
+
* 解析后的字段规则(已废弃,保留用于兼容)
|
|
59
|
+
*
|
|
60
|
+
* @deprecated 请使用 FieldDefinition 对象格式
|
|
40
61
|
*/
|
|
41
62
|
export interface ParsedFieldRule {
|
|
42
63
|
name: string; // 字段名称
|
package/util.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { projectDir } from './paths.js';
|
|
|
9
9
|
import type { KeyValue } from './types/common.js';
|
|
10
10
|
import type { JwtPayload, JwtSignOptions, JwtVerifyOptions } from './types/jwt';
|
|
11
11
|
import type { Plugin } from './types/plugin.js';
|
|
12
|
-
import type { ParsedFieldRule } from './types/common.js';
|
|
13
12
|
|
|
14
13
|
// ========================================
|
|
15
14
|
// API 响应工具
|
|
@@ -39,6 +38,10 @@ export const No = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue
|
|
|
39
38
|
};
|
|
40
39
|
};
|
|
41
40
|
|
|
41
|
+
// ========================================
|
|
42
|
+
// 动态导入工具
|
|
43
|
+
// ========================================
|
|
44
|
+
|
|
42
45
|
// ========================================
|
|
43
46
|
// 字段转换工具(重新导出 lib/convert.ts)
|
|
44
47
|
// ========================================
|
|
@@ -168,50 +171,3 @@ export const calcPerfTime = (startTime: number, endTime: number = Bun.nanosecond
|
|
|
168
171
|
return `${elapsedSeconds.toFixed(2)} 秒`;
|
|
169
172
|
}
|
|
170
173
|
};
|
|
171
|
-
|
|
172
|
-
// ========================================
|
|
173
|
-
// 表定义工具
|
|
174
|
-
// ========================================
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* 解析字段规则字符串
|
|
178
|
-
* 格式:"字段名|类型|最小值|最大值|默认值|必填|正则"
|
|
179
|
-
* 注意:只分割前6个|,第7个|之后的所有内容(包括|)都属于正则表达式
|
|
180
|
-
*/
|
|
181
|
-
export const parseRule = (rule: string): ParsedFieldRule => {
|
|
182
|
-
const parts: string[] = [];
|
|
183
|
-
let currentPart = '';
|
|
184
|
-
let pipeCount = 0;
|
|
185
|
-
|
|
186
|
-
for (let i = 0; i < rule.length; i++) {
|
|
187
|
-
if (rule[i] === '|' && pipeCount < 6) {
|
|
188
|
-
parts.push(currentPart);
|
|
189
|
-
currentPart = '';
|
|
190
|
-
pipeCount++;
|
|
191
|
-
} else {
|
|
192
|
-
currentPart += rule[i];
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
parts.push(currentPart);
|
|
196
|
-
|
|
197
|
-
const [fieldName = '', fieldType = 'string', fieldMinStr = 'null', fieldMaxStr = 'null', fieldDefaultStr = 'null', fieldIndexStr = '0', fieldRegx = 'null'] = parts;
|
|
198
|
-
|
|
199
|
-
const fieldIndex = Number(fieldIndexStr) as 0 | 1;
|
|
200
|
-
const fieldMin = fieldMinStr !== 'null' ? Number(fieldMinStr) : null;
|
|
201
|
-
const fieldMax = fieldMaxStr !== 'null' ? Number(fieldMaxStr) : null;
|
|
202
|
-
|
|
203
|
-
let fieldDefault: any = fieldDefaultStr;
|
|
204
|
-
if (fieldType === 'number' && fieldDefaultStr !== 'null') {
|
|
205
|
-
fieldDefault = Number(fieldDefaultStr);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
name: fieldName,
|
|
210
|
-
type: fieldType as 'string' | 'number' | 'text' | 'array_string' | 'array_text',
|
|
211
|
-
min: fieldMin,
|
|
212
|
-
max: fieldMax,
|
|
213
|
-
default: fieldDefault,
|
|
214
|
-
index: fieldIndex,
|
|
215
|
-
regex: fieldRegx !== 'null' ? fieldRegx : null
|
|
216
|
-
};
|
|
217
|
-
};
|