befly 3.8.2 → 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 +165 -92
- package/env.ts +3 -3
- package/lib/cacheHelper.ts +81 -0
- package/loader/loadApis.ts +172 -0
- package/{lifecycle → loader}/loadPlugins.ts +10 -10
- package/main.ts +108 -12
- package/menu.json +5 -5
- package/package.json +4 -4
- package/paths.ts +7 -0
- package/util.ts +85 -1
- package/lib/addon.ts +0 -77
- package/lifecycle/bootstrap.ts +0 -49
- package/lifecycle/checker.ts +0 -122
- package/lifecycle/lifecycle.ts +0 -61
- package/lifecycle/loadApis.ts +0 -164
- package/plugins/cache.ts +0 -22
- package/plugins/db.ts +0 -59
- package/plugins/logger.ts +0 -27
- package/plugins/redis.ts +0 -41
package/check.ts
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
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 { projectTableDir } from './paths.js';
|
|
9
|
-
import {
|
|
11
|
+
import { projectTableDir, projectApiDir } from './paths.js';
|
|
12
|
+
import { scanAddons, getAddonDir, addonDirExists } from './util.js';
|
|
10
13
|
import type { FieldDefinition } from './types/common.d.ts';
|
|
11
14
|
|
|
12
15
|
/**
|
|
@@ -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,60 +70,58 @@ 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
116
|
for (const [colKey, fieldDef] of Object.entries(table)) {
|
|
116
117
|
if (typeof fieldDef !== 'object' || fieldDef === null || Array.isArray(fieldDef)) {
|
|
117
|
-
Logger.warn(`${
|
|
118
|
-
fileValid = false;
|
|
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
127
|
// 直接使用字段对象
|
|
@@ -134,81 +129,66 @@ export const checkDefault = async function (): Promise<void> {
|
|
|
134
129
|
|
|
135
130
|
// 检查必填字段:name, type, min, max
|
|
136
131
|
if (!field.name || typeof field.name !== 'string') {
|
|
137
|
-
Logger.warn(`${
|
|
138
|
-
fileValid = false;
|
|
132
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
|
|
139
133
|
continue;
|
|
140
134
|
}
|
|
141
135
|
if (!field.type || typeof field.type !== 'string') {
|
|
142
|
-
Logger.warn(`${
|
|
143
|
-
fileValid = false;
|
|
136
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
|
|
144
137
|
continue;
|
|
145
138
|
}
|
|
146
139
|
if (field.min === undefined) {
|
|
147
|
-
Logger.warn(`${
|
|
148
|
-
fileValid = false;
|
|
140
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 min`);
|
|
149
141
|
continue;
|
|
150
142
|
}
|
|
151
143
|
if (field.max === undefined) {
|
|
152
|
-
Logger.warn(`${
|
|
153
|
-
fileValid = false;
|
|
144
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 max`);
|
|
154
145
|
continue;
|
|
155
146
|
}
|
|
156
147
|
|
|
157
148
|
// 检查可选字段的类型
|
|
158
149
|
if (field.detail !== undefined && typeof field.detail !== 'string') {
|
|
159
|
-
Logger.warn(`${
|
|
160
|
-
fileValid = false;
|
|
150
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
|
|
161
151
|
}
|
|
162
152
|
if (field.index !== undefined && typeof field.index !== 'boolean') {
|
|
163
|
-
Logger.warn(`${
|
|
164
|
-
fileValid = false;
|
|
153
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
|
|
165
154
|
}
|
|
166
155
|
if (field.unique !== undefined && typeof field.unique !== 'boolean') {
|
|
167
|
-
Logger.warn(`${
|
|
168
|
-
fileValid = false;
|
|
156
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
|
|
169
157
|
}
|
|
170
158
|
if (field.nullable !== undefined && typeof field.nullable !== 'boolean') {
|
|
171
|
-
Logger.warn(`${
|
|
172
|
-
fileValid = false;
|
|
159
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
|
|
173
160
|
}
|
|
174
161
|
if (field.unsigned !== undefined && typeof field.unsigned !== 'boolean') {
|
|
175
|
-
Logger.warn(`${
|
|
176
|
-
fileValid = false;
|
|
162
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
|
|
177
163
|
}
|
|
178
164
|
if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== 'string') {
|
|
179
|
-
Logger.warn(`${
|
|
180
|
-
fileValid = false;
|
|
165
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
|
|
181
166
|
}
|
|
182
167
|
|
|
183
168
|
const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault, index: fieldIndex, regexp: fieldRegexp } = field;
|
|
184
169
|
|
|
185
170
|
// 第1个值:名称必须为中文、数字、字母、下划线、短横线、空格
|
|
186
171
|
if (!FIELD_NAME_REGEX.test(fieldName)) {
|
|
187
|
-
Logger.warn(`${
|
|
188
|
-
fileValid = false;
|
|
172
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
|
|
189
173
|
}
|
|
190
174
|
|
|
191
175
|
// 第2个值:字段类型必须为string,number,text,array_string,array_text之一
|
|
192
176
|
if (!FIELD_TYPES.includes(fieldType as any)) {
|
|
193
|
-
Logger.warn(`${
|
|
194
|
-
fileValid = false;
|
|
177
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
|
|
195
178
|
}
|
|
196
179
|
|
|
197
180
|
// 第3/4个值:需要是 null 或 数字
|
|
198
181
|
if (!(fieldMin === null || typeof fieldMin === 'number')) {
|
|
199
|
-
Logger.warn(`${
|
|
200
|
-
fileValid = false;
|
|
182
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
|
|
201
183
|
}
|
|
202
184
|
if (!(fieldMax === null || typeof fieldMax === 'number')) {
|
|
203
|
-
Logger.warn(`${
|
|
204
|
-
fileValid = false;
|
|
185
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
|
|
205
186
|
}
|
|
206
187
|
|
|
207
188
|
// 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
|
|
208
189
|
if (fieldMin !== null && fieldMax !== null) {
|
|
209
190
|
if (fieldMin > fieldMax) {
|
|
210
|
-
Logger.warn(`${
|
|
211
|
-
fileValid = false;
|
|
191
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
|
|
212
192
|
}
|
|
213
193
|
}
|
|
214
194
|
|
|
@@ -216,57 +196,150 @@ export const checkDefault = async function (): Promise<void> {
|
|
|
216
196
|
if (fieldType === 'text') {
|
|
217
197
|
// text:min/max 必须为 null,默认值必须为 null
|
|
218
198
|
if (fieldMin !== null) {
|
|
219
|
-
Logger.warn(`${
|
|
220
|
-
fileValid = false;
|
|
199
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
|
|
221
200
|
}
|
|
222
201
|
if (fieldMax !== null) {
|
|
223
|
-
Logger.warn(`${
|
|
224
|
-
fileValid = false;
|
|
202
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
|
|
225
203
|
}
|
|
226
204
|
if (fieldDefault !== null) {
|
|
227
|
-
Logger.warn(`${
|
|
228
|
-
fileValid = false;
|
|
205
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
|
|
229
206
|
}
|
|
230
207
|
} else if (fieldType === 'string' || fieldType === 'array') {
|
|
231
208
|
if (fieldMax === null || typeof fieldMax !== 'number') {
|
|
232
|
-
Logger.warn(`${
|
|
233
|
-
fileValid = false;
|
|
209
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
|
|
234
210
|
} else if (fieldMax > MAX_VARCHAR_LENGTH) {
|
|
235
|
-
Logger.warn(`${
|
|
236
|
-
fileValid = false;
|
|
211
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
237
212
|
}
|
|
238
213
|
} else if (fieldType === 'number') {
|
|
239
214
|
// number 类型:default 如果存在,必须为 null 或 number
|
|
240
215
|
if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== 'number') {
|
|
241
|
-
Logger.warn(`${
|
|
242
|
-
fileValid = false;
|
|
216
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
|
|
243
217
|
}
|
|
244
218
|
}
|
|
245
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
|
+
}
|
|
246
273
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
}
|
|
297
|
+
|
|
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 属性必须是字符串数组`);
|
|
252
313
|
}
|
|
253
314
|
} catch (error: any) {
|
|
254
|
-
Logger.error(
|
|
255
|
-
invalidFiles++;
|
|
315
|
+
Logger.error(`[${item.displayName}] 接口 ${apiPath} 解析失败`, error);
|
|
256
316
|
}
|
|
257
317
|
}
|
|
258
318
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
319
|
+
return true;
|
|
320
|
+
} catch (error: any) {
|
|
321
|
+
Logger.error('API 定义检查过程中出错', error);
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
264
325
|
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
}
|
|
267
338
|
}
|
|
339
|
+
|
|
340
|
+
return true;
|
|
268
341
|
} catch (error: any) {
|
|
269
|
-
Logger.error('
|
|
270
|
-
|
|
342
|
+
Logger.error('项目结构检查过程中出错', error);
|
|
343
|
+
return false;
|
|
271
344
|
}
|
|
272
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
|
|
package/lib/cacheHelper.ts
CHANGED
|
@@ -166,4 +166,85 @@ export class CacheHelper {
|
|
|
166
166
|
|
|
167
167
|
Logger.info('========== 数据缓存完成 ==========\n');
|
|
168
168
|
}
|
|
169
|
+
|
|
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
|
+
}
|
|
197
|
+
|
|
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> {
|
|
221
|
+
try {
|
|
222
|
+
const redisKey = `role:apis:${roleCode}`;
|
|
223
|
+
const result = await this.befly.redis.sismember(redisKey, apiPath);
|
|
224
|
+
return result === 1;
|
|
225
|
+
} catch (error: any) {
|
|
226
|
+
Logger.error(`检查角色 ${roleCode} 权限失败:`, error);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
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
|
+
}
|
|
169
250
|
}
|