- 日期:2026-10-30
- 目标版本:核心 V1.2.18
- 范围:核心程序(app/、index.php、install.php、lang/、assets/css/、根目录三端规则文件)
- 状态:规划稿,未动代码
一、核查结论(实测 + 代码定位)
1.1 实测结果(本机 nginx,直接发 HTTP 请求)
| 请求 | 状态码 | 说明 |
|---|---|---|
/forum | 200 | 伪静态已生效(nginx rewrite 到 /index.php/$1) |
/index.php/forum | 404 | 直接访问(PATH_INFO 形式)不可用 |
/index.php?r=/forum | 200 | 仅查询参数回退可用 |
结论:当前程序不具备"不开伪静态也能用"的能力。一旦用户服务器没有配置 rewrite,全站除首页外的链接全部 404。
1.2 根因定位
| # | 问题 | 位置 | 影响 |
|---|---|---|---|
| 1 | 路由只按 REQUEST_URI 原路径匹配,不识别 /index.php 前缀 | Router.php | 直接访问模式全站 404 |
| 2 | ?r= 回退只在路径为 / 或 /index.php 时生效,且显式禁止 admin | 同上 L71-L84 | 后台无法用回退模式 |
| 3 | URL 生成只拼 BASE_PATH,不感知伪静态开关 | TemplateCompiler.php | 开关切换后全站链接形态不变 |
| 4 | 三端规则不一致:nginx/apache 转发到 index.php/$1(保留 REQUEST_URI),IIS 只转发到 index.php(不带 PATH_INFO) | nginx-server.conf、.htaccess、web.config | IIS 行为需实测确认 |
| 5 | 邮件里的验证/重置链接直接拼 SITE_URL + '/verify-email/...' | AuthController.php / L382 / L543 | 直接模式下邮件链接 404 |
| 6 | 15 处 header('Location: ...') 直接拼路径(后台登录/二次验证跳转等) | BaseController.php、Auth.php 等 | 直接模式下跳转目标 404 → 登录循环 |
| 7 | 2 处 htmx 绝对路径 hx-get="/..." | thread/show.php | 直接模式下 htmx 局部刷新 404 |
| 8 | 首页前置缓存只认 /forum、/forum/category/{id} 精确路径 | index.php | 直接模式下前置缓存全失效(仅性能退化,不影响功能) |
| 9 | PageCache 缓存整页 HTML(含生成好的链接) | PageCache.php | 切换开关后不清缓存 → 游客仍拿到旧形态链接 |
| 10 | 插件侧绝对 URL 直接拼 SITE_URL(seo 站点地图、oidc issuer、mobile_api、mcp_server、market_hub 回跳) | plugins/ 下 58 处 | 直接模式下插件产出的绝对链接 404 |
| 11 | 维护模式白名单 $exactAllowed 不含探针路径 | init.php | 站点关闭时探针恒 503,误判"伪静态没配好" |
| 12 | settings 是 KV 表(INSERT OR IGNORE 种子在每次 bootstrap 执行) | Schema.php / L451 | 不能把 rewrite_enabled 默认值直接塞进 seedDefaults,否则存量站升级后被静默改成"直接模式",全站链接变形 |
二、目标与红线
2.1 功能目标
- 新装用户开箱可用:伪静态默认关闭,链接自动带
index.php(/index.php/thread/1),无需配置服务器。 - 后台「系统设置」新增独立 Tab「伪静态」,位于「常规设置」之后。
- 总开关 + 服务器类型(Nginx / Apache / IIS)+ 对应规则代码块(可复制)+ 一键检测。
- 内置第三方程序伪静态规则(Discuz / WordPress / ThinkPHP / Laravel 等)作对照参考。
- 开启伪静态后,核心全部路由(登录、发帖、插件
/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动态展示(Alpinex-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_type | nginx / apache / iis | nginx | 只影响展示哪套规则与环境提示 |
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 设置
├─ 积分设置
├─ 限流设置
└─ 附件设置
面板内容(自上而下):
- 总开关:
rewrite_enabled(含说明:关闭=链接带 index.php,开箱可用;开启=需先配好服务器)。 - 服务器类型:三选一 + 自动检测结果 chip("检测到 nginx/1.15.11")。检测只做预填,可手动改。
- 当前 URL 形态预览:
/index.php/thread/1↔/thread/1。 - 规则代码块:按所选类型显示;含"改哪里"的说明(Nginx:
nginx-server.conf的location /;Apache:根目录.htaccess;IIS:根目录web.config)。 - 环境就位检查:Apache/IIS 显示
.htaccess/web.config是否存在、是否含主路由规则;Nginx 只能靠探针。 - 检测按钮:
POST /admin/settings/test-rewrite,返回成功/失败与具体错误("请检查 Nginx 配置")。 - 第三方程序规则(折叠):Discuz / WordPress / ThinkPHP / Laravel,标注"仅作对照,直接套用会与本程序冲突"。
- 风险提示:切换会改写全站链接并自动清前台缓存;已启用 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_client | issuer 必须与登记地址逐字节一致 | 不改逻辑,开关切换时强提示 |
| 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=2 | 200 | 200 |
/thread/1、/tag/1、/blog/1、/search?keyword=x | 200 | 200 |
| 登录(GET/POST)、注册、忘记密码 | 200 / PRG 正确 | 同 |
| 发帖 / 回帖 / 编辑 / 删除 | 成功且跳转正确 | 同 |
上传附件、/attachment/{id} | 200 | 200 |
/api/vote、/api/captcha、/api/pow | 200 | 200 |
/admin、/admin/verify、/admin/settings/system | 200 | 200 |
插件路由:/orange/*、/qa/*、/u/{id} | 200 | 200 |
/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 一次性发版。