feat(W4): 院系子站嵌套路径-cms_site.path列+uk_site_path/parent_code,门户单段+嵌套双映射,旧链301,public_base_url派生,sitePath预览,SQL三处+升级脚本,T-W4-01..15 全绿279/279
This commit is contained in:
@@ -38,9 +38,10 @@ public class ThemePreviewController {
|
||||
private final PortalPageRenderer pageRenderer;
|
||||
private final SiteService siteService;
|
||||
|
||||
/** 预览首页(page 参数 P1 仅支持 home):no-store 防止进缓存/CDN(方案 §10 风险表) */
|
||||
/** 预览首页(page 参数 P1 仅支持 home):no-store 防止进缓存/CDN(方案 §10 风险表)。
|
||||
* W4:内容源站点参数 siteCode→sitePath(嵌套路径语义,siteSnapshotByPath 解析)。 */
|
||||
@GetMapping("/preview")
|
||||
public String preview(@RequestParam("siteCode") String siteCode,
|
||||
public String preview(@RequestParam("sitePath") String sitePath,
|
||||
@RequestParam("theme") String theme,
|
||||
@RequestParam(value = "page", required = false, defaultValue = "home") String page,
|
||||
BoaUser authUserWithDept,
|
||||
@@ -51,10 +52,10 @@ public class ThemePreviewController {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "不支持的预览页面:" + page + "(当前仅支持 home)");
|
||||
}
|
||||
ThemeInfo info = themeService.requireRegistered(theme);
|
||||
PortalSiteSnapshot site = portalQueryService.siteSnapshot(siteCode);
|
||||
PortalSiteSnapshot site = portalQueryService.siteSnapshotByPath(sitePath);
|
||||
if (site == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"内容源站点不存在或已停用:" + siteCode);
|
||||
"内容源站点不存在或已停用:" + sitePath);
|
||||
}
|
||||
// B-6②:预览渲染的是内容源站点数据快照,与管理端同权限级(requireManageable)
|
||||
if (authUserWithDept == null) {
|
||||
@@ -66,6 +67,6 @@ public class ThemePreviewController {
|
||||
Map<String, String> params = themeService.extractPreviewParams(info, request.getParameterMap());
|
||||
previewSite.setThemeParams(params);
|
||||
previewSite.setThemeVarsCss(ThemeVarsCss.build(info, params));
|
||||
return pageRenderer.renderHome(previewSite, PortalUrlHelper.forPreview(request, siteCode, theme), model);
|
||||
return pageRenderer.renderHome(previewSite, PortalUrlHelper.forPreview(request, sitePath, theme), model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,15 +40,28 @@ public class PortalAssetController {
|
||||
public ResponseEntity<Resource> asset(@PathVariable("siteCode") String siteCode,
|
||||
@PathVariable("theme") String theme,
|
||||
HttpServletRequest request) {
|
||||
return doAsset(siteCode, theme, request);
|
||||
}
|
||||
|
||||
/** W4 嵌套变体:/portal/{p}/{s}/assets/{theme}/**(层级固定两层) */
|
||||
@GetMapping("/portal/{p}/{s}/assets/{theme}/**")
|
||||
public ResponseEntity<Resource> assetNested(@PathVariable("p") String p,
|
||||
@PathVariable("s") String s,
|
||||
@PathVariable("theme") String theme,
|
||||
HttpServletRequest request) {
|
||||
return doAsset(p + "/" + s, theme, request);
|
||||
}
|
||||
|
||||
private ResponseEntity<Resource> doAsset(String sitePath, String theme, HttpServletRequest request) {
|
||||
// 主题段必须已注册(兼防路径注入:未注册名一律 404)
|
||||
if (!themeRegistry.contains(theme)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
PortalSiteSnapshot site = portalQueryService.siteSnapshot(siteCode);
|
||||
PortalSiteSnapshot site = portalQueryService.siteSnapshotByPath(sitePath);
|
||||
if (site == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String path = extractRelativePath(request, siteCode, theme);
|
||||
String path = extractRelativePath(request, sitePath, theme);
|
||||
if (path == null || path.isEmpty() || path.contains("..")) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
@@ -68,10 +81,11 @@ public class PortalAssetController {
|
||||
/**
|
||||
* 从请求 URI 截取 /assets/{theme}/ 之后的相对路径。
|
||||
* 直接解析 URI(URL-decode 后),兼容 AntPathMatcher 与 PathPatternParser 两种匹配器语义。
|
||||
* W4:前缀用完整 sitePath(嵌套形态含两层段)。
|
||||
*/
|
||||
private String extractRelativePath(HttpServletRequest request, String siteCode, String theme) {
|
||||
private String extractRelativePath(HttpServletRequest request, String sitePath, String theme) {
|
||||
String uri = request.getRequestURI();
|
||||
String prefix = request.getContextPath() + "/portal/" + siteCode + "/assets/" + theme + "/";
|
||||
String prefix = request.getContextPath() + "/portal/" + sitePath + "/assets/" + theme + "/";
|
||||
int index = uri.indexOf(prefix);
|
||||
if (index < 0) {
|
||||
return null;
|
||||
|
||||
@@ -8,7 +8,9 @@ import com.yunhai.cms.model.portal.PortalBanner;
|
||||
import com.yunhai.cms.model.portal.PortalCategory;
|
||||
import com.yunhai.cms.model.portal.PortalListData;
|
||||
import com.yunhai.cms.model.portal.PortalSiteSnapshot;
|
||||
import com.yunhai.cms.entity.CmsSite;
|
||||
import com.yunhai.cms.repository.CmsArticleRepository;
|
||||
import com.yunhai.cms.repository.CmsSiteRepository;
|
||||
import com.yunhai.cms.service.impl.PortalQueryService;
|
||||
import com.yunhai.cms.service.impl.ViewCounterService;
|
||||
import com.yunhai.cms.utils.PortalUrlHelper;
|
||||
@@ -48,6 +50,7 @@ public class PortalController {
|
||||
|
||||
private final PortalQueryService portalQueryService;
|
||||
private final CmsArticleRepository articleRepository;
|
||||
private final CmsSiteRepository siteRepository;
|
||||
private final ViewCounterService viewCounterService;
|
||||
private final PortalPageRenderer pageRenderer;
|
||||
|
||||
@@ -58,11 +61,25 @@ public class PortalController {
|
||||
@Value("${cms.portal.base-origin:}")
|
||||
private String baseOrigin;
|
||||
|
||||
/** 首页:站点配置 + home 轮播位 + 各一级栏目 top N + 置顶文章 + 友链分组(装配在 PortalPageRenderer,方案 §5.5) */
|
||||
/** 首页:站点配置 + home 轮播位 + 各一级栏目 top N + 置顶文章 + 友链分组(装配在 PortalPageRenderer,方案 §5.5)。
|
||||
* W4:单段/嵌套双映射收敛到 doHome(sitePath 为唯一身份)。 */
|
||||
@GetMapping("/portal/{siteCode}/")
|
||||
public String home(@PathVariable("siteCode") String siteCode, HttpServletRequest request,
|
||||
HttpServletResponse response, Model model) throws Exception {
|
||||
PortalSiteSnapshot site = requireSite(siteCode, response);
|
||||
return doHome(siteCode, request, response, model);
|
||||
}
|
||||
|
||||
/** W4 嵌套变体:/portal/{p}/{s}/(层级固定两层) */
|
||||
@GetMapping("/portal/{p}/{s}/")
|
||||
public String homeNested(@PathVariable("p") String p, @PathVariable("s") String s,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
return doHome(p + "/" + s, request, response, model);
|
||||
}
|
||||
|
||||
private String doHome(String sitePath, HttpServletRequest request,
|
||||
HttpServletResponse response, Model model) throws Exception {
|
||||
PortalSiteSnapshot site = requireSiteByPath(sitePath, "", request, response);
|
||||
if (site == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -76,7 +93,23 @@ public class PortalController {
|
||||
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
PortalSiteSnapshot site = requireSite(siteCode, response);
|
||||
return doCategory(siteCode, categoryId, page, request, response, model);
|
||||
}
|
||||
|
||||
/** W4 嵌套变体:/portal/{p}/{s}/c/{categoryId} */
|
||||
@GetMapping("/portal/{p}/{s}/c/{categoryId}")
|
||||
public String categoryNested(@PathVariable("p") String p, @PathVariable("s") String s,
|
||||
@PathVariable("categoryId") Integer categoryId,
|
||||
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
return doCategory(p + "/" + s, categoryId, page, request, response, model);
|
||||
}
|
||||
|
||||
private String doCategory(String sitePath, Integer categoryId, Integer page,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
PortalSiteSnapshot site = requireSiteByPath(sitePath, "c/" + categoryId, request, response);
|
||||
if (site == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -126,7 +159,22 @@ public class PortalController {
|
||||
@PathVariable("articleId") Integer articleId,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
PortalSiteSnapshot site = requireSite(siteCode, response);
|
||||
return doArticle(siteCode, articleId, request, response, model);
|
||||
}
|
||||
|
||||
/** W4 嵌套变体:/portal/{p}/{s}/a/{articleId} */
|
||||
@GetMapping("/portal/{p}/{s}/a/{articleId}")
|
||||
public String articleNested(@PathVariable("p") String p, @PathVariable("s") String s,
|
||||
@PathVariable("articleId") Integer articleId,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
return doArticle(p + "/" + s, articleId, request, response, model);
|
||||
}
|
||||
|
||||
private String doArticle(String sitePath, Integer articleId,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
PortalSiteSnapshot site = requireSiteByPath(sitePath, "a/" + articleId, request, response);
|
||||
if (site == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -172,7 +220,23 @@ public class PortalController {
|
||||
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
PortalSiteSnapshot site = requireSite(siteCode, response);
|
||||
return doSearch(siteCode, q, page, request, response, model);
|
||||
}
|
||||
|
||||
/** W4 嵌套变体:/portal/{p}/{s}/search */
|
||||
@GetMapping("/portal/{p}/{s}/search")
|
||||
public String searchNested(@PathVariable("p") String p, @PathVariable("s") String s,
|
||||
@RequestParam(value = "q", required = false, defaultValue = "") String q,
|
||||
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
return doSearch(p + "/" + s, q, page, request, response, model);
|
||||
}
|
||||
|
||||
private String doSearch(String sitePath, String q, Integer page,
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
Model model) throws Exception {
|
||||
PortalSiteSnapshot site = requireSiteByPath(sitePath, "search", request, response);
|
||||
if (site == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -208,30 +272,57 @@ public class PortalController {
|
||||
|
||||
// ===== 内部装配 =====
|
||||
|
||||
private PortalSiteSnapshot requireSite(String siteCode, HttpServletResponse response) throws Exception {
|
||||
PortalSiteSnapshot site = portalQueryService.siteSnapshot(siteCode);
|
||||
if (site == null) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return null;
|
||||
/**
|
||||
* 站点身份解析(W4):优先按门户 path(siteSnapshotByPath);
|
||||
* 未命中且为单段旧链时按 code 回查(D3:已发出的院系列表链接/收藏不断)——
|
||||
* 命中子站(path≠code)则 301 到嵌套形态(保留子路径 subPath 与 query)后返回 null;
|
||||
* 仍未命中 404。
|
||||
*/
|
||||
private PortalSiteSnapshot requireSiteByPath(String sitePath, String subPath,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) throws Exception {
|
||||
PortalSiteSnapshot site = portalQueryService.siteSnapshotByPath(sitePath);
|
||||
if (site != null) {
|
||||
return site;
|
||||
}
|
||||
return site;
|
||||
if (sitePath != null && !sitePath.contains("/")) {
|
||||
CmsSite legacy = siteRepository.findByCode(sitePath).orElse(null);
|
||||
if (legacy != null && legacy.getPath() != null && !legacy.getPath().isEmpty()
|
||||
&& !legacy.getPath().equals(sitePath)) {
|
||||
StringBuilder location = new StringBuilder(request.getContextPath())
|
||||
.append("/portal/").append(legacy.getPath()).append('/');
|
||||
if (subPath != null && !subPath.isEmpty()) {
|
||||
location.append(subPath);
|
||||
}
|
||||
String query = request.getQueryString();
|
||||
if (query != null && !query.isEmpty()) {
|
||||
location.append('?').append(query);
|
||||
}
|
||||
response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
|
||||
response.setHeader("Location", location.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 生成器基准三级决策(方案 §8.6):
|
||||
* ① 站点 public_base_url(绑学校域名);
|
||||
* ① 站点 public_base_url(绑学校域名;W4 起子站可为 D4 派生值 {主站base}/{code});
|
||||
* ② 平台 cms.portal.base-origin(未绑域时的 base 域规范形态,C-6);
|
||||
* ③ 当前请求派生(本地/预览形态,反代下经 ForwardedHeaderFilter 修正 scheme/port,B-2)。
|
||||
* theme 随 helper 走,使资产 URL 含主题段(B-3)。
|
||||
* W4:构造参语义 code→sitePath——门户地址全站统一为 /portal/{path}/**。
|
||||
*/
|
||||
private PortalUrlHelper buildUrl(PortalSiteSnapshot site, HttpServletRequest request) {
|
||||
if (site.getPublicBaseUrl() != null) {
|
||||
return new PortalUrlHelper(site.getPublicBaseUrl(), site.getCode(), site.getTheme(), baseOrigin);
|
||||
return new PortalUrlHelper(site.getPublicBaseUrl(), site.getPath(), site.getTheme(), baseOrigin);
|
||||
}
|
||||
if (baseOrigin != null && !baseOrigin.trim().isEmpty()) {
|
||||
return PortalUrlHelper.fromBaseOrigin(baseOrigin, site.getCode(), site.getTheme());
|
||||
return PortalUrlHelper.fromBaseOrigin(baseOrigin, site.getPath(), site.getTheme());
|
||||
}
|
||||
return PortalUrlHelper.fromRequest(request, site.getCode(), site.getTheme());
|
||||
return PortalUrlHelper.fromRequest(request, site.getPath(), site.getTheme());
|
||||
}
|
||||
|
||||
/** 全树递归查找栏目(管理端允许 3 级以上栏目,门户不得因层级深而 404,bug D-1) */
|
||||
|
||||
@@ -32,6 +32,6 @@ public class PortalIndexController {
|
||||
.filter(s -> s.getParentSiteId() == null || s.getParentSiteId() == 0)
|
||||
.findFirst()
|
||||
.orElse(sites.get(0));
|
||||
response.sendRedirect(request.getContextPath() + "/portal/" + target.getCode() + "/");
|
||||
response.sendRedirect(request.getContextPath() + "/portal/" + target.getPath() + "/");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user