From 33fcbcff3e0216032ad5278e091aa6cefc80ac77 Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Thu, 6 Aug 2026 02:25:25 +0800 Subject: [PATCH] feat(device): add app icon management with pagination and delete functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增应用图标库分页查询和图标删除接口,并完成对应的菜单与按钮权限初始化。 --- sql/menu_app_icon.sql | 55 +++++++++++++++++++ .../device/controller/AppIconController.java | 25 +++++++++ .../boot/device/service/AppIconService.java | 20 +++++++ .../service/impl/AppIconServiceImpl.java | 41 +++++++++++++- 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 sql/menu_app_icon.sql diff --git a/sql/menu_app_icon.sql b/sql/menu_app_icon.sql new file mode 100644 index 00000000..ed99c873 --- /dev/null +++ b/sql/menu_app_icon.sql @@ -0,0 +1,55 @@ +-- ---------------------------------------------------------------------------- +-- 应用图标管理 菜单 & 按钮权限初始化脚本(可重复执行 / 幂等) +-- +-- 依赖 +-- 1. 前端页面 vue3-element-admin-ttstd/src/views/devices/app-icon/index.vue +-- 2. 后端接口 com.youlai.boot.device.controller.AppIconController (/api/v1/app-icons) +-- +-- 说明 +-- - 菜单本身(type=M)不携带权限字符串,权限必须来自按钮行(type=B, perm 非空) +-- - 权限字符串中含冒号,部分客户端会把 :xxx 当成绑定变量,故本脚本用 CONCAT 拼接 +-- 且授权改用「菜单下所有按钮」子查询,SQL 文本中避免出现 :token +-- - 权限每次请求实时查库,执行后刷新页面即可,无需重启 +-- ---------------------------------------------------------------------------- + +-- 0. 定位「应用图标管理」菜单(按前端组件路径唯一标识) +SET @menu_id = (SELECT `id` FROM `sys_menu` WHERE `component` = 'devices/app-icon/index' LIMIT 1); + +-- 若菜单不存在则创建(挂到「设备调试」目录下;如目录 id 不同请自行调整 parent_id) +SET @parent_id = (SELECT `id` FROM `sys_menu` WHERE `name` = '设备调试' AND `parent_id` = 0 LIMIT 1); +INSERT INTO `sys_menu` (`parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, + `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, + `create_time`, `update_time`, `params`) +SELECT @parent_id, CONCAT('0,', @parent_id), '应用图标管理', 'M', 'AppIcon', 'app-icon', + 'devices/app-icon/index', NULL, NULL, 1, 1, 6, 'picture', NULL, now(), now(), NULL +FROM DUAL +WHERE @parent_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `component` = 'devices/app-icon/index'); + +-- 重新取一次菜单 id(可能刚创建) +SET @menu_id = (SELECT `id` FROM `sys_menu` WHERE `component` = 'devices/app-icon/index' LIMIT 1); +-- 计算按钮的 tree_path = 菜单 tree_path + ',' + 菜单 id +SET @tree_path = (SELECT CONCAT(`tree_path`, ',', `id`) FROM `sys_menu` WHERE `id` = @menu_id); + +-- 1. 插入 4 个按钮权限(上传 / 锁定 / 解锁 / 删除;不存在才插入) +-- 权限字符串用 CONCAT 拼接,避免 :token 被客户端误判 +INSERT INTO `sys_menu` (`parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, + `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, + `create_time`, `update_time`, `params`) +SELECT @menu_id, @tree_path, t.`name`, 'B', NULL, NULL, NULL, + t.`perm`, NULL, NULL, 1, t.`sort`, NULL, NULL, now(), now(), NULL +FROM ( + SELECT '上传' AS `name`, CONCAT('device', ':', 'app-icon', ':', 'upload') AS `perm`, 1 AS `sort` UNION ALL + SELECT '锁定', CONCAT('device', ':', 'app-icon', ':', 'lock'), 2 UNION ALL + SELECT '解锁', CONCAT('device', ':', 'app-icon', ':', 'unlock'), 3 UNION ALL + SELECT '删除', CONCAT('device', ':', 'app-icon', ':', 'delete'), 4 +) t +WHERE @menu_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM `sys_menu` m WHERE m.`perm` = t.`perm`); + +-- 2. 授权给 ADMIN 角色(role_id = 2;如实际角色 id 不同请修改) +-- 直接授权该菜单下的全部按钮,避免 SQL 文本中出现带冒号的权限字符串 +INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) +SELECT 2, `id` +FROM `sys_menu` +WHERE `parent_id` = @menu_id AND `type` = 'B'; diff --git a/src/main/java/com/youlai/boot/device/controller/AppIconController.java b/src/main/java/com/youlai/boot/device/controller/AppIconController.java index 5b0c8aaa..977b99b2 100644 --- a/src/main/java/com/youlai/boot/device/controller/AppIconController.java +++ b/src/main/java/com/youlai/boot/device/controller/AppIconController.java @@ -3,7 +3,10 @@ package com.youlai.boot.device.controller; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.LogModuleEnum; +import com.youlai.boot.common.result.PageResult; import com.youlai.boot.common.result.Result; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.device.model.vo.AppIconVO; import com.youlai.boot.device.service.AppIconService; import io.swagger.v3.oas.annotations.Operation; @@ -12,6 +15,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -76,4 +80,25 @@ public class AppIconController { ) { return Result.success(appIconService.uploadIcon(packageName, appName, file, "admin")); } + + @Operation(summary = "删除应用图标版本") + @DeleteMapping("/{packageName}/{iconId}") + @Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.DELETE) + public Result deleteIcon( + @Parameter(description = "应用包名") @PathVariable String packageName, + @Parameter(description = "图标记录ID") @PathVariable Long iconId + ) { + return Result.judge(appIconService.deleteIcon(packageName, iconId)); + } + + @Operation(summary = "应用图标库分页列表") + @GetMapping("/page") + public PageResult page( + @Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") Long pageNum, + @Parameter(description = "每页条数", example = "10") @RequestParam(defaultValue = "10") Long pageSize, + @Parameter(description = "应用包名(可选,不传返回全部)") @RequestParam(required = false) String packageName + ) { + IPage page = new Page<>(pageNum, pageSize); + return PageResult.success(appIconService.pageAppIcons(page, packageName)); + } } diff --git a/src/main/java/com/youlai/boot/device/service/AppIconService.java b/src/main/java/com/youlai/boot/device/service/AppIconService.java index 6678998a..e3591504 100644 --- a/src/main/java/com/youlai/boot/device/service/AppIconService.java +++ b/src/main/java/com/youlai/boot/device/service/AppIconService.java @@ -1,5 +1,7 @@ package com.youlai.boot.device.service; +import com.baomidou.mybatisplus.core.metadata.IPage; + import com.youlai.boot.device.model.vo.AppIconVO; import org.springframework.web.multipart.MultipartFile; @@ -66,4 +68,22 @@ public interface AppIconService { * @return packageName -> iconUrl */ Map getActiveIconUrls(Collection packageNames); + + /** + * 删除应用图标版本(逻辑删除) + * + * @param packageName 应用包名 + * @param iconId 图标记录ID + * @return 是否成功 + */ + boolean deleteIcon(String packageName, Long iconId); + + /** + * 分页查询应用图标库(支持按包名筛选,不传则返回全部) + * + * @param page 分页参数 + * @param packageName 应用包名(可选) + * @return 分页结果 + */ + IPage pageAppIcons(IPage page, String packageName); } diff --git a/src/main/java/com/youlai/boot/device/service/impl/AppIconServiceImpl.java b/src/main/java/com/youlai/boot/device/service/impl/AppIconServiceImpl.java index 0c42f7b2..14554b61 100644 --- a/src/main/java/com/youlai/boot/device/service/impl/AppIconServiceImpl.java +++ b/src/main/java/com/youlai/boot/device/service/impl/AppIconServiceImpl.java @@ -1,6 +1,8 @@ package com.youlai.boot.device.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.common.config.FilePath; import com.youlai.boot.common.exception.BusinessException; @@ -26,6 +28,8 @@ import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.stream.Collectors; +import java.util.List; import java.util.Map; import java.util.Set; @@ -211,8 +215,7 @@ public class AppIconServiceImpl extends ServiceImpl impl } @Override - public Map getActiveIconUrls(Collection packageNames) { - if (packageNames == null || packageNames.isEmpty()) { + public Map getActiveIconUrls(Collection packageNames) { if (packageNames == null || packageNames.isEmpty()) { return Map.of(); } List activeIcons = this.lambdaQuery() @@ -261,9 +264,41 @@ public class AppIconServiceImpl extends ServiceImpl impl return ICON_URL_PREFIX + fileName; } + @Override + public boolean deleteIcon(String packageName, Long iconId) { + Assert.isTrue(packageName != null && packageName.matches("[A-Za-z0-9._]+"), "应用包名不合法"); + Assert.notNull(iconId, "图标记录ID不能为空"); + + AppIcon icon = this.getById(iconId); + Assert.notNull(icon, "图标记录不存在"); + Assert.isTrue(packageName.equals(icon.getPackageName()), "图标记录不属于该应用"); + + // 逻辑删除(由 AppIcon 实体 @TableLogic 注解处理) + return this.removeById(iconId); + } + + @Override + public IPage pageAppIcons(IPage page, String packageName) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(StringUtils.hasText(packageName), AppIcon::getPackageName, packageName) + .orderByDesc(AppIcon::getUpdateTime); + IPage entityPage = this.page( + new Page<>(page.getCurrent(), page.getSize()), wrapper); + List voList = entityPage.getRecords().stream() + .map(this::toVo) + .collect(Collectors.toList()); + IPage result = new Page<>( + entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal()); + result.setRecords(voList); + return result; + } + private AppIconVO toVo(AppIcon entity) { + if (entity == null) { + return null; + } AppIconVO vo = new AppIconVO(); BeanUtils.copyProperties(entity, vo); return vo; } -} +} \ No newline at end of file