From e7ca6dd84696ad4e499b6c26a06f6dfa6978caf4 Mon Sep 17 00:00:00 2001 From: chenxing Date: Fri, 28 Aug 2026 10:23:36 +0800 Subject: [PATCH] =?UTF-8?q?logo,=E5=A3=81=E7=BA=B8=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yunhai/userhome/config/WebMvcConfig.java | 4 + .../api/UserSettingsController.java | 118 ++++++++++++++++-- .../repository/UserSettingsRepository.java | 16 +++ .../service/IUserSettingsService.java | 10 +- .../service/impl/UserSettingsServiceImpl.java | 86 ++++++++++++- .../com/yunhai/userhome/utils/FileUtil.java | 34 ++++- 6 files changed, 250 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/yunhai/userhome/config/WebMvcConfig.java b/src/main/java/com/yunhai/userhome/config/WebMvcConfig.java index 1d7bf82..1574063 100644 --- a/src/main/java/com/yunhai/userhome/config/WebMvcConfig.java +++ b/src/main/java/com/yunhai/userhome/config/WebMvcConfig.java @@ -38,5 +38,9 @@ public class WebMvcConfig implements WebMvcConfigurer { // wallpaper 子目录:/upload/wallpaper/** → ${FILE_PATH}/wallpaper/ registry.addResourceHandler("/upload/wallpaper/**") .addResourceLocations("file:" + basePath + "wallpaper/"); + + // logo 子目录:/upload/logo/** → ${FILE_PATH}/logo/ + registry.addResourceHandler("/upload/logo/**") + .addResourceLocations("file:" + basePath + "logo/"); } } diff --git a/src/main/java/com/yunhai/userhome/controller/api/UserSettingsController.java b/src/main/java/com/yunhai/userhome/controller/api/UserSettingsController.java index 1eff8a2..25072f1 100644 --- a/src/main/java/com/yunhai/userhome/controller/api/UserSettingsController.java +++ b/src/main/java/com/yunhai/userhome/controller/api/UserSettingsController.java @@ -68,12 +68,14 @@ public class UserSettingsController { @PostMapping(value = "wallpaperUpload", produces = "application/json") public void wallpaperUpload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file) { + checkFile(file); String uploadUrl = this.upload(file, "wallpaper"); UserCurrentRole currentRole = userCurrentRoleService.findById(authUserWithDept.getId()) .orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR)); Integer type = authUserWithDept.getType() == 1 ? 0 : OtherUtil.isAdminById(currentRole.getRoleId()) ? 1 : 2; Wallpaper wallpaper = new Wallpaper(); - wallpaper.setSchId(currentRole.getSchId()); + // 超管上传的系统壁纸 schId 强制 0,避免切学校身份后产生孤儿数据 + wallpaper.setSchId(type == 0 ? 0 : currentRole.getSchId()); wallpaper.setWallpaperUrl(uploadUrl); wallpaper.setUserId(authUserWithDept.getId()); wallpaper.setType(type); @@ -82,6 +84,7 @@ public class UserSettingsController { @PostMapping(value = "iconUpload", produces = "application/json") public Map iconUpload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file) { + checkFile(file); Map result = new HashMap<>(); result.put("url", this.upload(file, "icon")); return result; @@ -89,12 +92,25 @@ public class UserSettingsController { @PostMapping(value = "deleteWallById", produces = "application/json") public void deleteWallById(BoaUser authUserWithDept, Integer id) { - userSettingsService.deleteWallById(id); + userSettingsService.deleteWallById(id, authUserWithDept); } @PostMapping(value = "checkWallpaper", produces = "application/json") public void checkWallpaper(BoaUser authUserWithDept, @RequestBody Map res) { String wallpaperUrl = res.get("wallpaperUrl").toString(); + // 校验 URL 属于该用户可见壁纸集合,防止设置任意字符串 + String normalized = normalizeWallpaperUrl(wallpaperUrl); + Set visible = new HashSet<>(); + userSettingsService.findWallList(authUserWithDept.getId()) + .forEach(w -> visible.add(w.getWallpaperUrl())); + Optional currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId()); + Integer schId = currentRoleOpt.isPresent() ? currentRoleOpt.get().getSchId() : null; + if (schId != null) { + getSchSettingsWallpaper(schId).forEach(visible::add); + } + if (!visible.contains(normalized)) { + throw new BizException(ErrorCode.FORBIDDEN, "壁纸不存在或无权使用"); + } userSettingsService.checkWallpaper(authUserWithDept.getId(), wallpaperUrl); } @@ -112,19 +128,73 @@ public class UserSettingsController { result.add(map); }); if (authUserWithDept.getType() != 1 && !list.stream().anyMatch(x -> x.getType() == 1)) { - List schList = getSchSettingsWallpaper(schId); - for (String schWall : schList) { - Map map = new HashMap(); - map.put("id", null); - map.put("type", 1); - map.put("url", schWall); - map.put("isEdit", false); - result.add(map); + // schId 以当前激活角色为准,忽略前端传参(防传他校 id 看他校壁纸) + Optional currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId()); + Integer curSchId = currentRoleOpt.isPresent() ? currentRoleOpt.get().getSchId() : null; + if (curSchId != null) { + List schList = getSchSettingsWallpaper(curSchId); + for (String schWall : schList) { + Map map = new HashMap(); + map.put("id", null); + map.put("type", 1); + map.put("url", schWall); + map.put("isEdit", false); + result.add(map); + } } } return result; } + // ==================== Logo ==================== + + @PostMapping(value = "logoUpload", produces = "application/json") + public void logoUpload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file) { + // 仅超管(系统默认)与校管理员(本校)可上传,普通用户无权设置 Logo + Integer type; + Integer schId; + if (authUserWithDept.getType() == 1) { + type = 0; + schId = 0; + } else { + UserCurrentRole currentRole = userCurrentRoleService.findById(authUserWithDept.getId()) + .orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR)); + if (!OtherUtil.isAdminById(currentRole.getRoleId())) { + throw new BizException(ErrorCode.FORBIDDEN, "无权设置Logo"); + } + type = 1; + schId = currentRole.getSchId(); + } + checkFile(file); + String uploadUrl = this.upload(file, "logo"); + Logo logo = new Logo(); + logo.setType(type); + logo.setSchId(schId); + logo.setLogoUrl(uploadUrl); + logo.setUserId(authUserWithDept.getId()); + userSettingsService.saveLogo(logo); + } + + @GetMapping(value = "getLogoList") + public List getLogoList(BoaUser authUserWithDept) { + List result = new ArrayList<>(); + List list = userSettingsService.findLogoList(authUserWithDept.getId()); + list.forEach(model -> { + Map map = new HashMap(); + map.put("id", model.getId()); + map.put("url", FILE_WEB_PATH + model.getLogoUrl().replace("/upload", "")); + map.put("type", model.getType()); + map.put("isEdit", model.getIsEdit()); + result.add(map); + }); + return result; + } + + @PostMapping(value = "deleteLogoById", produces = "application/json") + public void deleteLogoById(BoaUser authUserWithDept, Integer id) { + userSettingsService.deleteLogoById(id, authUserWithDept); + } + // ==================== 用户设置 ==================== @GetMapping(value = "getUserSettings") @@ -367,6 +437,34 @@ public class UserSettingsController { return "/upload/" + pathName + "/" + fileName; } + /** + * 上传文件前置校验:非空 + 大小上限(5MB)。扩展名白名单在 FileUtil 内校验。 + */ + private void checkFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new BizException(ErrorCode.CUSTOM, "请选择上传文件"); + } + if (file.getSize() > 5 * 1024 * 1024) { + throw new BizException(ErrorCode.CUSTOM, "图片大小不能超过5MB"); + } + } + + /** + * 归一化前端回传的壁纸 URL:去掉 FILE_WEB_PATH 前缀,并还原 getUserWallpaperList + * 输出时被 replace("/upload","") 移除的 /upload 前缀,得到与 wall 表存储一致的形式。 + */ + private String normalizeWallpaperUrl(String url) { + if (url == null) return null; + String u = url.trim(); + if (FILE_WEB_PATH != null && !FILE_WEB_PATH.isEmpty() && u.startsWith(FILE_WEB_PATH)) { + u = u.substring(FILE_WEB_PATH.length()); + } + if (u.startsWith("/wallpaper/") || u.startsWith("/icon/") || u.startsWith("/logo/")) { + u = "/upload" + u; + } + return u; + } + /** * 获取学校设置的壁纸集合(对齐旧 UserHome GetSchSettingsWallpaper,fastjson → Jackson)。 */ diff --git a/src/main/java/com/yunhai/userhome/repository/UserSettingsRepository.java b/src/main/java/com/yunhai/userhome/repository/UserSettingsRepository.java index bf2cc16..79cf4df 100644 --- a/src/main/java/com/yunhai/userhome/repository/UserSettingsRepository.java +++ b/src/main/java/com/yunhai/userhome/repository/UserSettingsRepository.java @@ -1,12 +1,28 @@ package com.yunhai.userhome.repository; import com.yunhai.userhome.entity.UserSettings; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; public interface UserSettingsRepository extends JpaRepository, JpaSpecificationExecutor { + /** + * desktopApps 为 LAZY 集合且 open-in-view=false,service 层无事务返回后 controller 仍需遍历该集合, + * 故查询时立即抓取,避免 LazyInitializationException。 + */ + @EntityGraph(attributePaths = "desktopApps") Optional findByUserIdAndSchIdAndRoleId(Integer userId, Integer schId, Integer roleId); + + /** + * 删除壁纸时将被删除壁纸引用的用户设置 wallpaper_url 置空,避免裂图(调用方需处于事务中)。 + */ + @Modifying + @Query("update UserSettings u set u.wallpaperUrl = null where u.wallpaperUrl = :url") + int clearWallpaperUrl(@Param("url") String url); } diff --git a/src/main/java/com/yunhai/userhome/service/IUserSettingsService.java b/src/main/java/com/yunhai/userhome/service/IUserSettingsService.java index d05c514..04c56a7 100644 --- a/src/main/java/com/yunhai/userhome/service/IUserSettingsService.java +++ b/src/main/java/com/yunhai/userhome/service/IUserSettingsService.java @@ -1,6 +1,8 @@ package com.yunhai.userhome.service; +import com.yunhai.platform.utils.model.BoaUser; import com.yunhai.userhome.entity.LightApp; +import com.yunhai.userhome.entity.Logo; import com.yunhai.userhome.entity.UserSettings; import com.yunhai.userhome.entity.UserSettingsDesktopApp; import com.yunhai.userhome.entity.Wallpaper; @@ -12,12 +14,18 @@ public interface IUserSettingsService { List findWallList(Integer userId); - void deleteWallById(Integer id); + void deleteWallById(Integer id, BoaUser user); void saveWallpaper(Wallpaper wallpaper); void checkWallpaper(Integer userId, String wallpaperUrl); + List findLogoList(Integer userId); + + void saveLogo(Logo logo); + + void deleteLogoById(Integer id, BoaUser user); + Optional findUserSettings(Integer userId); void saveUserSettings(UserSettings userSettings); diff --git a/src/main/java/com/yunhai/userhome/service/impl/UserSettingsServiceImpl.java b/src/main/java/com/yunhai/userhome/service/impl/UserSettingsServiceImpl.java index b98c10d..1f40fd9 100644 --- a/src/main/java/com/yunhai/userhome/service/impl/UserSettingsServiceImpl.java +++ b/src/main/java/com/yunhai/userhome/service/impl/UserSettingsServiceImpl.java @@ -2,18 +2,23 @@ package com.yunhai.userhome.service.impl; import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaUser; import com.yunhai.userhome.entity.*; import com.yunhai.userhome.repository.LightAppRepository; +import com.yunhai.userhome.repository.LogoRepository; import com.yunhai.userhome.repository.UserSettingsDesktopAppRepository; import com.yunhai.userhome.repository.UserSettingsRepository; import com.yunhai.userhome.repository.WallpaperRepository; import com.yunhai.userhome.service.IUserCurrentRoleService; import com.yunhai.userhome.service.IUserSettingsService; +import com.yunhai.userhome.utils.FileUtil; import com.yunhai.userhome.utils.OtherUtil; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import javax.persistence.criteria.Predicate; import java.util.ArrayList; @@ -30,6 +35,9 @@ public class UserSettingsServiceImpl implements IUserSettingsService { @Autowired private WallpaperRepository wallpaperRepository; + @Autowired + private LogoRepository logoRepository; + @Autowired private UserSettingsRepository userSettingsRepository; @@ -42,6 +50,9 @@ public class UserSettingsServiceImpl implements IUserSettingsService { @Autowired private UserSettingsDesktopAppRepository userSettingsDesktopAppRepository; + @Value("${FILE_PATH:./upload/}") + private String FILE_PATH; + @Override public List findWallList(Integer userId) { Optional currentRoleOpt = userCurrentRoleService.findById(userId); @@ -63,10 +74,27 @@ public class UserSettingsServiceImpl implements IUserSettingsService { } @Override - public void deleteWallById(Integer id) { - if (!wallpaperRepository.existsById(id)) - throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR); - wallpaperRepository.deleteById(id); + @Transactional + public void deleteWallById(Integer id, BoaUser user) { + Wallpaper wallpaper = wallpaperRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR)); + // 属主校验:超管可删任意;校管理员可删本校;普通用户仅可删自己上传的个人壁纸 + UserCurrentRole currentRole = userCurrentRoleService.findById(user.getId()) + .orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR)); + boolean superAdmin = user.getType() == 1; + boolean ownPersonal = wallpaper.getType() != null && wallpaper.getType() == 2 + && wallpaper.getUserId() != null && wallpaper.getUserId().equals(user.getId()); + boolean schAdminOwn = wallpaper.getType() != null && wallpaper.getType() == 1 + && OtherUtil.isAdminById(currentRole.getRoleId()) + && currentRole.getSchId() != null && currentRole.getSchId().equals(wallpaper.getSchId()); + if (!(superAdmin || ownPersonal || schAdminOwn)) { + throw new BizException(ErrorCode.FORBIDDEN); + } + // 使用中清理:被删壁纸被用户设置引用时置空,避免裂图 + userSettingsRepository.clearWallpaperUrl(wallpaper.getWallpaperUrl()); + // 清理物理文件(非本地上传的跨仓地址静默跳过) + FileUtil.deleteFile(FILE_PATH, wallpaper.getWallpaperUrl()); + wallpaperRepository.delete(wallpaper); } @Override @@ -81,6 +109,56 @@ public class UserSettingsServiceImpl implements IUserSettingsService { userSettingsRepository.save(model); } + @Override + public List findLogoList(Integer userId) { + Optional currentRoleOpt = userCurrentRoleService.findById(userId); + if (!currentRoleOpt.isPresent()) { + return new ArrayList<>(); + } + UserCurrentRole currentRole = currentRoleOpt.get(); + List resultList = new ArrayList<>(); + List baseList = logoRepository.findBySchIdAndType(0, 0); + List schList = logoRepository.findBySchIdAndType(currentRole.getSchId(), 1); + if (currentRole.getSchId() == 0) baseList.forEach(x -> x.setIsEdit(true)); + if (OtherUtil.isAdminById(currentRole.getRoleId())) schList.forEach(x -> x.setIsEdit(true)); + resultList.addAll(baseList); + resultList.addAll(schList); + return resultList; + } + + @Override + public void saveLogo(Logo logo) { + // 同 type+schId 已存在则覆盖(一校/系统各一行) + Optional exist = logoRepository.findByTypeAndSchId(logo.getType(), logo.getSchId()); + if (exist.isPresent()) { + Logo model = exist.get(); + model.setLogoUrl(logo.getLogoUrl()); + model.setUserId(logo.getUserId()); + logoRepository.save(model); + } else { + logoRepository.save(logo); + } + } + + @Override + @Transactional + public void deleteLogoById(Integer id, BoaUser user) { + Logo logo = logoRepository.findById(id) + .orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR)); + // 属主校验:超管可删系统默认;校管理员仅可删本校 + UserCurrentRole currentRole = userCurrentRoleService.findById(user.getId()) + .orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR)); + boolean superAdmin = user.getType() == 1; + boolean schAdminOwn = logo.getType() != null && logo.getType() == 1 + && OtherUtil.isAdminById(currentRole.getRoleId()) + && currentRole.getSchId() != null && currentRole.getSchId().equals(logo.getSchId()); + if (!(superAdmin || schAdminOwn)) { + throw new BizException(ErrorCode.FORBIDDEN); + } + FileUtil.deleteFile(FILE_PATH, logo.getLogoUrl()); + logoRepository.delete(logo); + } + @Override public Optional findUserSettings(Integer userId) { Optional currentRoleOpt = userCurrentRoleService.findById(userId); diff --git a/src/main/java/com/yunhai/userhome/utils/FileUtil.java b/src/main/java/com/yunhai/userhome/utils/FileUtil.java index 5e1ffaa..27e21cd 100644 --- a/src/main/java/com/yunhai/userhome/utils/FileUtil.java +++ b/src/main/java/com/yunhai/userhome/utils/FileUtil.java @@ -1,11 +1,17 @@ package com.yunhai.userhome.utils; import cn.hutool.core.util.IdUtil; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.utils.exception.ErrorCode; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; import java.util.UUID; /** @@ -13,6 +19,9 @@ import java.util.UUID; */ public class FileUtil { + /** 允许上传的图片扩展名(与前端 accept 对齐,含 icon 场景的 svg) */ + private static final Set ALLOWED_EXT = new HashSet<>(Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg")); + /** * 上传图片(byte[] 版),返回生成的文件名。 */ @@ -32,13 +41,14 @@ public class FileUtil { fos.write(imgData); fos.close(); } catch (IOException e) { - e.printStackTrace(); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "上传失败"); } return imgName; } /** * 上传图片(MultipartFile 版),保留原始扩展名,返回生成的文件名。 + *

扩展名不在白名单内抛 {@link BizException}(服务端校验,防绕过前端 accept)。

*/ public static String uploadImg(MultipartFile file, String pathName) { String imgName = null; @@ -50,7 +60,10 @@ public class FileUtil { int index = fileName.lastIndexOf("."); String ext = ""; if (index > 0) { - ext = fileName.substring(index); + ext = fileName.substring(index).toLowerCase(Locale.ROOT); + } + if (!ALLOWED_EXT.contains(ext)) { + throw new BizException(ErrorCode.CUSTOM, "不支持的图片格式"); } for (int i = 0; i < 100; i++) { imgName = UUID.randomUUID().toString().substring(0, 8) + ext; @@ -63,8 +76,23 @@ public class FileUtil { fos.write(imgData); fos.close(); } catch (IOException e) { - e.printStackTrace(); + throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "上传失败"); } return imgName; } + + /** + * 删除上传的物理文件(按 /upload/ 相对 URL 还原路径)。 + *

非本地上传(完整 URL / 跨仓地址)或文件不存在时静默跳过。

+ */ + public static void deleteFile(String basePath, String relativeUrl) { + if (relativeUrl == null || relativeUrl.isEmpty() || !relativeUrl.startsWith("/upload/")) { + return; + } + String rel = relativeUrl.substring("/upload/".length()); + File file = new File(basePath, rel); + if (file.exists()) { + file.delete(); + } + } }