From 26f4bb3f25860b62cfe7df76b6ee908570f3d40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Mon, 31 Aug 2026 01:48:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E8=8F=9C=E5=8D=95=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=9A=E8=8F=9C=E5=8D=95=E7=B1=BB=E5=9E=8B=E5=88=86?= =?UTF-8?q?=E7=BB=84=E6=8E=92=E5=BA=8F=EF=BC=88=E8=BD=AF=E4=BB=B6=E5=9C=A8?= =?UTF-8?q?=E5=89=8D/=E5=90=8E=E5=8F=B0=E5=9C=A8=E5=90=8E=EF=BC=89?= =?UTF-8?q?=E3=80=81=E6=96=B0=E5=A2=9E=E5=90=8C=E7=B1=BB=E5=9E=8B=E6=9C=AB?= =?UTF-8?q?=E5=B0=BE=E6=8E=92=E5=BA=8F=EF=BC=9B=E4=BB=BB=E5=8A=A1=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E5=88=86=E7=BB=84=E7=A7=BB=E9=99=A4=EF=BC=8C=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E4=BB=BB=E5=8A=A1=E8=AE=B0=E5=BD=95=E5=BD=92=E5=85=A5?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E4=B8=8E=E7=89=88=E6=9C=AC=E3=80=81=E5=BA=97?= =?UTF-8?q?=E9=93=BA=E6=95=B0=E6=8D=AE=E8=AE=B0=E5=BD=95=E5=BD=92=E5=85=A5?= =?UTF-8?q?=E5=BA=97=E9=93=BA=E7=AE=A1=E7=90=86=EF=BC=9B=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E5=90=8D=E8=AF=8D=E4=BC=98=E5=8C=96=EF=BC=88=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=9D=83=E9=99=90=E9=85=8D=E7=BD=AE/=E5=8E=BB=E9=87=8D?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=B1=87=E6=80=BB/=E6=97=A0=E6=95=88ASIN?= =?UTF-8?q?=E6=95=B0=E6=8D=AE/=E8=A7=86=E9=A2=91=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E8=AE=B0=E5=BD=95/=E5=BA=97=E9=93=BA=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=AE=B0=E5=BD=95/=E7=94=9F=E6=88=90=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PermissionMenuSchemaInitializer.java | 10 +-- .../service/PermissionMenuService.java | 6 +- .../resources/db/V97__admin_menu_rename.sql | 28 ++++++++ .../service/PermissionMenuServiceTest.java | 49 +++++++++++++ backend/blueprints/admin_api.py | 22 +++--- backend/static/admin-interactions.js | 2 +- backend/static/admin.js | 15 ++-- backend/web_source/admin.html | 68 ++++++++----------- 8 files changed, 134 insertions(+), 66 deletions(-) create mode 100644 backend-java/src/main/resources/db/V97__admin_menu_rename.sql diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java index fee0ddaf..f64e8277 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java @@ -49,15 +49,15 @@ public class PermissionMenuSchemaInitializer { private static final List DEFAULT_ADMIN_MENUS = List.of( new DefaultAdminMenu("用户管理", "admin_users", "users", 10), - new DefaultAdminMenu("栏目权限配置", "admin_columns", "columns", 20), - new DefaultAdminMenu("数据去重总数据", "admin_dedupe_total_data", "dedupe-total-data", 30), - new DefaultAdminMenu("不符合ASIN数据", "admin_invalid_asin_data", "invalid-asin-data", 35), + new DefaultAdminMenu("菜单权限配置", "admin_columns", "columns", 20), + new DefaultAdminMenu("去重数据汇总", "admin_dedupe_total_data", "dedupe-total-data", 30), + new DefaultAdminMenu("无效ASIN数据", "admin_invalid_asin_data", "invalid-asin-data", 35), new DefaultAdminMenu("店铺密钥管理", "admin_shop_keys", "shop-keys", 40), new DefaultAdminMenu("店铺管理", "admin_shop_manage", "shop-manage", 50), new DefaultAdminMenu("跳过跟价ASIN", "admin_skip_price_asin", "skip-price-asin", 60), new DefaultAdminMenu("查询ASIN", "admin_query_asin", "query-asin", 65), new DefaultAdminMenu("商品类目", "admin_product_categories", "product-categories", 66), - new DefaultAdminMenu("查看生成记录", "admin_history", "history", 70), + new DefaultAdminMenu("生成记录", "admin_history", "history", 70), new DefaultAdminMenu("软件版本管理", "admin_version", "version", 80), new DefaultAdminMenu("数字人版本管理", "digital_human_version", "digital-human-version", 81) ); @@ -179,7 +179,7 @@ public class PermissionMenuSchemaInitializer { private boolean ensureShopDataCrawlAdminDefaults() { executeQuietly(""" INSERT INTO columns (name, column_key, menu_type, route_path, sort_order) - SELECT '店铺数据任务管理', 'admin_shop_data_crawl_tasks', 'admin', 'shop-data-crawl-tasks', 82 + SELECT '店铺数据记录', 'admin_shop_data_crawl_tasks', 'admin', 'shop-data-crawl-tasks', 82 WHERE NOT EXISTS ( SELECT 1 FROM columns WHERE column_key = 'admin_shop_data_crawl_tasks' ) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java index de1c6f58..f522fe67 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java @@ -73,7 +73,7 @@ public class PermissionMenuService { String name = normalizeRequired(request.getName(), "菜单名称不能为空"); String columnKey = normalizeRequired(request.getColumnKey(), "菜单标识不能为空"); String menuType = normalizeMenuTypeRequired(request.getMenuType()); - String routePath = normalizeRequired(request.getRoutePath(), "菜单路由不能为空"); + String routePath = normalizeRequired(request.getRoutePath(), "页面路径不能为空"); Long parentId = normalizeParentId(request.getParentId()); ensureParentValid(parentId, null, menuType); ensureUniqueColumnKey(columnKey, null); @@ -106,7 +106,7 @@ public class PermissionMenuService { String name = normalizeRequired(request.getName(), "菜单名称不能为空"); String columnKey = normalizeRequired(request.getColumnKey(), "菜单标识不能为空"); String menuType = normalizeMenuTypeRequired(request.getMenuType()); - String routePath = normalizeRequired(request.getRoutePath(), "菜单路由不能为空"); + String routePath = normalizeRequired(request.getRoutePath(), "页面路径不能为空"); Long parentId = normalizeParentId(request.getParentId()); ensureParentValid(parentId, id, menuType); ensureMenuTypeChangeAllowed(entity, menuType); @@ -706,7 +706,7 @@ public class PermissionMenuService { .eq(PermissionMenuEntity::getRoutePath, routePath) .last("LIMIT 1")); if (exists != null && (excludeId == null || !excludeId.equals(exists.getId()))) { - throw new BusinessException("同一菜单类型下的菜单路由已存在"); + throw new BusinessException("同一菜单类型下的页面路径已存在"); } } diff --git a/backend-java/src/main/resources/db/V97__admin_menu_rename.sql b/backend-java/src/main/resources/db/V97__admin_menu_rename.sql new file mode 100644 index 00000000..2d8eef2b --- /dev/null +++ b/backend-java/src/main/resources/db/V97__admin_menu_rename.sql @@ -0,0 +1,28 @@ +-- V97: 后台管理菜单显示名改为用户易理解的命名 +-- 只更新 admin 类型菜单的 name 显示名,column_key / route_path 保持不变, +-- 所有权限校验、前端分组路由、面板加载逻辑均按 route_path 匹配,不受影响。 +-- 对应前端静态文案见 backend/static/admin.js 与 backend/web_source/admin.html。 + +-- 栏目权限配置 -> 菜单权限配置 +UPDATE `columns` SET `name` = '菜单权限配置' +WHERE `menu_type` = 'admin' AND `column_key` = 'admin_columns'; + +-- 数据去重总数据 -> 去重数据汇总 +UPDATE `columns` SET `name` = '去重数据汇总' +WHERE `menu_type` = 'admin' AND `column_key` = 'admin_dedupe_total_data'; + +-- 不符合ASIN数据 -> 无效ASIN数据 +UPDATE `columns` SET `name` = '无效ASIN数据' +WHERE `menu_type` = 'admin' AND `column_key` = 'admin_invalid_asin_data'; + +-- 视频任务管理 -> 视频任务记录 +UPDATE `columns` SET `name` = '视频任务记录' +WHERE `menu_type` = 'admin' AND `column_key` = 'admin_image_video_tasks'; + +-- 店铺数据任务管理 -> 店铺数据记录 +UPDATE `columns` SET `name` = '店铺数据记录' +WHERE `menu_type` = 'admin' AND `column_key` = 'admin_shop_data_crawl_tasks'; + +-- 查看生成记录 -> 生成记录 +UPDATE `columns` SET `name` = '生成记录' +WHERE `menu_type` = 'admin' AND `column_key` = 'admin_history'; diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/permission/service/PermissionMenuServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/permission/service/PermissionMenuServiceTest.java index c71127b8..25fb8715 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/permission/service/PermissionMenuServiceTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/permission/service/PermissionMenuServiceTest.java @@ -584,6 +584,55 @@ class PermissionMenuServiceTest { assertThat(inserted.getValue().getColumnId()).isEqualTo(76L); } + @Test + void fullCatalogOrdersAppMenusBeforeAdminMenus() { + PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class); + UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class); + AdminUserMapper userMapper = mock(AdminUserMapper.class); + PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper); + + // 全量目录:app/admin 交错出现,且各自 sort_order 交叉。期望先按类型分组 + // (app 在 admin 之前),组内再按 sort_order 升序。 + when(menuMapper.selectList(any())).thenReturn(List.of( + menu(11L, null, "admin", 20), + menu(21L, null, "app", 120), + menu(12L, null, "admin", 10), + menu(22L, null, "app", 110))); + + List result = service.list(null); + + assertThat(result).extracting(PermissionMenuItemVo::getId) + .containsExactly(22L, 21L, 12L, 11L); + } + + @Test + void createWithoutSortOrderAppendsWithinSameMenuType() { + PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class); + UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class); + AdminUserMapper userMapper = mock(AdminUserMapper.class); + PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper); + + // create 内部依次用 selectOne 做唯一性校验(column_key、route_path), + // 最后 resolveSortOrder 取同类型尾项(admin 最后一个 sort_order = 134)。 + PermissionMenuEntity adminTail = menu(9L, null, "admin", 134); + adminTail.setName("admin tail"); + when(menuMapper.selectOne(any())).thenReturn(null, null, adminTail); + // create 成功后 getMenuById(insert 未回填的 id) 会走 selectById + when(menuMapper.selectById(any())).thenReturn(adminTail); + + PermissionMenuCreateRequest request = createRequest(null, "admin"); + request.setName("新菜单"); + request.setColumnKey("new_admin_menu"); + request.setRoutePath("new-admin-menu"); + + service.create(request); + + ArgumentCaptor inserted = ArgumentCaptor.forClass(PermissionMenuEntity.class); + verify(menuMapper).insert(inserted.capture()); + assertThat(inserted.getValue().getSortOrder()).isEqualTo(135); + assertThat(inserted.getValue().getMenuType()).isEqualTo("admin"); + } + private PermissionMenuCreateRequest createRequest(Long parentId, String menuType) { PermissionMenuCreateRequest request = new PermissionMenuCreateRequest(); request.setName("child"); diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index 274c9aaa..775f673f 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -87,17 +87,17 @@ ADMIN_MENU_ACCESS_CONFIG = { 'invalid-asin-data': { 'column_key': 'admin_invalid_asin_data', 'route_path': 'invalid-asin-data', - 'error': '无权访问不符合ASIN数据模块', + 'error': '无权访问无效ASIN数据模块', }, 'image-video-tasks': { 'column_key': 'admin_image_video_tasks', 'route_path': 'image-video-tasks', - 'error': '无权访问视频任务管理模块', + 'error': '无权访问视频任务记录模块', }, 'shop-data-crawl-tasks': { 'column_key': 'admin_shop_data_crawl_tasks', 'route_path': 'shop-data-crawl-tasks', - 'error': '无权访问店铺数据任务管理模块', + 'error': '无权访问店铺数据记录模块', }, } @@ -110,7 +110,7 @@ ADMIN_MENU_ACCESS_CONFIG.update({ 'columns': { 'column_key': 'admin_columns', 'route_path': 'columns', - 'error': '无权访问栏目权限配置模块', + 'error': '无权访问菜单权限配置模块', }, 'shop-keys': { 'column_key': 'admin_shop_keys', @@ -120,7 +120,7 @@ ADMIN_MENU_ACCESS_CONFIG.update({ 'history': { 'column_key': 'admin_history', 'route_path': 'history', - 'error': '无权访问查看生成记录模块', + 'error': '无权访问生成记录模块', }, 'version': { 'column_key': 'admin_version', @@ -1506,7 +1506,7 @@ def manage_shop_data_crawl_task_permissions(): return jsonify({ 'success': True, 'granted_count': result.get('data'), - 'msg': result.get('message') or '店铺数据任务权限已更新', + 'msg': result.get('message') or '店铺数据记录权限已更新', }) @@ -1858,7 +1858,7 @@ def manage_image_video_task_permissions(): return jsonify({ 'success': True, 'granted_count': result.get('data'), - 'msg': result.get('message') or '视频任务权限已更新', + 'msg': result.get('message') or '视频任务记录权限已更新', }) @@ -2110,9 +2110,9 @@ def create_column(): if not name: return jsonify({'success': False, 'error': '栏目名不能为空'}) if not column_key: - return jsonify({'success': False, 'error': '栏目标识不能为空'}) + return jsonify({'success': False, 'error': '菜单标识不能为空'}) if not route_path: - return jsonify({'success': False, 'error': '菜单路由不能为空'}) + return jsonify({'success': False, 'error': '页面路径不能为空'}) result, error_response, status = _proxy_permission_java( 'POST', '/api/admin/permission-menus', @@ -2156,9 +2156,9 @@ def update_column(cid): if not name: return jsonify({'success': False, 'error': '栏目名不能为空'}) if not column_key: - return jsonify({'success': False, 'error': '栏目标识不能为空'}) + return jsonify({'success': False, 'error': '菜单标识不能为空'}) if not route_path: - return jsonify({'success': False, 'error': '菜单路由不能为空'}) + return jsonify({'success': False, 'error': '页面路径不能为空'}) result, error_response, status = _proxy_permission_java( 'PUT', f'/api/admin/permission-menus/{cid}', diff --git a/backend/static/admin-interactions.js b/backend/static/admin-interactions.js index 5e3f0f3c..63d76a9d 100644 --- a/backend/static/admin-interactions.js +++ b/backend/static/admin-interactions.js @@ -24,7 +24,7 @@ var guides = { users: { text: '先用筛选定位账号,再编辑角色和菜单权限;删除账号会要求二次确认。', steps: ['筛选账号', '编辑权限', '确认保存'] }, - columns: { text: '菜单会影响后台和软件端的可见范围。建议先填写名称、标识和路由,再设置父菜单。', steps: ['新增或调整菜单', '设置层级', '检查权限'] }, + columns: { text: '菜单会影响后台和软件端的可见范围。建议先填写名称、标识和页面路径,再设置上级菜单。', steps: ['新增或调整菜单', '设置层级', '检查权限'] }, 'dedupe-total-data': { text: '支持按关键词、用户、分组和国家筛选;导入前先确认所选分组,导出会按日期范围生成文件。', steps: ['选择分组', '筛选或导入', '核对并导出'] }, 'invalid-asin-data': { text: '维护不符合规则的 ASIN 或品牌。添加后可使用上方筛选快速回查。', steps: ['填写 ASIN/品牌', '选择分组', '保存并回查'] }, 'shop-keys': { text: '紫鸟令牌属于敏感配置。白名单状态可悬停查看检测详情,编辑前请先核对账号名称。', steps: ['新增或筛选密钥', '查看白名单状态', '编辑或删除'] }, diff --git a/backend/static/admin.js b/backend/static/admin.js index d9cb1365..7e6f4a00 100644 --- a/backend/static/admin.js +++ b/backend/static/admin.js @@ -97,9 +97,8 @@ var ADMIN_MENU_GROUPS = [ { key: 'account', title: '账号与权限', items: ['users', 'columns'] }, { key: 'data', title: '数据管理', items: ['dedupe-total-data', 'invalid-asin-data', 'query-asin', 'product-categories'] }, - { key: 'shop', title: '店铺管理', items: ['shop-keys', 'shop-manage', 'skip-price-asin'] }, - { key: 'tasks', title: '任务中心', items: ['image-video-tasks', 'shop-data-crawl-tasks'] }, - { key: 'record', title: '记录与版本', items: ['history', 'version', 'digital-human-version'] } + { key: 'shop', title: '店铺管理', items: ['shop-keys', 'shop-manage', 'skip-price-asin', 'shop-data-crawl-tasks'] }, + { key: 'record', title: '记录与版本', items: ['history', 'version', 'digital-human-version', 'image-video-tasks'] } ]; var ADMIN_MENU_ICONS = { 'users': '', @@ -831,7 +830,7 @@ var menuType = typeSelect ? (typeSelect.value || 'app') : 'app'; var editingId = selectId === 'editColumnParentId' ? Number(document.getElementById('editColumnId').value || 0) : 0; var blockedIds = editingId > 0 ? [editingId].concat(columnDescendantIds(editingId)) : []; - select.innerHTML = ''; + select.innerHTML = ''; allColumnsList.filter(function (item) { return !item._structureOnly && String(item.menu_type || 'app') === String(menuType) && blockedIds.indexOf(columnId(item)) < 0; }).forEach(function (item) { @@ -5866,10 +5865,10 @@ msgEl.className = 'msg'; if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } if (!key) { - msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; + msgEl.textContent = '请填写菜单标识'; msgEl.classList.add('err'); return; } if (!routePath) { - msgEl.textContent = '请填写菜单路由'; msgEl.classList.add('err'); return; + msgEl.textContent = '请填写页面路径'; msgEl.classList.add('err'); return; } if (sortOrderValue !== '' && isNaN(sortOrder)) { msgEl.textContent = '排序必须是数字'; msgEl.classList.add('err'); return; @@ -5914,10 +5913,10 @@ msgEl.className = 'msg'; if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } if (!key) { - msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; + msgEl.textContent = '请填写菜单标识'; msgEl.classList.add('err'); return; } if (!routePath) { - msgEl.textContent = '请填写菜单路由'; msgEl.classList.add('err'); return; + msgEl.textContent = '请填写页面路径'; msgEl.classList.add('err'); return; } if (sortOrderValue !== '' && isNaN(sortOrder)) { msgEl.textContent = '排序必须是数字'; msgEl.classList.add('err'); return; diff --git a/backend/web_source/admin.html b/backend/web_source/admin.html index 0788f329..c7ff6b4d 100644 --- a/backend/web_source/admin.html +++ b/backend/web_source/admin.html @@ -3877,7 +3877,7 @@ - - @@ -3989,7 +3981,7 @@
- +
@@ -4042,11 +4034,11 @@
- +
- +
@@ -4061,26 +4053,26 @@
- - + +

-

栏目列表

+

菜单列表

- + - + - + @@ -4310,7 +4302,7 @@
-

新增不符合ASIN数据

+

新增无效ASIN数据

@@ -4334,7 +4326,7 @@

-

不符合ASIN数据列表

+

无效ASIN数据列表

@@ -4788,7 +4780,7 @@
-

视频任务筛选

+

视频任务记录筛选

@@ -4830,7 +4822,7 @@
-

店铺数据任务筛选

+

店铺数据记录筛选

@@ -4939,7 +4931,7 @@
- +
@@ -4978,7 +4970,7 @@
ID 菜单名称栏目标识菜单标识 菜单类型父菜单上级菜单 排序菜单路由页面路径 创建时间 操作