package com.yj.earth.business.controller; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.UUID; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.yj.earth.annotation.CheckAuth; import com.yj.earth.business.domain.IconLibrary; import com.yj.earth.business.domain.IconType; import com.yj.earth.business.service.IconLibraryService; import com.yj.earth.common.util.ApiResponse; import com.yj.earth.common.util.FileCommonUtil; import com.yj.earth.common.util.SQLiteUtil; import com.yj.earth.dto.iconLibrary.AddIconTypeDto; import com.yj.earth.dto.iconLibrary.CreateIconLibraryDto; import com.yj.earth.dto.iconLibrary.DragIconTypeDto; import com.yj.earth.dto.iconLibrary.UpdateIconTypeNameDto; import com.yj.earth.vo.IconDataVo; import com.yj.earth.vo.IconTypeVo; import com.yj.earth.vo.IconVo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.File; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Tag(name = "图标数据管理") @CheckAuth @RestController @RequestMapping("/iconLibrary") public class IconLibraryController { @Resource private IconLibraryService iconLibraryService; @Operation(summary = "创建图标库") @PostMapping("/createIconLibrary") public ApiResponse createIconLibrary(@RequestBody CreateIconLibraryDto createDto) { try { // 参数校验与路径处理 String folderPath = createDto.getPath(); String libraryName = createDto.getName(); if (!StringUtils.hasText(folderPath) || !StringUtils.hasText(libraryName)) { return ApiResponse.failure("路径和名称不能为空"); } // 构建完整图标库文件路径(SQLite文件) File parentDir = new File(folderPath); File iconFile = new File(parentDir, libraryName); String iconPath = iconFile.getAbsolutePath().replace("\\", "/"); // 父目录不存在则创建 if (!parentDir.exists()) { boolean mkdirsSuccess = parentDir.mkdirs(); if (!mkdirsSuccess) { return ApiResponse.failure("创建父目录失败:" + folderPath); } } // 校验图标库文件是否已存在 if (iconFile.exists()) { if (iconFile.isDirectory()) { return ApiResponse.failure("同名目录已存在,无法创建文件:" + iconPath); } return ApiResponse.failure("图标库文件已存在:" + iconPath); } // 创建SQLite文件 boolean createFileSuccess = iconFile.createNewFile(); if (!createFileSuccess) { return ApiResponse.failure("创建图标库文件失败:" + iconPath); } // 初始化图标库表结构 SQLiteUtil.initializationIcon(iconPath); // 添加图标库配置到数据库 addIconLibrary(iconPath); return ApiResponse.success(null); } catch (Exception e) { return ApiResponse.failure("创建图标库失败:" + e.getMessage()); } } @Operation(summary = "导入图标库") @PostMapping("/importIconLibrary") public ApiResponse importIconLibrary( @RequestParam("iconPath") @Parameter(description = "图标库SQLite文件路径") String iconPath) { try { // 校验路径是否存在 File iconFile = new File(iconPath); if (!iconFile.exists() || !iconFile.isFile()) { return ApiResponse.failure("图标库文件不存在:" + iconPath); } // 添加到配置表并启用 addIconLibrary(iconPath); return ApiResponse.success(null); } catch (Exception e) { return ApiResponse.failure("导入图标库失败:" + e.getMessage()); } } @Operation(summary = "添加图标类型") @PostMapping("/addIconType") public ApiResponse addIconType(@RequestBody AddIconTypeDto addDto) throws SQLException, IllegalAccessException, InstantiationException { // 获取当前启用的图标库路径 String iconPath = getIconLibrary(); if (iconPath == null) { return ApiResponse.failure("请先创建或导入图标库"); } // 校验父级类型(若有) String parentId = addDto.getParentId(); if (StringUtils.hasText(parentId)) { String checkParentSql = "SELECT id FROM icon_type WHERE id = ?"; List parentParams = new ArrayList<>(); parentParams.add(parentId); IconType parentType = SQLiteUtil.queryForObject( iconPath, checkParentSql, parentParams, IconType.class ); if (parentType == null) { return ApiResponse.failure("父级图标类型不存在:" + parentId); } } // 插入图标类型 String insertSql = "INSERT INTO icon_type " + "(id, name, parent_id, tree_index, created_at) " + "VALUES (?, ?, ?, ?, ?)"; List params = new ArrayList<>(); params.add(UUID.fastUUID().toString(true)); params.add(addDto.getName()); params.add(parentId); params.add(0); params.add(LocalDateTime.now().toString()); SQLiteUtil.executeUpdate(iconPath, insertSql, params); return ApiResponse.success(null); } @Operation(summary = "修改图标类型名称") @PostMapping("/updateIconTypeName") public ApiResponse updateIconTypeName(@RequestBody UpdateIconTypeNameDto updateDto) throws SQLException { String iconPath = getIconLibrary(); if (iconPath == null) { return ApiResponse.failure("请先创建或导入图标库"); } // 执行更新 String updateSql = "UPDATE icon_type SET name = ?, updated_at = ? WHERE id = ?"; List params = new ArrayList<>(); params.add(updateDto.getName()); params.add(LocalDateTime.now().toString()); params.add(updateDto.getId()); SQLiteUtil.executeUpdate(iconPath, updateSql, params); return ApiResponse.success(null); } @Operation(summary = "删除图标类型") @PostMapping("/deleteIconType") public ApiResponse deleteIconType( @RequestParam("iconTypeId") @Parameter(description = "图标类型ID") String typeId) throws SQLException { String iconPath = getIconLibrary(); if (iconPath == null) { return ApiResponse.failure("请先创建或导入图标库"); } // 执行删除 String deleteSql = "DELETE FROM icon_type WHERE id = ?"; List params = new ArrayList<>(); params.add(typeId); SQLiteUtil.executeUpdate(iconPath, deleteSql, params); return ApiResponse.success(null); } @Operation(summary = "图标类型树形列表") @GetMapping("/iconTypeTree") public ApiResponse iconTypeTree() throws SQLException, IllegalAccessException, InstantiationException { List treeList = iconTypeList(); return ApiResponse.success(treeList); } @Operation(summary = "添加图标文件") @PostMapping("/addIconFile") public ApiResponse addIconFile(@RequestParam("files") MultipartFile[] files, @RequestParam("iconTypeId") @Parameter(description = "图标类型ID") String typeId) throws IOException, SQLException, IllegalAccessException, InstantiationException { // 获取当前启用的图标库 String iconPath = getIconLibrary(); if (iconPath == null) { return ApiResponse.failure("请先创建或导入图标库"); } // 校验类型是否存在 if (!isIconTypeExist(iconPath, typeId)) { return ApiResponse.failure("图标类型不存在:" + typeId); } // 循环处理每个文件 for (MultipartFile file : files) { if (file.isEmpty()) { continue; // 跳过空文件 } // 解析文件名与后缀 String originalFileName = file.getOriginalFilename(); if (originalFileName == null) { continue; } String fileSuffix = FileUtil.extName(originalFileName); String fileNameWithoutSuffix = FileUtil.mainName(originalFileName); // 插入图标数据 String insertSql = "INSERT INTO icon " + "(id, icon_type_id, icon_name, icon_type, icon_data, created_at) " + "VALUES (?, ?, ?, ?, ?, ?)"; List params = new ArrayList<>(); params.add(UUID.fastUUID().toString(true)); params.add(typeId); params.add(fileNameWithoutSuffix); params.add(fileSuffix); params.add(file.getBytes()); params.add(LocalDateTime.now().toString()); SQLiteUtil.executeUpdate(iconPath, insertSql, params); } return ApiResponse.success(null); } @Operation(summary = "获取图标文件数据") @GetMapping("/data/icon/{iconId}/{fileSuffix}") public ResponseEntity getIconData(@PathVariable("iconId") @Parameter(description = "图标ID") String iconId, @PathVariable("fileSuffix") @Parameter(description = "图标文件后缀") String fileSuffix) { try { String iconPath = getIconLibrary(); if (iconPath == null) { return ResponseEntity.notFound().build(); } // 查询图标二进制数据 String querySql = "SELECT icon_name, icon_data FROM icon WHERE id = ?"; List params = new ArrayList<>(); params.add(iconId); IconDataVo dataVo = SQLiteUtil.queryForObject( iconPath, querySql, params, IconDataVo.class ); if (dataVo == null || dataVo.getIconData() == null) { return ResponseEntity.notFound().build(); } // 构建响应头【支持中文文件名+预览】 String originalFileName = dataVo.getIconName() + "." + fileSuffix; String encodedFileName = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name()); MediaType contentType = FileCommonUtil.getImageMediaType(fileSuffix); return ResponseEntity.ok() .contentType(contentType) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodedFileName + "\"") .body(dataVo.getIconData()); } catch (Exception e) { e.printStackTrace(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @Operation(summary = "根据类型查询图标列表") @PostMapping("/iconList") public ApiResponse getIconList(@RequestParam("iconTypeId") @Parameter(description = "图标类型ID") String typeId) throws SQLException, IllegalAccessException, InstantiationException { String iconPath = getIconLibrary(); if (iconPath == null) { return ApiResponse.failure("请先创建或导入图标库"); } // 获取当前类型及所有子类型ID(递归) List typeIdList = getIconTypeIdsWithChildren(typeId); if (typeIdList == null || typeIdList.isEmpty()) { return ApiResponse.success(new ArrayList<>()); } // 构建IN条件(处理SQL注入风险) String idsWithQuotes = typeIdList.stream() .map(id -> "'" + id + "'") .collect(Collectors.joining(",")); // 多表联查 String querySql = """ SELECT i.id, i.icon_type_id as iconTypeId, i.icon_name as iconName, i.icon_type as iconType, i.created_at as createdAt, i.updated_at as updatedAt, t.name as iconTypeName FROM icon i JOIN icon_type t ON i.icon_type_id = t.id WHERE i.icon_type_id IN (%s) ORDER BY i.created_at DESC """.replace("%s", idsWithQuotes); // 查询并转换为VO List iconVoList = SQLiteUtil.queryForList( iconPath, querySql, null, IconVo.class ); for (IconVo vo : iconVoList) { vo.setIconDataUrl("/iconLibrary/data/icon/" + vo.getId() + "/" + vo.getIconType()); } return ApiResponse.success(iconVoList); } @Operation(summary = "更新图标名称") @PostMapping("/updateIconInfo") public ApiResponse updateIconInfo( @RequestParam("iconId") @Parameter(description = "图标ID") String iconId, @RequestParam("iconName") @Parameter(description = "新图标名称") String iconName) throws SQLException { String iconPath = getIconLibrary(); if (iconPath == null) { return ApiResponse.failure("请先创建或导入图标库"); } String updateSql = "UPDATE icon SET icon_name = ?, updated_at = ? WHERE id = ?"; List params = new ArrayList<>(); params.add(iconName); params.add(LocalDateTime.now().toString()); params.add(iconId); SQLiteUtil.executeUpdate(iconPath, updateSql, params); return ApiResponse.success(null); } @Operation(summary = "删除图标") @PostMapping("/deleteIcon") public ApiResponse deleteIcon(@RequestParam("iconId") @Parameter(description = "图标ID") String iconId) throws SQLException { String iconPath = getIconLibrary(); if (iconPath == null) { return ApiResponse.failure("请先创建或导入图标库"); } // 执行删除 String deleteSql = "DELETE FROM icon WHERE id = ?"; List params = new ArrayList<>(); params.add(iconId); SQLiteUtil.executeUpdate(iconPath, deleteSql, params); return ApiResponse.success("删除图标成功"); } @Operation(summary = "拖动调整图标类型层级") @PostMapping("/dragIconType") public ApiResponse dragIconType(@RequestBody List dragDtoList) throws SQLException, IllegalAccessException, InstantiationException { String iconPath = getIconLibrary(); if (iconPath == null) { return ApiResponse.failure("请先创建或导入图标库"); } // 批量更新层级和排序 for (DragIconTypeDto dto : dragDtoList) { String updateSql = "UPDATE icon_type " + "SET parent_id = ?, tree_index = ?, updated_at = ? " + "WHERE id = ?"; List params = new ArrayList<>(); params.add(dto.getParentId()); params.add(dto.getTreeIndex()); params.add(LocalDateTime.now().toString()); params.add(dto.getId()); SQLiteUtil.executeUpdate(iconPath, updateSql, params); } // 返回更新后的树形列表 List treeList = iconTypeList(); return ApiResponse.success(treeList); } private String getIconLibrary() { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(IconLibrary::getIsEnable, 1); // 1=启用,0=未启用 IconLibrary library = iconLibraryService.getOne(queryWrapper); return library == null ? null : library.getPath(); } private void addIconLibrary(String iconPath) { // 所有已存在的图标库设置为「未启用」 List existLibraries = iconLibraryService.list(); for (IconLibrary library : existLibraries) { library.setIsEnable(0); iconLibraryService.updateById(library); } // 检查路径是否已存在(存在则启用,不存在则新增) LambdaQueryWrapper pathWrapper = new LambdaQueryWrapper<>(); pathWrapper.eq(IconLibrary::getPath, iconPath); IconLibrary existLibrary = iconLibraryService.getOne(pathWrapper); if (existLibrary != null) { existLibrary.setIsEnable(1); iconLibraryService.updateById(existLibrary); } else { IconLibrary newLibrary = new IconLibrary(); newLibrary.setId(UUID.fastUUID().toString(true)); newLibrary.setPath(iconPath); newLibrary.setName(FileUtil.mainName(iconPath)); newLibrary.setIsEnable(1); newLibrary.setCreatedAt(LocalDateTime.now()); iconLibraryService.save(newLibrary); } } private List iconTypeList() throws SQLException, IllegalAccessException, InstantiationException { String iconPath = getIconLibrary(); if (iconPath == null) { return new ArrayList<>(); } String querySql = """ SELECT id, name, parent_id as parentId, tree_index as treeIndex, created_at as createdAt, updated_at as updatedAt FROM icon_type ORDER BY tree_index ASC """; List typeList = SQLiteUtil.queryForList(iconPath, querySql, null, IconType.class); // 构建树形结构 return buildIconTypeTree(typeList); } private List buildIconTypeTree(List typeList) { // 转换为VO List voList = typeList.stream() .map(iconType -> new IconTypeVo(iconType)).collect(Collectors.toList()); // 构建ID→VO的映射(方便快速查找父级) Map voMap = voList.stream() .collect(Collectors.toMap(IconTypeVo::getId, vo -> vo)); // 组装树形结构 List rootList = new ArrayList<>(); for (IconTypeVo vo : voList) { String parentId = vo.getParentId(); if (parentId == null || parentId.isEmpty()) { rootList.add(vo); } else { IconTypeVo parentVo = voMap.get(parentId); if (parentVo != null) { parentVo.getChildren().add(vo); } } } // 排序 rootList.sort(Comparator.comparingInt(IconTypeVo::getTreeIndex)); sortIconTypeChildren(rootList); return rootList; } private void sortIconTypeChildren(List voList) { if (voList == null || voList.isEmpty()) { return; } // 排序当前层级 voList.sort(Comparator.comparingInt(IconTypeVo::getTreeIndex)); // 递归排序子层级 for (IconTypeVo vo : voList) { sortIconTypeChildren(vo.getChildren()); } } private List getIconTypeIdsWithChildren(String typeId) throws SQLException, IllegalAccessException, InstantiationException { List idList = new ArrayList<>(); if (!StringUtils.hasText(typeId)) { return idList; } String iconPath = getIconLibrary(); if (iconPath == null) { return idList; } // 校验类型是否存在 if (!isIconTypeExist(iconPath, typeId)) { return idList; } // 递归收集ID(包含自身) recursiveGetIconTypeChildren(iconPath, typeId, idList); return idList; } private void recursiveGetIconTypeChildren(String iconPath, String currentId, List idList) throws SQLException, IllegalAccessException, InstantiationException { // 先添加当前ID idList.add(currentId); // 查询直接子类型 String querySql = "SELECT id FROM icon_type WHERE parent_id = ? ORDER BY tree_index ASC"; List params = new ArrayList<>(); params.add(currentId); List childList = SQLiteUtil.queryForList( iconPath, querySql, params, IconType.class ); // 递归查询子类型的子类型 if (childList != null && !childList.isEmpty()) { for (IconType child : childList) { recursiveGetIconTypeChildren(iconPath, child.getId(), idList); } } } private boolean isIconTypeExist(String iconPath, String typeId) throws SQLException, IllegalAccessException, InstantiationException { String checkSql = "SELECT id FROM icon_type WHERE id = ?"; List params = new ArrayList<>(); params.add(typeId); List existList = SQLiteUtil.queryForList( iconPath, checkSql, params, IconType.class ); return existList != null && !existList.isEmpty(); } }