feat(W1): 皮肤包完整管理-外部主题目录+热注册,zip上传/派生/编辑/删除,ThemePackageValidator收敛,Thymeleaf文件解析器,list增source/inUseCount/editable/usedBySiteNames,T-W1-01..20 全绿299/299
build / build-push (push) Successful in 52s

This commit is contained in:
chenxing
2026-09-08 14:52:29 +08:00
parent 536bd6cea4
commit 338ffb56dd
16 changed files with 1548 additions and 71 deletions
@@ -1,9 +1,10 @@
package com.yunhai.cms.controller.api;
import com.yunhai.cms.service.impl.ThemeService;
import com.yunhai.cms.theme.ThemeInfo;
import com.yunhai.cms.theme.ThemePackageService;
import com.yunhai.cms.theme.ThemeRegistry;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
@@ -12,7 +13,14 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.yunhai.platform.utils.model.BoaUser;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
@@ -33,12 +41,53 @@ public class ThemeController {
private final ThemeService themeService;
private final ThemeRegistry themeRegistry;
private final ThemePackageService themePackageService;
@GetMapping("/list")
public List<com.yunhai.cms.model.vo.ThemeVo> list() {
return themeService.list();
}
/** W1(G2):上传合规 zip——立即注册(免重启);不合规返回逐条校验报告,不落盘不注册 */
@PostMapping("/upload")
public com.yunhai.cms.model.vo.ThemeVo upload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file) {
ThemeInfo info = themePackageService.upload(authUserWithDept, file);
return themeService.voOf(info);
}
/** W1(G3):从现有包派生新皮肤(复制包体+改名,外部包可编辑可删除) */
@PostMapping("/derive")
public com.yunhai.cms.model.vo.ThemeVo derive(BoaUser authUserWithDept,
@RequestBody com.yunhai.cms.model.dto.ThemeDeriveRequest req) {
ThemeInfo info = themePackageService.derive(authUserWithDept, req.getSource(), req.getName());
return themeService.voOf(info);
}
/** W1(G4):编辑外部包 theme.json 的 title 与参数 schema(保存后引用站点立即生效) */
@PutMapping("/{name}/meta")
public com.yunhai.cms.model.vo.ThemeVo updateMeta(BoaUser authUserWithDept,
@PathVariable("name") String name,
@RequestBody com.yunhai.cms.model.dto.ThemeMetaRequest req) {
ThemeInfo info = themePackageService.updateMeta(authUserWithDept, name, req.getTitle(), req.getParams());
return themeService.voOf(info);
}
/** W1(G5):删除外部包(被用站点拒绝;内置不可删) */
@DeleteMapping("/{name}")
public java.util.Map<String, Object> remove(BoaUser authUserWithDept, @PathVariable("name") String name) {
themePackageService.remove(authUserWithDept, name);
java.util.Map<String, Object> result = new java.util.HashMap<>();
result.put("ok", true);
return result;
}
/** W1(G2/B4):热重载(运维直放目录后生效;default 缺失报错且旧集合不丢) */
@PostMapping("/reload")
public List<com.yunhai.cms.model.vo.ThemeVo> reload(BoaUser authUserWithDept) {
themePackageService.reload(authUserWithDept);
return themeService.list();
}
@GetMapping("/{name}/asset/**")
public ResponseEntity<Resource> asset(@PathVariable("name") String name,
HttpServletRequest request) {
@@ -49,7 +98,7 @@ public class ThemeController {
if (path == null || path.isEmpty() || path.contains("..")) {
return ResponseEntity.badRequest().build();
}
Resource resource = new ClassPathResource("templates/themes/" + name + "/assets/" + path);
Resource resource = themeRegistry.resource(name, "assets/" + path);
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
@@ -12,7 +12,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import javax.servlet.http.HttpServletRequest;
@@ -65,8 +64,8 @@ public class PortalAssetController {
if (path == null || path.isEmpty() || path.contains("..")) {
return ResponseEntity.badRequest().build();
}
Resource resource = new ClassPathResource(
"templates/themes/" + theme + "/assets/" + path);
// W1(T-W1-20):内置→ClassPathResource、外部→FileSystemResource,统一走 Registry 定位
Resource resource = themeRegistry.resource(theme, "assets/" + path);
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}