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
@@ -0,0 +1,75 @@
package com.yunhai.cms.theme;
import java.util.List;
import java.util.function.Predicate;
/**
* 主题包规范校验(W1 重构点,方案 §5.5:校验语义收敛于本类,Registry 与 PackageService 共用,
* 杜绝两套校验漂移)。原 ThemeRegistry.validate 的规则原文平移:
* specVersion=1、name 规范、name=目录名、必备模板(home/list/detail/single)、
* fragments/head.html、param 规范、screenshot 存在。
* existsFn 判定资源存在:classpath 包用 ClassPathResource、外部包用 Files.exists(§5.4 步骤 1)。
*/
public final class ThemePackageValidator {
private ThemePackageValidator() {
}
/** 校验不通过抛 IllegalArgumentException(bad package 语义,Registry 剔除 + log.warn) */
public static void validate(ThemeInfo info, String dirName, Predicate<String> existsFn) {
if (info == null) {
throw new IllegalArgumentException("清单为空");
}
if (info.getSpecVersion() == null || info.getSpecVersion() != 1) {
throw new IllegalArgumentException("specVersion 必须为 1");
}
if (!ThemeRegistry.isValidName(info.getName())) {
throw new IllegalArgumentException("name 缺失或不符合命名规范:" + info.getName());
}
if (!info.getName().equals(dirName)) {
throw new IllegalArgumentException("name 与目录名不一致:" + dirName + " != " + info.getName());
}
for (String template : ThemeRegistry.requiredTemplates()) {
if (!existsFn.test(template)) {
throw new IllegalArgumentException("缺必备模板 " + template);
}
}
// fragments/ 目录存在性以 head.html 片段代替(jar 内目录项不可靠)
if (!existsFn.test("fragments/head.html")) {
throw new IllegalArgumentException("缺 fragments/head.html 公共片段");
}
validateParams(info);
String shot = info.screenshotPath();
if (!existsFn.test(shot)) {
throw new IllegalArgumentException("声明的 screenshot 不存在:" + shot);
}
}
/** param 规范(key/类型/重复/cssVar 仅 color/select 必带 options);不合法抛 IllegalArgumentException */
public static void validateParams(ThemeInfo info) {
List<ThemeParam> params = info.getParams();
List<String> seen = new java.util.ArrayList<>();
for (ThemeParam p : params) {
if (p.getKey() == null || !ThemeParam.KEY_PATTERN.matcher(p.getKey()).matches()) {
throw new IllegalArgumentException("param key 非法:" + p.getKey());
}
if (seen.contains(p.getKey())) {
throw new IllegalArgumentException("param key 重复:" + p.getKey());
}
seen.add(p.getKey());
String type = p.getType();
boolean knownType = ThemeParam.TYPE_COLOR.equals(type) || ThemeParam.TYPE_IMAGE.equals(type)
|| ThemeParam.TYPE_SELECT.equals(type) || ThemeParam.TYPE_TEXT.equals(type);
if (!knownType) {
throw new IllegalArgumentException("param " + p.getKey() + " 类型非法:" + type);
}
if (p.getCssVar() != null && !p.getCssVar().isEmpty() && !ThemeParam.TYPE_COLOR.equals(type)) {
throw new IllegalArgumentException("cssVar 仅允许 color 类型参数:" + p.getKey());
}
if (ThemeParam.TYPE_SELECT.equals(type)
&& (p.getOptions() == null || p.getOptions().isEmpty())) {
throw new IllegalArgumentException("select 参数缺 options:" + p.getKey());
}
}
}
}