zero-ai 1.0.83 → 1.0.84
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/.r2mo/task/2026-03-05/2026-03-05.10-21-33-TASK@ai ex-api /345/221/275/344/273/244/346/225/264/346/224/271.md" +38 -0
- package/.r2mo/task/2026-03-05/2026-03-05.11-08-44-TASK@ai ex-api BUG.md +29 -0
- package/.r2mo/task/task-001.md +308 -2
- package/ExCommand.md +674 -0
- package/package.json +1 -1
- package/src/commander/ex-menu.json +6 -0
- package/src/commander-ai/fn.ex.api.js +194 -96
- package/src/commander-ai/fn.ex.menu.js +259 -0
- package/src/commander-ai/index.js +2 -0
|
@@ -12,6 +12,7 @@ const CONFIG_DIR = ".r2mo/task/command/ex-api";
|
|
|
12
12
|
const REQUIRED_ENV_DB = ["Z_DB_TYPE", "Z_DB_HOST", "Z_DB_PORT", "Z_DBS_INSTANCE", "Z_DB_APP_USER", "Z_DB_APP_PASS"];
|
|
13
13
|
const REQUIRED_ENV_APP = ["Z_APP_ID", "Z_TENANT", "Z_SIGMA"];
|
|
14
14
|
const R2_BY_UUID = "9a0d5018-33ad-4c64-80bf-8ae7947c482f";
|
|
15
|
+
const REFERENCE_ROLE_ID = "e501b47a-c08b-4c83-b12b-95ad82873e96";
|
|
15
16
|
|
|
16
17
|
/** 全局列(开发时按 RBAC Flyway 建表固定,执行时仅 DML,不查元数据):S_RESOURCE/S_ACTION/S_PERMISSION 写入,Excel 不写入 */
|
|
17
18
|
const GLOBAL_COLUMNS = [
|
|
@@ -383,28 +384,46 @@ async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
|
383
384
|
}
|
|
384
385
|
|
|
385
386
|
let roleIds = [];
|
|
387
|
+
let roleIdToCode = {}; // roleId -> CODE,供输出路径分流使用
|
|
388
|
+
let roleIdToName = {}; // roleId -> NAME,供汇总输出使用
|
|
386
389
|
if (!skip) {
|
|
387
390
|
const [roleRows] = await conn.execute("SELECT ID, NAME, CODE FROM S_ROLE ORDER BY NAME");
|
|
388
391
|
if (!roleRows || roleRows.length === 0) {
|
|
389
392
|
Ec.info("[ex-api] S_ROLE 中无角色,跳过授权");
|
|
390
393
|
} else {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
// 建立 roleId -> CODE/NAME 映射(所有角色)
|
|
395
|
+
roleRows.forEach((r) => {
|
|
396
|
+
const id = String(r.ID != null ? r.ID : r.id);
|
|
397
|
+
roleIdToCode[id] = r.CODE || "";
|
|
398
|
+
roleIdToName[id] = r.NAME || "";
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// 过滤掉超级管理员角色(固定输出,不需要用户选择)
|
|
402
|
+
const selectableRoles = roleRows.filter((r) => {
|
|
403
|
+
const id = String(r.ID != null ? r.ID : r.id);
|
|
404
|
+
return id !== REFERENCE_ROLE_ID;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (selectableRoles.length > 0) {
|
|
408
|
+
const answer = await inquirer.prompt([
|
|
409
|
+
{
|
|
410
|
+
type: "checkbox",
|
|
411
|
+
name: "selectedRoles",
|
|
412
|
+
message: "选择要授权当前 API 的角色(可多选,超级管理员已自动包含)",
|
|
413
|
+
choices: selectableRoles.map((r) => ({ name: `${r.NAME || r.CODE} (${r.CODE || '-'})`, value: String(r.ID != null ? r.ID : r.id) })),
|
|
414
|
+
pageSize: 999
|
|
415
|
+
}
|
|
416
|
+
]);
|
|
417
|
+
const raw = answer.selectedRoles;
|
|
418
|
+
if (Array.isArray(raw)) {
|
|
419
|
+
roleIds = raw.map((id) => (id != null ? String(id) : id));
|
|
420
|
+
} else if (raw != null && raw !== "") {
|
|
421
|
+
roleIds = [String(raw)];
|
|
422
|
+
} else {
|
|
423
|
+
roleIds = [];
|
|
397
424
|
}
|
|
398
|
-
]);
|
|
399
|
-
const raw = answer.selectedRoles;
|
|
400
|
-
if (Array.isArray(raw)) {
|
|
401
|
-
roleIds = raw.map((id) => (id != null ? String(id) : id));
|
|
402
|
-
} else if (raw != null && raw !== "") {
|
|
403
|
-
roleIds = [String(raw)];
|
|
404
|
-
} else {
|
|
405
|
-
roleIds = [];
|
|
406
425
|
}
|
|
407
|
-
Ec.info("[ex-api]
|
|
426
|
+
Ec.info("[ex-api] 已选角色数(不含超级管理员):" + roleIds.length + (roleIds.length > 0 ? ",ID=" + roleIds.slice(0, 5).join(",") + (roleIds.length > 5 ? "..." : "") : ""));
|
|
408
427
|
}
|
|
409
428
|
}
|
|
410
429
|
|
|
@@ -450,6 +469,10 @@ async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
|
450
469
|
Ec.info("[ex-api] R_ROLE_PERM 查询失败: " + e.message);
|
|
451
470
|
}
|
|
452
471
|
}
|
|
472
|
+
|
|
473
|
+
Ec.info("[ex-api] R_ROLE_PERM 查询失败: " + e.message);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
453
476
|
// 四张表行数据(Excel 列名与模板英文表头一致);S_PERM_SET 的 name/type 来自配置 pname/ptype
|
|
454
477
|
const resRow = resourceId && (insertedResource || existingResource) ? (insertedResource || existingResource) : null;
|
|
455
478
|
const actRow = actionId && (insertedAction || existingAction) ? (insertedAction || existingAction) : null;
|
|
@@ -489,47 +512,60 @@ async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
|
489
512
|
identifier: permRow.IDENTIFIER
|
|
490
513
|
}
|
|
491
514
|
: null;
|
|
492
|
-
// S_PERM_SET:name/type 必须从配置 pname/ptype 提取,key
|
|
493
|
-
const rowS_PERM_SET = resourceId && resRow
|
|
515
|
+
// S_PERM_SET:name/type 必须从配置 pname/ptype 提取,key 与资源一致,code 与权限一致
|
|
516
|
+
const rowS_PERM_SET = resourceId && resRow && permRow
|
|
494
517
|
? {
|
|
495
518
|
key: resourceId,
|
|
496
|
-
code:
|
|
519
|
+
code: permRow.CODE,
|
|
497
520
|
name: (metadata.pname != null && metadata.pname !== "") ? metadata.pname : resRow.NAME,
|
|
498
521
|
type: (metadata.ptype != null && metadata.ptype !== "") ? metadata.ptype : resRow.TYPE
|
|
499
522
|
}
|
|
500
523
|
: null;
|
|
501
|
-
// 写入 Excel 的数据(仅用于写 Excel
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
524
|
+
// 写入 Excel 的数据(仅用于写 Excel,不写库):
|
|
525
|
+
// 1. 超级管理员角色(REFERENCE_ROLE_ID)始终包含(如果有 permissionId)
|
|
526
|
+
// 2. 用户选中的其他角色
|
|
527
|
+
let rolePermsToWrite = [];
|
|
528
|
+
|
|
529
|
+
// 始终添加超级管理员角色
|
|
530
|
+
if (permissionId) {
|
|
531
|
+
rolePermsToWrite.push({ ROLE_ID: REFERENCE_ROLE_ID, PERM_ID: permissionId });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 添加用户选中的角色
|
|
535
|
+
if (permissionId && roleIds && roleIds.length > 0) {
|
|
536
|
+
roleIds.forEach((rid) => {
|
|
537
|
+
rolePermsToWrite.push({ ROLE_ID: rid, PERM_ID: permissionId });
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 如果本次有插入记录(R_ROLE_PERM),也合并进来(去重)
|
|
542
|
+
if (insertedRolePerms.length > 0) {
|
|
543
|
+
insertedRolePerms.forEach((rp) => {
|
|
544
|
+
const rid = rp.ROLE_ID != null ? String(rp.ROLE_ID) : String(rp.role_id);
|
|
545
|
+
const pid = rp.PERM_ID != null ? String(rp.PERM_ID) : String(rp.perm_id);
|
|
546
|
+
const exists = rolePermsToWrite.some((p) => String(p.ROLE_ID) === rid && String(p.PERM_ID) === pid);
|
|
547
|
+
if (!exists) {
|
|
548
|
+
rolePermsToWrite.push({ ROLE_ID: rid, PERM_ID: pid });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 如果库中已有记录(existingRolePerms),也合并进来(去重)
|
|
554
|
+
if (existingRolePerms.length > 0) {
|
|
555
|
+
existingRolePerms.forEach((rp) => {
|
|
556
|
+
const rid = rp.ROLE_ID != null ? String(rp.ROLE_ID) : String(rp.role_id);
|
|
557
|
+
const pid = rp.PERM_ID != null ? String(rp.PERM_ID) : String(rp.perm_id);
|
|
558
|
+
const exists = rolePermsToWrite.some((p) => String(p.ROLE_ID) === rid && String(p.PERM_ID) === pid);
|
|
559
|
+
if (!exists) {
|
|
560
|
+
rolePermsToWrite.push({ ROLE_ID: rid, PERM_ID: pid });
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
510
565
|
rolePermsToWrite = rolePermsToWrite.map((p) => ({
|
|
511
566
|
ROLE_ID: p.ROLE_ID != null ? p.ROLE_ID : p.role_id,
|
|
512
567
|
PERM_ID: p.PERM_ID != null ? p.PERM_ID : p.perm_id
|
|
513
568
|
}));
|
|
514
|
-
// 有 permissionId 但仍无一条可写时,从 S_ROLE 取一个角色,保证 Excel 至少有一行(仅写 Excel,不写库);优先超级管理员
|
|
515
|
-
if (permissionId && rolePermsToWrite.length === 0) {
|
|
516
|
-
try {
|
|
517
|
-
let [oneRole] = await conn.execute(
|
|
518
|
-
"SELECT ID FROM S_ROLE WHERE NAME = ? OR CODE = ? OR CODE = ? LIMIT 1",
|
|
519
|
-
["超级管理员", "ADMIN.SUPER", "ADMIN_SUPER"]
|
|
520
|
-
);
|
|
521
|
-
if (!oneRole || !oneRole[0]) {
|
|
522
|
-
[oneRole] = await conn.execute("SELECT ID FROM S_ROLE ORDER BY NAME LIMIT 1", []);
|
|
523
|
-
}
|
|
524
|
-
if (oneRole && oneRole[0]) {
|
|
525
|
-
const rid = oneRole[0].ID != null ? String(oneRole[0].ID) : String(oneRole[0].id);
|
|
526
|
-
rolePermsToWrite = [{ ROLE_ID: rid, PERM_ID: permissionId }];
|
|
527
|
-
Ec.info("[ex-api] R_ROLE_PERM 无选中/库中记录,已用 S_ROLE 补一条写 Excel(ROLE_ID=" + rid + ")");
|
|
528
|
-
}
|
|
529
|
-
} catch (e) {
|
|
530
|
-
Ec.info("[ex-api] S_ROLE 取角色失败: " + e.message);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
569
|
|
|
534
570
|
Ec.info("[ex-api] 📋 R_ROLE_PERM 写入前数据:");
|
|
535
571
|
Ec.info("[ex-api] insertedRolePerms.length = " + insertedRolePerms.length);
|
|
@@ -649,68 +685,130 @@ async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
|
649
685
|
await workbook.xlsx.writeFile(outResPath);
|
|
650
686
|
Ec.info("[ex-api] 已写入 RBAC_RESOURCE:" + outResPath);
|
|
651
687
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
688
|
+
const roleFileName = "falcon-" + fileName;
|
|
689
|
+
const roleWritePaths = [];
|
|
690
|
+
|
|
691
|
+
// 固定参考角色输出:维持原目录规则
|
|
692
|
+
const hasReferenceRole = rolePermsToWrite.some((p) => String(p.ROLE_ID) === REFERENCE_ROLE_ID);
|
|
693
|
+
if (hasReferenceRole) {
|
|
694
|
+
const refPerms = rolePermsToWrite.filter((p) => String(p.ROLE_ID) === REFERENCE_ROLE_ID);
|
|
695
|
+
if (refPerms.length > 0) {
|
|
696
|
+
let refWorkbook;
|
|
697
|
+
if (fs.existsSync(templateRolePath)) {
|
|
698
|
+
refWorkbook = await new ExcelJS.Workbook().xlsx.readFile(templateRolePath);
|
|
699
|
+
const wsRole = refWorkbook.getWorksheet(defRole.sheetName || "DATA-PERM") || refWorkbook.worksheets[0];
|
|
700
|
+
if (wsRole) {
|
|
701
|
+
const regionsRole = scanTableRegions(wsRole);
|
|
702
|
+
const region = regionsRole.find((r) => String(r.tableName).trim() === String(tableNameRole).trim());
|
|
703
|
+
if (region) {
|
|
704
|
+
const colRole = region.columnIndex["roleId"] || region.columnIndex["ROLE_ID"] || 1;
|
|
705
|
+
const colPerm = region.columnIndex["permId"] || region.columnIndex["PERM_ID"] || 2;
|
|
706
|
+
refPerms.forEach((pair, idx) => {
|
|
707
|
+
const row = wsRole.getRow(region.dataStartRow + idx);
|
|
708
|
+
row.getCell(colRole).value = pair.ROLE_ID;
|
|
709
|
+
row.getCell(colPerm).value = pair.PERM_ID;
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
refWorkbook = new ExcelJS.Workbook();
|
|
715
|
+
const wsRole = refWorkbook.addWorksheet(defRole.sheetName || "DATA-PERM");
|
|
716
|
+
wsRole.addRow([]);
|
|
717
|
+
wsRole.addRow([]);
|
|
718
|
+
wsRole.addRow(["{TABLE}", tableNameRole, "角色和权限关系", "", ""]);
|
|
719
|
+
wsRole.addRow(["角色ID", "权限ID"]);
|
|
720
|
+
wsRole.addRow(["roleId", "permId"]);
|
|
721
|
+
refPerms.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
722
|
+
}
|
|
723
|
+
const outRefRolePath = path.join(rbacRoleDir, roleFileName);
|
|
724
|
+
await refWorkbook.xlsx.writeFile(outRefRolePath);
|
|
725
|
+
roleWritePaths.push(outRefRolePath);
|
|
726
|
+
Ec.info("[ex-api] 已写入 RBAC_ROLE/ADMIN.SUPER(参考角色):" + outRefRolePath);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// 其他角色输出:当前工作目录下 src/main/resources/init/oob/role/{CODE}/ 目录
|
|
731
|
+
const nonReferenceRoleIds = roleIds.filter((rid) => String(rid) !== REFERENCE_ROLE_ID);
|
|
732
|
+
for (const rid of nonReferenceRoleIds) {
|
|
733
|
+
const code = roleIdToCode[String(rid)] || "";
|
|
734
|
+
if (!code) {
|
|
735
|
+
Ec.info("[ex-api] ⚠️ 警告:角色 ID=" + rid + " 缺少 CODE,跳过输出");
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const targetRoleDir = path.join(cwd, "src", "main", "resources", "init", "oob", "role", code);
|
|
739
|
+
if (!fs.existsSync(targetRoleDir)) {
|
|
740
|
+
Ec.info("[ex-api] ⚠️ 警告:未找到输出目录 " + targetRoleDir + ",跳过输出");
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const oneRolePerms = rolePermsToWrite.filter((p) => String(p.ROLE_ID) === String(rid));
|
|
745
|
+
if (oneRolePerms.length === 0) continue;
|
|
746
|
+
|
|
747
|
+
let oneRoleWorkbook;
|
|
748
|
+
if (fs.existsSync(templateRolePath)) {
|
|
749
|
+
oneRoleWorkbook = await new ExcelJS.Workbook().xlsx.readFile(templateRolePath);
|
|
750
|
+
const wsRole = oneRoleWorkbook.getWorksheet(defRole.sheetName || "DATA-PERM") || oneRoleWorkbook.worksheets[0];
|
|
751
|
+
if (wsRole) {
|
|
752
|
+
const regionsRole = scanTableRegions(wsRole);
|
|
753
|
+
const region = regionsRole.find((r) => String(r.tableName).trim() === String(tableNameRole).trim());
|
|
754
|
+
if (region) {
|
|
755
|
+
const colRole = region.columnIndex["roleId"] || region.columnIndex["ROLE_ID"] || 1;
|
|
756
|
+
const colPerm = region.columnIndex["permId"] || region.columnIndex["PERM_ID"] || 2;
|
|
757
|
+
oneRolePerms.forEach((pair, idx) => {
|
|
674
758
|
const row = wsRole.getRow(region.dataStartRow + idx);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
row.getCell(colRole).value = roleId;
|
|
678
|
-
row.getCell(colPerm).value = permId;
|
|
679
|
-
if (idx < 3) Ec.info("[ex-api] row[" + (region.dataStartRow + idx) + "] 已设 列" + colRole + "=" + roleId + " 列" + colPerm + "=" + permId);
|
|
759
|
+
row.getCell(colRole).value = pair.ROLE_ID;
|
|
760
|
+
row.getCell(colPerm).value = pair.PERM_ID;
|
|
680
761
|
});
|
|
681
|
-
const checkRow = wsRole.getRow(region.dataStartRow);
|
|
682
|
-
Ec.info("[ex-api] 已写入 R_ROLE_PERM " + rolePermsToWrite.length + " 行(dataStartRow=" + region.dataStartRow + ");写回读首行 列" + colRole + "=" + (checkRow.getCell(colRole).value) + " 列" + colPerm + "=" + (checkRow.getCell(colPerm).value));
|
|
683
|
-
} else {
|
|
684
|
-
Ec.info("[ex-api] R_ROLE_PERM 无数据可写(rolePermsToWrite.length=0,permissionId=" + (permissionId || "—") + ")");
|
|
685
762
|
}
|
|
686
|
-
} else {
|
|
687
|
-
Ec.info("[ex-api] 未找到 R_ROLE_PERM 区域(tableName=" + tableNameRole + "),跳过写入");
|
|
688
763
|
}
|
|
764
|
+
} else {
|
|
765
|
+
oneRoleWorkbook = new ExcelJS.Workbook();
|
|
766
|
+
const wsRole = oneRoleWorkbook.addWorksheet(defRole.sheetName || "DATA-PERM");
|
|
767
|
+
wsRole.addRow([]);
|
|
768
|
+
wsRole.addRow([]);
|
|
769
|
+
wsRole.addRow(["{TABLE}", tableNameRole, "角色和权限关系", "", ""]);
|
|
770
|
+
wsRole.addRow(["角色ID", "权限ID"]);
|
|
771
|
+
wsRole.addRow(["roleId", "permId"]);
|
|
772
|
+
oneRolePerms.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
689
773
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
rolePermsToWrite.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
774
|
+
|
|
775
|
+
const outOneRolePath = path.join(targetRoleDir, roleFileName);
|
|
776
|
+
await oneRoleWorkbook.xlsx.writeFile(outOneRolePath);
|
|
777
|
+
roleWritePaths.push(outOneRolePath);
|
|
778
|
+
Ec.info("[ex-api] 已写入角色目录(" + code + "):" + outOneRolePath);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (roleWritePaths.length === 0) {
|
|
782
|
+
Ec.info("[ex-api] RBAC_ROLE 未输出任何文件(可能角色未选中或目标目录不存在)");
|
|
700
783
|
}
|
|
701
|
-
const roleFileName = "falcon-" + fileName;
|
|
702
|
-
const outRolePath = path.join(rbacRoleDir, roleFileName);
|
|
703
|
-
await roleWorkbook.xlsx.writeFile(outRolePath);
|
|
704
|
-
Ec.info("[ex-api] 已写入 RBAC_ROLE/ADMIN.SUPER:" + outRolePath);
|
|
705
784
|
|
|
706
785
|
Ec.info("[ex-api] ✅ 执行完成(幂等)");
|
|
707
786
|
Ec.info("[ex-api] 📋 汇总:");
|
|
708
787
|
Ec.info("[ex-api] 🔑 ACTION_ID = " + (actionId || "—"));
|
|
709
788
|
Ec.info("[ex-api] 🔑 RESOURCE_ID = " + (resourceId || "—"));
|
|
710
789
|
Ec.info("[ex-api] 🔑 PERMISSION_ID = " + (permissionId || "—"));
|
|
711
|
-
Ec.info("[ex-api] 👥
|
|
790
|
+
Ec.info("[ex-api] 👥 授权角色总数 = " + (rolePermsToWrite.length > 0 ? rolePermsToWrite.length : 0));
|
|
791
|
+
|
|
792
|
+
// 固定输出:超级管理员角色
|
|
793
|
+
const refRoleName = roleIdToName[REFERENCE_ROLE_ID] || "超级管理员";
|
|
794
|
+
const refRoleCode = roleIdToCode[REFERENCE_ROLE_ID] || "ADMIN.SUPER";
|
|
795
|
+
Ec.info("[ex-api] 🔒 固定输出角色:");
|
|
796
|
+
Ec.info("[ex-api] [1] " + refRoleName + " (CODE: " + refRoleCode + ", ID: " + REFERENCE_ROLE_ID + ")");
|
|
797
|
+
|
|
798
|
+
// 用户选择的角色
|
|
799
|
+
if (roleIds && roleIds.length > 0) {
|
|
800
|
+
Ec.info("[ex-api] 👤 用户选择角色:");
|
|
801
|
+
roleIds.forEach((rid, idx) => {
|
|
802
|
+
const code = roleIdToCode[rid] || "—";
|
|
803
|
+
const name = roleIdToName[rid] || "—";
|
|
804
|
+
Ec.info("[ex-api] [" + (idx + 1) + "] " + name + " (CODE: " + code + ", ID: " + rid + ")");
|
|
805
|
+
});
|
|
806
|
+
} else {
|
|
807
|
+
Ec.info("[ex-api] 👤 用户选择角色:无");
|
|
808
|
+
}
|
|
809
|
+
|
|
712
810
|
Ec.info("[ex-api] 📁 RBAC_RESOURCE = " + outResPath);
|
|
713
|
-
Ec.info("[ex-api] 📁 RBAC_ROLE = " +
|
|
811
|
+
roleWritePaths.forEach((p) => Ec.info("[ex-api] 📁 RBAC_ROLE = " + p));
|
|
714
812
|
if (insertedResource) {
|
|
715
813
|
Ec.info("[ex-api] 📦 S_RESOURCE 本次插入字段:");
|
|
716
814
|
Object.keys(insertedResource).forEach((k) => Ec.info("[ex-api] " + k + " = " + (insertedResource[k] != null ? insertedResource[k] : "—")));
|
|
@@ -756,7 +854,7 @@ async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
|
756
854
|
permissionId: permissionId || "—",
|
|
757
855
|
roleCount: roleIds ? roleIds.length : 0,
|
|
758
856
|
outResPath: outResPath || "—",
|
|
759
|
-
|
|
857
|
+
outRolePaths: roleWritePaths || [],
|
|
760
858
|
ok: true
|
|
761
859
|
};
|
|
762
860
|
} catch (err) {
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const Ec = require("../epic");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const Ut = require("../commander-shared");
|
|
7
|
+
const inquirer = require("inquirer");
|
|
8
|
+
|
|
9
|
+
const REQUIRED_ENV_DB = ["Z_DB_HOST", "Z_DB_PORT", "Z_DBS_INSTANCE", "Z_DB_APP_USER", "Z_DB_APP_PASS"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 从 pom.xml 读取当前项目的 artifactId(排除 <parent> 内的)
|
|
13
|
+
*/
|
|
14
|
+
function getArtifactIdFromPom(cwd) {
|
|
15
|
+
const pomPath = path.resolve(cwd, "pom.xml");
|
|
16
|
+
if (!fs.existsSync(pomPath)) return null;
|
|
17
|
+
let content = fs.readFileSync(pomPath, "utf-8");
|
|
18
|
+
content = content.replace(/<parent>[\s\S]*?<\/parent>/i, "");
|
|
19
|
+
const m = content.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
20
|
+
return m ? m[1].trim() : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 解析 app.env:export KEY="value" 或 export KEY='value',写入 process.env
|
|
25
|
+
*/
|
|
26
|
+
function loadAppEnv(filePath) {
|
|
27
|
+
if (!fs.existsSync(filePath)) return false;
|
|
28
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
29
|
+
const lines = content.split(/\r?\n/);
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (trimmed.startsWith("#") || !trimmed.startsWith("export ")) continue;
|
|
33
|
+
const match = trimmed.match(/^export\s+([A-Za-z0-9_]+)=["']?([^"'\n]*)["']?/);
|
|
34
|
+
if (match) process.env[match[1]] = match[2].trim();
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 解析 .r2mo/app.env 路径:
|
|
41
|
+
* ONE:当前目录 .r2mo/app.env
|
|
42
|
+
* DPA:{id}-api/.r2mo/app.env,id 来自 pom.xml 或当前目录名
|
|
43
|
+
* 支持两种布局:api 在项目内 (cwd/{id}-api) 或 与项目并列 (cwd/../{id}-api)
|
|
44
|
+
*/
|
|
45
|
+
function resolveAppEnvPath(cwd) {
|
|
46
|
+
const primary = path.resolve(cwd, ".r2mo", "app.env");
|
|
47
|
+
if (fs.existsSync(primary)) return primary;
|
|
48
|
+
|
|
49
|
+
let artifactId = getArtifactIdFromPom(cwd);
|
|
50
|
+
if (!artifactId) {
|
|
51
|
+
const base = path.basename(cwd);
|
|
52
|
+
if (base && base !== ".") artifactId = base;
|
|
53
|
+
}
|
|
54
|
+
if (artifactId) {
|
|
55
|
+
const apiDir = `${artifactId}-api`;
|
|
56
|
+
const nested = path.resolve(cwd, apiDir, ".r2mo", "app.env");
|
|
57
|
+
if (fs.existsSync(nested)) return nested;
|
|
58
|
+
const sibling = path.resolve(cwd, "..", apiDir, ".r2mo", "app.env");
|
|
59
|
+
if (fs.existsSync(sibling)) return sibling;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = async (options) => {
|
|
65
|
+
try {
|
|
66
|
+
Ec.execute("ai ex-menu:从数据库 X_MENU 表生成角色菜单权限 JSON 文件");
|
|
67
|
+
|
|
68
|
+
const cwd = process.cwd();
|
|
69
|
+
|
|
70
|
+
// 1. 加载 .r2mo/app.env 环境变量
|
|
71
|
+
const appEnvPath = resolveAppEnvPath(cwd);
|
|
72
|
+
if (!appEnvPath) {
|
|
73
|
+
const tried = [path.resolve(cwd, ".r2mo", "app.env")];
|
|
74
|
+
const id = getArtifactIdFromPom(cwd) || path.basename(cwd);
|
|
75
|
+
if (id) {
|
|
76
|
+
tried.push(path.resolve(cwd, `${id}-api`, ".r2mo", "app.env"));
|
|
77
|
+
tried.push(path.resolve(cwd, "..", `${id}-api`, ".r2mo", "app.env"));
|
|
78
|
+
}
|
|
79
|
+
Ec.error(".r2mo/app.env 不存在;DPA 下也未找到 {id}-api/.r2mo/app.env");
|
|
80
|
+
Ec.info("已尝试路径(id=" + (id || "未解析") + "):");
|
|
81
|
+
tried.forEach((p) => Ec.info(` - ${p}`));
|
|
82
|
+
Ec.info("请确认:1) 在项目根执行 2) 存在 .r2mo/app.env 或 {id}-api/.r2mo/app.env(嵌套或与项目并列)");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
loadAppEnv(appEnvPath);
|
|
87
|
+
Ec.info(`✓ 已加载环境变量:${appEnvPath}`);
|
|
88
|
+
|
|
89
|
+
// 2. 检查数据库核心环境变量
|
|
90
|
+
const missing = REQUIRED_ENV_DB.filter((k) => !process.env[k] || !String(process.env[k]).trim());
|
|
91
|
+
if (missing.length > 0) {
|
|
92
|
+
Ec.error("环境变量不齐,以下前置条件必须全部已设置,否则不执行。");
|
|
93
|
+
Ec.info("当前缺失的环境变量:" + missing.join(", "));
|
|
94
|
+
Ec.info("请确保以下环境变量已设置(可在 .r2mo/app.env 中 export):");
|
|
95
|
+
Ec.info(" Z_DB_HOST # 数据库主机,如 127.0.0.1");
|
|
96
|
+
Ec.info(" Z_DB_PORT # 数据库端口,如 3306");
|
|
97
|
+
Ec.info(" Z_DBS_INSTANCE # 业务数据库实例名");
|
|
98
|
+
Ec.info(" Z_DB_APP_USER # 数据库用户");
|
|
99
|
+
Ec.info(" Z_DB_APP_PASS # 数据库密码");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
Ec.info(`✓ 环境变量检查通过:${REQUIRED_ENV_DB.join(", ")}`);
|
|
103
|
+
|
|
104
|
+
// 3. 连接数据库
|
|
105
|
+
const mysql = require("mysql2/promise");
|
|
106
|
+
const dbConfig = {
|
|
107
|
+
host: process.env.Z_DB_HOST || "localhost",
|
|
108
|
+
port: parseInt(process.env.Z_DB_PORT || "3306", 10),
|
|
109
|
+
user: process.env.Z_DB_APP_USER,
|
|
110
|
+
password: process.env.Z_DB_APP_PASS,
|
|
111
|
+
database: process.env.Z_DBS_INSTANCE
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
Ec.execute(`连接数据库:${dbConfig.database} @ ${dbConfig.host}:${dbConfig.port}(用户 ${dbConfig.user})`);
|
|
115
|
+
|
|
116
|
+
const conn = await mysql.createConnection(dbConfig);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// 4. 查询 S_ROLE 表,列出所有角色
|
|
120
|
+
Ec.execute("查询 S_ROLE 表,列出所有角色…");
|
|
121
|
+
const [roleRows] = await conn.execute("SELECT ID, NAME, CODE FROM S_ROLE ORDER BY NAME");
|
|
122
|
+
|
|
123
|
+
if (!roleRows || roleRows.length === 0) {
|
|
124
|
+
Ec.error("S_ROLE 表中无角色,无法继续");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Ec.info(`✓ 查询到 ${roleRows.length} 个角色`);
|
|
129
|
+
|
|
130
|
+
// 5. 用户单选角色(提示:菜单全开放模式,建议选择开发人员角色)
|
|
131
|
+
const roleChoices = roleRows.map((r) => ({
|
|
132
|
+
name: `${r.NAME || r.CODE || r.ID} (CODE: ${r.CODE || "-"})`,
|
|
133
|
+
value: r
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
const { selectedRole } = await inquirer.prompt([
|
|
137
|
+
{
|
|
138
|
+
type: "list",
|
|
139
|
+
name: "selectedRole",
|
|
140
|
+
message: "⚠️ 菜单全开放模式:请选择一个角色(建议选择开发人员角色 ADMIN.DEVELOPER)",
|
|
141
|
+
choices: roleChoices
|
|
142
|
+
}
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const roleCode = selectedRole.CODE;
|
|
146
|
+
if (!roleCode || !roleCode.trim()) {
|
|
147
|
+
Ec.error("所选角色的 CODE 为空,无法继续");
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Ec.info(`✓ 已选择角色:${selectedRole.NAME || "-"} (CODE: ${roleCode})`);
|
|
152
|
+
|
|
153
|
+
// 6. 查询 X_MENU 表,生成 NAME 数组
|
|
154
|
+
Ec.execute("查询 X_MENU 表,生成 NAME 数组…");
|
|
155
|
+
const [menuRows] = await conn.execute(
|
|
156
|
+
"SELECT NAME FROM X_MENU ORDER BY `ORDER`, LEVEL, NAME"
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const nameArray = menuRows.map((row) => row.NAME).filter((name) => name && name.trim());
|
|
160
|
+
Ec.info(`✓ 查询到 ${nameArray.length} 个菜单项`);
|
|
161
|
+
|
|
162
|
+
// 7. 查询 X_MENU 表,生成层级文本数组
|
|
163
|
+
Ec.execute("查询 X_MENU 表,生成层级文本数组…");
|
|
164
|
+
const [hierarchyRows] = await conn.execute(
|
|
165
|
+
"SELECT ID, IFNULL(PARENT_ID,'') AS PARENT_ID, NAME, IFNULL(TEXT,NAME) AS TEXT, IFNULL(`ORDER`,0) AS `ORDER` FROM X_MENU"
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// 构建菜单字典(按 ID 索引)
|
|
169
|
+
const byId = {};
|
|
170
|
+
hierarchyRows.forEach((row) => {
|
|
171
|
+
byId[row.ID] = {
|
|
172
|
+
id: row.ID,
|
|
173
|
+
pid: row.PARENT_ID || null,
|
|
174
|
+
name: row.NAME,
|
|
175
|
+
text: row.TEXT,
|
|
176
|
+
order: parseInt(row.ORDER, 10) || 0
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// 构建父子关系映射
|
|
181
|
+
const children = {};
|
|
182
|
+
Object.values(byId).forEach((node) => {
|
|
183
|
+
if (!children[node.pid]) children[node.pid] = [];
|
|
184
|
+
children[node.pid].push(node);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 对每个父节点的子节点按 ORDER 和 NAME 排序
|
|
188
|
+
Object.keys(children).forEach((pid) => {
|
|
189
|
+
children[pid].sort((a, b) => {
|
|
190
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
191
|
+
return a.name.localeCompare(b.name);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// 递归遍历生成带缩进的文本数组
|
|
196
|
+
const hierarchyArray = [];
|
|
197
|
+
function walk(pid, depth) {
|
|
198
|
+
const nodes = children[pid] || [];
|
|
199
|
+
nodes.forEach((node) => {
|
|
200
|
+
const indent = " ".repeat(4 * (depth - 1));
|
|
201
|
+
hierarchyArray.push(indent + node.text);
|
|
202
|
+
walk(node.id, depth + 1);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
walk(null, 1);
|
|
206
|
+
|
|
207
|
+
Ec.info(`✓ 生成层级文本数组,共 ${hierarchyArray.length} 项`);
|
|
208
|
+
|
|
209
|
+
// 8. 写入文件
|
|
210
|
+
const targetDir = path.resolve(cwd, "src", "main", "resources", "init", "permission", "ui.menu");
|
|
211
|
+
const roleDir = path.join(targetDir, "role");
|
|
212
|
+
|
|
213
|
+
if (!fs.existsSync(targetDir)) {
|
|
214
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
215
|
+
}
|
|
216
|
+
if (!fs.existsSync(roleDir)) {
|
|
217
|
+
fs.mkdirSync(roleDir, { recursive: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const nameJsonPath = path.join(targetDir, `${roleCode}.json`);
|
|
221
|
+
const hierarchyJsonPath = path.join(roleDir, `${roleCode}.json`);
|
|
222
|
+
|
|
223
|
+
const nameJson = { name: nameArray };
|
|
224
|
+
fs.writeFileSync(nameJsonPath, JSON.stringify(nameJson, null, 2), "utf-8");
|
|
225
|
+
Ec.info(`✓ 已写入 NAME 数组:${nameJsonPath}`);
|
|
226
|
+
|
|
227
|
+
fs.writeFileSync(hierarchyJsonPath, JSON.stringify(hierarchyArray, null, 2), "utf-8");
|
|
228
|
+
Ec.info(`✓ 已写入层级文本数组:${hierarchyJsonPath}`);
|
|
229
|
+
|
|
230
|
+
// 9. 汇总报告
|
|
231
|
+
const sep = "----------------------------------------";
|
|
232
|
+
Ec.info(sep);
|
|
233
|
+
Ec.info(" ai ex-menu 执行报告");
|
|
234
|
+
Ec.info(sep);
|
|
235
|
+
Ec.info(" ⚙️ 环境");
|
|
236
|
+
Ec.info(` app.env : ${appEnvPath}`);
|
|
237
|
+
Ec.info(` 数据库实例 : ${dbConfig.database}`);
|
|
238
|
+
Ec.info(` 连接地址 : ${dbConfig.host}:${dbConfig.port}`);
|
|
239
|
+
Ec.info(` 数据库用户 : ${dbConfig.user}`);
|
|
240
|
+
Ec.info(" 👤 目标角色");
|
|
241
|
+
Ec.info(` NAME : ${selectedRole.NAME || "-"}`);
|
|
242
|
+
Ec.info(` CODE : ${roleCode}`);
|
|
243
|
+
Ec.info(` ID : ${selectedRole.ID}`);
|
|
244
|
+
Ec.info(" 📋 菜单数据");
|
|
245
|
+
Ec.info(` X_MENU 总数 : ${nameArray.length} 项`);
|
|
246
|
+
Ec.info(" ✅ 生成文件");
|
|
247
|
+
Ec.info(` NAME 数组 : ${nameJsonPath}`);
|
|
248
|
+
Ec.info(` 层级文本数组 : ${hierarchyJsonPath}`);
|
|
249
|
+
Ec.info(sep);
|
|
250
|
+
} finally {
|
|
251
|
+
await conn.end();
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
Ec.error(`执行失败:${err.message}`);
|
|
255
|
+
if (err.code) Ec.info(`错误码:${err.code}`);
|
|
256
|
+
if (err.stack) Ec.info(err.stack);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
@@ -12,6 +12,7 @@ const executeExPerm = require('./fn.ex.perm');
|
|
|
12
12
|
const executeExApi = require('./fn.ex.api');
|
|
13
13
|
const executeExCrud = require('./fn.ex.crud');
|
|
14
14
|
const executeExApp = require('./fn.ex.app');
|
|
15
|
+
const executeExMenu = require('./fn.ex.menu');
|
|
15
16
|
const exported = {
|
|
16
17
|
executeUuid, // ai uuid
|
|
17
18
|
executeString, // ai str
|
|
@@ -29,6 +30,7 @@ const exported = {
|
|
|
29
30
|
executeExApi, // ai ex-api
|
|
30
31
|
executeExCrud, // ai ex-crud
|
|
31
32
|
executeExApp, // ai ex-app
|
|
33
|
+
executeExMenu, // ai ex-menu
|
|
32
34
|
};
|
|
33
35
|
module.exports = exported;
|
|
34
36
|
/**
|