v0.16.0: 后台体验升级——i18n(默认英文)/浅深主题/字号放大/关站维护页/个人设置(头像·语言·主题·密码·MCP Token)/权限管理(菜单+MCP·API)/询盘抽屉翻倍+PHP序列化数据清洗(22.7万行)+去裸JSON
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
/**
|
||||
* # 模块 全局请求中间件:旧链接 301/302 重定向
|
||||
* # 模块 全局请求中间件:旧链接 301/302 重定向 + 关站维护页
|
||||
* 在 router 请求阶段查 redirects 表:先精确匹配(findRedirect),
|
||||
* 再对 is_regex 的条目做正则匹配;命中即返回 301/302 Response。
|
||||
* 正则条目内存缓存 60 秒,避免每请求全表扫描。
|
||||
* v0.16.0:settings.maintenance.enabled 开启时前台全部页面返回 503 维护页
|
||||
* (内容为管理员在 设置→关闭网站 配置的 HTML;/admin 后台不受影响)。
|
||||
*/
|
||||
import { createMiddleware } from "@tanstack/react-start";
|
||||
import { db } from "~/db";
|
||||
import { redirects } from "~/db/schema";
|
||||
import { redirects, settings } from "~/db/schema";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { hasPublishedSlug, resolveRedirect } from "./data";
|
||||
|
||||
@@ -36,8 +38,35 @@ function bumpHits(id: number) {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/** # 类型 关站维护配置(settings.maintenance 值) **/
|
||||
type MaintenanceCfg = { enabled: boolean; html: string };
|
||||
|
||||
/** # 成员变量 维护配置缓存(每请求查询太重,60s TTL;关站开关生效最多延迟 60s) **/
|
||||
let maintenanceCache: { value: MaintenanceCfg; at: number } | null = null;
|
||||
const MAINTENANCE_TTL = 60_000;
|
||||
|
||||
/** # 函数 读取关站维护配置(带缓存) */
|
||||
async function getMaintenance(): Promise<MaintenanceCfg> {
|
||||
if (maintenanceCache && Date.now() - maintenanceCache.at < MAINTENANCE_TTL) return maintenanceCache.value;
|
||||
const [row] = await db.select().from(settings).where(eq(settings.key, "maintenance"));
|
||||
const v = (row?.value ?? {}) as { enabled?: boolean; html?: string };
|
||||
const value = { enabled: v.enabled === true, html: typeof v.html === "string" ? v.html : "" };
|
||||
maintenanceCache = { value, at: Date.now() };
|
||||
return value;
|
||||
}
|
||||
|
||||
/** # 函数 构造 503 维护页响应(管理员配置的 HTML 居中渲染) */
|
||||
function toMaintenanceResponse(request: Request, cfg: MaintenanceCfg): Response {
|
||||
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex"><title>Site temporarily unavailable</title><style>body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f5f6f8;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;color:#333}.wrap{max-width:680px;padding:48px 32px;text-align:center;line-height:1.7}.wrap img{max-width:100%;height:auto}</style></head><body><div class="wrap">${cfg.html || "<h1>Site temporarily unavailable</h1><p>We will be back shortly.</p>"}</div></body></html>`;
|
||||
return new Response(html, {
|
||||
status: 503,
|
||||
headers: { "content-type": "text/html; charset=utf-8", "retry-after": "3600", "cache-control": "no-store" },
|
||||
});
|
||||
}
|
||||
|
||||
/** # 函数 请求重定向中间件
|
||||
* 仅处理 router 类型请求;路径先带斜杠再无斜杠各查一次精确匹配。
|
||||
* 关站开启时(/admin 除外)直接返回 503 维护页,不再走重定向/内容渲染。
|
||||
*/
|
||||
export const redirectMiddleware = createMiddleware({ type: "request" }).server(async ({ request, pathname, next, handlerType }) => {
|
||||
// server function RPC 与静态资源不查重定向
|
||||
@@ -45,6 +74,16 @@ export const redirectMiddleware = createMiddleware({ type: "request" }).server(a
|
||||
|
||||
const path = decodeURI(pathname).replace(/\/+$/, "") || "/";
|
||||
|
||||
// 0) 关站维护:前台全部页面 503(后台 /admin、admin 登录会话接口不受影响)
|
||||
if (!path.startsWith("/admin")) {
|
||||
try {
|
||||
const cfg = await getMaintenance();
|
||||
if (cfg.enabled) return toMaintenanceResponse(request, cfg);
|
||||
} catch {
|
||||
// 配置读取失败不阻塞正常渲染
|
||||
}
|
||||
}
|
||||
|
||||
// 1) 链式解析(原样/规范化/带斜杠变体依次匹配,自指规则跳过防死循环)。
|
||||
// 内容优先:路径末段能命中已发布内容时直接出内容不重定向(与 sitemap 的
|
||||
// canonical 一致;导入的重定向表存在 A→B 且 B→A 的矛盾规则对,内容优先可破环)
|
||||
|
||||
Reference in New Issue
Block a user