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.
Files changed (93) hide show
  1. package/apis/health/info.ts +64 -0
  2. package/apis/tool/tokenCheck.ts +51 -0
  3. package/bin/befly.ts +202 -0
  4. package/checks/conflict.ts +408 -0
  5. package/checks/{table.js → table.ts} +139 -61
  6. package/config/env.ts +218 -0
  7. package/config/reserved.ts +96 -0
  8. package/main.ts +101 -0
  9. package/package.json +44 -8
  10. package/plugins/{db.js → db.ts} +24 -11
  11. package/plugins/logger.ts +28 -0
  12. package/plugins/redis.ts +51 -0
  13. package/plugins/tool.ts +34 -0
  14. package/scripts/syncDb/apply.ts +171 -0
  15. package/scripts/syncDb/constants.ts +70 -0
  16. package/scripts/syncDb/ddl.ts +182 -0
  17. package/scripts/syncDb/helpers.ts +172 -0
  18. package/scripts/syncDb/index.ts +215 -0
  19. package/scripts/syncDb/schema.ts +199 -0
  20. package/scripts/syncDb/sqlite.ts +50 -0
  21. package/scripts/syncDb/state.ts +104 -0
  22. package/scripts/syncDb/table.ts +204 -0
  23. package/scripts/syncDb/tableCreate.ts +142 -0
  24. package/scripts/syncDb/tests/constants.test.ts +104 -0
  25. package/scripts/syncDb/tests/ddl.test.ts +134 -0
  26. package/scripts/syncDb/tests/helpers.test.ts +70 -0
  27. package/scripts/syncDb/types.ts +92 -0
  28. package/scripts/syncDb/version.ts +73 -0
  29. package/scripts/syncDb.ts +9 -0
  30. package/scripts/{syncDev.js → syncDev.ts} +41 -25
  31. package/system.ts +149 -0
  32. package/tables/_common.json +21 -0
  33. package/tables/admin.json +10 -0
  34. package/tsconfig.json +58 -0
  35. package/types/api.d.ts +246 -0
  36. package/types/befly.d.ts +234 -0
  37. package/types/common.d.ts +215 -0
  38. package/types/context.ts +167 -0
  39. package/types/crypto.d.ts +23 -0
  40. package/types/database.d.ts +278 -0
  41. package/types/index.d.ts +16 -0
  42. package/types/index.ts +459 -0
  43. package/types/jwt.d.ts +99 -0
  44. package/types/logger.d.ts +43 -0
  45. package/types/plugin.d.ts +109 -0
  46. package/types/redis.d.ts +44 -0
  47. package/types/tool.d.ts +67 -0
  48. package/types/validator.d.ts +45 -0
  49. package/utils/addonHelper.ts +60 -0
  50. package/utils/api.ts +23 -0
  51. package/utils/{colors.js → colors.ts} +79 -21
  52. package/utils/crypto.ts +308 -0
  53. package/utils/datetime.ts +51 -0
  54. package/utils/dbHelper.ts +142 -0
  55. package/utils/errorHandler.ts +68 -0
  56. package/utils/index.ts +46 -0
  57. package/utils/jwt.ts +493 -0
  58. package/utils/logger.ts +284 -0
  59. package/utils/objectHelper.ts +68 -0
  60. package/utils/pluginHelper.ts +62 -0
  61. package/utils/redisHelper.ts +338 -0
  62. package/utils/response.ts +38 -0
  63. package/utils/{sqlBuilder.js → sqlBuilder.ts} +233 -97
  64. package/utils/sqlHelper.ts +447 -0
  65. package/utils/tableHelper.ts +167 -0
  66. package/utils/tool.ts +230 -0
  67. package/utils/typeHelper.ts +101 -0
  68. package/utils/validate.ts +451 -0
  69. package/utils/{xml.js → xml.ts} +100 -74
  70. package/.npmrc +0 -3
  71. package/.prettierignore +0 -2
  72. package/.prettierrc +0 -11
  73. package/apis/health/info.js +0 -49
  74. package/apis/tool/tokenCheck.js +0 -29
  75. package/bin/befly.js +0 -109
  76. package/config/env.js +0 -64
  77. package/main.js +0 -579
  78. package/plugins/logger.js +0 -14
  79. package/plugins/redis.js +0 -32
  80. package/plugins/tool.js +0 -8
  81. package/scripts/syncDb.js +0 -752
  82. package/system.js +0 -118
  83. package/tables/common.json +0 -16
  84. package/tables/tool.json +0 -6
  85. package/utils/api.js +0 -27
  86. package/utils/crypto.js +0 -260
  87. package/utils/index.js +0 -334
  88. package/utils/jwt.js +0 -387
  89. package/utils/logger.js +0 -143
  90. package/utils/redisHelper.js +0 -74
  91. package/utils/sqlManager.js +0 -471
  92. package/utils/tool.js +0 -31
  93. 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
- // 所有校验函数均复用 utils/index.js 导出的实现
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
- export const checkTable = async () => {
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 reservedFields = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'];
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
- // 示例:userTable、testCustomers、common
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 (reservedFields.includes(colKey)) {
87
- Logger.error(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},不能在表定义中使用以下字段: ${reservedFields.join(', ')}`);
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
- const allParts = rule.split('⚡');
92
-
93
- // 必须包含7个部分:显示名⚡类型⚡最小值⚡最大值⚡默认值⚡是否索引⚡正则约束
94
- if (allParts.length !== 7) {
95
- Logger.error(`${fileType} ${fileName} 文件 ${colKey} 字段规则格式错误,必须包含7个部分,当前包含${allParts.length}个部分`);
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 [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = allParts;
179
+ const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault, index: fieldIndex, regex: fieldRegx } = parsed;
100
180
 
101
- // 第1个值:名称必须为中文、数字、字母
102
- if (!/^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/.test(fieldName)) {
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 (!['string', 'number', 'text', 'array'].includes(fieldType)) {
109
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,必须为stringnumber、text、array之一`);
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 === 'null' || !Number.isNaN(Number(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 === 'null' || !Number.isNaN(Number(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 !== 'null' && fieldMax !== 'null') {
125
- if (Number(fieldMin) > Number(fieldMax)) {
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 !== '0' && fieldIndex !== '1') {
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
- if (fieldRegx !== 'null') {
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 !== 'null') {
223
+ if (fieldMin !== null) {
153
224
  Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
154
225
  fileValid = false;
155
226
  }
156
- if (fieldMax !== 'null') {
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 (Number.isNaN(Number(fieldMax))) {
166
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 string,array 类型,最大长度必须为数字,当前为 "${fieldMax}"`);
236
+ if (fieldMax === null || typeof fieldMax !== 'number') {
237
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
167
238
  fileValid = false;
168
- }
169
- const maxVal = parseInt(fieldMax, 10);
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 (Number.isNaN(Number(fieldDefault))) {
176
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 number 类型,默认值必须为数字,当前为 "${fieldDefault}"`);
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
- Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 验证规则解析失败: ${error.message}`);
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(`Tables 检查过程中出错:`, 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
+ }