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.
@@ -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) => {
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { resolveQuoteIdent, normalizeLimitValue, normalizeOffsetValue } from "./util.js";
7
- import { appendGroupByItems, appendHavingItems, appendJoinItem, appendOrderByItems, appendSelectItems, appendSelectRaw, appendWhereInput, appendWhereRaw, createWhereRoot, setFromValue } from "./parser.js";
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
  */
@@ -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 { isEnumInput, isFiniteNumber, isIntegerNumber, isJsonObject, isJsonPrimitive, isJsonStructure, isNullable, isNumber, isPlainObject, isRegexInput, isString } from "../utils/is.js";
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(["number", "integer", "string", "char", "array", "array_number", "array_integer", "json", "json_number", "json_integer"]);
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 = dataRecord[field];
61
+ const value = data[field];
63
62
  if (isNullable(value)) {
64
- const rawRule = rulesRecord[field];
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(rulesRecord)) {
70
+ for (const [field, rawRule] of Object.entries(rules)) {
72
71
  if (fieldErrors[field]) continue;
73
72
  // 字段值为 undefined 时跳过验证(除非是必填字段,但必填字段已在上面检查过)
74
- if (isNullable(dataRecord[field]) && !required.includes(field)) continue;
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(dataRecord[field], rule, field);
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
- const input = isString(fieldDef.input) ? fieldDef.input.trim() : "";
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
- const inputRaw = String(input || "").trim();
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 "array_number": {
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 "array_integer": {
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 "json_number":
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 "json_integer":
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 input = isString(fieldDef.input) ? fieldDef.input.trim() : "";
244
- const min = isNumber(fieldDef.min) ? fieldDef.min : 0;
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
- const inputRaw = String(input || "").trim();
248
- const inputKey = inputRaw.toLowerCase();
249
- const isRegex = !INPUT_TYPE_SET.has(inputKey) && isRegexInput(inputRaw);
250
- const isEnum = !INPUT_TYPE_SET.has(inputKey) && isEnumInput(inputRaw);
279
+ if (!isString(fieldDef.input)) {
280
+ return "input 不支持";
281
+ }
251
282
 
252
- if (inputKey === "number" || inputKey === "integer") {
253
- if (typeof value !== "number") return inputKey === "integer" ? "必须是整数" : "必须是数字";
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 (inputKey === "string" || inputKey === "char" || isRegex || isEnum) {
287
+ } else if (fieldDef.input === "string" || fieldDef.input === "char" || fieldDef.input === "regexp" || fieldDef.input === "enum") {
257
288
  if (!isString(value)) return "必须是字符串";
258
- if (inputKey === "char" && value.length !== 1) return "必须是单字符";
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 (isRegex) {
263
- const regex = this.resolveRegex(inputRaw);
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 (isEnum) {
268
- const enums = String(inputRaw || "")
269
- .trim()
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 (inputKey === "array" || inputKey === "array_number" || inputKey === "array_integer") {
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 (inputKey === "json" || inputKey === "json_number" || inputKey === "json_integer") {
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.18.23",
4
- "gitHead": "d18f2e30b2e70845d26b4b7b5190b87eb8709337",
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.2",
56
- "nodemailer": "^8.0.1",
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 { appDir } from "../paths.js";
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(appDir, "public", url.pathname);
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: "beflyMenu",
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: "beflyApi",
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: "beflyRole",
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: "beflyAdmin",
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: "beflyRole",
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: "beflyRole",
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: "beflyAdmin",
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: "beflyAdmin",
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: "beflyRole",
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: "beflyRole",
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: "beflyRole",
211
+ table: ROLE_TABLE_NAME,
207
212
  data: {
208
213
  code: roleConfig.code,
209
214
  name: roleConfig.name,