befly 3.22.0 → 3.22.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.
@@ -0,0 +1,27 @@
1
+ export default {
2
+ name: "获取图库列表",
3
+ method: "POST",
4
+ body: "none",
5
+ auth: true,
6
+ fields: {
7
+ page: { name: "页码", input: "integer", min: 1, max: 9999 },
8
+ limit: { name: "每页数量", input: "integer", min: 1, max: 100 },
9
+ keyword: { name: "关键词", input: "string", min: 0, max: 100 }
10
+ },
11
+ required: [],
12
+ handler: async (befly, ctx) => {
13
+ const result = await befly.mysql.getList({
14
+ table: "beflyFile",
15
+ where: {
16
+ state: 1,
17
+ isImage: 1,
18
+ fileName$like: ctx.body.keyword
19
+ },
20
+ orderBy: ["id#DESC"],
21
+ page: ctx.body.page,
22
+ limit: ctx.body.limit
23
+ });
24
+
25
+ return befly.tool.Yes("操作成功", result.data);
26
+ }
27
+ };
@@ -0,0 +1,105 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { extname, join } from "node:path";
3
+
4
+ import { Logger } from "../../lib/logger.js";
5
+ import { getAppPublicDir } from "../../paths.js";
6
+ import { getMonthDir } from "../../utils/datetime.js";
7
+
8
+ export default {
9
+ name: "上传文件",
10
+ method: "POST",
11
+ body: "raw",
12
+ auth: true,
13
+ fields: {},
14
+ required: [],
15
+ handler: async (befly, ctx) => {
16
+ try {
17
+ const maxFileSizeMb = Number(befly.config?.uploadMaxSize || 20);
18
+ const maxFileSize = maxFileSizeMb * 1024 * 1024;
19
+ const requestContentType = ctx.req.headers.get("content-type") || "";
20
+
21
+ if (requestContentType.toLowerCase().startsWith("multipart/form-data") === false) {
22
+ return befly.tool.No("请使用 FormData 上传文件");
23
+ }
24
+
25
+ if (!ctx.userId) {
26
+ return befly.tool.No("用户未登录");
27
+ }
28
+
29
+ const formData = await ctx.req.formData();
30
+ const file = formData.get("file");
31
+
32
+ if (!(file instanceof File)) {
33
+ return befly.tool.No("缺少上传文件");
34
+ }
35
+
36
+ const rawBuffer = await file.arrayBuffer();
37
+
38
+ if (!rawBuffer || rawBuffer.byteLength <= 0) {
39
+ return befly.tool.No("上传文件内容为空");
40
+ }
41
+
42
+ if (rawBuffer.byteLength > maxFileSize) {
43
+ return befly.tool.No(`文件不能超过${maxFileSizeMb}MB`);
44
+ }
45
+
46
+ const originalFileName = file.name || "未命名文件";
47
+ const extension = extname(originalFileName).toLowerCase();
48
+ const mimeType = String(file.type || "").toLowerCase();
49
+ const mimeTypeGroup = mimeType.split("/")[0];
50
+ const fileType = ["image", "video", "audio"].includes(mimeTypeGroup) ? mimeTypeGroup : "file";
51
+
52
+ const now = Date.now();
53
+ const categoryDir = extension ? extension.slice(1) : "files";
54
+ const monthDir = getMonthDir(now, befly.config?.tz);
55
+ const fileKey = Bun.randomUUIDv7();
56
+ const savedFileName = extension ? `${fileKey}${extension}` : fileKey;
57
+ const uploadDir = join(getAppPublicDir(befly.config?.publicDir || "./public"), categoryDir, monthDir);
58
+ const relativeFilePath = `/public/${categoryDir}/${monthDir}/${savedFileName}`;
59
+ const absoluteFileUrl = `${befly.config.publicHost}${relativeFilePath}`;
60
+ const isImage = fileType === "image" ? 1 : 0;
61
+
62
+ mkdirSync(uploadDir, { recursive: true });
63
+ await Bun.write(join(uploadDir, savedFileName), rawBuffer);
64
+
65
+ const insertRes = await befly.mysql.insData({
66
+ table: "beflyFile",
67
+ data: {
68
+ userId: ctx.userId,
69
+ filePath: relativeFilePath,
70
+ url: absoluteFileUrl,
71
+ isImage: isImage,
72
+ fileType: fileType,
73
+ fileSize: rawBuffer.byteLength,
74
+ fileKey: fileKey,
75
+ fileExt: extension,
76
+ fileName: originalFileName
77
+ }
78
+ });
79
+
80
+ if (!insertRes.data) {
81
+ return befly.tool.No("资源记录写入失败");
82
+ }
83
+
84
+ return befly.tool.Yes("上传成功", {
85
+ id: insertRes.data,
86
+ userId: ctx.userId,
87
+ filePath: relativeFilePath,
88
+ fileType: fileType,
89
+ fileSize: rawBuffer.byteLength,
90
+ fileKey: fileKey,
91
+ fileExt: extension,
92
+ fileName: originalFileName,
93
+ isImage: isImage,
94
+ url: absoluteFileUrl
95
+ });
96
+ } catch (error) {
97
+ Logger.error("上传文件失败", error, {
98
+ apiPath: ctx.apiPath,
99
+ userId: ctx.userId,
100
+ roleType: ctx.roleType
101
+ });
102
+ return befly.tool.No("上传文件失败");
103
+ }
104
+ }
105
+ };
package/checks/config.js CHANGED
@@ -17,9 +17,11 @@ const configSchema = z
17
17
  appName: noTrimString,
18
18
  appPort: z.int().min(1).max(65535),
19
19
  appHost: noTrimString,
20
+ publicHost: noTrimString,
20
21
  devEmail: z.union([z.literal(""), z.email()]),
21
22
  devPassword: z.string().min(6),
22
23
  bodyLimit: z.int().min(1),
24
+ uploadMaxSize: z.int().min(1),
23
25
  tz: z.string().refine((value) => isValidTimeZone(value), "无效的时区"),
24
26
  publicDir: noTrimString.min(1),
25
27
 
@@ -6,9 +6,10 @@
6
6
  "devEmail": "dev@qq.com",
7
7
  "devPassword": "111111",
8
8
  "bodyLimit": 1048576,
9
+ "uploadMaxSize": 20,
9
10
  "tz": "Asia/Shanghai",
10
11
  "publicDir": "./public",
11
-
12
+ "publicHost": "http://127.0.0.1:3000",
12
13
  "logger": {
13
14
  "debug": 1,
14
15
  "excludeFields": ["password", "token", "secret"],
@@ -86,5 +86,17 @@
86
86
  "sort": 4
87
87
  }
88
88
  ]
89
+ },
90
+ {
91
+ "name": "资源管理",
92
+ "path": "/resource",
93
+ "sort": 9004,
94
+ "children": [
95
+ {
96
+ "name": "图库管理",
97
+ "path": "/gallery",
98
+ "sort": 1
99
+ }
100
+ ]
89
101
  }
90
102
  ]
package/index.js CHANGED
@@ -208,15 +208,15 @@ export class Befly {
208
208
  fetch: async (req, server) => {
209
209
  const url = new URL(req.url);
210
210
 
211
- if (url.pathname === "/") {
212
- return Response.json({ code: 0, msg: `${this.context.config.appName} 接口服务已启动` });
213
- }
214
-
215
211
  if (url.pathname.startsWith("/api/")) {
216
212
  return apiFetch(req, server);
217
213
  }
218
214
 
219
- return staticFetch(req);
215
+ if (url.pathname.startsWith("/public/")) {
216
+ return staticFetch(req);
217
+ }
218
+
219
+ return Response.json({ code: 1, msg: "路径不存在" }, { status: 200 });
220
220
  },
221
221
  error: (error) => {
222
222
  Logger.error("服务启动时发生错误", error);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.22.0",
4
- "gitHead": "f3fd845d4fdbbf0db38a4e676162f3d710a9ce5b",
3
+ "version": "3.22.2",
4
+ "gitHead": "89a9ea96f129aa223da7a627ec8e7fb572438292",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
7
7
  "keywords": [
package/paths.js CHANGED
@@ -112,9 +112,9 @@ export const appApiDir = join(appDir, "apis");
112
112
  export const appTableDir = join(appDir, "tables");
113
113
 
114
114
  /**
115
- * 项目公共静态目录
115
+ * 项目上传/公共文件目录
116
116
  * @description 默认 {appDir}/public,可通过 config.publicDir 覆盖
117
- * @usage 用于静态文件访问与本地上传保存目录解析
117
+ * @usage 用于本地上传文件保存目录解析,不承担 HTTP 静态托管职责
118
118
  */
119
119
  export function getAppPublicDir(publicDir = "./public") {
120
120
  if (isAbsolute(publicDir)) {
package/router/static.js CHANGED
@@ -20,7 +20,8 @@ export function staticHandler(corsConfig = undefined, publicDir = "./public") {
20
20
  const corsHeaders = setCorsOptions(req, corsConfig);
21
21
 
22
22
  const url = new URL(req.url);
23
- const filePath = join(getAppPublicDir(publicDir), url.pathname);
23
+ const publicPath = url.pathname.replace(/^\/public/, "") || "/";
24
+ const filePath = join(getAppPublicDir(publicDir), publicPath);
24
25
 
25
26
  try {
26
27
  // OPTIONS预检请求
package/sql/befly.sql CHANGED
@@ -79,6 +79,26 @@ CREATE TABLE IF NOT EXISTS `befly_email_log` (
79
79
  PRIMARY KEY (`id`)
80
80
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
81
81
 
82
+ CREATE TABLE IF NOT EXISTS `befly_file` (
83
+ `id` BIGINT NOT NULL,
84
+ `user_id` BIGINT NOT NULL DEFAULT 0,
85
+ `file_path` VARCHAR(500) NOT NULL DEFAULT '',
86
+ `url` VARCHAR(1000) NOT NULL DEFAULT '',
87
+ `is_image` TINYINT NOT NULL DEFAULT 0,
88
+ `file_type` VARCHAR(20) NOT NULL DEFAULT '',
89
+ `file_size` BIGINT NOT NULL DEFAULT 0,
90
+ `file_key` VARCHAR(100) NOT NULL DEFAULT '',
91
+ `file_ext` VARCHAR(20) NOT NULL DEFAULT '',
92
+ `file_name` VARCHAR(200) NOT NULL DEFAULT '',
93
+ `state` TINYINT NOT NULL DEFAULT 1,
94
+ `created_at` BIGINT NOT NULL DEFAULT 0,
95
+ `updated_at` BIGINT NOT NULL DEFAULT 0,
96
+ `deleted_at` BIGINT NULL DEFAULT NULL,
97
+ PRIMARY KEY (`id`),
98
+ KEY `idx_befly_file_user_id` (`user_id`),
99
+ KEY `idx_befly_file_is_image` (`is_image`)
100
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
101
+
82
102
  CREATE TABLE IF NOT EXISTS `befly_login_log` (
83
103
  `id` BIGINT NOT NULL,
84
104
  `admin_id` BIGINT NOT NULL DEFAULT 0,
@@ -0,0 +1,81 @@
1
+ {
2
+ "id": {
3
+ "name": "ID",
4
+ "input": "integer",
5
+ "min": 1,
6
+ "max": null
7
+ },
8
+ "state": {
9
+ "name": "状态",
10
+ "input": "integer",
11
+ "min": 0,
12
+ "max": 2
13
+ },
14
+ "createdAt": {
15
+ "name": "创建时间",
16
+ "input": "number"
17
+ },
18
+ "updatedAt": {
19
+ "name": "更新时间",
20
+ "input": "number"
21
+ },
22
+ "deletedAt": {
23
+ "name": "删除时间",
24
+ "input": "number"
25
+ },
26
+ "userId": {
27
+ "name": "用户ID",
28
+ "input": "integer",
29
+ "min": 1,
30
+ "max": null
31
+ },
32
+ "filePath": {
33
+ "name": "文件路径",
34
+ "input": "string",
35
+ "min": 1,
36
+ "max": 500
37
+ },
38
+ "url": {
39
+ "name": "文件地址",
40
+ "input": "string",
41
+ "min": 1,
42
+ "max": 1000
43
+ },
44
+ "isImage": {
45
+ "name": "是否图片",
46
+ "input": "integer",
47
+ "min": 0,
48
+ "max": 1
49
+ },
50
+ "fileType": {
51
+ "name": "文件类型",
52
+ "input": "enum",
53
+ "check": "image|video|audio|file",
54
+ "min": 4,
55
+ "max": 5
56
+ },
57
+ "fileSize": {
58
+ "name": "文件大小",
59
+ "input": "integer",
60
+ "min": 0,
61
+ "max": null
62
+ },
63
+ "fileKey": {
64
+ "name": "文件标识",
65
+ "input": "string",
66
+ "min": 1,
67
+ "max": 100
68
+ },
69
+ "fileExt": {
70
+ "name": "文件扩展名",
71
+ "input": "string",
72
+ "min": 1,
73
+ "max": 20
74
+ },
75
+ "fileName": {
76
+ "name": "文件名",
77
+ "input": "string",
78
+ "min": 1,
79
+ "max": 200
80
+ }
81
+ }
package/utils/datetime.js CHANGED
@@ -1,38 +1,24 @@
1
- const visitStatsDateFormatter = new Intl.DateTimeFormat("en-CA", {
1
+ const ymdNumberFormatter = new Intl.DateTimeFormat("en-CA", {
2
2
  year: "numeric",
3
3
  month: "2-digit",
4
4
  day: "2-digit"
5
5
  });
6
6
 
7
- export const DAY_MS = 24 * 60 * 60 * 1000;
8
-
9
- function toDate(value = Date.now()) {
10
- if (value instanceof Date) {
11
- return value;
12
- }
13
-
14
- return Reflect.construct(Date, [value]);
15
- }
16
-
17
- function padDatePart(value) {
18
- if (value < 10) {
19
- return `0${value}`;
20
- }
7
+ const monthDirFormatterCache = new Map();
21
8
 
22
- return String(value);
23
- }
9
+ export const DAY_MS = 24 * 60 * 60 * 1000;
24
10
 
25
11
  export function addDays(timestamp = Date.now(), days = 0) {
26
12
  return Number(timestamp || 0) + Number(days || 0) * DAY_MS;
27
13
  }
28
14
 
29
15
  export function getDateYmdNumber(timestamp = Date.now()) {
30
- return Number(visitStatsDateFormatter.format(timestamp).replace(/[^0-9]/g, ""));
16
+ return Number(ymdNumberFormatter.format(timestamp).replace(/[^0-9]/g, ""));
31
17
  }
32
18
 
33
19
  export function getDayStartTime(timestamp = Date.now()) {
34
- const date = toDate(timestamp);
35
- return Reflect.construct(Date, [date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0]).getTime();
20
+ const date = new Date(timestamp);
21
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0).getTime();
36
22
  }
37
23
 
38
24
  export function getDayEndTime(timestamp = Date.now()) {
@@ -49,20 +35,31 @@ export function getTimeBucketStart(timestamp = Date.now(), bucketMs = 0) {
49
35
  return time - (time % bucket);
50
36
  }
51
37
 
38
+ export function getMonthDir(value = Date.now(), timeZone = "Asia/Shanghai") {
39
+ const zone = typeof timeZone === "string" && timeZone.trim().length > 0 ? timeZone.trim() : "Asia/Shanghai";
40
+
41
+ if (!monthDirFormatterCache.has(zone)) {
42
+ monthDirFormatterCache.set(
43
+ zone,
44
+ new Intl.DateTimeFormat("sv-SE", {
45
+ timeZone: zone,
46
+ year: "numeric",
47
+ month: "2-digit"
48
+ })
49
+ );
50
+ }
51
+
52
+ return monthDirFormatterCache.get(zone).format(value).replace("-", "");
53
+ }
54
+
52
55
  export function formatYmdHms(value = Date.now(), format = "dateTime") {
53
- const date = toDate(value);
56
+ const date = new Date(value);
54
57
  const y = date.getFullYear();
55
- const m = date.getMonth() + 1;
56
- const d = date.getDate();
57
- const h = date.getHours();
58
- const mi = date.getMinutes();
59
- const s = date.getSeconds();
60
-
61
- const mm = padDatePart(m);
62
- const dd = padDatePart(d);
63
- const hh = padDatePart(h);
64
- const mii = padDatePart(mi);
65
- const ss = padDatePart(s);
58
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
59
+ const dd = String(date.getDate()).padStart(2, "0");
60
+ const hh = String(date.getHours()).padStart(2, "0");
61
+ const mii = String(date.getMinutes()).padStart(2, "0");
62
+ const ss = String(date.getSeconds()).padStart(2, "0");
66
63
 
67
64
  if (format === "date") {
68
65
  return `${y}-${mm}-${dd}`;