init local code

This commit is contained in:
chenxing
2026-09-07 09:09:39 +08:00
commit 99cd26b3d2
259 changed files with 20554 additions and 0 deletions
@@ -0,0 +1,83 @@
package com.yunhai.cms.controller.api;
import com.yunhai.cms.service.impl.ThemeService;
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;
import org.springframework.http.MediaTypeFactory;
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.RestController;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.List;
/**
* 主题管理接口(方案 §5.2,路径对齐 /api/{domain} 约定):
* /api/theme/list 主题列表;/api/theme/{name}/asset/** 主题资产代理
* (screenshot 与预览资产共用,防穿越同 PortalAssetController,name 先过 Registry 白名单)。
* 走 /api/* CAS 鉴权(现状边界)。
*/
@RestController
@RequestMapping("/api/theme")
@RequiredArgsConstructor
public class ThemeController {
private static final Duration ASSET_CACHE = Duration.ofHours(1);
private final ThemeService themeService;
private final ThemeRegistry themeRegistry;
@GetMapping("/list")
public List<com.yunhai.cms.model.vo.ThemeVo> list() {
return themeService.list();
}
@GetMapping("/{name}/asset/**")
public ResponseEntity<Resource> asset(@PathVariable("name") String name,
HttpServletRequest request) {
if (!themeRegistry.contains(name)) {
return ResponseEntity.notFound().build();
}
String path = extractRelativePath(request, name);
if (path == null || path.isEmpty() || path.contains("..")) {
return ResponseEntity.badRequest().build();
}
Resource resource = new ClassPathResource("templates/themes/" + name + "/assets/" + path);
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
MediaType mediaType = MediaTypeFactory.getMediaType(path)
.orElse(MediaType.APPLICATION_OCTET_STREAM);
return ResponseEntity.ok()
.contentType(mediaType)
.cacheControl(CacheControl.maxAge(ASSET_CACHE).cachePublic())
.body(resource);
}
/**
* 从请求 URI 截取 /asset/ 之后的相对路径(同 PortalAssetController:
* 直接解析 URL-decode 后的 URI,兼容两种路径匹配器语义)。
*/
private String extractRelativePath(HttpServletRequest request, String name) {
String uri = request.getRequestURI();
String prefix = request.getContextPath() + "/api/theme/" + name + "/asset/";
int index = uri.indexOf(prefix);
if (index < 0) {
return null;
}
String relative = uri.substring(index + prefix.length());
try {
relative = java.net.URLDecoder.decode(relative, "UTF-8");
} catch (java.io.UnsupportedEncodingException e) {
return null;
}
return relative.replace('\\', '/');
}
}