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:
@@ -19,6 +19,7 @@ import com.yunhai.cms.repository.CmsBannerRepository;
|
||||
import com.yunhai.cms.repository.CmsCategoryRepository;
|
||||
import com.yunhai.cms.repository.CmsLinkRepository;
|
||||
import com.yunhai.cms.repository.CmsSiteRepository;
|
||||
import com.yunhai.cms.utils.ExtJsonUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
@@ -54,11 +55,28 @@ public class PortalQueryService {
|
||||
@Cacheable(cacheNames = CacheNames.SITE, key = "#siteCode")
|
||||
public PortalSiteSnapshot siteSnapshot(String siteCode) {
|
||||
CmsSite site = siteRepository.findByCode(siteCode).orElse(null);
|
||||
return assembleSnapshot(site);
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点快照(W4:按门户 path 解析,/portal/{path}/** 身份唯一入口)。
|
||||
* 复用 cms:site 缓存命名空间(key=path);失效由既有 SiteContentChangedEvent 整缓存 clear
|
||||
* 覆盖(PortalCacheEvictListener),T-W4-14。
|
||||
*/
|
||||
@Cacheable(cacheNames = CacheNames.SITE, key = "#path")
|
||||
public PortalSiteSnapshot siteSnapshotByPath(String path) {
|
||||
CmsSite site = siteRepository.findByPath(path).orElse(null);
|
||||
return assembleSnapshot(site);
|
||||
}
|
||||
|
||||
/** 快照装配(siteSnapshot/siteSnapshotByPath 共用;站点不存在/停用返回 null 缓存哨兵) */
|
||||
private PortalSiteSnapshot assembleSnapshot(CmsSite site) {
|
||||
if (site == null || site.getStatus() == null || site.getStatus() != 1) {
|
||||
return null;
|
||||
}
|
||||
PortalSiteSnapshot snapshot = PortalSiteSnapshot.of(site);
|
||||
snapshot.setTheme(resolveTheme(snapshot.getTheme()));
|
||||
derivePublicBaseUrl(site, snapshot);
|
||||
// 微调参数注入(方案 §5.3):按已解析主题 schema 过滤,回退主题与参数过滤天然一致
|
||||
com.yunhai.cms.theme.ThemeInfo themeInfo = themeRegistry.get(snapshot.getTheme());
|
||||
java.util.Map<String, String> themeParams =
|
||||
@@ -234,6 +252,32 @@ public class PortalQueryService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* D4(W4):public_base_url 继承派生——子站未配置且主站配置了 public_base_url 时,
|
||||
* 派生为 {主站public_base_url}/{本站code};子站自配优先(T-W4-15)。
|
||||
* 支撑"学校独立域名按 /portal/{schoolPath}/** 前缀整段代理"形态:子站链接自动正确。
|
||||
*/
|
||||
private void derivePublicBaseUrl(CmsSite site, PortalSiteSnapshot snapshot) {
|
||||
if (snapshot.getPublicBaseUrl() != null && !snapshot.getPublicBaseUrl().trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (snapshot.getParentSiteId() == null || snapshot.getParentSiteId() <= 0) {
|
||||
return;
|
||||
}
|
||||
CmsSite parent = siteRepository.findById(snapshot.getParentSiteId()).orElse(null);
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
String parentBase = ExtJsonUtils.getString(ExtJsonUtils.parse(parent.getConfigJson()), "public_base_url");
|
||||
if (parentBase != null && !parentBase.trim().isEmpty()) {
|
||||
String base = parentBase.trim();
|
||||
while (base.endsWith("/")) {
|
||||
base = base.substring(0, base.length() - 1);
|
||||
}
|
||||
snapshot.setPublicBaseUrl(base + "/" + site.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/** 皮肤包存在性检查:委托 ThemeRegistry(theme.json + 4 模板齐全校验,方案 §5.1 重构,缺省回退 default) */
|
||||
private String resolveTheme(String theme) {
|
||||
if (themeRegistry.contains(theme)) {
|
||||
|
||||
@@ -49,6 +49,7 @@ public class SchoolService {
|
||||
vo.setSchName(top.getSchName());
|
||||
vo.setMainSiteId(top.getId());
|
||||
vo.setMainSiteCode(top.getCode());
|
||||
vo.setMainSitePath(top.getPath());
|
||||
vo.setMainSiteName(top.getName());
|
||||
vo.setTheme(top.getTheme());
|
||||
vo.setStatus(top.getStatus());
|
||||
|
||||
@@ -120,22 +120,34 @@ public class SiteService {
|
||||
return ExtJsonUtils.getString(config, key);
|
||||
}
|
||||
|
||||
/** 站点保存(B1 权限拆分:校级可建/编辑本院系子站,顶层站仅超管;MVP 1-2 校以初始化脚本为主) */
|
||||
/** 站点保存(B1 权限拆分:校级可建/编辑本院系子站,顶层站仅超管;MVP 1-2 校以初始化脚本为主)
|
||||
* W4 嵌套路径(方案 §8.4 步骤 3):validateParent 后计算 path(顶层=code;子站=parent.path+'/'+code),
|
||||
* existsByPath 冲突拒绝(同父同码冲突=D2 uk_site_path 兜底);编辑分支 code 不可变(T-W4-05);改挂自然重算(T-W4-06)。 */
|
||||
@Transactional
|
||||
public SiteVo save(BoaUser user, SiteSaveRequest req) {
|
||||
requireSiteSavePermission(user, req);
|
||||
CmsSite site;
|
||||
if (req.getId() == null) {
|
||||
if (siteRepository.findByCode(req.getCode()).isPresent()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "站点编码已存在");
|
||||
}
|
||||
site = new CmsSite();
|
||||
site.setCode(req.getCode());
|
||||
} else {
|
||||
site = siteRepository.findById(req.getId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "站点不存在"));
|
||||
// W4:编码=URL 末段,创建后不可修改(对齐前端 SiteManage 编辑弹窗 code disabled)
|
||||
if (!site.getCode().equals(req.getCode())) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "编码创建后不可修改");
|
||||
}
|
||||
}
|
||||
validateParent(req, site);
|
||||
// W4:path 计算(唯一入口,SitePathBuilder 语义收敛于此)与冲突拒绝
|
||||
String path = buildPath(site.getParentSiteId(), site.getCode());
|
||||
boolean pathConflict = site.getId() == null
|
||||
? siteRepository.existsByPath(path)
|
||||
: siteRepository.existsByPathAndIdNot(path, site.getId());
|
||||
if (pathConflict) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "路径冲突:" + path + "(同上级下编码重复或地址已被占用)");
|
||||
}
|
||||
site.setPath(path);
|
||||
// B-8:顶层站 schId 必填——canManage 按 currentRole.schId ↔ site.schId 匹配,
|
||||
// 顶层站空 schId 建成即"校级角色不可见/不可管"(院系子站已在 validateParent 继承主站 schId,不受影响)
|
||||
if ((site.getParentSiteId() == null || site.getParentSiteId() == 0) && req.getSchId() == null) {
|
||||
@@ -160,6 +172,19 @@ public class SiteService {
|
||||
return SiteVo.of(site);
|
||||
}
|
||||
|
||||
/**
|
||||
* W4 门户 path 计算(唯一入口,前后端各一份纯函数语义:front/src/utils/sitePath.ts 同构):
|
||||
* 顶层(parentSiteId≤0)=code;子站=父站 path + '/' + code。
|
||||
*/
|
||||
private String buildPath(Integer parentSiteId, String code) {
|
||||
if (parentSiteId == null || parentSiteId <= 0) {
|
||||
return code;
|
||||
}
|
||||
CmsSite parent = siteRepository.findById(parentSiteId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "上级站点不存在"));
|
||||
return parent.getPath() + "/" + code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点保存权限(B1 建站权限拆分,安全边界收敛于本单一方法,前端按钮仅入口显性化):
|
||||
* 超管直通;非超管仅可新建/编辑「本校主站下的院系子站」,具体拒绝项——
|
||||
|
||||
Reference in New Issue
Block a user