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.

1613 lines
147 KiB

4 weeks ago
��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 %�?R��m PermissionSeeder seam
+
+**What to build:** crm-base �m^�~u�m?`PermissionSeeder` interface��cNPermissionModuleDescriptor` \�?`ButtonSeed` record���Orm-auth �m^��u�?`PermissionSeederImpl`�}\�`�����}S�?`DataInitializer` \�?`DictPermissionInitializer` ���]2�(�?find-or-create + bind ��f�}%��e� �e�[ `name+parentId` ̓�00m0��f�b��n]��yO�[ `perms` ̓�0�[����}���P�t����}��yO�[ `roleCode` ̓�0W�y��c߂b~u���P +hr `sys_menu` (�?schema �i?j{�J�wx}`menuType`��cNdenyBehavior="hide"`��cNstatus="enabled"`��cNvisible=true`�Y}�t�Qde“� {���qaqigk:|pAi�ᆒ?+
+**Blocked by:** None %�?Y���sW��Q}�o?+
+**Status:** ready-for-agent
+
+- [ ] `PermissionSeeder` interface p:jjn\m?crm-base�}\�`W��fܑ `seedModule(PermissionModuleDescriptor)` “ĉvx
+- [ ] `PermissionModuleDescriptor` record p:jjn\m?crm-base domain.dto�}\�tZ�x0}catalogName, menuName, menuPath, menuComponent, menuSort, List\<ButtonSeed\> buttons, List\<String\> bindToRoleCodes
+- [ ] `ButtonSeed` record p:jjn\m?crm-base domain.dto�}\�tZ�x0}name, perms, apiUrl, sort
+- [ ] `PermissionSeederImpl` p:jjn\m?crm-auth service.impl��[@Service` Z(1��\?m���0Z"�?SysMenuMapper / SysRoleMapper / SysRoleMenuMapper
+- [ ] seedModule 9p�pG^ find-or-create�1l0m0��f}type=1, parentId=0, by name�Y� }OM_W��f}type=2, parentId=catalogId, by name�Y� yO�[����}type=3, parentId=menuId, by perms�?+- [ ] seedModule 9p�pG^ bind�-l�[ roleCode ̓?sys_role�~\�XR�g�W bindIfAbsent�~\�X�m�]�W warn �t�[C~
+- [ ] seedModule ���P�t�,l�`�m� ���_*X�~A��v"�$1��Z2 {\mESe���]2��tg�}
+- [ ] seedModule )����}O��S���+l��Z��v"�%1�`�m� catalogName Y�D��W�[p��zZ�0m0�?+- [ ] �~�*}.��O�|�t�0� wx0}denyBehavior="hide", status="enabled", visible=true, menuType ˓+l�YJ�\� �e� �e?_ignb���m]� {Z���V~\m�m�tZ?+- [ ] `PermissionSeederImplTest`�W2 ƕ�U�W4Z-[/v�Y� 3lC~��\��)�&h�N�mCZ�XȓY��m?+- [ ] �HrAn `DataInitializer` \�?`DictPermissionInitializer` �m�o�[�m�]I_��\G^ȓ Yty�t�f�|�X� 3lC~
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 %�?igzO) DataInitializer R�?seam
+
+**What to build:** ���]SU crm-auth (�?`DataInitializer`%��e� �eZ~#�?`ROLE_ADMIN` R��m��f�}�m�]I_��\�RZ��^de 3 Z?`seedModule` �t�QdeǓ8Y,UP�pT�N(�?find-or-create \�?bind ��f�}�#XW�y�,�x��?5 �mE��[����� }OM_W��fx���U�h��Y3c���O4Q•'1x���U�h��Y3c�Y� �P�Y `@Order(1)` �~��Z~f�%1�S`m(hg�@i�W�o*[�[c�$1�{S��]M~t{\� �P�Wĕ?`DataInitializer` �ma�f� `PermissionSeederImpl` Z�JTye(��R�}T�B%�g Z�f� ?+
+**Blocked by:** 01 %�?R��m PermissionSeeder seam
+
+**Status:** ready-for-agent
+
+- [ ] `DataInitializer` Z%1�S `PermissionSeeder`�|\ {P��]^eO�?SysMenuMapper / SysRoleMapper / SysRoleMenuMapper�#XW�y��c�W�[p��|"�?SysRoleMapper�?+- [ ] `DataInitializer` R��m ROLE_ADMIN Z��^�v"�?seedModule 3 Z&0}Yt�c�X�~��`�? �m?crm:role:* ��Y3c�Y� }OM_W��fx���U}Ó�r�[����}���O4Q•'1x���U}Ó�r�[����}
+- [ ] `DataInitializer` ͓�V^e `@Order(1)`
+- [ ] `DataInitializer` �m]� {P��]�[Z�?findMenuByName / insertMenu / insertButtonIfAbsent / bindIfAbsent �~Y�}T�B%�g Z?+- [ ] �~d��|Z���YZ�?crm:role:* ɓ�Q�j�ĉ��~��R�p,a�[�o[OLE_ADMIN ���0AnO�)14Q��n]/��Y3cb~u
+- [ ] crm-auth O�)14Q4Z-[/v��3lC~
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 %�?igzO) DictPermissionInitializer + R��r�j0�^S�t9p�p�}
+
+**What to build:** ���]SU crm-dict (�?`DictPermissionInitializer`%��e� �ede 1 Z?`seedModule` �t�QdeǓ8Y,U0�^S�t9p�p�} + P�pT�N��f�}�X�f�����tO�LkM_W�?+ 10 �m?`dict:*` ��Y3c��zO�|?`ROLE_ADMIN`�Y� �P�Y `@Order(10)`���P�Wĕ�0{�mD�VYp,a�ucm�d}`SysMenuSeed`��cNSysRoleSeed`��cNSysRoleMenuSeed`�
Y�b5ppt2|(��R{�m?Mapper%��e� �e`u`m��QY�9j��Y��,U.�uO� �PxVP�?`DictPermissionInitializerTest` �m?mock `PermissionSeeder` (��R]O��Qty�t�f}`i�\ v���_*X�~@��tZ�x}�~ZZ�j H2 �m?`sys_menu`/`sys_role`/`sys_role_menu` t'1�k schema R��m��?+
+**Blocked by:** 01 %�?R��m PermissionSeeder seam
+
+**Status:** ready-for-agent
+
+- [ ] `DictPermissionInitializer` Z%1�S `PermissionSeeder`�|\ {P��]^eO�?SysMenuSeedMapper / SysRoleSeedMapper / SysRoleMenuSeedMapper
+- [ ] `DictPermissionInitializer` �t�Qde seedModule 1 Z&0}��HrA]p@iT��n] + 10 �m?dict:* ��Y3c + bindToRoleCodes=["ROLE_ADMIN"]
+- [ ] `DictPermissionInitializer` ͓�V^e `@Order(10)`
+- [ ] `SysMenuSeed`��cNSysRoleSeed`��cNSysRoleMenuSeed` �m Yܑ9p�p�}�~���QR��r�j
+- [ ] `SysMenuSeedMapper`��cNSysRoleSeedMapper`��cNSysRoleMenuSeedMapper` �m Yܑ Mapper �[�c�Wĕ?+- [ ] crm-dict �m_��h`m���}`mG�r�[�fde sys_menu / sys_role / sys_role_menu �C%�`t?+- [ ] `DictPermissionInitializerTest` ���� mock `PermissionSeeder`��\Ys�twO6}O��0�k PermissionModuleDescriptor pAi���HWatalogName��uNenuName��uNenuPath��eNuttons (�?perms R�Di0��eNindToRoleCodes�?+- [ ] `DictPermissionInitializerTest` �m�]@UR��m H2 (�?sys_menu / sys_role / sys_role_menu t?+- [ ] crm-dict O�)14Q4Z-[/v��3lC~
+- [ ] �~d��|Z���YZ�?dict:* ɓ�Q�j�ĉ��~��R�p,a�[
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-{i�3q` module is a bounded cont
| `crm-auth` | �t�0 v�m�^���m Y�g'����} | [`crm-auth/CONTEXT.md`](./crm-auth/CONTEXT.md) |
| `crm-dict` | ��HrA]p@iT(���Qt_�~GmeJ�Nq�tO�? | [`crm-dict/CONTEXT.md`](./crm-dict/CONTEXT.md) |
| `crm-file` | “�V"kp:jMP(�mAZ6}/�h�RM�) | [`crm-file/CONTEXT.md`](./crm-file/CONTEXT.md) |
-| `crm-base` | i�y��V�?��1ldes�Ʌ�Y,ȕ�p{T�3 Yq) | _(Ɠ�P�h���s�h�UYq�ta��%)_ |
+| `crm-base` | i�y��V�?��1ldes�Ʌ�Y,ȕ�p{T�3 Yq) | [`crm-base/CONTEXT.md`](./crm-base/CONTEXT.md) |
| `crm-app` | ^���zZ���YO��0[_ | _(Ɠ�P�h���s�h�UYq�ta��%)_ |
## System-wide decisions
-ADR )����} `docs/adr/`(�~d��|�~C�U�~?���OREADME.md` 0��d�X�[�fde\m?ADR-0003 / 0004 / 0006,cm�U�g`m���vȓG��`f�sb� �e� �eqe `/domain-modeling` f�%1�U�~+han�YH�f�YtE��UÓ�`��i)a� ?+ADR )����} `docs/adr/`(�~d��|�~C�U�~?���OREADME.md` 0��d�X�[�fde\m?ADR-0003 / 0004 / 0006,cm�U�g`m���vȓG��`f�sb� �e� �eqe `/domain-modeling` f�%1�U�~+han�YH�f�YtE��UÓ�`��i)a� �ODR-0016 �tg�}\m?PermissionSeeder seam P�]�t(Z%X�j�t&1g�AiHoĕ.aR�p,a�[(��RVYp,a�ucm?��?
## 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;
/**
* �~d��|��HrA]R��oP�V�5h}���P�t�Y0}�Y�_��Z���Y��0a 0�Y� ̓�0}Y�G��y��k��?- * <p>R��mP�oT�u�~��`[�?jW�y�B\� zO���qx���UM_W��f� }OW�y�,�x��?button ɓ�Q�j�xv}
- * �����vO�)14Q�t�R.|R��U�Skx���U�aYt�c�X�WDR-0012�Y� ?/p>
+ * <p>R��mP�oT�u�~��`[�?jW�y���}����`��3lC~ {@link PermissionSeeder} seam �~�]�tV�+h���qx���UM_W��f� ?+ * Yt�c�X�~��`/��n]�~��`/��)1�h�~��`(�?button ɓ�Q�j�xv}�����vO�)14Q�t�R.|R��U�Skx���U�aYt�c�X�WDR-0012��NDR-0016�Y� ?/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("t�0��P�oT�uYt�c�X͓�V���k}", adminRole.getRoleCode());
}
- // ---- �m� �~E0m0��f0}�~d��|�~��` ----
- SysMenu sysManage = findMenu("�~d��|�~��`", 0L);
- if (sysManage == null) {
- sysManage = insertMenu(new MenuSeed("�~d��|�~��`", 0L, 1, null, null, 0));
- }
-
- // ---- \m�\����n] ----
- SysMenu roleManage = findMenu("Yt�c�X�~��`", sysManage.getId());
- if (roleManage == null) {
- roleManage = insertMenu(new MenuSeed("Yt�c�X�~��`", sysManage.getId(), 2,
- "/system/role", "system/role/index", 1));
- }
- SysMenu menuManage = findMenu("��n]�~��`", sysManage.getId());
- if (menuManage == null) {
- menuManage = insertMenu(new MenuSeed("��n]�~��`", sysManage.getId(), 2,
- "/system/menu", "system/menu/index", 2));
- }
- SysMenu deptManage = findMenu("��)1�h�~��`", sysManage.getId());
- if (deptManage == null) {
- deptManage = insertMenu(new MenuSeed("��)1�h�~��`", sysManage.getId(), 2,
- "/system/dept", "system/dept/index", 3));
- }
-
- // ---- Yt�c�X�~��` button ɓ�Q�j�xv}ADR-0012�?----
- insertButtonIfAbsent("Yt�c�XR�Di0", roleManage.getId(), "crm:role:list", "/api/roles/page", 1);
- insertButtonIfAbsent("Yt�c�X�t=�Q", roleManage.getId(), "crm:role:detail", "/api/roles/detail", 2);
- insertButtonIfAbsent("Yt�c�X�m�o�t", roleManage.getId(), "crm:role:save", "/api/roles/saveOrUpdate", 3);
- insertButtonIfAbsent("Yt�c�XR��r�j", roleManage.getId(), "crm:role:delete", "/api/roles/delete", 4);
- insertButtonIfAbsent("ɓ�Q�jR��U�S", roleManage.getId(), "crm:role:assign", "/api/roles/assign-resources", 5);
-
- // ---- Yt�c�X-��n]b~u�"Xx���U�a���0An��� ȓYM_W��f�bɓ�Q�j�xv} ----
- bindIfAbsent(adminRole.getId(), sysManage.getId());
- bindIfAbsent(adminRole.getId(), roleManage.getId());
- bindIfAbsent(adminRole.getId(), menuManage.getId());
- bindIfAbsent(adminRole.getId(), deptManage.getId());
- // b~uYt�c�X�~��`�m,[�kO�)14Q button ɓ�Q�j�?- 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());
- }
+ // ---- Yt�c�X�~��`��n] + 5 �m?button ɓ�Q�j�xv}ADR-0012�?----
+ permissionSeeder.seedModule(new PermissionModuleDescriptor(
+ "�~d��|�~��`",
+ "Yt�c�X�~��`",
+ "/system/role",
+ "system/role/index",
+ 1,
+ List.of(
+ new ButtonSeed("Yt�c�XR�Di0", "crm:role:list", "/api/roles/page", 1),
+ new ButtonSeed("Yt�c�X�t=�Q", "crm:role:detail", "/api/roles/detail", 2),
+ new ButtonSeed("Yt�c�X�m�o�t", "crm:role:save", "/api/roles/saveOrUpdate", 3),
+ new ButtonSeed("Yt�c�XR��r�j", "crm:role:delete", "/api/roles/delete", 4),
+ new ButtonSeed("ɓ�Q�jR��U�S", "crm:role:assign", "/api/roles/assign-resources", 5)
+ ),
+ List.of("ROLE_ADMIN")
+ ));
+
+ // ---- ��n]�~��`��n]�X�h button�?----
+ permissionSeeder.seedModule(new PermissionModuleDescriptor(
+ "�~d��|�~��`",
+ "��n]�~��`",
+ "/system/menu",
+ "system/menu/index",
+ 2,
+ List.of(),
+ List.of("ROLE_ADMIN")
+ ));
+
+ // ---- ��)1�h�~��`��n]�X�h button�?----
+ permissionSeeder.seedModule(new PermissionModuleDescriptor(
+ "�~d��|�~��`",
+ "��)1�h�~��`",
+ "/system/dept",
+ "system/dept/index",
+ 3,
+ List.of(),
+ List.of("ROLE_ADMIN")
+ ));
log.info("ɓ�Q�j��HrA]R��oP�V�(h��̓�0lu��?);
}
-
- // ==================== ���P�tHgmT�Y“ĉvx ====================
-
- 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("R��m��n]z��PcP�k} (type={})", seed.name(), seed.type());
- return m;
- }
-
- /**
- * ��n]�~�]�t�-l�yĕ?insertMenu (��R�}��,_��Hr ��mY� ?- */
- 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("R��mɓ�Q�j�xv0}{} -> {}", 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;
+
+/**
+ * ɓ�Q�j�~�]�tV�?seam 9p�pG^�WDR-0016�?+ * <p>���0An sys_menu (��R�S��%1�tZ5uaq�t�U}�t�q�w find-or-create ���P�tP�k�S�m�^W�y�,��|9p%l� ?+ * �~�*}.�?schema �i?j{�J�wx0}denyBehavior="hide"��{Ntatus="enabled"��Nisible=true��uNenuType ˓+l�YJ�:{� ?/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("�~�]�tV�(hHoĕ-ag�Ki0}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("P�oT�uYt�c�X {} �m�]�tf���}�t�[C~ɓ�Q�jb~u�#X��Yt�c�XR��mZ��^{Z3 �`T�(1�/b}", roleCode);
+ continue;
+ }
+ bindIfAbsent(role.getId(), catalog.getId());
+ bindIfAbsent(role.getId(), menu.getId());
+ for (Long buttonId : buttonIds) {
+ bindIfAbsent(role.getId(), buttonId);
+ }
+ }
+
+ log.debug("ɓ�Q�j�Y3 ao�~�]�tV�'hlu��6a0}{} / {}", descriptor.catalogName(), descriptor.menuName());
+ }
+
+ // ==================== ���P�tHgmT�Y“ĉvx ====================
+
+ 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("R��m)����}z��PcP�k} (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); // ��n]
+ m.setPath(path);
+ m.setComponent(component);
+ m.setSort(sort);
+ m.setVisible(true);
+ sysMenuMapper.insert(m);
+ log.info("R��m��n]z��PcP�k} (type=2)", name);
+ return m;
+ }
+
+ /**
+ * �� YHoĕ.ar̓�0xV���c�S��Y3cɓ�Q�j�xv}ig�emO�?id�X�Qp:jjnR�kQ~e��pG^ȓ?id�Y� ?+ */
+ 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); // ��Y3c
+ btn.setSort(sort);
+ btn.setVisible(true);
+ btn.setPerms(perms);
+ btn.setDenyBehavior("hide");
+ btn.setApiUrl(apiUrl);
+ btn.setStatus("enabled");
+ sysMenuMapper.insert(btn);
+ log.info("R��mɓ�Q�j�xv0}{} -> {}", 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 ƕ�U�W4Z-[/v�WDR-0016�?+ * <p>`i�\ vɓ�Q�j�~�]�tV�?seam (�?find-or-create ���P�tP�k�S�m�^W�y�,��|9p6l� f�}��?/p>
+ */
+@DisplayName("ɓ�Q�j�~�]�tV�?seam ƕ�U�W4Z-[/v")
+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 R��m)����}��}OM_W��f� yO�[����߂b~uYt�c�X")
+ void seedModule_createsCatalogMenuButtonsAndBindsRole() {
+ // Q��U,��,l�W�[p��z�mG�W�y�?+ SysRole role = new SysRole();
+ role.setRoleName("�~��`[�?);
+ role.setRoleCode("ROLE_ADMIN");
+ role.setDataScope(4);
+ role.setSort(0);
+ role.setBuiltin(true);
+ sysRoleMapper.insert(role);
+ session.commit();
+
+ // ��F��1lR�p,a�[�m� �mE�g�?+ PermissionModuleDescriptor descriptor = new PermissionModuleDescriptor(
+ "�~d��|�~��`",
+ "��HrA]p@iT",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ List.of(
+ new ButtonSeed("R��U�|̓�0��", "dict:group:list", "/api/dict/group/page", 1),
+ new ButtonSeed("R��U�|�m�o�t", "dict:group:save", "/api/dict/group/saveOrUpdate", 2)
+ ),
+ List.of("ROLE_ADMIN")
+ );
+ seeder.seedModule(descriptor);
+ session.commit();
+
+ // `i�\ v�1l0m0��f�W�[?+ SysMenu catalog = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "�~d��|�~��`")
+ .eq(SysMenu::getParentId, 0L)
+ .eq(SysMenu::getMenuType, 1));
+ assertThat(catalog).isNotNull();
+ assertThat(catalog.getVisible()).isTrue();
+
+ // `i�\ v�3lM_W��f�W�[?+ SysMenu menu = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "��HrA]p@iT")
+ .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();
+
+ // `i�\ v�-l�[�����W�[?+ 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"));
+
+ // `i�\ v�3lW�y�,��|9p?+ 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 ���P�t�,l�`�m� ���_*X�~A��v"�$1��Z2 {\mESe���]2��tg�}")
+ void seedModule_idempotent_noDuplicates() {
+ // Q��U,��,l�W�[p��z�mG�W�y�?+ 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(
+ "�~d��|�~��`",
+ "��HrA]p@iT",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ List.of(new ButtonSeed("R��U�|̓�0��", "dict:group:list", "/api/dict/group/page", 1)),
+ List.of("ROLE_ADMIN")
+ );
+
+ // ��F��3l�v"�$1��Z?+ seeder.seedModule(descriptor);
+ session.commit();
+ seeder.seedModule(descriptor);
+ session.commit();
+
+ // `i�\ v�1l0m0��f� }OM_W��f� yO�[�����`Y�E�An�m� ɓ?+ Long catalogCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "�~d��|�~��`")
+ .eq(SysMenu::getParentId, 0L)
+ .eq(SysMenu::getMenuType, 1));
+ assertThat(catalogCount).isEqualTo(1);
+
+ Long menuCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "��HrA]p@iT")
+ .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);
+
+ // `i�\ v�3lW�y�,��|9p+l�{Y�E�An�m� ɓ&}catalog + menu + button = 3�?+ Long bindingCount = sysRoleMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, role.getId()));
+ assertThat(bindingCount).isEqualTo(3);
+ }
+
+ @Test
+ @DisplayName("seedModule )����}O��S���+l��Z��v"�%1�`�m� catalogName Y�D��W�[p��zZ�0m0�?)
+ void seedModule_sharedCatalog_createdOnce() {
+ // Q��U,��,l�W�[p��z�mG�W�y�?+ SysRole role = new SysRole();
+ role.setRoleName("�~��`[�?);
+ role.setRoleCode("ROLE_ADMIN");
+ role.setDataScope(4);
+ role.setSort(0);
+ role.setBuiltin(true);
+ sysRoleMapper.insert(role);
+ session.commit();
+
+ // ��F��+l���mC� {Z�~\g�@i�S\m���`�m� )����}
+ PermissionModuleDescriptor descriptor1 = new PermissionModuleDescriptor(
+ "�~d��|�~��`",
+ "Yt�c�X�~��`",
+ "/system/role",
+ "system/role/index",
+ 1,
+ List.of(),
+ List.of("ROLE_ADMIN")
+ );
+ PermissionModuleDescriptor descriptor2 = new PermissionModuleDescriptor(
+ "�~d��|�~��`",
+ "��HrA]p@iT",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ List.of(),
+ List.of("ROLE_ADMIN")
+ );
+ seeder.seedModule(descriptor1);
+ session.commit();
+ seeder.seedModule(descriptor2);
+ session.commit();
+
+ // `i�\ v�1l0m0��fg_ȓ Y�zɓ?+ Long catalogCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "�~d��|�~��`")
+ .eq(SysMenu::getParentId, 0L)
+ .eq(SysMenu::getMenuType, 1));
+ assertThat(catalogCount).isEqualTo(1);
+
+ // `i�\ v�+l���mG�M_W��fXQf�%1�`�m� )����}�m?+ SysMenu catalog = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "�~d��|�~��`")
+ .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 Yt�c�X�m�]�tf�&1i warn �t�[C~b~u")
+ void seedModule_roleNotFound_warnsAndSkipsBinding() {
+ // Q��U,��+l {R��mYt�c�X
+
+ PermissionModuleDescriptor descriptor = new PermissionModuleDescriptor(
+ "�~d��|�~��`",
+ "��HrA]p@iT",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ List.of(new ButtonSeed("R��U�|̓�0��", "dict:group:list", "/api/dict/group/page", 1)),
+ List.of("ROLE_ADMIN") // Yt�c�X�m�]�tf�?+ );
+
+ // ��F�
+ seeder.seedModule(descriptor);
+ session.commit();
+
+ // `i�\ v�1l0m0��f� }OM_W��f� yO�[����XQR��m\m?+ Long catalogCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "�~d��|�~��`")
+ .eq(SysMenu::getParentId, 0L));
+ assertThat(catalogCount).isEqualTo(1);
+
+ Long menuCount = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "��HrA]p@iT")
+ .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);
+
+ // `i�\ v�-l�uȓYW�y�,��|9p?+ Long bindingCount = sysRoleMenuMapper.selectCount(null);
+ assertThat(bindingCount).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("seedModule �~�p�[�����Wt��0}Y�D��W�[y�0m0��f�b��n]")
+ void seedModule_emptyButtons_onlyCatalogAndMenu() {
+ // Q��U,��,l�W�[p��z�mG�W�y�?+ 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(
+ "�~d��|�~��`",
+ "��n]�~��`",
+ "/system/menu",
+ "system/menu/index",
+ 2,
+ List.of(), // �~�p�[�����Wt?+ List.of("ROLE_ADMIN")
+ );
+
+ // ��F�
+ seeder.seedModule(descriptor);
+ session.commit();
+
+ // `i�\ v�1l0m0��f�b��n]R��m\m�U} Z!An��Y3c
+ SysMenu catalog = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "�~d��|�~��`")
+ .eq(SysMenu::getParentId, 0L));
+ assertThat(catalog).isNotNull();
+
+ SysMenu menu = sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, "��n]�~��`")
+ .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);
+
+ // `i�\ v�3lW�y�,��|9p,lg_ȓ?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 @@
+# i�y��V��T}crm-base�?+
+��1ldes�Ʌ�Y�m�^jo�m,l�Yi��q+^�t\�� �P {At?controller��wO {�[p�{T��0�}\g_��*a�}i�y���t�p�g�,l}/uP��}�~Gl� xO7d4d�e�vAtkT� xO�W$i�p� �O)mz�?ID��.NyBatis-Plus ��A%Mw��yO�f����Hoĕ/av�Y�V� xOhuO�$1{�m+[�g�[�0�S���P�l�Y3 ao seam �m�q�ef�(1V~��{\� ?+
+## Language
+
+**ɓ�Q�j�~�]�tc�?*:
+�m� �m?seam interface��\��`m�~pR�m,l�Y�Y3 ao�oHr�i�[�_tnOp�U�V�[���kɓ�Q�j�xv}button z��PcP� YR�p,a�[R�HrHoĕ/akygZ-a2r�DWsys_menu` �C%�`t��}��\� |\ {Ǖ� UtzOaq���d��t'1�k�C%�` schema���P�v"�&1�g��3lC~ `PermissionModuleDescriptor` �oHr�i)����}Z��]� }OM_W��f�`��yO�[�����Wt#1� zO0m͓�VW�y�,�*}.��O1}9p�pG^“xv}f�?crm-auth �mh�}���0An `sys_menu` (��R�S��%1�tZ5uaq�t�U}�t�q�w find-or-create ���P�tP�k�S�m�^W�y�,��|9p%l� �P[m`mD�!|�Y�0�X(��RVYp,a�ucm�d�g�YX� �e� erm-dict Ǔ�`�W�[?`SysMenuSeed` �~YxV�o�]�ucm�d��)�X[4^ē�r�v `sys_menu` t��}5pp��V schema Z�R!}��?+_Avoid_: ɓ�Q�jR��oP�V�'h�j��yOHoĕ-a^eO��0�j��yOHoĕ-a^eP�}\�j
+
+**ɓ�Q�j�Y3 ao���_*X�~?*:
+ɓ�Q�j�~�]�tV�+h�k�oHr�iW��f�S���P�z�mE�?_ignb�� = �m� �mF�0m0�?+ �m� �mG�M_W�?+ �m� �R�[��?+ )���#rYt�c�X+hrR�Di0���P0m0��f�{�~Y� �e� �e�`�m� )����}Z��]f��o-l�� `seedModule` Y�D��W�[p��zZ�� �P�v"�&1�g�m�]�kǕ?`sys_menu` (��Rbccm�f�tZ[��`��yOGp�m�`� |Q�W�i?j{�J�wx1}`denyBehavior`��cNstatus`��cNvisible` �~?schema �i?j{�J���qe�~�]�tc�%1�u�Hr�g�~�*}.�uO� ?+_Avoid_: ɓ�Q�j���]�u��zOR�p-a?_igHr�g`m?+
+**��Y3c�~�]�t**:
+ɓ�Q�j�Y3 ao���_*X�~?�Q��m� ɓ!�[����Hoĕ.acP(��R6nOp�_�ē�^0}Z��]��yOHoĕ.ar��yO4^Y�?URL��yO^4d�_� �P {V�mT�` `denyBehavior`/`status`/`visible`%��e� �eV~\m�m�i `sys_menu` (�?schema �U�Y��\qe�~�]�tc�%1�u�Hr�g�q�z�~�*}.�uO� �P6nUtwO {Z�}\� ���k�~��`[�:jjnZ���YZ��^� 3lC~�t�R.|�~��` API �m��|e��?+_Avoid_: ��Y3c���]�u��yOHoĕ.acP9p+l�{
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;
+
+/**
+ * ɓ�Q�j�Y3 ao���_*X�~?�Q��m� ɓ!�[����Hoĕ.acP(��R6nOp�_�ē�^}ADR-0016�?+ * <p>�m�]�[Z�?{@code denyBehavior}/{@code status}/{@code visible}%��e� �eV~\m�m�i sys_menu (�?schema �U�Y�?+ * "���R�p,a�j9p�pG^“-W�|�m� �~�*}.�uO� �P6nUtwO {Z�}\� ���k�~��`[�:jjnZ���YZ��^� 3lC~�t�R.|�~��` API �m��|e��?/p>
+ *
+ * @param name ��Y3cZ��]
+ * @param perms ɓ�Q�j.��O}�o?dict:group:list"�?+ * @param apiUrl ���0[_ URL�WpiPermissionInterceptor �� Y�V�R_�S�?+ * @param sort ���c-|
+ */
+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;
+
+/**
+ * ɓ�Q�j�~�]�tV�+h�k�oHr�iW��f�S�WDR-0016�?+ * <p>�m� �mE�?_ignb�� = �m� �mF�0m0�?+ �m� �mG�M_W�?+ �m� �R�[��?+ )���#rYt�c�X+hrR�Di0��?+ * )����}���P�t%��e� �e�`�m� )����}Z��]f��o-l�� {@code seedModule} Y�D��W�[p��zZ�� ?+ * �t�Qde“� {Ɠ�dvn sys_menu (��Rbccm�f�tZ[��`��yOGp�m�`� |Q�W�i?j{�J�wx1}
+ * {@code denyBehavior}���N@code status}���N@code visible} �~?schema �i?j{�J���qe�~�]�tc�%1�u�Hr�g�~�*}.�uO� ?/p>
+ *
+ * @param catalogName )����}Z��]}�o?�~d��|�~��`"�?+ * @param menuName ��n]Z��]}�o?��HrA]p@iT"�?+ * @param menuPath S��]l��t�qe�X��"/system/dict"�?+ * @param menuComponent S��]l��R"k�t��}�X��"system/dict/index"�?+ * @param menuSort ��n]���c-|
+ * @param buttons ��Y3cɓ�Q�j�xv}Y����~�T�Wt��}
+ * @param bindToRoleCodes b~u(��R4U��W�y�,�*}.��O}�o?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;
+
+/**
+ * ɓ�Q�j�~�]�tV�?seam�WDR-0016�?+ * <p>�t@%bc���_{T�!g�@i�ē�^!}f�g�vw�D�A~(��RHoĕ.acP�FWutton z��PcP� YR�p,a�[R�HrHoĕ/akygZ-a2r�pWys_menu �C%�`t��}�?+ * p�|\ {Ǖ� UtzOaq���d��t'1�k�C%�` schema���P�v"�&1�g��3lC~ {@link PermissionModuleDescriptor} �oHr�i)����}Z��]� }OM_W��f�`��?+ * ��Y3cR�Di0��zO0m͓�VW�y�,�*}.��O1}9p�pG^“xv}f�?crm-auth �mh�}���0An sys_menu (��R�S��%1�tZ5uaq�t�U}�t�q�w find-or-create
+ * ���P�tP�k�S�m�^W�y�,��|9p%l� ?/p>
+ * <p>Ǔ8Y,U\m�U�S��]�k0�^S�t9p�p�}“ĉ �%��e� erm-dict Ǔ�`�W�[?SysMenuSeed �~YxV�o�]�ucm�d��)�X[4^ē�r�v sys_menu t��}
+ * 5pp��V schema Z�R!}��?/p>
+ */
+public interface PermissionSeeder {
+
+ /**
+ * �~�]�tV�&h�z�mE�g�Ci�kɓ�Q�j�pt�Wɓ�Q�j�t�R.|͓b� ?+ *
+ * @param descriptor ɓ�Q�j�Y3 ao���_*X�~<�})����} + ��n] + ��Y3c + )���#rYt�c�X+hr�?+ */
+ 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;
/**
- * ��HrA]p@iTɓ�Q�j�~�]�tV�5h}���P�t�t[07�?- * <p>Op?10 �m?{@code dict:...} ɓ�Q�j.�zOR�p,a�[R�HrHoĕ/akygZ-a2r�pWys_menu �C%�`t��}��AR�|9p,l4U��x���U�aYt�c�X��?- * �m?crm-auth DataInitializer Ó�r� @��`�1l���qx���U0m0��f�]�o^S�Ww�D��{lOLE_ADMIN �TQ0R�k�pig�V�|9p,l߂ warn
+ * ��HrA]p@iTɓ�Q�j�~�]�tV�5h}���P�t�t[07�I[DR-0016�?+ * <p>��3lC~ {@link PermissionSeeder} seam Op?10 �m?{@code dict:...} ɓ�Q�j.�zOR�p,a�[R�HrHoĕ/akygZ-a2r
+ * �pWys_menu �C%�`t��}��AR�|9p,l4U��x���U�aYt�c�X���P { crm-auth DataInitializer �r� @��`�?+ * �~d��|�~��`)����}�TQ0R�?seam w�D��{lOLE_ADMIN �TQ0R�?seam warn �t�[C~b~u
* �HWrm-auth �h(h��Z���YR��mZ��^}p@iTɓ�Q�jf�$1{Z3 �`T�(1�/b}��?/p>
* <p>ɓ�Q�j�ptSUO��0!| api_url�]krm-auth (�?ApiPermissionInterceptor ��?api_url ���c�Y^gPI�R�k}
* �~�]�tV�'hlu��,a�`�md�l��xv}page/saveOrUpdate/delete/status/set-default �~Y}ȓE��]ɓ�Q��•��F]t���Z��N�1}
@@ -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;
-
- /** ��Y3c�~��7p�X { crm-auth ��n]�~��7p˓+l�Y�m� w���} */
- private static final int MENU_TYPE_BUTTON = 3;
+ private final PermissionSeeder permissionSeeder;
/** 10 �mE�Hoĕ.acP�~�]�t�?Z��], ɓ�Q�j.�? apiUrl, ���c-|)���PHoĕ.ar�m�^�X�~��g�Y?d8 �m� w�?*/
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("��F���HrA]p@iTɓ�Q�j�~�]�tV�(h��̓?..");
- // ---- �m� �~E0m0��f0}�~d��|�~��`�"X�]�o���V�[4s}�m?crm-auth DataInitializer $i�T-|�r�S�?----
- SysMenuSeed sysManage = findMenuByName("�~d��|�~��`", 0L);
- if (sysManage == null) {
- sysManage = insertMenu("�~d��|�~��`", 0L, 1, null, null, 0);
- }
-
- // ---- \m�\����n]�-l�f�����tO�i�}���Pjn�~d��|�~��`�m6[} ----
- SysMenuSeed dictMenu = findMenuByName("��HrA]p@iT", sysManage.getId());
- if (dictMenu == null) {
- dictMenu = insertMenu("��HrA]p@iT", sysManage.getId(), 2,
- "/system/dict", "system/dict/index", 4);
- }
-
- // ---- 10 �mE��[����Hoĕ.acP�X�[ perms ���P�t̓�0xV�}\�tf�%1F]�t�[C~�?----
- 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()));
- }
-
- // ---- b~uP�oT�u�~��`[�?jW�y���}�TQ0�t�[C~ + warn�?----
- SysRoleSeed admin = sysRoleMapper.selectOne(
- new LambdaQueryWrapper<SysRoleSeed>().eq(SysRoleSeed::getRoleCode, "ROLE_ADMIN"));
- if (admin == null) {
- log.warn("P�oT�uYt�c�X ROLE_ADMIN �m�]�tf���}�t�[C~��HrA]p@iTɓ�Q�jb~u�HWrm-auth R��m�t�0W�y��c�`�m+[��Z���Yt�0�|�?);
- return;
- }
- bindIfAbsent(admin.getId(), sysManage.getId());
- bindIfAbsent(admin.getId(), dictMenu.getId());
- for (Long buttonId : buttonIds) {
- bindIfAbsent(admin.getId(), buttonId);
- }
+ permissionSeeder.seedModule(new PermissionModuleDescriptor(
+ "�~d��|�~��`",
+ "��HrA]p@iT",
+ "/system/dict",
+ "system/dict/index",
+ 4,
+ BUTTON_SEEDS,
+ List.of("ROLE_ADMIN")
+ ));
log.info("��HrA]p@iTɓ�Q�j�~�]�tV�(h��̓�0lu��6a0}{} �mE�Hoĕ.acP", BUTTON_SEEDS.size());
}
-
- // ==================== ���P�tHgmT�Y“ĉvx ====================
-
- 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("R��m��n]z��PcP�k} (type={})", name, type);
- return m;
- }
-
- /**
- * �� YHoĕ.ar̓�0xV���c�S��Y3cɓ�Q�j�xv}ig�emO�?id�X�Qp:jjnR�kQ~e��pG^ȓ?id�Y� ?- */
- 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("R��mɓ�Q�j�xv0}{} -> {}", 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);
- }
-
- /** ��Y3c�~�]�t�-l�yĕ?insertButtonIfAbsent (��R�}��,_��Hr ��m?*/
- 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;
-
-/**
- * ɓ�Q�j�t�R.|͓b6nOp�_�[ē�r�v�pWys_menu �C%�`t��}T07�?- * <p>`moTde\m?crm-dict ���P�t�~�]�tV�(hHoĕ.acP�}\�W���_ {͓?@Entity�,lt%1�} crm-auth (�?SysMenu�?- * ȓ᫈Y�D��N MyBatis-Plus �t��SU��\)OO��] { crm-auth Y�}\�ucm�d�iOp�R�`�m� �[�r0�WDR-0015 ƕ5g�}�t+h�[ɓ�q}��?/p>
- */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("sys_menu")
-public class SysMenuSeed extends BaseEntity {
-
- /** �mDZ����n]ID�~\trz��PcP�m?0 */
- private Long parentId;
-
- /** ��n]Z��] */
- private String menuName;
-
- /** �~��7p�?=)����} 2=��n] 3=��Y3c */
- private Integer menuType;
-
- /** S��]l��t�qe�t��}�"X0m0�?��n]"���} */
- private String path;
-
- /** S��]l��R"k�t��}�#XM_W��fde�?*/
- private String component;
-
- /** ���c-| */
- private Integer sort;
-
- /** �`Y��F� */
- private Boolean visible;
-
- /** ɓ�Q�j.��O}��Y3c�~DHoĕ-a#r�t�U}�o?dict:item:list�?*/
- private String perms;
-
- /** Ó�rHoĕ-ai(��R�X�~���m4s0}hide ŕ/a�h��[isable �~zOde */
- private String denyBehavior;
-
- /** ɓ�Q�j�-W�Y���O0}enabled Z��de��[isabled K��nde�X�S^p� “a��w�?*/
- private String status;
-
- /** ���0[_ URL�X�[������4d�e�kZ��^l� API �t��}�I[piPermissionInterceptor �� Y�V�R_�SYt�R�W�?*/
- 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;
-
-/**
- * Yt�c�X-�t�R.|b~uȓ� Op�_�[ē�r�v�pWys_role_menu �C%�`t��}T07�?- * <p>`moTde\m?crm-dict ���P�t�~�]�tV�(hiOp�UHoĕ.acPb~uR�g4U��x���U�aYt�c�X�?- * R��~pR�m�]#r @Entity�}\t%1�} crm-auth (�?SysRoleMenu�WDR-0015 ƕ5g�}�t+h�[ɓ�q}��?/p>
- */
-@Data
-@TableName("sys_role_menu")
-public class SysRoleMenuSeed {
-
- @TableId(type = IdType.ASSIGN_ID)
- private Long id;
-
- /** Yt�c�XID */
- private Long roleId;
-
- /** ��n]/ɓ�Q�j�={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;
-
-/**
- * �~d��|Yt�c�Xȓ� Op�_�[ē�r�v�pWys_role �C%�`t��}T07�?- * <p>`moTde\m?crm-dict ���P�t�~�]�tV�(hi9p+l�}P�oT�u�~��`[�?jW�y�?ROLE_ADMIN�}\�W���_ {͓?@Entity�?- * �[?�00�?crm-auth (�?SysRole�WDR-0015 ƕ5g�}�t+h�[ɓ�q}��?/p>
- */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("sys_role")
-public class SysRoleSeed extends BaseEntity {
-
- /** Yt�c�XZ��] */
- private String roleName;
-
- /** Yt�c�X+hr�X�� ROLE_ADMIN�?*/
- private String roleCode;
-
- /** ��HrA]|��Q?m�?=ȓ��Il 2=ȓ�4Q•?3=ȓ�4Q•%17_p0a4Q•?4=O�)14Q */
- private Integer dataScope;
-
- /** ���c-| */
- private Integer sort;
-
- /** P�oT�uYt�c�X͓�V���ukrue=�~d��|P�oT�u�m�]r_R��r�j�m�]r_��-W*}.�?*/
- 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 ȓ� Op�_�[ē�r�v Mapper�1W07 ɓ�Q�j�~�]�tV�&h{"���}
- */
-@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 ȓ� Op�_�[ē�r�v Mapper�1W07 ɓ�Q�j�~�]�tV�&h{"���}
- */
-@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 ȓ� Op�_�[ē�r�v Mapper�1W07 ɓ�Q�j�~�]�tV�&h{"���}
- */
-@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 ɓ�Q�j�~�]�tV�-h�l��-aty�t�f0}�~�]�tV�'h�` 10 �mE�Hoĕ.acP���P2r��AR�|9p1lx���U�a / ���P�t / ROLE_ADMIN �TQ0�t�[C~ / Controller Z(1�o b�[
- * <p>H2 ��*[OAttT�S�Xb� AbstractDictH2Test�Y1}"Ó�rHoĕ/a��•��f緓?(��R�Y��}O��my�qe crm-auth
- * ApiPermissionInterceptor�X�[ api_url ���c�Y^gPI�R�k}�m?Spring Security �m�o v�?- * ȓ�ty�t�fYs�tzOR�p,a�[\mC�VJZ��Q�t�0�Z��D��j�o b�[�EWpi_url + status=enabled + perms�Y� ?/p>
+ * T07 ɓ�Q�j�~�]�tV�(hty�t�f}ADR-0016�Y0}`i�\ v DictPermissionInitializer ��3lC~ PermissionSeeder seam
+ * �oHr�i�YG� (��RHoĕ-a?_ignb���|\{ Controller �~�cP/u?@PreAuthorize Z(1��?+ * <p>�m�]@Ucm��de H2 �[?sys_menu/sys_role/sys_role_menu t"1� �e� eeam 9p�pG^f�?crm-auth (�?+ * PermissionSeederImplTest �mb��l��-aty�t�f� ?/p>
*/
@DisplayName("T07 ɓ�Q�j�~�]�tV�?)
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 �mE�Hoĕ.ar�X { DictConstants + S��]l�“�V0 d8 �m� w���} */
@@ -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("�~�]�tV�5h0}�~d��|�~��`/��HrA]p@iT���P2r�?0 �mE�Hoĕ.acP/u?api_url �m�e�|9p1lx���U�a")
- void seedAll_createsTreeAndBindsAdmin() {
- SysRoleSeed admin = insertAdminRole();
-
+ @DisplayName("seedModule �oHr�i�YG� (��R0m0��f� }OM_W��f� ?0 �mE��[����Hoĕ.acP\��\W�y�,��|9p?)
+ void seedModule_declaresCorrectDescriptor() {
initializer.run();
- // )����}�m�^M_W�?- SysMenuSeed sysManage = findMenu("�~d��|�~��`", 0L);
- assertThat(sysManage).isNotNull();
- SysMenuSeed dictMenu = findMenu("��HrA]p@iT", 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("�~d��|�~��`");
+ assertThat(descriptor.menuName()).isEqualTo("��HrA]p@iT");
+ 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 �mE�Hoĕ.acP�qkerms �i,a�S��yO�[���᫈h�([� jNnabled��dNpi_url ȕ�p%�WpiPermissionInterceptor T��rGm�o b�[�?- List<SysMenuSeed> buttons = sysMenuMapper.selectList(null).stream()
- .filter(m -> m.getMenuType() == 3)
- .toList();
+ // 10 �mE��[����Hoĕ.acP
+ 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);
+
+ // �Y�_ܑ��Y3c����An 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();
});
-
- // b~u�~��`[�Hj0})����} + ��n] + 10 ��Y3c
- List<SysRoleMenuSeed> binds = sysRoleMenuMapper.selectList(null);
- assertThat(binds).hasSize(12);
- assertThat(binds).allMatch(b -> b.getRoleId().equals(admin.getId()));
- }
-
- @Test
- @DisplayName("���P�t�6lxV�o�]�Xt|\ {\mESe���]2���n]/ɓ�Q�j�?b~u")
- void seedAll_idempotent() {
- insertAdminRole();
-
- initializer.run();
- initializer.run();
-
- assertThat(sysMenuMapper.selectCount(null)).isEqualTo(12);
- assertThat(sysRoleMenuMapper.selectCount(null)).isEqualTo(12);
- }
-
- @Test
- @DisplayName("ROLE_ADMIN �TQ0�-lHoĕ.acP�C6r�~�]�tV�5h}�t�[C~b~u�m�e {���m}/u?)
- void seedAll_skipsBindingWhenAdminMissing() {
- initializer.run();
-
- assertThat(sysMenuMapper.selectCount(null)).isEqualTo(12);
- assertThat(sysRoleMenuMapper.selectCount(null)).isZero();
- }
-
- @Test
- @DisplayName("�o�]deÓ!2An͓/b0}�~d��|�~��`/��HrA]p@iT�[�c�tf�&1i�m�]�V�[4s}ɓ�Q�j�ĉ�[ perms t�0�}�m�]xV�o?)
- void seedAll_reusesExistingMenuTree() {
- insertAdminRole();
- // �Y!�Z crm-auth DataInitializer �[�c�W�[y�0m0��f}�m�e4QR��UHoĕ.acP�[�c�tf���}/u?apiUrl�?- SysMenuSeed sysManage = new SysMenuSeed();
- sysManage.setParentId(0L);
- sysManage.setMenuName("�~d��|�~��`");
- sysManage.setMenuType(1);
- sysManage.setSort(0);
- sysManage.setVisible(true);
- sysMenuMapper.insert(sysManage);
-
- SysMenuSeed dictMenu = new SysMenuSeed();
- dictMenu.setParentId(sysManage.getId());
- dictMenu.setMenuName("��HrA]p@iT");
- 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("R��U�|̓�0��");
- 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();
-
- // ͓b {���]2��6ljo��Y3cz��PcP`m?2 �mN�}��Y3c��g�0 10 �m?- 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);
- // �[�c�tf�&1�[����Z~��xO+^ id �m�]xV�[?- assertThat(buttons.stream().filter(b -> b.getPerms().equals(DictConstants.PERM_GROUP_LIST))
- .map(SysMenuSeed::getId)).containsExactly(existingButton.getId());
- // b~ut�0�}
- assertThat(sysRoleMenuMapper.selectCount(null)).isEqualTo(12);
}
@Test
@@ -242,49 +92,10 @@ class DictPermissionInitializerTest {
}
}
- /*-------- HgmT�Y --------*/
-
- 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);
- }
-
/** `m?hasAuthority('xxx') t(1�c�[�_Q���,aG_ɓ�Q�j.�?*/
private static String extractPerm(String expression) {
int start = expression.indexOf('\'');
int end = expression.lastIndexOf('\'');
return expression.substring(start + 1, end);
}
-
- /*-------- �[?�0 DDL�X { crm-auth 9p�p�}�d/p5pR_�}�tkys_menu Z�?perms/api_url �~ Y�X^p�f�W�?-------*/
-
- 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 %�?Z%X�j�t&1g�AiHoĕ.aR�p,a�[(��RVYp,a�ucm?+
+## Status
+
+Accepted
+
+## Context
+
+crm-dict (�?`DictPermissionInitializer` Ǖ� UtxO�v 10 �m?`dict:*` ɓ�Q�j�-WR�p,a�[R�HrHoĕ/akygZ-a2r�DWsys_menu` �C%�`t��}��AR�|9p,l�WP�oT�u�~��`[�?jW�y�B\� �ODR-0015 �~yO� crm-dict ,h'vȓ�q�}�t?crm-auth�?crm-dict Y�C��}�t?crm-base�|\ { import crm-auth (��Rbccm�f��"�Y� ?+
+�my��|ig�VV~�m� �~=�ko��[rm-dict R��m\m�U{�m?*0�^S�t9p�p�}**%��e� �dSysMenuSeed`��cNSysRoleSeed`��cNSysRoleMenuSeed`%��e� �ede `@TableName("sys_menu")` �~ Y?m���0�iOp?crm-auth (��R�X��U0��\� 3lC~w�D�A~(�?Mapper P�k�S���PV~5pp��V�?+
+1. **Schema Z�R!}**�]krm-dict -��0�N `sys_menu` (��R�S��%1�tZ[��`�DWmenuType`��cNparentId`��cNperms`��cNdenyBehavior`��cNstatus`��cNapiUrl`��cNvisible`�Y� yOGp�m�`� wx}`menuType=3` t'1Z0 button�Y� �O�|�t�0� wx}`denyBehavior="hide"`��cNstatus="enabled"`�Y� �Orm-auth ��?schema ÓXdo�i<jlrg�?crm-dict (��RR�p,a�[��?+2. **��f�}���]2�**�ZkDictPermissionInitializer` �m?crm-auth (�?`DataInitializer` ȓ
Y�V�m�^ImZ��\�kHgmT�Y“ĉvx�DWfindMenuByName`��cNinsertButtonIfAbsent`��cNbindIfAbsent`�Y}Z�?~100 t{\� ?+3. **ADR-0015 ���_Xmt��Z~Y�?*�0kDR �t?ɓ�Q�j.�xO�t�~?���ē� { auth cm�d��(��Rne�m� �o b�["�|\~}0�^S�t9p�p�}��CZ�f�m?`sys_menu` �C%�`�d/p Z�R!}\m�UC~ɓ�0� �e� �e���~A�Y~�m�]�ē?pCi���m?��?+
+## Considered Options
+
+- **0�^S�t9p�p�}�"XG^���}**�tkchema Z�R!} + ��f�}���]2��|\~},h'vȓ�q�big/a�ȓ�qXQ�m�]�}�t?crm-auth���P�`P��e� �e� echema Z�R!}ē�srG��Q�a��:�� ?+- **SQL igzO)t�-lpn�Wlyway/Liquibase�?*�6l 0)���de JPA `ddl-auto: update`�~\�higzO)�Y�Ufp���P)}O�?Flyway `mlT��Y�06n��ptI_O�)1 0)����k schema �~��`�~+h�f�~\�Wȓ�� {��-a.vn([� �P�`P��p� ?+- **`@Autowired(required=false)`**�-l^eO�?`PermissionSeeder` Ó5��X�m�]�W9p�pG^Op���pig�VR�p,a�[���PSe\mEF^�o�Q0}Q�y�G^"�~�]�tV�'hOOp�e {�t?(��Rǃ�8u�h�h8j� �P�`P��p� ?+- **PermissionSeeder seam�%XpV�~y{}**�,ljn crm-base ��,aG_ interface��[rm-auth ��*a�}9p�pG^���P*}�tbanY�C��}�t?crm-base�UWnterface ��� f�%1tn�Y}ig/a�ȓ�q�}�t?crm-auth�%X� 3lC~ crm-app q�,l�` classpath ��*a�} `PermissionSeederImpl`�Y� ?+
+## Decision
+
+### 1. Interface cm�]�u�m�^0��?+
+`PermissionSeeder` interface ���`jn **crm-base** (�?`service` V��T}`PermissionModuleDescriptor` \�?`ButtonSeed` record ���`jn crm-base (�?`domain.dto` V�kT� ?+
+```java
+// crm-base
+public interface PermissionSeeder {
+ void seedModule(PermissionModuleDescriptor descriptor);
+}
+
+public record PermissionModuleDescriptor(
+ String catalogName, // )����}Z��]}�o?�~d��|�~��`"�?+ String menuName, // ��n]Z��]}�o?��HrA]p@iT"�?+ String menuPath, // S��]l��t�qe�X��"/system/dict"�?+ String menuComponent, // S��]l��R"k�t��}�X��"system/dict/index"�?+ int menuSort, // ��n]���c-|
+ List<ButtonSeed> buttons, // ��Y3cɓ�Q�j�xv}Y����~�T�Wt��}
+ List<String> bindToRoleCodes // b~u(��R4U��W�y�,�*}.��O}�o?ROLE_ADMIN"�?+) {}
+
+public record ButtonSeed(
+ String name, // ��Y3cZ��]
+ String perms, // ɓ�Q�j.��O}�o?dict:group:list"�?+ String apiUrl, // ���0[_ URL�WpiPermissionInterceptor �� Y�V�R_�S�?+ int sort // ���c-|
+) {}
+```
+
+**�oHr�i�[?*�3l�v"�&1�gY�D��ē�^pRe��d}"��bAnigk:|ɓ�Q�j�^� yO�[f�(1V~�mG�M_W��f{��zO�|kV~�mG�W�y�?�Y}9p�pG^“pt�`��5��Xȓ?`sys_menu` schema �U�Y��?+
+### 2. 9p�pG^cm�]�u
+
+`PermissionSeederImpl` ���`jn **crm-auth** (�?`service.impl` V��T}`@Service` Z(1��\?m���0Z"�?`SysMenuMapper`��cNSysRoleMapper`��cNSysRoleMenuMapper`���P`u���0An�?+
+- )����}z��PcP�qWype=1� Y�k find-or-create�-l�[ `name + parentId=0` ̓�0}�m�]�tf�%1�W�[?+- ��n]z��PcP�qWype=2� Y�k find-or-create�-l�[ `name + parentId=catalogId` ̓�0}�m�]�tf�%1�W�[?+- ��Y3cz��PcP�qWype=3� Y�k find-or-create�-l�[ `perms` ̓�0}���P�t����}�|\ {p:jjnR�k
+- Yt�c�Xb~u�-l�[ `roleCode` ̓?`sys_role`�~\�XR�g�W `bindIfAbsent`�~\�X�m�]�W warn �t�[C~�XZ~#�k�}S��]��m4s}
+
+�~�*}.��O�|�t�0� wx0}`denyBehavior="hide"`��cNstatus="enabled"`��cNvisible=true`��cNmenuType` ˓+l�YJ�\� �e� �e�v"�&1�g�m�]�R-��0V~\m�m�tZ�p� ?+
+### 3. �m� Z2 �z�mG�M_W�?+
+���_*X�~?= �m� �m?catalog + �m� �m?menu + �m� ?buttons���ODataInitializer` �t?3 Z?`seedModule`�#XW�y�,�x��?/ ��n]�~��` / ��)1�h�~��`�Y}catalog ���P�t�~��Z~�m�]xV�o�]�W�[R`� �P {K�,l?���n]�[}\��%��e� �e�}S��]I��Y&}ȓ� �o?3 �mG�M_W��f}�m�]� Nq�}�[�f�S `List<MenuSeed>` (��Ruy�oCi�|˓�R� ?+
+### 4. Yt�c�XR��m#�kjn DataInitializer
+
+seam Y�F�x���n]/��Y3c/b~u�|\ {�~�W�y��c�W�[R`� �ODataInitializer` `m�] y�tE��W�[?`ROLE_ADMIN`�X�` `builtin=true`��cNdataScope=ALL`�Y}����`�t?`seedModule`���ODictPermissionInitializer` �m�]@UR��mYt�c�X%��e� �e`uY�G��v `seedModule`��[eam ��?`roleCode` ̓�0�XYt�c�X��?+
+### 5. Z���Y$i�T-|
+
+`DataInitializer` ͓?`@Order(1)`��[DictPermissionInitializer` ͓?`@Order(10)`���P �m?`ROLE_ADMIN` O�X�W�[4s}Z��^;u�Y3 ao(��R�|9p+l�zZ3 �Wcm�]� ?+
+### 6. ig/a�ȓ�q�}�t?+
+crm-dict ,h'vȓ�qg_n�o�y crm-base�DWPermissionSeeder` interface�Y� �PM~t~\an `PermissionSeederImpl` "�?crm-auth ��*a�}��\� 3lC~ crm-app q�,l�` classpath Z%1�S���P�uĕoT���qg_��3lC~ crm-app Z���Y%��e� erm-auth \�?crm-dict ����jnZ�|\�z classpath �m?Z� ?+
+### 7. R��r�j(��RVYp,a�ucm?+
+- `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` �m`��kHgmT�Y“ĉvx�DWfindMenuByName`��cNinsertMenu`��cNinsertButtonIfAbsent`��cNbindIfAbsent`� Yqe `PermissionSeederImpl` Z�JTye�|\���mD��W�o*[�[c�%1I_�m�T�ē�^!}���R�v"�&1�g��?+
+## Consequences
+
+- **ADR-0015 ,h'vȓ�q�[ɓ�qZ~��?*�]krm-dict `m�]g_ import crm-base (��R���|\ { import crm-auth��?+- **ig/a�ȓ�q�}�t(h�g�o?*�]krm-dict (�?`DictPermissionInitializer` ig/a�ȓ�q6nUt?crm-auth (�?`PermissionSeederImpl`���P�uĕtT4Q�g� 3lC~ crm-app q�,l�`�|\ {0�^S7d"��q�����Orm-dict ���s�t?`DictApplication` ÓX6n crm-auth f�?classpath �m?Z� ?+- **ɓ�Q�jcm�d��Y��2]��D?m�o?*�-l2]ɓ�Q�j�[�f8dY�H�6n��?`PermissionSeederImpl`��[rm-dict `mG�r�m� t|\ {��^� �PVYp,a�ucm�d�g�YX=_p��\ +hr\m?`sys_menu` �C%�`�d/p�~\?m�m�]r_��l�� ?+- **Schema Y�;j?m�m�]@U�t&1g�Cilrg�?*�]krm-auth ��?`sys_menu` pAi��Y�D�VY]�?`PermissionSeederImpl`��[rm-dict Ó�r�R��?+- **ȓE�uo�Y3 ao�o�]de**�]krm-rule��gNrm-audit �~ Y�g�Y3 ao�~�]�tV�(hHoĕ.acPÓ��}Y�H�6n�t?`seedModule`�|\ {Ǖ� UtxO@U���rVYp,a�ucm�d� ?+- **crm-dict 4Z-[/v&�A���**�ZkDictPermissionInitializerTest` `m?H2 ƕ�U�W4Z-[/v���� mock `PermissionSeeder` (��R]O��Qty�t�f}�m�]@UǕ� UtxOjn H2 �m^� `sys_menu`/`sys_role`/`sys_role_menu` t#1� ?