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.
- package/apis/source/imageList.js +27 -0
- package/apis/upload/file.js +105 -0
- package/checks/config.js +2 -0
- package/configs/beflyConfig.json +2 -1
- package/configs/beflyMenus.json +12 -0
- package/index.js +5 -5
- package/package.json +2 -2
- package/paths.js +2 -2
- package/router/static.js +2 -1
- package/sql/befly.sql +20 -0
- package/tables/file.json +81 -0
- package/utils/datetime.js +29 -32
|
@@ -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
|
|
package/configs/beflyConfig.json
CHANGED
|
@@ -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"],
|
package/configs/beflyMenus.json
CHANGED
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
|
-
|
|
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.
|
|
4
|
-
"gitHead": "
|
|
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
|
|
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,
|
package/tables/file.json
ADDED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
35
|
-
return
|
|
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 =
|
|
56
|
+
const date = new Date(value);
|
|
54
57
|
const y = date.getFullYear();
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
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}`;
|