befly 2.3.3 → 3.0.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.
- package/apis/health/info.ts +64 -0
- package/apis/tool/tokenCheck.ts +51 -0
- package/bin/befly.ts +202 -0
- package/checks/conflict.ts +408 -0
- package/checks/{table.js → table.ts} +139 -61
- package/config/env.ts +218 -0
- package/config/reserved.ts +96 -0
- package/main.ts +101 -0
- package/package.json +44 -8
- package/plugins/{db.js → db.ts} +24 -11
- package/plugins/logger.ts +28 -0
- package/plugins/redis.ts +51 -0
- package/plugins/tool.ts +34 -0
- package/scripts/syncDb/apply.ts +171 -0
- package/scripts/syncDb/constants.ts +70 -0
- package/scripts/syncDb/ddl.ts +182 -0
- package/scripts/syncDb/helpers.ts +172 -0
- package/scripts/syncDb/index.ts +215 -0
- package/scripts/syncDb/schema.ts +199 -0
- package/scripts/syncDb/sqlite.ts +50 -0
- package/scripts/syncDb/state.ts +104 -0
- package/scripts/syncDb/table.ts +204 -0
- package/scripts/syncDb/tableCreate.ts +142 -0
- package/scripts/syncDb/tests/constants.test.ts +104 -0
- package/scripts/syncDb/tests/ddl.test.ts +134 -0
- package/scripts/syncDb/tests/helpers.test.ts +70 -0
- package/scripts/syncDb/types.ts +92 -0
- package/scripts/syncDb/version.ts +73 -0
- package/scripts/syncDb.ts +9 -0
- package/scripts/{syncDev.js → syncDev.ts} +41 -25
- package/system.ts +149 -0
- package/tables/_common.json +21 -0
- package/tables/admin.json +10 -0
- package/tsconfig.json +58 -0
- package/types/api.d.ts +246 -0
- package/types/befly.d.ts +234 -0
- package/types/common.d.ts +215 -0
- package/types/context.ts +167 -0
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +278 -0
- package/types/index.d.ts +16 -0
- package/types/index.ts +459 -0
- package/types/jwt.d.ts +99 -0
- package/types/logger.d.ts +43 -0
- package/types/plugin.d.ts +109 -0
- package/types/redis.d.ts +44 -0
- package/types/tool.d.ts +67 -0
- package/types/validator.d.ts +45 -0
- package/utils/addonHelper.ts +60 -0
- package/utils/api.ts +23 -0
- package/utils/{colors.js → colors.ts} +79 -21
- package/utils/crypto.ts +308 -0
- package/utils/datetime.ts +51 -0
- package/utils/dbHelper.ts +142 -0
- package/utils/errorHandler.ts +68 -0
- package/utils/index.ts +46 -0
- package/utils/jwt.ts +493 -0
- package/utils/logger.ts +284 -0
- package/utils/objectHelper.ts +68 -0
- package/utils/pluginHelper.ts +62 -0
- package/utils/redisHelper.ts +338 -0
- package/utils/response.ts +38 -0
- package/utils/{sqlBuilder.js → sqlBuilder.ts} +233 -97
- package/utils/sqlHelper.ts +447 -0
- package/utils/tableHelper.ts +167 -0
- package/utils/tool.ts +230 -0
- package/utils/typeHelper.ts +101 -0
- package/utils/validate.ts +451 -0
- package/utils/{xml.js → xml.ts} +100 -74
- package/.npmrc +0 -3
- package/.prettierignore +0 -2
- package/.prettierrc +0 -11
- package/apis/health/info.js +0 -49
- package/apis/tool/tokenCheck.js +0 -29
- package/bin/befly.js +0 -109
- package/config/env.js +0 -64
- package/main.js +0 -579
- package/plugins/logger.js +0 -14
- package/plugins/redis.js +0 -32
- package/plugins/tool.js +0 -8
- package/scripts/syncDb.js +0 -752
- package/system.js +0 -118
- package/tables/common.json +0 -16
- package/tables/tool.json +0 -6
- package/utils/api.js +0 -27
- package/utils/crypto.js +0 -260
- package/utils/index.js +0 -334
- package/utils/jwt.js +0 -387
- package/utils/logger.js +0 -143
- package/utils/redisHelper.js +0 -74
- package/utils/sqlManager.js +0 -471
- package/utils/tool.js +0 -31
- package/utils/validate.js +0 -226
|
@@ -1,11 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表规则检查器 - TypeScript 版本
|
|
3
|
+
* 验证表定义文件的格式和规则
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
import path from 'node:path';
|
|
2
7
|
import { Logger } from '../utils/logger.js';
|
|
3
8
|
import { parseRule } from '../utils/index.js';
|
|
4
9
|
import { __dirtables, getProjectDir } from '../system.js';
|
|
10
|
+
import { scanAddons, getAddonDir } from '../utils/addonHelper.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 表文件信息接口
|
|
14
|
+
*/
|
|
15
|
+
interface TableFileInfo {
|
|
16
|
+
/** 表文件路径 */
|
|
17
|
+
file: string;
|
|
18
|
+
/** 文件类型:core(核心)、project(项目)或 addon(组件) */
|
|
19
|
+
type: 'core' | 'project' | 'addon';
|
|
20
|
+
/** 如果是 addon 类型,记录 addon 名称 */
|
|
21
|
+
addonName?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 保留字段列表
|
|
26
|
+
*/
|
|
27
|
+
const RESERVED_FIELDS = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'] as const;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 允许的字段类型
|
|
31
|
+
*/
|
|
32
|
+
const FIELD_TYPES = ['string', 'number', 'text', 'array'] as const;
|
|
5
33
|
|
|
6
|
-
|
|
34
|
+
/**
|
|
35
|
+
* 小驼峰命名正则
|
|
36
|
+
* 可选:以下划线开头(用于特殊文件如 _common.json)
|
|
37
|
+
* 必须以小写字母开头,后续可包含小写/数字,或多个 [大写+小写/数字] 片段
|
|
38
|
+
* 示例:userTable、testCustomers、common、_common
|
|
39
|
+
*/
|
|
40
|
+
const LOWER_CAMEL_CASE_REGEX = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
7
41
|
|
|
8
|
-
|
|
42
|
+
/**
|
|
43
|
+
* 字段名称正则
|
|
44
|
+
* 必须为中文、数字、字母、下划线、短横线、空格
|
|
45
|
+
*/
|
|
46
|
+
const FIELD_NAME_REGEX = /^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* VARCHAR 最大长度限制
|
|
50
|
+
*/
|
|
51
|
+
const MAX_VARCHAR_LENGTH = 65535;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 检查表定义文件
|
|
55
|
+
* @returns 检查是否通过
|
|
56
|
+
*/
|
|
57
|
+
export default async function (): Promise<boolean> {
|
|
9
58
|
try {
|
|
10
59
|
const tablesGlob = new Bun.Glob('*.json');
|
|
11
60
|
|
|
@@ -16,8 +65,8 @@ export const checkTable = async () => {
|
|
|
16
65
|
let invalidFiles = 0;
|
|
17
66
|
|
|
18
67
|
// 收集所有表文件
|
|
19
|
-
const allTableFiles = [];
|
|
20
|
-
const coreTableNames = new Set(); // 存储内核表文件名
|
|
68
|
+
const allTableFiles: TableFileInfo[] = [];
|
|
69
|
+
const coreTableNames = new Set<string>(); // 存储内核表文件名
|
|
21
70
|
|
|
22
71
|
// 收集内核表字段定义文件
|
|
23
72
|
for await (const file of tablesGlob.scan({
|
|
@@ -49,23 +98,46 @@ export const checkTable = async () => {
|
|
|
49
98
|
allTableFiles.push({ file, type: 'project' });
|
|
50
99
|
}
|
|
51
100
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
101
|
+
// 收集 addon 表字段定义文件,并检查是否与内核表同名
|
|
102
|
+
const addons = scanAddons();
|
|
103
|
+
for (const addonName of addons) {
|
|
104
|
+
const addonTablesDir = getAddonDir(addonName, 'tables');
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
for await (const file of tablesGlob.scan({
|
|
108
|
+
cwd: addonTablesDir,
|
|
109
|
+
absolute: true,
|
|
110
|
+
onlyFiles: true
|
|
111
|
+
})) {
|
|
112
|
+
const fileName = path.basename(file, '.json');
|
|
113
|
+
|
|
114
|
+
// 检查 addon 表是否与内核表同名
|
|
115
|
+
if (coreTableNames.has(fileName)) {
|
|
116
|
+
Logger.error(`组件${addonName}表 ${fileName}.json 与内核表同名,addon 表不能与内核表定义文件同名`);
|
|
117
|
+
invalidFiles++;
|
|
118
|
+
totalFiles++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
allTableFiles.push({ file, type: 'addon', addonName });
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// addon 的 tables 目录可能不存在,跳过
|
|
126
|
+
}
|
|
127
|
+
}
|
|
54
128
|
|
|
55
129
|
// 合并进行验证逻辑
|
|
56
|
-
for (const { file, type } of allTableFiles) {
|
|
130
|
+
for (const { file, type, addonName } of allTableFiles) {
|
|
57
131
|
totalFiles++;
|
|
58
132
|
const fileName = path.basename(file);
|
|
59
133
|
const fileBaseName = path.basename(file, '.json');
|
|
60
|
-
const fileType = type === 'core' ? '内核' : '项目'
|
|
134
|
+
const fileType = type === 'core' ? '内核' : type === 'project' ? '项目' : `组件${addonName}`;
|
|
61
135
|
|
|
62
136
|
try {
|
|
63
|
-
// 1)
|
|
64
|
-
|
|
65
|
-
const lowerCamelCaseRegex = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
66
|
-
if (!lowerCamelCaseRegex.test(fileBaseName)) {
|
|
137
|
+
// 1) 文件名小驼峰校验
|
|
138
|
+
if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
|
|
67
139
|
Logger.error(`${fileType}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
|
|
68
|
-
//
|
|
140
|
+
// 命名不合规,记录错误并计为无效文件,继续下一个文件
|
|
69
141
|
invalidFiles++;
|
|
70
142
|
continue;
|
|
71
143
|
}
|
|
@@ -77,83 +149,82 @@ export const checkTable = async () => {
|
|
|
77
149
|
|
|
78
150
|
// 检查 table 中的每个验证规则
|
|
79
151
|
for (const [colKey, rule] of Object.entries(table)) {
|
|
152
|
+
if (typeof rule !== 'string') {
|
|
153
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 规则必须为字符串`);
|
|
154
|
+
fileValid = false;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
80
158
|
// 验证规则格式
|
|
81
159
|
try {
|
|
82
160
|
fileRules++;
|
|
83
161
|
totalRules++;
|
|
84
162
|
|
|
85
163
|
// 检查是否使用了保留字段
|
|
86
|
-
if (
|
|
87
|
-
Logger.error(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey}
|
|
164
|
+
if (RESERVED_FIELDS.includes(colKey as any)) {
|
|
165
|
+
Logger.error(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
|
|
88
166
|
fileValid = false;
|
|
89
167
|
}
|
|
90
168
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
169
|
+
// 使用 parseRule 解析字段规则
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = parseRule(rule);
|
|
173
|
+
} catch (error: any) {
|
|
174
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段规则解析失败:${error.message}`);
|
|
96
175
|
fileValid = false;
|
|
176
|
+
continue;
|
|
97
177
|
}
|
|
98
178
|
|
|
99
|
-
const
|
|
179
|
+
const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault, index: fieldIndex, regex: fieldRegx } = parsed;
|
|
100
180
|
|
|
101
|
-
// 第1
|
|
102
|
-
if (
|
|
103
|
-
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}"
|
|
181
|
+
// 第1个值:名称必须为中文、数字、字母、下划线、短横线、空格
|
|
182
|
+
if (!FIELD_NAME_REGEX.test(fieldName)) {
|
|
183
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
|
|
104
184
|
fileValid = false;
|
|
105
185
|
}
|
|
106
186
|
|
|
107
187
|
// 第2个值:字段类型必须为string,number,text,array之一
|
|
108
|
-
if (!
|
|
109
|
-
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}"
|
|
188
|
+
if (!FIELD_TYPES.includes(fieldType as any)) {
|
|
189
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
|
|
110
190
|
fileValid = false;
|
|
111
191
|
}
|
|
112
192
|
|
|
113
|
-
// 第3/4个值:需要是 null 或
|
|
114
|
-
if (!(fieldMin ===
|
|
193
|
+
// 第3/4个值:需要是 null 或 数字
|
|
194
|
+
if (!(fieldMin === null || typeof fieldMin === 'number')) {
|
|
115
195
|
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
|
|
116
196
|
fileValid = false;
|
|
117
197
|
}
|
|
118
|
-
if (!(fieldMax ===
|
|
198
|
+
if (!(fieldMax === null || typeof fieldMax === 'number')) {
|
|
119
199
|
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
|
|
120
200
|
fileValid = false;
|
|
121
201
|
}
|
|
122
202
|
|
|
123
203
|
// 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
|
|
124
|
-
if (fieldMin !==
|
|
125
|
-
if (
|
|
204
|
+
if (fieldMin !== null && fieldMax !== null) {
|
|
205
|
+
if (fieldMin > fieldMax) {
|
|
126
206
|
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
|
|
127
207
|
fileValid = false;
|
|
128
208
|
}
|
|
129
209
|
}
|
|
130
210
|
|
|
131
211
|
// 第6个值:是否创建索引必须为0或1
|
|
132
|
-
if (fieldIndex !==
|
|
212
|
+
if (fieldIndex !== 0 && fieldIndex !== 1) {
|
|
133
213
|
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 索引标识 "${fieldIndex}" 格式错误,必须为0或1`);
|
|
134
214
|
fileValid = false;
|
|
135
215
|
}
|
|
136
216
|
|
|
137
|
-
// 第7个值:必须为null
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
// 仅尝试构造以校验有效性
|
|
141
|
-
// eslint-disable-next-line no-new
|
|
142
|
-
new RegExp(fieldRegx);
|
|
143
|
-
} catch (_) {
|
|
144
|
-
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 正则约束 "${fieldRegx}" 格式错误,必须为null或有效的正则表达式`);
|
|
145
|
-
fileValid = false;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
217
|
+
// 第7个值:必须为null或正则表达式(parseRule已经验证过了)
|
|
218
|
+
// parseRule 已经将正则字符串转换为 RegExp 或 null,这里不需要再验证
|
|
148
219
|
|
|
149
|
-
// 第4个值与类型联动校验 +
|
|
220
|
+
// 第4个值与类型联动校验 + 默认值规则
|
|
150
221
|
if (fieldType === 'text') {
|
|
151
222
|
// text:min/max 必须为 null,默认值必须为 'null'
|
|
152
|
-
if (fieldMin !==
|
|
223
|
+
if (fieldMin !== null) {
|
|
153
224
|
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
|
|
154
225
|
fileValid = false;
|
|
155
226
|
}
|
|
156
|
-
if (fieldMax !==
|
|
227
|
+
if (fieldMax !== null) {
|
|
157
228
|
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
|
|
158
229
|
fileValid = false;
|
|
159
230
|
}
|
|
@@ -162,45 +233,52 @@ export const checkTable = async () => {
|
|
|
162
233
|
fileValid = false;
|
|
163
234
|
}
|
|
164
235
|
} else if (fieldType === 'string' || fieldType === 'array') {
|
|
165
|
-
if (
|
|
166
|
-
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为
|
|
236
|
+
if (fieldMax === null || typeof fieldMax !== 'number') {
|
|
237
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
|
|
167
238
|
fileValid = false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (maxVal > 65535) {
|
|
171
|
-
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,string,array 类型长度必须在 1..65535 范围内`);
|
|
239
|
+
} else if (fieldMax > MAX_VARCHAR_LENGTH) {
|
|
240
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
172
241
|
fileValid = false;
|
|
173
242
|
}
|
|
174
243
|
} else if (fieldType === 'number') {
|
|
175
|
-
if (
|
|
176
|
-
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 number
|
|
244
|
+
if (fieldDefault !== 'null' && typeof fieldDefault !== 'number') {
|
|
245
|
+
Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或null,当前为 "${fieldDefault}"`);
|
|
177
246
|
fileValid = false;
|
|
178
247
|
}
|
|
179
248
|
}
|
|
180
|
-
} catch (error) {
|
|
181
|
-
|
|
182
|
-
fileValid = false;
|
|
249
|
+
} catch (error: any) {
|
|
250
|
+
// 单个字段规则解析失败已在上面处理
|
|
183
251
|
}
|
|
184
252
|
}
|
|
185
253
|
|
|
186
254
|
if (fileValid) {
|
|
187
255
|
validFiles++;
|
|
256
|
+
Logger.info(`${fileType}表 ${fileName} 验证通过(${fileRules} 个字段)`);
|
|
188
257
|
} else {
|
|
189
258
|
invalidFiles++;
|
|
190
259
|
}
|
|
191
|
-
} catch (error) {
|
|
260
|
+
} catch (error: any) {
|
|
192
261
|
Logger.error(`${fileType}表 ${fileName} 解析失败: ${error.message}`);
|
|
193
262
|
invalidFiles++;
|
|
194
263
|
}
|
|
195
264
|
}
|
|
196
265
|
|
|
266
|
+
// 输出统计信息
|
|
267
|
+
Logger.info(`\n表定义检查完成:`);
|
|
268
|
+
Logger.info(` 总文件数: ${totalFiles}`);
|
|
269
|
+
Logger.info(` 总规则数: ${totalRules}`);
|
|
270
|
+
Logger.info(` 通过文件: ${validFiles}`);
|
|
271
|
+
Logger.info(` 失败文件: ${invalidFiles}`);
|
|
272
|
+
|
|
197
273
|
if (invalidFiles > 0) {
|
|
274
|
+
Logger.error(`\n表定义检查失败,请修复上述错误后重试`);
|
|
198
275
|
return false;
|
|
199
276
|
} else {
|
|
277
|
+
Logger.info(`\n所有表定义检查通过 ✓`);
|
|
200
278
|
return true;
|
|
201
279
|
}
|
|
202
|
-
} catch (error) {
|
|
203
|
-
Logger.error(
|
|
280
|
+
} catch (error: any) {
|
|
281
|
+
Logger.error('数据表定义检查过程中出错:', error);
|
|
204
282
|
return false;
|
|
205
283
|
}
|
|
206
|
-
}
|
|
284
|
+
}
|
package/config/env.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 环境变量配置 - TypeScript 版本
|
|
3
|
+
* 类型化所有环境变量
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 环境变量配置接口
|
|
8
|
+
*/
|
|
9
|
+
export interface EnvConfig {
|
|
10
|
+
// ========== 项目配置 ==========
|
|
11
|
+
/** 项目模式:development | production | test */
|
|
12
|
+
NODE_ENV: string;
|
|
13
|
+
/** 应用名称 */
|
|
14
|
+
APP_NAME: string;
|
|
15
|
+
/** MD5 加密盐 */
|
|
16
|
+
MD5_SALT: string;
|
|
17
|
+
/** 监听端口 */
|
|
18
|
+
APP_PORT: number;
|
|
19
|
+
/** 监听主机 */
|
|
20
|
+
APP_HOST: string;
|
|
21
|
+
/** 超级管理员密码 */
|
|
22
|
+
DEV_PASSWORD: string;
|
|
23
|
+
/** 请求体大小限制(字节) */
|
|
24
|
+
BODY_LIMIT: number;
|
|
25
|
+
/** 是否进行参数验证 */
|
|
26
|
+
PARAMS_CHECK: string;
|
|
27
|
+
|
|
28
|
+
// ========== 日志配置 ==========
|
|
29
|
+
/** 日志等级:debug | info | warn | error */
|
|
30
|
+
LOG_LEVEL: string;
|
|
31
|
+
/** 日志排除字段(逗号分隔) */
|
|
32
|
+
LOG_EXCLUDE_FIELDS: string;
|
|
33
|
+
/** 日志目录 */
|
|
34
|
+
LOG_DIR: string;
|
|
35
|
+
/** 是否输出到控制台:0 | 1 */
|
|
36
|
+
LOG_TO_CONSOLE: number;
|
|
37
|
+
/** 日志文件最大大小(字节) */
|
|
38
|
+
LOG_MAX_SIZE: number;
|
|
39
|
+
|
|
40
|
+
// ========== 时区配置 ==========
|
|
41
|
+
/** 时区:Asia/Shanghai */
|
|
42
|
+
TZ: string;
|
|
43
|
+
|
|
44
|
+
// ========== 数据库配置 ==========
|
|
45
|
+
/** 是否启用数据库:0 | 1 */
|
|
46
|
+
DB_ENABLE: number;
|
|
47
|
+
/** 数据库类型:sqlite | mysql | postgresql */
|
|
48
|
+
DB_TYPE: string;
|
|
49
|
+
/** 数据库主机 */
|
|
50
|
+
DB_HOST: string;
|
|
51
|
+
/** 数据库端口 */
|
|
52
|
+
DB_PORT: number;
|
|
53
|
+
/** 数据库用户名 */
|
|
54
|
+
DB_USER: string;
|
|
55
|
+
/** 数据库密码 */
|
|
56
|
+
DB_PASS: string;
|
|
57
|
+
/** 数据库名称 */
|
|
58
|
+
DB_NAME: string;
|
|
59
|
+
/** 是否启用调试:0 | 1 */
|
|
60
|
+
DB_DEBUG: number;
|
|
61
|
+
/** 连接池最大连接数 */
|
|
62
|
+
DB_POOL_MAX: number;
|
|
63
|
+
|
|
64
|
+
// ========== Redis 配置 ==========
|
|
65
|
+
/** 是否启用 Redis:0 | 1 */
|
|
66
|
+
REDIS_ENABLE: number;
|
|
67
|
+
/** Redis 主机 */
|
|
68
|
+
REDIS_HOST: string;
|
|
69
|
+
/** Redis 端口 */
|
|
70
|
+
REDIS_PORT: number;
|
|
71
|
+
/** Redis 用户名 */
|
|
72
|
+
REDIS_USERNAME: string;
|
|
73
|
+
/** Redis 密码 */
|
|
74
|
+
REDIS_PASSWORD: string;
|
|
75
|
+
/** Redis 数据库索引 */
|
|
76
|
+
REDIS_DB: number;
|
|
77
|
+
/** Redis 键前缀 */
|
|
78
|
+
REDIS_KEY_PREFIX: string;
|
|
79
|
+
|
|
80
|
+
// ========== JWT 配置 ==========
|
|
81
|
+
/** JWT 密钥 */
|
|
82
|
+
JWT_SECRET: string;
|
|
83
|
+
/** JWT 过期时间:7d | 30d | 1h */
|
|
84
|
+
JWT_EXPIRES_IN: string;
|
|
85
|
+
/** JWT 算法:HS256 | HS384 | HS512 */
|
|
86
|
+
JWT_ALGORITHM: string;
|
|
87
|
+
|
|
88
|
+
// ========== CORS 配置 ==========
|
|
89
|
+
/** 允许的来源 */
|
|
90
|
+
ALLOWED_ORIGIN: string;
|
|
91
|
+
/** 允许的方法 */
|
|
92
|
+
ALLOWED_METHODS: string;
|
|
93
|
+
/** 允许的头部 */
|
|
94
|
+
ALLOWED_HEADERS: string;
|
|
95
|
+
/** 暴露的头部 */
|
|
96
|
+
EXPOSE_HEADERS: string;
|
|
97
|
+
/** 预检请求缓存时间(秒) */
|
|
98
|
+
MAX_AGE: number;
|
|
99
|
+
/** 是否允许凭证 */
|
|
100
|
+
ALLOW_CREDENTIALS: string;
|
|
101
|
+
|
|
102
|
+
// ========== 邮件配置 ==========
|
|
103
|
+
/** 邮件服务器主机 */
|
|
104
|
+
MAIL_HOST: string;
|
|
105
|
+
/** 邮件服务器端口 */
|
|
106
|
+
MAIL_PORT: number;
|
|
107
|
+
/** 是否使用连接池 */
|
|
108
|
+
MAIL_POOL: string;
|
|
109
|
+
/** 是否使用 SSL */
|
|
110
|
+
MAIL_SECURE: string;
|
|
111
|
+
/** 邮件用户名 */
|
|
112
|
+
MAIL_USER: string;
|
|
113
|
+
/** 邮件密码 */
|
|
114
|
+
MAIL_PASS: string;
|
|
115
|
+
/** 发件人名称 */
|
|
116
|
+
MAIL_SENDER: string;
|
|
117
|
+
/** 发件人地址 */
|
|
118
|
+
MAIL_ADDRESS: string;
|
|
119
|
+
|
|
120
|
+
// ========== 同步脚本配置 ==========
|
|
121
|
+
/** 是否合并 ALTER 语句 */
|
|
122
|
+
SYNC_MERGE_ALTER: string;
|
|
123
|
+
/** 是否同步在线索引 */
|
|
124
|
+
SYNC_ONLINE_INDEX: string;
|
|
125
|
+
/** 是否禁止字段缩小 */
|
|
126
|
+
SYNC_DISALLOW_SHRINK: string;
|
|
127
|
+
/** 是否允许类型变更 */
|
|
128
|
+
SYNC_ALLOW_TYPE_CHANGE: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 获取环境变量值(带默认值)
|
|
133
|
+
*/
|
|
134
|
+
const getEnv = (key: string, defaultValue: string = ''): string => {
|
|
135
|
+
return process.env[key] || defaultValue;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 获取数字类型环境变量
|
|
140
|
+
*/
|
|
141
|
+
const getEnvNumber = (key: string, defaultValue: number = 0): number => {
|
|
142
|
+
const value = process.env[key];
|
|
143
|
+
return value ? Number(value) : defaultValue;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 环境变量配置对象
|
|
148
|
+
*/
|
|
149
|
+
export const Env: EnvConfig = {
|
|
150
|
+
// ========== 项目配置 ==========
|
|
151
|
+
NODE_ENV: getEnv('NODE_ENV', 'development'),
|
|
152
|
+
APP_NAME: getEnv('APP_NAME', 'befly'),
|
|
153
|
+
MD5_SALT: getEnv('MD5_SALT', 'befly'),
|
|
154
|
+
APP_PORT: getEnvNumber('APP_PORT', 3000),
|
|
155
|
+
APP_HOST: getEnv('APP_HOST', '0.0.0.0'),
|
|
156
|
+
DEV_PASSWORD: getEnv('DEV_PASSWORD', ''),
|
|
157
|
+
BODY_LIMIT: getEnvNumber('BODY_LIMIT', 10485760), // 10MB
|
|
158
|
+
PARAMS_CHECK: getEnv('PARAMS_CHECK', 'true'),
|
|
159
|
+
|
|
160
|
+
// ========== 日志配置 ==========
|
|
161
|
+
LOG_LEVEL: getEnv('LOG_LEVEL', 'info'),
|
|
162
|
+
LOG_EXCLUDE_FIELDS: getEnv('LOG_EXCLUDE_FIELDS', 'password,token,secret'),
|
|
163
|
+
LOG_DIR: getEnv('LOG_DIR', './logs'),
|
|
164
|
+
LOG_TO_CONSOLE: getEnvNumber('LOG_TO_CONSOLE', 1),
|
|
165
|
+
LOG_MAX_SIZE: getEnvNumber('LOG_MAX_SIZE', 10485760), // 10MB
|
|
166
|
+
|
|
167
|
+
// ========== 时区配置 ==========
|
|
168
|
+
TZ: getEnv('TZ', 'Asia/Shanghai'),
|
|
169
|
+
|
|
170
|
+
// ========== 数据库配置 ==========
|
|
171
|
+
DB_ENABLE: getEnvNumber('DB_ENABLE', 1),
|
|
172
|
+
DB_TYPE: getEnv('DB_TYPE', 'mysql'),
|
|
173
|
+
DB_HOST: getEnv('DB_HOST', 'localhost'),
|
|
174
|
+
DB_PORT: getEnvNumber('DB_PORT', 3306),
|
|
175
|
+
DB_USER: getEnv('DB_USER', 'root'),
|
|
176
|
+
DB_PASS: getEnv('DB_PASS', ''),
|
|
177
|
+
DB_NAME: getEnv('DB_NAME', 'befly'),
|
|
178
|
+
DB_DEBUG: getEnvNumber('DB_DEBUG', 0),
|
|
179
|
+
DB_POOL_MAX: getEnvNumber('DB_POOL_MAX', 10),
|
|
180
|
+
|
|
181
|
+
// ========== Redis 配置 ==========
|
|
182
|
+
REDIS_ENABLE: getEnvNumber('REDIS_ENABLE', 1),
|
|
183
|
+
REDIS_HOST: getEnv('REDIS_HOST', 'localhost'),
|
|
184
|
+
REDIS_PORT: getEnvNumber('REDIS_PORT', 6379),
|
|
185
|
+
REDIS_USERNAME: getEnv('REDIS_USERNAME', ''),
|
|
186
|
+
REDIS_PASSWORD: getEnv('REDIS_PASSWORD', ''),
|
|
187
|
+
REDIS_DB: getEnvNumber('REDIS_DB', 0),
|
|
188
|
+
REDIS_KEY_PREFIX: getEnv('REDIS_KEY_PREFIX', 'befly'),
|
|
189
|
+
|
|
190
|
+
// ========== JWT 配置 ==========
|
|
191
|
+
JWT_SECRET: getEnv('JWT_SECRET', 'befly-secret'),
|
|
192
|
+
JWT_EXPIRES_IN: getEnv('JWT_EXPIRES_IN', '7d'),
|
|
193
|
+
JWT_ALGORITHM: getEnv('JWT_ALGORITHM', 'HS256'),
|
|
194
|
+
|
|
195
|
+
// ========== CORS 配置 ==========
|
|
196
|
+
ALLOWED_ORIGIN: getEnv('ALLOWED_ORIGIN', '*'),
|
|
197
|
+
ALLOWED_METHODS: getEnv('ALLOWED_METHODS', 'GET, POST, PUT, DELETE, OPTIONS'),
|
|
198
|
+
ALLOWED_HEADERS: getEnv('ALLOWED_HEADERS', 'Content-Type, Authorization, authorization, token'),
|
|
199
|
+
EXPOSE_HEADERS: getEnv('EXPOSE_HEADERS', 'Content-Range, X-Content-Range, Authorization, authorization, token'),
|
|
200
|
+
MAX_AGE: getEnvNumber('MAX_AGE', 86400),
|
|
201
|
+
ALLOW_CREDENTIALS: getEnv('ALLOW_CREDENTIALS', 'true'),
|
|
202
|
+
|
|
203
|
+
// ========== 邮件配置 ==========
|
|
204
|
+
MAIL_HOST: getEnv('MAIL_HOST', ''),
|
|
205
|
+
MAIL_PORT: getEnvNumber('MAIL_PORT', 587),
|
|
206
|
+
MAIL_POOL: getEnv('MAIL_POOL', 'true'),
|
|
207
|
+
MAIL_SECURE: getEnv('MAIL_SECURE', 'false'),
|
|
208
|
+
MAIL_USER: getEnv('MAIL_USER', ''),
|
|
209
|
+
MAIL_PASS: getEnv('MAIL_PASS', ''),
|
|
210
|
+
MAIL_SENDER: getEnv('MAIL_SENDER', ''),
|
|
211
|
+
MAIL_ADDRESS: getEnv('MAIL_ADDRESS', ''),
|
|
212
|
+
|
|
213
|
+
// ========== 同步脚本配置 ==========
|
|
214
|
+
SYNC_MERGE_ALTER: getEnv('SYNC_MERGE_ALTER', 'false'),
|
|
215
|
+
SYNC_ONLINE_INDEX: getEnv('SYNC_ONLINE_INDEX', 'false'),
|
|
216
|
+
SYNC_DISALLOW_SHRINK: getEnv('SYNC_DISALLOW_SHRINK', 'true'),
|
|
217
|
+
SYNC_ALLOW_TYPE_CHANGE: getEnv('SYNC_ALLOW_TYPE_CHANGE', 'false')
|
|
218
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 核心保留名称配置
|
|
3
|
+
* 定义框架保留的资源名称,防止用户和 addon 使用
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 保留名称配置
|
|
8
|
+
*/
|
|
9
|
+
export const RESERVED_NAMES = {
|
|
10
|
+
/**
|
|
11
|
+
* 核心表前缀(禁止用户使用)
|
|
12
|
+
*/
|
|
13
|
+
tablePrefix: ['sys_'],
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 核心 API 路由前缀(禁止用户使用)
|
|
17
|
+
*/
|
|
18
|
+
apiRoutes: ['/api/health', '/api/tool'],
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 核心插件名(禁止用户使用)
|
|
22
|
+
*/
|
|
23
|
+
plugins: ['db', 'logger', 'redis', 'tool'],
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 禁止用作 addon 名称
|
|
27
|
+
*/
|
|
28
|
+
addonNames: ['core', 'system', 'admin', 'sys', 'befly', 'app', 'api', 'config', 'utils', 'types']
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 检测表名是否使用了保留前缀
|
|
33
|
+
*/
|
|
34
|
+
export function isReservedTableName(tableName: string): boolean {
|
|
35
|
+
return RESERVED_NAMES.tablePrefix.some((prefix) => tableName.startsWith(prefix));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 检测 API 路由是否使用了保留路径
|
|
40
|
+
*/
|
|
41
|
+
export function isReservedRoute(route: string): boolean {
|
|
42
|
+
// 移除方法前缀(如 POST/GET)
|
|
43
|
+
const path = route.replace(/^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\//i, '/');
|
|
44
|
+
return RESERVED_NAMES.apiRoutes.some((reserved) => path.startsWith(reserved));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 检测插件名是否使用了保留名称
|
|
49
|
+
*/
|
|
50
|
+
export function isReservedPluginName(pluginName: string): boolean {
|
|
51
|
+
// 检测核心插件名
|
|
52
|
+
if (RESERVED_NAMES.plugins.includes(pluginName)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
// 检测是否使用点号命名空间但前缀是保留名称
|
|
56
|
+
if (pluginName.includes('.')) {
|
|
57
|
+
const prefix = pluginName.split('.')[0];
|
|
58
|
+
return RESERVED_NAMES.plugins.includes(prefix);
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 检测 addon 名称是否使用了保留名称
|
|
65
|
+
*/
|
|
66
|
+
export function isReservedAddonName(addonName: string): boolean {
|
|
67
|
+
return RESERVED_NAMES.addonNames.includes(addonName.toLowerCase());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 获取保留前缀列表(用于错误提示)
|
|
72
|
+
*/
|
|
73
|
+
export function getReservedTablePrefixes(): string[] {
|
|
74
|
+
return [...RESERVED_NAMES.tablePrefix];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 获取保留路由列表(用于错误提示)
|
|
79
|
+
*/
|
|
80
|
+
export function getReservedRoutes(): string[] {
|
|
81
|
+
return [...RESERVED_NAMES.apiRoutes];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取保留插件名列表(用于错误提示)
|
|
86
|
+
*/
|
|
87
|
+
export function getReservedPlugins(): string[] {
|
|
88
|
+
return [...RESERVED_NAMES.plugins];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 获取保留 addon 名称列表(用于错误提示)
|
|
93
|
+
*/
|
|
94
|
+
export function getReservedAddonNames(): string[] {
|
|
95
|
+
return [...RESERVED_NAMES.addonNames];
|
|
96
|
+
}
|