befly 3.18.23 → 3.19.5

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.
@@ -0,0 +1,380 @@
1
+ import { addDays, getDateYmdNumber, getTimeBucketStart } from "../../utils/datetime.js";
2
+ import { VISIT_STATS_BUCKET_MS, VISIT_STATS_REDIS_TTL_SECONDS, VISIT_STATS_UA_FIELDS, getVisitStatsBucketKey, getVisitStatsBucketProductPvKey, getVisitStatsBucketProductUvKey, getVisitStatsDayKey, getVisitStatsDayProductKeysKey, getVisitStatsDayProductPvKey, getVisitStatsDayProductUvKey, getVisitStatsDayUaCountKey, getVisitStatsDayUaValuesKey, getVisitStatsProductMetaKey, normalizeVisitStatsUaValue, parseVisitStatsProductKey, toVisitStatsNumber } from "../../utils/visitStats.js";
3
+
4
+ const VISIT_STATS_DAY_LIMIT = 30;
5
+
6
+ async function ensureVisitStatsTableReady(befly) {
7
+ if (befly.visitStatsTableReady !== undefined) {
8
+ return befly.visitStatsTableReady;
9
+ }
10
+
11
+ const tableExistsResult = await befly.mysql.tableExists("beflyVisitStats");
12
+ befly.visitStatsTableReady = tableExistsResult.data === true;
13
+ return befly.visitStatsTableReady;
14
+ }
15
+
16
+ async function flushVisitStatsBucket(befly, bucketTime) {
17
+ if (bucketTime <= 0) {
18
+ return;
19
+ }
20
+
21
+ const currentBucketTime = getTimeBucketStart(Date.now(), VISIT_STATS_BUCKET_MS);
22
+ if (bucketTime >= currentBucketTime) {
23
+ return;
24
+ }
25
+
26
+ const tableReady = await ensureVisitStatsTableReady(befly);
27
+ if (!tableReady) {
28
+ return;
29
+ }
30
+
31
+ const flushedKey = `visitStats:flushed:${bucketTime}`;
32
+ const flushed = await befly.redis.exists(flushedKey);
33
+ if (flushed) {
34
+ return;
35
+ }
36
+
37
+ const bucketDate = getDateYmdNumber(bucketTime);
38
+ const bucketKey = getVisitStatsBucketKey(bucketTime);
39
+ const pv = toVisitStatsNumber(await befly.redis.getString(`${bucketKey}:pv`));
40
+ const uv = await befly.redis.scard(`${bucketKey}:uv`);
41
+ const errorCount = toVisitStatsNumber(await befly.redis.getString(`${bucketKey}:errorCount`));
42
+ const durationSum = toVisitStatsNumber(await befly.redis.getString(`${bucketKey}:durationSum`));
43
+ const durationCount = toVisitStatsNumber(await befly.redis.getString(`${bucketKey}:durationCount`));
44
+ const avgDuration = durationCount > 0 ? Math.round(durationSum / durationCount) : 0;
45
+
46
+ if (pv <= 0 && uv <= 0 && errorCount <= 0 && durationCount <= 0) {
47
+ await befly.redis.setString(flushedKey, "1", VISIT_STATS_REDIS_TTL_SECONDS);
48
+ return;
49
+ }
50
+
51
+ const data = {
52
+ bucketTime: bucketTime,
53
+ bucketDate: bucketDate,
54
+ pv: pv,
55
+ uv: uv,
56
+ errorCount: errorCount,
57
+ durationSum: durationSum,
58
+ durationCount: durationCount,
59
+ avgDuration: avgDuration
60
+ };
61
+
62
+ const detail = await befly.mysql.getOne({
63
+ table: "beflyVisitStats",
64
+ where: { bucketTime: bucketTime }
65
+ });
66
+
67
+ if (detail.data?.id) {
68
+ await befly.mysql.updData({
69
+ table: "beflyVisitStats",
70
+ data: data,
71
+ where: { bucketTime: bucketTime }
72
+ });
73
+ } else {
74
+ await befly.mysql.insData({
75
+ table: "beflyVisitStats",
76
+ data: data
77
+ });
78
+ }
79
+
80
+ await befly.redis.setString(flushedKey, "1", VISIT_STATS_REDIS_TTL_SECONDS);
81
+ }
82
+
83
+ async function flushVisitStatsPreviousBucket(befly, now = Date.now()) {
84
+ const currentBucketTime = getTimeBucketStart(now, VISIT_STATS_BUCKET_MS);
85
+ await flushVisitStatsBucket(befly, currentBucketTime - VISIT_STATS_BUCKET_MS);
86
+ }
87
+
88
+ async function getVisitStatsOnlineCount(befly) {
89
+ const members = await befly.redis.smembers("visitStats:online:visitors");
90
+ if (members.length === 0) {
91
+ return 0;
92
+ }
93
+
94
+ const existsList = await befly.redis.existsBatch(members.map((member) => `visitStats:online:visitor:${member}`));
95
+ const expiredMembers = [];
96
+ let onlineCount = 0;
97
+
98
+ for (let i = 0; i < members.length; i++) {
99
+ if (existsList[i]) {
100
+ onlineCount += 1;
101
+ continue;
102
+ }
103
+ expiredMembers.push(members[i]);
104
+ }
105
+
106
+ if (expiredMembers.length > 0) {
107
+ await befly.redis.srem("visitStats:online:visitors", expiredMembers);
108
+ }
109
+
110
+ return onlineCount;
111
+ }
112
+
113
+ async function getVisitStatsUaFieldList(befly, bucketDate, field, defaultValue) {
114
+ const valuesKey = getVisitStatsDayUaValuesKey(bucketDate, field);
115
+ const values = await befly.redis.smembers(valuesKey);
116
+ const list = [];
117
+
118
+ for (const rawValue of values) {
119
+ const value = normalizeVisitStatsUaValue(rawValue, defaultValue);
120
+ const count = toVisitStatsNumber(await befly.redis.getString(getVisitStatsDayUaCountKey(bucketDate, field, value)));
121
+ if (count <= 0) {
122
+ continue;
123
+ }
124
+
125
+ list.push({
126
+ name: value,
127
+ count: count
128
+ });
129
+ }
130
+
131
+ list.sort((a, b) => {
132
+ if (b.count !== a.count) {
133
+ return b.count - a.count;
134
+ }
135
+
136
+ return String(a.name).localeCompare(String(b.name));
137
+ });
138
+
139
+ return list.slice(0, 10);
140
+ }
141
+
142
+ async function getVisitStatsUaStats(befly, bucketDate) {
143
+ const data = {};
144
+
145
+ for (const item of VISIT_STATS_UA_FIELDS) {
146
+ const field = item.field;
147
+ const defaultValue = item.defaultValue;
148
+ data[field] = await getVisitStatsUaFieldList(befly, bucketDate, field, defaultValue);
149
+ }
150
+
151
+ return {
152
+ deviceTypes: data.deviceType || [],
153
+ browsers: data.browserName || [],
154
+ browserVersions: data.browserVersion || [],
155
+ osList: data.osName || [],
156
+ osVersions: data.osVersion || [],
157
+ deviceVendors: data.deviceVendor || [],
158
+ deviceModels: data.deviceModel || [],
159
+ engines: data.engineName || [],
160
+ cpuArchitectures: data.cpuArchitecture || []
161
+ };
162
+ }
163
+
164
+ function getVisitStatsRecentDayList(now, limit = VISIT_STATS_DAY_LIMIT) {
165
+ const list = [];
166
+
167
+ for (let i = limit - 1; i >= 0; i--) {
168
+ list.push(getDateYmdNumber(addDays(now, -i)));
169
+ }
170
+
171
+ return list;
172
+ }
173
+
174
+ function getVisitStatsBucketList(bucketDate, currentBucketTime) {
175
+ const list = [];
176
+ let startBucketTime = currentBucketTime;
177
+
178
+ while (getDateYmdNumber(startBucketTime - VISIT_STATS_BUCKET_MS) === bucketDate) {
179
+ startBucketTime -= VISIT_STATS_BUCKET_MS;
180
+ }
181
+
182
+ for (let bucketTime = startBucketTime; bucketTime <= currentBucketTime; bucketTime += VISIT_STATS_BUCKET_MS) {
183
+ list.push(bucketTime);
184
+ }
185
+
186
+ return list;
187
+ }
188
+
189
+ async function getVisitStatsProductMeta(befly, productKey) {
190
+ const metaText = await befly.redis.getString(getVisitStatsProductMetaKey(productKey));
191
+
192
+ if (!metaText) {
193
+ return parseVisitStatsProductKey(productKey);
194
+ }
195
+
196
+ try {
197
+ const meta = JSON.parse(metaText);
198
+ return {
199
+ ...parseVisitStatsProductKey(productKey),
200
+ ...meta
201
+ };
202
+ } catch (_error) {
203
+ return parseVisitStatsProductKey(productKey);
204
+ }
205
+ }
206
+
207
+ async function getVisitStatsProducts(befly, now, bucketDate, currentBucketTime) {
208
+ const recentDayList = getVisitStatsRecentDayList(now);
209
+ const productKeySet = new Set();
210
+
211
+ for (const itemBucketDate of recentDayList) {
212
+ const productKeys = await befly.redis.smembers(getVisitStatsDayProductKeysKey(itemBucketDate));
213
+
214
+ for (const productKey of productKeys) {
215
+ if (productKey) {
216
+ productKeySet.add(productKey);
217
+ }
218
+ }
219
+ }
220
+
221
+ const bucketList = getVisitStatsBucketList(bucketDate, currentBucketTime);
222
+ const products = [];
223
+
224
+ for (const productKey of productKeySet) {
225
+ const meta = await getVisitStatsProductMeta(befly, productKey);
226
+ const trend = [];
227
+ const days = [];
228
+ let totalPv = 0;
229
+ let totalUv = 0;
230
+
231
+ for (const itemBucketTime of bucketList) {
232
+ trend.push({
233
+ bucketTime: itemBucketTime,
234
+ bucketDate: bucketDate,
235
+ pv: toVisitStatsNumber(await befly.redis.getString(getVisitStatsBucketProductPvKey(itemBucketTime, productKey))),
236
+ uv: await befly.redis.scard(getVisitStatsBucketProductUvKey(itemBucketTime, productKey))
237
+ });
238
+ }
239
+
240
+ for (const itemBucketDate of recentDayList) {
241
+ const pv = toVisitStatsNumber(await befly.redis.getString(getVisitStatsDayProductPvKey(itemBucketDate, productKey)));
242
+ const uv = await befly.redis.scard(getVisitStatsDayProductUvKey(itemBucketDate, productKey));
243
+
244
+ totalPv += pv;
245
+ totalUv += uv;
246
+ days.push({
247
+ bucketDate: itemBucketDate,
248
+ pv: pv,
249
+ uv: uv
250
+ });
251
+ }
252
+
253
+ products.push({
254
+ key: productKey,
255
+ productName: String(meta.productName || "Unknown"),
256
+ productCode: String(meta.productCode || "unknown"),
257
+ productVersion: String(meta.productVersion || "Unknown"),
258
+ today: days[days.length - 1] || {
259
+ bucketDate: bucketDate,
260
+ pv: 0,
261
+ uv: 0
262
+ },
263
+ trend: trend,
264
+ days: days,
265
+ totalPv: totalPv,
266
+ totalUv: totalUv
267
+ });
268
+ }
269
+
270
+ products.sort((a, b) => {
271
+ if (b.today.pv !== a.today.pv) {
272
+ return b.today.pv - a.today.pv;
273
+ }
274
+
275
+ if (b.totalPv !== a.totalPv) {
276
+ return b.totalPv - a.totalPv;
277
+ }
278
+
279
+ return `${a.productCode}|${a.productVersion}`.localeCompare(`${b.productCode}|${b.productVersion}`);
280
+ });
281
+
282
+ return products;
283
+ }
284
+
285
+ export default {
286
+ name: "获取访问统计",
287
+ method: "POST",
288
+ body: "none",
289
+ auth: true,
290
+ fields: {},
291
+ required: [],
292
+ handler: async (befly) => {
293
+ const now = Date.now();
294
+ await flushVisitStatsPreviousBucket(befly, now);
295
+ const tableReady = await ensureVisitStatsTableReady(befly);
296
+
297
+ const bucketTime = getTimeBucketStart(now, VISIT_STATS_BUCKET_MS);
298
+ const bucketDate = getDateYmdNumber(now);
299
+ const bucketKey = getVisitStatsBucketKey(bucketTime);
300
+ const dayKey = getVisitStatsDayKey(bucketDate);
301
+
302
+ const todayPv = toVisitStatsNumber(await befly.redis.getString(`${dayKey}:pv`));
303
+ const todayUv = await befly.redis.scard(`${dayKey}:uv`);
304
+ const trend = [];
305
+ const currentPv = toVisitStatsNumber(await befly.redis.getString(`${bucketKey}:pv`));
306
+ const currentUv = await befly.redis.scard(`${bucketKey}:uv`);
307
+
308
+ if (tableReady) {
309
+ const trendResult = await befly.mysql.getAll({
310
+ table: "beflyVisitStats",
311
+ where: { bucketDate: bucketDate },
312
+ orderBy: ["bucketTime#ASC"]
313
+ });
314
+
315
+ if (Array.isArray(trendResult.data?.lists)) {
316
+ trend.push(...trendResult.data.lists);
317
+ }
318
+ }
319
+
320
+ if (currentPv > 0 || currentUv > 0) {
321
+ trend.push({
322
+ bucketTime: bucketTime,
323
+ bucketDate: bucketDate,
324
+ pv: currentPv,
325
+ uv: currentUv,
326
+ errorCount: 0,
327
+ durationSum: 0,
328
+ durationCount: 0,
329
+ avgDuration: 0
330
+ });
331
+ }
332
+
333
+ const daysMap = new Map();
334
+
335
+ if (tableReady) {
336
+ const daysSqlRes = await befly.mysql.execute(`SELECT bucket_date as bucketDate, SUM(pv) as pv, SUM(uv) as uv FROM befly_visit_stats WHERE state = 1 GROUP BY bucket_date ORDER BY bucket_date DESC LIMIT ${VISIT_STATS_DAY_LIMIT}`, []);
337
+
338
+ for (const item of daysSqlRes.data || []) {
339
+ const itemBucketDate = toVisitStatsNumber(item.bucketDate);
340
+ daysMap.set(itemBucketDate, {
341
+ bucketDate: itemBucketDate,
342
+ pv: toVisitStatsNumber(item.pv),
343
+ uv: toVisitStatsNumber(item.uv)
344
+ });
345
+ }
346
+ }
347
+
348
+ daysMap.set(bucketDate, {
349
+ bucketDate: bucketDate,
350
+ pv: todayPv,
351
+ uv: todayUv
352
+ });
353
+
354
+ const days = [];
355
+ for (const item of daysMap.values()) {
356
+ days.push(item);
357
+ }
358
+
359
+ days.sort((a, b) => a.bucketDate - b.bucketDate);
360
+ if (days.length > VISIT_STATS_DAY_LIMIT) {
361
+ days.splice(0, days.length - VISIT_STATS_DAY_LIMIT);
362
+ }
363
+
364
+ const onlineCount = await getVisitStatsOnlineCount(befly);
365
+ const uaStats = await getVisitStatsUaStats(befly, bucketDate);
366
+ const products = await getVisitStatsProducts(befly, now, bucketDate, bucketTime);
367
+ return befly.tool.Yes("获取成功", {
368
+ onlineCount: onlineCount,
369
+ today: {
370
+ bucketDate: bucketDate,
371
+ pv: todayPv,
372
+ uv: todayUv
373
+ },
374
+ trend: trend,
375
+ days: days,
376
+ uaStats: uaStats,
377
+ products: products
378
+ });
379
+ }
380
+ };
package/checks/api.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as z from "zod";
2
2
 
3
+ import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
3
4
  import { Logger } from "../lib/logger.js";
4
5
  import { formatZodIssues } from "../utils/formatZodIssues.js";
5
- import { isNoTrimStringAllowEmpty } from "../utils/is.js";
6
+ import { isNoTrimStringAllowEmpty, isNullable, isRegexInput } from "../utils/is.js";
6
7
 
7
8
  z.config(z.locales.zhCN());
8
9
 
@@ -10,14 +11,69 @@ const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首
10
11
  const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
11
12
  const apiPathRegex = /^\/api\/(?:core\/[a-z][a-zA-Z0-9]*(?:\/[a-z][a-zA-Z0-9]*)*|(?!app(?:\/|$))[a-z][a-zA-Z0-9]*(?:\/[a-z][a-zA-Z0-9]*)*)$/;
12
13
 
14
+ function addIssue(context, path, message) {
15
+ context.addIssue({
16
+ path: path,
17
+ message: message
18
+ });
19
+ }
20
+
13
21
  const fieldSchema = z
14
22
  .object({
15
23
  name: noTrimString.min(1),
16
- input: noTrimString.min(1),
24
+ input: z.enum(FIELD_RULE_INPUT_TYPES),
25
+ check: noTrimString.optional(),
17
26
  min: z.number().nullable().optional(),
18
- max: z.number().nullable().optional()
27
+ max: z.number().nullable().optional(),
28
+ detail: z.string().optional()
19
29
  })
20
- .strict();
30
+ .strict()
31
+ .superRefine((value, context) => {
32
+ const min = isNullable(value.min) ? FIELD_RULE_DEFAULT_MIN : value.min;
33
+ const max = isNullable(value.max) ? FIELD_RULE_DEFAULT_MAX : value.max;
34
+ if (min > max) {
35
+ addIssue(context, ["min"], "min 不能大于 max");
36
+ }
37
+
38
+ const checkValue = value.check;
39
+ const input = value.input;
40
+
41
+ if ((input === "enum" || input === "enumNumber" || input === "enumInteger" || input === "regexp") && (!checkValue || checkValue.length === 0)) {
42
+ addIssue(context, ["check"], `${input} 必须提供 check`);
43
+ return;
44
+ }
45
+
46
+ if (input === "regexp" && checkValue && !isRegexInput(checkValue)) {
47
+ addIssue(context, ["check"], "regexp.check 必须是正则字面量或 @ 别名");
48
+ return;
49
+ }
50
+
51
+ if ((input === "enum" || input === "enumNumber" || input === "enumInteger") && checkValue) {
52
+ const items = String(checkValue)
53
+ .split("|")
54
+ .map((item) => item.trim())
55
+ .filter((item) => item.length > 0);
56
+
57
+ if (items.length === 0) {
58
+ addIssue(context, ["check"], `${input}.check 必须是非空枚举值列表`);
59
+ return;
60
+ }
61
+
62
+ if (input === "enumNumber" || input === "enumInteger") {
63
+ for (const item of items) {
64
+ const num = Number(item);
65
+ if (!Number.isFinite(num)) {
66
+ addIssue(context, ["check"], `${input}.check 存在非法数字值: ${item}`);
67
+ return;
68
+ }
69
+ if (input === "enumInteger" && !Number.isInteger(num)) {
70
+ addIssue(context, ["check"], `${input}.check 存在非法整数值: ${item}`);
71
+ return;
72
+ }
73
+ }
74
+ }
75
+ }
76
+ });
21
77
 
22
78
  const fieldsSchema = z.record(z.string().regex(lowerCamelRegex), fieldSchema);
23
79
 
package/checks/config.js CHANGED
@@ -20,6 +20,7 @@ const configSchema = z
20
20
  devPassword: z.string().min(6),
21
21
  bodyLimit: z.int().min(1),
22
22
  tz: z.string().refine((value) => isValidTimeZone(value), "无效的时区"),
23
+ publicDir: noTrimString.min(1),
23
24
 
24
25
  logger: z
25
26
  .object({
package/checks/table.js CHANGED
@@ -1,22 +1,15 @@
1
1
  import * as z from "zod";
2
2
 
3
+ import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
3
4
  import { Logger } from "../lib/logger.js";
4
5
  import { formatZodIssues } from "../utils/formatZodIssues.js";
5
- import { isNoTrimStringAllowEmpty, isNullable } from "../utils/is.js";
6
+ import { isNoTrimStringAllowEmpty, isNullable, isRegexInput } from "../utils/is.js";
6
7
 
7
8
  z.config(z.locales.zhCN());
8
9
 
9
10
  const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
10
11
  const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
11
- const INPUT_TYPES = ["number", "integer", "string", "char", "array", "array_number", "array_integer", "json", "json_number", "json_integer"];
12
-
13
- const inputRegexLiteral = /^\/.*?\/[gimsuy]*$/;
14
- const inputEnumRegex = /^[^/].*\|.*$/;
15
- const inputAliasRegex = /^@.+$/;
16
-
17
- const inputSchema = z.union([z.enum(INPUT_TYPES), z.string().regex(inputRegexLiteral), z.string().regex(inputEnumRegex), z.string().regex(inputAliasRegex)]).refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
18
- const DEFAULT_MIN = 0;
19
- const DEFAULT_MAX = Number.MAX_SAFE_INTEGER;
12
+ const inputSchema = z.enum(FIELD_RULE_INPUT_TYPES);
20
13
 
21
14
  function addIssue(context, path, message) {
22
15
  context.addIssue({
@@ -29,16 +22,57 @@ const fieldDefSchema = z
29
22
  .object({
30
23
  name: z.string().min(1),
31
24
  input: inputSchema,
25
+ check: noTrimString.optional(),
32
26
  min: z.number().nullable().optional(),
33
- max: z.number().nullable().optional()
27
+ max: z.number().nullable().optional(),
28
+ detail: z.string().optional()
34
29
  })
35
30
  .strict()
36
31
  .superRefine((value, context) => {
37
- const min = isNullable(value.min) ? DEFAULT_MIN : value.min;
38
- const max = isNullable(value.max) ? DEFAULT_MAX : value.max;
32
+ const min = isNullable(value.min) ? FIELD_RULE_DEFAULT_MIN : value.min;
33
+ const max = isNullable(value.max) ? FIELD_RULE_DEFAULT_MAX : value.max;
39
34
  if (min > max) {
40
35
  addIssue(context, ["min"], "min 不能大于 max");
41
36
  }
37
+
38
+ const checkValue = value.check;
39
+ const input = value.input;
40
+
41
+ if ((input === "enum" || input === "enumNumber" || input === "enumInteger" || input === "regexp") && (!checkValue || checkValue.length === 0)) {
42
+ addIssue(context, ["check"], `${input} 必须提供 check`);
43
+ return;
44
+ }
45
+
46
+ if (input === "regexp" && checkValue && !isRegexInput(checkValue)) {
47
+ addIssue(context, ["check"], "regexp.check 必须是正则字面量或 @ 别名");
48
+ return;
49
+ }
50
+
51
+ if ((input === "enum" || input === "enumNumber" || input === "enumInteger") && checkValue) {
52
+ const items = String(checkValue)
53
+ .split("|")
54
+ .map((item) => item.trim())
55
+ .filter((item) => item.length > 0);
56
+
57
+ if (items.length === 0) {
58
+ addIssue(context, ["check"], `${input}.check 必须是非空枚举值列表`);
59
+ return;
60
+ }
61
+
62
+ if (input === "enumNumber" || input === "enumInteger") {
63
+ for (const item of items) {
64
+ const num = Number(item);
65
+ if (!Number.isFinite(num)) {
66
+ addIssue(context, ["check"], `${input}.check 存在非法数字值: ${item}`);
67
+ return;
68
+ }
69
+ if (input === "enumInteger" && !Number.isInteger(num)) {
70
+ addIssue(context, ["check"], `${input}.check 存在非法整数值: ${item}`);
71
+ return;
72
+ }
73
+ }
74
+ }
75
+ }
42
76
  });
43
77
 
44
78
  const tableContentSchema = z.record(z.string().regex(lowerCamelRegex), fieldDefSchema);
@@ -7,6 +7,7 @@
7
7
  "devPassword": "111111",
8
8
  "bodyLimit": 1048576,
9
9
  "tz": "Asia/Shanghai",
10
+ "publicDir": "./public",
10
11
 
11
12
  "logger": {
12
13
  "debug": 1,
@@ -65,20 +65,30 @@
65
65
  "path": "/log",
66
66
  "sort": 9003,
67
67
  "children": [
68
+ {
69
+ "name": "访问统计",
70
+ "path": "/visit",
71
+ "sort": 1
72
+ },
68
73
  {
69
74
  "name": "登录日志",
70
75
  "path": "/login",
71
- "sort": 1
76
+ "sort": 2
72
77
  },
73
78
  {
74
79
  "name": "邮件日志",
75
80
  "path": "/email",
76
- "sort": 2
81
+ "sort": 3
77
82
  },
78
83
  {
79
84
  "name": "操作日志",
80
85
  "path": "/operate",
81
- "sort": 3
86
+ "sort": 4
87
+ },
88
+ {
89
+ "name": "错误报告",
90
+ "path": "/error",
91
+ "sort": 5
82
92
  }
83
93
  ]
84
94
  }
@@ -32,3 +32,14 @@ export const RUN_MODE_VALUES = ["development", "production"];
32
32
 
33
33
  // 数据库 ID 生成模式可选值
34
34
  export const DB_ID_MODE_VALUES = ["timeId", "autoId"];
35
+
36
+ // ==========================
37
+ // 字段验证规则配置
38
+ // ==========================
39
+
40
+ // 字段 input 可选值(统一用于 validator / checkTable / checkApi)
41
+ export const FIELD_RULE_INPUT_TYPES = ["number", "integer", "string", "char", "array", "arrayNumber", "arrayInteger", "json", "jsonNumber", "jsonInteger", "regexp", "enum", "enumNumber", "enumInteger"];
42
+
43
+ // 字段 min/max 缺省值
44
+ export const FIELD_RULE_DEFAULT_MIN = 0;
45
+ export const FIELD_RULE_DEFAULT_MAX = Number.MAX_SAFE_INTEGER;
package/index.js CHANGED
@@ -202,7 +202,7 @@ export class Befly {
202
202
 
203
203
  // 启动 HTTP服务器
204
204
  const apiFetch = apiHandler(this.apis, this.hooks, this.context);
205
- const staticFetch = staticHandler(this.context.config.cors);
205
+ const staticFetch = staticHandler(this.context.config.cors, this.context.config.publicDir);
206
206
 
207
207
  const server = Bun.serve({
208
208
  port: this.context.config.appPort || 3000,
package/lib/logger.js CHANGED
@@ -6,7 +6,7 @@ import { createWriteStream, existsSync, mkdirSync } from "node:fs";
6
6
  import { stat } from "node:fs/promises";
7
7
  import { join as nodePathJoin, resolve as nodePathResolve } from "node:path";
8
8
 
9
- import { formatYmdHms } from "../utils/formatYmdHms.js";
9
+ import { formatYmdHms } from "../utils/datetime.js";
10
10
  import { buildSensitiveKeyMatcher, sanitizeLogObject } from "../utils/loggerUtils.js";
11
11
  import { isFiniteNumber, isNumber, isPlainObject, isString } from "../utils/is.js";
12
12
  import { normalizePositiveInt } from "../utils/util.js";
@@ -236,7 +236,7 @@ class LogFileSink {
236
236
  }
237
237
 
238
238
  async ensureStreamReady(nextChunkBytes) {
239
- const date = formatYmdHms(Reflect.construct(Date, []), "date");
239
+ const date = formatYmdHms(Date.now(), "date");
240
240
 
241
241
  // 日期变化:切新文件
242
242
  if (this.stream && this.streamDate && date !== this.streamDate) {
@@ -403,7 +403,7 @@ export function setMockLogger(mock) {
403
403
  function buildJsonLine(level, timeMs, record) {
404
404
  const base = {
405
405
  level: level,
406
- time: formatYmdHms(Reflect.construct(Date, [timeMs])),
406
+ time: formatYmdHms(timeMs),
407
407
  pid: process.pid
408
408
  };
409
409