You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1610 lines
147 KiB

diff --git a/.scratch/permission-seeder-seam/issues/01-create-permission-seeder-seam.md b/.scratch/permission-seeder-seam/issues/01-create-permission-seeder-seam.md
new file mode 100644
index 0000000..2d62a94
--- /dev/null
+++ b/.scratch/permission-seeder-seam/issues/01-create-permission-seeder-seam.md
@@ -0,0 +1,19 @@
+# 01 鈥?鍒涘缓 PermissionSeeder seam
+
+**What to build:** crm-base 涓畾涔?`PermissionSeeder` interface銆乣PermissionModuleDescriptor` 鍜?`ButtonSeed` record銆俢rm-auth 涓疄鐜?`PermissionSeederImpl`锛屽惛鏀跺綋鍓?`DataInitializer` 鍜?`DictPermissionInitializer` 閲嶅鐨?find-or-create + bind 閫昏緫鈥斺€旀寜 `name+parentId` 鏌ョ洰褰曞拰鑿滃崟銆佹寜 `perms` 鏌ユ寜閽紙骞傜瓑閿級銆佹寜 `roleCode` 鏌ヨ鑹插苟缁戝畾銆傜‖缂栫爜 `sys_menu` 鐨?schema 榛樿鍊硷紙`menuType`銆乣denyBehavior="hide"`銆乣status="enabled"`銆乣visible=true`锛夛紝璋冪敤鏂逛笉鎰熺煡杩欎簺瀛楁銆?+
+**Blocked by:** None 鈥?鍙珛鍗冲紑濮?+
+**Status:** ready-for-agent
+
+- [ ] `PermissionSeeder` interface 瀛樺湪浜?crm-base锛屽惈鍗曚釜 `seedModule(PermissionModuleDescriptor)` 鏂规硶
+- [ ] `PermissionModuleDescriptor` record 瀛樺湪浜?crm-base domain.dto锛屽瓧娈碉細catalogName, menuName, menuPath, menuComponent, menuSort, List\<ButtonSeed\> buttons, List\<String\> bindToRoleCodes
+- [ ] `ButtonSeed` record 瀛樺湪浜?crm-base domain.dto锛屽瓧娈碉細name, perms, apiUrl, sort
+- [ ] `PermissionSeederImpl` 瀛樺湪浜?crm-auth service.impl锛宍@Service` 娉ㄨВ锛岀洿鎺ヤ娇鐢?SysMenuMapper / SysRoleMapper / SysRoleMenuMapper
+- [ ] seedModule 瀹炵幇 find-or-create锛氱洰褰曪紙type=1, parentId=0, by name锛夈€佽彍鍗曪紙type=2, parentId=catalogId, by name锛夈€佹寜閽紙type=3, parentId=menuId, by perms锛?+- [ ] seedModule 瀹炵幇 bind锛氭寜 roleCode 鏌?sys_role锛屾壘鍒板垯 bindIfAbsent锛屾壘涓嶅埌 warn 璺宠繃
+- [ ] seedModule 骞傜瓑锛氬悓涓€鎻忚堪绗﹁皟鐢ㄤ袱娆′笉浜х敓閲嶅璁板綍
+- [ ] seedModule 鐩綍鍏变韩锛氫袱娆¤皟鐢ㄥ悓涓€ catalogName 鍙垱寤轰竴娆$洰褰?+- [ ] 纭紪鐮侀粯璁ゅ€硷細denyBehavior="hide", status="enabled", visible=true, menuType 鏋氫妇鍊尖€斺€旀弿杩扮涓笉鍚繖浜涘瓧娈?+- [ ] `PermissionSeederImplTest`锛圚2 闆嗘垚娴嬭瘯锛夐€氳繃锛岃鐩栦互涓婃墍鏈夎涓?+- [ ] 鐜版湁 `DataInitializer` 鍜?`DictPermissionInitializer` 淇濇寔涓嶅彉锛岀幇鏈夋祴璇曚粛鐒堕€氳繃
diff --git a/.scratch/permission-seeder-seam/issues/02-migrate-data-initializer.md b/.scratch/permission-seeder-seam/issues/02-migrate-data-initializer.md
new file mode 100644
index 0000000..21a13c4
--- /dev/null
+++ b/.scratch/permission-seeder-seam/issues/02-migrate-data-initializer.md
@@ -0,0 +1,14 @@
+# 02 鈥?杩佺Щ DataInitializer 鍒?seam
+
+**What to build:** 閲嶅啓 crm-auth 鐨?`DataInitializer`鈥斺€斾繚鐣?`ROLE_ADMIN` 鍒涘缓閫昏緫涓嶅彉锛岀劧鍚庣敤 3 娆?`seedModule` 璋冪敤鏇夸唬鍐呰仈鐨?find-or-create 鍜?bind 閫昏緫锛堣鑹茬鐞?5 涓寜閽€佽彍鍗曠鐞嗘棤鎸夐挳銆侀儴闂ㄧ鐞嗘棤鎸夐挳锛夈€傚姞 `@Order(1)` 纭繚鍦ㄥ叾浠栨ā鍧楀垵濮嬪寲鍣ㄤ箣鍓嶈繍琛屻€傚垹闄?`DataInitializer` 涓 `PermissionSeederImpl` 鍚告敹鐨勮緟鍔╂柟娉曘€?+
+**Blocked by:** 01 鈥?鍒涘缓 PermissionSeeder seam
+
+**Status:** ready-for-agent
+
+- [ ] `DataInitializer` 娉ㄥ叆 `PermissionSeeder`锛屼笉鍐嶆敞鍏?SysMenuMapper / SysRoleMapper / SysRoleMenuMapper锛堣鑹插垱寤轰粛鐢?SysRoleMapper锛?+- [ ] `DataInitializer` 鍒涘缓 ROLE_ADMIN 鍚庤皟鐢?seedModule 3 娆★細瑙掕壊绠$悊锛? 涓?crm:role:* 鎸夐挳锛夈€佽彍鍗曠鐞嗭紙鏃犳寜閽級銆侀儴闂ㄧ鐞嗭紙鏃犳寜閽級
+- [ ] `DataInitializer` 鏍囨敞 `@Order(1)`
+- [ ] `DataInitializer` 涓笉鍐嶅寘鍚?findMenuByName / insertMenu / insertButtonIfAbsent / bindIfAbsent 绛夎緟鍔╂柟娉?+- [ ] 绯荤粺鍚姩鍚?crm:role:* 鏉冮檺鐐规纭瀛愬寲锛孯OLE_ADMIN 鎷ユ湁鍏ㄩ儴鑿滃崟/鎸夐挳缁戝畾
+- [ ] crm-auth 鍏ㄩ儴娴嬭瘯閫氳繃
diff --git a/.scratch/permission-seeder-seam/issues/03-migrate-dict-initializer-delete-shadows.md b/.scratch/permission-seeder-seam/issues/03-migrate-dict-initializer-delete-shadows.md
new file mode 100644
index 0000000..fdba50f
--- /dev/null
+++ b/.scratch/permission-seeder-seam/issues/03-migrate-dict-initializer-delete-shadows.md
@@ -0,0 +1,18 @@
+# 03 鈥?杩佺Щ DictPermissionInitializer + 鍒犻櫎褰卞瓙瀹炰綋
+
+**What to build:** 閲嶅啓 crm-dict 鐨?`DictPermissionInitializer`鈥斺€旂敤 1 娆?`seedModule` 璋冪敤鏇夸唬褰卞瓙瀹炰綋 + 鍐呰仈閫昏緫锛堟暟鎹瓧鍏歌彍鍗?+ 10 涓?`dict:*` 鎸夐挳銆佺粦缁?`ROLE_ADMIN`锛夈€傚姞 `@Order(10)`銆傚垹闄や笁涓奖瀛愬疄浣擄紙`SysMenuSeed`銆乣SysRoleSeed`銆乣SysRoleMenuSeed`锛夊拰瀵瑰簲鐨勪笁涓?Mapper鈥斺€斿畠浠凡鍙樹负姝讳唬鐮併€傞噸鍐?`DictPermissionInitializerTest` 涓?mock `PermissionSeeder` 鐨勫崟鍏冩祴璇曪紝楠岃瘉鎻忚堪绗﹀瓧娈碉紝绉婚櫎 H2 涓?`sys_menu`/`sys_role`/`sys_role_menu` 琛ㄧ殑 schema 鍒涘缓銆?+
+**Blocked by:** 01 鈥?鍒涘缓 PermissionSeeder seam
+
+**Status:** ready-for-agent
+
+- [ ] `DictPermissionInitializer` 娉ㄥ叆 `PermissionSeeder`锛屼笉鍐嶆敞鍏?SysMenuSeedMapper / SysRoleSeedMapper / SysRoleMenuSeedMapper
+- [ ] `DictPermissionInitializer` 璋冪敤 seedModule 1 娆★細鏁版嵁瀛楀吀鑿滃崟 + 10 涓?dict:* 鎸夐挳 + bindToRoleCodes=["ROLE_ADMIN"]
+- [ ] `DictPermissionInitializer` 鏍囨敞 `@Order(10)`
+- [ ] `SysMenuSeed`銆乣SysRoleSeed`銆乣SysRoleMenuSeed` 涓変釜瀹炰綋绫诲凡鍒犻櫎
+- [ ] `SysMenuSeedMapper`銆乣SysRoleSeedMapper`銆乣SysRoleMenuSeedMapper` 涓変釜 Mapper 宸插垹闄?+- [ ] crm-dict 涓棤浠讳綍浠g爜寮曠敤 sys_menu / sys_role / sys_role_menu 鐗╃悊琛?+- [ ] `DictPermissionInitializerTest` 鏀逛负 mock `PermissionSeeder`锛岄獙璇佷紶鍏ョ殑 PermissionModuleDescriptor 瀛楁锛坈atalogName銆乵enuName銆乵enuPath銆乥uttons 鐨?perms 鍒楄〃銆乥indToRoleCodes锛?+- [ ] `DictPermissionInitializerTest` 涓嶅啀鍒涘缓 H2 鐨?sys_menu / sys_role / sys_role_menu 琛?+- [ ] crm-dict 鍏ㄩ儴娴嬭瘯閫氳繃
+- [ ] 绯荤粺鍚姩鍚?dict:* 鏉冮檺鐐规纭瀛愬寲
diff --git a/CONTEXT-MAP.md b/CONTEXT-MAP.md
index f8253ec..71c6541 100644
--- a/CONTEXT-MAP.md
+++ b/CONTEXT-MAP.md
@@ -9,12 +9,12 @@ This is a multi-module Maven monorepo. Each `crm-{鍩焳` module is a bounded cont
| `crm-auth` | 璁よ瘉涓庣涓夋柟鐧诲綍 | [`crm-auth/CONTEXT.md`](./crm-auth/CONTEXT.md) |
| `crm-dict` | 鏁版嵁瀛楀吀(骞冲彴绾ч敭鍊煎瓧鍏? | [`crm-dict/CONTEXT.md`](./crm-dict/CONTEXT.md) |
| `crm-file` | 鏂囦欢瀛樺偍(涓婁紶/棰勮) | [`crm-file/CONTEXT.md`](./crm-file/CONTEXT.md) |
-| `crm-base` | 鍩虹鍖?閫氱敤鑳藉姏,闈炰笟鍔″煙) | _(鏆傛棤鐙珛棰嗗煙璇█)_ |
+| `crm-base` | 鍩虹鍖?閫氱敤鑳藉姏,闈炰笟鍔″煙) | [`crm-base/CONTEXT.md`](./crm-base/CONTEXT.md) |
| `crm-app` | 鍞竴鍚姩鍏ュ彛 | _(鏆傛棤鐙珛棰嗗煙璇█)_ |
## System-wide decisions
-ADR 鐩綍 `docs/adr/`(绯荤粺绾у喅绛?銆俙README.md` 褰撳墠寮曠敤浜?ADR-0003 / 0004 / 0006,浣嗘枃浠跺皻鏈惤鍦扳€斺€旂敱 `/domain-modeling` 鍦ㄥ喅绛栫湡姝h瑙e喅鏃惰ˉ榻愩€?+ADR 鐩綍 `docs/adr/`(绯荤粺绾у喅绛?銆俙README.md` 褰撳墠寮曠敤浜?ADR-0003 / 0004 / 0006,浣嗘枃浠跺皻鏈惤鍦扳€斺€旂敱 `/domain-modeling` 鍦ㄥ喅绛栫湡姝h瑙e喅鏃惰ˉ榻愩€侫DR-0016 璁板綍浜?PermissionSeeder seam 鍐崇瓥(娑堥櫎璺ㄦā鍧楁潈闄愮瀛愬寲鐨勫奖瀛愬疄浣?銆?
## Adding a new context
diff --git a/crm-auth/src/main/java/com/crm/auth/config/DataInitializer.java b/crm-auth/src/main/java/com/crm/auth/config/DataInitializer.java
index 5411a9e..aa18e1b 100644
--- a/crm-auth/src/main/java/com/crm/auth/config/DataInitializer.java
+++ b/crm-auth/src/main/java/com/crm/auth/config/DataInitializer.java
@@ -1,33 +1,33 @@
package com.crm.auth.config;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.crm.auth.domain.entity.SysMenu;
import com.crm.auth.domain.entity.SysRole;
-import com.crm.auth.domain.entity.SysRoleMenu;
import com.crm.auth.domain.enums.DataScopeEnum;
-import com.crm.auth.mapper.SysMenuMapper;
import com.crm.auth.mapper.SysRoleMapper;
-import com.crm.auth.mapper.SysRoleMenuMapper;
+import com.crm.base.domain.dto.ButtonSeed;
+import com.crm.base.domain.dto.PermissionModuleDescriptor;
+import com.crm.base.service.PermissionSeeder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 绯荤粺鏁版嵁鍒濆鍖栵紙骞傜瓑锛夛細姣忔鍚姩閫愰」妫€鏌ワ紝鍙ˉ缂虹殑銆?- * <p>鍒涘缓鍐呯疆绠$悊鍛樿鑹层€佺郴缁熺鐞嗚彍鍗曘€佽鑹茬鐞?button 鏉冮檺鐐癸紝
- * 骞跺皢鍏ㄩ儴璧勬簮鍒嗛厤缁欑鐞嗗憳瑙掕壊锛圓DR-0012锛夈€?/p>
+ * <p>鍒涘缓鍐呯疆绠$悊鍛樿鑹诧紝鐒跺悗閫氳繃 {@link PermissionSeeder} seam 绉嶅瓙鍖栫郴缁熺鐞嗚彍鍗曘€?+ * 瑙掕壊绠$悊/鑿滃崟绠$悊/閮ㄩ棬绠$悊鐨?button 鏉冮檺鐐癸紝骞跺皢鍏ㄩ儴璧勬簮鍒嗛厤缁欑鐞嗗憳瑙掕壊锛圓DR-0012銆丄DR-0016锛夈€?/p>
*/
@Slf4j
@Component
+@Order(1)
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {
private final SysRoleMapper sysRoleMapper;
- private final SysMenuMapper sysMenuMapper;
- private final SysRoleMenuMapper sysRoleMenuMapper;
+ private final PermissionSeeder permissionSeeder;
@Override
public void run(String... args) {
@@ -51,115 +51,45 @@ public class DataInitializer implements CommandLineRunner {
log.info("琛ヨ鍐呯疆瑙掕壊鏍囪锛歿}", adminRole.getRoleCode());
}
- // ---- 涓€绾х洰褰曪細绯荤粺绠$悊 ----
- SysMenu sysManage = findMenu("绯荤粺绠$悊", 0L);
- if (sysManage == null) {
- sysManage = insertMenu(new MenuSeed("绯荤粺绠$悊", 0L, 1, null, null, 0));
- }
-
- // ---- 浜岀骇鑿滃崟 ----
- SysMenu roleManage = findMenu("瑙掕壊绠$悊", sysManage.getId());
- if (roleManage == null) {
- roleManage = insertMenu(new MenuSeed("瑙掕壊绠$悊", sysManage.getId(), 2,
- "/system/role", "system/role/index", 1));
- }
- SysMenu menuManage = findMenu("鑿滃崟绠$悊", sysManage.getId());
- if (menuManage == null) {
- menuManage = insertMenu(new MenuSeed("鑿滃崟绠$悊", sysManage.getId(), 2,
- "/system/menu", "system/menu/index", 2));
- }
- SysMenu deptManage = findMenu("閮ㄩ棬绠$悊", sysManage.getId());
- if (deptManage == null) {
- deptManage = insertMenu(new MenuSeed("閮ㄩ棬绠$悊", sysManage.getId(), 2,
- "/system/dept", "system/dept/index", 3));
- }
-
- // ---- 瑙掕壊绠$悊 button 鏉冮檺鐐癸紙ADR-0012锛?----
- insertButtonIfAbsent("瑙掕壊鍒楄〃", roleManage.getId(), "crm:role:list", "/api/roles/page", 1);
- insertButtonIfAbsent("瑙掕壊璇︽儏", roleManage.getId(), "crm:role:detail", "/api/roles/detail", 2);
- insertButtonIfAbsent("瑙掕壊淇濆瓨", roleManage.getId(), "crm:role:save", "/api/roles/saveOrUpdate", 3);
- insertButtonIfAbsent("瑙掕壊鍒犻櫎", roleManage.getId(), "crm:role:delete", "/api/roles/delete", 4);
- insertButtonIfAbsent("鏉冮檺鍒嗛厤", roleManage.getId(), "crm:role:assign", "/api/roles/assign-resources", 5);
-
- // ---- 瑙掕壊-鑿滃崟缁戝畾锛堢鐞嗗憳鎷ユ湁鎵€鏈夎彍鍗曞拰鏉冮檺鐐癸級 ----
- bindIfAbsent(adminRole.getId(), sysManage.getId());
- bindIfAbsent(adminRole.getId(), roleManage.getId());
- bindIfAbsent(adminRole.getId(), menuManage.getId());
- bindIfAbsent(adminRole.getId(), deptManage.getId());
- // 缁戝畾瑙掕壊绠$悊涓嬬殑鍏ㄩ儴 button 鏉冮檺鐐?- List<SysMenu> roleButtons = sysMenuMapper.selectList(
- new LambdaQueryWrapper<SysMenu>()
- .eq(SysMenu::getParentId, roleManage.getId())
- .eq(SysMenu::getMenuType, 3));
- for (SysMenu btn : roleButtons) {
- bindIfAbsent(adminRole.getId(), btn.getId());
- }
+ // ---- 瑙掕壊绠$悊鑿滃崟 + 5 涓?button 鏉冮檺鐐癸紙ADR-0012锛?----
+ permissionSeeder.seedModule(new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "瑙掕壊绠$悊",
+ "/system/role",
+ "system/role/index",
+ 1,
+ List.of(
+ new ButtonSeed("瑙掕壊鍒楄〃", "crm:role:list", "/api/roles/page", 1),
+ new ButtonSeed("瑙掕壊璇︽儏", "crm:role:detail", "/api/roles/detail", 2),
+ new ButtonSeed("瑙掕壊淇濆瓨", "crm:role:save", "/api/roles/saveOrUpdate", 3),
+ new ButtonSeed("瑙掕壊鍒犻櫎", "crm:role:delete", "/api/roles/delete", 4),
+ new ButtonSeed("鏉冮檺鍒嗛厤", "crm:role:assign", "/api/roles/assign-resources", 5)
+ ),
+ List.of("ROLE_ADMIN")
+ ));
+
+ // ---- 鑿滃崟绠$悊鑿滃崟锛堟棤 button锛?----
+ permissionSeeder.seedModule(new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "鑿滃崟绠$悊",
+ "/system/menu",
+ "system/menu/index",
+ 2,
+ List.of(),
+ List.of("ROLE_ADMIN")
+ ));
+
+ // ---- 閮ㄩ棬绠$悊鑿滃崟锛堟棤 button锛?----
+ permissionSeeder.seedModule(new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "閮ㄩ棬绠$悊",
+ "/system/dept",
+ "system/dept/index",
+ 3,
+ List.of(),
+ List.of("ROLE_ADMIN")
+ ));
log.info("鏉冮檺鏁版嵁鍒濆鍖栨鏌ュ畬鎴?);
}
-
- // ==================== 骞傜瓑杈呭姪鏂规硶 ====================
-
- private SysMenu findMenu(String name, Long parentId) {
- return sysMenuMapper.selectOne(
- new LambdaQueryWrapper<SysMenu>()
- .eq(SysMenu::getMenuName, name)
- .eq(SysMenu::getParentId, parentId));
- }
-
- private SysMenu insertMenu(MenuSeed seed) {
- SysMenu m = new SysMenu();
- m.setParentId(seed.parentId());
- m.setMenuName(seed.name());
- m.setMenuType(seed.type());
- m.setPath(seed.path());
- m.setComponent(seed.component());
- m.setSort(seed.sort());
- m.setVisible(true);
- sysMenuMapper.insert(m);
- log.info("鍒涘缓鑿滃崟鑺傜偣锛歿} (type={})", seed.name(), seed.type());
- return m;
- }
-
- /**
- * 鑿滃崟绉嶅瓙锛氭秷闄?insertMenu 鐨勪綅缃弬鏁版涔夈€?- */
- private record MenuSeed(String name, Long parentId, int type,
- String path, String component, int sort) {
- }
-
- private void insertButtonIfAbsent(String name, Long parentId,
- String perms, String apiUrl, int sort) {
- Long count = sysMenuMapper.selectCount(
- new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getPerms, perms));
- if (count > 0) {
- return;
- }
- SysMenu btn = new SysMenu();
- btn.setParentId(parentId);
- btn.setMenuName(name);
- btn.setMenuType(3);
- btn.setSort(sort);
- btn.setVisible(true);
- btn.setPerms(perms);
- btn.setDenyBehavior("hide");
- btn.setApiUrl(apiUrl);
- btn.setStatus("enabled");
- sysMenuMapper.insert(btn);
- log.info("鍒涘缓鏉冮檺鐐癸細{} -> {}", name, perms);
- }
-
- private void bindIfAbsent(Long roleId, Long menuId) {
- Long count = sysRoleMenuMapper.selectCount(
- new LambdaQueryWrapper<SysRoleMenu>()
- .eq(SysRoleMenu::getRoleId, roleId)
- .eq(SysRoleMenu::getMenuId, menuId));
- if (count > 0) {
- return;
- }
- SysRoleMenu rm = new SysRoleMenu();
- rm.setRoleId(roleId);
- rm.setMenuId(menuId);
- sysRoleMenuMapper.insert(rm);
- }
}
diff --git a/crm-auth/src/main/java/com/crm/auth/service/impl/PermissionSeederImpl.java b/crm-auth/src/main/java/com/crm/auth/service/impl/PermissionSeederImpl.java
new file mode 100644
index 0000000..3b0691f
--- /dev/null
+++ b/crm-auth/src/main/java/com/crm/auth/service/impl/PermissionSeederImpl.java
@@ -0,0 +1,152 @@
+package com.crm.auth.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.crm.auth.domain.entity.SysMenu;
+import com.crm.auth.domain.entity.SysRole;
+import com.crm.auth.domain.entity.SysRoleMenu;
+import com.crm.auth.mapper.SysMenuMapper;
+import com.crm.auth.mapper.SysRoleMapper;
+import com.crm.auth.mapper.SysRoleMenuMapper;
+import com.crm.base.domain.dto.ButtonSeed;
+import com.crm.base.domain.dto.PermissionModuleDescriptor;
+import com.crm.base.service.PermissionSeeder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 鏉冮檺绉嶅瓙鍖?seam 瀹炵幇锛圓DR-0016锛?+ * <p>鎷ユ湁 sys_menu 鐨勫叏閮ㄥ瓧娈电煡璇嗭紝璐熻矗 find-or-create 骞傜瓑鍐欏叆涓庤鑹茬粦瀹氥€?+ * 纭紪鐮?schema 榛樿鍊硷細denyBehavior="hide"銆乻tatus="enabled"銆乿isible=true銆乵enuType 鏋氫妇鍊笺€?/p>
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PermissionSeederImpl implements PermissionSeeder {
+
+ private final SysMenuMapper sysMenuMapper;
+ private final SysRoleMapper sysRoleMapper;
+ private final SysRoleMenuMapper sysRoleMenuMapper;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void seedModule(PermissionModuleDescriptor descriptor) {
+ log.debug("绉嶅瓙鍖栨潈闄愭ā鍧楋細catalog={}, menu={}", descriptor.catalogName(), descriptor.menuName());
+
+ // 1. Find-or-create catalog (type=1, parentId=0)
+ SysMenu catalog = findMenuByName(descriptor.catalogName(), 0L);
+ if (catalog == null) {
+ catalog = insertCatalog(descriptor.catalogName());
+ }
+
+ // 2. Find-or-create menu (type=2, parentId=catalogId)
+ SysMenu menu = findMenuByName(descriptor.menuName(), catalog.getId());
+ if (menu == null) {
+ menu = insertMenu(descriptor.menuName(), catalog.getId(),
+ descriptor.menuPath(), descriptor.menuComponent(), descriptor.menuSort());
+ }
+
+ // 3. Find-or-create buttons (type=3, parentId=menuId, by perms)
+ List<Long> buttonIds = new ArrayList<>();
+ for (ButtonSeed seed : descriptor.buttons()) {
+ Long buttonId = insertButtonIfAbsent(seed.name(), menu.getId(),
+ seed.perms(), seed.apiUrl(), seed.sort());
+ buttonIds.add(buttonId);
+ }
+
+ // 4. Bind to roles (by roleCode, warn-skip if not found)
+ for (String roleCode : descriptor.bindToRoleCodes()) {
+ SysRole role = sysRoleMapper.selectOne(
+ new LambdaQueryWrapper<SysRole>().eq(SysRole::getRoleCode, roleCode));
+ if (role == null) {
+ log.warn("鍐呯疆瑙掕壊 {} 涓嶅瓨鍦紝璺宠繃鏉冮檺缁戝畾锛堣瑙掕壊鍒涘缓鍚庝笅娆″惎鍔ㄨˉ缁戯級", roleCode);
+ continue;
+ }
+ bindIfAbsent(role.getId(), catalog.getId());
+ bindIfAbsent(role.getId(), menu.getId());
+ for (Long buttonId : buttonIds) {
+ bindIfAbsent(role.getId(), buttonId);
+ }
+ }
+
+ log.debug("鏉冮檺妯″潡绉嶅瓙鍖栧畬鎴愶細{} / {}", descriptor.catalogName(), descriptor.menuName());
+ }
+
+ // ==================== 骞傜瓑杈呭姪鏂规硶 ====================
+
+ private SysMenu findMenuByName(String name, Long parentId) {
+ return sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, name)
+ .eq(SysMenu::getParentId, parentId));
+ }
+
+ private SysMenu insertCatalog(String name) {
+ SysMenu m = new SysMenu();
+ m.setParentId(0L);
+ m.setMenuName(name);
+ m.setMenuType(1); // 鐩綍
+ m.setSort(0);
+ m.setVisible(true);
+ sysMenuMapper.insert(m);
+ log.info("鍒涘缓鐩綍鑺傜偣锛歿} (type=1)", name);
+ return m;
+ }
+
+ private SysMenu insertMenu(String name, Long parentId, String path, String component, int sort) {
+ SysMenu m = new SysMenu();
+ m.setParentId(parentId);
+ m.setMenuName(name);
+ m.setMenuType(2); // 鑿滃崟
+ m.setPath(path);
+ m.setComponent(component);
+ m.setSort(sort);
+ m.setVisible(true);
+ sysMenuMapper.insert(m);
+ log.info("鍒涘缓鑿滃崟鑺傜偣锛歿} (type=2)", name);
+ return m;
+ }
+
+ /**
+ * 鎸夋潈闄愮爜鏌ラ噸鎻掑叆鎸夐挳鏉冮檺鐐癸紝杩斿洖鍏?id锛堝凡瀛樺湪鍒欒繑鍥炵幇鏈?id锛夈€?+ */
+ private Long insertButtonIfAbsent(String name, Long parentId, String perms,
+ String apiUrl, int sort) {
+ SysMenu exist = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getPerms, perms));
+ if (exist != null) {
+ return exist.getId();
+ }
+ SysMenu btn = new SysMenu();
+ btn.setParentId(parentId);
+ btn.setMenuName(name);
+ btn.setMenuType(3); // 鎸夐挳
+ btn.setSort(sort);
+ btn.setVisible(true);
+ btn.setPerms(perms);
+ btn.setDenyBehavior("hide");
+ btn.setApiUrl(apiUrl);
+ btn.setStatus("enabled");
+ sysMenuMapper.insert(btn);
+ log.info("鍒涘缓鏉冮檺鐐癸細{} -> {}", name, perms);
+ return btn.getId();
+ }
+
+ private void bindIfAbsent(Long roleId, Long menuId) {
+ Long count = sysRoleMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysRoleMenu>()
+ .eq(SysRoleMenu::getRoleId, roleId)
+ .eq(SysRoleMenu::getMenuId, menuId));
+ if (count > 0) {
+ return;
+ }
+ SysRoleMenu rm = new SysRoleMenu();
+ rm.setRoleId(roleId);
+ rm.setMenuId(menuId);
+ sysRoleMenuMapper.insert(rm);
+ }
+}
diff --git a/crm-auth/src/test/java/com/crm/auth/service/impl/PermissionSeederImplTest.java b/crm-auth/src/test/java/com/crm/auth/service/impl/PermissionSeederImplTest.java
new file mode 100644
index 0000000..bd66db4
--- /dev/null
+++ b/crm-auth/src/test/java/com/crm/auth/service/impl/PermissionSeederImplTest.java
@@ -0,0 +1,400 @@
+package com.crm.auth.service.impl;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
+import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
+import com.crm.auth.domain.entity.SysMenu;
+import com.crm.auth.domain.entity.SysRole;
+import com.crm.auth.domain.entity.SysRoleMenu;
+import com.crm.auth.mapper.SysMenuMapper;
+import com.crm.auth.mapper.SysRoleMapper;
+import com.crm.auth.mapper.SysRoleMenuMapper;
+import com.crm.base.config.MetaObjectFillHandler;
+import com.crm.base.domain.dto.ButtonSeed;
+import com.crm.base.domain.dto.PermissionModuleDescriptor;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.h2.jdbcx.JdbcDataSource;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.Statement;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * PermissionSeederImpl 闆嗘垚娴嬭瘯锛圓DR-0016锛?+ * <p>楠岃瘉鏉冮檺绉嶅瓙鍖?seam 鐨?find-or-create 骞傜瓑鍐欏叆涓庤鑹茬粦瀹氶€昏緫銆?/p>
+ */
+@DisplayName("鏉冮檺绉嶅瓙鍖?seam 闆嗘垚娴嬭瘯")
+class PermissionSeederImplTest {
+
+ private static SqlSessionFactory sqlSessionFactory;
+
+ private SqlSession session;
+ private PermissionSeederImpl seeder;
+ private SysMenuMapper sysMenuMapper;
+ private SysRoleMapper sysRoleMapper;
+ private SysRoleMenuMapper sysRoleMenuMapper;
+
+ private static final String[] SCHEMA = {
+ """
+ create table sys_role (
+ id bigint primary key, creator_id varchar(50), create_time datetime,
+ updater_id varchar(50), update_time datetime, deleted tinyint not null default 0,
+ role_name varchar(50), role_code varchar(50), data_scope tinyint, sort int, remark varchar(200),
+ builtin boolean default false)
+ """,
+ """
+ create table sys_menu (
+ id bigint primary key, creator_id varchar(50), create_time datetime,
+ updater_id varchar(50), update_time datetime, deleted tinyint not null default 0,
+ parent_id bigint not null default 0, menu_name varchar(50) not null, menu_type tinyint not null,
+ path varchar(200), component varchar(200), icon varchar(50), sort int default 0,
+ visible tinyint default 1, description varchar(100), perms varchar(100),
+ deny_behavior varchar(16), status varchar(16) default 'enabled', api_url varchar(200))
+ """,
+ """
+ create table sys_role_menu (
+ id bigint primary key, role_id bigint not null, menu_id bigint not null,
+ constraint uk_role_menu unique (role_id, menu_id))
+ """,
+ };
+
+ @BeforeAll
+ static void bootstrap() throws Exception {
+ JdbcDataSource ds = new JdbcDataSource();
+ ds.setURL("jdbc:h2:mem:permission-seeder-test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1");
+
+ MybatisConfiguration configuration = new MybatisConfiguration();
+ configuration.setEnvironment(new Environment("seeder-test", new JdbcTransactionFactory(), ds));
+ configuration.setMapUnderscoreToCamelCase(true);
+ GlobalConfigUtils.getGlobalConfig(configuration).setMetaObjectHandler(new MetaObjectFillHandler());
+ GlobalConfigUtils.getGlobalConfig(configuration).setIdentifierGenerator(DefaultIdentifierGenerator.getInstance());
+ configuration.addMapper(SysMenuMapper.class);
+ configuration.addMapper(SysRoleMapper.class);
+ configuration.addMapper(SysRoleMenuMapper.class);
+ sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
+
+ try (SqlSession s = sqlSessionFactory.openSession(true)) {
+ Connection conn = s.getConnection();
+ try (Statement st = conn.createStatement()) {
+ for (String sql : SCHEMA) {
+ st.execute(sql);
+ }
+ }
+ }
+ }
+
+ @BeforeEach
+ void openSession() {
+ session = sqlSessionFactory.openSession();
+ sysMenuMapper = session.getMapper(SysMenuMapper.class);
+ sysRoleMapper = session.getMapper(SysRoleMapper.class);
+ sysRoleMenuMapper = session.getMapper(SysRoleMenuMapper.class);
+ seeder = new PermissionSeederImpl(sysMenuMapper, sysRoleMapper, sysRoleMenuMapper);
+ }
+
+ @AfterEach
+ void cleanup() {
+ if (session != null) {
+ try (Statement st = session.getConnection().createStatement()) {
+ st.execute("DELETE FROM sys_role_menu");
+ st.execute("DELETE FROM sys_menu");
+ st.execute("DELETE FROM sys_role");
+ } catch (Exception e) {
+ // ignore
+ }
+ session.commit();
+ session.close();
+ }
+ }
+
+ @Test
+ @DisplayName("seedModule 鍒涘缓鐩綍銆佽彍鍗曘€佹寜閽苟缁戝畾瑙掕壊")
+ void seedModule_createsCatalogMenuButtonsAndBindsRole() {
+ // 鍑嗗锛氬垱寤轰竴涓鑹?+ SysRole role = new SysRole();
+ role.setRoleName("绠$悊鍛?);
+ role.setRoleCode("ROLE_ADMIN");
+ role.setDataScope(4);
+ role.setSort(0);
+ role.setBuiltin(true);
+ sysRoleMapper.insert(role);
+ session.commit();
+
+ // 鎵ц锛氱瀛愬寲涓€涓ā鍧?+ PermissionModuleDescriptor descriptor = new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "鏁版嵁瀛楀吀",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ List.of(
+ new ButtonSeed("鍒嗙粍鏌ヨ", "dict:group:list", "/api/dict/group/page", 1),
+ new ButtonSeed("鍒嗙粍淇濆瓨", "dict:group:save", "/api/dict/group/saveOrUpdate", 2)
+ ),
+ List.of("ROLE_ADMIN")
+ );
+ seeder.seedModule(descriptor);
+ session.commit();
+
+ // 楠岃瘉锛氱洰褰曞垱寤?+ SysMenu catalog = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "绯荤粺绠$悊")
+ .eq(SysMenu::getParentId, 0L)
+ .eq(SysMenu::getMenuType, 1));
+ assertThat(catalog).isNotNull();
+ assertThat(catalog.getVisible()).isTrue();
+
+ // 楠岃瘉锛氳彍鍗曞垱寤?+ SysMenu menu = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "鏁版嵁瀛楀吀")
+ .eq(SysMenu::getParentId, catalog.getId())
+ .eq(SysMenu::getMenuType, 2));
+ assertThat(menu).isNotNull();
+ assertThat(menu.getPath()).isEqualTo("/system/dict");
+ assertThat(menu.getComponent()).isEqualTo("system/dict/index");
+ assertThat(menu.getSort()).isEqualTo(4);
+ assertThat(menu.getVisible()).isTrue();
+
+ // 楠岃瘉锛氭寜閽垱寤?+ List<SysMenu> buttons = sysMenuMapper.selectList(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getParentId, menu.getId())
+ .eq(SysMenu::getMenuType, 3));
+ assertThat(buttons).hasSize(2);
+ assertThat(buttons).anyMatch(b -> b.getPerms().equals("dict:group:list")
+ && b.getApiUrl().equals("/api/dict/group/page")
+ && b.getDenyBehavior().equals("hide")
+ && b.getStatus().equals("enabled"));
+ assertThat(buttons).anyMatch(b -> b.getPerms().equals("dict:group:save")
+ && b.getApiUrl().equals("/api/dict/group/saveOrUpdate"));
+
+ // 楠岃瘉锛氳鑹茬粦瀹?+ List<SysRoleMenu> bindings = sysRoleMenuMapper.selectList(
+ new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, role.getId()));
+ assertThat(bindings).hasSize(4); // catalog + menu + 2 buttons
+ assertThat(bindings).anyMatch(bm -> bm.getMenuId().equals(catalog.getId()));
+ assertThat(bindings).anyMatch(bm -> bm.getMenuId().equals(menu.getId()));
+ assertThat(bindings).anyMatch(bm -> bm.getMenuId().equals(buttons.get(0).getId()));
+ assertThat(bindings).anyMatch(bm -> bm.getMenuId().equals(buttons.get(1).getId()));
+ }
+
+ @Test
+ @DisplayName("seedModule 骞傜瓑锛氬悓涓€鎻忚堪绗﹁皟鐢ㄤ袱娆′笉浜х敓閲嶅璁板綍")
+ void seedModule_idempotent_noDuplicates() {
+ // 鍑嗗锛氬垱寤轰竴涓鑹?+ SysRole role = new SysRole();
+ role.setRoleName("绠$悊鍛?);
+ role.setRoleCode("ROLE_ADMIN");
+ role.setDataScope(4);
+ role.setSort(0);
+ role.setBuiltin(true);
+ sysRoleMapper.insert(role);
+ session.commit();
+
+ PermissionModuleDescriptor descriptor = new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "鏁版嵁瀛楀吀",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ List.of(new ButtonSeed("鍒嗙粍鏌ヨ", "dict:group:list", "/api/dict/group/page", 1)),
+ List.of("ROLE_ADMIN")
+ );
+
+ // 鎵ц锛氳皟鐢ㄤ袱娆?+ seeder.seedModule(descriptor);
+ session.commit();
+ seeder.seedModule(descriptor);
+ session.commit();
+
+ // 楠岃瘉锛氱洰褰曘€佽彍鍗曘€佹寜閽悇鍙湁涓€鏉?+ Long catalogCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "绯荤粺绠$悊")
+ .eq(SysMenu::getParentId, 0L)
+ .eq(SysMenu::getMenuType, 1));
+ assertThat(catalogCount).isEqualTo(1);
+
+ Long menuCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "鏁版嵁瀛楀吀")
+ .eq(SysMenu::getMenuType, 2));
+ assertThat(menuCount).isEqualTo(1);
+
+ Long buttonCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getPerms, "dict:group:list")
+ .eq(SysMenu::getMenuType, 3));
+ assertThat(buttonCount).isEqualTo(1);
+
+ // 楠岃瘉锛氳鑹茬粦瀹氫篃鍙湁涓€鏉★紙catalog + menu + button = 3锛?+ Long bindingCount = sysRoleMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, role.getId()));
+ assertThat(bindingCount).isEqualTo(3);
+ }
+
+ @Test
+ @DisplayName("seedModule 鐩綍鍏变韩锛氫袱娆¤皟鐢ㄥ悓涓€ catalogName 鍙垱寤轰竴娆$洰褰?)
+ void seedModule_sharedCatalog_createdOnce() {
+ // 鍑嗗锛氬垱寤轰竴涓鑹?+ SysRole role = new SysRole();
+ role.setRoleName("绠$悊鍛?);
+ role.setRoleCode("ROLE_ADMIN");
+ role.setDataScope(4);
+ role.setSort(0);
+ role.setBuiltin(true);
+ sysRoleMapper.insert(role);
+ session.commit();
+
+ // 鎵ц锛氫袱涓笉鍚屾ā鍧楀叡浜悓涓€鐩綍
+ PermissionModuleDescriptor descriptor1 = new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "瑙掕壊绠$悊",
+ "/system/role",
+ "system/role/index",
+ 1,
+ List.of(),
+ List.of("ROLE_ADMIN")
+ );
+ PermissionModuleDescriptor descriptor2 = new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "鏁版嵁瀛楀吀",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ List.of(),
+ List.of("ROLE_ADMIN")
+ );
+ seeder.seedModule(descriptor1);
+ session.commit();
+ seeder.seedModule(descriptor2);
+ session.commit();
+
+ // 楠岃瘉锛氱洰褰曞彧鏈変竴鏉?+ Long catalogCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "绯荤粺绠$悊")
+ .eq(SysMenu::getParentId, 0L)
+ .eq(SysMenu::getMenuType, 1));
+ assertThat(catalogCount).isEqualTo(1);
+
+ // 楠岃瘉锛氫袱涓彍鍗曢兘鍦ㄥ悓涓€鐩綍涓?+ SysMenu catalog = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "绯荤粺绠$悊")
+ .eq(SysMenu::getParentId, 0L));
+ Long menuCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getParentId, catalog.getId())
+ .eq(SysMenu::getMenuType, 2));
+ assertThat(menuCount).isEqualTo(2);
+ }
+
+ @Test
+ @DisplayName("seedModule 瑙掕壊涓嶅瓨鍦ㄦ椂 warn 璺宠繃缁戝畾")
+ void seedModule_roleNotFound_warnsAndSkipsBinding() {
+ // 鍑嗗锛氫笉鍒涘缓瑙掕壊
+
+ PermissionModuleDescriptor descriptor = new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "鏁版嵁瀛楀吀",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ List.of(new ButtonSeed("鍒嗙粍鏌ヨ", "dict:group:list", "/api/dict/group/page", 1)),
+ List.of("ROLE_ADMIN") // 瑙掕壊涓嶅瓨鍦?+ );
+
+ // 鎵ц
+ seeder.seedModule(descriptor);
+ session.commit();
+
+ // 楠岃瘉锛氱洰褰曘€佽彍鍗曘€佹寜閽兘鍒涘缓浜?+ Long catalogCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "绯荤粺绠$悊")
+ .eq(SysMenu::getParentId, 0L));
+ assertThat(catalogCount).isEqualTo(1);
+
+ Long menuCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "鏁版嵁瀛楀吀")
+ .eq(SysMenu::getMenuType, 2));
+ assertThat(menuCount).isEqualTo(1);
+
+ Long buttonCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getPerms, "dict:group:list"));
+ assertThat(buttonCount).isEqualTo(1);
+
+ // 楠岃瘉锛氭病鏈夎鑹茬粦瀹?+ Long bindingCount = sysRoleMenuMapper.selectCount(null);
+ assertThat(bindingCount).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("seedModule 绌烘寜閽垪琛細鍙垱寤虹洰褰曞拰鑿滃崟")
+ void seedModule_emptyButtons_onlyCatalogAndMenu() {
+ // 鍑嗗锛氬垱寤轰竴涓鑹?+ SysRole role = new SysRole();
+ role.setRoleName("绠$悊鍛?);
+ role.setRoleCode("ROLE_ADMIN");
+ role.setDataScope(4);
+ role.setSort(0);
+ role.setBuiltin(true);
+ sysRoleMapper.insert(role);
+ session.commit();
+
+ PermissionModuleDescriptor descriptor = new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "鑿滃崟绠$悊",
+ "/system/menu",
+ "system/menu/index",
+ 2,
+ List.of(), // 绌烘寜閽垪琛?+ List.of("ROLE_ADMIN")
+ );
+
+ // 鎵ц
+ seeder.seedModule(descriptor);
+ session.commit();
+
+ // 楠岃瘉锛氱洰褰曞拰鑿滃崟鍒涘缓浜嗭紝娌℃湁鎸夐挳
+ SysMenu catalog = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "绯荤粺绠$悊")
+ .eq(SysMenu::getParentId, 0L));
+ assertThat(catalog).isNotNull();
+
+ SysMenu menu = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "鑿滃崟绠$悊")
+ .eq(SysMenu::getParentId, catalog.getId()));
+ assertThat(menu).isNotNull();
+
+ Long buttonCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getParentId, menu.getId())
+ .eq(SysMenu::getMenuType, 3));
+ assertThat(buttonCount).isEqualTo(0);
+
+ // 楠岃瘉锛氳鑹茬粦瀹氬彧鏈?catalog + menu = 2
+ Long bindingCount = sysRoleMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, role.getId()));
+ assertThat(bindingCount).isEqualTo(2);
+ }
+}
diff --git a/crm-base/CONTEXT.md b/crm-base/CONTEXT.md
new file mode 100644
index 0000000..d768397
--- /dev/null
+++ b/crm-base/CONTEXT.md
@@ -0,0 +1,17 @@
+# 鍩虹鍖咃紙crm-base锛?+
+閫氱敤鑳藉姏涓庨潪涓氬姟鍩熷師璇€備笉瑁?controller銆佷笉寤轰笟鍔¤〃锛屽彧鎻愪緵鍩虹璁炬柦锛氬紓甯镐綋绯汇€佸搷搴斿皝瑁呫€佸垎椤点€侀洩鑺?ID銆丮yBatis-Plus 鎵╁睍銆佹暟鎹潈闄愯瘝姹囥€佸畨鍏ㄤ笂涓嬫枃宸ュ叿銆傝法妯″潡 seam 涔熸斁鍦ㄨ繖閲屻€?+
+## Language
+
+**鏉冮檺绉嶅瓙鍣?*:
+涓€涓?seam interface锛岃浠绘剰涓氬姟妯″潡澹版槑寮忓湴灏嗚嚜宸辩殑鏉冮檺鐐癸紙button 鑺傜偣锛夌瀛愬寲鍒版潈闄愯祫婧愭爲锛坄sys_menu` 鐗╃悊琛級锛岃€屼笉闇€瑕佺煡閬撹琛ㄧ殑鐗╃悊 schema銆傝皟鐢ㄦ柟閫氳繃 `PermissionModuleDescriptor` 澹版槑鐩綍鍚嶃€佽彍鍗曞悕銆佹寜閽垪琛ㄣ€佺洰鏍囪鑹茬紪鐮侊紱瀹炵幇鏂癸紙鍦?crm-auth 涓級鎷ユ湁 `sys_menu` 鐨勫叏閮ㄥ瓧娈电煡璇嗭紝璐熻矗 find-or-create 骞傜瓑鍐欏叆涓庤鑹茬粦瀹氥€傛浛浠d簡姝ゅ墠鐨勫奖瀛愬疄浣撴柟妗堚€斺€攃rm-dict 鏇惧垱寤?`SysMenuSeed` 绛夐噸澶嶅疄浣撶被鐩存帴鏄犲皠 `sys_menu` 琛紝瀵艰嚧 schema 娉勬紡銆?+_Avoid_: 鏉冮檺鍒濆鍖栧櫒銆佹潈闄愭敞鍏ュ櫒銆佹潈闄愭敞鍐屽櫒
+
+**鏉冮檺妯″潡鎻忚堪绗?*:
+鏉冮檺绉嶅瓙鍖栫殑澹版槑鍗曞厓銆備竴涓弿杩扮 = 涓€涓洰褰?+ 涓€涓彍鍗?+ 涓€缁勬寜閽?+ 鐩爣瑙掕壊缂栫爜鍒楄〃銆傜洰褰曞箓绛夆€斺€斿悓涓€鐩綍鍚嶈澶氭 `seedModule` 鍙垱寤轰竴娆°€傝皟鐢ㄦ柟涓嶆毚闇?`sys_menu` 鐨勪换浣曞瓧娈靛悕銆佹灇涓惧€兼垨榛樿鍊硷紱`denyBehavior`銆乣status`銆乣visible` 绛?schema 榛樿鍊肩敱绉嶅瓙鍣ㄥ疄鐜版柟纭紪鐮併€?+_Avoid_: 鏉冮檺閰嶇疆銆佺瀛愭弿杩版枃浠?+
+**鎸夐挳绉嶅瓙**:
+鏉冮檺妯″潡鎻忚堪绗︿腑涓€鏉℃寜閽潈闄愮偣鐨勬渶灏忓0鏄庯細鍚嶇О銆佹潈闄愮爜銆佹帴鍙?URL銆佹帓搴忋€備笉鍖呭惈 `denyBehavior`/`status`/`visible`鈥斺€旇繖浜涙槸 `sys_menu` 鐨?schema 缁嗚妭锛岀敱绉嶅瓙鍣ㄥ疄鐜版柟缁熶竴纭紪鐮併€傞渶瑕佷笉鍚屽€肩殑绠$悊鍛樺湪鍚姩鍚庨€氳繃璧勬簮绠$悊 API 淇敼銆?+_Avoid_: 鎸夐挳閰嶇疆銆佹潈闄愮偣瀹氫箟
diff --git a/crm-base/src/main/java/com/crm/base/domain/dto/ButtonSeed.java b/crm-base/src/main/java/com/crm/base/domain/dto/ButtonSeed.java
new file mode 100644
index 0000000..e1425bc
--- /dev/null
+++ b/crm-base/src/main/java/com/crm/base/domain/dto/ButtonSeed.java
@@ -0,0 +1,19 @@
+package com.crm.base.domain.dto;
+
+/**
+ * 鏉冮檺妯″潡鎻忚堪绗︿腑涓€鏉℃寜閽潈闄愮偣鐨勬渶灏忓0鏄庯紙ADR-0016锛?+ * <p>涓嶅寘鍚?{@code denyBehavior}/{@code status}/{@code visible}鈥斺€旇繖浜涙槸 sys_menu 鐨?schema 缁嗚妭锛?+ * 鐢辩瀛愬櫒瀹炵幇鏂圭粺涓€纭紪鐮併€傞渶瑕佷笉鍚屽€肩殑绠$悊鍛樺湪鍚姩鍚庨€氳繃璧勬簮绠$悊 API 淇敼銆?/p>
+ *
+ * @param name 鎸夐挳鍚嶇О
+ * @param perms 鏉冮檺鐮侊紙濡?dict:group:list"锛?+ * @param apiUrl 鎺ュ彛 URL锛圓piPermissionInterceptor 鎸夋鍖归厤锛?+ * @param sort 鎺掑簭
+ */
+public record ButtonSeed(
+ String name,
+ String perms,
+ String apiUrl,
+ int sort
+) {
+}
diff --git a/crm-base/src/main/java/com/crm/base/domain/dto/PermissionModuleDescriptor.java b/crm-base/src/main/java/com/crm/base/domain/dto/PermissionModuleDescriptor.java
new file mode 100644
index 0000000..6619a80
--- /dev/null
+++ b/crm-base/src/main/java/com/crm/base/domain/dto/PermissionModuleDescriptor.java
@@ -0,0 +1,29 @@
+package com.crm.base.domain.dto;
+
+import java.util.List;
+
+/**
+ * 鏉冮檺绉嶅瓙鍖栫殑澹版槑鍗曞厓锛圓DR-0016锛?+ * <p>涓€涓弿杩扮 = 涓€涓洰褰?+ 涓€涓彍鍗?+ 涓€缁勬寜閽?+ 鐩爣瑙掕壊缂栫爜鍒楄〃銆?+ * 鐩綍骞傜瓑鈥斺€斿悓涓€鐩綍鍚嶈澶氭 {@code seedModule} 鍙垱寤轰竴娆°€?+ * 璋冪敤鏂逛笉鏆撮湶 sys_menu 鐨勪换浣曞瓧娈靛悕銆佹灇涓惧€兼垨榛樿鍊硷紱
+ * {@code denyBehavior}銆亄@code status}銆亄@code visible} 绛?schema 榛樿鍊肩敱绉嶅瓙鍣ㄥ疄鐜版柟纭紪鐮併€?/p>
+ *
+ * @param catalogName 鐩綍鍚嶏紙濡?绯荤粺绠$悊"锛?+ * @param menuName 鑿滃崟鍚嶏紙濡?鏁版嵁瀛楀吀"锛?+ * @param menuPath 鍓嶇璺敱锛堝"/system/dict"锛?+ * @param menuComponent 鍓嶇缁勪欢璺緞锛堝"system/dict/index"锛?+ * @param menuSort 鑿滃崟鎺掑簭
+ * @param buttons 鎸夐挳鏉冮檺鐐癸紙鍙负绌哄垪琛級
+ * @param bindToRoleCodes 缁戝畾鐨勫唴缃鑹茬紪鐮侊紙濡?ROLE_ADMIN"锛?+ */
+public record PermissionModuleDescriptor(
+ String catalogName,
+ String menuName,
+ String menuPath,
+ String menuComponent,
+ int menuSort,
+ List<ButtonSeed> buttons,
+ List<String> bindToRoleCodes
+) {
+}
diff --git a/crm-base/src/main/java/com/crm/base/service/PermissionSeeder.java b/crm-base/src/main/java/com/crm/base/service/PermissionSeeder.java
new file mode 100644
index 0000000..67b2d05
--- /dev/null
+++ b/crm-base/src/main/java/com/crm/base/service/PermissionSeeder.java
@@ -0,0 +1,22 @@
+package com.crm.base.service;
+
+import com.crm.base.domain.dto.PermissionModuleDescriptor;
+
+/**
+ * 鏉冮檺绉嶅瓙鍖?seam锛圓DR-0016锛?+ * <p>璁╀换鎰忎笟鍔℃ā鍧楀0鏄庡紡鍦板皢鑷繁鐨勬潈闄愮偣锛坆utton 鑺傜偣锛夌瀛愬寲鍒版潈闄愯祫婧愭爲锛坰ys_menu 鐗╃悊琛級锛?+ * 鑰屼笉闇€瑕佺煡閬撹琛ㄧ殑鐗╃悊 schema銆傝皟鐢ㄦ柟閫氳繃 {@link PermissionModuleDescriptor} 澹版槑鐩綍鍚嶃€佽彍鍗曞悕銆?+ * 鎸夐挳鍒楄〃銆佺洰鏍囪鑹茬紪鐮侊紱瀹炵幇鏂癸紙鍦?crm-auth 涓級鎷ユ湁 sys_menu 鐨勫叏閮ㄥ瓧娈电煡璇嗭紝璐熻矗 find-or-create
+ * 骞傜瓑鍐欏叆涓庤鑹茬粦瀹氥€?/p>
+ * <p>鏇夸唬浜嗘鍓嶇殑褰卞瓙瀹炰綋鏂规鈥斺€攃rm-dict 鏇惧垱寤?SysMenuSeed 绛夐噸澶嶅疄浣撶被鐩存帴鏄犲皠 sys_menu 琛紝
+ * 瀵艰嚧 schema 娉勬紡銆?/p>
+ */
+public interface PermissionSeeder {
+
+ /**
+ * 绉嶅瓙鍖栦竴涓ā鍧楃殑鏉冮檺鐐瑰埌鏉冮檺璧勬簮鏍戙€?+ *
+ * @param descriptor 鏉冮檺妯″潡鎻忚堪绗︼紙鐩綍 + 鑿滃崟 + 鎸夐挳 + 鐩爣瑙掕壊缂栫爜锛?+ */
+ void seedModule(PermissionModuleDescriptor descriptor);
+}
diff --git a/crm-dict/src/main/java/com/crm/dict/config/DictPermissionInitializer.java b/crm-dict/src/main/java/com/crm/dict/config/DictPermissionInitializer.java
index b3cc8fe..20a5df5 100644
--- a/crm-dict/src/main/java/com/crm/dict/config/DictPermissionInitializer.java
+++ b/crm-dict/src/main/java/com/crm/dict/config/DictPermissionInitializer.java
@@ -1,25 +1,22 @@
package com.crm.dict.config;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.crm.base.domain.dto.ButtonSeed;
+import com.crm.base.domain.dto.PermissionModuleDescriptor;
+import com.crm.base.service.PermissionSeeder;
import com.crm.dict.constant.DictConstants;
-import com.crm.dict.domain.entity.SysMenuSeed;
-import com.crm.dict.domain.entity.SysRoleMenuSeed;
-import com.crm.dict.domain.entity.SysRoleSeed;
-import com.crm.dict.mapper.SysMenuSeedMapper;
-import com.crm.dict.mapper.SysRoleMenuSeedMapper;
-import com.crm.dict.mapper.SysRoleSeedMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
-import java.util.ArrayList;
import java.util.List;
/**
- * 鏁版嵁瀛楀吀鏉冮檺绉嶅瓙鍖栵紙骞傜瓑锛孴07锛?- * <p>灏?10 涓?{@code dict:...} 鏉冮檺鐮佺瀛愬寲鍒版潈闄愯祫婧愭爲锛坰ys_menu 鐗╃悊琛級骞剁粦瀹氬唴缃鐞嗗憳瑙掕壊銆?- * 涓?crm-auth DataInitializer 鏃犺€﹀悎锛氱郴缁熺鐞嗙洰褰曠己澶卞垯鑷缓锛汻OLE_ADMIN 缂哄け鍒欒烦杩囩粦瀹氬苟 warn
+ * 鏁版嵁瀛楀吀鏉冮檺绉嶅瓙鍖栵紙骞傜瓑锛孴07锛孉DR-0016锛?+ * <p>閫氳繃 {@link PermissionSeeder} seam 灏?10 涓?{@code dict:...} 鏉冮檺鐮佺瀛愬寲鍒版潈闄愯祫婧愭爲
+ * 锛坰ys_menu 鐗╃悊琛級骞剁粦瀹氬唴缃鐞嗗憳瑙掕壊銆備笌 crm-auth DataInitializer 鏃犺€﹀悎锛?+ * 绯荤粺绠$悊鐩綍缂哄け鍒?seam 鑷缓锛汻OLE_ADMIN 缂哄け鍒?seam warn 璺宠繃缁戝畾
* 锛坈rm-auth 棣栨鍚姩鍒涘缓鍚庯紝瀛楀吀鏉冮檺鍦ㄤ笅娆″惎鍔ㄨˉ缁戯級銆?/p>
* <p>鏉冮檺鐐瑰啓鍏ヤ簡 api_url锛歝rm-auth 鐨?ApiPermissionInterceptor 鎸?api_url 鎳掑姞杞借鍒欙紝
* 绉嶅瓙鍖栧畬鎴愬悗涓荤鐐癸紙page/saveOrUpdate/delete/status/set-default 绛夛級鏈巿鏉冭闂嵆琚嫤鎴紱
@@ -27,15 +24,11 @@ import java.util.List;
*/
@Slf4j
@Component
+@Order(10)
@RequiredArgsConstructor
public class DictPermissionInitializer implements CommandLineRunner {
- private final SysMenuSeedMapper sysMenuMapper;
- private final SysRoleSeedMapper sysRoleMapper;
- private final SysRoleMenuSeedMapper sysRoleMenuMapper;
-
- /** 鎸夐挳绫诲瀷锛堜笌 crm-auth 鑿滃崟绫诲瀷鏋氫妇涓€鑷达級 */
- private static final int MENU_TYPE_BUTTON = 3;
+ private final PermissionSeeder permissionSeeder;
/** 10 涓潈闄愮偣绉嶅瓙锛?鍚嶇О, 鏉冮檺鐮? apiUrl, 鎺掑簭)銆傛潈闄愮爜涓庡墠绔枃妗?搂8 涓€鑷?*/
private static final List<ButtonSeed> BUTTON_SEEDS = List.of(
@@ -54,104 +47,16 @@ public class DictPermissionInitializer implements CommandLineRunner {
public void run(String... args) {
log.info("鎵ц鏁版嵁瀛楀吀鏉冮檺绉嶅瓙鍖栨鏌?..");
- // ---- 涓€绾х洰褰曪細绯荤粺绠$悊锛堢己澶辫嚜寤猴紝涓?crm-auth DataInitializer 椤哄簭鏃犲叧锛?----
- SysMenuSeed sysManage = findMenuByName("绯荤粺绠$悊", 0L);
- if (sysManage == null) {
- sysManage = insertMenu("绯荤粺绠$悊", 0L, 1, null, null, 0);
- }
-
- // ---- 浜岀骇鑿滃崟锛氭暟鎹瓧鍏革紙鎸傚湪绯荤粺绠$悊涓嬶級 ----
- SysMenuSeed dictMenu = findMenuByName("鏁版嵁瀛楀吀", sysManage.getId());
- if (dictMenu == null) {
- dictMenu = insertMenu("鏁版嵁瀛楀吀", sysManage.getId(), 2,
- "/system/dict", "system/dict/index", 4);
- }
-
- // ---- 10 涓寜閽潈闄愮偣锛堟寜 perms 骞傜瓑鏌ラ噸锛屽瓨鍦ㄥ嵆璺宠繃锛?----
- List<Long> buttonIds = new ArrayList<>(BUTTON_SEEDS.size());
- for (ButtonSeed seed : BUTTON_SEEDS) {
- buttonIds.add(insertButtonIfAbsent(seed.name(), dictMenu.getId(),
- seed.perms(), seed.apiUrl(), seed.sort()));
- }
-
- // ---- 缁戝畾鍐呯疆绠$悊鍛樿鑹诧紙缂哄け璺宠繃 + warn锛?----
- SysRoleSeed admin = sysRoleMapper.selectOne(
- new LambdaQueryWrapper<SysRoleSeed>().eq(SysRoleSeed::getRoleCode, "ROLE_ADMIN"));
- if (admin == null) {
- log.warn("鍐呯疆瑙掕壊 ROLE_ADMIN 涓嶅瓨鍦紝璺宠繃鏁版嵁瀛楀吀鏉冮檺缁戝畾锛坈rm-auth 鍒涘缓璇ヨ鑹插悗涓嬫鍚姩琛ョ粦锛?);
- return;
- }
- bindIfAbsent(admin.getId(), sysManage.getId());
- bindIfAbsent(admin.getId(), dictMenu.getId());
- for (Long buttonId : buttonIds) {
- bindIfAbsent(admin.getId(), buttonId);
- }
+ permissionSeeder.seedModule(new PermissionModuleDescriptor(
+ "绯荤粺绠$悊",
+ "鏁版嵁瀛楀吀",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ BUTTON_SEEDS,
+ List.of("ROLE_ADMIN")
+ ));
log.info("鏁版嵁瀛楀吀鏉冮檺绉嶅瓙鍖栨鏌ュ畬鎴愶細{} 涓潈闄愮偣", BUTTON_SEEDS.size());
}
-
- // ==================== 骞傜瓑杈呭姪鏂规硶 ====================
-
- private SysMenuSeed findMenuByName(String name, Long parentId) {
- return sysMenuMapper.selectOne(new LambdaQueryWrapper<SysMenuSeed>()
- .eq(SysMenuSeed::getMenuName, name)
- .eq(SysMenuSeed::getParentId, parentId));
- }
-
- private SysMenuSeed insertMenu(String name, Long parentId, int type,
- String path, String component, int sort) {
- SysMenuSeed m = new SysMenuSeed();
- m.setParentId(parentId);
- m.setMenuName(name);
- m.setMenuType(type);
- m.setPath(path);
- m.setComponent(component);
- m.setSort(sort);
- m.setVisible(true);
- sysMenuMapper.insert(m);
- log.info("鍒涘缓鑿滃崟鑺傜偣锛歿} (type={})", name, type);
- return m;
- }
-
- /**
- * 鎸夋潈闄愮爜鏌ラ噸鎻掑叆鎸夐挳鏉冮檺鐐癸紝杩斿洖鍏?id锛堝凡瀛樺湪鍒欒繑鍥炵幇鏈?id锛夈€?- */
- private Long insertButtonIfAbsent(String name, Long parentId, String perms,
- String apiUrl, int sort) {
- SysMenuSeed exist = sysMenuMapper.selectOne(
- new LambdaQueryWrapper<SysMenuSeed>().eq(SysMenuSeed::getPerms, perms));
- if (exist != null) {
- return exist.getId();
- }
- SysMenuSeed btn = new SysMenuSeed();
- btn.setParentId(parentId);
- btn.setMenuName(name);
- btn.setMenuType(MENU_TYPE_BUTTON);
- btn.setSort(sort);
- btn.setVisible(true);
- btn.setPerms(perms);
- btn.setDenyBehavior("hide");
- btn.setApiUrl(apiUrl);
- btn.setStatus("enabled");
- sysMenuMapper.insert(btn);
- log.info("鍒涘缓鏉冮檺鐐癸細{} -> {}", name, perms);
- return btn.getId();
- }
-
- private void bindIfAbsent(Long roleId, Long menuId) {
- Long count = sysRoleMenuMapper.selectCount(new LambdaQueryWrapper<SysRoleMenuSeed>()
- .eq(SysRoleMenuSeed::getRoleId, roleId)
- .eq(SysRoleMenuSeed::getMenuId, menuId));
- if (count > 0) {
- return;
- }
- SysRoleMenuSeed rm = new SysRoleMenuSeed();
- rm.setRoleId(roleId);
- rm.setMenuId(menuId);
- sysRoleMenuMapper.insert(rm);
- }
-
- /** 鎸夐挳绉嶅瓙锛氭秷闄?insertButtonIfAbsent 鐨勪綅缃弬鏁版涔?*/
- private record ButtonSeed(String name, String perms, String apiUrl, int sort) {
- }
}
diff --git a/crm-dict/src/main/java/com/crm/dict/domain/entity/SysMenuSeed.java b/crm-dict/src/main/java/com/crm/dict/domain/entity/SysMenuSeed.java
deleted file mode 100644
index 9982ea6..0000000
--- a/crm-dict/src/main/java/com/crm/dict/domain/entity/SysMenuSeed.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.crm.dict.domain.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import com.crm.base.domain.entity.BaseEntity;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * 鏉冮檺璧勬簮鏍戞渶灏忓寲鏄犲皠锛坰ys_menu 鐗╃悊琛紝T07锛?- * <p>浠呯敤浜?crm-dict 骞傜瓑绉嶅瓙鍖栨潈闄愮偣锛屽埢鎰忎笉鏍?@Entity锛氬缓琛ㄥ綊 crm-auth 鐨?SysMenu锛?- * 鏈被鍙仛 MyBatis-Plus 璇诲啓锛岄伩鍏嶄笌 crm-auth 鍙屽疄浣撴槧灏勫悓涓€寮犺〃锛圓DR-0015 闆朵緷璧栫害鏉燂級銆?/p>
- */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("sys_menu")
-public class SysMenuSeed extends BaseEntity {
-
- /** 涓婄骇鑿滃崟ID锛屾牴鑺傜偣涓?0 */
- private Long parentId;
-
- /** 鑿滃崟鍚嶇О */
- private String menuName;
-
- /** 绫诲瀷锛?=鐩綍 2=鑿滃崟 3=鎸夐挳 */
- private Integer menuType;
-
- /** 鍓嶇璺敱璺緞锛堢洰褰?鑿滃崟鐢級 */
- private String path;
-
- /** 鍓嶇缁勪欢璺緞锛堣彍鍗曠敤锛?*/
- private String component;
-
- /** 鎺掑簭 */
- private Integer sort;
-
- /** 鏄惁鍙 */
- private Boolean visible;
-
- /** 鏉冮檺鐮侊紙鎸夐挳绾ф潈闄愭爣璇嗭紝濡?dict:item:list锛?*/
- private String perms;
-
- /** 鏃犳潈闄愭椂鐨勫墠绔涓猴細hide 闅愯棌锛宒isable 绂佺敤 */
- private String denyBehavior;
-
- /** 鏉冮檺鐐圭姸鎬侊細enabled 鍚敤锛宒isabled 鍋滅敤锛堝叏灞€鏂矾锛?*/
- private String status;
-
- /** 鎺ュ彛 URL锛堟寜閽搴旂殑鍚庣 API 璺緞锛孉piPermissionInterceptor 鎸夋鍖归厤瑙勫垯锛?*/
- private String apiUrl;
-}
diff --git a/crm-dict/src/main/java/com/crm/dict/domain/entity/SysRoleMenuSeed.java b/crm-dict/src/main/java/com/crm/dict/domain/entity/SysRoleMenuSeed.java
deleted file mode 100644
index dc1d863..0000000
--- a/crm-dict/src/main/java/com/crm/dict/domain/entity/SysRoleMenuSeed.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.crm.dict.domain.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-/**
- * 瑙掕壊-璧勬簮缁戝畾鏈€灏忓寲鏄犲皠锛坰ys_role_menu 鐗╃悊琛紝T07锛?- * <p>浠呯敤浜?crm-dict 骞傜瓑绉嶅瓙鍖栨椂灏嗘潈闄愮偣缁戝畾鍒板唴缃鐞嗗憳瑙掕壊锛?- * 鍒绘剰涓嶆爣 @Entity锛屽缓琛ㄥ綊 crm-auth 鐨?SysRoleMenu锛圓DR-0015 闆朵緷璧栫害鏉燂級銆?/p>
- */
-@Data
-@TableName("sys_role_menu")
-public class SysRoleMenuSeed {
-
- @TableId(type = IdType.ASSIGN_ID)
- private Long id;
-
- /** 瑙掕壊ID */
- private Long roleId;
-
- /** 鑿滃崟/鏉冮檺鐐笽D */
- private Long menuId;
-}
diff --git a/crm-dict/src/main/java/com/crm/dict/domain/entity/SysRoleSeed.java b/crm-dict/src/main/java/com/crm/dict/domain/entity/SysRoleSeed.java
deleted file mode 100644
index 1a25d56..0000000
--- a/crm-dict/src/main/java/com/crm/dict/domain/entity/SysRoleSeed.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.crm.dict.domain.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import com.crm.base.domain.entity.BaseEntity;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * 绯荤粺瑙掕壊鏈€灏忓寲鏄犲皠锛坰ys_role 鐗╃悊琛紝T07锛?- * <p>浠呯敤浜?crm-dict 骞傜瓑绉嶅瓙鍖栨椂瀹氫綅鍐呯疆绠$悊鍛樿鑹?ROLE_ADMIN锛屽埢鎰忎笉鏍?@Entity锛?- * 寤鸿〃褰?crm-auth 鐨?SysRole锛圓DR-0015 闆朵緷璧栫害鏉燂級銆?/p>
- */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("sys_role")
-public class SysRoleSeed extends BaseEntity {
-
- /** 瑙掕壊鍚嶇О */
- private String roleName;
-
- /** 瑙掕壊缂栫爜锛堝 ROLE_ADMIN锛?*/
- private String roleCode;
-
- /** 鏁版嵁鑼冨洿锛?=鏈汉 2=鏈儴闂?3=鏈儴闂ㄥ強瀛愰儴闂?4=鍏ㄩ儴 */
- private Integer dataScope;
-
- /** 鎺掑簭 */
- private Integer sort;
-
- /** 鍐呯疆瑙掕壊鏍囪锛歵rue=绯荤粺鍐呯疆涓嶅彲鍒犻櫎涓嶅彲鏀圭紪鐮?*/
- private Boolean builtin;
-}
diff --git a/crm-dict/src/main/java/com/crm/dict/mapper/SysMenuSeedMapper.java b/crm-dict/src/main/java/com/crm/dict/mapper/SysMenuSeedMapper.java
deleted file mode 100644
index 2251978..0000000
--- a/crm-dict/src/main/java/com/crm/dict/mapper/SysMenuSeedMapper.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.crm.dict.mapper;
-
-import com.crm.base.mapper.CrmBaseMapper;
-import com.crm.dict.domain.entity.SysMenuSeed;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * sys_menu 鏈€灏忓寲鏄犲皠 Mapper锛圱07 鏉冮檺绉嶅瓙鍖栦笓鐢級
- */
-@Mapper
-public interface SysMenuSeedMapper extends CrmBaseMapper<SysMenuSeed> {
-}
diff --git a/crm-dict/src/main/java/com/crm/dict/mapper/SysRoleMenuSeedMapper.java b/crm-dict/src/main/java/com/crm/dict/mapper/SysRoleMenuSeedMapper.java
deleted file mode 100644
index c7c98d5..0000000
--- a/crm-dict/src/main/java/com/crm/dict/mapper/SysRoleMenuSeedMapper.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.crm.dict.mapper;
-
-import com.crm.base.mapper.CrmBaseMapper;
-import com.crm.dict.domain.entity.SysRoleMenuSeed;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * sys_role_menu 鏈€灏忓寲鏄犲皠 Mapper锛圱07 鏉冮檺绉嶅瓙鍖栦笓鐢級
- */
-@Mapper
-public interface SysRoleMenuSeedMapper extends CrmBaseMapper<SysRoleMenuSeed> {
-}
diff --git a/crm-dict/src/main/java/com/crm/dict/mapper/SysRoleSeedMapper.java b/crm-dict/src/main/java/com/crm/dict/mapper/SysRoleSeedMapper.java
deleted file mode 100644
index 985c73c..0000000
--- a/crm-dict/src/main/java/com/crm/dict/mapper/SysRoleSeedMapper.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.crm.dict.mapper;
-
-import com.crm.base.mapper.CrmBaseMapper;
-import com.crm.dict.domain.entity.SysRoleSeed;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * sys_role 鏈€灏忓寲鏄犲皠 Mapper锛圱07 鏉冮檺绉嶅瓙鍖栦笓鐢級
- */
-@Mapper
-public interface SysRoleSeedMapper extends CrmBaseMapper<SysRoleSeed> {
-}
diff --git a/crm-dict/src/test/java/com/crm/dict/config/DictPermissionInitializerTest.java b/crm-dict/src/test/java/com/crm/dict/config/DictPermissionInitializerTest.java
index 2c7b490..40d52d9 100644
--- a/crm-dict/src/test/java/com/crm/dict/config/DictPermissionInitializerTest.java
+++ b/crm-dict/src/test/java/com/crm/dict/config/DictPermissionInitializerTest.java
@@ -1,63 +1,34 @@
package com.crm.dict.config;
-import com.baomidou.mybatisplus.annotation.DbType;
-import com.baomidou.mybatisplus.core.MybatisConfiguration;
-import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
-import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
-import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
-import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
-import com.crm.base.config.MetaObjectFillHandler;
+import com.crm.base.domain.dto.ButtonSeed;
+import com.crm.base.domain.dto.PermissionModuleDescriptor;
+import com.crm.base.service.PermissionSeeder;
import com.crm.dict.constant.DictConstants;
import com.crm.dict.controller.DictGroupController;
import com.crm.dict.controller.DictItemController;
-import com.crm.dict.domain.entity.SysMenuSeed;
-import com.crm.dict.domain.entity.SysRoleMenuSeed;
-import com.crm.dict.domain.entity.SysRoleSeed;
-import com.crm.dict.mapper.SysMenuSeedMapper;
-import com.crm.dict.mapper.SysRoleMenuSeedMapper;
-import com.crm.dict.mapper.SysRoleSeedMapper;
-import org.apache.ibatis.mapping.Environment;
-import org.apache.ibatis.session.LocalCacheScope;
-import org.apache.ibatis.session.SqlSession;
-import org.apache.ibatis.session.SqlSessionFactory;
-import org.apache.ibatis.session.SqlSessionFactoryBuilder;
-import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
-import org.h2.jdbcx.JdbcDataSource;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.reflect.Method;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Arrays;
import java.util.List;
import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
/**
- * T07 鏉冮檺绉嶅瓙鍖栭泦鎴愭祴璇曪細绉嶅瓙鍖栧悗 10 涓潈闄愮偣鎸傛爲骞剁粦瀹氱鐞嗗憳 / 骞傜瓑 / ROLE_ADMIN 缂哄け璺宠繃 / Controller 娉ㄨВ濂戠害
- * <p>H2 鎵嬪伐瑁呴厤锛堜豢 AbstractDictH2Test锛夛紱"鏃犳潈闄愯闂鎷?鐨勫姩鎬佽涓虹敱 crm-auth
- * ApiPermissionInterceptor锛堟寜 api_url 鎳掑姞杞借鍒欙級涓?Spring Security 淇濊瘉锛?- * 鏈祴璇曢獙璇佺瀛愬寲浜у嚭婊¤冻璇ユ嫤鎴櫒濂戠害锛坅pi_url + status=enabled + perms锛夈€?/p>
+ * T07 鏉冮檺绉嶅瓙鍖栨祴璇曪紙ADR-0016锛夛細楠岃瘉 DictPermissionInitializer 閫氳繃 PermissionSeeder seam
+ * 澹版槑姝g‘鐨勬潈闄愭弿杩扮锛屼笖 Controller 绔偣甯?@PreAuthorize 娉ㄨВ銆?+ * <p>涓嶅啀浣跨敤 H2 寤?sys_menu/sys_role/sys_role_menu 琛ㄢ€斺€攕eam 瀹炵幇鍦?crm-auth 鐨?+ * PermissionSeederImplTest 涓泦鎴愭祴璇曘€?/p>
*/
@DisplayName("T07 鏉冮檺绉嶅瓙鍖?)
class DictPermissionInitializerTest {
- private static final AtomicBoolean BOOTSTRAPPED = new AtomicBoolean(false);
-
- private static SqlSessionFactory sqlSessionFactory;
-
- private SqlSession session;
- private SysMenuSeedMapper sysMenuMapper;
- private SysRoleSeedMapper sysRoleMapper;
- private SysRoleMenuSeedMapper sysRoleMenuMapper;
+ private PermissionSeeder permissionSeeder;
private DictPermissionInitializer initializer;
/** 10 涓潈闄愮爜锛堜笌 DictConstants + 鍓嶇鏂囨。 搂8 涓€鑷达級 */
@@ -68,160 +39,39 @@ class DictPermissionInitializerTest {
DictConstants.PERM_ITEM_DELETE, DictConstants.PERM_ITEM_FORCE_DELETE,
DictConstants.PERM_ITEM_STATUS, DictConstants.PERM_ITEM_DEFAULT);
- @BeforeAll
- static void bootstrap() throws Exception {
- if (!BOOTSTRAPPED.compareAndSet(false, true)) {
- return;
- }
- JdbcDataSource ds = new JdbcDataSource();
- ds.setURL("jdbc:h2:mem:dict-perm;MODE=MySQL;DATABASE_TO_LOWER=TRUE;" +
- "CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1;NON_KEYWORDS=VALUE");
-
- MybatisConfiguration configuration = new MybatisConfiguration();
- configuration.setEnvironment(new Environment("dict-perm-test", new JdbcTransactionFactory(), ds));
- configuration.setMapUnderscoreToCamelCase(true);
- configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
-
- MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
- mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
- configuration.addInterceptor(mpInterceptor);
-
- GlobalConfigUtils.getGlobalConfig(configuration).setMetaObjectHandler(new MetaObjectFillHandler());
- GlobalConfigUtils.getGlobalConfig(configuration).setIdentifierGenerator(DefaultIdentifierGenerator.getInstance());
-
- configuration.addMapper(SysMenuSeedMapper.class);
- configuration.addMapper(SysRoleSeedMapper.class);
- configuration.addMapper(SysRoleMenuSeedMapper.class);
- sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
-
- try (SqlSession s = sqlSessionFactory.openSession(true);
- Statement st = s.getConnection().createStatement()) {
- for (String sql : SCHEMA) {
- st.execute(sql);
- }
- }
- }
-
@BeforeEach
- void setUp() throws SQLException {
- session = sqlSessionFactory.openSession(true);
- try (Statement st = session.getConnection().createStatement()) {
- st.execute("TRUNCATE TABLE sys_role_menu");
- st.execute("TRUNCATE TABLE sys_menu");
- st.execute("TRUNCATE TABLE sys_role");
- }
- sysMenuMapper = session.getMapper(SysMenuSeedMapper.class);
- sysRoleMapper = session.getMapper(SysRoleSeedMapper.class);
- sysRoleMenuMapper = session.getMapper(SysRoleMenuSeedMapper.class);
- initializer = new DictPermissionInitializer(sysMenuMapper, sysRoleMapper, sysRoleMenuMapper);
- }
-
- @AfterEach
- void tearDown() {
- if (session != null) {
- session.close();
- }
+ void setUp() {
+ permissionSeeder = mock(PermissionSeeder.class);
+ initializer = new DictPermissionInitializer(permissionSeeder);
}
@Test
- @DisplayName("绉嶅瓙鍖栵細绯荤粺绠$悊/鏁版嵁瀛楀吀鎸傛爲锛?0 涓潈闄愮偣甯?api_url 涓旂粦瀹氱鐞嗗憳")
- void seedAll_createsTreeAndBindsAdmin() {
- SysRoleSeed admin = insertAdminRole();
-
+ @DisplayName("seedModule 澹版槑姝g‘鐨勭洰褰曘€佽彍鍗曘€?0 涓寜閽潈闄愮偣鍜岃鑹茬粦瀹?)
+ void seedModule_declaresCorrectDescriptor() {
initializer.run();
- // 鐩綍涓庤彍鍗?- SysMenuSeed sysManage = findMenu("绯荤粺绠$悊", 0L);
- assertThat(sysManage).isNotNull();
- SysMenuSeed dictMenu = findMenu("鏁版嵁瀛楀吀", sysManage.getId());
- assertThat(dictMenu).isNotNull();
- assertThat(dictMenu.getMenuType()).isEqualTo(2);
- assertThat(dictMenu.getPath()).isEqualTo("/system/dict");
+ ArgumentCaptor<PermissionModuleDescriptor> captor = ArgumentCaptor.forClass(PermissionModuleDescriptor.class);
+ verify(permissionSeeder, times(1)).seedModule(captor.capture());
+
+ PermissionModuleDescriptor descriptor = captor.getValue();
+ assertThat(descriptor.catalogName()).isEqualTo("绯荤粺绠$悊");
+ assertThat(descriptor.menuName()).isEqualTo("鏁版嵁瀛楀吀");
+ assertThat(descriptor.menuPath()).isEqualTo("/system/dict");
+ assertThat(descriptor.menuComponent()).isEqualTo("system/dict/index");
+ assertThat(descriptor.menuSort()).isEqualTo(4);
+ assertThat(descriptor.bindToRoleCodes()).containsExactly("ROLE_ADMIN");
- // 10 涓潈闄愮偣锛歱erms 榻愬叏銆佹寜閽被鍨嬨€乪nabled銆乤pi_url 闈炵┖锛圓piPermissionInterceptor 鍔犺浇濂戠害锛?- List<SysMenuSeed> buttons = sysMenuMapper.selectList(null).stream()
- .filter(m -> m.getMenuType() == 3)
- .toList();
+ // 10 涓寜閽潈闄愮偣
+ List<ButtonSeed> buttons = descriptor.buttons();
assertThat(buttons).hasSize(10);
- assertThat(buttons).extracting(SysMenuSeed::getPerms).containsExactlyInAnyOrderElementsOf(PERM_CODES);
+ assertThat(buttons).extracting(ButtonSeed::perms).containsExactlyInAnyOrderElementsOf(PERM_CODES);
+
+ // 姣忎釜鎸夐挳閮芥湁 apiUrl 鍜?sort
assertThat(buttons).allSatisfy(b -> {
- assertThat(b.getStatus()).isEqualTo("enabled");
- assertThat(b.getApiUrl()).isNotBlank();
- assertThat(b.getParentId()).isEqualTo(dictMenu.getId());
+ assertThat(b.apiUrl()).isNotBlank();
+ assertThat(b.sort()).isGreaterThanOrEqualTo(1);
+ assertThat(b.name()).isNotBlank();
});
-
- // 缁戝畾绠$悊鍛橈細鐩綍 + 鑿滃崟 + 10 鎸夐挳
- List<SysRoleMenuSeed> binds = sysRoleMenuMapper.selectList(null);
- assertThat(binds).hasSize(12);
- assertThat(binds).allMatch(b -> b.getRoleId().equals(admin.getId()));
- }
-
- @Test
- @DisplayName("骞傜瓑锛氶噸澶嶆墽琛屼笉浜х敓閲嶅鑿滃崟/鏉冮檺鐐?缁戝畾")
- void seedAll_idempotent() {
- insertAdminRole();
-
- initializer.run();
- initializer.run();
-
- assertThat(sysMenuMapper.selectCount(null)).isEqualTo(12);
- assertThat(sysRoleMenuMapper.selectCount(null)).isEqualTo(12);
- }
-
- @Test
- @DisplayName("ROLE_ADMIN 缂哄け锛氭潈闄愮偣鐓у父绉嶅瓙鍖栵紝璺宠繃缁戝畾涓斾笉鎶涘紓甯?)
- void seedAll_skipsBindingWhenAdminMissing() {
- initializer.run();
-
- assertThat(sysMenuMapper.selectCount(null)).isEqualTo(12);
- assertThat(sysRoleMenuMapper.selectCount(null)).isZero();
- }
-
- @Test
- @DisplayName("澶嶇敤鏃㈡湁鏍戯細绯荤粺绠$悊/鏁版嵁瀛楀吀宸插瓨鍦ㄦ椂涓嶈嚜寤猴紝鏉冮檺鐐规寜 perms 琛ラ綈涓嶉噸澶?)
- void seedAll_reusesExistingMenuTree() {
- insertAdminRole();
- // 妯℃嫙 crm-auth DataInitializer 宸插垱寤虹洰褰曪紝涓旈儴鍒嗘潈闄愮偣宸插瓨鍦紙甯?apiUrl锛?- SysMenuSeed sysManage = new SysMenuSeed();
- sysManage.setParentId(0L);
- sysManage.setMenuName("绯荤粺绠$悊");
- sysManage.setMenuType(1);
- sysManage.setSort(0);
- sysManage.setVisible(true);
- sysMenuMapper.insert(sysManage);
-
- SysMenuSeed dictMenu = new SysMenuSeed();
- dictMenu.setParentId(sysManage.getId());
- dictMenu.setMenuName("鏁版嵁瀛楀吀");
- dictMenu.setMenuType(2);
- dictMenu.setPath("/system/dict");
- dictMenu.setSort(4);
- dictMenu.setVisible(true);
- sysMenuMapper.insert(dictMenu);
-
- SysMenuSeed existingButton = new SysMenuSeed();
- existingButton.setParentId(dictMenu.getId());
- existingButton.setMenuName("鍒嗙粍鏌ヨ");
- existingButton.setMenuType(3);
- existingButton.setPerms(DictConstants.PERM_GROUP_LIST);
- existingButton.setApiUrl("/api/dict/group/page");
- existingButton.setStatus("enabled");
- existingButton.setVisible(true);
- sysMenuMapper.insert(existingButton);
-
- initializer.run();
-
- // 鏍戜笉閲嶅锛氶潪鎸夐挳鑺傜偣浠?2 涓紝鎸夐挳鎭板ソ 10 涓?- assertThat(sysMenuMapper.selectCount(null)).isEqualTo(12);
- List<SysMenuSeed> buttons = sysMenuMapper.selectList(null).stream()
- .filter(m -> m.getMenuType() == 3)
- .toList();
- assertThat(buttons).extracting(SysMenuSeed::getPerms).containsExactlyInAnyOrderElementsOf(PERM_CODES);
- // 宸插瓨鍦ㄦ寜閽繚鎸佸師 id 涓嶉噸寤?- assertThat(buttons.stream().filter(b -> b.getPerms().equals(DictConstants.PERM_GROUP_LIST))
- .map(SysMenuSeed::getId)).containsExactly(existingButton.getId());
- // 缁戝畾琛ラ綈
- assertThat(sysRoleMenuMapper.selectCount(null)).isEqualTo(12);
}
@Test
@@ -242,49 +92,10 @@ class DictPermissionInitializerTest {
}
}
- /*-------- 杈呭姪 --------*/
-
- private SysRoleSeed insertAdminRole() {
- SysRoleSeed admin = new SysRoleSeed();
- admin.setRoleName("绠$悊鍛?);
- admin.setRoleCode("ROLE_ADMIN");
- admin.setDataScope(4);
- admin.setSort(0);
- admin.setBuiltin(true);
- sysRoleMapper.insert(admin);
- return admin;
- }
-
- private SysMenuSeed findMenu(String name, Long parentId) {
- return sysMenuMapper.selectList(null).stream()
- .filter(m -> name.equals(m.getMenuName()) && parentId.equals(m.getParentId()))
- .findFirst().orElse(null);
- }
-
/** 浠?hasAuthority('xxx') 琛ㄨ揪寮忎腑鎻愬彇鏉冮檺鐮?*/
private static String extractPerm(String expression) {
int start = expression.indexOf('\'');
int end = expression.lastIndexOf('\'');
return expression.substring(start + 1, end);
}
-
- /*-------- 寤鸿〃 DDL锛堜笌 crm-auth 瀹炰綋缁撴瀯瀵归綈锛歴ys_menu 鍚?perms/api_url 绛夋墿灞曞垪锛?-------*/
-
- private static final String[] SCHEMA = {
- "CREATE TABLE IF NOT EXISTS sys_menu (" +
- "id BIGINT NOT NULL PRIMARY KEY, creator_id VARCHAR(50), create_time DATETIME, " +
- "updater_id VARCHAR(50), update_time DATETIME, deleted TINYINT NOT NULL DEFAULT 0, " +
- "parent_id BIGINT NOT NULL DEFAULT 0, menu_name VARCHAR(50) NOT NULL, " +
- "menu_type TINYINT NOT NULL, path VARCHAR(200), component VARCHAR(200), icon VARCHAR(50), " +
- "sort INT DEFAULT 0, visible TINYINT DEFAULT 1, description VARCHAR(100), " +
- "perms VARCHAR(100), deny_behavior VARCHAR(16), status VARCHAR(16) DEFAULT 'enabled', " +
- "api_url VARCHAR(200))",
- "CREATE TABLE IF NOT EXISTS sys_role (" +
- "id BIGINT NOT NULL PRIMARY KEY, creator_id VARCHAR(50), create_time DATETIME, " +
- "updater_id VARCHAR(50), update_time DATETIME, deleted TINYINT NOT NULL DEFAULT 0, " +
- "role_name VARCHAR(50), role_code VARCHAR(50), data_scope TINYINT, sort INT, " +
- "remark VARCHAR(200), builtin BOOLEAN DEFAULT FALSE)",
- "CREATE TABLE IF NOT EXISTS sys_role_menu (" +
- "id BIGINT NOT NULL PRIMARY KEY, role_id BIGINT NOT NULL, menu_id BIGINT NOT NULL)",
- };
}
diff --git a/docs/adr/0016-permission-seeder-seam.md b/docs/adr/0016-permission-seeder-seam.md
new file mode 100644
index 0000000..d050e14
--- /dev/null
+++ b/docs/adr/0016-permission-seeder-seam.md
@@ -0,0 +1,101 @@
+# ADR-0016: PermissionSeeder seam 鈥?娑堥櫎璺ㄦā鍧楁潈闄愮瀛愬寲鐨勫奖瀛愬疄浣?+
+## Status
+
+Accepted
+
+## Context
+
+crm-dict 鐨?`DictPermissionInitializer` 闇€瑕佸皢 10 涓?`dict:*` 鏉冮檺鐐圭瀛愬寲鍒版潈闄愯祫婧愭爲锛坄sys_menu` 鐗╃悊琛級骞剁粦瀹氬埌鍐呯疆绠$悊鍛樿鑹层€侫DR-0015 绂佹 crm-dict 缂栬瘧鏈熶緷璧?crm-auth锛?crm-dict 鍙緷璧?crm-base锛屼笉 import crm-auth 鐨勪换浣曠被"锛夈€?+
+涓虹粫杩囪繖涓€绾︽潫锛宑rm-dict 鍒涘缓浜嗕笁涓?*褰卞瓙瀹炰綋**鈥斺€擿SysMenuSeed`銆乣SysRoleSeed`銆乣SysRoleMenuSeed`鈥斺€旂敤 `@TableName("sys_menu")` 绛夌洿鎺ユ槧灏?crm-auth 鐨勭墿鐞嗚〃锛岄€氳繃鑷繁鐨?Mapper 鍐欏叆銆傝繖瀵艰嚧锛?+
+1. **Schema 娉勬紡**锛歝rm-dict 鐭ラ亾 `sys_menu` 鐨勫叏閮ㄥ瓧娈靛悕锛坄menuType`銆乣parentId`銆乣perms`銆乣denyBehavior`銆乣status`銆乣apiUrl`銆乣visible`锛夈€佹灇涓惧€硷紙`menuType=3` 琛ㄧず button锛夈€侀粯璁ゅ€硷紙`denyBehavior="hide"`銆乣status="enabled"`锛夈€俢rm-auth 鏀?schema 鏃堕潤榛樼牬鍧?crm-dict 鐨勭瀛愬寲銆?+2. **閫昏緫閲嶅**锛歚DictPermissionInitializer` 涓?crm-auth 鐨?`DataInitializer` 鏈夊嚑涔庣浉鍚岀殑杈呭姪鏂规硶锛坄findMenuByName`銆乣insertButtonIfAbsent`銆乣bindIfAbsent`锛夛紝鍚?~100 琛屻€?+3. **ADR-0015 鎰忓浘琚繚鍙?*锛欰DR 璇?鏉冮檺鐮佸瓧绗︿覆鏄笌 auth 浣撶郴鐨勫敮涓€濂戠害"锛屼絾褰卞瓙瀹炰綋鎶婃暣涓?`sys_menu` 鐗╃悊缁撴瀯娉勬紡浜嗚繃鏉モ€斺€斿绾﹁繙涓嶆鏄?瀛楃涓?銆?+
+## Considered Options
+
+- **褰卞瓙瀹炰綋锛堢幇鐘讹級**锛歴chema 娉勬紡 + 閫昏緫閲嶅锛屼絾缂栬瘧鏈熷拰杩愯鏈熼兘涓嶄緷璧?crm-auth銆傚惁鍐斥€斺€攕chema 娉勬紡鏄牳蹇冩懇鎿︺€?+- **SQL 杩佺Щ鑴氭湰锛團lyway/Liquibase锛?*锛氶」鐩敤 JPA `ddl-auto: update`锛屾棤杩佺Щ妗嗘灦銆傚紩鍏?Flyway 浠呬负姝ら渶鏀瑰彉鍏ㄩ」鐩殑 schema 绠$悊绛栫暐锛屾垚鏈笉鎴愭瘮渚嬨€傚惁鍐炽€?+- **`@Autowired(required=false)`**锛氭敞鍏?`PermissionSeeder` 鏃舵壘涓嶅埌瀹炵幇灏辫烦杩囩瀛愬寲銆傜敓浜х幆澧冧細鍑虹幇"绉嶅瓙鍖栧伓灏斾笉璺?鐨勫菇鐏甸棶棰樸€傚惁鍐炽€?+- **PermissionSeeder seam锛堥噰绾筹級**锛氬湪 crm-base 鎻愬彇 interface锛宑rm-auth 鎻愪緵瀹炵幇銆傜紪璇戞湡鍙緷璧?crm-base锛坕nterface 鎵€鍦ㄥ湴锛夛紝杩愯鏈熶緷璧?crm-auth锛堥€氳繃 crm-app 鑱氬悎 classpath 鎻愪緵 `PermissionSeederImpl`锛夈€?+
+## Decision
+
+### 1. Interface 浣嶇疆涓庡舰鐘?+
+`PermissionSeeder` interface 鏀惧湪 **crm-base** 鐨?`service` 鍖咃紝`PermissionModuleDescriptor` 鍜?`ButtonSeed` record 鏀惧湪 crm-base 鐨?`domain.dto` 鍖呫€?+
+```java
+// crm-base
+public interface PermissionSeeder {
+ void seedModule(PermissionModuleDescriptor descriptor);
+}
+
+public record PermissionModuleDescriptor(
+ String catalogName, // 鐩綍鍚嶏紙濡?绯荤粺绠$悊"锛?+ String menuName, // 鑿滃崟鍚嶏紙濡?鏁版嵁瀛楀吀"锛?+ String menuPath, // 鍓嶇璺敱锛堝"/system/dict"锛?+ String menuComponent, // 鍓嶇缁勪欢璺緞锛堝"system/dict/index"锛?+ int menuSort, // 鑿滃崟鎺掑簭
+ List<ButtonSeed> buttons, // 鎸夐挳鏉冮檺鐐癸紙鍙负绌哄垪琛級
+ List<String> bindToRoleCodes // 缁戝畾鐨勫唴缃鑹茬紪鐮侊紙濡?ROLE_ADMIN"锛?+) {}
+
+public record ButtonSeed(
+ String name, // 鎸夐挳鍚嶇О
+ String perms, // 鏉冮檺鐮侊紙濡?dict:group:list"锛?+ String apiUrl, // 鎺ュ彛 URL锛圓piPermissionInterceptor 鎸夋鍖归厤锛?+ int sort // 鎺掑簭
+) {}
+```
+
+**澹版槑寮?*锛氳皟鐢ㄦ柟鍙0鏄庢剰鍥撅紙"鎴戞湁杩欎簺鏉冮檺鐐广€佹寕鍦ㄨ繖涓彍鍗曚笅銆佺粦缁欒繖涓鑹?锛夛紝瀹炵幇鏂瑰惛鏀舵墍鏈?`sys_menu` schema 缁嗚妭銆?+
+### 2. 瀹炵幇浣嶇疆
+
+`PermissionSeederImpl` 鏀惧湪 **crm-auth** 鐨?`service.impl` 鍖咃紝`@Service` 娉ㄨВ锛岀洿鎺ヤ娇鐢?`SysMenuMapper`銆乣SysRoleMapper`銆乣SysRoleMenuMapper`銆傚畠鎷ユ湁锛?+
+- 鐩綍鑺傜偣锛坱ype=1锛夌殑 find-or-create锛氭寜 `name + parentId=0` 鏌ワ紝涓嶅瓨鍦ㄥ垯寤?+- 鑿滃崟鑺傜偣锛坱ype=2锛夌殑 find-or-create锛氭寜 `name + parentId=catalogId` 鏌ワ紝涓嶅瓨鍦ㄥ垯寤?+- 鎸夐挳鑺傜偣锛坱ype=3锛夌殑 find-or-create锛氭寜 `perms` 鏌ワ紙骞傜瓑閿級锛屼笉瀛樺湪鍒欏缓
+- 瑙掕壊缁戝畾锛氭寜 `roleCode` 鏌?`sys_role`锛屾壘鍒板垯 `bindIfAbsent`锛屾壘涓嶅埌 warn 璺宠繃锛堜繚鐣欏綋鍓嶈涓猴級
+
+纭紪鐮侀粯璁ゅ€硷細`denyBehavior="hide"`銆乣status="enabled"`銆乣visible=true`銆乣menuType` 鏋氫妇鍊尖€斺€旇皟鐢ㄦ柟涓嶆劅鐭ヨ繖浜涘瓧娈点€?+
+### 3. 涓€娆′竴涓彍鍗?+
+鎻忚堪绗?= 涓€涓?catalog + 涓€涓?menu + 涓€缁?buttons銆俙DataInitializer` 璋?3 娆?`seedModule`锛堣鑹茬鐞?/ 鑿滃崟绠$悊 / 閮ㄩ棬绠$悊锛夛紝catalog 骞傜瓑纭繚涓嶉噸澶嶅垱寤恒€備笉鍋氬鑿滃崟宓屽鈥斺€斿綋鍓嶈妯★紙鏈€澶?3 涓彍鍗曪級涓嶅€煎緱寮曞叆 `List<MenuSeed>` 鐨勫祵濂楃粨鏋勩€?+
+### 4. 瑙掕壊鍒涘缓鐣欏湪 DataInitializer
+
+seam 鍙鑿滃崟/鎸夐挳/缁戝畾锛屼笉绠¤鑹插垱寤恒€俙DataInitializer` 浠嶈礋璐e垱寤?`ROLE_ADMIN`锛堝惈 `builtin=true`銆乣dataScope=ALL`锛夛紝鐒跺悗璋?`seedModule`銆俙DictPermissionInitializer` 涓嶅啀鍒涘缓瑙掕壊鈥斺€斿畠鍙皟 `seedModule`锛宻eam 鎸?`roleCode` 鏌ユ壘瑙掕壊銆?+
+### 5. 鍚姩椤哄簭
+
+`DataInitializer` 鏍?`@Order(1)`锛宍DictPermissionInitializer` 鏍?`@Order(10)`銆傜‘淇?`ROLE_ADMIN` 鍏堝垱寤猴紝鍚庣画妯″潡鐨勭粦瀹氫竴娆″埌浣嶃€?+
+### 6. 杩愯鏈熶緷璧?+
+crm-dict 缂栬瘧鏈熷彧渚濊禆 crm-base锛坄PermissionSeeder` interface锛夈€傝繍琛屾湡 `PermissionSeederImpl` 鐢?crm-auth 鎻愪緵锛岄€氳繃 crm-app 鑱氬悎 classpath 娉ㄥ叆銆傚疄闄呯郴缁熷彧閫氳繃 crm-app 鍚姩鈥斺€攃rm-auth 鍜?crm-dict 鎬诲湪鍚屼竴 classpath 涓娿€?+
+### 7. 鍒犻櫎鐨勫奖瀛愬疄浣?+
+- `crm-dict/domain/entity/SysMenuSeed.java`
+- `crm-dict/domain/entity/SysRoleSeed.java`
+- `crm-dict/domain/entity/SysRoleMenuSeed.java`
+- `crm-dict/mapper/SysMenuSeedMapper.java`
+- `crm-dict/mapper/SysRoleSeedMapper.java`
+- `crm-dict/mapper/SysRoleMenuSeedMapper.java`
+
+`DictPermissionInitializer` 鍜?`DataInitializer` 涓殑杈呭姪鏂规硶锛坄findMenuByName`銆乣insertMenu`銆乣insertButtonIfAbsent`銆乣bindIfAbsent`锛夌敱 `PermissionSeederImpl` 鍚告敹锛屼袱涓垵濮嬪寲鍣ㄥ彉涓哄0鏄庡紡钖勮皟鐢ㄦ柟銆?+
+## Consequences
+
+- **ADR-0015 缂栬瘧鏈熺害鏉熶繚鎸?*锛歝rm-dict 浠嶅彧 import crm-base 鐨勭被锛屼笉 import crm-auth銆?+- **杩愯鏈熶緷璧栨柊澧?*锛歝rm-dict 鐨?`DictPermissionInitializer` 杩愯鏈熼渶瑕?crm-auth 鐨?`PermissionSeederImpl`銆傚疄闄呴儴缃查€氳繃 crm-app 鑱氬悎锛屼笉褰卞搷鐢熶骇銆俢rm-dict 鐙珛璺?`DictApplication` 鏃堕渶 crm-auth 鍦?classpath 涓娿€?+- **鏉冮檺浣撶郴鍙崲鎬ф洿濂?*锛氭崲鏉冮檺寮曟搸鍙渶鎹?`PermissionSeederImpl`锛宑rm-dict 浠g爜涓€琛屼笉鏀广€傚奖瀛愬疄浣撴柟妗堝弽鑰岀‖缂栫爜浜?`sys_menu` 鐗╃悊缁撴瀯锛屾洿涓嶅彲鎹€?+- **Schema 鍙樻洿涓嶅啀璺ㄦā鍧楃牬鍧?*锛歝rm-auth 鏀?`sys_menu` 瀛楁鍙奖鍝?`PermissionSeederImpl`锛宑rm-dict 鏃犳劅銆?+- **鏈潵妯″潡澶嶇敤**锛歝rm-rule銆乧rm-audit 绛夋柊妯″潡绉嶅瓙鍖栨潈闄愮偣鏃讹紝鍙渶璋?`seedModule`锛屼笉闇€瑕佸啀閫犲奖瀛愬疄浣撱€?+- **crm-dict 娴嬭瘯鐦﹁韩**锛歚DictPermissionInitializerTest` 浠?H2 闆嗘垚娴嬭瘯鏀逛负 mock `PermissionSeeder` 鐨勫崟鍏冩祴璇曪紝涓嶅啀闇€瑕佸湪 H2 涓缓 `sys_menu`/`sys_role`/`sys_role_menu` 琛ㄣ€?