logo,壁纸修复
build / build-push (push) Failing after 40s

This commit is contained in:
chenxing
2026-08-28 10:23:36 +08:00
parent a3a2cd0cf0
commit e7ca6dd846
6 changed files with 250 additions and 18 deletions
@@ -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/");
}
}
@@ -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<String, String> iconUpload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file) {
checkFile(file);
Map<String, String> 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<String> visible = new HashSet<>();
userSettingsService.findWallList(authUserWithDept.getId())
.forEach(w -> visible.add(w.getWallpaperUrl()));
Optional<UserCurrentRole> 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<String> 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<UserCurrentRole> currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId());
Integer curSchId = currentRoleOpt.isPresent() ? currentRoleOpt.get().getSchId() : null;
if (curSchId != null) {
List<String> 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<Map> getLogoList(BoaUser authUserWithDept) {
List<Map> result = new ArrayList<>();
List<Logo> 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)。
*/
@@ -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<UserSettings, Integer>, JpaSpecificationExecutor<UserSettings> {
/**
* desktopApps 为 LAZY 集合且 open-in-view=false,service 层无事务返回后 controller 仍需遍历该集合,
* 故查询时立即抓取,避免 LazyInitializationException。
*/
@EntityGraph(attributePaths = "desktopApps")
Optional<UserSettings> 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);
}
@@ -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<Wallpaper> findWallList(Integer userId);
void deleteWallById(Integer id);
void deleteWallById(Integer id, BoaUser user);
void saveWallpaper(Wallpaper wallpaper);
void checkWallpaper(Integer userId, String wallpaperUrl);
List<Logo> findLogoList(Integer userId);
void saveLogo(Logo logo);
void deleteLogoById(Integer id, BoaUser user);
Optional<UserSettings> findUserSettings(Integer userId);
void saveUserSettings(UserSettings userSettings);
@@ -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<Wallpaper> findWallList(Integer userId) {
Optional<UserCurrentRole> 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<Logo> findLogoList(Integer userId) {
Optional<UserCurrentRole> currentRoleOpt = userCurrentRoleService.findById(userId);
if (!currentRoleOpt.isPresent()) {
return new ArrayList<>();
}
UserCurrentRole currentRole = currentRoleOpt.get();
List<Logo> resultList = new ArrayList<>();
List<Logo> baseList = logoRepository.findBySchIdAndType(0, 0);
List<Logo> 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<Logo> 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<UserSettings> findUserSettings(Integer userId) {
Optional<UserCurrentRole> currentRoleOpt = userCurrentRoleService.findById(userId);
@@ -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<String> 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 版),保留原始扩展名,返回生成的文件名。
* <p>扩展名不在白名单内抛 {@link BizException}(服务端校验,防绕过前端 accept)。</p>
*/
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 还原路径)。
* <p>非本地上传(完整 URL / 跨仓地址)或文件不存在时静默跳过。</p>
*/
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();
}
}
}