befly 3.35.0 → 3.37.0
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/dashboard/serviceStatus.js +1 -33
- package/apis/tongJi/_tongJi.js +48 -0
- package/apis/tongJi/errorReport.js +1 -21
- package/apis/tongJi/errorStats.js +65 -51
- package/apis/tongJi/infoReport.js +47 -148
- package/apis/tongJi/infoStats.js +151 -186
- package/apis/tongJi/onlineReport.js +24 -59
- package/apis/tongJi/onlineStats.js +34 -191
- package/apis/upload/file.js +24 -6
- package/checks/api.js +12 -1
- package/checks/config.js +26 -6
- package/checks/menu.js +1 -1
- package/configs/beflyConfig.json +11 -6
- package/index.js +1 -1
- package/lib/dbHelper.js +52 -0
- package/package.json +1 -1
- package/paths.js +1 -1
- package/router/api.js +17 -2
- package/router/static.js +36 -4
- package/sql/befly.sql +0 -32
- package/tables/errorReport.json +2 -1
- package/utils/cors.js +10 -19
- package/apis/tongJi/cacheHealth.js +0 -192
- package/apis/tongJi/fallbackReset.js +0 -61
- package/tables/infoReport.json +0 -123
|
@@ -1,177 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOnlineStatsActiveProductKey } from "./_tongJi.js";
|
|
2
2
|
|
|
3
|
-
import { getTongJiMonthStartDate, getTongJiNumber, getTongJiRecentDateList, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
4
|
-
|
|
5
|
-
const ONLINE_STATS_DAY_LIMIT = 30;
|
|
6
3
|
const ONLINE_STATS_ACTIVE_KEY = "online:active";
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
function getPeriodKeys(periodType, periodValue) {
|
|
10
|
-
return {
|
|
11
|
-
pv: `online:${periodType}:${periodValue}:pv`,
|
|
12
|
-
members: `online:${periodType}:${periodValue}:members`,
|
|
13
|
-
reportTime: `online:${periodType}:${periodValue}:reportTime`
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// 构造单产品统计的 Redis 键。
|
|
18
|
-
function getOnlineStatsProductPeriodKeys(periodType, periodValue, productName) {
|
|
19
|
-
const productKey = encodeURIComponent(productName);
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
pv: `online:${periodType}:${periodValue}:product:${productKey}:pv`,
|
|
23
|
-
members: `online:${periodType}:${periodValue}:product:${productKey}:members`,
|
|
24
|
-
reportTime: `online:${periodType}:${periodValue}:product:${productKey}:reportTime`
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 构造单产品实时在线集合的 Redis 键。
|
|
29
|
-
function getOnlineStatsActiveProductKey(productName) {
|
|
30
|
-
const productKey = encodeURIComponent(productName);
|
|
31
|
-
|
|
32
|
-
return `online:active:product:${productKey}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 读取 Redis 字符串并转成统计数字。
|
|
36
|
-
async function getRedisNumber(befly, key) {
|
|
37
|
-
return getTongJiNumber(await befly.redis.getString(key));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 读取某个周期的全量统计数据。
|
|
41
|
-
async function getOnlineStatsPeriodData(befly, periodType, periodValue) {
|
|
42
|
-
const keys = getPeriodKeys(periodType, periodValue);
|
|
43
|
-
return {
|
|
44
|
-
reportTime: await getRedisNumber(befly, keys.reportTime),
|
|
45
|
-
pv: await getRedisNumber(befly, keys.pv),
|
|
46
|
-
uv: getTongJiNumber(await befly.redis.scard(keys.members))
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 读取某个周期的单产品统计数据。
|
|
51
|
-
async function getOnlineStatsProductPeriodData(befly, periodType, periodValue, productName) {
|
|
52
|
-
const keys = getOnlineStatsProductPeriodKeys(periodType, periodValue, productName);
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
reportTime: await getRedisNumber(befly, keys.reportTime),
|
|
56
|
-
pv: await getRedisNumber(befly, keys.pv),
|
|
57
|
-
uv: getTongJiNumber(await befly.redis.scard(keys.members))
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// 清理过期成员后返回全站实时在线人数。
|
|
62
|
-
async function getOnlineStatsTotalOnlineCount(befly) {
|
|
63
|
-
const now = Date.now();
|
|
64
|
-
|
|
5
|
+
async function getOnlineStatsTotalOnlineCount(befly, now) {
|
|
65
6
|
await befly.redis.zremrangebyscore(ONLINE_STATS_ACTIVE_KEY, "-inf", now);
|
|
66
7
|
return await befly.redis.zcard(ONLINE_STATS_ACTIVE_KEY);
|
|
67
8
|
}
|
|
68
9
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const now = Date.now();
|
|
72
|
-
const productKey = getOnlineStatsActiveProductKey(productName);
|
|
10
|
+
async function getOnlineStatsProductOnlineCount(befly, productCode, now) {
|
|
11
|
+
const productKey = getOnlineStatsActiveProductKey(productCode);
|
|
73
12
|
|
|
74
13
|
await befly.redis.zremrangebyscore(productKey, "-inf", now);
|
|
75
14
|
return await befly.redis.zcard(productKey);
|
|
76
15
|
}
|
|
77
16
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const days = [];
|
|
81
|
-
|
|
82
|
-
for (const item of recentDateList) {
|
|
83
|
-
const dayData = await getOnlineStatsPeriodData(befly, "day", item);
|
|
84
|
-
|
|
85
|
-
if (dayData.reportTime <= 0) {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
17
|
+
async function buildOnlineStatsProducts(befly, projectLists, now) {
|
|
18
|
+
const products = [];
|
|
88
19
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
20
|
+
for (const item of projectLists) {
|
|
21
|
+
products.push({
|
|
22
|
+
key: item.productCode,
|
|
23
|
+
productName: item.productName,
|
|
24
|
+
productCode: item.productCode,
|
|
25
|
+
productVersion: item.productVersion || "",
|
|
26
|
+
onlineCount: await getOnlineStatsProductOnlineCount(befly, item.productCode, now)
|
|
94
27
|
});
|
|
95
28
|
}
|
|
96
29
|
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// 构造单产品统计的按天趋势列表。
|
|
101
|
-
async function buildOnlineStatsProductDays(befly, recentDateList, productName) {
|
|
102
|
-
const days = [];
|
|
103
|
-
|
|
104
|
-
for (const item of recentDateList) {
|
|
105
|
-
const dayData = await getOnlineStatsProductPeriodData(befly, "day", item, productName);
|
|
106
|
-
|
|
107
|
-
if (dayData.reportTime <= 0) {
|
|
108
|
-
continue;
|
|
30
|
+
return products.toSorted((a, b) => {
|
|
31
|
+
if (b.onlineCount !== a.onlineCount) {
|
|
32
|
+
return b.onlineCount - a.onlineCount;
|
|
109
33
|
}
|
|
110
34
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
reportTime: dayData.reportTime,
|
|
114
|
-
pv: dayData.pv,
|
|
115
|
-
uv: dayData.uv
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return days;
|
|
35
|
+
return String(a.productName).localeCompare(String(b.productName), "zh-CN");
|
|
36
|
+
});
|
|
120
37
|
}
|
|
121
38
|
|
|
122
|
-
|
|
123
|
-
async function buildOnlineStatsProducts(befly, recentDateList, reportDate, weekStartDate, monthStartDate, projectLists) {
|
|
39
|
+
function buildEmptyOnlineStatsProducts(projectLists) {
|
|
124
40
|
const products = [];
|
|
125
41
|
|
|
126
42
|
for (const item of projectLists) {
|
|
127
|
-
const productDays = await buildOnlineStatsProductDays(befly, recentDateList, item.productName);
|
|
128
|
-
let totalPv = 0;
|
|
129
|
-
|
|
130
|
-
for (const productDay of productDays) {
|
|
131
|
-
totalPv += productDay.pv;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const productToday = await getOnlineStatsProductPeriodData(befly, "day", reportDate, item.productName);
|
|
135
|
-
const productWeek = await getOnlineStatsProductPeriodData(befly, "week", weekStartDate, item.productName);
|
|
136
|
-
const productMonth = await getOnlineStatsProductPeriodData(befly, "month", monthStartDate, item.productName);
|
|
137
|
-
const productOnlineCount = await getOnlineStatsProductOnlineCount(befly, item.productName);
|
|
138
|
-
|
|
139
43
|
products.push({
|
|
140
44
|
key: item.productCode,
|
|
141
45
|
productName: item.productName,
|
|
142
46
|
productCode: item.productCode,
|
|
143
47
|
productVersion: item.productVersion || "",
|
|
144
|
-
onlineCount:
|
|
145
|
-
today: {
|
|
146
|
-
pv: productToday.pv,
|
|
147
|
-
uv: productToday.uv
|
|
148
|
-
},
|
|
149
|
-
week: {
|
|
150
|
-
pv: productWeek.pv,
|
|
151
|
-
uv: productWeek.uv
|
|
152
|
-
},
|
|
153
|
-
month: {
|
|
154
|
-
pv: productMonth.pv,
|
|
155
|
-
uv: productMonth.uv
|
|
156
|
-
},
|
|
157
|
-
days: productDays,
|
|
158
|
-
totalPv: totalPv
|
|
48
|
+
onlineCount: 0
|
|
159
49
|
});
|
|
160
50
|
}
|
|
161
51
|
|
|
162
|
-
|
|
163
|
-
if (b.totalPv !== a.totalPv) {
|
|
164
|
-
return b.totalPv - a.totalPv;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return String(a.productName).localeCompare(String(b.productName), "zh-CN");
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
for (const item of sortedProducts) {
|
|
171
|
-
delete item.totalPv;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return sortedProducts;
|
|
52
|
+
return products;
|
|
175
53
|
}
|
|
176
54
|
|
|
177
55
|
export default {
|
|
@@ -180,64 +58,29 @@ export default {
|
|
|
180
58
|
body: "none",
|
|
181
59
|
auth: true,
|
|
182
60
|
fields: {
|
|
183
|
-
|
|
61
|
+
productCode: { name: "产品代号", input: "string", min: 0, max: 100 }
|
|
184
62
|
},
|
|
185
63
|
required: [],
|
|
186
|
-
// 汇总当前查询范围的在线统计,并在未筛选产品时附带配置项目统计。
|
|
187
64
|
handler: async (befly, ctx) => {
|
|
188
65
|
const now = Date.now();
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
let monthCurrent;
|
|
200
|
-
let onlineCount;
|
|
201
|
-
|
|
202
|
-
if (hasProductFilter) {
|
|
203
|
-
currentDay = await getOnlineStatsProductPeriodData(befly, "day", reportDate, productName);
|
|
204
|
-
weekCurrent = await getOnlineStatsProductPeriodData(befly, "week", weekStartDate, productName);
|
|
205
|
-
monthCurrent = await getOnlineStatsProductPeriodData(befly, "month", monthStartDate, productName);
|
|
206
|
-
onlineCount = await getOnlineStatsProductOnlineCount(befly, productName);
|
|
207
|
-
} else {
|
|
208
|
-
currentDay = await getOnlineStatsPeriodData(befly, "day", reportDate);
|
|
209
|
-
weekCurrent = await getOnlineStatsPeriodData(befly, "week", weekStartDate);
|
|
210
|
-
monthCurrent = await getOnlineStatsPeriodData(befly, "month", monthStartDate);
|
|
211
|
-
onlineCount = await getOnlineStatsTotalOnlineCount(befly);
|
|
66
|
+
const productCode = ctx?.body?.productCode ?? "";
|
|
67
|
+
const hasProductFilter = productCode.length > 0;
|
|
68
|
+
const projectLists = hasProductFilter ? [] : befly.config?.projectLists || [];
|
|
69
|
+
|
|
70
|
+
if (!befly.redis) {
|
|
71
|
+
return befly.tool.Yes("获取成功", {
|
|
72
|
+
queryTime: now,
|
|
73
|
+
onlineCount: 0,
|
|
74
|
+
products: buildEmptyOnlineStatsProducts(projectLists)
|
|
75
|
+
});
|
|
212
76
|
}
|
|
213
77
|
|
|
214
|
-
const
|
|
215
|
-
const products = await buildOnlineStatsProducts(befly,
|
|
78
|
+
const onlineCount = hasProductFilter ? await getOnlineStatsProductOnlineCount(befly, productCode, now) : await getOnlineStatsTotalOnlineCount(befly, now);
|
|
79
|
+
const products = await buildOnlineStatsProducts(befly, projectLists, now);
|
|
216
80
|
|
|
217
81
|
return befly.tool.Yes("获取成功", {
|
|
218
82
|
queryTime: now,
|
|
219
83
|
onlineCount: onlineCount,
|
|
220
|
-
today: {
|
|
221
|
-
reportDate: reportDate,
|
|
222
|
-
reportTime: currentDay.reportTime,
|
|
223
|
-
pv: currentDay.pv,
|
|
224
|
-
uv: currentDay.uv
|
|
225
|
-
},
|
|
226
|
-
week: {
|
|
227
|
-
startDate: weekStartDate,
|
|
228
|
-
endDate: reportDate,
|
|
229
|
-
reportTime: weekCurrent.reportTime,
|
|
230
|
-
pv: weekCurrent.pv,
|
|
231
|
-
uv: weekCurrent.uv
|
|
232
|
-
},
|
|
233
|
-
month: {
|
|
234
|
-
startDate: monthStartDate,
|
|
235
|
-
endDate: reportDate,
|
|
236
|
-
reportTime: monthCurrent.reportTime,
|
|
237
|
-
pv: monthCurrent.pv,
|
|
238
|
-
uv: monthCurrent.uv
|
|
239
|
-
},
|
|
240
|
-
days: days,
|
|
241
84
|
products: products
|
|
242
85
|
});
|
|
243
86
|
}
|
package/apis/upload/file.js
CHANGED
|
@@ -14,7 +14,7 @@ export default {
|
|
|
14
14
|
required: [],
|
|
15
15
|
handler: async (befly, ctx) => {
|
|
16
16
|
try {
|
|
17
|
-
const maxFileSizeMb = Number(befly.config
|
|
17
|
+
const maxFileSizeMb = Number(befly.config.upload.maxSize);
|
|
18
18
|
const maxFileSize = maxFileSizeMb * 1024 * 1024;
|
|
19
19
|
const requestContentType = ctx.req.headers.get("content-type") || "";
|
|
20
20
|
|
|
@@ -22,6 +22,11 @@ export default {
|
|
|
22
22
|
return befly.tool.No("请使用 FormData 上传文件");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const requestContentLength = Number(ctx.req.headers.get("content-length") || 0);
|
|
26
|
+
if (Number.isFinite(requestContentLength) && requestContentLength > maxFileSize) {
|
|
27
|
+
return befly.tool.No(`文件不能超过${maxFileSizeMb}MB`);
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
if (!ctx.userId) {
|
|
26
31
|
return befly.tool.No("用户未登录");
|
|
27
32
|
}
|
|
@@ -33,6 +38,22 @@ export default {
|
|
|
33
38
|
return befly.tool.No("缺少上传文件");
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
const originalFileName = file.name || "未命名文件";
|
|
42
|
+
const extension = extname(originalFileName).toLowerCase();
|
|
43
|
+
const mimeType = String(file.type || "").toLowerCase();
|
|
44
|
+
|
|
45
|
+
if (!extension) {
|
|
46
|
+
return befly.tool.No("上传文件缺少扩展名");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!befly.config.upload.allowedExtensions.split(",").includes(extension)) {
|
|
50
|
+
return befly.tool.No(`不允许上传 ${extension} 类型文件`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!befly.config.upload.allowedMimeTypes.split(",").includes(mimeType)) {
|
|
54
|
+
return befly.tool.No("不允许上传该文件类型");
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
const rawBuffer = await file.arrayBuffer();
|
|
37
58
|
|
|
38
59
|
if (!rawBuffer || rawBuffer.byteLength <= 0) {
|
|
@@ -43,9 +64,6 @@ export default {
|
|
|
43
64
|
return befly.tool.No(`文件不能超过${maxFileSizeMb}MB`);
|
|
44
65
|
}
|
|
45
66
|
|
|
46
|
-
const originalFileName = file.name || "未命名文件";
|
|
47
|
-
const extension = extname(originalFileName).toLowerCase();
|
|
48
|
-
const mimeType = String(file.type || "").toLowerCase();
|
|
49
67
|
const mimeTypeGroup = mimeType.split("/")[0];
|
|
50
68
|
const fileType = ["image", "video", "audio"].includes(mimeTypeGroup) ? mimeTypeGroup : "file";
|
|
51
69
|
|
|
@@ -54,9 +72,9 @@ export default {
|
|
|
54
72
|
const monthDir = getMonthDir(now, befly.config?.tz);
|
|
55
73
|
const fileKey = Bun.randomUUIDv7();
|
|
56
74
|
const savedFileName = extension ? `${fileKey}${extension}` : fileKey;
|
|
57
|
-
const uploadDir = join(getAppPublicDir(befly.config
|
|
75
|
+
const uploadDir = join(getAppPublicDir(befly.config.upload.publicDir), categoryDir, monthDir);
|
|
58
76
|
const relativeFilePath = `/public/${categoryDir}/${monthDir}/${savedFileName}`;
|
|
59
|
-
const absoluteFileUrl = `${befly.config.publicHost}${relativeFilePath}`;
|
|
77
|
+
const absoluteFileUrl = `${befly.config.upload.publicHost}${relativeFilePath}`;
|
|
60
78
|
const isImage = fileType === "image" ? 1 : 0;
|
|
61
79
|
|
|
62
80
|
mkdirSync(uploadDir, { recursive: true });
|
package/checks/api.js
CHANGED
|
@@ -93,7 +93,18 @@ const apiSchema = z
|
|
|
93
93
|
fields: fieldsSchema,
|
|
94
94
|
required: z.array(noTrimString)
|
|
95
95
|
})
|
|
96
|
-
.strict()
|
|
96
|
+
.strict()
|
|
97
|
+
.superRefine((value, context) => {
|
|
98
|
+
if (value.source !== "app") {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (value.relativePath.split("/")[0] !== "core") {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
addIssue(context, ["relativePath"], "app API 不能使用 core 作为一级目录,避免占用 /api/core 命名空间");
|
|
107
|
+
});
|
|
97
108
|
|
|
98
109
|
const apiListSchema = z.array(apiSchema);
|
|
99
110
|
|
package/checks/config.js
CHANGED
|
@@ -10,6 +10,8 @@ z.config(z.locales.zhCN());
|
|
|
10
10
|
const boolIntSchema = z.union([z.literal(0), z.literal(1), z.literal(true), z.literal(false)]);
|
|
11
11
|
const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
|
|
12
12
|
const beflyModeSchema = z.union([z.literal("manual"), z.literal("auto")]);
|
|
13
|
+
const uploadExtensionListSchema = noTrimString.regex(/^\.[a-z0-9]+(?:,\.[a-z0-9]+)*$/);
|
|
14
|
+
const uploadMimeTypeListSchema = noTrimString.regex(/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*(?:,[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*)*$/);
|
|
13
15
|
const projectListCodeSchema = z.string().regex(/^[a-z][a-zA-Z0-9]*$/, "必须是小驼峰命名");
|
|
14
16
|
const projectListItemSchema = z
|
|
15
17
|
.object({
|
|
@@ -30,7 +32,7 @@ const projectListsSchema = z.array(projectListItemSchema).superRefine((projectLi
|
|
|
30
32
|
|
|
31
33
|
if (codeSet.has(code)) {
|
|
32
34
|
ctx.addIssue({
|
|
33
|
-
code:
|
|
35
|
+
code: "custom",
|
|
34
36
|
message: `projectLists[${index}].code 重复`,
|
|
35
37
|
path: [index, "code"]
|
|
36
38
|
});
|
|
@@ -41,7 +43,7 @@ const projectListsSchema = z.array(projectListItemSchema).superRefine((projectLi
|
|
|
41
43
|
|
|
42
44
|
if (productCodeSet.has(productCode)) {
|
|
43
45
|
ctx.addIssue({
|
|
44
|
-
code:
|
|
46
|
+
code: "custom",
|
|
45
47
|
message: `projectLists[${index}].productCode 重复`,
|
|
46
48
|
path: [index, "productCode"]
|
|
47
49
|
});
|
|
@@ -59,13 +61,20 @@ const configSchema = z
|
|
|
59
61
|
appPort: z.int().min(1).max(65535),
|
|
60
62
|
appHost: noTrimString,
|
|
61
63
|
apiHost: noTrimString,
|
|
62
|
-
publicHost: noTrimString,
|
|
63
64
|
devEmail: z.union([z.literal(""), z.email()]),
|
|
64
65
|
devPassword: z.string().min(6),
|
|
65
66
|
bodyLimit: z.int().min(1),
|
|
66
|
-
|
|
67
|
+
upload: z
|
|
68
|
+
.object({
|
|
69
|
+
maxSize: z.int().min(1),
|
|
70
|
+
publicDir: noTrimString.min(1),
|
|
71
|
+
publicHost: noTrimString,
|
|
72
|
+
allowedExtensions: uploadExtensionListSchema,
|
|
73
|
+
allowedMimeTypes: uploadMimeTypeListSchema,
|
|
74
|
+
forceDownloadExtensions: z.union([z.literal(""), uploadExtensionListSchema])
|
|
75
|
+
})
|
|
76
|
+
.strict(),
|
|
67
77
|
tz: z.string().refine((value) => isValidTimeZone(value), "无效的时区"),
|
|
68
|
-
publicDir: noTrimString.min(1),
|
|
69
78
|
excludeApisLog: z.array(noTrimString),
|
|
70
79
|
|
|
71
80
|
logger: z
|
|
@@ -128,7 +137,18 @@ const configSchema = z
|
|
|
128
137
|
maxAge: z.int().min(0),
|
|
129
138
|
credentials: z.union([z.literal("true"), z.literal("false"), z.literal(true), z.literal(false)])
|
|
130
139
|
})
|
|
131
|
-
.strict()
|
|
140
|
+
.strict()
|
|
141
|
+
.superRefine((cors, ctx) => {
|
|
142
|
+
if (cors.origin !== "*" || (cors.credentials !== true && cors.credentials !== "true")) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ctx.addIssue({
|
|
147
|
+
code: "custom",
|
|
148
|
+
message: "cors.credentials 为 true 时 cors.origin 不能为 *",
|
|
149
|
+
path: ["credentials"]
|
|
150
|
+
});
|
|
151
|
+
}),
|
|
132
152
|
|
|
133
153
|
rateLimit: z
|
|
134
154
|
.object({
|
package/checks/menu.js
CHANGED
|
@@ -36,7 +36,7 @@ const menuListSchema = z.array(menuSchema).superRefine((menuList, refineCtx) =>
|
|
|
36
36
|
const childFullPath = `${menu.path}${child.path}`;
|
|
37
37
|
if (!fullMenuPathRegex.test(childFullPath)) {
|
|
38
38
|
refineCtx.addIssue({
|
|
39
|
-
code:
|
|
39
|
+
code: "custom",
|
|
40
40
|
message: "菜单完整路径必须是 /core/xxx 或无前缀路径",
|
|
41
41
|
path: [menuIndex, "children", childIndex, "path"]
|
|
42
42
|
});
|
package/configs/beflyConfig.json
CHANGED
|
@@ -6,11 +6,16 @@
|
|
|
6
6
|
"devEmail": "dev@qq.com",
|
|
7
7
|
"devPassword": "111111",
|
|
8
8
|
"bodyLimit": 1048576,
|
|
9
|
-
"
|
|
9
|
+
"upload": {
|
|
10
|
+
"maxSize": 20,
|
|
11
|
+
"publicDir": "./public",
|
|
12
|
+
"publicHost": "http://127.0.0.1:3000",
|
|
13
|
+
"allowedExtensions": ".jpg,.jpeg,.png,.gif,.webp,.bmp,.mp4,.webm,.mp3,.wav,.pdf,.txt,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip",
|
|
14
|
+
"allowedMimeTypes": "image/jpeg,image/png,image/gif,image/webp,image/bmp,video/mp4,video/webm,audio/mpeg,audio/wav,audio/x-wav,application/pdf,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/zip,application/x-zip-compressed",
|
|
15
|
+
"forceDownloadExtensions": ".html,.htm,.js,.mjs,.css,.svg,.xml,.xhtml,.wasm,.json,.map"
|
|
16
|
+
},
|
|
10
17
|
"tz": "Asia/Shanghai",
|
|
11
18
|
"apiHost": "http://127.0.0.1",
|
|
12
|
-
"publicDir": "./public",
|
|
13
|
-
"publicHost": "http://127.0.0.1:3000",
|
|
14
19
|
"excludeApisLog": ["/api/core/tongJi/*Report"],
|
|
15
20
|
"logger": {
|
|
16
21
|
"debug": 1,
|
|
@@ -58,7 +63,7 @@
|
|
|
58
63
|
"allowedHeaders": "Content-Type,Authorization",
|
|
59
64
|
"exposedHeaders": "",
|
|
60
65
|
"maxAge": 86400,
|
|
61
|
-
"credentials": "
|
|
66
|
+
"credentials": "false"
|
|
62
67
|
},
|
|
63
68
|
|
|
64
69
|
"rateLimit": {
|
|
@@ -71,10 +76,10 @@
|
|
|
71
76
|
},
|
|
72
77
|
"projectLists": [
|
|
73
78
|
{
|
|
74
|
-
"code": "
|
|
79
|
+
"code": "beflyAdmin",
|
|
75
80
|
"name": "后台管理",
|
|
76
81
|
"productName": "后台管理",
|
|
77
|
-
"productCode": "
|
|
82
|
+
"productCode": "beflyAdmin",
|
|
78
83
|
"productVersion": "1.0.0"
|
|
79
84
|
}
|
|
80
85
|
]
|
package/index.js
CHANGED
|
@@ -214,7 +214,7 @@ export class Befly {
|
|
|
214
214
|
|
|
215
215
|
// 启动 HTTP服务器
|
|
216
216
|
const apiFetch = apiHandler(this.apis, this.hooks, this.context);
|
|
217
|
-
const staticFetch = staticHandler(this.context.config.cors, this.context.config.
|
|
217
|
+
const staticFetch = staticHandler(this.context.config.cors, this.context.config.upload);
|
|
218
218
|
|
|
219
219
|
const server = Bun.serve({
|
|
220
220
|
port: this.context.config.appPort || 3000,
|
package/lib/dbHelper.js
CHANGED
|
@@ -154,6 +154,42 @@ function assertWriteDataHasFields(data, message, table) {
|
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function getTableFieldNames(tableInfo) {
|
|
158
|
+
return Object.keys(tableInfo)
|
|
159
|
+
.map((fieldName) => snakeCase(fieldName))
|
|
160
|
+
.toSorted();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function assertWriteFieldNamesDefined(tableInfo, fieldNames, table, label) {
|
|
164
|
+
const allowedFields = getTableFieldNames(tableInfo);
|
|
165
|
+
const allowedFieldSet = new Set(allowedFields);
|
|
166
|
+
const invalidFields = [];
|
|
167
|
+
|
|
168
|
+
for (const fieldName of fieldNames) {
|
|
169
|
+
const snakeFieldName = snakeCase(fieldName);
|
|
170
|
+
|
|
171
|
+
if (!allowedFieldSet.has(snakeFieldName)) {
|
|
172
|
+
invalidFields.push(snakeFieldName);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (invalidFields.length < 1) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
throw new Error(`${label} 包含表 ${table} 未定义字段: ${invalidFields.join(", ")}`, {
|
|
181
|
+
cause: null,
|
|
182
|
+
code: "validation",
|
|
183
|
+
table: table,
|
|
184
|
+
invalidFields: invalidFields,
|
|
185
|
+
allowedFields: allowedFields
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function assertWriteFieldsDefined(tableInfo, data, table, label) {
|
|
190
|
+
assertWriteFieldNamesDefined(tableInfo, Object.keys(data), table, label);
|
|
191
|
+
}
|
|
192
|
+
|
|
157
193
|
function assertNoUndefinedInRecord(row, label) {
|
|
158
194
|
for (const [key, value] of Object.entries(row)) {
|
|
159
195
|
if (value === undefined) {
|
|
@@ -744,6 +780,7 @@ class DbHelper {
|
|
|
744
780
|
async insData(options) {
|
|
745
781
|
const parsed = this.createDbParse().parseInsert(options);
|
|
746
782
|
const { table, snakeTable, data } = parsed;
|
|
783
|
+
const tableInfo = this.getTableDef(table);
|
|
747
784
|
const inputData = this.prepareWriteInputData(data);
|
|
748
785
|
assertWriteDataHasFields(inputData, "插入数据必须至少有一个字段", snakeTable);
|
|
749
786
|
const now = Date.now();
|
|
@@ -751,6 +788,7 @@ class DbHelper {
|
|
|
751
788
|
const processed = insertRows.processedList[0];
|
|
752
789
|
|
|
753
790
|
assertWriteDataHasFields(processed, "插入数据必须至少有一个字段", snakeTable);
|
|
791
|
+
assertWriteFieldsDefined(tableInfo, processed, table, "insData.data");
|
|
754
792
|
|
|
755
793
|
assertNoUndefinedInRecord(processed, `insData 插入数据 (table: ${snakeTable})`);
|
|
756
794
|
|
|
@@ -779,11 +817,13 @@ class DbHelper {
|
|
|
779
817
|
assertInsertBatchSize(dataList.length, MAX_BATCH_SIZE);
|
|
780
818
|
|
|
781
819
|
const snakeTable = parsed.snakeTable;
|
|
820
|
+
const tableInfo = this.getTableDef(table);
|
|
782
821
|
const now = Date.now();
|
|
783
822
|
const insertRows = await this.createInsertRows(table, snakeTable, dataList, now);
|
|
784
823
|
const processedList = insertRows.processedList;
|
|
785
824
|
|
|
786
825
|
const insertFields = assertBatchInsertRowsConsistent(processedList, { table: snakeTable });
|
|
826
|
+
assertWriteFieldNamesDefined(tableInfo, insertFields, table, "insBatch.dataList");
|
|
787
827
|
const builder = this.createSqlBuilder();
|
|
788
828
|
const { sql, params } = builder.toInsertSql(snakeTable, processedList);
|
|
789
829
|
|
|
@@ -851,6 +891,7 @@ class DbHelper {
|
|
|
851
891
|
}
|
|
852
892
|
|
|
853
893
|
const snakeTable = parsed.snakeTable;
|
|
894
|
+
const tableInfo = this.getTableDef(table);
|
|
854
895
|
const now = Date.now();
|
|
855
896
|
|
|
856
897
|
const processedList = [];
|
|
@@ -873,6 +914,11 @@ class DbHelper {
|
|
|
873
914
|
sql: { sql: "", params: [], duration: 0 }
|
|
874
915
|
};
|
|
875
916
|
}
|
|
917
|
+
const writeFields = fields.slice();
|
|
918
|
+
if (this.beflyMode === "auto") {
|
|
919
|
+
writeFields.push("updated_at");
|
|
920
|
+
}
|
|
921
|
+
assertWriteFieldNamesDefined(tableInfo, writeFields, table, "updBatch.dataList");
|
|
876
922
|
|
|
877
923
|
const query = SqlBuilder.toUpdateCaseByIdSql({
|
|
878
924
|
table: snakeTable,
|
|
@@ -896,6 +942,7 @@ class DbHelper {
|
|
|
896
942
|
|
|
897
943
|
async updData(options) {
|
|
898
944
|
const parsed = this.createDbParse().parseUpdate(options);
|
|
945
|
+
const tableInfo = this.getTableDef(parsed.table);
|
|
899
946
|
const inputData = this.prepareWriteInputData(parsed.data);
|
|
900
947
|
assertWriteDataHasFields(inputData, "更新数据必须至少有一个字段", parsed.snakeTable);
|
|
901
948
|
|
|
@@ -906,6 +953,7 @@ class DbHelper {
|
|
|
906
953
|
beflyMode: this.beflyMode
|
|
907
954
|
});
|
|
908
955
|
assertWriteDataHasFields(processed, "更新数据必须至少有一个字段", parsed.snakeTable);
|
|
956
|
+
assertWriteFieldsDefined(tableInfo, processed, parsed.table, "updData.data");
|
|
909
957
|
const builder = this.createSqlBuilder().where(parsed.where);
|
|
910
958
|
const { sql, params } = builder.toUpdateSql(parsed.snakeTable, processed);
|
|
911
959
|
|
|
@@ -919,6 +967,7 @@ class DbHelper {
|
|
|
919
967
|
|
|
920
968
|
async delData(options) {
|
|
921
969
|
const parsed = this.createDbParse().parseDelete(options, false);
|
|
970
|
+
const tableInfo = this.getTableDef(parsed.table);
|
|
922
971
|
let processed;
|
|
923
972
|
|
|
924
973
|
if (parsed.deleteMode === "manual") {
|
|
@@ -932,6 +981,7 @@ class DbHelper {
|
|
|
932
981
|
updated_at: now
|
|
933
982
|
};
|
|
934
983
|
}
|
|
984
|
+
assertWriteFieldsDefined(tableInfo, processed, parsed.table, "delData.data");
|
|
935
985
|
|
|
936
986
|
const builder = this.createSqlBuilder().where(parsed.where);
|
|
937
987
|
const { sql, params } = builder.toUpdateSql(parsed.snakeTable, processed);
|
|
@@ -960,6 +1010,8 @@ class DbHelper {
|
|
|
960
1010
|
|
|
961
1011
|
async increment(table, field, where, value = 1) {
|
|
962
1012
|
const parsed = this.createDbParse().parseIncrement(table, field, where, value, "increment");
|
|
1013
|
+
const tableInfo = this.getTableDef(table);
|
|
1014
|
+
assertWriteFieldNamesDefined(tableInfo, [parsed.snakeField], table, "increment.field");
|
|
963
1015
|
|
|
964
1016
|
const builder = this.createSqlBuilder().where(parsed.where);
|
|
965
1017
|
const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
|
package/package.json
CHANGED
package/paths.js
CHANGED
|
@@ -113,7 +113,7 @@ export const appTableDir = join(appDir, "tables");
|
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* 项目上传/公共文件目录
|
|
116
|
-
* @description 默认 {appDir}/public,可通过 config.publicDir 覆盖
|
|
116
|
+
* @description 默认 {appDir}/public,可通过 config.upload.publicDir 覆盖
|
|
117
117
|
* @usage 用于本地上传文件保存目录解析,不承担 HTTP 静态托管职责
|
|
118
118
|
*/
|
|
119
119
|
export function getAppPublicDir(publicDir = "./public") {
|