befly 3.8.1 → 3.8.3
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 +201 -95
- package/env.ts +3 -3
- package/{plugins/cache.ts → lib/cacheHelper.ts} +99 -35
- package/lib/redisHelper.ts +49 -65
- package/lib/validator.ts +29 -32
- package/loader/loadApis.ts +172 -0
- package/loader/loadPlugins.ts +244 -0
- package/main.ts +116 -23
- package/menu.json +14 -14
- package/package.json +4 -4
- package/paths.ts +7 -0
- package/types/common.d.ts +28 -7
- package/util.ts +76 -36
- package/lib/addon.ts +0 -77
- package/lifecycle/bootstrap.ts +0 -63
- package/lifecycle/checker.ts +0 -122
- package/lifecycle/lifecycle.ts +0 -104
- package/lifecycle/loader.ts +0 -427
- package/plugins/db.ts +0 -59
- package/plugins/logger.ts +0 -27
- package/plugins/redis.ts +0 -41
package/check.ts
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
* 验证表定义文件的格式和规则
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { basename } from 'pathe';
|
|
6
|
+
import { basename, relative } from 'pathe';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { isPlainObject } from 'es-toolkit/compat';
|
|
7
10
|
import { Logger } from './lib/logger.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
+
import { projectTableDir, projectApiDir } from './paths.js';
|
|
12
|
+
import { scanAddons, getAddonDir, addonDirExists } from './util.js';
|
|
13
|
+
import type { FieldDefinition } from './types/common.d.ts';
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* 表文件信息接口
|
|
@@ -54,16 +57,10 @@ const MAX_VARCHAR_LENGTH = 65535;
|
|
|
54
57
|
* 检查表定义文件
|
|
55
58
|
* @throws 当检查失败时抛出异常
|
|
56
59
|
*/
|
|
57
|
-
export const
|
|
60
|
+
export const checkTable = async function (): Promise<boolean> {
|
|
58
61
|
try {
|
|
59
62
|
const tablesGlob = new Bun.Glob('*.json');
|
|
60
63
|
|
|
61
|
-
// 统计信息
|
|
62
|
-
let totalFiles = 0;
|
|
63
|
-
let totalRules = 0;
|
|
64
|
-
let validFiles = 0;
|
|
65
|
-
let invalidFiles = 0;
|
|
66
|
-
|
|
67
64
|
// 收集所有表文件
|
|
68
65
|
const allTableFiles: TableFileInfo[] = [];
|
|
69
66
|
|
|
@@ -73,167 +70,276 @@ export const checkDefault = async function (): Promise<void> {
|
|
|
73
70
|
absolute: true,
|
|
74
71
|
onlyFiles: true
|
|
75
72
|
})) {
|
|
76
|
-
allTableFiles.push({
|
|
73
|
+
allTableFiles.push({
|
|
74
|
+
file: file,
|
|
75
|
+
typeCode: 'project',
|
|
76
|
+
typeName: '项目'
|
|
77
|
+
});
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
// 收集 addon 表字段定义文件
|
|
80
|
-
const addons =
|
|
81
|
+
const addons = scanAddons();
|
|
81
82
|
for (const addonName of addons) {
|
|
82
|
-
const addonTablesDir =
|
|
83
|
+
const addonTablesDir = getAddonDir(addonName, 'tables');
|
|
83
84
|
|
|
84
85
|
for await (const file of tablesGlob.scan({
|
|
85
86
|
cwd: addonTablesDir,
|
|
86
87
|
absolute: true,
|
|
87
88
|
onlyFiles: true
|
|
88
89
|
})) {
|
|
89
|
-
allTableFiles.push({
|
|
90
|
+
allTableFiles.push({
|
|
91
|
+
file: file,
|
|
92
|
+
typeCode: 'addon',
|
|
93
|
+
typeName: `组件${addonName}`,
|
|
94
|
+
addonName: addonName
|
|
95
|
+
});
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
// 合并进行验证逻辑
|
|
94
|
-
for (const
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const fileBaseName = basename(file, '.json');
|
|
98
|
-
const fileType = type === 'project' ? '项目' : `组件${addonName}`;
|
|
100
|
+
for (const item of allTableFiles) {
|
|
101
|
+
const fileName = basename(item.file);
|
|
102
|
+
const fileBaseName = basename(item.file, '.json');
|
|
99
103
|
|
|
100
104
|
try {
|
|
101
105
|
// 1) 文件名小驼峰校验
|
|
102
106
|
if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
|
|
103
|
-
Logger.warn(`${
|
|
104
|
-
// 命名不合规,记录错误并计为无效文件,继续下一个文件
|
|
105
|
-
invalidFiles++;
|
|
107
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
|
|
106
108
|
continue;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
let fileRules = 0;
|
|
111
|
+
// 动态导入 JSON 文件
|
|
112
|
+
const tableModule = await import(item.file, { with: { type: 'json' } });
|
|
113
|
+
const table = tableModule.default;
|
|
113
114
|
|
|
114
115
|
// 检查 table 中的每个验证规则
|
|
115
|
-
for (const [colKey,
|
|
116
|
-
if (typeof
|
|
117
|
-
Logger.warn(`${
|
|
118
|
-
fileValid = false;
|
|
116
|
+
for (const [colKey, fieldDef] of Object.entries(table)) {
|
|
117
|
+
if (typeof fieldDef !== 'object' || fieldDef === null || Array.isArray(fieldDef)) {
|
|
118
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 规则必须为对象`);
|
|
119
119
|
continue;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// 验证规则格式
|
|
123
|
-
fileRules++;
|
|
124
|
-
totalRules++;
|
|
125
|
-
|
|
126
122
|
// 检查是否使用了保留字段
|
|
127
123
|
if (RESERVED_FIELDS.includes(colKey as any)) {
|
|
128
|
-
Logger.warn(`${
|
|
129
|
-
fileValid = false;
|
|
124
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
|
|
130
125
|
}
|
|
131
126
|
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
Logger.warn(`${
|
|
138
|
-
|
|
127
|
+
// 直接使用字段对象
|
|
128
|
+
const field = fieldDef as FieldDefinition;
|
|
129
|
+
|
|
130
|
+
// 检查必填字段:name, type, min, max
|
|
131
|
+
if (!field.name || typeof field.name !== 'string') {
|
|
132
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!field.type || typeof field.type !== 'string') {
|
|
136
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (field.min === undefined) {
|
|
140
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 min`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (field.max === undefined) {
|
|
144
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 max`);
|
|
139
145
|
continue;
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
|
|
148
|
+
// 检查可选字段的类型
|
|
149
|
+
if (field.detail !== undefined && typeof field.detail !== 'string') {
|
|
150
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
|
|
151
|
+
}
|
|
152
|
+
if (field.index !== undefined && typeof field.index !== 'boolean') {
|
|
153
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
|
|
154
|
+
}
|
|
155
|
+
if (field.unique !== undefined && typeof field.unique !== 'boolean') {
|
|
156
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
|
|
157
|
+
}
|
|
158
|
+
if (field.nullable !== undefined && typeof field.nullable !== 'boolean') {
|
|
159
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
|
|
160
|
+
}
|
|
161
|
+
if (field.unsigned !== undefined && typeof field.unsigned !== 'boolean') {
|
|
162
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
|
|
163
|
+
}
|
|
164
|
+
if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== 'string') {
|
|
165
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault, index: fieldIndex, regexp: fieldRegexp } = field;
|
|
143
169
|
|
|
144
170
|
// 第1个值:名称必须为中文、数字、字母、下划线、短横线、空格
|
|
145
171
|
if (!FIELD_NAME_REGEX.test(fieldName)) {
|
|
146
|
-
Logger.warn(`${
|
|
147
|
-
fileValid = false;
|
|
172
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
|
|
148
173
|
}
|
|
149
174
|
|
|
150
175
|
// 第2个值:字段类型必须为string,number,text,array_string,array_text之一
|
|
151
176
|
if (!FIELD_TYPES.includes(fieldType as any)) {
|
|
152
|
-
Logger.warn(`${
|
|
153
|
-
fileValid = false;
|
|
177
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
|
|
154
178
|
}
|
|
155
179
|
|
|
156
180
|
// 第3/4个值:需要是 null 或 数字
|
|
157
181
|
if (!(fieldMin === null || typeof fieldMin === 'number')) {
|
|
158
|
-
Logger.warn(`${
|
|
159
|
-
fileValid = false;
|
|
182
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
|
|
160
183
|
}
|
|
161
184
|
if (!(fieldMax === null || typeof fieldMax === 'number')) {
|
|
162
|
-
Logger.warn(`${
|
|
163
|
-
fileValid = false;
|
|
185
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
// 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
|
|
167
189
|
if (fieldMin !== null && fieldMax !== null) {
|
|
168
190
|
if (fieldMin > fieldMax) {
|
|
169
|
-
Logger.warn(`${
|
|
170
|
-
fileValid = false;
|
|
191
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
|
|
171
192
|
}
|
|
172
193
|
}
|
|
173
194
|
|
|
174
|
-
// 第6个值:是否创建索引必须为0或1
|
|
175
|
-
if (fieldIndex !== 0 && fieldIndex !== 1) {
|
|
176
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 索引标识 "${fieldIndex}" 格式错误,必须为0或1`);
|
|
177
|
-
fileValid = false;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// 第7个值:必须为null或正则表达式(parseRule已经验证过了)
|
|
181
|
-
// parseRule 已经将正则字符串转换为 RegExp 或 null,这里不需要再验证
|
|
182
|
-
|
|
183
195
|
// 第4个值与类型联动校验 + 默认值规则
|
|
184
196
|
if (fieldType === 'text') {
|
|
185
|
-
// text:min/max 必须为 null,默认值必须为
|
|
197
|
+
// text:min/max 必须为 null,默认值必须为 null
|
|
186
198
|
if (fieldMin !== null) {
|
|
187
|
-
Logger.warn(`${
|
|
188
|
-
fileValid = false;
|
|
199
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
|
|
189
200
|
}
|
|
190
201
|
if (fieldMax !== null) {
|
|
191
|
-
Logger.warn(`${
|
|
192
|
-
fileValid = false;
|
|
202
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
|
|
193
203
|
}
|
|
194
|
-
if (fieldDefault !==
|
|
195
|
-
Logger.warn(`${
|
|
196
|
-
fileValid = false;
|
|
204
|
+
if (fieldDefault !== null) {
|
|
205
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
|
|
197
206
|
}
|
|
198
207
|
} else if (fieldType === 'string' || fieldType === 'array') {
|
|
199
208
|
if (fieldMax === null || typeof fieldMax !== 'number') {
|
|
200
|
-
Logger.warn(`${
|
|
201
|
-
fileValid = false;
|
|
209
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
|
|
202
210
|
} else if (fieldMax > MAX_VARCHAR_LENGTH) {
|
|
203
|
-
Logger.warn(`${
|
|
204
|
-
fileValid = false;
|
|
211
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
205
212
|
}
|
|
206
213
|
} else if (fieldType === 'number') {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
// number 类型:default 如果存在,必须为 null 或 number
|
|
215
|
+
if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== 'number') {
|
|
216
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
|
|
210
217
|
}
|
|
211
218
|
}
|
|
212
219
|
}
|
|
220
|
+
} catch (error: any) {
|
|
221
|
+
Logger.error(`${item.typeName}表 ${fileName} 解析失败`, error);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return true;
|
|
226
|
+
} catch (error: any) {
|
|
227
|
+
Logger.error('数据表定义检查过程中出错', error);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 检查所有 API 定义
|
|
234
|
+
*/
|
|
235
|
+
export const checkApi = async function (): Promise<boolean> {
|
|
236
|
+
try {
|
|
237
|
+
const apiGlob = new Bun.Glob('**/*.ts');
|
|
238
|
+
|
|
239
|
+
// 收集所有 API 文件
|
|
240
|
+
const allApiFiles: Array<{ file: string; displayName: string }> = [];
|
|
241
|
+
|
|
242
|
+
// 收集项目 API 文件
|
|
243
|
+
if (existsSync(projectApiDir)) {
|
|
244
|
+
for await (const file of apiGlob.scan({
|
|
245
|
+
cwd: projectApiDir,
|
|
246
|
+
onlyFiles: true,
|
|
247
|
+
absolute: true
|
|
248
|
+
})) {
|
|
249
|
+
allApiFiles.push({
|
|
250
|
+
file: file,
|
|
251
|
+
displayName: '用户'
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 收集组件 API 文件
|
|
257
|
+
const addons = scanAddons();
|
|
258
|
+
for (const addon of addons) {
|
|
259
|
+
if (!addonDirExists(addon, 'apis')) continue;
|
|
260
|
+
const addonApiDir = getAddonDir(addon, 'apis');
|
|
261
|
+
|
|
262
|
+
for await (const file of apiGlob.scan({
|
|
263
|
+
cwd: addonApiDir,
|
|
264
|
+
onlyFiles: true,
|
|
265
|
+
absolute: true
|
|
266
|
+
})) {
|
|
267
|
+
allApiFiles.push({
|
|
268
|
+
file: file,
|
|
269
|
+
displayName: `组件${addon}`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 合并进行验证逻辑
|
|
275
|
+
for (const item of allApiFiles) {
|
|
276
|
+
const fileName = basename(item.file).replace(/\.ts$/, '');
|
|
277
|
+
const apiPath = relative(item.displayName === '用户' ? projectApiDir : getAddonDir(item.displayName.replace('组件', ''), 'apis'), item.file).replace(/\.ts$/, '');
|
|
278
|
+
|
|
279
|
+
// 跳过以下划线开头的文件
|
|
280
|
+
if (apiPath.indexOf('_') !== -1) continue;
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Windows 下路径需要转换为正斜杠格式
|
|
284
|
+
const filePath = item.file.replace(/\\/g, '/');
|
|
285
|
+
const apiImport = await import(filePath);
|
|
286
|
+
const api = apiImport.default;
|
|
287
|
+
|
|
288
|
+
// 验证必填属性:name 和 handler
|
|
289
|
+
if (typeof api.name !== 'string' || api.name.trim() === '') {
|
|
290
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 name 属性必须是非空字符串`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (typeof api.handler !== 'function') {
|
|
294
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 handler 属性必须是函数`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
213
297
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
298
|
+
// 验证可选属性的类型(如果提供了)
|
|
299
|
+
if (api.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(api.method.toUpperCase())) {
|
|
300
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 method 属性必须是有效的 HTTP 方法`);
|
|
301
|
+
}
|
|
302
|
+
if (api.auth !== undefined && typeof api.auth !== 'boolean') {
|
|
303
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 auth 属性必须是布尔值 (true=需登录, false=公开)`);
|
|
304
|
+
}
|
|
305
|
+
if (api.fields && !isPlainObject(api.fields)) {
|
|
306
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 fields 属性必须是对象`);
|
|
307
|
+
}
|
|
308
|
+
if (api.required && !Array.isArray(api.required)) {
|
|
309
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是数组`);
|
|
310
|
+
}
|
|
311
|
+
if (api.required && api.required.some((item: any) => typeof item !== 'string')) {
|
|
312
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是字符串数组`);
|
|
219
313
|
}
|
|
220
314
|
} catch (error: any) {
|
|
221
|
-
Logger.error(
|
|
222
|
-
invalidFiles++;
|
|
315
|
+
Logger.error(`[${item.displayName}] 接口 ${apiPath} 解析失败`, error);
|
|
223
316
|
}
|
|
224
317
|
}
|
|
225
318
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
319
|
+
return true;
|
|
320
|
+
} catch (error: any) {
|
|
321
|
+
Logger.error('API 定义检查过程中出错', error);
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
231
325
|
|
|
232
|
-
|
|
233
|
-
|
|
326
|
+
/**
|
|
327
|
+
* 检查项目结构
|
|
328
|
+
*/
|
|
329
|
+
export const checkApp = async function (): Promise<boolean> {
|
|
330
|
+
try {
|
|
331
|
+
// 检查项目 apis 目录下是否存在名为 addon 的目录
|
|
332
|
+
if (existsSync(projectApiDir)) {
|
|
333
|
+
const addonDir = join(projectApiDir, 'addon');
|
|
334
|
+
if (existsSync(addonDir)) {
|
|
335
|
+
Logger.error('项目 apis 目录下不能存在名为 addon 的目录,addon 是保留名称,用于组件接口路由');
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
234
338
|
}
|
|
339
|
+
|
|
340
|
+
return true;
|
|
235
341
|
} catch (error: any) {
|
|
236
|
-
Logger.error('
|
|
237
|
-
|
|
342
|
+
Logger.error('项目结构检查过程中出错', error);
|
|
343
|
+
return false;
|
|
238
344
|
}
|
|
239
345
|
};
|
package/env.ts
CHANGED
|
@@ -18,14 +18,14 @@ const coreEnv: EnvConfig = {
|
|
|
18
18
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
19
19
|
APP_NAME: isProd ? '野蜂飞舞正式环境' : '野蜂飞舞开发环境',
|
|
20
20
|
APP_PORT: 3000,
|
|
21
|
-
APP_HOST:
|
|
21
|
+
APP_HOST: '127.0.0.1',
|
|
22
22
|
DEV_EMAIL: 'dev@qq.com',
|
|
23
23
|
DEV_PASSWORD: '123456',
|
|
24
24
|
BODY_LIMIT: 10485760, // 10MB
|
|
25
25
|
PARAMS_CHECK: false,
|
|
26
26
|
|
|
27
27
|
// ========== 日志配置 ==========
|
|
28
|
-
LOG_DEBUG:
|
|
28
|
+
LOG_DEBUG: 0,
|
|
29
29
|
LOG_EXCLUDE_FIELDS: 'password,token,secret',
|
|
30
30
|
LOG_DIR: './logs',
|
|
31
31
|
LOG_TO_CONSOLE: 1,
|
|
@@ -55,7 +55,7 @@ const coreEnv: EnvConfig = {
|
|
|
55
55
|
REDIS_KEY_PREFIX: 'befly',
|
|
56
56
|
|
|
57
57
|
// ========== JWT 配置 ==========
|
|
58
|
-
JWT_SECRET:
|
|
58
|
+
JWT_SECRET: 'befly-secret',
|
|
59
59
|
JWT_EXPIRES_IN: '7d',
|
|
60
60
|
JWT_ALGORITHM: 'HS256',
|
|
61
61
|
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* 缓存助手 - TypeScript 版本
|
|
3
3
|
* 负责在服务器启动时缓存接口、菜单和角色权限到 Redis
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Logger } from '
|
|
7
|
-
import type { Plugin } from '../types/plugin.js';
|
|
6
|
+
import { Logger } from './logger.js';
|
|
8
7
|
import type { BeflyContext } from '../types/befly.js';
|
|
9
|
-
import type { ApiRoute } from '../types/api.js';
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
|
-
*
|
|
10
|
+
* 缓存助手类
|
|
13
11
|
*/
|
|
14
|
-
class
|
|
15
|
-
private
|
|
12
|
+
export class CacheHelper {
|
|
13
|
+
private befly: BeflyContext;
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
/**
|
|
16
|
+
* 构造函数
|
|
17
|
+
* @param befly - Befly 上下文
|
|
18
|
+
*/
|
|
19
|
+
constructor(befly: BeflyContext) {
|
|
20
|
+
this.befly = befly;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -24,21 +26,21 @@ class CacheManager {
|
|
|
24
26
|
async cacheApis(): Promise<void> {
|
|
25
27
|
try {
|
|
26
28
|
// 检查表是否存在
|
|
27
|
-
const tableExists = await this.
|
|
29
|
+
const tableExists = await this.befly.db.tableExists('core_api');
|
|
28
30
|
if (!tableExists) {
|
|
29
31
|
Logger.warn('⚠️ 接口表不存在,跳过接口缓存');
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
// 从数据库查询所有接口(与 apiAll.ts 保持一致)
|
|
34
|
-
const apiList = await this.
|
|
36
|
+
const apiList = await this.befly.db.getAll({
|
|
35
37
|
table: 'core_api',
|
|
36
38
|
fields: ['id', 'name', 'path', 'method', 'description', 'addonName', 'addonTitle'],
|
|
37
39
|
orderBy: ['addonName#ASC', 'path#ASC']
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
// 缓存到 Redis
|
|
41
|
-
const result = await this.
|
|
43
|
+
const result = await this.befly.redis.setObject('apis:all', apiList);
|
|
42
44
|
|
|
43
45
|
if (result === null) {
|
|
44
46
|
Logger.warn('⚠️ 接口缓存失败');
|
|
@@ -56,21 +58,21 @@ class CacheManager {
|
|
|
56
58
|
async cacheMenus(): Promise<void> {
|
|
57
59
|
try {
|
|
58
60
|
// 检查表是否存在
|
|
59
|
-
const tableExists = await this.
|
|
61
|
+
const tableExists = await this.befly.db.tableExists('core_menu');
|
|
60
62
|
if (!tableExists) {
|
|
61
63
|
Logger.warn('⚠️ 菜单表不存在,跳过菜单缓存');
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// 从数据库查询所有菜单
|
|
66
|
-
const menus = await this.
|
|
68
|
+
const menus = await this.befly.db.getAll({
|
|
67
69
|
table: 'core_menu',
|
|
68
70
|
fields: ['id', 'pid', 'name', 'path', 'icon', 'type', 'sort'],
|
|
69
71
|
orderBy: ['sort#ASC', 'id#ASC']
|
|
70
72
|
});
|
|
71
73
|
|
|
72
74
|
// 缓存到 Redis
|
|
73
|
-
const result = await this.
|
|
75
|
+
const result = await this.befly.redis.setObject('menus:all', menus);
|
|
74
76
|
|
|
75
77
|
if (result === null) {
|
|
76
78
|
Logger.warn('⚠️ 菜单缓存失败');
|
|
@@ -89,8 +91,8 @@ class CacheManager {
|
|
|
89
91
|
async cacheRolePermissions(): Promise<void> {
|
|
90
92
|
try {
|
|
91
93
|
// 检查表是否存在
|
|
92
|
-
const apiTableExists = await this.
|
|
93
|
-
const roleTableExists = await this.
|
|
94
|
+
const apiTableExists = await this.befly.db.tableExists('core_api');
|
|
95
|
+
const roleTableExists = await this.befly.db.tableExists('core_role');
|
|
94
96
|
|
|
95
97
|
if (!apiTableExists || !roleTableExists) {
|
|
96
98
|
Logger.warn('⚠️ 接口或角色表不存在,跳过角色权限缓存');
|
|
@@ -98,13 +100,13 @@ class CacheManager {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
// 查询所有角色
|
|
101
|
-
const roles = await this.
|
|
103
|
+
const roles = await this.befly.db.getAll({
|
|
102
104
|
table: 'core_role',
|
|
103
105
|
fields: ['id', 'code', 'apis']
|
|
104
106
|
});
|
|
105
107
|
|
|
106
108
|
// 查询所有接口(用于权限映射)
|
|
107
|
-
const allApis = await this.
|
|
109
|
+
const allApis = await this.befly.db.getAll({
|
|
108
110
|
table: 'core_api',
|
|
109
111
|
fields: ['id', 'name', 'path', 'method', 'description', 'addonName']
|
|
110
112
|
});
|
|
@@ -129,10 +131,10 @@ class CacheManager {
|
|
|
129
131
|
const redisKey = `role:apis:${role.code}`;
|
|
130
132
|
|
|
131
133
|
// 先删除旧数据
|
|
132
|
-
await this.
|
|
134
|
+
await this.befly.redis.del(redisKey);
|
|
133
135
|
|
|
134
136
|
// 批量添加到 Set
|
|
135
|
-
const result = await this.
|
|
137
|
+
const result = await this.befly.redis.sadd(redisKey, roleApiPaths);
|
|
136
138
|
|
|
137
139
|
if (result > 0) {
|
|
138
140
|
cachedRoles++;
|
|
@@ -164,23 +166,85 @@ class CacheManager {
|
|
|
164
166
|
|
|
165
167
|
Logger.info('========== 数据缓存完成 ==========\n');
|
|
166
168
|
}
|
|
167
|
-
}
|
|
168
169
|
|
|
169
|
-
/**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
/**
|
|
171
|
+
* 获取缓存的所有接口
|
|
172
|
+
* @returns 接口列表
|
|
173
|
+
*/
|
|
174
|
+
async getApis(): Promise<any[]> {
|
|
175
|
+
try {
|
|
176
|
+
const apis = await this.befly.redis.getObject<any[]>('apis:all');
|
|
177
|
+
return apis || [];
|
|
178
|
+
} catch (error: any) {
|
|
179
|
+
Logger.error('获取接口缓存失败:', error);
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 获取缓存的所有菜单
|
|
186
|
+
* @returns 菜单列表
|
|
187
|
+
*/
|
|
188
|
+
async getMenus(): Promise<any[]> {
|
|
189
|
+
try {
|
|
190
|
+
const menus = await this.befly.redis.getObject<any[]>('menus:all');
|
|
191
|
+
return menus || [];
|
|
192
|
+
} catch (error: any) {
|
|
193
|
+
Logger.error('获取菜单缓存失败:', error);
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
175
197
|
|
|
176
|
-
|
|
198
|
+
/**
|
|
199
|
+
* 获取角色的接口权限
|
|
200
|
+
* @param roleCode - 角色代码
|
|
201
|
+
* @returns 接口路径列表
|
|
202
|
+
*/
|
|
203
|
+
async getRolePermissions(roleCode: string): Promise<string[]> {
|
|
204
|
+
try {
|
|
205
|
+
const redisKey = `role:apis:${roleCode}`;
|
|
206
|
+
const permissions = await this.befly.redis.smembers(redisKey);
|
|
207
|
+
return permissions || [];
|
|
208
|
+
} catch (error: any) {
|
|
209
|
+
Logger.error(`获取角色 ${roleCode} 权限缓存失败:`, error);
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 检查角色是否有指定接口权限
|
|
216
|
+
* @param roleCode - 角色代码
|
|
217
|
+
* @param apiPath - 接口路径(格式:METHOD/path)
|
|
218
|
+
* @returns 是否有权限
|
|
219
|
+
*/
|
|
220
|
+
async checkRolePermission(roleCode: string, apiPath: string): Promise<boolean> {
|
|
177
221
|
try {
|
|
178
|
-
const
|
|
179
|
-
|
|
222
|
+
const redisKey = `role:apis:${roleCode}`;
|
|
223
|
+
const result = await this.befly.redis.sismember(redisKey, apiPath);
|
|
224
|
+
return result === 1;
|
|
180
225
|
} catch (error: any) {
|
|
181
|
-
|
|
226
|
+
Logger.error(`检查角色 ${roleCode} 权限失败:`, error);
|
|
227
|
+
return false;
|
|
182
228
|
}
|
|
183
229
|
}
|
|
184
|
-
};
|
|
185
230
|
|
|
186
|
-
|
|
231
|
+
/**
|
|
232
|
+
* 删除角色的接口权限缓存
|
|
233
|
+
* @param roleCode - 角色代码
|
|
234
|
+
* @returns 是否删除成功
|
|
235
|
+
*/
|
|
236
|
+
async deleteRolePermissions(roleCode: string): Promise<boolean> {
|
|
237
|
+
try {
|
|
238
|
+
const redisKey = `role:apis:${roleCode}`;
|
|
239
|
+
const result = await this.befly.redis.del(redisKey);
|
|
240
|
+
if (result > 0) {
|
|
241
|
+
Logger.info(`✅ 已删除角色 ${roleCode} 的权限缓存`);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
} catch (error: any) {
|
|
246
|
+
Logger.error(`删除角色 ${roleCode} 权限缓存失败:`, error);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|