befly 3.76.1 → 3.76.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.
Files changed (149) hide show
  1. package/Befly.js +159 -0
  2. package/README.md +50 -11
  3. package/apis/admin/delete.js +1 -3
  4. package/apis/admin/select.js +2 -7
  5. package/apis/admin/update.js +1 -5
  6. package/apis/api/select.js +2 -7
  7. package/apis/auth/login.js +16 -20
  8. package/apis/dict/delete.js +1 -1
  9. package/apis/dict/detail.js +1 -3
  10. package/apis/dict/select.js +1 -2
  11. package/apis/dict/update.js +1 -1
  12. package/apis/dictType/delete.js +1 -1
  13. package/apis/dictType/detail.js +1 -1
  14. package/apis/dictType/insert.js +1 -1
  15. package/apis/dictType/select.js +2 -7
  16. package/apis/dictType/update.js +2 -2
  17. package/apis/email/config.js +4 -7
  18. package/apis/email/logList.js +2 -7
  19. package/apis/email/send.js +1 -2
  20. package/apis/email/verify.js +1 -3
  21. package/apis/loginLog/select.js +2 -7
  22. package/apis/menu/select.js +2 -7
  23. package/apis/operateLog/select.js +2 -7
  24. package/apis/role/apiSave.js +2 -2
  25. package/apis/role/menuSave.js +2 -2
  26. package/apis/role/select.js +2 -7
  27. package/apis/source/imageList.js +2 -5
  28. package/apis/tongJi/dailyReport.js +12 -13
  29. package/apis/tongJi/dailyReportSelect.js +1 -2
  30. package/apis/tongJi/dailyStats.js +1 -1
  31. package/apis/tongJi/dailyStatsDistribution.js +2 -2
  32. package/apis/tongJi/errorList.js +1 -2
  33. package/apis/tongJi/errorReport.js +1 -2
  34. package/apis/tongJi/projectDelete.js +1 -3
  35. package/apis/tongJi/projectSelect.js +2 -4
  36. package/apis/tongJi/projectUpdate.js +2 -2
  37. package/apis/upload/file.js +7 -5
  38. package/checks/api.js +43 -66
  39. package/checks/config.js +17 -12
  40. package/checks/corns.js +28 -46
  41. package/checks/field.js +18 -0
  42. package/checks/items.js +16 -60
  43. package/checks/menu.js +28 -39
  44. package/checks/table.js +106 -26
  45. package/checks/validation.js +13 -0
  46. package/configs/beflyConfig.json +6 -6
  47. package/configs/constConfig.js +2 -63
  48. package/exports.js +2 -0
  49. package/hooks/auth.js +11 -7
  50. package/hooks/parser.js +55 -59
  51. package/hooks/permission.js +1 -0
  52. package/hooks/rateLimit.js +25 -62
  53. package/hooks/validator.js +43 -31
  54. package/index.js +43 -344
  55. package/{lib → libs}/cacheHelper.js +3 -8
  56. package/libs/cron.js +60 -0
  57. package/libs/logger/index.js +1 -0
  58. package/libs/logger/logger.js +245 -0
  59. package/libs/logger/sanitize.js +115 -0
  60. package/{lib → libs/mysql}/dbHelper.js +100 -73
  61. package/{lib → libs/mysql}/dbParse.js +16 -9
  62. package/{lib → libs/mysql}/dbUtil.js +15 -32
  63. package/libs/mysql/error.js +29 -0
  64. package/libs/mysql/index.js +6 -0
  65. package/libs/mysql/is.js +29 -0
  66. package/libs/mysql/mysql.js +43 -0
  67. package/libs/mysql/sql/identifier.js +85 -0
  68. package/libs/mysql/sql/index.js +2 -0
  69. package/{lib → libs/mysql/sql}/sqlBuilder.js +2 -3
  70. package/libs/mysql/util.js +44 -0
  71. package/libs/redis/index.js +2 -0
  72. package/libs/redis/redis.js +204 -0
  73. package/libs/redis/redisError.js +8 -0
  74. package/libs/tableModel.js +72 -0
  75. package/libs/tool.js +33 -0
  76. package/libs/validator/compiler.js +248 -0
  77. package/libs/validator/index.js +10 -0
  78. package/libs/validator/parser.js +173 -0
  79. package/libs/validator/util.js +5 -0
  80. package/package.json +10 -8
  81. package/paths.js +2 -3
  82. package/plugins/cache.js +2 -1
  83. package/plugins/email.js +4 -4
  84. package/plugins/mysql.js +5 -15
  85. package/plugins/redis.js +4 -12
  86. package/router/api.js +14 -10
  87. package/router/root.js +14 -0
  88. package/router/static.js +3 -3
  89. package/schemas/api.json +99 -0
  90. package/schemas/config.json +255 -0
  91. package/schemas/corn.json +39 -0
  92. package/schemas/item.json +43 -0
  93. package/schemas/menu.json +44 -0
  94. package/schemas/table.json +80 -0
  95. package/scripts/syncDb/apply.js +87 -0
  96. package/scripts/syncDb/ddl.js +43 -0
  97. package/scripts/syncDb/index.js +175 -30
  98. package/scripts/syncDb/model.js +60 -0
  99. package/scripts/syncDb/plan.js +160 -0
  100. package/scripts/syncDb/preflight.js +98 -0
  101. package/scripts/syncDb/report.js +177 -162
  102. package/scripts/syncDb/snapshot.js +118 -0
  103. package/tables/admin.json +51 -53
  104. package/tables/api.json +28 -39
  105. package/tables/dailyReport.json +85 -73
  106. package/tables/dict.json +27 -39
  107. package/tables/dictType.json +20 -34
  108. package/tables/emailLog.json +49 -47
  109. package/tables/errorReport.json +86 -60
  110. package/tables/file.json +47 -50
  111. package/tables/loginLog.json +68 -55
  112. package/tables/menu.json +20 -34
  113. package/tables/operateLog.json +53 -47
  114. package/tables/project.json +18 -30
  115. package/tables/role.json +33 -37
  116. package/utils/cors.js +37 -41
  117. package/utils/deepMerge.js +3 -1
  118. package/utils/error.js +1 -56
  119. package/utils/id.js +44 -0
  120. package/utils/is.js +6 -243
  121. package/utils/response.js +12 -1
  122. package/utils/scanFiles.js +13 -4
  123. package/utils/scanSources.js +58 -20
  124. package/utils/util.js +11 -163
  125. package/apis/_apis.js +0 -20
  126. package/apis/auth/sendSmsCode.js +0 -24
  127. package/configs/regexpAlias.json +0 -55
  128. package/lib/connect.js +0 -119
  129. package/lib/logger.js +0 -430
  130. package/lib/redisHelper.js +0 -399
  131. package/lib/validator.js +0 -274
  132. package/lib/xmlParse.js +0 -149
  133. package/plugins/config.js +0 -13
  134. package/plugins/cron.js +0 -115
  135. package/plugins/logger.js +0 -17
  136. package/plugins/tool.js +0 -131
  137. package/plugins/xmlParse.js +0 -10
  138. package/schemas/config.js +0 -67
  139. package/scripts/syncDb/context.js +0 -120
  140. package/scripts/syncDb/diff.js +0 -279
  141. package/scripts/syncDb/transform.js +0 -113
  142. package/sql/befly.sql +0 -271
  143. package/utils/email.js +0 -3
  144. package/utils/fieldClear.js +0 -22
  145. package/utils/formatValidationIssues.js +0 -28
  146. package/utils/getClientIp.js +0 -47
  147. package/utils/loggerUtils.js +0 -196
  148. package/utils/regexpUtil.js +0 -52
  149. /package/{lib → libs}/smtpText.js +0 -0
@@ -1,399 +0,0 @@
1
- /**
2
- * 提供 Redis 操作的便捷方法
3
- */
4
-
5
- import { createError } from "../utils/error.js";
6
- import { Connect } from "./connect.js";
7
- import { Logger } from "./logger.js";
8
-
9
- /**
10
- * Redis 助手类
11
- * 约定:除构造函数与 ping 外,方法默认不抛异常;失败时返回 null/false/0/[] 并记录日志。
12
- */
13
- export class RedisHelper {
14
- /**
15
- * 构造函数
16
- * @param prefix - Key 前缀
17
- */
18
- constructor(prefix = "") {
19
- const client = Connect.getRedis();
20
- if (!client) {
21
- throw createError("Redis 客户端未初始化,请先调用 Connect.connectRedis()", { code: "runtime" });
22
- }
23
- this.client = client;
24
- this.prefix = prefix ? `${prefix}:` : "";
25
- }
26
-
27
- /**
28
- * 吞错执行:失败记录日志并返回 fallback(不抛出)
29
- * @param label - 操作名(用于日志文案)
30
- * @param operation - 实际操作
31
- * @param fallback - 失败返回值
32
- */
33
- async safeCall(label, operation, fallback) {
34
- try {
35
- return await operation();
36
- } catch (error) {
37
- Logger.error(`Redis ${label} 错误`, error);
38
- return fallback;
39
- }
40
- }
41
-
42
- /**
43
- * 设置对象到 Redis
44
- * @param key - 键名
45
- * @param obj - 对象
46
- * @param ttl - 过期时间(秒)
47
- * @returns 操作结果
48
- */
49
- async setObject(key, obj, ttl = null) {
50
- return this.safeCall(
51
- "setObject",
52
- async () => {
53
- const data = JSON.stringify(obj);
54
- const pkey = `${this.prefix}${key}`;
55
- return ttl ? this.client.setex(pkey, ttl, data) : this.client.set(pkey, data);
56
- },
57
- null
58
- );
59
- }
60
-
61
- /**
62
- * 从 Redis 获取对象
63
- * @param key - 键名
64
- * @returns 对象或 null
65
- */
66
- async getObject(key) {
67
- return this.safeCall(
68
- "getObject",
69
- async () => {
70
- const pkey = `${this.prefix}${key}`;
71
- const data = await this.client.get(pkey);
72
- return data ? JSON.parse(data) : null;
73
- },
74
- null
75
- );
76
- }
77
-
78
- /**
79
- * 从 Redis 删除对象
80
- * @param key - 键名
81
- */
82
- async delObject(key) {
83
- return this.safeCall("delObject", async () => {
84
- const pkey = `${this.prefix}${key}`;
85
- await this.client.del(pkey);
86
- });
87
- }
88
-
89
- // ==================== ID 生成 ====================
90
- // 注意:ID 生成功能强依赖 Redis 原子操作(INCR)保证分布式唯一性
91
- // 主要被 DbHelper.insData 使用
92
-
93
- /**
94
- * 生成基于时间的唯一 ID
95
- * 格式: 毫秒时间戳(13位) + 3位后缀(100-999) = 16位纯数字
96
- * 每毫秒起点基于时间戳偏移,后缀分布更均匀
97
- * @returns 唯一 ID (16位纯数字)
98
- */
99
- async genTimeID() {
100
- const timestamp = Date.now();
101
- const key = `${this.prefix}time_id:${timestamp}`;
102
-
103
- const counter = await this.client.incr(key);
104
- if (counter === 1) {
105
- await this.client.expire(key, 1);
106
- }
107
-
108
- // 基于时间戳偏移起点,后缀 100-999 循环
109
- const suffix = 100 + (((timestamp % 900) + counter - 1) % 900);
110
-
111
- const timeId = Number(`${timestamp}${suffix}`);
112
- return timeId;
113
- }
114
-
115
- /**
116
- * 设置字符串值
117
- * @param key - 键名
118
- * @param value - 值
119
- * @param ttl - 过期时间(秒)
120
- */
121
- async setString(key, value, ttl = null) {
122
- return this.safeCall(
123
- "setString",
124
- async () => {
125
- const pkey = `${this.prefix}${key}`;
126
- return ttl ? this.client.setex(pkey, ttl, value) : this.client.set(pkey, value);
127
- },
128
- null
129
- );
130
- }
131
-
132
- /**
133
- * 获取字符串值
134
- * @param key - 键名
135
- */
136
- async getString(key) {
137
- return this.safeCall(
138
- "getString",
139
- async () => {
140
- const pkey = `${this.prefix}${key}`;
141
- return this.client.get(pkey);
142
- },
143
- null
144
- );
145
- }
146
-
147
- /**
148
- * 检查键是否存在
149
- * @param key - 键名
150
- * @returns 是否存在(true/false)
151
- */
152
- async exists(key) {
153
- return this.safeCall(
154
- "exists",
155
- async () => {
156
- const pkey = `${this.prefix}${key}`;
157
- return this.client.exists(pkey);
158
- },
159
- false
160
- );
161
- }
162
-
163
- /**
164
- * 原子自增
165
- * @param key - 键名
166
- * @returns 自增后的值
167
- */
168
- async incr(key) {
169
- return this.safeCall(
170
- "incr",
171
- async () => {
172
- const pkey = `${this.prefix}${key}`;
173
- return this.client.incr(pkey);
174
- },
175
- 0
176
- );
177
- }
178
-
179
- /**
180
- * 原子自增并在首次自增时设置过期时间(常用于限流/计数)
181
- * @param key - 键名
182
- * @param seconds - 过期秒数
183
- * @returns 自增后的值
184
- */
185
- async incrWithExpire(key, seconds) {
186
- return this.safeCall(
187
- "incrWithExpire",
188
- async () => {
189
- const pkey = `${this.prefix}${key}`;
190
- const res = await this.client.incr(pkey);
191
- if (res === 1) {
192
- await this.client.expire(pkey, seconds);
193
- }
194
- return res;
195
- },
196
- 0
197
- );
198
- }
199
-
200
- /**
201
- * 设置过期时间
202
- * @param key - 键名
203
- * @param seconds - 秒数
204
- */
205
- async expire(key, seconds) {
206
- return this.safeCall(
207
- "expire",
208
- async () => {
209
- const pkey = `${this.prefix}${key}`;
210
- return this.client.expire(pkey, seconds);
211
- },
212
- 0
213
- );
214
- }
215
-
216
- /**
217
- * 获取剩余过期时间
218
- * @param key - 键名
219
- */
220
- async ttl(key) {
221
- return this.safeCall(
222
- "ttl",
223
- async () => {
224
- const pkey = `${this.prefix}${key}`;
225
- return this.client.ttl(pkey);
226
- },
227
- -1
228
- );
229
- }
230
-
231
- /**
232
- * 向 Set 中添加一个或多个成员
233
- * @param key - 键名
234
- * @param members - 成员数组
235
- * @returns 成功添加的成员数量
236
- */
237
- async sadd(key, members) {
238
- return this.safeCall(
239
- "sadd",
240
- async () => {
241
- if (members.length === 0) return 0;
242
-
243
- const pkey = `${this.prefix}${key}`;
244
- const args = [pkey];
245
- for (const member of members) {
246
- args.push(member);
247
- }
248
- return this.client.sadd.apply(this.client, args);
249
- },
250
- 0
251
- );
252
- }
253
-
254
- /**
255
- * 判断成员是否在 Set 中
256
- * @param key - 键名
257
- * @param member - 成员
258
- * @returns 是否存在(true/false)
259
- */
260
- async sismember(key, member) {
261
- return this.safeCall(
262
- "sismember",
263
- async () => {
264
- const pkey = `${this.prefix}${key}`;
265
- return this.client.sismember(pkey, member);
266
- },
267
- false
268
- );
269
- }
270
-
271
- /**
272
- * 获取 Set 的成员数量
273
- * @param key - 键名
274
- */
275
- async scard(key) {
276
- return this.safeCall(
277
- "scard",
278
- async () => {
279
- const pkey = `${this.prefix}${key}`;
280
- return this.client.scard(pkey);
281
- },
282
- 0
283
- );
284
- }
285
-
286
- /**
287
- * 获取 Set 的所有成员
288
- * @param key - 键名
289
- * @returns 成员数组
290
- */
291
- async smembers(key) {
292
- return this.safeCall(
293
- "smembers",
294
- async () => {
295
- const pkey = `${this.prefix}${key}`;
296
- return this.client.smembers(pkey);
297
- },
298
- []
299
- );
300
- }
301
-
302
- /**
303
- * 批量向多个 Set 添加成员(利用 Bun Redis 自动管道优化)
304
- * @param items - [{ key, members }] 数组
305
- * @returns 成功添加的总成员数量
306
- */
307
- async saddBatch(items) {
308
- return this.safeCall(
309
- "saddBatch",
310
- async () => {
311
- if (items.length === 0) {
312
- return 0;
313
- }
314
-
315
- const results = await Promise.all(items.map((item) => this.sadd(item.key, item.members)));
316
- return results.reduce((sum, count) => sum + count, 0);
317
- },
318
- 0
319
- );
320
- }
321
-
322
- /**
323
- * 删除键
324
- * @param key - 键名
325
- * @returns 删除的键数量
326
- */
327
- async del(key) {
328
- return this.safeCall(
329
- "del",
330
- async () => {
331
- const pkey = `${this.prefix}${key}`;
332
- return this.client.del(pkey);
333
- },
334
- 0
335
- );
336
- }
337
-
338
- /**
339
- * 批量删除键(利用 Bun Redis 自动管道优化)
340
- * @param keys - 键名数组
341
- * @returns 成功删除的键数量
342
- */
343
- async delBatch(keys) {
344
- return this.safeCall(
345
- "delBatch",
346
- async () => {
347
- if (keys.length === 0) {
348
- return 0;
349
- }
350
-
351
- const results = await Promise.all(
352
- keys.map((key) => {
353
- const pkey = `${this.prefix}${key}`;
354
- return this.client.del(pkey);
355
- })
356
- );
357
- return results.reduce((sum, count) => sum + count, 0);
358
- },
359
- 0
360
- );
361
- }
362
-
363
- /**
364
- * 获取 Redis INFO 文本
365
- * @param section - 可选 section(如 "server")
366
- */
367
- async info(section) {
368
- return this.safeCall(
369
- "info",
370
- async () => {
371
- const client = this.client;
372
-
373
- if (typeof client.info === "function") {
374
- return client.info(section);
375
- }
376
-
377
- if (typeof client.send === "function") {
378
- return client.send("INFO", section ? [String(section)] : []);
379
- }
380
-
381
- throw createError("Redis info 不支持", { code: "runtime" });
382
- },
383
- ""
384
- );
385
- }
386
-
387
- /**
388
- * 测试 Redis 连接(异常时抛出,供调用方感知连接状态)
389
- * @returns ping 响应结果
390
- */
391
- async ping() {
392
- try {
393
- return await this.client.ping();
394
- } catch (error) {
395
- Logger.error("Redis ping 错误", error);
396
- throw createError("Redis ping 错误", { cause: error, code: "runtime", subsystem: "redis", operation: "ping" });
397
- }
398
- }
399
- }
package/lib/validator.js DELETED
@@ -1,274 +0,0 @@
1
- import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
2
- import RegexAliases from "../configs/regexpAlias.json";
3
- import { configSchema } from "../schemas/config.js";
4
- import { isFiniteNumber, isIntegerNumber, isJsonPrimitive, isNoTrimStringAllowEmpty, isNullable, isPlainObject, isRegexInput, isString, isValidTimeZone } from "../utils/is.js";
5
- import { getCompiledRegex } from "../utils/regexpUtil.js";
6
-
7
- const INPUT_TYPE_SET = new Set(FIELD_RULE_INPUT_TYPES);
8
- const FIELD_RULE_KEYS = new Set(["input", "check", "name", "min", "max", "detail"]);
9
-
10
- function addIssue(issues, path, message) {
11
- issues.push({ path: Array.isArray(path) ? path : [path], message: message });
12
- }
13
-
14
- function isJsonScalar(value) {
15
- return isJsonPrimitive(value) && (typeof value !== "number" || Number.isFinite(value));
16
- }
17
-
18
- function isScalarArray(value) {
19
- return Array.isArray(value) && value.every((item) => isJsonScalar(item));
20
- }
21
-
22
- function isFlatJson(value, kind = "any") {
23
- if (Array.isArray(value)) {
24
- return value.every((item) => isJsonScalar(item) && (kind === "any" || (kind === "number" ? isFiniteNumber(item) : isIntegerNumber(item))));
25
- }
26
-
27
- if (!isPlainObject(value)) {
28
- return false;
29
- }
30
-
31
- return Object.values(value).every((item) => isJsonScalar(item) && (kind === "any" || (kind === "number" ? isFiniteNumber(item) : isIntegerNumber(item))));
32
- }
33
-
34
- function toNumber(value, integer = false) {
35
- if (isFiniteNumber(value) && (!integer || isIntegerNumber(value))) {
36
- return { value: value, error: null };
37
- }
38
-
39
- if (!isString(value) || value.trim().length === 0) {
40
- return { value: null, error: integer ? "必须是整数" : "必须是数字" };
41
- }
42
-
43
- const number = Number(value.trim());
44
- if (!Number.isFinite(number) || (integer && !Number.isInteger(number))) {
45
- return { value: null, error: integer ? "必须是整数" : "必须是数字" };
46
- }
47
-
48
- return { value: number, error: null };
49
- }
50
-
51
- function getEnumValues(check, numeric = false, integer = false) {
52
- if (!isString(check) || check.length === 0) {
53
- return null;
54
- }
55
-
56
- const values = check
57
- .split("|")
58
- .map((item) => item.trim())
59
- .filter(Boolean);
60
-
61
- if (values.length === 0) {
62
- return null;
63
- }
64
-
65
- if (!numeric) {
66
- return values;
67
- }
68
-
69
- const numbers = values.map(Number);
70
- if (numbers.some((value) => !Number.isFinite(value) || (integer && !Number.isInteger(value)))) {
71
- return null;
72
- }
73
-
74
- return numbers;
75
- }
76
-
77
- function checkFieldValue(value, rule) {
78
- const min = isFiniteNumber(rule.min) ? rule.min : FIELD_RULE_DEFAULT_MIN;
79
- const max = isFiniteNumber(rule.max) ? rule.max : FIELD_RULE_DEFAULT_MAX;
80
- const input = rule.input;
81
- let converted = value;
82
-
83
- if (input === "number" || input === "integer" || input === "enumNumber" || input === "enumInteger") {
84
- const parsed = toNumber(value, input === "integer" || input === "enumInteger");
85
- if (parsed.error) return parsed.error;
86
- converted = parsed.value;
87
- } else if (input === "string" || input === "char" || input === "regexp" || input === "enum") {
88
- if (!isString(value)) return "必须是字符串";
89
- } else if (input === "array" || input === "arrayNumber" || input === "arrayInteger") {
90
- if (!isScalarArray(value)) return "必须是仅包含基础值的一层数组";
91
- if (input === "arrayNumber" && !value.every(isFiniteNumber)) return "数组元素必须是数字";
92
- if (input === "arrayInteger" && !value.every(isIntegerNumber)) return "数组元素必须是整数";
93
- } else if (input === "arrayObject") {
94
- if (!Array.isArray(value) || !value.every((item) => isPlainObject(item) && isFlatJson(item))) return "必须是对象成员仅包含基础值的一层数组";
95
- } else if (input === "json" || input === "jsonNumber" || input === "jsonInteger") {
96
- const kind = input === "json" ? "any" : input === "jsonNumber" ? "number" : "integer";
97
- if (!isFlatJson(value, kind)) return kind === "any" ? "必须是一层JSON对象或数组,成员只能是基础值" : `JSON值必须是${kind === "number" ? "数字" : "整数"}`;
98
- }
99
-
100
- if (input === "number" || input === "integer") {
101
- if (converted < min) return `不能小于${min}`;
102
- if (converted > max) return `不能大于${max}`;
103
- }
104
-
105
- if (input === "string" || input === "char" || input === "regexp" || input === "enum") {
106
- if (input === "char" && converted.length !== 1) return "必须是单字符";
107
- if (converted.length < min) return `长度不能少于${min}个字符`;
108
- if (converted.length > max) return `长度不能超过${max}个字符`;
109
-
110
- if (input === "regexp") {
111
- if (!isRegexInput(rule.check)) return "check 规则无效";
112
- const pattern = rule.check.startsWith("@") ? RegexAliases[rule.check.slice(1)] || rule.check : rule.check;
113
- try {
114
- if (!getCompiledRegex(pattern).test(converted)) return "格式不正确";
115
- } catch {
116
- return "check 规则无效";
117
- }
118
- }
119
-
120
- if (input === "enum") {
121
- const values = getEnumValues(rule.check);
122
- if (!values) return "check 规则无效";
123
- if (!values.includes(converted)) return "值不在枚举范围内";
124
- }
125
- }
126
-
127
- if (input === "enumNumber" || input === "enumInteger") {
128
- const values = getEnumValues(rule.check, true, input === "enumInteger");
129
- if (!values) return "check 规则无效";
130
- if (!values.includes(converted)) return "值不在枚举范围内";
131
- }
132
-
133
- if (input === "array" || input === "arrayNumber" || input === "arrayInteger" || input === "arrayObject") {
134
- if (value.length < min) return `至少需要${min}个元素`;
135
- if (value.length > max) return `最多只能有${max}个元素`;
136
- }
137
-
138
- return null;
139
- }
140
-
141
- function buildFieldResult(fieldErrors) {
142
- const errors = Object.values(fieldErrors);
143
- const errorFields = Object.keys(fieldErrors);
144
- const failed = errors.length > 0;
145
-
146
- return {
147
- code: failed ? 1 : 0,
148
- failed: failed,
149
- firstError: failed ? errors[0] : null,
150
- errors: errors,
151
- errorFields: errorFields,
152
- fieldErrors: fieldErrors
153
- };
154
- }
155
-
156
- export function validateFieldRule(rule) {
157
- const issues = [];
158
- if (!isPlainObject(rule)) {
159
- addIssue(issues, [], "验证规则必须是对象格式");
160
- return issues;
161
- }
162
-
163
- for (const key of Object.keys(rule)) {
164
- if (!FIELD_RULE_KEYS.has(key)) addIssue(issues, [key], "不允许出现");
165
- }
166
-
167
- if (!isString(rule.name) || rule.name.length === 0 || !isNoTrimStringAllowEmpty(rule.name)) addIssue(issues, ["name"], "必须是非空且无首尾空格的字符串");
168
- if (!isString(rule.input) || !INPUT_TYPE_SET.has(rule.input)) addIssue(issues, ["input"], "不支持该类型");
169
- if (rule.check !== undefined && (!isString(rule.check) || !isNoTrimStringAllowEmpty(rule.check))) addIssue(issues, ["check"], "必须是无首尾空格的字符串");
170
- if (rule.detail !== undefined && !isString(rule.detail)) addIssue(issues, ["detail"], "必须是字符串");
171
- if (rule.min !== undefined && rule.min !== null && !isFiniteNumber(rule.min)) addIssue(issues, ["min"], "必须是有限数字或 null");
172
- if (rule.max !== undefined && rule.max !== null && !isFiniteNumber(rule.max)) addIssue(issues, ["max"], "必须是有限数字或 null");
173
-
174
- if (issues.length > 0) return issues;
175
-
176
- const min = rule.min === null || rule.min === undefined ? FIELD_RULE_DEFAULT_MIN : rule.min;
177
- const max = rule.max === null || rule.max === undefined ? FIELD_RULE_DEFAULT_MAX : rule.max;
178
- if (min > max) addIssue(issues, ["min"], "不能大于 max");
179
-
180
- if (["regexp", "enum", "enumNumber", "enumInteger"].includes(rule.input) && !rule.check) {
181
- addIssue(issues, ["check"], `${rule.input} 必须提供 check`);
182
- }
183
- if (rule.input === "regexp" && rule.check && !isRegexInput(rule.check)) addIssue(issues, ["check"], "regexp.check 必须是正则字面量或 @ 别名");
184
- if (["enum", "enumNumber", "enumInteger"].includes(rule.input) && rule.check && !getEnumValues(rule.check, rule.input !== "enum", rule.input === "enumInteger")) {
185
- addIssue(issues, ["check"], `${rule.input}.check 必须是非空枚举值列表`);
186
- }
187
-
188
- return issues;
189
- }
190
-
191
- export function validateFields(data, fields, required = []) {
192
- const fieldErrors = {};
193
- if (!isPlainObject(data)) return buildFieldResult({ rootError: "数据必须是对象格式" });
194
- if (!isPlainObject(fields)) return buildFieldResult({ rootError: "验证规则必须是对象格式" });
195
-
196
- for (const field of required) {
197
- if (isNullable(data[field])) {
198
- fieldErrors[field] = `${isPlainObject(fields[field]) && isString(fields[field].name) ? fields[field].name : field}为必填项`;
199
- }
200
- }
201
-
202
- for (const [field, rule] of Object.entries(fields)) {
203
- if (fieldErrors[field] || (isNullable(data[field]) && !required.includes(field))) continue;
204
- const error = checkFieldValue(data[field], rule);
205
- if (error) fieldErrors[field] = `${rule.name}${error}`;
206
- }
207
-
208
- return buildFieldResult(fieldErrors);
209
- }
210
-
211
- function addUnknownKeys(issues, value, allowedKeys, path) {
212
- for (const key of Object.keys(value)) {
213
- if (!allowedKeys.has(key)) addIssue(issues, [...path, key], "不允许出现");
214
- }
215
- }
216
-
217
- function isConfigCheckValid(value, check) {
218
- if (check === "emailOrEmpty") return value === "" || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
219
- if (check === "timeZone") return isValidTimeZone(value);
220
- if (check === "redisPrefix") return !value.includes(":");
221
- return false;
222
- }
223
-
224
- function checkConfigLeaf(issues, value, def, path) {
225
- if (value === undefined) {
226
- if (!def.optional) addIssue(issues, path, "为必填项");
227
- return;
228
- }
229
-
230
- if (def.type === "string") {
231
- if (!isString(value)) addIssue(issues, path, "必须是字符串");
232
- else if (def.noTrim && !isNoTrimStringAllowEmpty(value)) addIssue(issues, path, "不允许首尾空格");
233
- else if (def.min !== undefined && value.length < def.min) addIssue(issues, path, `长度不能少于${def.min}个字符`);
234
- } else if (def.type === "number") {
235
- if (!isFiniteNumber(value)) addIssue(issues, path, "必须是有限数字");
236
- else if (def.integer && !isIntegerNumber(value)) addIssue(issues, path, "必须是整数");
237
- else if (def.min !== undefined && value < def.min) addIssue(issues, path, `不能小于${def.min}`);
238
- else if (def.max !== undefined && value > def.max) addIssue(issues, path, `不能大于${def.max}`);
239
- } else if (def.type === "boolInt") {
240
- if (![0, 1, true, false].includes(value)) addIssue(issues, path, "仅允许 0|1|true|false");
241
- } else if (def.type === "values") {
242
- if (!def.values.includes(value)) addIssue(issues, path, `仅允许 ${def.values.join("|")}`);
243
- } else if (def.type === "stringArray") {
244
- if (!Array.isArray(value) || !value.every((item) => isString(item) && (!def.noTrim || isNoTrimStringAllowEmpty(item)))) addIssue(issues, path, "必须是仅包含无首尾空格字符串的一层数组");
245
- }
246
-
247
- if (def.pattern && isString(value) && !def.pattern.test(value)) addIssue(issues, path, "格式不正确");
248
- if (def.check && isString(value) && !isConfigCheckValid(value, def.check)) addIssue(issues, path, "无效值");
249
- }
250
-
251
- export function validateConfig(config) {
252
- const issues = [];
253
- if (!isPlainObject(config)) {
254
- addIssue(issues, [], "配置必须是对象格式");
255
- return issues;
256
- }
257
-
258
- const { root, groups } = configSchema;
259
- addUnknownKeys(issues, config, new Set([...Object.keys(root), ...Object.keys(groups)]), []);
260
- for (const [key, def] of Object.entries(root)) checkConfigLeaf(issues, config[key], def, [key]);
261
-
262
- for (const [groupName, fields] of Object.entries(groups)) {
263
- const group = config[groupName];
264
- if (group === undefined && groupName === "email") continue;
265
- if (!isPlainObject(group)) {
266
- addIssue(issues, [groupName], "必须是对象格式");
267
- continue;
268
- }
269
- addUnknownKeys(issues, group, new Set(Object.keys(fields)), [groupName]);
270
- for (const [fieldName, def] of Object.entries(fields)) checkConfigLeaf(issues, group[fieldName], def, [groupName, fieldName]);
271
- }
272
-
273
- return issues;
274
- }