feat(device): add app icon management with pagination and delete functionality
新增应用图标库分页查询和图标删除接口,并完成对应的菜单与按钮权限初始化。
This commit is contained in:
55
sql/menu_app_icon.sql
Normal file
55
sql/menu_app_icon.sql
Normal file
@@ -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';
|
||||
@@ -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<Boolean> 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<AppIconVO> 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<AppIconVO> page = new Page<>(pageNum, pageSize);
|
||||
return PageResult.success(appIconService.pageAppIcons(page, packageName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, String> getActiveIconUrls(Collection<String> packageNames);
|
||||
|
||||
/**
|
||||
* 删除应用图标版本(逻辑删除)
|
||||
*
|
||||
* @param packageName 应用包名
|
||||
* @param iconId 图标记录ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteIcon(String packageName, Long iconId);
|
||||
|
||||
/**
|
||||
* 分页查询应用图标库(支持按包名筛选,不传则返回全部)
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param packageName 应用包名(可选)
|
||||
* @return 分页结果
|
||||
*/
|
||||
IPage<AppIconVO> pageAppIcons(IPage<AppIconVO> page, String packageName);
|
||||
}
|
||||
|
||||
@@ -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<AppIconMapper, AppIcon> impl
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getActiveIconUrls(Collection<String> packageNames) {
|
||||
if (packageNames == null || packageNames.isEmpty()) {
|
||||
public Map<String, String> getActiveIconUrls(Collection<String> packageNames) { if (packageNames == null || packageNames.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<AppIcon> activeIcons = this.lambdaQuery()
|
||||
@@ -261,7 +264,39 @@ public class AppIconServiceImpl extends ServiceImpl<AppIconMapper, AppIcon> 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<AppIconVO> pageAppIcons(IPage<AppIconVO> page, String packageName) {
|
||||
LambdaQueryWrapper<AppIcon> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(StringUtils.hasText(packageName), AppIcon::getPackageName, packageName)
|
||||
.orderByDesc(AppIcon::getUpdateTime);
|
||||
IPage<AppIcon> entityPage = this.page(
|
||||
new Page<>(page.getCurrent(), page.getSize()), wrapper);
|
||||
List<AppIconVO> voList = entityPage.getRecords().stream()
|
||||
.map(this::toVo)
|
||||
.collect(Collectors.toList());
|
||||
IPage<AppIconVO> 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;
|
||||
|
||||
Reference in New Issue
Block a user