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,220 @@
package com.yunhai.cms.utils;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 门户 URL 生成器(方案 §8.6 前置约定2):所有链接、主题资产、canonical 统一经此生成,
* 禁止模板硬编码绝对地址——base 域子路径与学校独立域名两种暴露形态共用同一套渲染。
*
* <p>基准 = 站点 config_json.public_base_url(绑学校域名后为学校域),
* 未配置时从当前请求派生(预览/平台内跳转形态)。
*/
public class PortalUrlHelper {
/** 正文/单页内容里出现的上传资源地址:可选历史绝对前缀 + 相对路径(B-1 渲染期统一补基准) */
private static final Pattern UPLOAD_URL_IN_CONTENT =
Pattern.compile("(?:https?://[^/\\s\"']+)?((?:/cms)?/upload/[^\\s\"']+)");
/** 不带尾斜杠的门户基地址,形如 {scheme}://{host}{context}/portal/{siteCode} 或 https://www.xxx.edu.cn */
private final String baseUrl;
private final String siteCode;
/** 当前皮肤包名:进入资产 URL(B-3,换肤即换 URL 使浏览器/CDN 缓存天然失效) */
private final String theme;
/**
* base 域对外基地址(cms.portal.base-origin,含 context-path,如 https://base.yunhaixingtong.com/cms)。
* C-6:站点未配 public_base_url 时的规范派生基准;B-4:siteHome 无段形态退化目标。空 = 未配置。
*/
private final String baseOrigin;
/** 预览模式资产前缀(非 null 时 asset() 改走管理端代理,方案 §5.5 防资产串包) */
private final String assetProxyPrefix;
public PortalUrlHelper(String baseUrl, String siteCode, String theme) {
this(baseUrl, siteCode, theme, null, null);
}
public PortalUrlHelper(String baseUrl, String siteCode, String theme, String baseOrigin) {
this(baseUrl, siteCode, theme, baseOrigin, null);
}
private PortalUrlHelper(String baseUrl, String siteCode, String theme, String baseOrigin,
String assetProxyPrefix) {
this.baseUrl = trimTail(baseUrl);
this.siteCode = siteCode;
this.theme = theme;
this.baseOrigin = trimTail(baseOrigin);
this.assetProxyPrefix = assetProxyPrefix;
}
/**
* 预览模式工厂(方案 §5.5):资产 URL 指向管理端主题资产代理 /api/theme/{theme}/asset/**,
* 其余站内链接仍指向真实门户(点击回真实门户是已接受的预览局限)。
*/
public static PortalUrlHelper forPreview(HttpServletRequest request, String siteCode, String theme) {
PortalUrlHelper base = fromRequest(request, siteCode, theme);
// 显式走 5 参构造:第 4 参是 base-origin,代理前缀必须落第 5 参(避免误绑 4 参重载)
return new PortalUrlHelper(base.baseUrl, siteCode, theme, null, "/api/theme/" + theme + "/asset/");
}
/**
* 从当前请求派生门户基地址(public_base_url / base-origin 均未配置时的兜底;
* 反代形态经 ForwardedHeaderFilter 修正后的 scheme/host/port 已反映在请求上,B-2)。
*/
public static PortalUrlHelper fromRequest(HttpServletRequest request, String siteCode, String theme) {
String scheme = request.getScheme();
String host = request.getServerName();
int port = request.getServerPort();
String context = request.getContextPath();
StringBuilder sb = new StringBuilder(scheme).append("://").append(host);
boolean defaultPort = "http".equals(scheme) && port == 80 || "https".equals(scheme) && port == 443;
if (!defaultPort) {
sb.append(':').append(port);
}
sb.append(context).append("/portal/").append(siteCode);
return new PortalUrlHelper(sb.toString(), siteCode, theme);
}
/**
* base 域规范形态工厂(C-6):站点未配 public_base_url 且平台配了 cms.portal.base-origin 时,
* 以 base 域规范路径为基准(base-origin + /portal/{siteCode}),不随访问 host 漂移——
* 方案 §8.6「默认派生 https://base.yunhaixingtong.com/cms/portal/{code}」的落地。
*/
public static PortalUrlHelper fromBaseOrigin(String baseOrigin, String siteCode, String theme) {
return new PortalUrlHelper(trimTail(baseOrigin) + "/portal/" + siteCode, siteCode, theme, baseOrigin);
}
/** 首页 */
public String home() {
return baseUrl + "/";
}
/**
* 其他站点首页("专业院系"下拉用,《院系子站方案》):目标站配置了 public_base_url 时以它为准;
* 否则把当前基准里的站点段替换为目标站点(base 域子路径形态);
* 学校域形态(当前基准无 /portal/ 段)时退化为 base 域规范路径(B-4,base-origin 未配置才回 "/")。
*/
public String siteHome(String targetCode, String targetPublicBaseUrl) {
if (targetPublicBaseUrl != null && !targetPublicBaseUrl.trim().isEmpty()) {
return trimTail(targetPublicBaseUrl) + "/";
}
int idx = baseUrl.lastIndexOf("/portal/");
if (idx >= 0) {
return baseUrl.substring(0, idx + "/portal/".length()) + targetCode + "/";
}
if (baseOrigin != null && !baseOrigin.isEmpty()) {
return baseOrigin + "/portal/" + targetCode + "/";
}
return "/";
}
/** 栏目列表/单页(type=3 外链不在此渲染,直接取 linkUrl) */
public String category(Object categoryId) {
return baseUrl + "/c/" + categoryId;
}
/** 文章详情 */
public String article(Object articleId) {
return baseUrl + "/a/" + articleId;
}
/** 站内搜索 */
public String search(String q, Integer page) {
String url = baseUrl + "/search?q=" + urlEncode(q == null ? "" : q);
return page != null && page > 1 ? url + "&page=" + page : url;
}
/**
* 主题资产(css/图片):常规经 PortalAssetController 按站点皮肤解析;预览模式走管理端代理。
* URL 含主题段(B-3):换肤即换 URL,浏览器/CDN 的 max-age 缓存天然失效。
*/
public String asset(String path) {
if (assetProxyPrefix != null) {
return assetProxyPrefix + trimLead(path);
}
String themeSeg = theme == null || theme.isEmpty() ? "default" : theme;
return baseUrl + "/assets/" + themeSeg + "/" + trimLead(path);
}
/**
* 上传资源地址补基准(B-1):入库的 webUrl 一律为相对路径(FILE_WEB_PATH 相对形态),
* 渲染时按当前基准补 origin 成完整 URL——base 域/学校域/任意外部域访问不断图。
* 已是完整 http(s) 地址(外链图片等)原样返回;空值原样返回。
*/
public String upload(String storedUrl) {
if (storedUrl == null || storedUrl.trim().isEmpty()) {
return storedUrl;
}
String s = storedUrl.trim();
if (s.startsWith("http://") || s.startsWith("https://")) {
return s;
}
return originOf(baseUrl) + (s.startsWith("/") ? s : "/" + s);
}
/**
* 富文本内容后处理(B-1):把正文里历史绝对前缀(http://localhost:8072 等)
* 与相对形态的上传地址统一重写为当前基准下的完整 URL。外链图片不受影响。
*/
public String rewriteUploadUrls(String html) {
if (html == null || html.isEmpty()) {
return html;
}
Matcher matcher = UPLOAD_URL_IN_CONTENT.matcher(html);
StringBuffer sb = new StringBuffer(html.length());
while (matcher.find()) {
matcher.appendReplacement(sb, Matcher.quoteReplacement(upload(matcher.group(1))));
}
matcher.appendTail(sb);
return sb.toString();
}
public String getSiteCode() {
return siteCode;
}
public String getBaseUrl() {
return baseUrl;
}
/** 从基地址截取 scheme://host[:port](去掉 /portal 段及任何路径),upload() 补基准用 */
private static String originOf(String baseUrl) {
if (baseUrl == null) {
return "";
}
int schemeEnd = baseUrl.indexOf("://");
if (schemeEnd < 0) {
return "";
}
int pathStart = baseUrl.indexOf('/', schemeEnd + 3);
return pathStart < 0 ? baseUrl : baseUrl.substring(0, pathStart);
}
private static String trimTail(String url) {
String s = url == null ? "" : url.trim();
while (s.endsWith("/")) {
s = s.substring(0, s.length() - 1);
}
return s;
}
private static String trimLead(String path) {
String s = path == null ? "" : path;
while (s.startsWith("/")) {
s = s.substring(1);
}
return s;
}
private static String urlEncode(String value) {
try {
return java.net.URLEncoder.encode(value, "UTF-8");
} catch (java.io.UnsupportedEncodingException e) {
return value;
}
}
}