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.
- package/apis/auth/login.js +2 -3
- package/apis/tongJi/errorList.js +102 -0
- package/apis/tongJi/errorReport.js +90 -0
- package/apis/tongJi/errorStats.js +70 -0
- package/apis/tongJi/visitReport.js +241 -0
- package/apis/tongJi/visitStats.js +380 -0
- package/checks/api.js +60 -4
- package/checks/config.js +1 -0
- package/checks/table.js +47 -13
- package/configs/beflyConfig.json +1 -0
- package/configs/beflyMenus.json +13 -3
- package/configs/constConfig.js +11 -0
- package/index.js +1 -1
- package/lib/logger.js +3 -3
- package/lib/redisHelper.js +38 -0
- package/lib/sqlBuilder/compiler.js +0 -9
- package/lib/sqlBuilder/index.js +1 -19
- package/lib/sqlBuilder/parser.js +0 -21
- package/lib/validator.js +108 -49
- package/package.json +4 -4
- package/paths.js +14 -1
- package/router/static.js +3 -3
- package/sql/befly.sql +54 -0
- package/sync/dev.js +16 -11
- package/sync/menu.js +6 -4
- package/tables/admin.json +10 -5
- package/tables/dict.json +4 -2
- package/tables/dictType.json +2 -1
- package/tables/emailLog.json +2 -1
- package/tables/role.json +2 -1
- package/tables/sysConfig.json +4 -2
- package/utils/cors.js +34 -1
- package/utils/datetime.js +76 -0
- package/utils/visitStats.js +121 -0
- package/utils/formatYmdHms.js +0 -23
package/lib/redisHelper.js
CHANGED
|
@@ -169,6 +169,22 @@ export class RedisHelper {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* 原子按指定值自增
|
|
174
|
+
* @param key - 键名
|
|
175
|
+
* @param value - 自增值
|
|
176
|
+
* @returns 自增后的值
|
|
177
|
+
*/
|
|
178
|
+
async incrBy(key, value) {
|
|
179
|
+
try {
|
|
180
|
+
const pkey = `${this.prefix}${key}`;
|
|
181
|
+
return await this.client.incrby(pkey, value);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
Logger.error("Redis incrBy 错误", error);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
172
188
|
/**
|
|
173
189
|
* 原子自增并在首次自增时设置过期时间(常用于限流/计数)
|
|
174
190
|
* @param key - 键名
|
|
@@ -305,6 +321,28 @@ export class RedisHelper {
|
|
|
305
321
|
}
|
|
306
322
|
}
|
|
307
323
|
|
|
324
|
+
/**
|
|
325
|
+
* 从 Set 中删除一个或多个成员
|
|
326
|
+
* @param key - 键名
|
|
327
|
+
* @param members - 成员数组
|
|
328
|
+
* @returns 删除的成员数量
|
|
329
|
+
*/
|
|
330
|
+
async srem(key, members) {
|
|
331
|
+
try {
|
|
332
|
+
if (members.length === 0) return 0;
|
|
333
|
+
|
|
334
|
+
const pkey = `${this.prefix}${key}`;
|
|
335
|
+
const args = [pkey];
|
|
336
|
+
for (const member of members) {
|
|
337
|
+
args.push(member);
|
|
338
|
+
}
|
|
339
|
+
return await this.client.srem.apply(this.client, args);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
Logger.error("Redis srem 错误", error);
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
308
346
|
/**
|
|
309
347
|
* 批量向多个 Set 添加成员(利用 Bun Redis 自动管道优化)
|
|
310
348
|
* @param items - [{ key, members }] 数组
|
|
@@ -48,15 +48,6 @@ export function compileSelect(model, quoteIdent) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
if (model.groupBy.length > 0) {
|
|
52
|
-
const groupSql = model.groupBy.map((field) => escapeField(field, quoteIdent)).join(", ");
|
|
53
|
-
sql += ` GROUP BY ${groupSql}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (model.having.length > 0) {
|
|
57
|
-
sql += ` HAVING ${model.having.join(" AND ")}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
51
|
if (model.orderBy.length > 0) {
|
|
61
52
|
const orderSql = model.orderBy
|
|
62
53
|
.map((item) => {
|
package/lib/sqlBuilder/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { resolveQuoteIdent, normalizeLimitValue, normalizeOffsetValue } from "./util.js";
|
|
7
|
-
import {
|
|
7
|
+
import { appendJoinItem, appendOrderByItems, appendSelectItems, appendSelectRaw, appendWhereInput, appendWhereRaw, createWhereRoot, setFromValue } from "./parser.js";
|
|
8
8
|
import { compileCount, compileDelete, compileInsert, compileSelect, compileUpdate, compileWhere } from "./compiler.js";
|
|
9
9
|
import { toDeleteInSql, toUpdateCaseByIdSql } from "./batch.js";
|
|
10
10
|
|
|
@@ -15,8 +15,6 @@ function createModel() {
|
|
|
15
15
|
where: createWhereRoot(),
|
|
16
16
|
joins: [],
|
|
17
17
|
orderBy: [],
|
|
18
|
-
groupBy: [],
|
|
19
|
-
having: [],
|
|
20
18
|
limit: null,
|
|
21
19
|
offset: null
|
|
22
20
|
};
|
|
@@ -131,22 +129,6 @@ export class SqlBuilder {
|
|
|
131
129
|
return this;
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
/**
|
|
135
|
-
* GROUP BY
|
|
136
|
-
*/
|
|
137
|
-
groupBy(field) {
|
|
138
|
-
appendGroupByItems(this._model.groupBy, field);
|
|
139
|
-
return this;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* HAVING
|
|
144
|
-
*/
|
|
145
|
-
having(condition) {
|
|
146
|
-
appendHavingItems(this._model.having, condition);
|
|
147
|
-
return this;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
132
|
/**
|
|
151
133
|
* LIMIT
|
|
152
134
|
*/
|
package/lib/sqlBuilder/parser.js
CHANGED
|
@@ -105,27 +105,6 @@ export function appendOrderByItems(list, fields) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export function appendGroupByItems(list, field) {
|
|
109
|
-
if (Array.isArray(field)) {
|
|
110
|
-
for (const item of field) {
|
|
111
|
-
if (isString(item)) {
|
|
112
|
-
list.push(item);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (isString(field)) {
|
|
119
|
-
list.push(field);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function appendHavingItems(list, condition) {
|
|
124
|
-
if (isString(condition)) {
|
|
125
|
-
list.push(condition);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
108
|
export function appendWhereInput(root, conditionOrField, value) {
|
|
130
109
|
if (conditionOrField && typeof conditionOrField === "object" && !Array.isArray(conditionOrField)) {
|
|
131
110
|
const node = parseWhereObject(conditionOrField);
|
package/lib/validator.js
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import RegexAliases from "../configs/regexpAlias.json";
|
|
7
|
+
import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
|
|
7
8
|
import { getCompiledRegex } from "../utils/regexpUtil.js";
|
|
8
|
-
import {
|
|
9
|
+
import { isFiniteNumber, isIntegerNumber, isJsonObject, isJsonPrimitive, isJsonStructure, isNullable, isNumber, isPlainObject, isRegexInput, isString } from "../utils/is.js";
|
|
9
10
|
|
|
10
|
-
const INPUT_TYPE_SET = new Set(
|
|
11
|
+
const INPUT_TYPE_SET = new Set(FIELD_RULE_INPUT_TYPES);
|
|
12
|
+
const RULE_ALLOWED_KEYS = new Set(["input", "check", "name", "min", "max", "detail"]);
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* 递归校验 JSON 结构叶子节点是否满足数值规则。
|
|
@@ -54,30 +56,38 @@ export class Validator {
|
|
|
54
56
|
return this.buildResult({ _error: "验证规则必须是对象格式" });
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
const dataRecord = data;
|
|
58
|
-
const rulesRecord = rules;
|
|
59
|
-
|
|
60
59
|
// 检查必填字段
|
|
61
60
|
for (const field of required) {
|
|
62
|
-
const value =
|
|
61
|
+
const value = data[field];
|
|
63
62
|
if (isNullable(value)) {
|
|
64
|
-
const rawRule =
|
|
63
|
+
const rawRule = rules[field];
|
|
65
64
|
const label = isPlainObject(rawRule) && isString(rawRule["name"]) ? rawRule["name"] : field;
|
|
66
65
|
fieldErrors[field] = `${label}为必填项`;
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
// 验证有值的字段
|
|
71
|
-
for (const [field, rawRule] of Object.entries(
|
|
70
|
+
for (const [field, rawRule] of Object.entries(rules)) {
|
|
72
71
|
if (fieldErrors[field]) continue;
|
|
73
72
|
// 字段值为 undefined 时跳过验证(除非是必填字段,但必填字段已在上面检查过)
|
|
74
|
-
if (isNullable(
|
|
73
|
+
if (isNullable(data[field]) && !required.includes(field)) continue;
|
|
75
74
|
|
|
76
75
|
if (!isPlainObject(rawRule)) {
|
|
77
76
|
fieldErrors[field] = `${field}验证规则必须是对象格式`;
|
|
78
77
|
continue;
|
|
79
78
|
}
|
|
80
79
|
|
|
80
|
+
const invalidKeys = [];
|
|
81
|
+
for (const key of Object.keys(rawRule)) {
|
|
82
|
+
if (!RULE_ALLOWED_KEYS.has(key)) {
|
|
83
|
+
invalidKeys.push(key);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (invalidKeys.length > 0) {
|
|
87
|
+
fieldErrors[field] = `${field}存在非法属性: ${invalidKeys.join(",")}`;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
const ruleName = rawRule["name"];
|
|
82
92
|
const ruleInput = rawRule["input"];
|
|
83
93
|
if (!isString(ruleName) || !isString(ruleInput)) {
|
|
@@ -85,18 +95,29 @@ export class Validator {
|
|
|
85
95
|
continue;
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
if (!INPUT_TYPE_SET.has(ruleInput)) {
|
|
99
|
+
fieldErrors[field] = `${field}input 不支持: ${ruleInput}`;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
const rule = {
|
|
89
104
|
name: ruleName,
|
|
90
105
|
input: ruleInput
|
|
91
106
|
};
|
|
92
107
|
|
|
108
|
+
const check = rawRule["check"];
|
|
109
|
+
if (isString(check)) rule.check = check;
|
|
110
|
+
|
|
111
|
+
const detail = rawRule["detail"];
|
|
112
|
+
if (isString(detail)) rule.detail = detail;
|
|
113
|
+
|
|
93
114
|
const min = rawRule["min"];
|
|
94
115
|
if (isNumber(min) || min === null) rule.min = min;
|
|
95
116
|
|
|
96
117
|
const max = rawRule["max"];
|
|
97
118
|
if (isNumber(max) || max === null) rule.max = max;
|
|
98
119
|
|
|
99
|
-
const error = this.checkField(
|
|
120
|
+
const error = this.checkField(data[field], rule, field);
|
|
100
121
|
if (error) fieldErrors[field] = error;
|
|
101
122
|
}
|
|
102
123
|
|
|
@@ -107,7 +128,10 @@ export class Validator {
|
|
|
107
128
|
* 验证单个值(带类型转换)
|
|
108
129
|
*/
|
|
109
130
|
static single(value, fieldDef) {
|
|
110
|
-
|
|
131
|
+
if (!isString(fieldDef.input) || !INPUT_TYPE_SET.has(fieldDef.input)) {
|
|
132
|
+
const input = isString(fieldDef.input) ? fieldDef.input : "";
|
|
133
|
+
return { value: null, error: `input 不支持: ${input}` };
|
|
134
|
+
}
|
|
111
135
|
|
|
112
136
|
// 处理空值
|
|
113
137
|
if (isNullable(value, [""])) {
|
|
@@ -115,7 +139,7 @@ export class Validator {
|
|
|
115
139
|
}
|
|
116
140
|
|
|
117
141
|
// 类型转换
|
|
118
|
-
const converted = this.convert(value, input);
|
|
142
|
+
const converted = this.convert(value, fieldDef.input);
|
|
119
143
|
if (converted.error) {
|
|
120
144
|
return { value: null, error: converted.error };
|
|
121
145
|
}
|
|
@@ -150,9 +174,7 @@ export class Validator {
|
|
|
150
174
|
/** 验证单个字段 */
|
|
151
175
|
static checkField(value, fieldDef, fieldName) {
|
|
152
176
|
const label = fieldDef.name || fieldName;
|
|
153
|
-
|
|
154
|
-
const input = isString(fieldDef.input) ? fieldDef.input.trim() : "";
|
|
155
|
-
const converted = this.convert(value, input);
|
|
177
|
+
const converted = this.convert(value, fieldDef.input);
|
|
156
178
|
if (converted.error) {
|
|
157
179
|
return `${label}${converted.error}`;
|
|
158
180
|
}
|
|
@@ -163,18 +185,7 @@ export class Validator {
|
|
|
163
185
|
|
|
164
186
|
/** 类型转换 */
|
|
165
187
|
static convert(value, input) {
|
|
166
|
-
|
|
167
|
-
const inputKey = inputRaw.toLowerCase();
|
|
168
|
-
|
|
169
|
-
if (!INPUT_TYPE_SET.has(inputKey) && isRegexInput(inputRaw)) {
|
|
170
|
-
return isString(value) ? { value: value, error: null } : { value: null, error: "必须是字符串" };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (!INPUT_TYPE_SET.has(inputKey) && isEnumInput(inputRaw)) {
|
|
174
|
-
return isString(value) ? { value: value, error: null } : { value: null, error: "必须是字符串" };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
switch (inputKey) {
|
|
188
|
+
switch (input) {
|
|
178
189
|
case "number": {
|
|
179
190
|
if (isFiniteNumber(value)) return { value: value, error: null };
|
|
180
191
|
if (isString(value)) {
|
|
@@ -204,7 +215,7 @@ export class Validator {
|
|
|
204
215
|
case "array":
|
|
205
216
|
return Array.isArray(value) ? { value: value, error: null } : { value: null, error: "必须是数组" };
|
|
206
217
|
|
|
207
|
-
case "
|
|
218
|
+
case "arrayNumber": {
|
|
208
219
|
if (!Array.isArray(value)) return { value: null, error: "必须是数组" };
|
|
209
220
|
for (const item of value) {
|
|
210
221
|
if (!isFiniteNumber(item)) return { value: null, error: "数组元素必须是数字" };
|
|
@@ -212,7 +223,7 @@ export class Validator {
|
|
|
212
223
|
return { value: value, error: null };
|
|
213
224
|
}
|
|
214
225
|
|
|
215
|
-
case "
|
|
226
|
+
case "arrayInteger": {
|
|
216
227
|
if (!Array.isArray(value)) return { value: null, error: "必须是数组" };
|
|
217
228
|
for (const item of value) {
|
|
218
229
|
if (!isIntegerNumber(item)) return { value: null, error: "数组元素必须是整数" };
|
|
@@ -224,14 +235,36 @@ export class Validator {
|
|
|
224
235
|
if (!isJsonStructure(value)) return { value: null, error: "必须是JSON对象或数组" };
|
|
225
236
|
return { value: value, error: null };
|
|
226
237
|
|
|
227
|
-
case "
|
|
238
|
+
case "jsonNumber":
|
|
228
239
|
if (!isJsonStructure(value)) return { value: null, error: "必须是JSON对象或数组" };
|
|
229
240
|
return checkJsonLeaves(value, "number") ? { value: value, error: null } : { value: null, error: "JSON值必须是数字" };
|
|
230
241
|
|
|
231
|
-
case "
|
|
242
|
+
case "jsonInteger":
|
|
232
243
|
if (!isJsonStructure(value)) return { value: null, error: "必须是JSON对象或数组" };
|
|
233
244
|
return checkJsonLeaves(value, "integer") ? { value: value, error: null } : { value: null, error: "JSON值必须是整数" };
|
|
234
245
|
|
|
246
|
+
case "regexp":
|
|
247
|
+
case "enum":
|
|
248
|
+
return isString(value) ? { value: value, error: null } : { value: null, error: "必须是字符串" };
|
|
249
|
+
|
|
250
|
+
case "enumNumber": {
|
|
251
|
+
if (isFiniteNumber(value)) return { value: value, error: null };
|
|
252
|
+
if (isString(value)) {
|
|
253
|
+
const num = Number(value);
|
|
254
|
+
return Number.isFinite(num) ? { value: num, error: null } : { value: null, error: "必须是数字" };
|
|
255
|
+
}
|
|
256
|
+
return { value: null, error: "必须是数字" };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case "enumInteger": {
|
|
260
|
+
if (isIntegerNumber(value)) return { value: value, error: null };
|
|
261
|
+
if (isString(value)) {
|
|
262
|
+
const num = Number(value);
|
|
263
|
+
return Number.isFinite(num) && Number.isInteger(num) ? { value: num, error: null } : { value: null, error: "必须是整数" };
|
|
264
|
+
}
|
|
265
|
+
return { value: null, error: "必须是整数" };
|
|
266
|
+
}
|
|
267
|
+
|
|
235
268
|
default: {
|
|
236
269
|
return { value: value, error: null };
|
|
237
270
|
}
|
|
@@ -240,43 +273,69 @@ export class Validator {
|
|
|
240
273
|
|
|
241
274
|
/** 规则验证 */
|
|
242
275
|
static checkRule(value, fieldDef) {
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
const max = isNumber(fieldDef.max) ? fieldDef.max : Number.MAX_SAFE_INTEGER;
|
|
276
|
+
const min = isNumber(fieldDef.min) ? fieldDef.min : FIELD_RULE_DEFAULT_MIN;
|
|
277
|
+
const max = isNumber(fieldDef.max) ? fieldDef.max : FIELD_RULE_DEFAULT_MAX;
|
|
246
278
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const isEnum = !INPUT_TYPE_SET.has(inputKey) && isEnumInput(inputRaw);
|
|
279
|
+
if (!isString(fieldDef.input)) {
|
|
280
|
+
return "input 不支持";
|
|
281
|
+
}
|
|
251
282
|
|
|
252
|
-
if (
|
|
253
|
-
if (typeof value !== "number") return
|
|
283
|
+
if (fieldDef.input === "number" || fieldDef.input === "integer") {
|
|
284
|
+
if (typeof value !== "number") return fieldDef.input === "integer" ? "必须是整数" : "必须是数字";
|
|
254
285
|
if (value < min) return `不能小于${min}`;
|
|
255
286
|
if (value > max) return `不能大于${max}`;
|
|
256
|
-
} else if (
|
|
287
|
+
} else if (fieldDef.input === "string" || fieldDef.input === "char" || fieldDef.input === "regexp" || fieldDef.input === "enum") {
|
|
257
288
|
if (!isString(value)) return "必须是字符串";
|
|
258
|
-
if (
|
|
289
|
+
if (fieldDef.input === "char" && value.length !== 1) return "必须是单字符";
|
|
259
290
|
if (value.length < min) return `长度不能少于${min}个字符`;
|
|
260
291
|
if (value.length > max) return `长度不能超过${max}个字符`;
|
|
261
292
|
|
|
262
|
-
if (
|
|
263
|
-
|
|
293
|
+
if (fieldDef.input === "regexp") {
|
|
294
|
+
if (!isString(fieldDef.check) || fieldDef.check.length === 0) return "缺少 check 规则";
|
|
295
|
+
if (!isRegexInput(fieldDef.check)) return "check 规则无效";
|
|
296
|
+
const regex = this.resolveRegex(fieldDef.check);
|
|
264
297
|
if (!this.testRegex(regex, value)) return "格式不正确";
|
|
265
298
|
}
|
|
266
299
|
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
300
|
+
if (fieldDef.input === "enum") {
|
|
301
|
+
if (!isString(fieldDef.check) || fieldDef.check.length === 0) return "缺少 check 规则";
|
|
302
|
+
const enums = fieldDef.check
|
|
270
303
|
.split("|")
|
|
271
304
|
.map((x) => x.trim())
|
|
272
305
|
.filter((x) => x !== "");
|
|
306
|
+
if (enums.length === 0) return "check 规则无效";
|
|
273
307
|
if (!enums.includes(value)) return "值不在枚举范围内";
|
|
274
308
|
}
|
|
275
|
-
} else if (
|
|
309
|
+
} else if (fieldDef.input === "enumNumber" || fieldDef.input === "enumInteger") {
|
|
310
|
+
if (typeof value !== "number") return fieldDef.input === "enumInteger" ? "必须是整数" : "必须是数字";
|
|
311
|
+
if (!isString(fieldDef.check) || fieldDef.check.length === 0) return "缺少 check 规则";
|
|
312
|
+
|
|
313
|
+
const enumValues = [];
|
|
314
|
+
for (const item of fieldDef.check.split("|")) {
|
|
315
|
+
const normalized = item.trim();
|
|
316
|
+
if (normalized.length === 0) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const num = Number(normalized);
|
|
321
|
+
if (!Number.isFinite(num)) {
|
|
322
|
+
return "check 规则无效";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (fieldDef.input === "enumInteger" && !Number.isInteger(num)) {
|
|
326
|
+
return "check 规则无效";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
enumValues.push(num);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (enumValues.length === 0) return "check 规则无效";
|
|
333
|
+
if (!enumValues.includes(value)) return "值不在枚举范围内";
|
|
334
|
+
} else if (fieldDef.input === "array" || fieldDef.input === "arrayNumber" || fieldDef.input === "arrayInteger") {
|
|
276
335
|
if (!Array.isArray(value)) return "必须是数组";
|
|
277
336
|
if (value.length < min) return `至少需要${min}个元素`;
|
|
278
337
|
if (value.length > max) return `最多只能有${max}个元素`;
|
|
279
|
-
} else if (
|
|
338
|
+
} else if (fieldDef.input === "json" || fieldDef.input === "jsonNumber" || fieldDef.input === "jsonInteger") {
|
|
280
339
|
if (!isJsonStructure(value)) return "必须是JSON对象或数组";
|
|
281
340
|
}
|
|
282
341
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "3.19.5",
|
|
4
|
+
"gitHead": "ba7fa7cc0534c48de2df4970d69a14853cf6b2a6",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
|
|
7
7
|
"keywords": [
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"test": "bun test"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"fast-xml-parser": "^5.4
|
|
56
|
-
"nodemailer": "^8.0.
|
|
55
|
+
"fast-xml-parser": "^5.5.4",
|
|
56
|
+
"nodemailer": "^8.0.2",
|
|
57
57
|
"pathe": "^2.0.3",
|
|
58
58
|
"ua-parser-js": "^2.0.9",
|
|
59
59
|
"zod": "^4.0.0"
|
package/paths.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
|
|
15
|
-
import { dirname, join, normalize } from "pathe";
|
|
15
|
+
import { dirname, isAbsolute, join, normalize, resolve } from "pathe";
|
|
16
16
|
|
|
17
17
|
// 当前文件的路径信息
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -110,3 +110,16 @@ export const appApiDir = join(appDir, "apis");
|
|
|
110
110
|
* @usage 存放用户业务表定义(JSON 格式)
|
|
111
111
|
*/
|
|
112
112
|
export const appTableDir = join(appDir, "tables");
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 项目公共静态目录
|
|
116
|
+
* @description 默认 {appDir}/public,可通过 config.publicDir 覆盖
|
|
117
|
+
* @usage 用于静态文件访问与本地上传保存目录解析
|
|
118
|
+
*/
|
|
119
|
+
export function getAppPublicDir(publicDir = "./public") {
|
|
120
|
+
if (isAbsolute(publicDir)) {
|
|
121
|
+
return resolve(publicDir);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return resolve(appDir, publicDir);
|
|
125
|
+
}
|
package/router/static.js
CHANGED
|
@@ -8,19 +8,19 @@ import { join } from "pathe";
|
|
|
8
8
|
|
|
9
9
|
import { Logger } from "../lib/logger.js";
|
|
10
10
|
// 相对导入
|
|
11
|
-
import {
|
|
11
|
+
import { getAppPublicDir } from "../paths.js";
|
|
12
12
|
import { setCorsOptions } from "../utils/cors.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 静态文件处理器工厂
|
|
16
16
|
*/
|
|
17
|
-
export function staticHandler(corsConfig = undefined) {
|
|
17
|
+
export function staticHandler(corsConfig = undefined, publicDir = "./public") {
|
|
18
18
|
return async (req) => {
|
|
19
19
|
// 设置 CORS 响应头
|
|
20
20
|
const corsHeaders = setCorsOptions(req, corsConfig);
|
|
21
21
|
|
|
22
22
|
const url = new URL(req.url);
|
|
23
|
-
const filePath = join(
|
|
23
|
+
const filePath = join(getAppPublicDir(publicDir), url.pathname);
|
|
24
24
|
|
|
25
25
|
try {
|
|
26
26
|
// OPTIONS预检请求
|
package/sql/befly.sql
CHANGED
|
@@ -171,4 +171,58 @@ CREATE TABLE IF NOT EXISTS `befly_sys_config` (
|
|
|
171
171
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
172
172
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
173
173
|
PRIMARY KEY (`id`)
|
|
174
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
175
|
+
|
|
176
|
+
CREATE TABLE IF NOT EXISTS `befly_visit_stats` (
|
|
177
|
+
`id` BIGINT NOT NULL,
|
|
178
|
+
`bucket_time` BIGINT NOT NULL DEFAULT 0,
|
|
179
|
+
`bucket_date` INT NOT NULL DEFAULT 0,
|
|
180
|
+
`pv` BIGINT NOT NULL DEFAULT 0,
|
|
181
|
+
`uv` BIGINT NOT NULL DEFAULT 0,
|
|
182
|
+
`error_count` BIGINT NOT NULL DEFAULT 0,
|
|
183
|
+
`duration_sum` BIGINT NOT NULL DEFAULT 0,
|
|
184
|
+
`duration_count` BIGINT NOT NULL DEFAULT 0,
|
|
185
|
+
`avg_duration` BIGINT NOT NULL DEFAULT 0,
|
|
186
|
+
`state` TINYINT NOT NULL DEFAULT 1,
|
|
187
|
+
`created_at` BIGINT NOT NULL DEFAULT 0,
|
|
188
|
+
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
189
|
+
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
190
|
+
PRIMARY KEY (`id`),
|
|
191
|
+
UNIQUE KEY `uk_befly_visit_stats_bucket_time` (`bucket_time`),
|
|
192
|
+
KEY `idx_befly_visit_stats_bucket_date` (`bucket_date`)
|
|
193
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
194
|
+
|
|
195
|
+
CREATE TABLE IF NOT EXISTS `befly_error_report` (
|
|
196
|
+
`id` BIGINT NOT NULL,
|
|
197
|
+
`report_time` BIGINT NOT NULL DEFAULT 0,
|
|
198
|
+
`first_report_time` BIGINT NOT NULL DEFAULT 0,
|
|
199
|
+
`bucket_time` BIGINT NOT NULL DEFAULT 0,
|
|
200
|
+
`bucket_date` INT NOT NULL DEFAULT 0,
|
|
201
|
+
`hit_count` BIGINT NOT NULL DEFAULT 1,
|
|
202
|
+
`source` VARCHAR(50) NOT NULL DEFAULT '',
|
|
203
|
+
`product_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
204
|
+
`product_code` VARCHAR(100) NOT NULL DEFAULT '',
|
|
205
|
+
`product_version` VARCHAR(100) NOT NULL DEFAULT '',
|
|
206
|
+
`page_path` VARCHAR(200) NOT NULL DEFAULT '',
|
|
207
|
+
`page_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
208
|
+
`error_type` VARCHAR(50) NOT NULL DEFAULT '',
|
|
209
|
+
`message` VARCHAR(500) NOT NULL DEFAULT '',
|
|
210
|
+
`detail` TEXT NULL,
|
|
211
|
+
`user_agent` VARCHAR(500) NOT NULL DEFAULT '',
|
|
212
|
+
`browser_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
213
|
+
`browser_version` VARCHAR(100) NOT NULL DEFAULT '',
|
|
214
|
+
`os_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
215
|
+
`os_version` VARCHAR(100) NOT NULL DEFAULT '',
|
|
216
|
+
`device_type` VARCHAR(50) NOT NULL DEFAULT '',
|
|
217
|
+
`device_vendor` VARCHAR(100) NOT NULL DEFAULT '',
|
|
218
|
+
`device_model` VARCHAR(100) NOT NULL DEFAULT '',
|
|
219
|
+
`engine_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
220
|
+
`cpu_architecture` VARCHAR(100) NOT NULL DEFAULT '',
|
|
221
|
+
`state` TINYINT NOT NULL DEFAULT 1,
|
|
222
|
+
`created_at` BIGINT NOT NULL DEFAULT 0,
|
|
223
|
+
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
224
|
+
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
225
|
+
PRIMARY KEY (`id`),
|
|
226
|
+
KEY `idx_befly_error_report_time` (`report_time`),
|
|
227
|
+
KEY `idx_befly_error_report_bucket_date` (`bucket_date`)
|
|
174
228
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
package/sync/dev.js
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
import { isNumber } from "../utils/is.js";
|
|
2
2
|
|
|
3
|
+
const MENU_TABLE_NAME = "beflyMenu";
|
|
4
|
+
const API_TABLE_NAME = "beflyApi";
|
|
5
|
+
const ROLE_TABLE_NAME = "beflyRole";
|
|
6
|
+
const ADMIN_TABLE_NAME = "beflyAdmin";
|
|
7
|
+
|
|
3
8
|
export async function syncDev(ctx) {
|
|
4
9
|
try {
|
|
5
10
|
const allMenus = await ctx.mysql.getAll({
|
|
6
|
-
table:
|
|
11
|
+
table: MENU_TABLE_NAME,
|
|
7
12
|
fields: ["path"],
|
|
8
13
|
where: { state$gte: 0 },
|
|
9
14
|
orderBy: ["id#ASC"]
|
|
10
15
|
});
|
|
11
16
|
|
|
12
17
|
const allApis = await ctx.mysql.getAll({
|
|
13
|
-
table:
|
|
18
|
+
table: API_TABLE_NAME,
|
|
14
19
|
fields: ["path", "auth"],
|
|
15
20
|
where: { state$gte: 0 },
|
|
16
21
|
orderBy: ["id#ASC"]
|
|
17
22
|
});
|
|
18
23
|
|
|
19
24
|
const devRole = await ctx.mysql.getOne({
|
|
20
|
-
table:
|
|
25
|
+
table: ROLE_TABLE_NAME,
|
|
21
26
|
where: { code: "dev", state$gte: 0 }
|
|
22
27
|
});
|
|
23
28
|
|
|
24
29
|
const devAdmin = await ctx.mysql.getOne({
|
|
25
|
-
table:
|
|
30
|
+
table: ADMIN_TABLE_NAME,
|
|
26
31
|
where: { username: "dev", state$gte: 0 }
|
|
27
32
|
});
|
|
28
33
|
|
|
@@ -47,7 +52,7 @@ export async function syncDev(ctx) {
|
|
|
47
52
|
|
|
48
53
|
if (isNumber(devRole.data.id)) {
|
|
49
54
|
await ctx.mysql.updData({
|
|
50
|
-
table:
|
|
55
|
+
table: ROLE_TABLE_NAME,
|
|
51
56
|
where: { code: "dev", state$gte: 0 },
|
|
52
57
|
data: {
|
|
53
58
|
name: devRoleData.name,
|
|
@@ -60,7 +65,7 @@ export async function syncDev(ctx) {
|
|
|
60
65
|
});
|
|
61
66
|
} else {
|
|
62
67
|
await ctx.mysql.insData({
|
|
63
|
-
table:
|
|
68
|
+
table: ROLE_TABLE_NAME,
|
|
64
69
|
data: devRoleData
|
|
65
70
|
});
|
|
66
71
|
}
|
|
@@ -78,7 +83,7 @@ export async function syncDev(ctx) {
|
|
|
78
83
|
|
|
79
84
|
if (isNumber(devAdmin.data.id)) {
|
|
80
85
|
await ctx.mysql.updData({
|
|
81
|
-
table:
|
|
86
|
+
table: ADMIN_TABLE_NAME,
|
|
82
87
|
where: { username: "dev" },
|
|
83
88
|
data: {
|
|
84
89
|
nickname: devAdminData.nickname,
|
|
@@ -91,7 +96,7 @@ export async function syncDev(ctx) {
|
|
|
91
96
|
});
|
|
92
97
|
} else {
|
|
93
98
|
await ctx.mysql.insData({
|
|
94
|
-
table:
|
|
99
|
+
table: ADMIN_TABLE_NAME,
|
|
95
100
|
data: {
|
|
96
101
|
nickname: devAdminData.nickname,
|
|
97
102
|
email: devAdminData.email,
|
|
@@ -127,7 +132,7 @@ export async function syncDev(ctx) {
|
|
|
127
132
|
|
|
128
133
|
for (const roleConfig of roles) {
|
|
129
134
|
const existingRole = await ctx.mysql.getOne({
|
|
130
|
-
table:
|
|
135
|
+
table: ROLE_TABLE_NAME,
|
|
131
136
|
where: {
|
|
132
137
|
code: roleConfig.code,
|
|
133
138
|
state$gte: 0
|
|
@@ -191,7 +196,7 @@ export async function syncDev(ctx) {
|
|
|
191
196
|
if (existingRole.data?.id) {
|
|
192
197
|
// 角色存在则强制更新(不做差异判断)
|
|
193
198
|
await ctx.mysql.updData({
|
|
194
|
-
table:
|
|
199
|
+
table: ROLE_TABLE_NAME,
|
|
195
200
|
where: { code: roleConfig.code, state$gte: 0 },
|
|
196
201
|
data: {
|
|
197
202
|
name: roleConfig.name,
|
|
@@ -203,7 +208,7 @@ export async function syncDev(ctx) {
|
|
|
203
208
|
});
|
|
204
209
|
} else {
|
|
205
210
|
await ctx.mysql.insData({
|
|
206
|
-
table:
|
|
211
|
+
table: ROLE_TABLE_NAME,
|
|
207
212
|
data: {
|
|
208
213
|
code: roleConfig.code,
|
|
209
214
|
name: roleConfig.name,
|