befly-tpl 3.0.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.
Files changed (142) hide show
  1. package/.env.development +83 -0
  2. package/LICENSE +201 -0
  3. package/README.md +20 -0
  4. package/README.ts.md +175 -0
  5. package/addon-loader.example.ts +99 -0
  6. package/addons/_template/README.md +123 -0
  7. package/addons/_template/addon.config.json +17 -0
  8. package/addons/_template/apis/example.ts +33 -0
  9. package/addons/_template/checks/example.ts +23 -0
  10. package/addons/_template/config/default.ts +12 -0
  11. package/addons/_template/plugins/example.ts +39 -0
  12. package/addons/_template/tables/example.json +7 -0
  13. package/addons/_template/types/index.d.ts +21 -0
  14. package/addons/admin/README.md +179 -0
  15. package/addons/admin/addon.config.json +13 -0
  16. package/addons/admin/apis/adminDel.ts +37 -0
  17. package/addons/admin/apis/adminInfo.ts +50 -0
  18. package/addons/admin/apis/adminIns.ts +70 -0
  19. package/addons/admin/apis/adminList.ts +24 -0
  20. package/addons/admin/apis/adminRoleDetail.ts +38 -0
  21. package/addons/admin/apis/adminRoleSave.ts +40 -0
  22. package/addons/admin/apis/adminUpd.ts +54 -0
  23. package/addons/admin/apis/apiAll.ts +38 -0
  24. package/addons/admin/apis/cacheRefresh.ts +36 -0
  25. package/addons/admin/apis/dashboardAddonList.ts +16 -0
  26. package/addons/admin/apis/dashboardChangelog.ts +41 -0
  27. package/addons/admin/apis/dashboardConfigStatus.ts +56 -0
  28. package/addons/admin/apis/dashboardEnvironmentInfo.ts +48 -0
  29. package/addons/admin/apis/dashboardPerformanceMetrics.ts +25 -0
  30. package/addons/admin/apis/dashboardPermissionStats.ts +33 -0
  31. package/addons/admin/apis/dashboardServiceStatus.ts +84 -0
  32. package/addons/admin/apis/dashboardSystemInfo.ts +34 -0
  33. package/addons/admin/apis/dashboardSystemOverview.ts +34 -0
  34. package/addons/admin/apis/dashboardSystemResources.ts +119 -0
  35. package/addons/admin/apis/dictAll.ts +26 -0
  36. package/addons/admin/apis/dictDel.ts +27 -0
  37. package/addons/admin/apis/dictDetail.ts +28 -0
  38. package/addons/admin/apis/dictIns.ts +38 -0
  39. package/addons/admin/apis/dictList.ts +27 -0
  40. package/addons/admin/apis/dictUpd.ts +44 -0
  41. package/addons/admin/apis/login.ts +123 -0
  42. package/addons/admin/apis/logout.ts +23 -0
  43. package/addons/admin/apis/menuAll.ts +70 -0
  44. package/addons/admin/apis/menuDel.ts +40 -0
  45. package/addons/admin/apis/menuIns.ts +31 -0
  46. package/addons/admin/apis/menuList.ts +26 -0
  47. package/addons/admin/apis/menuUpd.ts +41 -0
  48. package/addons/admin/apis/register.ts +50 -0
  49. package/addons/admin/apis/roleApiDetail.ts +34 -0
  50. package/addons/admin/apis/roleApiSave.ts +44 -0
  51. package/addons/admin/apis/roleDel.ts +48 -0
  52. package/addons/admin/apis/roleDetail.ts +28 -0
  53. package/addons/admin/apis/roleIns.ts +40 -0
  54. package/addons/admin/apis/roleList.ts +18 -0
  55. package/addons/admin/apis/roleMenuDetail.ts +33 -0
  56. package/addons/admin/apis/roleMenuSave.ts +39 -0
  57. package/addons/admin/apis/roleSave.ts +45 -0
  58. package/addons/admin/apis/roleUpd.ts +53 -0
  59. package/addons/admin/apis/sendSmsCode.ts +36 -0
  60. package/addons/admin/checks/admin.ts +36 -0
  61. package/addons/admin/config/index.ts +45 -0
  62. package/addons/admin/config/menu.json +44 -0
  63. package/addons/admin/scripts/syncApi.ts +285 -0
  64. package/addons/admin/scripts/syncDev.ts +203 -0
  65. package/addons/admin/scripts/syncMenu.ts +210 -0
  66. package/addons/admin/tables/admin.json +14 -0
  67. package/addons/admin/tables/api.json +8 -0
  68. package/addons/admin/tables/dict.json +8 -0
  69. package/addons/admin/tables/menu.json +8 -0
  70. package/addons/admin/tables/role.json +8 -0
  71. package/addons/admin/types/index.ts +44 -0
  72. package/addons/admin/util.ts +266 -0
  73. package/addons/befly/addon.config.json +13 -0
  74. package/addons/befly/apis/health/info.ts +77 -0
  75. package/addons/befly/apis/tool/tokenCheck.ts +52 -0
  76. package/addons/demo/README.md +62 -0
  77. package/addons/demo/addon.config.json +13 -0
  78. package/addons/demo/apis/demoIns.ts +36 -0
  79. package/addons/demo/apis/demoList.ts +36 -0
  80. package/addons/demo/checks/demo.ts +30 -0
  81. package/addons/demo/config/default.ts +17 -0
  82. package/addons/demo/plugins/tool.ts +61 -0
  83. package/addons/demo/tables/todo.json +6 -0
  84. package/addons/demo/types/index.d.ts +56 -0
  85. package/apis/article/articleDel.ts +33 -0
  86. package/apis/article/articleDetail.ts +26 -0
  87. package/apis/article/articleIns.ts +47 -0
  88. package/apis/article/articleList.ts +47 -0
  89. package/apis/article/articleUpd.ts +55 -0
  90. package/apis/article/increment.ts +37 -0
  91. package/apis/test/hi.ts +9 -0
  92. package/apis/user/login.ts +56 -0
  93. package/apis/user/userList.ts +40 -0
  94. package/bun.lock +140 -0
  95. package/checks/demo.ts +25 -0
  96. package/logs/2025-08-22.0.log +197 -0
  97. package/logs/2025-08-23.0.log +151 -0
  98. package/logs/2025-08-24.0.log +296 -0
  99. package/logs/2025-08-25.0.log +162 -0
  100. package/logs/2025-08-26.0.log +19 -0
  101. package/logs/2025-08-27.0.log +63 -0
  102. package/logs/2025-08-28.0.log +286 -0
  103. package/logs/2025-08-30.0.log +1 -0
  104. package/logs/2025-09-01.0.log +296 -0
  105. package/logs/2025-09-02.0.log +298 -0
  106. package/logs/2025-10-11.0.log +2718 -0
  107. package/logs/2025-10-12.0.log +4374 -0
  108. package/logs/2025-10-13.0.log +759 -0
  109. package/logs/2025-10-14.0.log +2350 -0
  110. package/logs/2025-10-15.0.log +2386 -0
  111. package/logs/2025-10-16.0.log +2807 -0
  112. package/logs/2025-10-17.0.log +1143 -0
  113. package/logs/2025-10-18.0.log +1292 -0
  114. package/logs/2025-10-19.0.log +1752 -0
  115. package/logs/2025-10-20.0.log +722 -0
  116. package/logs/2025-10-21.0.log +1075 -0
  117. package/logs/2025-10-23.0.log +3291 -0
  118. package/logs/2025-10-24.0.log +2341 -0
  119. package/logs/2025-10-25.0.log +1367 -0
  120. package/logs/debug.0.log +25174 -0
  121. package/main.ts +9 -0
  122. package/package.json +29 -0
  123. package/pm2.config.cjs +85 -0
  124. package/tables/article.json +11 -0
  125. package/tables/user.json +8 -0
  126. package/temp/addon-route-prefix-migration.md +400 -0
  127. package/temp/api-route-conflict-analysis.md +441 -0
  128. package/temp/interactive-cli-guide.md +199 -0
  129. package/temp/missing-apis-fix.md +362 -0
  130. package/temp/remove-status-field.md +239 -0
  131. package/temp/roleid-to-rolecode-optimization.md +321 -0
  132. package/temp/status-to-state-migration-complete.md +176 -0
  133. package/temp/syncMenu-guide.md +235 -0
  134. package/temp/test-admin-menus-cache.ts +125 -0
  135. package/temp/test-admin-menus.ts +110 -0
  136. package/temp/test-interactive-cli.ps1 +14 -0
  137. package/tests/core.test.ts +13 -0
  138. package/tsconfig.json +23 -0
  139. package/types/api.ts +128 -0
  140. package/types/index.ts +6 -0
  141. package/types/models.example.ts +267 -0
  142. package/types/models.ts +67 -0
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * 同步 API 接口数据到数据库
4
+ * 说明:遍历所有 API 文件,收集接口路由信息并同步到 addon_admin_api 表
5
+ *
6
+ * 流程:
7
+ * 1. 扫描 tpl/apis 目录下所有 API 文件
8
+ * 2. 扫描 tpl/addons/组件/apis 目录下所有 API 文件
9
+ * 3. 提取每个 API 的 name、method、auth 等信息
10
+ * 4. 根据接口路径检查是否存在
11
+ * 5. 存在则更新,不存在则新增
12
+ * 6. 删除配置中不存在的接口记录
13
+ */
14
+
15
+ import { Env, Logger, initDatabase, closeDatabase } from 'befly';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { readdirSync, statSync } from 'node:fs';
19
+ import { scanTsFiles, checkTableExists, deleteObsoleteRecords, logSyncStats, getAddonDirs } from '../util';
20
+
21
+ // CLI 参数类型
22
+ interface CliArgs {
23
+ DRY_RUN: boolean;
24
+ }
25
+
26
+ // API 信息类型
27
+ interface ApiInfo {
28
+ name: string;
29
+ path: string;
30
+ method: string;
31
+ description: string;
32
+ addonName: string;
33
+ addonTitle: string;
34
+ }
35
+
36
+ // 解析命令行参数
37
+ const CLI: CliArgs = {
38
+ DRY_RUN: process.argv.includes('--plan')
39
+ };
40
+
41
+ const __filename = fileURLToPath(import.meta.url);
42
+ const __dirname = path.dirname(__filename);
43
+ const tplDir = path.resolve(__dirname, '../../../');
44
+
45
+ /**
46
+ * 从 API 文件中提取接口信息
47
+ * @param filePath - API 文件路径
48
+ * @param addonName - 插件名称
49
+ * @param addonTitle - 插件标题
50
+ * @returns API 信息对象或 null
51
+ */
52
+ async function extractApiInfo(filePath: string, addonName: string = '', addonTitle: string = ''): Promise<ApiInfo | null> {
53
+ try {
54
+ // 动态导入 API 文件
55
+ const apiModule = await import(filePath);
56
+ const apiConfig = apiModule.default;
57
+
58
+ if (!apiConfig || !apiConfig.name) {
59
+ return null;
60
+ }
61
+
62
+ // 构建接口路径
63
+ let apiPath = '';
64
+ if (addonName) {
65
+ // 插件接口:/addon/{addonName}/{fileName}
66
+ const fileName = path.basename(filePath, '.ts');
67
+ apiPath = `/addon/${addonName}/${fileName}`;
68
+ } else {
69
+ // 项目接口:/api/{dirName}/{fileName}
70
+ const relativePath = path.relative(path.join(tplDir, 'apis'), filePath);
71
+ const parts = relativePath.replace(/\\/g, '/').split('/');
72
+ const dirName = parts[0];
73
+ const fileName = path.basename(filePath, '.ts');
74
+ apiPath = `/api/${dirName}/${fileName}`;
75
+ }
76
+
77
+ return {
78
+ name: apiConfig.name || '',
79
+ path: apiPath,
80
+ method: apiConfig.method || 'POST',
81
+ description: apiConfig.description || '',
82
+ addonName: addonName,
83
+ addonTitle: addonTitle || addonName
84
+ };
85
+ } catch (error: any) {
86
+ Logger.warn(`解析 API 文件失败: ${filePath}`, error.message);
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 读取 Addon 配置文件(使用 Bun 原生方法)
93
+ * @param addonDir - Addon 目录路径
94
+ * @returns Addon 配置对象或 null
95
+ */
96
+ async function loadAddonConfig(addonDir: string): Promise<{ name: string; title: string } | null> {
97
+ try {
98
+ const configPath = path.join(addonDir, 'addon.config.json');
99
+ const file = Bun.file(configPath);
100
+ const config = await file.json();
101
+ return {
102
+ name: config.name || '',
103
+ title: config.title || config.name || ''
104
+ };
105
+ } catch (error) {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 扫描所有 API 文件
112
+ * @returns API 信息数组
113
+ */
114
+ async function scanAllApis(): Promise<ApiInfo[]> {
115
+ const apis: ApiInfo[] = [];
116
+
117
+ // 1. 扫描项目 API (tpl/apis)
118
+ Logger.info('=== 扫描项目 API (tpl/apis) ===');
119
+ const projectApisDir = path.join(tplDir, 'apis');
120
+ const projectApiFiles = scanTsFiles(projectApisDir);
121
+ Logger.info(` 找到 ${projectApiFiles.length} 个项目 API 文件`);
122
+
123
+ for (const filePath of projectApiFiles) {
124
+ const apiInfo = await extractApiInfo(filePath, '', '项目接口');
125
+ if (apiInfo) {
126
+ apis.push(apiInfo);
127
+ Logger.info(` └ ${apiInfo.path} - ${apiInfo.name}`);
128
+ }
129
+ }
130
+
131
+ // 2. 扫描插件 API (tpl/addons/*/apis)
132
+ Logger.info('\n=== 扫描插件 API (tpl/addons/*/apis) ===');
133
+ const addonsDir = path.join(tplDir, 'addons');
134
+ const addonDirs = getAddonDirs(addonsDir);
135
+
136
+ for (const addonName of addonDirs) {
137
+ const addonDir = path.join(addonsDir, addonName);
138
+ const addonApisDir = path.join(addonDir, 'apis');
139
+
140
+ // 读取 addon 配置(使用 Bun 原生方法)
141
+ const addonConfig = await loadAddonConfig(addonDir);
142
+ const addonTitle = addonConfig?.title || addonName;
143
+
144
+ try {
145
+ const addonApiFiles = scanTsFiles(addonApisDir);
146
+ Logger.info(` [${addonName}] 找到 ${addonApiFiles.length} 个 API 文件`);
147
+
148
+ for (const filePath of addonApiFiles) {
149
+ const apiInfo = await extractApiInfo(filePath, addonName, addonTitle);
150
+ if (apiInfo) {
151
+ apis.push(apiInfo);
152
+ Logger.info(` └ ${apiInfo.path} - ${apiInfo.name}`);
153
+ }
154
+ }
155
+ } catch (error: any) {
156
+ Logger.warn(` [${addonName}] 扫描失败:`, error.message);
157
+ }
158
+ }
159
+
160
+ return apis;
161
+ }
162
+
163
+ /**
164
+ * 同步 API 数据到数据库
165
+ * @param helper - DbHelper 实例
166
+ * @param apis - API 信息数组
167
+ * @returns 同步统计信息
168
+ */
169
+ async function syncApis(helper: any, apis: ApiInfo[]): Promise<{ created: number; updated: number }> {
170
+ const stats = { created: 0, updated: 0 };
171
+
172
+ for (const api of apis) {
173
+ try {
174
+ const existing = await helper.getOne({
175
+ table: 'addon_admin_api',
176
+ where: { path: api.path }
177
+ });
178
+
179
+ if (existing) {
180
+ // 存在则更新
181
+ await helper.updData({
182
+ table: 'addon_admin_api',
183
+ where: { id: existing.id },
184
+ data: {
185
+ name: api.name,
186
+ method: api.method,
187
+ description: api.description,
188
+ addonName: api.addonName,
189
+ addonTitle: api.addonTitle
190
+ }
191
+ });
192
+ stats.updated++;
193
+ Logger.info(` └ 更新接口: ${api.name} (ID: ${existing.id}, Path: ${api.path})`);
194
+ } else {
195
+ // 不存在则新增
196
+ const id = await helper.insData({
197
+ table: 'addon_admin_api',
198
+ data: {
199
+ name: api.name,
200
+ path: api.path,
201
+ method: api.method,
202
+ description: api.description,
203
+ addonName: api.addonName,
204
+ addonTitle: api.addonTitle
205
+ }
206
+ });
207
+ stats.created++;
208
+ Logger.info(` └ 新增接口: ${api.name} (ID: ${id}, Path: ${api.path})`);
209
+ }
210
+ } catch (error: any) {
211
+ Logger.error(`同步接口 "${api.name}" 失败:`, error.message || String(error));
212
+ }
213
+ }
214
+
215
+ return stats;
216
+ }
217
+
218
+ /**
219
+ * 同步 API 主函数
220
+ */
221
+ async function syncApi(): Promise<boolean> {
222
+ let dbInitialized = false;
223
+
224
+ try {
225
+ if (CLI.DRY_RUN) {
226
+ Logger.info('[计划] 同步 API 接口到数据库(plan 模式不执行)');
227
+ Logger.info('[计划] 1. 扫描 tpl/apis 和 tpl/addons/*/apis 目录');
228
+ Logger.info('[计划] 2. 提取每个 API 的配置信息');
229
+ Logger.info('[计划] 3. 根据 path 检查接口是否存在');
230
+ Logger.info('[计划] 4. 存在则更新,不存在则新增');
231
+ Logger.info('[计划] 5. 删除文件中不存在的接口记录');
232
+ return true;
233
+ }
234
+
235
+ Logger.info('开始同步 API 接口到数据库...\n');
236
+
237
+ // 初始化数据库连接
238
+ const { helper } = await initDatabase({ max: 1 });
239
+ dbInitialized = true;
240
+
241
+ // 1. 检查表是否存在
242
+ if (!(await checkTableExists(helper, 'addon_admin_api'))) {
243
+ return false;
244
+ }
245
+
246
+ Logger.info('');
247
+
248
+ // 2. 扫描所有 API 文件
249
+ Logger.info('=== 步骤 2: 扫描 API 文件 ===');
250
+ const apis = await scanAllApis();
251
+ const apiPaths = new Set(apis.map((api) => api.path));
252
+ Logger.info(`\n✅ 共扫描到 ${apis.length} 个 API 接口\n`);
253
+
254
+ // 3. 同步 API 数据(新增和更新)
255
+ Logger.info('=== 步骤 3: 同步 API 数据(新增/更新) ===');
256
+ const stats = await syncApis(helper, apis);
257
+
258
+ // 4. 删除文件中不存在的接口
259
+ const deletedCount = await deleteObsoleteRecords(helper, 'addon_admin_api', apiPaths);
260
+
261
+ // 5. 输出统计信息
262
+ logSyncStats(stats, deletedCount, '接口');
263
+ Logger.info(`当前总接口数: ${apis.length} 个`);
264
+ Logger.info('提示: 接口缓存将在服务器启动时自动完成');
265
+
266
+ return true;
267
+ } catch (error: any) {
268
+ Logger.error('API 同步失败:', { message: error.message, stack: error.stack });
269
+ return false;
270
+ } finally {
271
+ if (dbInitialized) {
272
+ await closeDatabase();
273
+ }
274
+ }
275
+ }
276
+
277
+ // 执行同步
278
+ syncApi()
279
+ .then((success) => {
280
+ process.exit(success ? 0 : 1);
281
+ })
282
+ .catch((error) => {
283
+ Logger.error('执行失败:', error);
284
+ process.exit(1);
285
+ });
@@ -0,0 +1,203 @@
1
+ /**
2
+ * 同步开发者管理员到数据库(使用 dbHelper)
3
+ * - 邮箱: dev@qq.com
4
+ * - 姓名: 开发者
5
+ * - 密码: 使用 bcrypt 加密
6
+ * - 角色: roleCode=dev, roleType=admin
7
+ * - 表名: addon_admin_admin
8
+ */
9
+
10
+ import { Env, Logger, Crypto2, initDatabase, closeDatabase } from 'befly';
11
+
12
+ // CLI 参数类型
13
+ interface CliArgs {
14
+ DRY_RUN: boolean;
15
+ }
16
+
17
+ // 解析命令行参数
18
+ const ARGV = Array.isArray(process.argv) ? process.argv : [];
19
+ const CLI: CliArgs = { DRY_RUN: ARGV.includes('--plan') };
20
+
21
+ /**
22
+ * 同步开发管理员账号(使用统一的 database 工具)
23
+ * 表名: addon_admin_admin
24
+ * 邮箱: dev@qq.com
25
+ * 用户名: dev
26
+ * 姓名: 开发者
27
+ * 角色: roleCode=dev, roleType=admin
28
+ * @returns 是否成功
29
+ */
30
+ export async function SyncDev(): Promise<boolean> {
31
+ let dbInitialized = false;
32
+
33
+ try {
34
+ if (CLI.DRY_RUN) {
35
+ Logger.info('[计划] 同步完成后将初始化/更新开发管理员账号(plan 模式不执行)');
36
+ return true;
37
+ }
38
+
39
+ if (!Env.DEV_PASSWORD || !Env.MD5_SALT) {
40
+ Logger.warn('跳过开发管理员初始化:缺少 DEV_PASSWORD 或 MD5_SALT 配置');
41
+ return false;
42
+ }
43
+
44
+ // 初始化数据库连接(Redis + SQL + DbHelper)
45
+ const { helper } = await initDatabase({ max: 1 });
46
+ dbInitialized = true;
47
+
48
+ // 检查 addon_admin_admin 表是否存在(使用 helper.query 执行元数据查询)
49
+ const existAdmin = await helper.tableExists('addon_admin_admin');
50
+ if (!existAdmin) {
51
+ Logger.warn('跳过开发管理员初始化:未检测到 addon_admin_admin 表');
52
+ return false;
53
+ }
54
+
55
+ // 检查 addon_admin_role 表是否存在
56
+ const existRole = await helper.tableExists('addon_admin_role');
57
+ if (!existRole) {
58
+ Logger.warn('跳过开发管理员初始化:未检测到 addon_admin_role 表');
59
+ return false;
60
+ }
61
+
62
+ // 检查 addon_admin_menu 表是否存在
63
+ const existMenu = await helper.tableExists('addon_admin_menu');
64
+ if (!existMenu) {
65
+ Logger.warn('跳过开发管理员初始化:未检测到 addon_admin_menu 表');
66
+ return false;
67
+ }
68
+
69
+ // 查询所有菜单 ID
70
+ const allMenus = await helper.getAll({
71
+ table: 'addon_admin_menu',
72
+ fields: ['id']
73
+ });
74
+
75
+ if (!allMenus || !Array.isArray(allMenus)) {
76
+ Logger.warn('查询菜单失败或菜单表为空');
77
+ return false;
78
+ }
79
+
80
+ const menuIds = allMenus.length > 0 ? allMenus.map((m: any) => m.id).join(',') : '';
81
+ Logger.info(`查询到 ${allMenus.length} 个菜单,ID 列表: ${menuIds || '(空)'}`);
82
+
83
+ // 查询所有接口 ID
84
+ const existApi = await helper.tableExists('addon_admin_api');
85
+ let apiIds = '';
86
+ if (existApi) {
87
+ const allApis = await helper.getAll({
88
+ table: 'addon_admin_api',
89
+ fields: ['id']
90
+ });
91
+
92
+ if (allApis && Array.isArray(allApis) && allApis.length > 0) {
93
+ apiIds = allApis.map((a: any) => a.id).join(',');
94
+ Logger.info(`查询到 ${allApis.length} 个接口,ID 列表: ${apiIds}`);
95
+ } else {
96
+ Logger.info('未查询到接口数据');
97
+ }
98
+ } else {
99
+ Logger.info('接口表不存在,跳过接口权限配置');
100
+ }
101
+
102
+ // 查询或创建 dev 角色
103
+ let devRole = await helper.getOne({
104
+ table: 'addon_admin_role',
105
+ where: { code: 'dev' }
106
+ });
107
+
108
+ if (devRole) {
109
+ // 更新 dev 角色的菜单和接口权限
110
+ await helper.updData({
111
+ table: 'addon_admin_role',
112
+ where: { code: 'dev' },
113
+ data: {
114
+ name: '开发者角色',
115
+ description: '拥有所有菜单和接口权限的开发者角色',
116
+ menus: menuIds,
117
+ apis: apiIds
118
+ }
119
+ });
120
+ Logger.info('dev 角色菜单和接口权限已更新');
121
+ } else {
122
+ // 创建 dev 角色
123
+ const roleId = await helper.insData({
124
+ table: 'addon_admin_role',
125
+ data: {
126
+ name: '开发者角色',
127
+ code: 'dev',
128
+ description: '拥有所有菜单和接口权限的开发者角色',
129
+ menus: menuIds,
130
+ apis: apiIds,
131
+ sort: 0
132
+ }
133
+ });
134
+ devRole = { id: roleId };
135
+ Logger.info('dev 角色已创建');
136
+ }
137
+
138
+ // 使用 bcrypt 加密密码(与登录验证一致)
139
+ const hashed = await Crypto2.hashPassword(Env.DEV_PASSWORD);
140
+
141
+ // 准备开发管理员数据
142
+ const devData = {
143
+ name: '开发者',
144
+ nickname: '开发者',
145
+ email: 'dev@qq.com',
146
+ username: 'dev',
147
+ password: hashed,
148
+ roleId: devRole.id,
149
+ roleCode: 'dev',
150
+ roleType: 'admin' // 小驼峰,自动转换为 role_type
151
+ };
152
+
153
+ // 查询现有账号
154
+ const existing = await helper.getOne({
155
+ table: 'addon_admin_admin',
156
+ where: { email: 'dev@qq.com' }
157
+ });
158
+
159
+ if (existing) {
160
+ // 更新现有账号
161
+ await helper.updData({
162
+ table: 'addon_admin_admin',
163
+ where: { email: 'dev@qq.com' },
164
+ data: devData
165
+ });
166
+ Logger.info('开发管理员已更新:email=dev@qq.com, username=dev, roleCode=dev, roleType=admin');
167
+ } else {
168
+ // 插入新账号
169
+ await helper.insData({
170
+ table: 'addon_admin_admin',
171
+ data: devData
172
+ });
173
+ Logger.info('开发管理员已初始化:email=dev@qq.com, username=dev, roleCode=dev, roleType=admin');
174
+ }
175
+
176
+ return true;
177
+ } catch (error: any) {
178
+ Logger.warn(`开发管理员初始化步骤出错:${error.message}`);
179
+ return false;
180
+ } finally {
181
+ // 清理资源:关闭所有数据库连接
182
+ if (dbInitialized) {
183
+ await closeDatabase();
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * 允许直接运行该脚本
190
+ */
191
+ if (import.meta.main) {
192
+ SyncDev()
193
+ .then((ok: boolean) => {
194
+ if (CLI.DRY_RUN) {
195
+ process.exit(0);
196
+ }
197
+ process.exit(ok ? 0 : 1);
198
+ })
199
+ .catch((err: Error) => {
200
+ Logger.error('❌ 开发管理员同步失败:', err);
201
+ process.exit(1);
202
+ });
203
+ }
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * 同步菜单数据到数据库
4
+ * 说明:根据 menu.json 配置文件增量同步菜单数据(最多2级:父级和子级)
5
+ *
6
+ * 流程:
7
+ * 1. 读取 menu.json 配置文件
8
+ * 2. 根据菜单的 path 字段检查是否存在
9
+ * 3. 存在则更新其他字段(name、icon、sort、type、pid)
10
+ * 4. 不存在则新增菜单记录
11
+ * 5. 处理两层菜单结构(父级和子级,不支持多层嵌套)
12
+ * 注:state 字段由框架自动管理(1=正常,2=禁用,0=删除)
13
+ */
14
+
15
+ import { Env, Logger, initDatabase, closeDatabase } from 'befly';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import menuConfig from '../config/menu.json';
19
+ import { collectPaths, checkTableExists, deleteObsoleteRecords, logSyncStats } from '../util';
20
+
21
+ // CLI 参数类型
22
+ interface CliArgs {
23
+ DRY_RUN: boolean;
24
+ }
25
+
26
+ // 解析命令行参数
27
+ const CLI: CliArgs = {
28
+ DRY_RUN: process.argv.includes('--plan')
29
+ };
30
+
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = path.dirname(__filename);
33
+
34
+ /**
35
+ * 同步菜单(两层结构:父级和子级)
36
+ * @param helper - DbHelper 实例
37
+ * @param menus - 菜单数组
38
+ * @returns 同步统计信息
39
+ */
40
+ async function syncMenus(helper: any, menus: any[]): Promise<{ created: number; updated: number }> {
41
+ const stats = { created: 0, updated: 0 };
42
+
43
+ for (const menu of menus) {
44
+ try {
45
+ // 1. 同步父级菜单
46
+ const existingParent = await helper.getOne({
47
+ table: 'addon_admin_menu',
48
+ where: { path: menu.path || '' }
49
+ });
50
+
51
+ let parentId: number;
52
+
53
+ if (existingParent) {
54
+ // 存在则更新
55
+ await helper.updData({
56
+ table: 'addon_admin_menu',
57
+ where: { id: existingParent.id },
58
+ data: {
59
+ pid: 0,
60
+ name: menu.name,
61
+ icon: menu.icon || '',
62
+ sort: menu.sort || 0,
63
+ type: menu.type || 1
64
+ }
65
+ });
66
+ parentId = existingParent.id;
67
+ stats.updated++;
68
+ Logger.info(` └ 更新父级菜单: ${menu.name} (ID: ${parentId}, Path: ${menu.path})`);
69
+ } else {
70
+ // 不存在则新增
71
+ parentId = await helper.insData({
72
+ table: 'addon_admin_menu',
73
+ data: {
74
+ pid: 0,
75
+ name: menu.name,
76
+ path: menu.path || '',
77
+ icon: menu.icon || '',
78
+ sort: menu.sort || 0,
79
+ type: menu.type || 1
80
+ }
81
+ });
82
+ stats.created++;
83
+ Logger.info(` └ 新增父级菜单: ${menu.name} (ID: ${parentId}, Path: ${menu.path})`);
84
+ }
85
+
86
+ // 2. 同步子级菜单
87
+ if (menu.children && menu.children.length > 0) {
88
+ for (const child of menu.children) {
89
+ const existingChild = await helper.getOne({
90
+ table: 'addon_admin_menu',
91
+ where: { path: child.path || '' }
92
+ });
93
+
94
+ if (existingChild) {
95
+ // 存在则更新
96
+ await helper.updData({
97
+ table: 'addon_admin_menu',
98
+ where: { id: existingChild.id },
99
+ data: {
100
+ pid: parentId,
101
+ name: child.name,
102
+ icon: child.icon || '',
103
+ sort: child.sort || 0,
104
+ type: child.type || 1
105
+ }
106
+ });
107
+ stats.updated++;
108
+ Logger.info(` └ 更新子级菜单: ${child.name} (ID: ${existingChild.id}, PID: ${parentId}, Path: ${child.path})`);
109
+ } else {
110
+ // 不存在则新增
111
+ const childId = await helper.insData({
112
+ table: 'addon_admin_menu',
113
+ data: {
114
+ pid: parentId,
115
+ name: child.name,
116
+ path: child.path || '',
117
+ icon: child.icon || '',
118
+ sort: child.sort || 0,
119
+ type: child.type || 1
120
+ }
121
+ });
122
+ stats.created++;
123
+ Logger.info(` └ 新增子级菜单: ${child.name} (ID: ${childId}, PID: ${parentId}, Path: ${child.path})`);
124
+ }
125
+ }
126
+ }
127
+ } catch (error: any) {
128
+ Logger.error(`同步菜单 "${menu.name}" 失败:`, error.message || String(error));
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ return stats;
134
+ }
135
+
136
+ /**
137
+ * 同步菜单主函数
138
+ */
139
+ async function syncMenu(): Promise<boolean> {
140
+ let dbInitialized = false;
141
+
142
+ try {
143
+ if (CLI.DRY_RUN) {
144
+ Logger.info('[计划] 同步菜单配置到数据库(plan 模式不执行)');
145
+ Logger.info('[计划] 1. 读取 menu.json 配置文件');
146
+ Logger.info('[计划] 2. 根据 path 检查菜单是否存在');
147
+ Logger.info('[计划] 3. 存在则更新,不存在则新增');
148
+ Logger.info('[计划] 4. 处理两层菜单结构(父级和子级)');
149
+ Logger.info('[计划] 5. 显示菜单结构预览');
150
+ return true;
151
+ }
152
+
153
+ Logger.info('开始同步菜单配置到数据库...\n');
154
+
155
+ // 初始化数据库连接(Redis + SQL + DbHelper)
156
+ const { helper } = await initDatabase({ max: 1 });
157
+ dbInitialized = true;
158
+
159
+ // 1. 检查表是否存在
160
+ if (!(await checkTableExists(helper, 'addon_admin_menu'))) {
161
+ return false;
162
+ }
163
+
164
+ // 2. 收集配置文件中所有菜单的 path
165
+ Logger.info('\n=== 步骤 2: 收集配置菜单路径 ===');
166
+ const configPaths = collectPaths(menuConfig);
167
+ Logger.info(`✅ 配置文件中共有 ${configPaths.size} 个菜单路径`);
168
+
169
+ // 3. 同步菜单(新增和更新)
170
+ Logger.info('\n=== 步骤 3: 同步菜单数据(新增/更新) ===');
171
+ const stats = await syncMenus(helper, menuConfig);
172
+
173
+ // 4. 删除文件中不存在的菜单
174
+ const deletedCount = await deleteObsoleteRecords(helper, 'addon_admin_menu', configPaths);
175
+
176
+ // 5. 构建树形结构预览
177
+ Logger.info('\n=== 步骤 5: 菜单结构预览 ===');
178
+ const allMenus = await helper.getAll({
179
+ table: 'addon_admin_menu',
180
+ fields: ['id', 'pid', 'name', 'path', 'type'],
181
+ orderBy: ['pid#ASC', 'sort#ASC', 'id#ASC']
182
+ });
183
+
184
+ // 6. 输出统计信息
185
+ logSyncStats(stats, deletedCount, '菜单');
186
+ Logger.info(`当前父级菜单: ${allMenus.filter((m: any) => m.pid === 0).length} 个`);
187
+ Logger.info(`当前子级菜单: ${allMenus.filter((m: any) => m.pid !== 0).length} 个`);
188
+ Logger.info('提示: 菜单缓存将在服务器启动时自动完成');
189
+
190
+ return true;
191
+ } catch (error: any) {
192
+ console.log('🔥[ error ]-262', error);
193
+ Logger.error('菜单同步失败:', { message: error.message, stack: error.stack });
194
+ return false;
195
+ } finally {
196
+ if (dbInitialized) {
197
+ await closeDatabase();
198
+ }
199
+ }
200
+ }
201
+
202
+ // 执行同步
203
+ syncMenu()
204
+ .then((success) => {
205
+ process.exit(success ? 0 : 1);
206
+ })
207
+ .catch((error) => {
208
+ Logger.error('执行失败:', error);
209
+ process.exit(1);
210
+ });