zero-ai 1.0.83 → 1.0.85
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 +190 -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
|
|
|
@@ -489,47 +508,60 @@ async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
|
489
508
|
identifier: permRow.IDENTIFIER
|
|
490
509
|
}
|
|
491
510
|
: null;
|
|
492
|
-
// S_PERM_SET:name/type 必须从配置 pname/ptype 提取,key
|
|
493
|
-
const rowS_PERM_SET = resourceId && resRow
|
|
511
|
+
// S_PERM_SET:name/type 必须从配置 pname/ptype 提取,key 与资源一致,code 与权限一致
|
|
512
|
+
const rowS_PERM_SET = resourceId && resRow && permRow
|
|
494
513
|
? {
|
|
495
514
|
key: resourceId,
|
|
496
|
-
code:
|
|
515
|
+
code: permRow.CODE,
|
|
497
516
|
name: (metadata.pname != null && metadata.pname !== "") ? metadata.pname : resRow.NAME,
|
|
498
517
|
type: (metadata.ptype != null && metadata.ptype !== "") ? metadata.ptype : resRow.TYPE
|
|
499
518
|
}
|
|
500
519
|
: null;
|
|
501
|
-
// 写入 Excel 的数据(仅用于写 Excel
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
520
|
+
// 写入 Excel 的数据(仅用于写 Excel,不写库):
|
|
521
|
+
// 1. 超级管理员角色(REFERENCE_ROLE_ID)始终包含(如果有 permissionId)
|
|
522
|
+
// 2. 用户选中的其他角色
|
|
523
|
+
let rolePermsToWrite = [];
|
|
524
|
+
|
|
525
|
+
// 始终添加超级管理员角色
|
|
526
|
+
if (permissionId) {
|
|
527
|
+
rolePermsToWrite.push({ ROLE_ID: REFERENCE_ROLE_ID, PERM_ID: permissionId });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 添加用户选中的角色
|
|
531
|
+
if (permissionId && roleIds && roleIds.length > 0) {
|
|
532
|
+
roleIds.forEach((rid) => {
|
|
533
|
+
rolePermsToWrite.push({ ROLE_ID: rid, PERM_ID: permissionId });
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 如果本次有插入记录(R_ROLE_PERM),也合并进来(去重)
|
|
538
|
+
if (insertedRolePerms.length > 0) {
|
|
539
|
+
insertedRolePerms.forEach((rp) => {
|
|
540
|
+
const rid = rp.ROLE_ID != null ? String(rp.ROLE_ID) : String(rp.role_id);
|
|
541
|
+
const pid = rp.PERM_ID != null ? String(rp.PERM_ID) : String(rp.perm_id);
|
|
542
|
+
const exists = rolePermsToWrite.some((p) => String(p.ROLE_ID) === rid && String(p.PERM_ID) === pid);
|
|
543
|
+
if (!exists) {
|
|
544
|
+
rolePermsToWrite.push({ ROLE_ID: rid, PERM_ID: pid });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// 如果库中已有记录(existingRolePerms),也合并进来(去重)
|
|
550
|
+
if (existingRolePerms.length > 0) {
|
|
551
|
+
existingRolePerms.forEach((rp) => {
|
|
552
|
+
const rid = rp.ROLE_ID != null ? String(rp.ROLE_ID) : String(rp.role_id);
|
|
553
|
+
const pid = rp.PERM_ID != null ? String(rp.PERM_ID) : String(rp.perm_id);
|
|
554
|
+
const exists = rolePermsToWrite.some((p) => String(p.ROLE_ID) === rid && String(p.PERM_ID) === pid);
|
|
555
|
+
if (!exists) {
|
|
556
|
+
rolePermsToWrite.push({ ROLE_ID: rid, PERM_ID: pid });
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
510
561
|
rolePermsToWrite = rolePermsToWrite.map((p) => ({
|
|
511
562
|
ROLE_ID: p.ROLE_ID != null ? p.ROLE_ID : p.role_id,
|
|
512
563
|
PERM_ID: p.PERM_ID != null ? p.PERM_ID : p.perm_id
|
|
513
564
|
}));
|
|
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
565
|
|
|
534
566
|
Ec.info("[ex-api] 📋 R_ROLE_PERM 写入前数据:");
|
|
535
567
|
Ec.info("[ex-api] insertedRolePerms.length = " + insertedRolePerms.length);
|
|
@@ -649,68 +681,130 @@ async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
|
649
681
|
await workbook.xlsx.writeFile(outResPath);
|
|
650
682
|
Ec.info("[ex-api] 已写入 RBAC_RESOURCE:" + outResPath);
|
|
651
683
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
684
|
+
const roleFileName = "falcon-" + fileName;
|
|
685
|
+
const roleWritePaths = [];
|
|
686
|
+
|
|
687
|
+
// 固定参考角色输出:维持原目录规则
|
|
688
|
+
const hasReferenceRole = rolePermsToWrite.some((p) => String(p.ROLE_ID) === REFERENCE_ROLE_ID);
|
|
689
|
+
if (hasReferenceRole) {
|
|
690
|
+
const refPerms = rolePermsToWrite.filter((p) => String(p.ROLE_ID) === REFERENCE_ROLE_ID);
|
|
691
|
+
if (refPerms.length > 0) {
|
|
692
|
+
let refWorkbook;
|
|
693
|
+
if (fs.existsSync(templateRolePath)) {
|
|
694
|
+
refWorkbook = await new ExcelJS.Workbook().xlsx.readFile(templateRolePath);
|
|
695
|
+
const wsRole = refWorkbook.getWorksheet(defRole.sheetName || "DATA-PERM") || refWorkbook.worksheets[0];
|
|
696
|
+
if (wsRole) {
|
|
697
|
+
const regionsRole = scanTableRegions(wsRole);
|
|
698
|
+
const region = regionsRole.find((r) => String(r.tableName).trim() === String(tableNameRole).trim());
|
|
699
|
+
if (region) {
|
|
700
|
+
const colRole = region.columnIndex["roleId"] || region.columnIndex["ROLE_ID"] || 1;
|
|
701
|
+
const colPerm = region.columnIndex["permId"] || region.columnIndex["PERM_ID"] || 2;
|
|
702
|
+
refPerms.forEach((pair, idx) => {
|
|
703
|
+
const row = wsRole.getRow(region.dataStartRow + idx);
|
|
704
|
+
row.getCell(colRole).value = pair.ROLE_ID;
|
|
705
|
+
row.getCell(colPerm).value = pair.PERM_ID;
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
refWorkbook = new ExcelJS.Workbook();
|
|
711
|
+
const wsRole = refWorkbook.addWorksheet(defRole.sheetName || "DATA-PERM");
|
|
712
|
+
wsRole.addRow([]);
|
|
713
|
+
wsRole.addRow([]);
|
|
714
|
+
wsRole.addRow(["{TABLE}", tableNameRole, "角色和权限关系", "", ""]);
|
|
715
|
+
wsRole.addRow(["角色ID", "权限ID"]);
|
|
716
|
+
wsRole.addRow(["roleId", "permId"]);
|
|
717
|
+
refPerms.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
718
|
+
}
|
|
719
|
+
const outRefRolePath = path.join(rbacRoleDir, roleFileName);
|
|
720
|
+
await refWorkbook.xlsx.writeFile(outRefRolePath);
|
|
721
|
+
roleWritePaths.push(outRefRolePath);
|
|
722
|
+
Ec.info("[ex-api] 已写入 RBAC_ROLE/ADMIN.SUPER(参考角色):" + outRefRolePath);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 其他角色输出:当前工作目录下 src/main/resources/init/oob/role/{CODE}/ 目录
|
|
727
|
+
const nonReferenceRoleIds = roleIds.filter((rid) => String(rid) !== REFERENCE_ROLE_ID);
|
|
728
|
+
for (const rid of nonReferenceRoleIds) {
|
|
729
|
+
const code = roleIdToCode[String(rid)] || "";
|
|
730
|
+
if (!code) {
|
|
731
|
+
Ec.info("[ex-api] ⚠️ 警告:角色 ID=" + rid + " 缺少 CODE,跳过输出");
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const targetRoleDir = path.join(cwd, "src", "main", "resources", "init", "oob", "role", code);
|
|
735
|
+
if (!fs.existsSync(targetRoleDir)) {
|
|
736
|
+
Ec.info("[ex-api] ⚠️ 警告:未找到输出目录 " + targetRoleDir + ",跳过输出");
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const oneRolePerms = rolePermsToWrite.filter((p) => String(p.ROLE_ID) === String(rid));
|
|
741
|
+
if (oneRolePerms.length === 0) continue;
|
|
742
|
+
|
|
743
|
+
let oneRoleWorkbook;
|
|
744
|
+
if (fs.existsSync(templateRolePath)) {
|
|
745
|
+
oneRoleWorkbook = await new ExcelJS.Workbook().xlsx.readFile(templateRolePath);
|
|
746
|
+
const wsRole = oneRoleWorkbook.getWorksheet(defRole.sheetName || "DATA-PERM") || oneRoleWorkbook.worksheets[0];
|
|
747
|
+
if (wsRole) {
|
|
748
|
+
const regionsRole = scanTableRegions(wsRole);
|
|
749
|
+
const region = regionsRole.find((r) => String(r.tableName).trim() === String(tableNameRole).trim());
|
|
750
|
+
if (region) {
|
|
751
|
+
const colRole = region.columnIndex["roleId"] || region.columnIndex["ROLE_ID"] || 1;
|
|
752
|
+
const colPerm = region.columnIndex["permId"] || region.columnIndex["PERM_ID"] || 2;
|
|
753
|
+
oneRolePerms.forEach((pair, idx) => {
|
|
674
754
|
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);
|
|
755
|
+
row.getCell(colRole).value = pair.ROLE_ID;
|
|
756
|
+
row.getCell(colPerm).value = pair.PERM_ID;
|
|
680
757
|
});
|
|
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
758
|
}
|
|
686
|
-
} else {
|
|
687
|
-
Ec.info("[ex-api] 未找到 R_ROLE_PERM 区域(tableName=" + tableNameRole + "),跳过写入");
|
|
688
759
|
}
|
|
760
|
+
} else {
|
|
761
|
+
oneRoleWorkbook = new ExcelJS.Workbook();
|
|
762
|
+
const wsRole = oneRoleWorkbook.addWorksheet(defRole.sheetName || "DATA-PERM");
|
|
763
|
+
wsRole.addRow([]);
|
|
764
|
+
wsRole.addRow([]);
|
|
765
|
+
wsRole.addRow(["{TABLE}", tableNameRole, "角色和权限关系", "", ""]);
|
|
766
|
+
wsRole.addRow(["角色ID", "权限ID"]);
|
|
767
|
+
wsRole.addRow(["roleId", "permId"]);
|
|
768
|
+
oneRolePerms.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
689
769
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
rolePermsToWrite.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
770
|
+
|
|
771
|
+
const outOneRolePath = path.join(targetRoleDir, roleFileName);
|
|
772
|
+
await oneRoleWorkbook.xlsx.writeFile(outOneRolePath);
|
|
773
|
+
roleWritePaths.push(outOneRolePath);
|
|
774
|
+
Ec.info("[ex-api] 已写入角色目录(" + code + "):" + outOneRolePath);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (roleWritePaths.length === 0) {
|
|
778
|
+
Ec.info("[ex-api] RBAC_ROLE 未输出任何文件(可能角色未选中或目标目录不存在)");
|
|
700
779
|
}
|
|
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
780
|
|
|
706
781
|
Ec.info("[ex-api] ✅ 执行完成(幂等)");
|
|
707
782
|
Ec.info("[ex-api] 📋 汇总:");
|
|
708
783
|
Ec.info("[ex-api] 🔑 ACTION_ID = " + (actionId || "—"));
|
|
709
784
|
Ec.info("[ex-api] 🔑 RESOURCE_ID = " + (resourceId || "—"));
|
|
710
785
|
Ec.info("[ex-api] 🔑 PERMISSION_ID = " + (permissionId || "—"));
|
|
711
|
-
Ec.info("[ex-api] 👥
|
|
786
|
+
Ec.info("[ex-api] 👥 授权角色总数 = " + (rolePermsToWrite.length > 0 ? rolePermsToWrite.length : 0));
|
|
787
|
+
|
|
788
|
+
// 固定输出:超级管理员角色
|
|
789
|
+
const refRoleName = roleIdToName[REFERENCE_ROLE_ID] || "超级管理员";
|
|
790
|
+
const refRoleCode = roleIdToCode[REFERENCE_ROLE_ID] || "ADMIN.SUPER";
|
|
791
|
+
Ec.info("[ex-api] 🔒 固定输出角色:");
|
|
792
|
+
Ec.info("[ex-api] [1] " + refRoleName + " (CODE: " + refRoleCode + ", ID: " + REFERENCE_ROLE_ID + ")");
|
|
793
|
+
|
|
794
|
+
// 用户选择的角色
|
|
795
|
+
if (roleIds && roleIds.length > 0) {
|
|
796
|
+
Ec.info("[ex-api] 👤 用户选择角色:");
|
|
797
|
+
roleIds.forEach((rid, idx) => {
|
|
798
|
+
const code = roleIdToCode[rid] || "—";
|
|
799
|
+
const name = roleIdToName[rid] || "—";
|
|
800
|
+
Ec.info("[ex-api] [" + (idx + 1) + "] " + name + " (CODE: " + code + ", ID: " + rid + ")");
|
|
801
|
+
});
|
|
802
|
+
} else {
|
|
803
|
+
Ec.info("[ex-api] 👤 用户选择角色:无");
|
|
804
|
+
}
|
|
805
|
+
|
|
712
806
|
Ec.info("[ex-api] 📁 RBAC_RESOURCE = " + outResPath);
|
|
713
|
-
Ec.info("[ex-api] 📁 RBAC_ROLE = " +
|
|
807
|
+
roleWritePaths.forEach((p) => Ec.info("[ex-api] 📁 RBAC_ROLE = " + p));
|
|
714
808
|
if (insertedResource) {
|
|
715
809
|
Ec.info("[ex-api] 📦 S_RESOURCE 本次插入字段:");
|
|
716
810
|
Object.keys(insertedResource).forEach((k) => Ec.info("[ex-api] " + k + " = " + (insertedResource[k] != null ? insertedResource[k] : "—")));
|
|
@@ -756,7 +850,7 @@ async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
|
756
850
|
permissionId: permissionId || "—",
|
|
757
851
|
roleCount: roleIds ? roleIds.length : 0,
|
|
758
852
|
outResPath: outResPath || "—",
|
|
759
|
-
|
|
853
|
+
outRolePaths: roleWritePaths || [],
|
|
760
854
|
ok: true
|
|
761
855
|
};
|
|
762
856
|
} 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
|
/**
|