@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user