befly 3.76.0 → 3.76.2

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 (155) 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/insert.js +2 -1
  5. package/apis/admin/select.js +2 -7
  6. package/apis/admin/update.js +3 -8
  7. package/apis/api/select.js +2 -7
  8. package/apis/auth/login.js +16 -20
  9. package/apis/dict/delete.js +1 -1
  10. package/apis/dict/detail.js +1 -3
  11. package/apis/dict/select.js +1 -2
  12. package/apis/dict/update.js +1 -1
  13. package/apis/dictType/delete.js +1 -1
  14. package/apis/dictType/detail.js +1 -1
  15. package/apis/dictType/insert.js +1 -1
  16. package/apis/dictType/select.js +2 -7
  17. package/apis/dictType/update.js +2 -2
  18. package/apis/email/config.js +4 -7
  19. package/apis/email/logList.js +2 -7
  20. package/apis/email/send.js +1 -2
  21. package/apis/email/verify.js +1 -3
  22. package/apis/loginLog/select.js +2 -7
  23. package/apis/menu/select.js +2 -7
  24. package/apis/operateLog/select.js +2 -7
  25. package/apis/role/apiSave.js +2 -2
  26. package/apis/role/menuSave.js +2 -2
  27. package/apis/role/select.js +2 -7
  28. package/apis/source/imageList.js +2 -5
  29. package/apis/tongJi/dailyReport.js +12 -13
  30. package/apis/tongJi/dailyReportSelect.js +1 -2
  31. package/apis/tongJi/dailyStats.js +1 -1
  32. package/apis/tongJi/dailyStatsDistribution.js +2 -2
  33. package/apis/tongJi/errorList.js +1 -2
  34. package/apis/tongJi/errorReport.js +1 -2
  35. package/apis/tongJi/projectDelete.js +1 -3
  36. package/apis/tongJi/projectSelect.js +2 -4
  37. package/apis/tongJi/projectUpdate.js +2 -2
  38. package/apis/upload/file.js +7 -5
  39. package/checks/api.js +43 -66
  40. package/checks/config.js +17 -12
  41. package/checks/corns.js +28 -46
  42. package/checks/field.js +18 -0
  43. package/checks/items.js +16 -60
  44. package/checks/menu.js +28 -39
  45. package/checks/table.js +106 -26
  46. package/checks/validation.js +13 -0
  47. package/configs/beflyConfig.json +6 -6
  48. package/configs/constConfig.js +2 -63
  49. package/exports.js +2 -0
  50. package/hooks/auth.js +11 -7
  51. package/hooks/parser.js +55 -59
  52. package/hooks/permission.js +5 -2
  53. package/hooks/rateLimit.js +25 -62
  54. package/hooks/validator.js +43 -31
  55. package/index.js +43 -344
  56. package/{lib → libs}/cacheHelper.js +3 -8
  57. package/libs/cron.js +60 -0
  58. package/libs/logger/index.js +1 -0
  59. package/libs/logger/logger.js +245 -0
  60. package/libs/logger/sanitize.js +115 -0
  61. package/{lib → libs/mysql}/dbHelper.js +100 -73
  62. package/{lib → libs/mysql}/dbParse.js +16 -9
  63. package/{lib → libs/mysql}/dbUtil.js +15 -32
  64. package/libs/mysql/error.js +29 -0
  65. package/libs/mysql/index.js +6 -0
  66. package/libs/mysql/is.js +29 -0
  67. package/libs/mysql/mysql.js +43 -0
  68. package/libs/mysql/sql/identifier.js +85 -0
  69. package/libs/mysql/sql/index.js +2 -0
  70. package/{lib → libs/mysql/sql}/sqlBuilder.js +2 -3
  71. package/libs/mysql/util.js +44 -0
  72. package/libs/redis/index.js +2 -0
  73. package/libs/redis/redis.js +204 -0
  74. package/libs/redis/redisError.js +8 -0
  75. package/libs/tableModel.js +72 -0
  76. package/libs/tool.js +33 -0
  77. package/libs/validator/compiler.js +248 -0
  78. package/libs/validator/index.js +10 -0
  79. package/libs/validator/parser.js +173 -0
  80. package/libs/validator/util.js +5 -0
  81. package/package.json +10 -8
  82. package/paths.js +2 -3
  83. package/plugins/cache.js +2 -1
  84. package/plugins/email.js +4 -4
  85. package/plugins/mysql.js +5 -15
  86. package/plugins/redis.js +4 -12
  87. package/router/api.js +14 -10
  88. package/router/root.js +14 -0
  89. package/router/static.js +3 -3
  90. package/schemas/api.json +99 -0
  91. package/schemas/config.json +255 -0
  92. package/schemas/corn.json +39 -0
  93. package/schemas/item.json +43 -0
  94. package/schemas/menu.json +44 -0
  95. package/schemas/table.json +80 -0
  96. package/scripts/syncDb/apply.js +87 -0
  97. package/scripts/syncDb/ddl.js +43 -0
  98. package/scripts/syncDb/index.js +175 -30
  99. package/scripts/syncDb/model.js +60 -0
  100. package/scripts/syncDb/plan.js +160 -0
  101. package/scripts/syncDb/preflight.js +98 -0
  102. package/scripts/syncDb/report.js +177 -162
  103. package/scripts/syncDb/snapshot.js +118 -0
  104. package/sync/syncUtil.js +7 -13
  105. package/tables/admin.json +51 -53
  106. package/tables/api.json +29 -40
  107. package/tables/dailyReport.json +85 -73
  108. package/tables/dict.json +27 -39
  109. package/tables/dictType.json +20 -34
  110. package/tables/emailLog.json +49 -47
  111. package/tables/errorReport.json +86 -60
  112. package/tables/file.json +47 -50
  113. package/tables/loginLog.json +68 -55
  114. package/tables/menu.json +20 -34
  115. package/tables/operateLog.json +53 -47
  116. package/tables/project.json +18 -30
  117. package/tables/role.json +33 -37
  118. package/utils/cors.js +37 -41
  119. package/utils/deepMerge.js +3 -1
  120. package/utils/error.js +1 -56
  121. package/utils/id.js +44 -0
  122. package/utils/is.js +6 -243
  123. package/utils/response.js +12 -1
  124. package/utils/scanFiles.js +13 -4
  125. package/utils/scanSources.js +58 -20
  126. package/utils/util.js +11 -163
  127. package/apis/_apis.js +0 -20
  128. package/apis/auth/sendSmsCode.js +0 -24
  129. package/apis/role/delete.js +0 -41
  130. package/apis/role/insert.js +0 -45
  131. package/apis/role/save.js +0 -36
  132. package/apis/role/update.js +0 -66
  133. package/configs/regexpAlias.json +0 -55
  134. package/lib/connect.js +0 -119
  135. package/lib/logger.js +0 -430
  136. package/lib/redisHelper.js +0 -399
  137. package/lib/validator.js +0 -274
  138. package/lib/xmlParse.js +0 -149
  139. package/plugins/config.js +0 -13
  140. package/plugins/cron.js +0 -115
  141. package/plugins/logger.js +0 -17
  142. package/plugins/tool.js +0 -131
  143. package/plugins/xmlParse.js +0 -10
  144. package/schemas/config.js +0 -67
  145. package/scripts/syncDb/context.js +0 -120
  146. package/scripts/syncDb/diff.js +0 -279
  147. package/scripts/syncDb/transform.js +0 -113
  148. package/sql/befly.sql +0 -271
  149. package/utils/email.js +0 -3
  150. package/utils/fieldClear.js +0 -22
  151. package/utils/formatValidationIssues.js +0 -28
  152. package/utils/getClientIp.js +0 -47
  153. package/utils/loggerUtils.js +0 -196
  154. package/utils/regexpUtil.js +0 -52
  155. /package/{lib → libs}/smtpText.js +0 -0
package/utils/is.js CHANGED
@@ -1,267 +1,30 @@
1
- /**
2
- * 判断值是否为 plain object(原型为 Object.prototype 或 null)。
3
- */
4
1
  export function isPlainObject(value) {
5
- if (typeof value !== "object" || value === null) {
6
- return false;
7
- }
8
-
9
- const proto = Object.getPrototypeOf(value);
10
- return proto === Object.prototype || proto === null;
11
- }
12
-
13
- /**
14
- * 判断值是否为 JSON 基础类型(string/number/boolean/null)。
15
- */
16
- export function isJsonPrimitive(value) {
17
- if (value === null) {
18
- return true;
19
- }
20
- return typeof value === "string" || isNumber(value) || isBoolean(value);
2
+ if (typeof value !== "object" || value === null) return false;
3
+ const prototype = Object.getPrototypeOf(value);
4
+ return prototype === Object.prototype || prototype === null;
21
5
  }
22
6
 
23
- /**
24
- * 判断值是否为有限数值。
25
- */
26
- export function isFiniteNumber(value) {
27
- return isNumber(value) && Number.isFinite(value);
28
- }
29
-
30
- /**
31
- * 判断值是否为字符串。
32
- */
33
7
  export function isString(value) {
34
8
  return typeof value === "string";
35
9
  }
36
10
 
37
- /**
38
- * 判断值是否为 number。
39
- */
40
11
  export function isNumber(value) {
41
- return Object.prototype.toString.call(value) === "[object Number]";
42
- }
43
-
44
- /**
45
- * 判断值是否为 boolean。
46
- */
47
- export function isBoolean(value) {
48
- return Object.prototype.toString.call(value) === "[object Boolean]";
49
- }
50
-
51
- /**
52
- * 判断值是否为 function。
53
- */
54
- export function isFunction(value) {
55
- return typeof value === "function";
56
- }
57
-
58
- /**
59
- * 判断值是否为 nullable(默认仅 null/undefined)。
60
- * 可通过 extraValues 指定更多“空值”。
61
- */
62
- export function isNullable(value, extraValues = []) {
63
- if (value === null || value === undefined) {
64
- return true;
65
- }
66
-
67
- if (!Array.isArray(extraValues) || extraValues.length === 0) {
68
- return false;
69
- }
70
-
71
- for (const item of extraValues) {
72
- if (Object.is(value, item)) {
73
- return true;
74
- }
75
- }
76
-
77
- return false;
12
+ return typeof value === "number";
78
13
  }
79
14
 
80
- /**
81
- * 判断字符串长度是否至少为指定值(trim 后)。
82
- */
83
- export function isMinLengthString(value, minLength) {
84
- if (typeof value !== "string") {
85
- return false;
86
- }
87
- if (!Number.isFinite(minLength)) {
88
- return false;
89
- }
90
- if (minLength <= 0) {
91
- return true;
92
- }
93
- return value.trim().length >= minLength;
15
+ export function isNullable(value) {
16
+ return value === null || value === undefined;
94
17
  }
95
18
 
96
- /**
97
- * 判断值是否为非空字符串。
98
- */
99
19
  export function isNonEmptyString(value) {
100
20
  return typeof value === "string" && value.trim().length > 0;
101
21
  }
102
22
 
103
- /**
104
- * 判断字符串是否允许空字符串且不允许首尾空白。
105
- */
106
- export function isNoTrimStringAllowEmpty(value) {
107
- if (typeof value !== "string") {
108
- return false;
109
- }
110
-
111
- if (value.length === 0) {
112
- return true;
113
- }
114
-
115
- const firstCharCode = value.codePointAt(0);
116
- if (isWhitespaceCharCode(firstCharCode)) {
117
- return false;
118
- }
119
-
120
- const lastCharCode = value.codePointAt(value.length - 1);
121
- if (isWhitespaceCharCode(lastCharCode)) {
122
- return false;
123
- }
124
-
125
- return true;
126
- }
127
-
128
- function isWhitespaceCharCode(code) {
129
- if (code === 9 || code === 10 || code === 11 || code === 12 || code === 13) {
130
- return true;
131
- }
132
- if (code === 32 || code === 133 || code === 160) {
133
- return true;
134
- }
135
- if (code === 5760 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279) {
136
- return true;
137
- }
138
- if (code >= 8192 && code <= 8202) {
139
- return true;
140
- }
141
- return false;
142
- }
143
-
144
- /**
145
- * 判断是否为小驼峰命名(可选以下划线开头)。
146
- */
147
- export function isLowerCamelCaseName(value) {
148
- if (!isNonEmptyString(value)) {
149
- return false;
150
- }
151
- return /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/.test(value);
152
- }
153
-
154
- /**
155
- * 判断值是否为合法端口号(1..65535)。
156
- */
157
- export function isValidPort(value) {
158
- return isFiniteNumber(value) && value >= 1 && value <= 65535;
159
- }
160
-
161
- /**
162
- * 判断值是否为合法非负整数。
163
- */
164
- export function isValidNonNegativeInt(value) {
165
- return Number.isInteger(value) && value >= 0;
166
- }
167
-
168
- /**
169
- * 判断值是否为合法正整数。
170
- */
171
23
  export function isValidPositiveInt(value) {
172
24
  return Number.isInteger(value) && value >= 1;
173
25
  }
174
26
 
175
- /**
176
- * 判断值是否为有效 IANA 时区字符串。
177
- */
178
- export function isValidTimeZone(value) {
179
- if (!isNonEmptyString(value)) return false;
180
-
181
- try {
182
- // RangeError: Invalid time zone specified
183
- new Intl.DateTimeFormat("en-US", { timeZone: value }).format(0);
184
- return true;
185
- } catch {
186
- return false;
187
- }
188
- }
189
-
190
- /**
191
- * 判断值是否为可注册的 cron 表达式(与 Bun.cron 注册行为一致:
192
- * parse 抛异常或 8 年内无匹配都视为非法,避免启动后才报错)。
193
- */
194
- export function isValidCronSchedule(value) {
195
- if (!isString(value)) return false;
196
-
197
- try {
198
- return Bun.cron.parse(value) !== null;
199
- } catch {
200
- return false;
201
- }
202
- }
203
-
204
- /**
205
- * 判断值是否为有限整数。
206
- */
207
- export function isIntegerNumber(value) {
208
- return isNumber(value) && Number.isInteger(value) && Number.isFinite(value);
209
- }
210
-
211
- /**
212
- * 判断字符串是否为正则字面量(/source/flags)。
213
- */
214
- export function isRegexLiteral(input) {
215
- const trimmed = String(input || "").trim();
216
- if (!isMinLengthString(trimmed, 2)) {
217
- return false;
218
- }
219
- if (!trimmed.startsWith("/")) {
220
- return false;
221
- }
222
-
223
- const lastSlash = trimmed.lastIndexOf("/");
224
- if (lastSlash <= 0) {
225
- return false;
226
- }
227
-
228
- const flags = trimmed.slice(lastSlash + 1);
229
- if (flags !== "" && !/^[gimsuy]+$/.test(flags)) {
230
- return false;
231
- }
232
-
233
- return true;
234
- }
235
-
236
- /**
237
- * 判断输入是否为正则规则(字面量或 @ 别名)。
238
- */
239
- export function isRegexInput(input) {
240
- const trimmed = String(input || "").trim();
241
- // 约束:正则 input 仅允许 /source/flags 形式;同时保留 @alias 作为正则别名。
242
- // 说明:不再支持直接写 source(如 ^\d+$),避免与普通字符串/枚举产生歧义。
243
- return isRegexLiteral(trimmed) || trimmed.startsWith("@");
244
- }
245
-
246
- /**
247
- * 判断当前进程是否为主进程。
248
- * bm2 的实例 0 是主进程;未由 bm2 启动时视为主进程。
249
- */
250
27
  export function isPrimaryProcess() {
251
28
  const instance = Bun.env.BM2_APP_INSTANCE;
252
29
  return instance === "0" || instance === 0 || instance === undefined;
253
30
  }
254
-
255
- /**
256
- * 日志敏感键判定
257
- */
258
- export function isSensitiveKey(key, matcher) {
259
- const lower = String(key).toLowerCase();
260
- if (matcher.exactSet.has(lower)) return true;
261
-
262
- for (const part of matcher.contains) {
263
- if (lower.includes(part)) return true;
264
- }
265
-
266
- return false;
267
- }
package/utils/response.js CHANGED
@@ -1,6 +1,16 @@
1
- import { Logger } from "../lib/logger.js";
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+
2
3
  import { isString } from "./is.js";
3
4
 
5
+ const errorStatus = {
6
+ parser: 400,
7
+ validator: 400,
8
+ auth: 401,
9
+ permission: 403,
10
+ method: 405,
11
+ rateLimit: 429
12
+ };
13
+
4
14
  /**
5
15
  * 创建错误响应(专用于 Hook 中间件)
6
16
  * 在钩子中提前拦截请求时使用
@@ -39,6 +49,7 @@ export function ErrorResponse(ctx, msg, code = 1, data = null, detail = null, re
39
49
  reason: reason
40
50
  },
41
51
  {
52
+ status: errorStatus[reason] || 400,
42
53
  headers: ctx.corsHeaders
43
54
  }
44
55
  );
@@ -1,11 +1,17 @@
1
1
  import { existsSync } from "node:fs";
2
-
3
- import { relative, normalize, parse, join } from "pathe";
2
+ import { join, normalize, parse, relative } from "node:path";
4
3
 
5
4
  import { createError } from "./error.js";
6
5
  import { importDefault } from "./importDefault.js";
7
6
  import { isPlainObject } from "./is.js";
8
7
 
8
+ const selectFields = {
9
+ page: { name: "页码", paramType: "integer", minValue: 1, maxValue: 10000, paramDefault: 1 },
10
+ limit: { name: "每页数量", paramType: "integer", minValue: 1, maxValue: 100, paramDefault: 20 },
11
+ keyword: { name: "关键字", paramType: "string", minValue: 0, maxValue: 100, paramDefault: "" },
12
+ state: { name: "状态", paramType: "integer" }
13
+ };
14
+
9
15
  /**
10
16
  * 扫描指定目录下的文件
11
17
  * @param dir 目录路径
@@ -28,7 +34,6 @@ export async function scanFiles(dir, source, type, pattern) {
28
34
  });
29
35
 
30
36
  for await (const file of files) {
31
- // 使用 pathe.normalize 统一路径分隔符为 /
32
37
  const filePath = normalize(file);
33
38
  const parsedFile = parse(filePath);
34
39
 
@@ -37,7 +42,7 @@ export async function scanFiles(dir, source, type, pattern) {
37
42
 
38
43
  // 计算相对路径(去除扩展名)
39
44
  const parsedRelativePath = parse(relative(dir, filePath));
40
- const relativePath = parsedRelativePath.dir ? join(parsedRelativePath.dir, parsedRelativePath.name) : parsedRelativePath.name;
45
+ const relativePath = (parsedRelativePath.dir ? join(parsedRelativePath.dir, parsedRelativePath.name) : parsedRelativePath.name).replaceAll("\\", "/");
41
46
 
42
47
  // 固定默认过滤(不可关闭):忽略下划线开头的文件/目录
43
48
  if (relativePath.split("/").some((part) => part.startsWith("_"))) continue;
@@ -64,6 +69,10 @@ export async function scanFiles(dir, source, type, pattern) {
64
69
  base[key] = value;
65
70
  }
66
71
  }
72
+ if (type === "api") {
73
+ base.category ||= "other";
74
+ if (base.category === "select") base.fields = { ...selectFields, ...base.fields };
75
+ }
67
76
  results.push(base);
68
77
  }
69
78
  return results;
@@ -1,3 +1,4 @@
1
+ import { buildTableModel } from "../libs/tableModel.js";
1
2
  import {
2
3
  //
3
4
  coreTableDir,
@@ -14,23 +15,63 @@ import {
14
15
  import { createError } from "./error.js";
15
16
  import { scanFiles } from "./scanFiles.js";
16
17
 
17
- export const scanSources = async () => {
18
+ export function assertUnique(items, key, label) {
19
+ const values = new Map();
20
+ for (const item of items) {
21
+ const value = item[key];
22
+ if (!values.has(value)) {
23
+ values.set(value, item);
24
+ continue;
25
+ }
26
+ const first = values.get(value);
27
+ throw createError(`${label}冲突:${value},${first.filePath} 与 ${item.filePath} 不允许同时存在`, {
28
+ code: "policy",
29
+ subsystem: "scan",
30
+ operation: "unique",
31
+ key: key,
32
+ value: value
33
+ });
34
+ }
35
+ }
36
+
37
+ export function assertAppApiNamespace(item) {
38
+ if (item.relativePath.split("/")[0] !== "core") return;
39
+ throw createError(`app 接口目录下禁止存在 core 一级目录:${item.filePath}`, {
40
+ code: "policy",
41
+ subsystem: "scan",
42
+ operation: "scanSources"
43
+ });
44
+ }
45
+
46
+ function addTable(tables, table) {
47
+ if (tables[table.codeName]) {
48
+ throw createError(`Table 冲突:${table.codeName},${tables[table.codeName].fileName} 与 ${table.fileName} 不允许同时存在`, {
49
+ code: "policy",
50
+ subsystem: "scan",
51
+ operation: "unique",
52
+ key: "codeName",
53
+ value: table.codeName
54
+ });
55
+ }
56
+ tables[table.codeName] = table;
57
+ }
58
+
59
+ export async function scanTables({ beflyMode = "auto" } = {}) {
60
+ const tables = {};
61
+ const allCoreTables = await scanFiles(coreTableDir, "core", "table", "*.json");
62
+ for (const item of allCoreTables) addTable(tables, buildTableModel(item.fieldsDef, { source: "core", fileName: item.fileName, beflyMode: beflyMode }));
63
+ const allAppTables = await scanFiles(appTableDir, "app", "table", "*.json");
64
+ for (const item of allAppTables) addTable(tables, buildTableModel(item.fieldsDef, { source: "app", fileName: item.fileName, beflyMode: beflyMode }));
65
+ return tables;
66
+ }
67
+
68
+ export const scanSources = async ({ beflyMode = "auto" } = {}) => {
18
69
  const apis = [];
19
70
  const plugins = [];
20
71
  const hooks = [];
21
- const tables = {};
72
+ const tables = await scanTables({ beflyMode: beflyMode });
22
73
  const corns = [];
23
74
 
24
- // 处理表格
25
- const allCoreTables = await scanFiles(coreTableDir, "core", "table", "*.json");
26
- for (const item of allCoreTables) {
27
- tables[`befly${item.fileName.charAt(0).toUpperCase()}${item.fileName.slice(1)}`] = item.fieldsDef;
28
- }
29
- const allAppTables = await scanFiles(appTableDir, "app", "table", "*.json");
30
- for (const item of allAppTables) {
31
- tables[item.fileName] = item.fieldsDef;
32
- }
33
-
34
75
  // 处理插件
35
76
  const allCorePlugins = await scanFiles(corePluginDir, "core", "plugin", "*.js");
36
77
  for (const item of allCorePlugins) {
@@ -68,17 +109,14 @@ export const scanSources = async () => {
68
109
  }
69
110
  const allAppApis = await scanFiles(appApiDir, "app", "api", "**/*.js");
70
111
  for (const item of allAppApis) {
71
- // 禁止 app 接口目录下的 core 一级目录:会与框架内置 /api/core 命名空间冲突,从源头保证 apiPath 唯一
72
- if (item.relativePath.split("/")[0] === "core") {
73
- throw createError(`app 接口目录下禁止存在 core 一级目录:${item.filePath}`, {
74
- code: "policy",
75
- subsystem: "scan",
76
- operation: "scanSources"
77
- });
78
- }
112
+ assertAppApiNamespace(item);
79
113
  apis.push(item);
80
114
  }
81
115
 
116
+ assertUnique(plugins, "fileName", "Plugin 文件名");
117
+ assertUnique(hooks, "fileName", "Hook 文件名");
118
+ assertUnique(corns, "fileName", "Cron 文件名");
119
+
82
120
  return {
83
121
  hooks: hooks,
84
122
  plugins: plugins,
package/utils/util.js CHANGED
@@ -1,197 +1,45 @@
1
- /**
2
- * 注意:本文件用于集中维护 core 内部通用 utils。
3
- * - 按项目规范:避免零散小文件噪音;实现集中在本文件,而不是做 re-export 聚合。
4
- * - 仅在 packages/core 内部使用;core 对外不承诺这些路径导出。
5
- */
6
-
7
1
  import { createError } from "./error.js";
8
- import { isPlainObject } from "./is.js";
9
2
 
10
- /**
11
- * 通用等待工具:轮询直到条件满足或超时(while + sleep 实现,无定时器句柄,无泄漏风险)
12
- * @param {string} label - 等待说明(用于超时错误信息)
13
- * @param {Function} check - 异步条件函数,返回 true 表示就绪
14
- * @param {number} [intervalMs=200] - 轮询间隔(毫秒)
15
- * @param {number} [timeoutMs=60000] - 超时时间(毫秒)
16
- */
17
3
  export async function waitFor({ label, check, intervalMs = 200, timeoutMs = 60000 }) {
18
4
  const deadline = Date.now() + timeoutMs;
19
5
  while (Date.now() < deadline) {
20
- if (await check()) {
21
- return;
22
- }
6
+ if (await check()) return;
23
7
  await Bun.sleep(intervalMs);
24
8
  }
25
9
  throw createError(`等待${label}超时`, { code: "runtime", subsystem: "start", operation: "waitFor" });
26
10
  }
27
11
 
28
- /**
29
- * 判断 bigint 是否可安全转换为 number。
30
- * - 超出安全整数范围则不可转换
31
- * - 转换后若是科学计数法表示则不可转换
32
- */
33
- export function canConvertToNumber(value) {
34
- const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
35
- const minSafe = BigInt(Number.MIN_SAFE_INTEGER);
36
-
37
- if (value > maxSafe || value < minSafe) {
38
- return null;
39
- }
40
-
41
- const asNumber = Number(value);
42
- if (!Number.isSafeInteger(asNumber)) {
43
- return null;
44
- }
45
-
46
- const text = String(asNumber);
47
- if (text.includes("e") || text.includes("E")) {
48
- return null;
49
- }
50
-
51
- return asNumber;
52
- }
53
-
54
- /**
55
- * 获取值的类型标识(数组/对象等做细分)。
56
- */
57
12
  export function getTypeTag(value) {
58
13
  if (value === null) return "null";
59
14
  if (value === undefined) return "undefined";
60
15
  if (Array.isArray(value)) return "array";
61
-
62
- const t = typeof value;
63
- if (t === "string") return "string";
64
- if (t === "number") return "number";
65
- if (t === "boolean") return "boolean";
66
- if (t === "bigint") return "bigint";
67
- if (t === "symbol") return "symbol";
68
- if (t === "function") return "function";
69
-
70
- return "object";
71
- }
72
-
73
- /**
74
- * 归一化正整数:非法值回退,超出范围裁剪。
75
- */
76
- export function normalizePositiveInt(value, fallback, min, max) {
77
- if (!Number.isFinite(value)) return fallback;
78
- const v = Math.floor(value);
79
- if (v < min) return fallback;
80
- if (v > max) return max;
81
- return v;
16
+ return typeof value;
82
17
  }
83
18
 
84
- // 将字符串拆分为词段(用于 camelCase/snakeCase)
85
19
  function toWordParts(input) {
86
- const normalized = String(input)
20
+ return String(input)
87
21
  .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
88
22
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
89
23
  .replace(/[^a-zA-Z0-9]+/g, " ")
90
- .trim();
91
-
92
- if (normalized.length === 0) {
93
- return [];
94
- }
95
-
96
- return normalized.split(/\s+/).filter((p) => p.length > 0);
97
- }
98
-
99
- // 首字母大写
100
- function upperFirst(s) {
101
- if (s.length === 0) {
102
- return s;
103
- }
104
- return s.charAt(0).toUpperCase() + s.slice(1);
24
+ .trim()
25
+ .split(/\s+/)
26
+ .filter(Boolean);
105
27
  }
106
28
 
107
- /**
108
- * 把字符串转为小驼峰。
109
- * - 主要用于文件名/目录名(例如 my_plugin / my-plugin / my plugin)。
110
- */
111
- export function camelCase(input) {
112
- const parts = toWordParts(input);
113
- if (parts.length === 0) {
114
- return "";
115
- }
116
-
117
- const firstPart = parts[0];
118
- if (!firstPart) {
119
- return "";
120
- }
121
-
122
- const first = firstPart.toLowerCase();
123
- const rest = parts.slice(1).map((p) => upperFirst(p.toLowerCase()));
124
- return [first].concat(rest).join("");
125
- }
126
-
127
- /**
128
- * 把字符串转为 snake_case。
129
- * - 主要用于表名/字段名(例如 userId -> user_id)。
130
- */
131
29
  export function snakeCase(input) {
132
- const parts = toWordParts(input);
133
- if (parts.length === 0) {
134
- return "";
135
- }
136
-
137
- return parts.map((p) => p.toLowerCase()).join("_");
30
+ return toWordParts(input)
31
+ .map((part) => part.toLowerCase())
32
+ .join("_");
138
33
  }
139
34
 
140
- /**
141
- * 对象字段名转下划线
142
- * @param obj - 源对象
143
- * @returns 字段名转为下划线格式的新对象
144
- */
145
- export const keysToSnake = (obj) => {
146
- if (!obj || !isPlainObject(obj)) return obj;
147
-
148
- const result = {};
149
- for (const [key, value] of Object.entries(obj)) {
150
- const snakeKey = snakeCase(key);
151
- result[snakeKey] = value;
152
- }
153
- return result;
154
- };
155
-
156
- /**
157
- * 获取运行模式(开发/正式的统一判断,仅以 NODE_ENV 为准)
158
- * - 线上部署(bm2 等)注入 NODE_ENV=production
159
- * - 本地开发未设置 NODE_ENV,默认 "development"
160
- */
161
35
  export function getRunMode() {
162
36
  return Bun.env.NODE_ENV || "development";
163
37
  }
164
38
 
165
- /**
166
- * 将 session 有效天数转换为秒。
167
- */
168
39
  export function toSessionTtlSeconds(ttlDays) {
169
- if (typeof ttlDays === "number" && Number.isFinite(ttlDays) && ttlDays > 0) {
170
- return Math.floor(ttlDays * 24 * 60 * 60);
171
- }
172
-
173
- if (typeof ttlDays === "string") {
174
- const parsed = Number(ttlDays.trim());
175
- if (Number.isFinite(parsed) && parsed > 0) {
176
- return Math.floor(parsed * 24 * 60 * 60);
177
- }
178
- }
179
-
180
- return 7 * 24 * 60 * 60;
40
+ return ttlDays * 24 * 60 * 60;
181
41
  }
182
42
 
183
- /**
184
- * 生成短 ID
185
- * 由时间戳(base36)+ 随机字符组成,约 13 位
186
- * - 前 8 位:时间戳(可排序)
187
- * - 后 5 位:随机字符(防冲突)
188
- * @returns 短 ID 字符串
189
- */
190
43
  export function genShortId() {
191
- const timePart = Date.now().toString(36);
192
- let rand = "";
193
- while (rand.length < 10) {
194
- rand = rand + Math.random().toString(36).slice(2);
195
- }
196
- return timePart + rand.slice(0, 10);
44
+ return `${Date.now().toString(36)}${crypto.randomUUID().replaceAll("-", "").slice(0, 10)}`;
197
45
  }
package/apis/_apis.js DELETED
@@ -1,20 +0,0 @@
1
- export const queryFields = {
2
- page: {
3
- name: "页码",
4
- input: "integer",
5
- min: 1,
6
- max: 10000
7
- },
8
- limit: {
9
- name: "每页数量",
10
- input: "integer",
11
- min: 1,
12
- max: 100
13
- },
14
- keyword: {
15
- name: "关键字",
16
- input: "string",
17
- min: 0,
18
- max: 100
19
- }
20
- };
@@ -1,24 +0,0 @@
1
- import adminTable from "#befly/tables/admin.json";
2
-
3
- export default {
4
- name: "发送短信验证码",
5
- method: "POST",
6
- body: "none",
7
- auth: false,
8
- fields: {
9
- phone: adminTable.phone
10
- },
11
- required: ["phone"],
12
- handler: async (befly, ctx) => {
13
- const code = Math.floor(100000 + Math.random() * 900000).toString();
14
-
15
- if (befly.redis) {
16
- const key = `sms_code:${ctx.body.phone}`;
17
- await befly.redis.setString(key, code, 300);
18
- }
19
-
20
- const isDev = befly.config ? befly.config.runMode === "development" : false;
21
-
22
- return befly.tool.Yes("验证码已发送", isDev ? { code: code } : null);
23
- }
24
- };