befly 3.75.3 → 3.76.1

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.
@@ -51,7 +51,8 @@ export default {
51
51
  username: ctx.body.username,
52
52
  password: hashedPassword,
53
53
  nickname: ctx.body.nickname,
54
- roleCode: ctx.body.roleCode
54
+ roleCode: ctx.body.roleCode,
55
+ roleType: "admin"
55
56
  }
56
57
  });
57
58
 
@@ -16,8 +16,7 @@ export default {
16
16
  lastLoginIp: adminTable.lastLoginIp,
17
17
  lastLoginTime: adminTable.lastLoginTime,
18
18
  password: adminTable.password,
19
- phone: adminTable.phone,
20
- roleType: adminTable.roleType
19
+ phone: adminTable.phone
21
20
  },
22
21
  required: ["id"],
23
22
  handler: async (befly, ctx) => {
@@ -79,7 +78,7 @@ export default {
79
78
  updateData.password = await hashPassword(ctx.body.password);
80
79
  }
81
80
  if (ctx.body.phone !== undefined) updateData.phone = ctx.body.phone;
82
- if (ctx.body.roleType !== undefined) updateData.roleType = ctx.body.roleType;
81
+ // 管理员表类型固定为 admin,roleType 不允许通过更新接口修改
83
82
  if (ctx.body.username !== undefined) updateData.username = ctx.body.username;
84
83
  if (ctx.body.nickname !== undefined) updateData.nickname = ctx.body.nickname;
85
84
  if (ctx.body.roleCode !== undefined) updateData.roleCode = ctx.body.roleCode;
@@ -0,0 +1,54 @@
1
+ import { Logger } from "../lib/logger.js";
2
+ import { formatValidationIssues } from "../utils/formatValidationIssues.js";
3
+ import { isFunction, isPlainObject, isValidCronSchedule } from "../utils/is.js";
4
+
5
+ // scanFiles 基础字段 + 定时器业务字段(schedule/handler;时区统一走 config.tz)
6
+ const cornKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "schedule", "handler"]);
7
+
8
+ export const checkCorn = (corns) => {
9
+ if (!Array.isArray(corns)) {
10
+ const errors = formatValidationIssues([{ path: [], message: "必须是数组" }], { item: corns, itemLabel: "corn" });
11
+ Logger.warn(`定时器校验失败`, { errors: errors });
12
+ return errors;
13
+ }
14
+
15
+ const errors = [];
16
+
17
+ // 任务名唯一性:文件名即任务名(core + app 合并后全局唯一,防止同名任务被 register 静默去重)
18
+ const nameMap = new Map();
19
+ for (const corn of corns) {
20
+ if (!corn?.fileName) continue;
21
+ nameMap.set(corn.fileName, (nameMap.get(corn.fileName) || 0) + 1);
22
+ }
23
+ const duplicateNames = [...nameMap].filter(([, count]) => count > 1).map(([name]) => name);
24
+ if (duplicateNames.length > 0) {
25
+ const duplicateCorns = corns.filter((corn) => duplicateNames.includes(corn.fileName));
26
+ errors.push(
27
+ ...formatValidationIssues(
28
+ duplicateCorns.map((corn, index) => ({ path: [index], message: `任务名 ${corn.fileName} 重复,请显式区分` })),
29
+ { items: duplicateCorns, itemLabel: "corn" }
30
+ )
31
+ );
32
+ }
33
+
34
+ for (const [index, corn] of corns.entries()) {
35
+ if (!isPlainObject(corn)) {
36
+ errors.push(...formatValidationIssues([{ path: [index], message: "必须是对象格式" }], { items: corns, itemLabel: "corn" }));
37
+ continue;
38
+ }
39
+
40
+ const issues = [];
41
+ for (const key of Object.keys(corn)) {
42
+ if (!cornKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
43
+ }
44
+ if (!isFunction(corn.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
45
+ if (!isValidCronSchedule(corn.schedule)) issues.push({ path: ["schedule"], message: "必须是可注册的 cron 表达式" });
46
+ if (issues.length === 0) continue;
47
+ errors.push(...formatValidationIssues(issues, { items: corns, itemLabel: "corn" }));
48
+ }
49
+
50
+ if (errors.length > 0) {
51
+ Logger.warn(`定时器校验失败`, { errors: errors });
52
+ }
53
+ return errors;
54
+ };
@@ -6,9 +6,11 @@ import { ErrorResponse } from "../utils/response.js";
6
6
  /**
7
7
  * 权限检查钩子
8
8
  * - 接口无需权限(auth=false):直接通过
9
- * - auth ctx.roleType 白名单(string[]):登录后按 ctx.roleType 放行
9
+ * - auth 为角色类型白名单(string[],如 ["admin"] / ["user"]):按 ctx.roleType 校验。
10
+ * 类型与角色解耦:管理员表(beflyAdmin)roleType 固定 "admin",用户表固定 "user",
11
+ * 任意角色 code 的管理员共享 "admin" 类型门槛,细粒度授权由下方集合检查(roleCode)负责
10
12
  * - 用户未登录:返回 401
11
- * - 开发者角色(dev):最高权限,直接通过
13
+ * - 开发者角色(dev):最高权限,直接通过(唯一内置角色,不可创建)
12
14
  * - 其他角色:检查 Redis 中的角色权限集合
13
15
  */
14
16
  export default {
package/index.js CHANGED
@@ -6,6 +6,7 @@
6
6
  // 检查
7
7
  import { checkApi } from "#befly/checks/api.js";
8
8
  import { checkConfig } from "#befly/checks/config.js";
9
+ import { checkCorn } from "#befly/checks/corns.js";
9
10
  import { checkHook, checkPlugin } from "#befly/checks/items.js";
10
11
  import { checkMenu } from "#befly/checks/menu.js";
11
12
  import { checkTable } from "#befly/checks/table.js";
@@ -18,6 +19,8 @@ import { BEFLY_ADMIN_TABLE, BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ROLE_TABLE
18
19
  import { Connect } from "#befly/lib/connect.js";
19
20
  // oxlint-disable-next-line unicorn/prefer-export-from -- Logger 同时被本文件大量使用,不能合并为 re-export
20
21
  import { Logger } from "#befly/lib/logger.js";
22
+ // 定时任务
23
+ import { registerCorns } from "#befly/plugins/cron.js";
21
24
  // 路由处理
22
25
  import { apiHandler } from "#befly/router/api.js";
23
26
  import { staticHandler } from "#befly/router/static.js";
@@ -108,7 +111,7 @@ export async function createBefly(config = {}, menus = []) {
108
111
  Logger.info(`启动 检查配置 耗时 ${calcPerfTime(stageStartTime)}`);
109
112
 
110
113
  stageStartTime = Bun.nanoseconds();
111
- const { apis, tables, plugins, hooks } = await scanSources();
114
+ const { apis, tables, plugins, hooks, corns } = await scanSources();
112
115
  Logger.info(`启动 扫描源码 耗时 ${calcPerfTime(stageStartTime)}`);
113
116
 
114
117
  stageStartTime = Bun.nanoseconds();
@@ -127,6 +130,10 @@ export async function createBefly(config = {}, menus = []) {
127
130
  const hookErrors = await checkHook(hooks);
128
131
  Logger.info(`启动 检查钩子 耗时 ${calcPerfTime(stageStartTime)}`);
129
132
 
133
+ stageStartTime = Bun.nanoseconds();
134
+ const cornErrors = await checkCorn(corns);
135
+ Logger.info(`启动 检查定时器 耗时 ${calcPerfTime(stageStartTime)}`);
136
+
130
137
  stageStartTime = Bun.nanoseconds();
131
138
  const menuErrors = await checkMenu(mergedMenus);
132
139
  Logger.info(`启动 检查菜单 耗时 ${calcPerfTime(stageStartTime)}`);
@@ -138,6 +145,7 @@ export async function createBefly(config = {}, menus = []) {
138
145
  { check: "表结构", errors: tableErrors },
139
146
  { check: "插件", errors: pluginErrors },
140
147
  { check: "钩子", errors: hookErrors },
148
+ { check: "定时器", errors: cornErrors },
141
149
  { check: "菜单", errors: menuErrors }
142
150
  ].filter((entry) => entry.errors.length > 0);
143
151
 
@@ -161,6 +169,7 @@ export async function createBefly(config = {}, menus = []) {
161
169
  apis: apis,
162
170
  hooks: hooks,
163
171
  plugins: plugins,
172
+ corns: corns,
164
173
  createdByFactory: true
165
174
  });
166
175
  }
@@ -179,6 +188,7 @@ export class Befly {
179
188
  this.hooks = Array.isArray(init.hooks) ? init.hooks : [];
180
189
  this.apis = Array.isArray(init.apis) ? init.apis : [];
181
190
  this.plugins = Array.isArray(init.plugins) ? init.plugins : [];
191
+ this.corns = Array.isArray(init.corns) ? init.corns : [];
182
192
  this.createdByFactory = init.createdByFactory === true;
183
193
  }
184
194
 
@@ -217,6 +227,11 @@ export class Befly {
217
227
  Logger.info(`启动 插件 ${item.fileName} 耗时 ${calcPerfTime(pluginStartTime)}`);
218
228
  }
219
229
 
230
+ // 注册定时器(依赖插件加载完成的 ctx.cron,非主进程自动跳过)
231
+ stageStartTime = Bun.nanoseconds();
232
+ registerCorns(this.context, this.corns);
233
+ Logger.info(`启动 注册定时器 耗时 ${calcPerfTime(stageStartTime)}`);
234
+
220
235
  stageStartTime = Bun.nanoseconds();
221
236
  await syncReady(this.context);
222
237
  Logger.info(`启动 同步就绪 耗时 ${calcPerfTime(stageStartTime)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.75.3",
3
+ "version": "3.76.1",
4
4
  "gitHead": "49c39d36695036e85fc64083cc43c1652fff96cb",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
package/paths.js CHANGED
@@ -46,6 +46,13 @@ export const coreCheckDir = join(moduleDir, "checks");
46
46
  */
47
47
  export const corePluginDir = join(moduleDir, "plugins");
48
48
 
49
+ /**
50
+ * Core 框架定时器目录
51
+ * @description packages/core/corns/
52
+ * @usage 存放框架内置定时任务(每文件一个定时器)
53
+ */
54
+ export const coreCornDir = join(moduleDir, "corns");
55
+
49
56
  /**
50
57
  * Core 框架钩子目录
51
58
  * @description packages/core/hooks/
@@ -90,6 +97,13 @@ export const appCheckDir = join(appDir, "checks");
90
97
  */
91
98
  export const appPluginDir = join(appDir, "plugins");
92
99
 
100
+ /**
101
+ * 项目定时器目录
102
+ * @description {appDir}/corns/
103
+ * @usage 存放用户业务定时任务(每文件一个定时器)
104
+ */
105
+ export const appCornDir = join(appDir, "corns");
106
+
93
107
  /**
94
108
  * 项目钩子目录
95
109
  * @description {appDir}/hooks/
package/plugins/cron.js CHANGED
@@ -4,7 +4,8 @@ import { PLUGIN_ORDER } from "../configs/constConfig.js";
4
4
  * 基于 Bun.cron 提供进程内定时任务能力。
5
5
  */
6
6
  import { Logger } from "../lib/logger.js";
7
- import { isPrimaryProcess } from "../utils/is.js";
7
+ import { createError } from "../utils/error.js";
8
+ import { isPrimaryProcess, isValidCronSchedule } from "../utils/is.js";
8
9
 
9
10
  const jobs = new Map();
10
11
 
@@ -24,15 +25,26 @@ function register(context, name, schedule, task) {
24
25
  return jobs.get(name);
25
26
  }
26
27
 
27
- const job = Bun.cron(schedule, async function () {
28
- Logger.info(`[定时任务] ${name} 开始执行`);
29
- try {
30
- await task(context);
31
- Logger.info(`[定时任务] ${name} 执行完成`);
32
- } catch (error) {
33
- Logger.error(`[定时任务] ${name} 执行失败`, error);
34
- }
35
- });
28
+ // 前置校验:表达式非法时启动即报错,避免 Bun.cron 注册时抛裸错
29
+ if (!isValidCronSchedule(schedule)) {
30
+ throw createError(`定时任务 ${name} 的 schedule 不是合法的 cron 表达式`, { code: "validation", subsystem: "cron", operation: "register" });
31
+ }
32
+
33
+ // 时区统一走 config.tz(checkConfig 已校验 IANA 合法性),未配置时使用系统时区
34
+ const tz = context.config?.tz;
35
+ const job = Bun.cron(
36
+ schedule,
37
+ async function () {
38
+ Logger.info(`[定时任务] ${name} 开始执行`);
39
+ try {
40
+ await task(context);
41
+ Logger.info(`[定时任务] ${name} 执行完成`);
42
+ } catch (error) {
43
+ Logger.error(`[定时任务] ${name} 执行失败`, error);
44
+ }
45
+ },
46
+ tz ? { tz: tz } : undefined
47
+ );
36
48
 
37
49
  jobs.set(name, job);
38
50
  return job;
@@ -73,6 +85,15 @@ function second(context, name, seconds, task) {
73
85
  return job;
74
86
  }
75
87
 
88
+ /**
89
+ * 批量注册扫描到的定时器(corns/*.js),任务名取文件名
90
+ */
91
+ export function registerCorns(context, corns) {
92
+ for (const corn of corns) {
93
+ register(context, corn.fileName, corn.schedule, corn.handler);
94
+ }
95
+ }
96
+
76
97
  /**
77
98
  * 停止所有已注册的定时任务
78
99
  */
package/sync/syncUtil.js CHANGED
@@ -1,23 +1,17 @@
1
1
  // 接口 auth 的存储协议(serializeAuth 与 authAllowsRole 配套使用):
2
- // - true -> "是"
3
- // - false -> "否"
4
- // - string[] -> "a,b,c"
5
- export const AUTH_YES = "是";
6
- export const AUTH_NO = "否";
7
-
8
- /**
9
- * 序列化 api.auth 为存储字符串
10
- */
2
+ // - true -> 1(需登录)
3
+ // - false -> 0(免登录)
4
+ // - string[] -> "a,b,c"(角色类型白名单,如 admin/user)
11
5
  export function serializeAuth(auth) {
12
- return auth === false ? AUTH_NO : Array.isArray(auth) ? auth.join(",") : AUTH_YES;
6
+ return auth === false ? 0 : Array.isArray(auth) ? auth.join(",") : 1;
13
7
  }
14
8
 
15
9
  /**
16
- * 判断 auth 存储串是否授权给指定角色
10
+ * 判断 auth 存储值是否授权给指定角色
17
11
  */
18
12
  export function authAllowsRole(auth, roleCode) {
19
- if (auth === AUTH_NO) return true;
20
- if (typeof auth === "string" && auth !== AUTH_YES) {
13
+ if (Number(auth) === 0) return true;
14
+ if (typeof auth === "string" && Number(auth) !== 1) {
21
15
  return auth.split(",").some((code) => code.trim() === roleCode);
22
16
  }
23
17
  return false;
package/tables/api.json CHANGED
@@ -1,4 +1,4 @@
1
- {
1
+ {
2
2
  "id": {
3
3
  "name": "ID",
4
4
  "input": "integer",
@@ -12,7 +12,7 @@
12
12
  "max": 100
13
13
  },
14
14
  "auth": {
15
- "name": "是否需要登录",
15
+ "name": "登录与权限(0=免登录,1=需登录,其他=角色类型白名单)",
16
16
  "input": "string",
17
17
  "min": 1,
18
18
  "max": 200
package/utils/is.js CHANGED
@@ -187,6 +187,20 @@ export function isValidTimeZone(value) {
187
187
  }
188
188
  }
189
189
 
190
+ /**
191
+ * 判断值是否为可注册的 cron 表达式(与 Bun.cron 注册行为一致:
192
+ * parse 抛异常或 8 年内无匹配都视为非法,避免启动后才报错)。
193
+ */
194
+ export function isValidCronSchedule(value) {
195
+ if (!isString(value)) return false;
196
+
197
+ try {
198
+ return Bun.cron.parse(value) !== null;
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+
190
204
  /**
191
205
  * 判断值是否为有限整数。
192
206
  */
@@ -7,7 +7,9 @@ import {
7
7
  coreHookDir,
8
8
  appHookDir,
9
9
  coreApiDir,
10
- appApiDir
10
+ appApiDir,
11
+ coreCornDir,
12
+ appCornDir
11
13
  } from "../paths.js";
12
14
  import { createError } from "./error.js";
13
15
  import { scanFiles } from "./scanFiles.js";
@@ -17,6 +19,7 @@ export const scanSources = async () => {
17
19
  const plugins = [];
18
20
  const hooks = [];
19
21
  const tables = {};
22
+ const corns = [];
20
23
 
21
24
  // 处理表格
22
25
  const allCoreTables = await scanFiles(coreTableDir, "core", "table", "*.json");
@@ -48,6 +51,16 @@ export const scanSources = async () => {
48
51
  hooks.push(item);
49
52
  }
50
53
 
54
+ // 处理定时器
55
+ const allCoreCorns = await scanFiles(coreCornDir, "core", "corn", "*.js");
56
+ for (const item of allCoreCorns) {
57
+ corns.push(item);
58
+ }
59
+ const allAppCorns = await scanFiles(appCornDir, "app", "corn", "*.js");
60
+ for (const item of allAppCorns) {
61
+ corns.push(item);
62
+ }
63
+
51
64
  // 处理接口
52
65
  const allCoreApis = await scanFiles(coreApiDir, "core", "api", "**/*.js");
53
66
  for (const item of allCoreApis) {
@@ -70,6 +83,7 @@ export const scanSources = async () => {
70
83
  hooks: hooks,
71
84
  plugins: plugins,
72
85
  apis: apis,
73
- tables: tables
86
+ tables: tables,
87
+ corns: corns
74
88
  };
75
89
  };
@@ -1,41 +0,0 @@
1
- import { getRoleById, systemRoleCodes } from "./_role.js";
2
-
3
- export default {
4
- name: "删除角色",
5
- method: "POST",
6
- body: "none",
7
- auth: true,
8
- fields: {
9
- id: { name: "ID", input: "integer", min: 1, max: null }
10
- },
11
- required: ["id"],
12
- handler: async (befly, ctx) => {
13
- const role = await getRoleById(befly, ctx.body.id, ["code"]);
14
-
15
- if (!role.data?.code) {
16
- return befly.tool.No("角色不存在");
17
- }
18
-
19
- if (systemRoleCodes.includes(role.data.code)) {
20
- return befly.tool.No(`系统角色 [${role.data.code}] 不允许删除`);
21
- }
22
-
23
- const adminList = await befly.mysql.getList({
24
- table: "beflyAdmin",
25
- where: { roleCode: role.data.code }
26
- });
27
-
28
- if (adminList.data.total > 0) {
29
- return befly.tool.No("该角色已分配给用户,无法删除");
30
- }
31
-
32
- await befly.mysql.delData({
33
- table: "beflyRole",
34
- where: { id: ctx.body.id }
35
- });
36
-
37
- await befly.cache.deleteRoleApis(role.data.code);
38
-
39
- return befly.tool.Yes("操作成功");
40
- }
41
- };
@@ -1,45 +0,0 @@
1
- import roleTable from "#befly/tables/role.json";
2
-
3
- export default {
4
- name: "创建角色",
5
- method: "POST",
6
- body: "none",
7
- auth: true,
8
- fields: {
9
- name: roleTable.name,
10
- code: roleTable.code,
11
- description: roleTable.description,
12
- menus: roleTable.menus,
13
- apis: roleTable.apis,
14
- sort: roleTable.sort
15
- },
16
- required: [],
17
- handler: async (befly, ctx) => {
18
- const menuPaths = ctx.body.menus || [];
19
- const apiPaths = ctx.body.apis || [];
20
- const existing = await befly.mysql.getOne({
21
- table: "beflyRole",
22
- where: { code: ctx.body.code }
23
- });
24
-
25
- if (existing.data?.id) {
26
- return befly.tool.No("角色代码已存在");
27
- }
28
-
29
- const roleId = await befly.mysql.insData({
30
- table: "beflyRole",
31
- data: {
32
- name: ctx.body.name,
33
- code: ctx.body.code,
34
- description: ctx.body.description,
35
- menus: menuPaths,
36
- apis: apiPaths,
37
- sort: ctx.body.sort
38
- }
39
- });
40
-
41
- await befly.cache.refreshRoleApis(ctx.body.code, apiPaths);
42
-
43
- return befly.tool.Yes("操作成功", { id: roleId.data });
44
- }
45
- };
package/apis/role/save.js DELETED
@@ -1,36 +0,0 @@
1
- import adminTable from "#befly/tables/admin.json";
2
- import roleTable from "#befly/tables/role.json";
3
-
4
- import { getRoleByCode } from "./_role.js";
5
-
6
- export default {
7
- name: "角色保存",
8
- method: "POST",
9
- body: "none",
10
- auth: true,
11
- fields: {
12
- adminId: adminTable.id,
13
- roleCode: roleTable.code
14
- },
15
- required: ["adminId", "roleCode"],
16
- handler: async (befly, ctx) => {
17
- const role = await getRoleByCode(befly, ctx.body.roleCode);
18
-
19
- if (!role.data?.id) {
20
- return befly.tool.No("角色不存在");
21
- }
22
-
23
- const roleType = role.data.code === "dev" || role.data.code === "admin" ? "admin" : "user";
24
-
25
- await befly.mysql.updData({
26
- table: "beflyAdmin",
27
- where: { id: ctx.body.adminId },
28
- data: {
29
- roleCode: role.data.code,
30
- roleType: roleType
31
- }
32
- });
33
-
34
- return befly.tool.Yes("操作成功");
35
- }
36
- };
@@ -1,66 +0,0 @@
1
- import roleTable from "#befly/tables/role.json";
2
-
3
- import { getRoleById } from "./_role.js";
4
-
5
- export default {
6
- name: "更新角色",
7
- method: "POST",
8
- body: "none",
9
- auth: true,
10
- fields: {
11
- id: roleTable.id,
12
- name: roleTable.name,
13
- code: roleTable.code,
14
- description: roleTable.description,
15
- menus: roleTable.menus,
16
- apis: roleTable.apis,
17
- sort: roleTable.sort
18
- },
19
- required: ["id"],
20
- handler: async (befly, ctx) => {
21
- const role = await getRoleById(befly, ctx.body.id);
22
-
23
- if (!role.data?.id) {
24
- return befly.tool.No("角色不存在");
25
- }
26
-
27
- if (ctx.body.code !== undefined) {
28
- const existing = await befly.mysql.getOne({
29
- table: "beflyRole",
30
- where: {
31
- code: ctx.body.code,
32
- id$not: ctx.body.id
33
- }
34
- });
35
-
36
- if (existing.data?.id) {
37
- return befly.tool.No("角色代码已被其他角色使用");
38
- }
39
- }
40
-
41
- const roleCode = ctx.body.code === undefined ? role.data.code : ctx.body.code;
42
- const apiPaths = ctx.body.apis === undefined ? role.data.apis || [] : ctx.body.apis;
43
- const menuPaths = ctx.body.menus === undefined ? role.data.menus || [] : ctx.body.menus;
44
-
45
- await befly.mysql.updData({
46
- table: "beflyRole",
47
- where: { id: ctx.body.id },
48
- data: {
49
- name: ctx.body.name,
50
- code: roleCode,
51
- description: ctx.body.description,
52
- menus: menuPaths,
53
- apis: apiPaths,
54
- sort: ctx.body.sort
55
- }
56
- });
57
-
58
- if (role.data.code !== roleCode) {
59
- await befly.cache.deleteRoleApis(role.data.code);
60
- }
61
-
62
- await befly.cache.refreshRoleApis(roleCode, apiPaths);
63
-
64
- return befly.tool.Yes("操作成功");
65
- }
66
- };