[开发] 伪静态(URL Rewrite)功能开发规划方案

👑Lv.11 元老 🌞 骨干会员
2026-10-30 15:01:15
  • 日期:2026-10-30
  • 目标版本:核心 V1.2.18
  • 范围:核心程序(app/、index.php、install.php、lang/、assets/css/、根目录三端规则文件)
  • 状态:规划稿,未动代码

一、核查结论(实测 + 代码定位)

1.1 实测结果(本机 nginx,直接发 HTTP 请求)

请求状态码说明
/forum200伪静态已生效(nginx rewrite 到 /index.php/$1)
/index.php/forum404直接访问(PATH_INFO 形式)不可用
/index.php?r=/forum200仅查询参数回退可用

结论:当前程序不具备"不开伪静态也能用"的能力。一旦用户服务器没有配置 rewrite,全站除首页外的链接全部 404。

1.2 根因定位

#问题位置影响
1路由只按 REQUEST_URI 原路径匹配,不识别 /index.php 前缀Router.php直接访问模式全站 404
2?r= 回退只在路径为 / 或 /index.php 时生效,且显式禁止 admin同上 L71-L84后台无法用回退模式
3URL 生成只拼 BASE_PATH,不感知伪静态开关TemplateCompiler.php开关切换后全站链接形态不变
4三端规则不一致:nginx/apache 转发到 index.php/$1(保留 REQUEST_URI),IIS 只转发到 index.php(不带 PATH_INFO)nginx-server.conf、.htaccess、web.configIIS 行为需实测确认
5邮件里的验证/重置链接直接拼 SITE_URL + '/verify-email/...'AuthController.php / L382 / L543直接模式下邮件链接 404
615 处 header('Location: ...') 直接拼路径(后台登录/二次验证跳转等)BaseController.php、Auth.php 等直接模式下跳转目标 404 → 登录循环
72 处 htmx 绝对路径 hx-get="/..."thread/show.php直接模式下 htmx 局部刷新 404
8首页前置缓存只认 /forum、/forum/category/{id} 精确路径index.php直接模式下前置缓存全失效(仅性能退化,不影响功能)
9PageCache 缓存整页 HTML(含生成好的链接)PageCache.php切换开关后不清缓存 → 游客仍拿到旧形态链接
10插件侧绝对 URL 直接拼 SITE_URL(seo 站点地图、oidc issuer、mobile_api、mcp_server、market_hub 回跳)plugins/ 下 58 处直接模式下插件产出的绝对链接 404
11维护模式白名单 $exactAllowed 不含探针路径init.php站点关闭时探针恒 503,误判"伪静态没配好"
12settings 是 KV 表(INSERT OR IGNORE 种子在每次 bootstrap 执行)Schema.php / L451不能把 rewrite_enabled 默认值直接塞进 seedDefaults,否则存量站升级后被静默改成"直接模式",全站链接变形

二、目标与红线

2.1 功能目标

  1. 新装用户开箱可用:伪静态默认关闭,链接自动带 index.php(/index.php/thread/1),无需配置服务器。
  2. 后台「系统设置」新增独立 Tab「伪静态」,位于「常规设置」之后。
  3. 总开关 + 服务器类型(Nginx / Apache / IIS)+ 对应规则代码块(可复制)+ 一键检测。
  4. 内置第三方程序伪静态规则(Discuz / WordPress / ThinkPHP / Laravel 等)作对照参考。
  5. 开启伪静态后,核心全部路由(登录、发帖、插件 /orange/*、API、后台)正常,不出现 404。

2.2 红线

  • 不改路由注册表结构,不改控制器方法签名,不引入第三方前端库(沿用 Alpine.js / 原生 JS)。
  • 零内联样式:新增 CSS 全部进 assets/css/admin.css,视图内不得出现 style=""。
  • 三语语言包(zh / en / zh_tw)键集与键序逐行一致,不允许裸键。
  • 存量站升级后 URL 形态必须保持不变(= 维持伪静态开启),不得强制切换。
  • 不允许出现「保存后把自己锁在后台外」的路径:开启动作必须服务端探针通过才落库。
  • 不自动改写服务器配置(nginx 配置在 webroot 之外;.htaccess / web.config 只检测不写入)。

三、总体设计

3.1 双模式

模式rewrite_enabled生成的链接服务器要求
直接访问(新装默认)0/index.php/thread/1、/index.php/admin无(Apache/nginx 均原生支持 PATH_INFO)
伪静态(存量站默认)1/thread/1、/admin需 nginx rewrite / .htaccess / web.config

路由层两种形态都能解析:Router 剥掉 BASE_PATH 后再剥掉 /index.php 前缀。这一点与伪静态开关解耦——开关只影响"生成什么链接",不影响"哪些链接能访问"。

3.2 URL 出口唯一化

新增 app/Helpers/Url.php:

Url::to('/thread/12');        // 直接模式 → /index.php/thread/12
                              // 伪静态   → /thread/12
Url::absolute('/thread/12');  // SITE_URL + Url::to(...)
Url::isRewriteEnabled();      // 请求内 static 缓存,读 settings

所有出站 URL 收敛到这两处:TemplateCompiler::url() 改为委托 Url::to()(所有视图与插件零改动自动适配),Controller::redirect() 改为内部路径先过 Url::to()。

3.3 存量站兼容策略(关键)

rewrite_enabled 键在存量站中不存在时,读侧按 1(旧行为)回落;只有安装流程会显式写入 '0'。

  • 新增 Settings::has(string $key): bool(判断键是否真实存在,非默认值)。
  • 读取:Url::isRewriteEnabled() = Settings::has('rewrite_enabled') ? (get() === '1') : true。
  • 安装:install.php 安装成功后显式 setValue('rewrite_enabled', '0')、setValue('rewrite_type', 'nginx')。
  • 后台首次打开该 Tab 时,若键不存在,界面提示"当前为兼容模式(按开启处理)",保存后写死显式值。

四、实施阶段(每阶段独立验收,出验证报告后再进下一阶段)

B1 — 路由与 URL 层(不动视图)

改动:

  • 新增 app/Helpers/Url.php(to / absolute / isRewriteEnabled / basePath)。
  • Router::dispatch():剥 BASE_PATH 后新增 /index.php 前缀剥离;?r= 回退保留(老书签兼容)。
  • TemplateCompiler::url() 委托 Url::to()。
  • Controller::redirect() 内部路径过 Url::to()。
  • Auth::loginUrl()、Auth::isAuthPath()、Auth::rememberAdminVerifyTarget() 适配前缀;BaseController 两处 Location 改用 Url::to()。
  • index.php 前置缓存:路径归一化(剥 /index.php 前缀再比对 /forum 等)。
  • Settings::has()。

验收:直接模式下(临时把 rewrite_enabled 置 0)手动走一遍 首页/论坛/分类/帖子/登录/注册/发帖/私信/后台首页,全部 200;伪静态模式下行为与现在完全一致(回归)。

B2 — 后台 Tab + 保存

改动:

  • settings_system.php:Tab 按钮插在「常规设置」之后;新增 #tab-rewrite 面板。
  • SettingsController::system():读写 rewrite_enabled / rewrite_type(白名单 nginx|apache|iis);变更时 Settings::buildCache() + PageCache::invalidate() + 审计日志。
  • assets/css/admin.css 新增:admin-code-block、admin-code-copy、admin-env-chip 等(零内联)。
  • 三语语言键(约 25 条,见 §5.3)。

验收:保存后 PRG 回显、Tab 记忆生效、缓存已清、三语键集一致。

B3 — 探针接口 + 环境检测

改动:

  • 新增路由:POST /admin/settings/test-rewrite(index.php 后台分组内)。
  • 新增核心探测路由:GET /__rewrite_probe(返回 200 text/plain: fh-rewrite-ok,X-Robots-Tag: noindex,零 DB 操作)。
  • SettingsController::testRewrite():CSRF + 限流(RateLimiter::isLimited('rewrite_probe', 5, 60))+ 服务端 HTTP GET 自测。
  • init.php 维护模式精确白名单追加 /__rewrite_probe。
  • 后台页面展示环境:SERVER_SOFTWARE 识别 nginx/apache/IIS;根目录 .htaccess / web.config 存在性与内容特征检查。

验收契约见 §5.4;错误码全部可映射 i18n。

B4 — 规则模板库(三端 + 第三方)

改动:

  • 新增 app/Helpers/RewriteRules.php:集中存放三端规则与第三方规则文本(纯静态数组,视图直接渲染,便于后续维护)。
  • 视图:按 rewrite_type 动态展示(Alpine x-show,服务端先渲染三块均可),每块带「复制」按钮(navigator.clipboard + textarea 回退)。
  • 规则内容见 §5.5。

验收:三种服务器切换正确、复制可用、代码块在浅色/深色主题下可读。

B5 — 绝对 URL 收口 + PWA/SEO 适配

改动:

  • 邮件三处:SITE_URL . Url::to('/verify-email/...') 等。
  • PwaController::manifest():start_url / scope / id 改为 Url::to('/') 的绝对值(否则直接模式下 manifest 位于 /index.php/manifest.webmanifest,相对 ./ 会偏)。
  • 插件侧:seo 站点地图、mobile_api 文档、market_hub 回跳优先改用 Url::absolute();oidc / mcp_server 属"已对外登记地址",不改逻辑,只在 Tab 内做开关切换风险提示。
  • 可选(建议):伪静态已开启时,GET 访问 /index.php/... 308 跳转到干净 URL(POST 不动),避免重复内容。

验收:直接模式下邮件链接可达、manifest 安装正常;伪静态模式下 URL 唯一。

B6 — 全量回归 + 发版

  • 双模式 × 全场景矩阵(§7),错误日志零新增、SQL 计划无变化。
  • 更新 CHANGELOG.md、version.php → V1.2.18。
  • 按核心发布流程打包(cli/release_build.php,带 --notes),生成增量包与清单。

五、详细设计

5.1 配置项

键值域默认说明
rewrite_enabled'0' / '1'新装 '0';存量站缺键按 1 兼容只影响 URL 生成
rewrite_typenginx / apache / iisnginx只影响展示哪套规则与环境提示

rewrite_type 不参与路由/URL 生成逻辑,避免"类型选错导致站点异常"。

5.2 路由归一化伪代码

// Router::dispatch() 内,BASE_PATH 剥离之后
if (preg_match('#^/index\.php(/.*)?$#', $uri, $m)) {
    $uri = ($m[1] ?? '') === '' ? '/' : $m[1];
}

前置缓存路径判定同样先剥 /index.php,再比 /forum、/forum/category/{id}。

5.3 后台 Tab 结构

[系统设置]
 ├─ 常规设置
 ├─ 伪静态            ← 新增,位于常规设置之后
 ├─ PoW 设置
 ├─ 积分设置
 ├─ 限流设置
 └─ 附件设置

面板内容(自上而下):

  1. 总开关:rewrite_enabled(含说明:关闭=链接带 index.php,开箱可用;开启=需先配好服务器)。
  2. 服务器类型:三选一 + 自动检测结果 chip("检测到 nginx/1.15.11")。检测只做预填,可手动改。
  3. 当前 URL 形态预览:/index.php/thread/1 ↔ /thread/1。
  4. 规则代码块:按所选类型显示;含"改哪里"的说明(Nginx:nginx-server.conf 的 location /;Apache:根目录 .htaccess;IIS:根目录 web.config)。
  5. 环境就位检查:Apache/IIS 显示 .htaccess / web.config 是否存在、是否含主路由规则;Nginx 只能靠探针。
  6. 检测按钮:POST /admin/settings/test-rewrite,返回成功/失败与具体错误("请检查 Nginx 配置")。
  7. 第三方程序规则(折叠):Discuz / WordPress / ThinkPHP / Laravel,标注"仅作对照,直接套用会与本程序冲突"。
  8. 风险提示:切换会改写全站链接并自动清前台缓存;已启用 OIDC / MCP / mobile_api 的站点需同步更新已登记地址。

新增 i18n 键(三语同序):admin.tab_rewrite、admin.tab_rewrite_desc、admin.rewrite_enable、admin.rewrite_enable_hint、admin.rewrite_server、admin.rewrite_server_nginx/apache/iis、admin.rewrite_rules_title、admin.rewrite_rules_nginx/apache/iis、admin.rewrite_copy、admin.rewrite_copied、admin.rewrite_copy_fail、admin.rewrite_test、admin.rewrite_testing、admin.rewrite_test_ok、admin.rewrite_test_fail、admin.rewrite_err_disabled/timeout/dns/ssl/http/body/bad_url、admin.rewrite_env_detected、admin.rewrite_env_missing、admin.rewrite_url_sample、admin.rewrite_switch_warning、admin.rewrite_thirdparty_title、admin.rewrite_thirdparty_desc、admin.rewrite_lockout_warning。

5.4 探针接口契约

POST /admin/settings/test-rewrite
Body: csrf=<token>
Header: X-Requested-With: XMLHttpRequest
权限: 管理员 + 二次验证(继承 Admin\BaseController)
限流: 5 次 / 60 秒(RateLimiter)

200 {ok:true,  http:200, url:"https://host/__rewrite_probe", ms:87, server:"nginx/1.15.11"}
200 {ok:false, code:"http_404", message:"请检查 Nginx 配置", url:"...", http:404}

失败码:disabled(未开启)/ bad_url / dns / ssl / timeout / http_<code> / body(返回体不含约定 token,防"任意 200 假阳性")。

目标地址优先级:SITE_URL(已校验)→ scheme://HTTP_HOST;再拼 BASE_PATH 与 /__rewrite_probe。curl 5 秒超时、不跟随重定向(3xx 视为失败)、保持 SSL 校验;SSL 失败单独报 ssl 并提示自签证书场景。

防锁死:保存 rewrite_enabled 由 0 → 1 时,服务端强制执行同一探针,失败则拒绝落库并回显错误码。关闭(1 → 0)不校验。

安全:目标主机只允许 SITE_URL 的 host 或当前请求 host,禁止任意 URL 输入(不作 SSRF 跳板)。

5.5 规则模板内容

Nginx(推荐,与现有 nginx-server.conf 一致)

location / {
    index index.php index.html;
    if (!-e $request_filename) {
        rewrite ^/(.*)$ /index.php/$1 last;
    }
}
# fastcgi 段必须保留(PATH_INFO 支持):
# fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;

并在 Tab 内写明三种落地方式:整文件 include、location / 片段粘贴、面板"伪静态"输入框粘贴(宝塔/phpstudy)。

Apache(根目录 .htaccess)

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php/$1 [QSA,L]
</IfModule>

附提示:需 AllowOverride FileInfo(否则规则静默无效,探针可发现)。

IIS(根目录 web.config)

<rule name="FlintHub Main" stopProcessing="true">
  <match url=".*" />
  <conditions logicalGrouping="MatchAll">
    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
    <add input="{URL}" pattern="^/(assets|index\.php)" negate="true" />
  </conditions>
  <action type="Rewrite" url="index.php" />
</rule>

附提示:需安装 URL Rewrite 模块 + PHP FastCGI 处理程序;IIS 为唯一"需实测确认"项,B3 探针负责兜底。

第三方(仅对照):Discuz(rewrite ^/forum-(\d+)-(\d+)\.html$ /forum.php?mod=forumdisplay&fid=$1&page=$2 last;)、WordPress(try_files $uri $uri/ /index.php?$args;)、ThinkPHP(rewrite ^(.*)$ /index.php?s=$1 last;)、Laravel(try_files $uri $uri/ /index.php?$query_string;)。每段标注"不可直接套用"。

5.6 缓存与体验

  • 保存开关:Settings::buildCache() → PageCache::invalidate()(整页 HTML 里带旧链接)。
  • 前置缓存:路径归一化后恢复命中。
  • PWA:start_url / scope 用绝对 Url::to('/');sw.js 注册仍走根目录物理文件,不受影响。
  • 邮件/二维码/复制链接:统一 Url::absolute(),right_sidebar.php 的 SITE_URL . $this->url() 形式保留(url() 已带前缀,等价)。

5.7 插件兼容清单(B5 内逐项验证)

插件关注点处置
orange_city / qa / inspiration_square / plughub / board_games / zupu 等视图仍用 BASE_PATH 手拼站内链接(非 $this->url()),B1 实测关闭模式下出现裸链接(orange 18 条、qa 5 条、inspiration 8 条、plughub 11 条、board_games 1 条)逐个迁移到 \app\Helpers\Url::to();插入附件/表情等静态资源仍用 BASE_PATH 不动
seo站点地图/robots 绝对 URL改 Url::absolute()
mobile_api客户端 base URL、route_before_dispatch 自行解析 URI补 /index.php 剥离;文档同步
oidc_provider / oidc_clientissuer 必须与登记地址逐字节一致不改逻辑,开关切换时强提示
mcp_server对外 endpoint 地址同上
market_hub授权回调 site_url同上

六、安全与风险

风险处置
开启后管理员被锁在后台外服务端探针门禁;失败不落库;文档给出 DB 手改应急值
探针被当作 SSRF 探针目标限定本站 host / 127.0.0.1,无用户可控 URL,5s 超时
探针被限流/维护模式拦截误判独立路由 + 维护白名单 + 独立限流键
两种 URL 共存产生重复内容伪静态开启时 GET /index.php/... → 308 干净地址(POST 不动)
切换后静态缓存仍返回旧链接保存即 PageCache::invalidate();提示用户 Ctrl+F5
.htaccess 存在但 AllowOverride 关闭探针失败 → 错误码 http_404 + 对应提示
IIS PATH_INFO 不可用探针兜底;文档建议 IIS 用户直接开伪静态(web.config 已随包)

七、验收矩阵(双模式各跑一遍)

场景直接模式伪静态模式
/、/forum、/forum/category/1?page=2200200
/thread/1、/tag/1、/blog/1、/search?keyword=x200200
登录(GET/POST)、注册、忘记密码200 / PRG 正确同
发帖 / 回帖 / 编辑 / 删除成功且跳转正确同
上传附件、/attachment/{id}200200
/api/vote、/api/captcha、/api/pow200200
/admin、/admin/verify、/admin/settings/system200200
插件路由:/orange/*、/qa/*、/u/{id}200200
/manifest.webmanifest、/offline、sw.js正常正常
404 页、403 页正确正确
二级目录 BASE_PATH正确正确
邮件链接(注册验证 / 重置密码)可达可达
探针接口关闭时 disabled开启且配置正确时 ok:true

另需:php -l 全量语法、protected/error.log 零新增、三语键集一致、零内联样式扫描。


八、发布与回滚

  • 版本 V1.2.18,CHANGELOG.md 增补「伪静态可开关」条目。
  • 发版走 cli/release_build.php(核心增量包 + 清单),--notes 面向站长写 3~6 条祈使句(不提文件名/内部术语)。
  • 回滚:rewrite_enabled 置 '0' 即回到"全站 index.php 链接",不依赖服务器配置;无需回退代码。
  • 上线顺序:先发 B1(纯兼容层,零行为变化)→ B2/B3(后台)→ B4/B5(模板与收口)→ B6 一次性发版。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 瀏覽 7 次 | 回覆 0 次

全部回覆 (0)

暫無回覆

請 登入