logo,壁纸修复,加入小应用管理,样式待调整
build / build-push (push) Successful in 2m11s

This commit is contained in:
chenxing
2026-08-28 14:24:46 +08:00
parent 567c8b7786
commit 759b120f2a
22 changed files with 813 additions and 163 deletions
@@ -42,5 +42,9 @@ public class WebMvcConfig implements WebMvcConfigurer {
// logo 子目录:/upload/logo/** → ${FILE_PATH}/logo/
registry.addResourceHandler("/upload/logo/**")
.addResourceLocations("file:" + basePath + "logo/");
// tempUpload 子目录(应用模板):/upload/tempUpload/** → ${FILE_PATH}/tempUpload/
registry.addResourceHandler("/upload/tempUpload/**")
.addResourceLocations("file:" + basePath + "tempUpload/");
}
}
@@ -0,0 +1,243 @@
package com.yunhai.userhome.controller.api;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userhome.config.Config;
import com.yunhai.userhome.entity.AppCategory;
import com.yunhai.userhome.entity.UserCurrentRole;
import com.yunhai.userhome.service.IAppCategoryService;
import com.yunhai.userhome.service.IUserCurrentRoleService;
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.core.io.InputStreamResource;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* 应用设置控制器(自 systemsetting AppSettingController + SysController 迁移,1:1 对齐)。
*
* <p>与原版差异点:写操作(应用 CRUD / UpdateResDate / 模板上传保存)校验调用者
* 为超管(type==1)或校管理员(roleId∈{2,5,7})——原版任何登录用户可写,因新入口仅管理员可见,收紧。
* 查询(分页列表 / FindAppCategory / GetTempList / downloadTemp)登录即可。
*/
@RestController
@RequestMapping(Config.API_PATH)
public class AppSettingController {
@Value("${FILE_PATH:./upload/}")
private String FILE_PATH;
@Value("${FILE_WEB_PATH:}")
private String FILE_WEB_PATH;
@Autowired
private IAppCategoryService appCategoryService;
@Autowired
private IUserCurrentRoleService userCurrentRoleService;
// ==================== 应用类别 CRUD ====================
/** 新增/编辑应用(JSON body,1:1 对齐原 SaveAppCategory) */
@PostMapping(value = "SaveAppCategory", produces = "application/json")
public void saveAppCategory(BoaUser authUserWithDept, @RequestBody AppCategory appCategory) {
checkAdmin(authUserWithDept);
if (appCategory.getCDate() == null) appCategory.setCDate(new Date());
if (appCategory.getResDate() == null) appCategory.setResDate(new Date());
appCategory.setCId(authUserWithDept.getId());
appCategoryService.save(appCategory);
}
@PostMapping(value = "DelAppCategory", produces = "application/json")
public void delAppCategory(BoaUser authUserWithDept, Integer id) {
checkAdmin(authUserWithDept);
appCategoryService.deleteById(id);
}
@GetMapping(value = "GetAppCategoryList")
public Map getAppCategoryList(BoaUser authUserWithDept, Integer page, Integer count,
@RequestParam(value = "roleType") Integer roleType,
@RequestParam(value = "name", required = false) String name) {
Page<AppCategory> data = appCategoryService.findPageList(page, count, roleType, name);
Map map = new HashMap();
map.put("total", data.getTotalElements());
map.put("list", data.getContent());
return map;
}
@GetMapping(value = "FindAppCategory")
public Optional<AppCategory> findAppCategory(BoaUser authUserWithDept, Integer id) {
return appCategoryService.findById(id);
}
@PostMapping(value = "UpdateResDate", produces = "application/json")
public void updateResDate(BoaUser authUserWithDept, Integer id) {
checkAdmin(authUserWithDept);
appCategoryService.updateResDate(id);
}
// ==================== 模板管理(对齐原 SysController) ====================
/** 上传模板压缩包(zip/rar),解压覆盖到 tempUpload/&lt;appModId&gt;/,删除压缩包 */
@PostMapping(value = "tempUpload", produces = "application/json")
public void tempUpload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file, Integer appModId) {
checkAdmin(authUserWithDept);
if (file == null || file.isEmpty()) {
throw new BizException(ErrorCode.CUSTOM, "请选择上传文件");
}
String originalName = file.getOriginalFilename();
String ext = originalName == null ? "" : originalName.substring(originalName.lastIndexOf(".")).toLowerCase(Locale.ROOT);
if (!".zip".equals(ext) && !".rar".equals(ext)) {
throw new BizException(ErrorCode.CUSTOM, "仅支持 zip/rar 格式");
}
// 不走图片 5MB 校验(模板包允许更大,依赖 multipart 配置),存到 tempUpload 子目录
String uploadPath = FILE_PATH + "/tempUpload/";
File fl = new File(uploadPath);
if (!fl.exists()) fl.mkdirs();
String fileName = FileUtil.uploadFile(file, uploadPath);
String filePath = "/upload/tempUpload/" + fileName;
File unzipFiles = new File(FILE_PATH + "/tempUpload/" + appModId);
if (!unzipFiles.exists()) unzipFiles.mkdirs();
else FileUtil.delAllFile(unzipFiles.getPath());
File zipFile = new File(filePath.replace("/upload", FILE_PATH));
try {
FileUtil.zipDecompress(zipFile, unzipFiles);
} catch (Exception e) {
throw new BizException(ErrorCode.CUSTOM, e.getMessage());
} finally {
zipFile.delete();
}
}
/** 模板文件列表(name + 访问 path) */
@GetMapping(value = "GetTempList")
public List<Map> getTempList(BoaUser authUserWithDept, Integer appModId) {
File sourceFile = new File(FILE_PATH + "/tempUpload/" + appModId);
List<Map> result = new ArrayList<>();
if (sourceFile.exists() && sourceFile.isDirectory() && sourceFile.listFiles() != null) {
Arrays.stream(sourceFile.listFiles()).forEach(file -> {
Map map = new HashMap();
map.put("name", file.getName());
map.put("path", FILE_WEB_PATH + "/tempUpload/" + appModId + "/" + file.getName());
result.add(map);
});
}
return result;
}
/** 保存模板文件内容(新增 fileName 路径穿越校验,原版没有;content 走 JSON body 避免 URL 长度限制) */
@PostMapping(value = "SaveTempFile", produces = "application/json")
public void saveTempFile(BoaUser authUserWithDept, @RequestBody Map<String, Object> body) {
checkAdmin(authUserWithDept);
Object appModIdObj = body.get("appModId");
String fileName = body.get("fileName") == null ? null : body.get("fileName").toString();
String content = body.get("content") == null ? "" : body.get("content").toString();
if (appModIdObj == null) {
throw new BizException(ErrorCode.CUSTOM, "缺少appModId");
}
Integer appModId;
try {
appModId = Integer.parseInt(appModIdObj.toString());
} catch (NumberFormatException e) {
throw new BizException(ErrorCode.CUSTOM, "非法appModId");
}
if (fileName == null || fileName.contains("/") || fileName.contains("\\") || fileName.contains("..")) {
throw new BizException(ErrorCode.CUSTOM, "非法文件名");
}
File dir = new File(FILE_PATH + "/tempUpload/" + appModId);
if (!dir.exists()) dir.mkdirs();
File file = new File(FILE_PATH + "/tempUpload/" + appModId + "/" + fileName);
try {
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
fos.write(content.getBytes());
fos.flush();
fos.close();
} catch (IOException e) {
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "保存失败");
}
}
/** 打包下载模板目录(zip 流式下载,临时 zip 关闭即删) */
@GetMapping(value = "downloadTemp", produces = "application/json;charset=UTF-8")
public ResponseEntity<InputStreamResource> downloadTemp(BoaUser authUserWithDept, Integer appModId, String name) {
File zipFilePath = new File(FILE_PATH + "/tempUpload/" + appModId);
if (!zipFilePath.exists() || !zipFilePath.isDirectory()) {
throw new BizException(ErrorCode.CUSTOM, "模板不存在");
}
String zipName = UUID.randomUUID().toString().substring(0, 8) + ".zip";
File zipFile = new File(FILE_PATH + "/tempUpload/" + zipName);
try {
FileUtil.zipCompress(zipFilePath, zipFile);
} catch (Exception e) {
throw new BizException(ErrorCode.CUSTOM, e.getMessage());
}
String downloadName = (name == null || name.trim().isEmpty()) ? "template" : name;
try {
InputStream inputStream = new FileInputStream(zipFile) {
@Override
public void close() throws IOException {
super.close();
zipFile.delete();
}
};
HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add("Content-Disposition",
"attachment; filename=" + new String((downloadName + ".zip").getBytes("gbk"), "iso8859-1"));
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
return ResponseEntity.ok().headers(headers)
.contentType(MediaType.parseMediaType("application/octet-stream"))
.body(new InputStreamResource(inputStream));
} catch (IOException e) {
zipFile.delete();
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "下载失败");
}
}
// ==================== 私有方法 ====================
/**
* 写操作权限校验:超管(type==1) 或 校管理员(roleId∈{2,5,7}),其余 403。
*/
private void checkAdmin(BoaUser authUserWithDept) {
if (authUserWithDept.getType() == 1) {
return;
}
Optional<UserCurrentRole> currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId());
if (!currentRoleOpt.isPresent() || !OtherUtil.isAdminById(currentRoleOpt.get().getRoleId())) {
throw new BizException(ErrorCode.FORBIDDEN);
}
}
}
@@ -16,8 +16,8 @@ import java.util.Optional;
/**
* 学校设置接口(CAS 登录态,路径 {@code /{platform}/}),对齐旧 UserHome SchSettingsController。
* <p>getSchSettings 实际跨仓调用 schConfig(见 SchSettingsServiceImpl);
* saveSchTheme/saveSchLogo(P1)本期写入本地 sch_settings 表(跨仓回写可后续对接)。 </p>
* <p>getSchSettings 查本地 sch_settings 表(不再依赖跨仓 schConfig);
* saveSchTheme/saveSchLogo 写本地 sch_settings 表。</p>
*/
@RestController
@RequestMapping(value = Config.API_PATH)
@@ -1,13 +1,12 @@
package com.yunhai.userhome.controller.api;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.utils.enums.ClientEnum;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaApp;
import com.yunhai.platform.utils.model.BoaAppType;
import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userhome.config.Config;
import com.yunhai.userhome.entity.*;
@@ -23,7 +22,6 @@ import com.yunhai.userhome.utils.OtherUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
@@ -36,7 +34,6 @@ import java.util.stream.Collectors;
* <p>待确认接口(已写入待确认清单):
* <ul>
* <li>getUserSettings 中 bschBaseOpenApi.getPcGrzxPage —— 新框架 YschBaseOpenApi 是否有此方法待确认</li>
* <li>GetSchSettingsWallpaper 依赖 SCH_CONF_URL —— 需确认环境地址</li>
* </ul>
*/
@RestController
@@ -49,9 +46,6 @@ public class UserSettingsController {
@Value("${FILE_WEB_PATH:}")
private String FILE_WEB_PATH;
@Value("${SCH_CONF_URL:}")
private String SCH_CONF_URL;
@Autowired
private IUserSettingsService userSettingsService;
@@ -61,21 +55,38 @@ public class UserSettingsController {
@Autowired
private UsedAppService usedAppService;
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
// ==================== 壁纸 ====================
/**
* 上传壁纸(Header 弹窗与系统设置页共用)。
*
* <p>超管:可选 schId 参数——传 schId&gt;0 归属该校(type=1),不传/0 维持系统级(type=0);
* 校管理员:忽略入参固定本校(type=1);普通用户维持个人壁纸(type=2)。</p>
*/
@PostMapping(value = "wallpaperUpload", produces = "application/json")
public void wallpaperUpload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file) {
public void wallpaperUpload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file,
@RequestParam(value = "schId", required = false) Integer schId) {
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;
Integer type;
Integer targetSchId;
if (authUserWithDept.getType() == 1) {
if (schId != null && schId > 0) {
type = 1;
targetSchId = schId;
} else {
// 超管上传的系统壁纸 schId 强制 0,避免切学校身份后产生孤儿数据
type = 0;
targetSchId = 0;
}
} else {
type = OtherUtil.isAdminById(currentRole.getRoleId()) ? 1 : 2;
targetSchId = currentRole.getSchId();
}
Wallpaper wallpaper = new Wallpaper();
// 超管上传的系统壁纸 schId 强制 0,避免切学校身份后产生孤儿数据
wallpaper.setSchId(type == 0 ? 0 : currentRole.getSchId());
wallpaper.setSchId(targetSchId);
wallpaper.setWallpaperUrl(uploadUrl);
wallpaper.setUserId(authUserWithDept.getId());
wallpaper.setType(type);
@@ -98,16 +109,11 @@ public class UserSettingsController {
@PostMapping(value = "checkWallpaper", produces = "application/json")
public void checkWallpaper(BoaUser authUserWithDept, @RequestBody Map res) {
String wallpaperUrl = res.get("wallpaperUrl").toString();
// 校验 URL 属于该用户可见壁纸集合,防止设置任意字符串
// 校验 URL 属于该用户可见壁纸集合(本地 wallpaper 表),防止设置任意字符串
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, "壁纸不存在或无权使用");
}
@@ -127,35 +133,25 @@ public class UserSettingsController {
map.put("isEdit", model.getIsEdit());
result.add(map);
});
if (authUserWithDept.getType() != 1 && !list.stream().anyMatch(x -> x.getType() == 1)) {
// 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
public void logoUpload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file,
@RequestParam(value = "schId", required = false) Integer schId) {
// 仅超管(系统默认/指定学校)与校管理员(本校)可上传,普通用户无权设置 Logo
Integer type;
Integer schId;
Integer targetSchId;
if (authUserWithDept.getType() == 1) {
type = 0;
schId = 0;
if (schId != null && schId > 0) {
type = 1;
targetSchId = schId;
} else {
type = 0;
targetSchId = 0;
}
} else {
UserCurrentRole currentRole = userCurrentRoleService.findById(authUserWithDept.getId())
.orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR));
@@ -163,13 +159,13 @@ public class UserSettingsController {
throw new BizException(ErrorCode.FORBIDDEN, "无权设置Logo");
}
type = 1;
schId = currentRole.getSchId();
targetSchId = currentRole.getSchId();
}
checkFile(file);
String uploadUrl = this.upload(file, "logo");
Logo logo = new Logo();
logo.setType(type);
logo.setSchId(schId);
logo.setSchId(targetSchId);
logo.setLogoUrl(uploadUrl);
logo.setUserId(authUserWithDept.getId());
userSettingsService.saveLogo(logo);
@@ -195,6 +191,82 @@ public class UserSettingsController {
userSettingsService.deleteLogoById(id, authUserWithDept);
}
// ==================== 系统设置(学校维度管理视图) ====================
/**
* 机构树(仅超管,403 兜底):YschBaseOpenApi.getGroupTree() → 递归裁剪到学校层
* (对齐原 systemsetting SysController.RecursionGroup:type==1 普通组跳过、type==2 学校组置叶子)。
*/
@GetMapping(value = "getSchTree")
public BoaGroup getSchTree(BoaUser authUserWithDept) {
if (authUserWithDept.getType() != 1) {
throw new BizException(ErrorCode.FORBIDDEN);
}
BoaGroup group = YschBaseOpenApi.getGroupTree();
return recursionGroup(group);
}
/**
* 壁纸管理列表(系统设置页):系统壁纸(type=0) + 目标学校壁纸(type=1),不含个人壁纸(type=2)。
* <p>超管可传任意 schId(不传按 0 即平台默认);校管理员忽略入参固定本校,系统壁纸仅超管可删。</p>
*/
@GetMapping(value = "getSchWallpaperManageList")
public List<Map> getSchWallpaperManageList(BoaUser authUserWithDept, Integer schId) {
List<Map> result = new ArrayList<>();
userSettingsService.findSchWallpaperManageList(authUserWithDept, schId).forEach(model -> {
Map map = new HashMap();
map.put("id", model.getId());
map.put("url", FILE_WEB_PATH + model.getWallpaperUrl().replace("/upload", ""));
map.put("type", model.getType());
map.put("isEdit", model.getIsEdit());
result.add(map);
});
return result;
}
/**
* Logo 管理列表(系统设置页):系统默认(type=0) + 目标学校(type=1)。
* <p>同 getSchWallpaperManageList 权限语义;logo 单张,前端取第一张显示,上传即覆盖。</p>
*/
@GetMapping(value = "getSchLogoManageList")
public List<Map> getSchLogoManageList(BoaUser authUserWithDept, Integer schId) {
List<Map> result = new ArrayList<>();
userSettingsService.findSchLogoManageList(authUserWithDept, schId).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;
}
/**
* 递归裁剪机构树到学校层(对齐原 systemsetting SysController.RecursionGroup)。
*/
private BoaGroup recursionGroup(BoaGroup group) {
// 组类型:1 普通组 2 学校组 3 年级组 4 班级组 5 区域组 6 学段组 7 教师组 8 学生组
List<BoaGroup> list = new ArrayList<>();
List<BoaGroup> children = group.getChildren();
if (children == null) {
return group;
}
for (BoaGroup boaGroup : children) {
if (boaGroup.getType() == null) continue;
if (boaGroup.getType() == 1) continue;
if (boaGroup.getType() == 2) {
boaGroup.setChildren(null);
list.add(boaGroup);
} else {
BoaGroup model = this.recursionGroup(boaGroup);
list.add(model);
}
}
group.setChildren(list);
return group;
}
// ==================== 用户设置 ====================
@GetMapping(value = "getUserSettings")
@@ -207,9 +279,8 @@ public class UserSettingsController {
// List<BoaApp> appPageList = bschBaseOpenApi.getPcGrzxPage(authUserWithDept.getId(), schId);
List<BoaApp> appPageList = Collections.emptyList();
if (optional.isPresent()) model = optional.get();
if ((model.getWallpaperUrl() == null || model.getWallpaperUrl().isEmpty()) && schId != null) {
List<String> schList = getSchSettingsWallpaper(schId);
if (schList.size() > 0) model.setWallpaperUrl(schList.get(0));
if (model.getWallpaperUrl() == null || model.getWallpaperUrl().isEmpty()) {
model.setWallpaperUrl(resolveFallbackWallpaper(authUserWithDept, schId));
}
List<Map> mapList = appPageList.stream().map(boaApp -> {
Map map = new HashMap<>();
@@ -224,6 +295,32 @@ public class UserSettingsController {
return model;
}
/**
* 兜底壁纸(用户未设置时):本地学校壁纸第一张 → 超管全局壁纸第一张。
* <p>学校壁纸完全本地管理(wallpaper 表 type=1),不再依赖跨仓 schConfig。</p>
*/
private String resolveFallbackWallpaper(BoaUser authUserWithDept, Integer schId) {
List<Wallpaper> wallList = userSettingsService.findWallList(authUserWithDept.getId());
if (schId != null) {
// 1. 本地学校壁纸第一张(type=1,取最新)
Optional<Wallpaper> schWall = wallList.stream()
.filter(w -> w.getType() != null && w.getType() == 1
&& w.getSchId() != null && w.getSchId().equals(schId))
.max(Comparator.comparing(Wallpaper::getId));
if (schWall.isPresent()) {
return FILE_WEB_PATH + schWall.get().getWallpaperUrl().replace("/upload", "");
}
}
// 2. 超管全局壁纸第一张(type=0,取最新)
Optional<Wallpaper> sysWall = wallList.stream()
.filter(w -> w.getType() != null && w.getType() == 0)
.max(Comparator.comparing(Wallpaper::getId));
if (sysWall.isPresent()) {
return FILE_WEB_PATH + sysWall.get().getWallpaperUrl().replace("/upload", "");
}
return null;
}
@GetMapping(value = "getUserDesktopAppList")
public List<Map> getUserDesktopAppList(BoaUser authUserWithDept) {
Optional<UserCurrentRole> currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId());
@@ -465,37 +562,6 @@ public class UserSettingsController {
return u;
}
/**
* 获取学校设置的壁纸集合(对齐旧 UserHome GetSchSettingsWallpaper,fastjson → Jackson)。
*/
private List<String> getSchSettingsWallpaper(Integer schId) {
if (SCH_CONF_URL == null || SCH_CONF_URL.isEmpty()) return Collections.emptyList();
String apiPath = SCH_CONF_URL + "/oapi/getSchSetting?schId=" + schId;
List<String> result = new ArrayList<>();
try {
String resultStr = restTemplate.getForObject(apiPath, String.class);
if (resultStr == null || resultStr.isEmpty()) return result;
JsonNode root = objectMapper.readTree(resultStr);
JsonNode dataNode = root.get("data");
if (root.has("code") && "0".equals(root.get("code").asText()) && dataNode != null && !dataNode.isNull()) {
JsonNode appBgUrlNode = dataNode.get("appBackgroundUrl");
if (appBgUrlNode == null) appBgUrlNode = dataNode.get("appBgUrl");
String appBgUrlStr = appBgUrlNode != null ? appBgUrlNode.asText("") : "";
if (appBgUrlStr.isEmpty()) return result;
String[] imgUrls = appBgUrlStr.split(",");
for (String imgUrl : imgUrls) {
if (!imgUrl.contains(SCH_CONF_URL))
result.add(SCH_CONF_URL + imgUrl);
else
result.add(imgUrl);
}
}
} catch (Exception e) {
// 跨仓调用失败时返回空列表
}
return result;
}
/**
* 将某个人常用应用转化为桌面应用
*/
@@ -0,0 +1,60 @@
package com.yunhai.userhome.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
* 桌面应用类别(自 systemsetting.app_category 1:1 迁移)。
*
* <p>roleType:1 校管理员 | 2 教师 | 3 学生(见 {@code enums.RoleType});
* resDate 为资源更新时间戳,前端上传模板后经 UpdateResDate 刷新缓存。</p>
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table(name = "app_category")
public class AppCategory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@NotNull(groups = Update.class)
private Integer id;
private String name;
@Column(name = "api_url", length = 500)
private String apiUrl;
@Column(name = "data_key")
private String dataKey;
/** 接口参数 JSON 数组字符串([{k,v,type}]) */
@Column(name = "params_key", columnDefinition = "TEXT")
private String paramsKey;
@Column(name = "role_type")
private Integer roleType;
@Column(name = "c_id")
private Integer cId;
@Column(name = "c_date")
private Date cDate = new Date();
/** 资源更新时间戳 */
@Column(name = "res_date")
private Date resDate = new Date();
public @interface Update {}
}
@@ -13,8 +13,8 @@ import javax.persistence.Table;
/**
* 学校设置(sch_settings 表,字段名/表名对齐旧 UserHome model.SchSettings)。
* <p>注意:旧版 {@code SchSettingsService.getSchSettings} 并不查本地表,而是跨仓调用
* {@code SCH_CONF_URL/oapi/getSchSetting},返回结构经 fastjson 映射为此实体。本工程保持同一行为。</p>
* <p>本工程学校设置完全本地化:saveSchTheme/saveSchLogo 写入本地表,getSchSettings 直接查本地表,
* 不再依赖旧版跨仓 {@code SCH_CONF_URL/oapi/getSchSetting}。</p>
*/
@AllArgsConstructor
@NoArgsConstructor
@@ -0,0 +1,22 @@
package com.yunhai.userhome.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 桌面应用角色枚举(替代原 GetBaseDict 字典接口,前端本地枚举)。
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum RoleType {
SCHOOL_ADMIN(1, "校管理员"),
TEACHER(2, "教师"),
STUDENT(3, "学生"),
;
private Integer key;
private String value;
}
@@ -0,0 +1,12 @@
package com.yunhai.userhome.repository;
import com.yunhai.userhome.entity.AppCategory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.util.List;
public interface AppCategoryRepository extends JpaRepository<AppCategory, Integer>, JpaSpecificationExecutor<AppCategory> {
List<AppCategory> findAllByRoleType(Integer roleType);
}
@@ -0,0 +1,22 @@
package com.yunhai.userhome.service;
import com.yunhai.userhome.entity.AppCategory;
import org.springframework.data.domain.Page;
import java.util.List;
import java.util.Optional;
public interface IAppCategoryService {
void save(AppCategory appCategory);
void deleteById(Integer id);
Optional<AppCategory> findById(Integer id);
List<AppCategory> findByRoleType(Integer roleType);
Page<AppCategory> findPageList(Integer page, Integer count, Integer roleType, String name);
void updateResDate(Integer id);
}
@@ -14,6 +14,12 @@ public interface IUserSettingsService {
List<Wallpaper> findWallList(Integer userId);
/** 管理视角壁纸列表(系统设置页):系统 + 目标学校,不含个人壁纸;isEdit 按调用者权限标记 */
List<Wallpaper> findSchWallpaperManageList(BoaUser user, Integer targetSchId);
/** 管理视角 Logo 列表(系统设置页):系统 + 目标学校 */
List<Logo> findSchLogoManageList(BoaUser user, Integer targetSchId);
void deleteWallById(Integer id, BoaUser user);
void saveWallpaper(Wallpaper wallpaper);
@@ -0,0 +1,82 @@
package com.yunhai.userhome.service.impl;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.userhome.entity.AppCategory;
import com.yunhai.userhome.repository.AppCategoryRepository;
import com.yunhai.userhome.service.IAppCategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
/**
* 应用类别服务(自 systemsetting AppCategoryService 1:1 迁移,GetAppCategorySel/getTimestampByIds 无消费方不迁)。
*/
@Service
public class AppCategoryServiceImpl implements IAppCategoryService {
@Autowired
private AppCategoryRepository appCategoryRepository;
@Override
public void save(AppCategory appCategory) {
appCategoryRepository.save(appCategory);
}
@Override
public void deleteById(Integer id) {
if (!appCategoryRepository.existsById(id)) {
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR);
}
appCategoryRepository.deleteById(id);
}
@Override
public Optional<AppCategory> findById(Integer id) {
return appCategoryRepository.findById(id);
}
@Override
public List<AppCategory> findByRoleType(Integer roleType) {
return appCategoryRepository.findAllByRoleType(roleType);
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public Page<AppCategory> findPageList(Integer page, Integer count, Integer roleType, String name) {
Sort sort = Sort.by(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(page - 1, count, sort);
Specification specification = (Specification) (root, criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if (roleType != null) {
predicates.add(criteriaBuilder.equal(root.get("roleType"), roleType));
}
if (name != null && !name.trim().isEmpty()) {
predicates.add(criteriaBuilder.like(root.get("name"), "%" + name + "%"));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
};
return appCategoryRepository.findAll(specification, pageable);
}
@Override
public void updateResDate(Integer id) {
Optional<AppCategory> optionalAppCategory = findById(id);
if (!optionalAppCategory.isPresent()) {
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR);
}
AppCategory model = optionalAppCategory.get();
model.setResDate(new Date());
appCategoryRepository.save(model);
}
}
@@ -1,22 +1,16 @@
package com.yunhai.userhome.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.userhome.entity.SchSettings;
import com.yunhai.userhome.repository.SchSettingsRepository;
import com.yunhai.userhome.service.ISchSettingsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Optional;
/**
* 学校设置服务:行为对齐旧 UserHome SchSettingsService。
* <p>关键:{@code findBySchId} 不查本地表,而是跨仓调用 {@code SCH_CONF_URL/oapi/getSchSetting},
* 旧版如此(schConfig 服务为 homeImgUrl/isEnableDesktop/isLinkDesktop 等字段的权威来源),
* 因此本地 sch_settings 表变更类能力(saveSchTheme/saveSchLogo)本期仅保留 P1 占位写入本地表。</p>
* 学校设置服务:学校设置完全本地化(sch_settings 表),不再依赖跨仓 schConfig。
* <p>findBySchId 直接查本地表,查不到兜底默认主题;saveSchTheme/saveSchLogo 写本地表即可闭环。</p>
*/
@Service
public class SchSettingsServiceImpl implements ISchSettingsService {
@@ -24,12 +18,6 @@ public class SchSettingsServiceImpl implements ISchSettingsService {
@Autowired
private SchSettingsRepository schSettingsRepository;
@Value("${SCH_CONF_URL:}")
private String schConfUrl;
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void save(SchSettings schSettings) {
schSettingsRepository.save(schSettings);
@@ -37,41 +25,16 @@ public class SchSettingsServiceImpl implements ISchSettingsService {
@Override
public Optional<SchSettings> findBySchId(Integer schId) {
SchSettings model = new SchSettings();
try {
if (schConfUrl == null || schConfUrl.isEmpty()) {
return Optional.of(localFallbackBySchId(schId));
}
String apiPath = schConfUrl + "/oapi/getSchSetting?schId=" + schId;
String resultStr = restTemplate.getForObject(apiPath, String.class);
if (resultStr == null || resultStr.isEmpty()) {
return Optional.of(localFallbackBySchId(schId));
}
JsonNode root = objectMapper.readTree(resultStr);
JsonNode dataNode = root.get("data");
if (root.has("code") && "0".equals(root.get("code").asText()) && dataNode != null && !dataNode.isNull()) {
model = objectMapper.convertValue(dataNode, SchSettings.class);
} else {
return Optional.of(localFallbackBySchId(schId));
}
} catch (Exception e) {
return Optional.of(localFallbackBySchId(schId));
}
return Optional.of(model);
}
/** 跨仓不通时先查本地表,再兜底默认主题(对齐旧版异常分支)。 */
private SchSettings localFallbackBySchId(Integer schId) {
Optional<SchSettings> local = schSettingsRepository.findFirstBySchId(schId);
if (local.isPresent()) {
return local.get();
return local;
}
SchSettings def = new SchSettings();
def.setSchId(schId);
def.setThemeName("default-theme");
def.setAppBgUrl("");
def.setLogoUrl("");
return def;
return Optional.of(def);
}
@Override
@@ -73,6 +73,52 @@ public class UserSettingsServiceImpl implements IUserSettingsService {
return resultList;
}
@Override
public List<Wallpaper> findSchWallpaperManageList(BoaUser user, Integer targetSchId) {
UserCurrentRole currentRole = userCurrentRoleService.findById(user.getId())
.orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR));
boolean superAdmin = user.getType() == 1;
if (!superAdmin && !OtherUtil.isAdminById(currentRole.getRoleId())) {
throw new BizException(ErrorCode.FORBIDDEN);
}
Integer schId = superAdmin ? (targetSchId == null ? 0 : targetSchId) : currentRole.getSchId();
List<Wallpaper> resultList = new ArrayList<>();
List<Wallpaper> baseList = wallpaperRepository.findBySchIdAndType(0, 0);
List<Wallpaper> schList = wallpaperRepository.findBySchIdAndType(schId, 1);
if (superAdmin) {
baseList.forEach(x -> x.setIsEdit(true));
schList.forEach(x -> x.setIsEdit(true));
} else {
schList.forEach(x -> x.setIsEdit(true));
}
resultList.addAll(baseList);
resultList.addAll(schList);
return resultList;
}
@Override
public List<Logo> findSchLogoManageList(BoaUser user, Integer targetSchId) {
UserCurrentRole currentRole = userCurrentRoleService.findById(user.getId())
.orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR));
boolean superAdmin = user.getType() == 1;
if (!superAdmin && !OtherUtil.isAdminById(currentRole.getRoleId())) {
throw new BizException(ErrorCode.FORBIDDEN);
}
Integer schId = superAdmin ? (targetSchId == null ? 0 : targetSchId) : currentRole.getSchId();
List<Logo> resultList = new ArrayList<>();
List<Logo> baseList = logoRepository.findBySchIdAndType(0, 0);
List<Logo> schList = logoRepository.findBySchIdAndType(schId, 1);
if (superAdmin) {
baseList.forEach(x -> x.setIsEdit(true));
schList.forEach(x -> x.setIsEdit(true));
} else {
schList.forEach(x -> x.setIsEdit(true));
}
resultList.addAll(baseList);
resultList.addAll(schList);
return resultList;
}
@Override
@Transactional
public void deleteWallById(Integer id, BoaUser user) {
@@ -6,13 +6,20 @@ import com.yunhai.platform.utils.exception.ErrorCode;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/**
* File工具类(对齐旧 UserHome utils.FileUtil,仅保留 uploadImg 方法——本期迁移实际使用的唯一方法)。
@@ -95,4 +102,156 @@ public class FileUtil {
file.delete();
}
}
/**
* 通用文件上传(MultipartFile 版),保留原始扩展名,返回生成的文件名。
* <p>与 {@link #uploadImg(MultipartFile, String)} 的区别:不做图片扩展名白名单校验,
* 供模板上传(zip/rar)等非图片场景使用,扩展名合法性由调用方自行校验。</p>
*/
public static String uploadFile(MultipartFile file, String pathName) {
String imgName = null;
String path = null;
File floorFile = null;
try {
byte[] fileData = file.getBytes();
String fileName = file.getOriginalFilename();
int index = fileName == null ? -1 : fileName.lastIndexOf(".");
String ext = "";
if (index > 0) {
ext = fileName.substring(index).toLowerCase(Locale.ROOT);
}
for (int i = 0; i < 100; i++) {
imgName = UUID.randomUUID().toString().substring(0, 8) + ext;
path = pathName + imgName;
floorFile = new File(path);
if (floorFile.exists()) continue;
else break;
}
FileOutputStream fos = new FileOutputStream(floorFile);
fos.write(fileData);
fos.close();
} catch (IOException e) {
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "上传失败");
}
return imgName;
}
/**
* 解压缩 ZIP 文件到目标目录(覆盖同名文件,防 zip-slip 路径穿越)。
*
* @param zipFile 压缩包
* @param destDir 目标目录
*/
public static void zipDecompress(File zipFile, File destDir) {
byte[] buffer = new byte[1024];
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry entry = zis.getNextEntry();
while (entry != null) {
File file = new File(destDir, entry.getName());
String destPath = destDir.getCanonicalPath();
String childPath = file.getCanonicalPath();
if (!childPath.startsWith(destPath + File.separator)) {
throw new BizException(ErrorCode.CUSTOM, "压缩包内存在非法路径");
}
if (entry.isDirectory()) {
file.mkdirs();
} else {
File parent = file.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
try (FileOutputStream fos = new FileOutputStream(file)) {
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}
}
entry = zis.getNextEntry();
}
} catch (IOException e) {
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "解压失败");
}
}
/**
* 压缩文件/文件夹为 ZIP。
*
* @param sourceFile 被压缩文件/文件夹
* @param zipFile Zip 文件
*/
public static void zipCompress(File sourceFile, File zipFile) {
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
zos.setMethod(ZipOutputStream.DEFLATED);
zos.setLevel(Deflater.BEST_COMPRESSION);
if (sourceFile.exists() && sourceFile.isDirectory() && sourceFile.listFiles() != null) {
Arrays.stream(sourceFile.listFiles()).forEach(file -> addZipFile(file, zos));
} else {
addZipFile(sourceFile, zos);
}
} catch (IOException e) {
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "压缩失败");
}
}
/**
* 向 ZIP 中添加单个文件(zipCompress 辅助)。
*/
private static void addZipFile(File file, ZipOutputStream zos) {
if (!file.exists() || file.isDirectory()) {
throw new RuntimeException("文件不存在或该文件为文件夹,请检查");
}
try (FileInputStream fis = new FileInputStream(file)) {
ZipEntry entry = new ZipEntry(file.getName());
entry.setMethod(ZipEntry.DEFLATED);
entry.setSize(file.length());
entry.setCreationTime(FileTime.from(Instant.now()));
entry.setLastAccessTime(FileTime.from(Instant.now()));
entry.setLastModifiedTime(FileTime.from(Instant.now()));
zos.putNextEntry(entry);
byte[] buf = new byte[1024];
int len;
while ((len = fis.read(buf)) > 0) {
zos.write(buf, 0, len);
}
zos.closeEntry();
} catch (IOException e) {
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "压缩失败");
}
}
/**
* 删除文件夹(含内容)。
*/
public static void delFolder(String folderPath) {
delAllFile(folderPath);
new File(folderPath).delete();
}
/**
* 删除指定文件夹下所有文件(保留目录本身)。
*/
public static boolean delAllFile(String path) {
boolean flag = false;
File file = new File(path);
if (!file.exists() || !file.isDirectory()) {
return flag;
}
String[] tempList = file.list();
if (tempList == null) {
return flag;
}
for (String s : tempList) {
File temp = new File(path, s);
if (temp.isFile()) {
temp.delete();
}
if (temp.isDirectory()) {
delAllFile(temp.getPath());
delFolder(temp.getPath());
flag = true;
}
}
return flag;
}
}