本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:暂存20260913/docs/CHANGELOG (2).md。因篇幅较长拆分为上下两篇。
title: SoSite 1.0 (SplitDB 引擎迁移) — CHANGELOG
date: 2026-08-17
category: 程序开发
source: 暂存20260913/docs/CHANGELOG (2).md
SoSite 1.0 (SplitDB 引擎迁移) — CHANGELOG
基线:SoSite 5.0(MySQL)→ SoSite 1.0(SplitDB 纯 SQLite 分片引擎)
规范:严格参照《SplitDB v2.3 白皮书》;兼容 PHP 8.0.2(SQLite 3.33.0,无 RETURNING 支持)
编码:全部文件 UTF-8 without BOM + LF
本目录为「可运行骨架」镜像:与站点根目录同步的改动文件
多语言改造(i18n):zh/en 双语语言包 + 前台/后台/JS/邮件全链路文案抽取
2026-08-17
按《多语言改造计划.md》五阶段串行实施:基础设施 → 前台 → JS/邮件 → 后台 → 注释精简。默认语言中文(zh),中文用户零感知(zh 不分缓存键、
t()缺 key 回退 key 本身)。语言判定顺序:URL?lang=> Cookieflinthub_lang> Sessionlang> 后台设置site_lang> 浏览器 Accept-Language > 中文兜底。插件目录(plugins/)按约定不改造。
| 文件 | 状态 | 变更说明 |
|---|---|---|
lang/zh.php、lang/en.php | 🆕 新增 | 双语语言包 999 key,zh/en 完全对齐(双向 array_diff_key 0/0)。key 点分语义化命名(forum.latest、admin.db_archive、js.、time.、pagination.*),值支持 {placeholder} 占位符;缺 key 回退链 en→zh→key 本身,永不白屏 |
app/Helpers/I18n.php | 🆕 新增 | 核心类:懒加载 / 语言码白名单清洗防路径穿越 / 判定顺序探测 / jsSubset() 供 JS 注入(仅 js. + lang. 子集)+ 全局函数 t() |
app/Core/TemplateCompiler.php | ✏️ 改 | 新增 t() 方法 + {$t:key} 模板回调;timeAgo() 改 time.* key(刚刚/x 分钟前/小时前/天前/月前/年前)、pagination() 上一页/下一页改 pagination.prev/next |
app/init.php | ✏️ 改 | ?lang= PRG 切换(去参回跳)+ 语言探测预热 |
app/Helpers/PageCache.php | ✏️ 改 | 语言分键:zh 用原名(home_1),非默认语言加后缀(home_1_en);invalidate() 全量失效 |
app/Views/layouts/main.php | ✏️ 改 | 页头语言切换下拉(Alpine 交互,≥2 语言才显示,保留当前 URL 的 ?lang= 切换);注入 window.FlintHubI18n(jsSubset)+ window.__t(key, params) + Alpine.store('i18n') |
app/Views/ 前台 28 个 + 后台 27 个 | ✏️ 改 | 全部视图可见文案抽取为 $this->t('key') 或 {$t:key}(forum/home/thread/post/auth/blog/search/tags/profile/message/errors/layouts/_components + admin 全部含 database/settings.php 自动扩容/容量/归档/扩容记录/一致性检查) |
app/Controllers/ 前台 13 个 + 后台 17 个 | ✏️ 改 | redirect 提示 / error / success / flash 消息 / Dashboard env 表格 label 全部改 \app\Helpers\I18n::get('admin.*');缺 key 已补语言包(zh/en 同步对齐) |
app/Helpers/Mailer.php | ✏️ 改 | 默认发件人名称 mail.site_name |
assets/js/editor.js、assets/js/auth.js | ✏️ 改 | 工具栏 / prompt / alert / 字数统计 / 上传提示 / 密码强度等全部改 window.__t('js.*') |
app/Helpers/PageCache.php 等 | ✏️ 改 | 缓存分键 + 失效联动(见上) |
本阶段验证结论
- 默认中文零变化:zh 语言不分缓存键、回退链永不白屏,默认行为与改造前完全一致
- ?lang=en 切换链路:前台 21 项冒烟 + 英文下发帖/回帖/登录/注册/搜索 5 项 + JS/邮件 9 项全部通过
- 后台抽取:17 个 Admin 控制器全部
php -l通过;残留扫描(剔除注释)仅剩 AuditLog/error_log 数据性中文(属数据记录非 UI,与前台约定一致) - 语言包对齐:999/999,
array_diff_key双向 0/0 - 使用说明见
update/多语言使用说明.md
P41 智能扩容与旧帖归档(SplitDB):单桶容量阈值 + 扩容路由 + 归档只读 + 爆火安全余量
2026-08-17
结论:按《智能扩容与归档改造计划.md》实施完整「智能扩容与归档」机制,核心原则旧数据零迁移、只影响新增数据。设计决策已按用户四点修正确认:D1 桶负载仅 topic 行数(不含 reply);D2 归档保留 0=关闭(默认 365 天);D3 扩容后所有新主题帖全部进新增桶范围(旧桶冻结);D4 回复前桶负载 ≥90% 紧急扩容 / ≥100% 拒绝写入,建议扩容提示保持 ≥80%;D5 建议扩容提示放站点设置页(/admin/settings/basic)顶部。
| 文件 | 状态 | 变更说明 |
|---|---|---|
app/SplitDB/ShardRouter.php | ✏️ 改 | 新增 bucketForWrite($id, $createdAt):扩容后(created_at ≥ SPLITDB_LAST_EXPANSION_AT)新帖强制进新增桶范围 PREV + (id % (NEW-PREV));未扩容/历史导入/常量缺失一律回退旧哈希 id % bucketSize(绝不抛异常) |
app/SplitDB/Schema.php | ✏️ 改 | topic_index 建表新增 is_archived INTEGER DEFAULT 0;ensureMainIndex() 内联存量库幂等 ALTER(PRAGMA table_info 探测缺列补列,存量行默认 0 零迁移);新增 idx_topic_index_archive(is_archived, create_time) 归档扫描索引(首列 is_archived 防列表查询误选,P25 教训复核) |
app/Models/Thread.php | ✏️ 改 | insert() 改用 bucketForWrite($id, $createdAt)(created_at 上移复用);getById() 从索引行带出 is_archived(桶 topic 表无此字段) |
app/Models/Post.php | ✏️ 改 | insert() 回复写入前新增 guardBucketCapacity()(在写 extern/桶/索引之前):桶负载=该桶 COUNT(*) FROM topic(D1 口径),≥90% 触发 DatabaseController::maybeEmergencyExpand()、≥100% 抛 RuntimeException(code=1001) 拒绝写入;进程内静态 60s 缓存(key=bucket_path);计数/扩容失败 try/catch 放行写入 |
app/Controllers/Admin/DatabaseController.php | ✏️ 改 | ① 新增 upsertDefine() 幂等写 config 常量行(存在替换/缺失追加锚点后);② updateBucketSize() 写 SPLITDB_BUCKET_SIZE 同时落盘 SPLITDB_LAST_EXPANSION_AT(time) + SPLITDB_PREV_BUCKET_SIZE(旧桶数),四步安全流程不变;③ maybeAutoExpand() 触发口径从总帖数改为 maxBucketLoad() ≥ bucket_safe_capacity(仅 topic);④ 新增 maxBucketLoad()(跨全部活跃季度逐桶 COUNT(topic),query_only 直连,60s 缓存 cache_bucket_load.json);⑤ 新增 maybeEmergencyExpand()(不受开关限制,保留 24h 守卫 + flock + 翻倍≤256 + AuditLog emergency_expand);⑥ 新增 suggestExpandData() 供站点设置页提示(≥80% 命中);⑦ settings() 新增 save_capacity / save_archive 两个 POST action + handleSaveCapacity(白名单 3万/5万/8万/10万)/ handleSaveArchive(0/180/365/自定义,intval 0~3650 钳制);视图注入 bucketCapacity/archiveDays 回显 |
app/Views/admin/database/settings.php | ✏️ 改 | 新增「单桶容量(智能扩容阈值)」区块(下拉 3万/5万/8万/10万 + 需求原文提示语 + 触发规则说明)、「旧帖归档设置」区块(关闭/180/365/自定义 + 自定义天数联动显隐 JS);success=capacity/archive 提示 |
app/Controllers/Admin/SettingsController.php + app/Views/admin/settings_basic.php | ✏️ 改 | basic() 注入 suggestExpandData();站点设置页顶部渲染「⚠️ 建议扩容」警示条(D5:maxBucketLoad ≥ 80% 命中,显示最大桶负载/阈值/占比 + 跳转引擎设置链接) |
app/Controllers/ThreadController.php | ✏️ 改 | show():读取时惰性归档标记(is_archived=0 且 create_time 超阈值 → 单行 UPDATE 置 1 兜底,每日脚本未跑时即时生效)+ 计算 archiveDays;POST 回复非管理员归档守卫(redirect ?msg=archived_no_reply);视图传 isArchived/archiveDays;edit() 非管理员归档守卫 |
app/Controllers/PostController.php | ✏️ 改 | edit() 非管理员归档守卫(帖子已归档时拒绝编辑回复) |
app/Views/thread/show.php | ✏️ 改 | 顶部归档横幅("该帖已归档(发布于 XXX 天前),无法回复或编辑",管理员追加豁免提示);回复表单区:归档非管理员渲染"已归档无法回复"空态(管理员仍显示表单);新增 msg=bucket_full / archived_no_reply 错误提示 |
cli/archive_mark.php | 🆕 新建 | 每日归档标记脚本:读 archive_after_days(0=关闭直接退出),分批 UPDATE(每批 LIMIT 5000 写放大上限)循环至 0 行,幂等可重跑,--dry-run 预览待标记数 |
cron_trigger.php | ✏️ 改 | 消费流程内调用 cli/archive_mark.php(web 可触发,虚拟主机无 crontab 场景) |
config.php / install.php | ✏️ 改 | 新增 SPLITDB_LAST_EXPANSION_AT(0) / SPLITDB_PREV_BUCKET_SIZE(32) 常量定义 + install.php configTemplate 模板同步(P33 坑:改 config 必须同步模板) |
update/ 镜像同步 | 📦 同步 | 上述 15 个文件 fc /b 全部逐字节一致 |
update/智能扩容与归档改造计划.md | 🆕 新建 | 改造计划(已按用户四点修正确认 D1~D5) |
update/SplitDB引擎控制台使用说明.md | ✏️ 改 | 补充单桶容量/归档/紧急扩容配置说明 |
验证结论
php -l全过(15 个改动文件 0 错误)- P41 冒烟脚本(
cli/_tmp_verify_p41.php,用后即删)25/25 通过:bucketForWrite 路由矩阵(未扩容/扩容后新帖进新增范围/历史 created_at 回退旧哈希/边界/非法常量兜底)→ upsertDefine(替换/追加锚点后/锚点缺失追加末尾/完整写 config 内容级验证)→ 新装 is_archived 列 + 归档索引 → 存量库幂等 ALTER(存量行默认 0,零迁移)→ 归档标记分批 5000+1000 两批/幂等重跑 0 行 → 回复守卫 90% 紧急扩容(24h 守卫拦截不抛异常)/100% 拒绝(code=1001)→ maxBucketLoad=100 + 缓存文件 → suggestExpandData 命中/不命中 → 真实 config.php 全程未被修改(sha256 快照一致) - 遗留:浏览器人工复核——① 设置页单桶容量/归档下拉交互 ② 站点设置页建议扩容提示条 ③ 归档帖详情页横幅与回复框隐藏 ④ 扩容后新帖分布(可在后台扩容记录确认)
P40 code_repo 插件修复:服务器 md 文档无法查看($isMarkdown 缺参 + resolvePath 中文名混编码容错)
2026-08-16
结论:修复「md 文档在网络虚拟空间无法浏览,本地正常」的两层根因。①
$isMarkdown未定义警告:服务器部署的镜像版views/file.php缺 getData 变量兜底、RepoController::notFound()/download()阻塞分支未传isMarkdown(P36 视图统一前的旧版)→ 上传根目录最新版file.php+RepoController.php后警告消失。②resolvePath返回 null →「文件不存在或已被禁止访问」:用户反馈的服务器 URL 为决定性证据——仓库目录段SplitDB%BF%AA%B7%A2%CE%C4%B5%B5(GBK 编码「开发文档」)与文件段SplitDB%E5%BC%95%E6%93%8E...(UTF-8 编码「引擎控制台使用说明」)混编码,说明虚拟主机/FTP 上传时中文目录名在磁盘上被存成 GBK 字节,而 URL/会话按 UTF-8 处理;resolvePath用 UTF-8 段直拼路径 →realpath命中不了 GBK 磁盘名 → 误报不存在。修复:resolvePath改为逐段匹配磁盘真实条目——新增matchDiskEntry():对每段生成「原样 / UTF-8→GBK / GBK→UTF-8」三种字节形态,scandir 当前目录逐一比对,命中即采用磁盘真实名继续拼接(保留磁盘字节),再走 realpath + 根内校验。安全不降级(穿越/黑名单检查仍在匹配前执行)。
| 文件 | 状态 | 变更说明 |
|---|---|---|
plugins/code_repo/Plugin.php | ✏️ 改 | 新增 matchDiskEntry()(三种字节形态逐条比对 + 黑名单过滤);resolvePath() 逐段改为 matchDiskEntry 命中磁盘真实名后拼接(注释标注 P40 编码容错:虚拟主机上传中文名可能 GBK 存储) |
plugins/code_repo/RepoController.php | ✏️ 镜像同步 | 根目录既有修复(notFound/download 传 isMarkdown=false)同步到镜像(fc /b 逐字节一致) |
plugins/code_repo/views/file.php | ✏️ 镜像同步 | 根目录既有 getData 兜底版同步到镜像(fc /b 逐字节一致) |
update/plugins/code_repo/ | ✏️ 同步 | robocopy 全量重同步(/XD repos 排除用户数据;/XF *.sqlite);fc /b 逐字节一致 |
nginx-server.conf | ✏️ 改 | 新增 location ^~ /repo/ 前缀块 + try_files $uri /index.php;:.php 结尾的仓库文件 URL 不再被 location ~ \.php 当脚本交给 php-cgi(403),由前端控制器 Router 路由到 RepoController。⚠ 实测不能用 rewrite ... last 转发到 /index.php/{原路径}:若仓库文件名为 .php(如 index.php),转发后 URI 含 .php 段会再次命中 location ~ \.php,phpstudy php-cgi(cgi.fix_pathinfo 默认开启)二次解析 → "No input file specified" → 403;try_files 保持 REQUEST_URI 为 /repo/...,path_info 不含 .php,问题消失 |
验证结论
php -l全过(Plugin.php / RepoController.php / views/file.php 0 错误)- P40 冒烟脚本(
cli/_tmp_repo_p40_smoke.php,用后即删)8/8 通过:UTF-8 段命中(回归)→ 模拟服务器 GBK 字节段命中 md/目录 → 混编码段命中(目录 GBK + 文件 UTF-8) → 安全回归(../.git/ 不存在仍拦截) - 镜像一致性:
fc /bPlugin.php / RepoController.php / file.php 全部逐字节一致 - 待用户操作:上传
plugins/code_repo/Plugin.php覆盖服务器同名文件(file.php / RepoController.php 已覆盖),刷新浏览 md 文档;若仍有异常可跑cli/_diag_repo_server.php诊断脚本(输出磁盘字节 hex 与 resolvePath 命中情况) - 附:本地 Nginx
/repo下.php文件 403 已定位并修复配置(location ^~ /repo/),curl 验证待用户指令(P40b 待记)
P39 code_repo 插件:移动端浏览体验优化
2026-08-16
结论:优化 code_repo 插件在移动端(≤768px)的浏览体验。此前仅 3 条 640px 规则(隐藏文件信息/缩小 padding),窄屏下头部按钮挤压、面包屑换行错乱、目录行信息堆叠、代码区过大。本次重写移动端适配为 768px + 480px 双断点:① 头部 纵向堆叠、按钮整行(flex:1 均分);② 面包屑 单行横向滚动(nowrap + overflow-x auto + 隐藏滚动条),长路径不换行挤压;③ 目录列表 改两行布局(第一行 图标+名称+语言+禁下载徽标,第二行 大小/行数/时间 缩进对齐),信息不再丢失;④ 仓库卡片 单列网格(grid-template-columns:1fr)+ 图标/名称紧凑;⑤ 文件查看 信息行换行、代码区字体 12px + max-height 60vh、MD 预览 padding 缩小;⑥ 480px 超窄屏 进一步收紧按钮/卡片/信息缩进。
| 文件 | 状态 | 变更说明 |
|---|---|---|
plugins/code_repo/assets/style.css | ✏️ 改 | 移动端适配段重写:@media (max-width:768px)(头部堆叠/面包屑滚动/目录两行/卡片单列/代码区缩小/设置表单紧凑)+ @media (max-width:480px)(按钮/卡片/缩进收紧);原 640px 三规则移除 |
update/plugins/code_repo/assets/style.css | ✏️ 同步 | 镜像同步(fc /b 逐字节一致;assets 属插件自包含目录,随插件镜像) |
验证结论
php -l全过(RepoController + 3 视图 0 错误;纯 CSS 变更无 PHP 风险)- 镜像一致性:
fc /bstyle.css 逐字节一致 - 待人工浏览器复核(DevTools 移动端模拟 ≤768px / 480px):① /repo 仓库卡片单列显示;② 仓库目录行两行布局(信息在第二行缩进);③ 长路径面包屑单行横向滚动;④ 文件查看页代码区缩小可横向滚动;⑤ 下载 ZIP/返回按钮整行均分不溢出
P38 code_repo 插件修复:含空格仓库/文件名的二次编码(链接生成统一基于真实名单次编码)
2026-08-16
结论:修复用户反馈「仓库名含空格时点文件浏览仍报错,去空格即可浏览」——根因是 P37 只修了
resolvePath()内部解码,但控制器传给视图的面包屑/链接仍基于 Router 传来的编码串,视图再rawurlencode()一次 → 空格名被二次编码(My%20Repo→My%2520Repo)→ 下一跳请求解码后仍与磁盘真实名不匹配。本次统一编码链路:解码只发生在控制器入口一次(RepoController::orderedSegments()每段rawurldecode、archive()入口同样解码),控制器/视图内部全程使用真实名,生成链接时才rawurlencode()一次(面包屑 URL、browse 目录链接、index 仓库卡、downloadUrl、backUrl 全部按真实名单次编码);Plugin::resolvePath()移除 P37 的内层解码(避免真实文件名含%xx序列时二次误解码),输入即真实名。链路闭环:链接(编码一次) → Router(不解码) → 控制器入口(解码一次=真实名) → resolvePath(不再解码) → 命中磁盘。
| 文件 | 状态 | 变更说明 |
|---|---|---|
plugins/code_repo/RepoController.php | ✏️ 改 | orderedSegments() 每段 rawurldecode()(注释标注:控制器/视图统一真实名,生成链接时再编码一次防二次编码);archive() 入口 rawurldecode($repo);renderDir() 面包屑 URL 每段 rawurlencode 一次;downloadUrl()/backUrl() 返回 URL 每段 rawurlencode 一次(此前直拼真实名,含空格 URL 无编码) |
plugins/code_repo/Plugin.php | ✏️ 改 | resolvePath() 移除 P37 内层 rawurldecode(注释标注:解码统一在控制器入口,此处输入即真实名,防 %xx 文件名二次误解码) |
update/plugins/code_repo/ | ✏️ 同步 | 镜像同步(robocopy /XD repos 排除用户数据);fc /b 逐字节一致 |
验证结论
php -l全过(Plugin.php / RepoController.php / 3 视图 0 错误)- P38 冒烟脚本(
cli/_tmp_repo_p38_smoke.php,用后即删)19/19 通过:含空格仓库My Repo+ 含空格文件hello world.php+ 中文文件说明文档.md全链路——Router 编码段 →orderedSegments一次解码为真实名 →resolvePath命中 realpath;downloadUrl/backUrl含%20且无%25(未二次编码);downloadUrl 往返(编码→解码)再命中;面包屑 URL 单次编码无%25;listRepos/listDir 输出真实名 - 镜像一致性:
fc /bRepoController.php / Plugin.php 逐字节一致 - 待人工浏览器复核:仓库名/文件名含空格(如
My Repo、hello world.php)点击浏览/下载 ZIP/单文件下载均正常
P37 code_repo 插件修复:中文/特殊字符文件名 URL 解码 + 文件标题图标方框
2026-08-16
结论:修复用户反馈的两个问题。① 「路径不存在或已被禁止访问」:根因是
Router::dispatch()用parse_url取路径后不做 urldecode,而 browse 视图链接用rawurlencode()生成——用户把仓库/文件改成中文或含空格等特殊字符名后,URL 是%E4%B8%AD.../%20编码形式,Plugin::resolvePath()里basename()拿到的是编码串,与磁盘真实名(中文/空格)不匹配 →realpath失败 → 误报「路径不存在」。修复:resolvePath()每段先rawurldecode()再 basename 清洗(解码后%2e%2e→..、%2f→/、%00→NUL 仍被既有检查拦截,安全不降级);② 文件查看页标题图标方框:根因是file.php标题处<?php echo $icon; ?>输出裸 HTML 实体()但未包 FontAwesome 字体元素(对比TemplateCompiler::icon()返回<i class="fa" ...>,h2 默认字体不含该字形 → 方框)。修复:包一层<i class="fa" style="font-size:20px">。浏览页.repo-item-icon本就带font-family: FontAwesome不受影响(已核实 fontawesome.css 家族名匹配)。
| 文件 | 状态 | 变更说明 |
|---|---|---|
plugins/code_repo/Plugin.php | ✏️ 改 | resolvePath() 每段首行加 rawurldecode()(注释标注 Router 不自动解码 + 编码穿越串仍被拦截) |
plugins/code_repo/views/file.php | ✏️ 改 | 标题 <?php echo $icon; ?> → <i class="fa" style="font-size:20px"><?php echo $icon; ?></i>(FontAwesome 字体渲染) |
update/plugins/code_repo/ | ✏️ 同步 | 镜像同步(robocopy /XD repos 排除用户数据);fc /b 逐字节一致 |
验证结论
php -l全过(Plugin.php / views/file.php 0 错误)- P37 冒烟脚本(
cli/_tmp_repo_p37_smoke.php,用后即删)16/16 通过:中文仓库/中文文件/含空格文件「编码段解码」解析全部命中 realpath;5 项编码穿越拦截全过(%2e%2e、%2e%2e/%2e%2e、%2f、%00、中文仓库后..);file 视图<i class="fa">结构核对通过;中文/空格文件在 listDir 列表正常、PHP 文件 blocked=true - 镜像一致性:
fc /bPlugin.php / file.php 逐字节一致 - 待人工浏览器复核:① 仓库/文件改名含中文或空格后点击可正常打开;② 文件查看页标题图标显示正常(不再方框)
P36 code_repo 插件视图统一:前台套 mn-section 标准卡片容器 + 后台改 admin-container 标准结构
2026-08-16
结论:按用户要求统一 code_repo 插件视图外层容器规范——其他插件/核心视图最外层均套主题标准大容器(前台
mn-section白底卡片、后台admin-container+ 侧栏 +admin-content结构),而 P34/P35 的 code_repo 用的是自定义.repo-page(仅 max-width/margin/padding,无白底卡片外观),风格不一致。本次统一:① 前台 3 视图(index/browse/file)最外层<div class="repo-page">→<div class="mn-section mn-p-24">(主题标准卡片:白底/圆角/边框,padding 24px);② 后台 admin 视图 重写为admin-container+include('admin/_sidebar')+<main class="admin-content">+admin-header+admin-section结构,与 announcements/friend_links 后台完全同构,表单改用后台标准类(form-group/form-input/alert/btn);③ style.css 适配:删除.repo-page规则(视图已不再引用,注释说明前台用主题标准容器),修正后台视图误用 friend_links 私有类fl-label-check→ 本插件repo-check-label;保留 .repo-msg 系列死代码(防回退惯例,后台已改用标准 alert)。
| 文件 | 状态 | 变更说明 |
|---|---|---|
plugins/code_repo/views/index.php | ✏️ 改 | 最外层 repo-page → mn-section mn-p-24(仓库卡片列表) |
plugins/code_repo/views/browse.php | ✏️ 改 | 最外层 repo-page → mn-section mn-p-24(目录树) |
plugins/code_repo/views/file.php | ✏️ 改 | 最外层 repo-page → mn-section mn-p-24(文件查看) |
plugins/code_repo/views/admin.php | ✏️ 重写 | 改 admin-container + _sidebar + admin-content + admin-header + admin-section 标准后台结构;表单用 form-group/form-input/alert/btn;fl-label-check → repo-check-label |
plugins/code_repo/assets/style.css | ✏️ 改 | 删除 .repo-page 规则(max-width/margin/padding,已无引用);文件头注释说明前台用主题标准容器 |
update/plugins/code_repo/ | ✏️ 同步 | 镜像全量重同步(robocopy /XD repos 排除用户仓库数据;data/ 仅防护文件);fc /b 逐字节一致 |
验证结论
php -l全过(12 个 PHP 文件 0 错误:4 视图 + 2 控制器 + Plugin + 5 hook)- 镜像一致性:
fc /bindex/browse/file/admin 四视图 + style.css 全部逐字节一致 - 镜像范围正确:
update/plugins/code_repo/data/仅.htaccess+web.config(用户仓库数据 repos/ 未入镜像) - 待人工浏览器复核:前台 /repo 三页显示白底卡片(与论坛/私信等页面观感一致);后台「扩展 → 代码仓库」页显示标准后台布局(含左侧菜单栏)
P35 code_repo 插件调整:仓库默认路径移至插件目录 + 仓库目录防护 + 高亮主题本地化 + 导航移至「应用」
2026-08-16
结论:按用户要求对 P34 的 code_repo 插件做四项调整:① 仓库默认路径改为插件目录
plugins/code_repo/data/repos/(三重优先级:后台设置 > config 常量 > 内置默认,三处同步;插件自包含,整目录打包即迁移);② 仓库目录 Web 访问防护:新增plugins/code_repo/data/.htaccess(Apache deny)+web.config(IIS hiddenSegments: repos)+nginx-server.conf追加location ~ ^/plugins/code_repo/data/ { deny all; return 403; }(Nginx 兜底——此前 plugins/ 目录无任何 Web 防护,仓库源码可被直接下载);③ 高亮主题 CSS 本地化:下载github.min.css(1309B)内置至assets/highlight-github.min.css,视图本地优先、CDN 回退(修复浏览器访问不了 cdnjs 时高亮无颜色的问题);④ 导航入口从页眉主导航移至「应用」:plugin.jsonhooks 改nav_plugin_links,删除hook/nav_main_links.php,新建hook/nav_plugin_links.php(含/repo激活态mn-cat-active)——桌面端进左侧栏「应用」面板,移动端进顶部「应用」下拉。
| 文件 | 状态 | 变更说明 |
|---|---|---|
plugins/code_repo/Plugin.php | ✏️ 改 | rootPath 内置默认 dirname(__DIR__,2)/data/repos → __DIR__ . '/data/repos'(即 plugins/code_repo/data/repos) |
config.php | ✏️ 改 | CODE_REPO_ROOT 常量 __DIR__/data/repos → __DIR__/plugins/code_repo/data/repos |
install.php | ✏️ 改 | configTemplate() 模板同步新常量值(新装生成一致) |
plugins/code_repo/data/.htaccess | 🆕 新建 | Apache 禁止直接访问插件数据目录(Require all denied + FilesMatch 双保险) |
plugins/code_repo/data/web.config | 🆕 新建 | IIS hiddenSegments: repos(返回 404) |
nginx-server.conf | ✏️ 改 | 追加 location ~ ^/plugins/code_repo/data/ deny(P35 注释标注) |
plugins/code_repo/assets/highlight-github.min.css | 🆕 新建 | GitHub 浅色主题本地内置(highlight.js 11.9.0 配套,1309B) |
plugins/code_repo/views/file.php | ✏️ 改 | 高亮 CSS 本地优先($hasHighlightCss 判断),缺失回退 CDN |
plugins/code_repo/plugin.json | ✏️ 改 | hooks: nav_main_links → nav_plugin_links |
plugins/code_repo/hook/nav_main_links.php | 🗑 删除 | 页眉主导航入口移除 |
plugins/code_repo/hook/nav_plugin_links.php | 🆕 新建 | 左侧栏「应用」/顶部「应用」下拉入口($__nav_uri 判断 /repo 激活态) |
plugins/code_repo/说明.md | ✏️ 改 | 目录结构(data/ 防护 + 新 assets)、默认路径、Hooks 表、安全节(三重防护)、导航说明同步 |
update/plugins/code_repo/ | ✏️ 同步 | 镜像全量重同步(旧 nav_main_links.php 已清、repos/ 用户数据不纳入镜像、data/ 仅含防护文件);fc /b 逐字节一致 |
update/config.php / update/install.php / update/nginx-server.conf | ✏️ 改 | 镜像同步(fc /b 逐字节一致) |
验证结论
php -l全过(8 个 PHP 文件 0 错误)- P35 冒烟脚本(
cli/_tmp_repo_p35_smoke.php,用后即删)23/23 通过:默认路径 realpath 归一 =plugins/code_repo/data/repos(含修正:内置默认曾误写dirname(__DIR__)/data/repos少一层,已修为__DIR__)→ ensureRoot 建目录 → 防护文件齐全(.htaccess deny / web.config hiddenSegments / nginx deny / 双高亮资源)→ resolvePath 合法解析 + 5 项穿越拦截 → 列表/ZIP/版本检测 - 镜像一致性:
fc /bconfig / install / nginx-server.conf / Plugin.php / nav_plugin_links.php / highlight-github.min.css / data/.htaccess / data/web.config 全部逐字节一致;旧 nav_main_links.php 已从镜像清除 - 待人工浏览器复核:左侧栏「应用」出现「代码仓库」入口且 /repo 激活高亮、代码高亮显示颜色(本地 CSS)、仓库目录 HTTP 直访 403(
curl http://localhost/plugins/code_repo/data/repos/xxx应拒绝)
P34 新增 code_repo 插件:代码浏览与下载中心(/repo)
2026-08-16
结论:新增
plugins/code_repo/插件,在 FlintHub 中搭建类似 GitHub 的代码浏览与下载中心——仅做代码展示与下载,不含 Git 管理,所有数据经文件系统读取(不建独立 SQLite 库)。核心能力:① 仓库列表(CODE_REPO_ROOT下每子目录 = 一个仓库,卡片展示);② 多层目录浏览(/repo/{仓库}/{子路径...},最多 8 层,面包屑导航);③ 文件查看(highlight.js 本地内置 11.9.0 高亮 + marked.min.js Markdown 预览 + 大小/修改时间/行数);④ ZIP 打包下载(ZipArchive,文件名仓库名_版本或日期.zip,过滤黑名单目录);⑤ 单文件下载(.php/.sqlite等敏感扩展名禁下载仅高亮);⑥ 安全隔离(路径段 basename 清洗 + realpath 根内校验防穿越、.git/node_modules等隐藏目录黑名单、2MB 查看阈值/512KB 截断)。权限默认仅管理员(Auth::isAdmin()),后台设置页提供「开放给登录用户」开关(settings 键code_repo_open_users)。仓库根路径三重优先级:后台设置code_repo_root> config.php 常量CODE_REPO_ROOT(默认__DIR__ . '/data/repos')> 内置默认;config.php 与 install.php 模板同步新增常量定义。
| 文件 | 状态 | 变更说明 |
|---|---|---|
plugins/code_repo/plugin.json | 🆕 新建 | 插件清单(hooks: init_after / route_register / nav_main_links / admin_route_register / admin_sidebar_links) |
plugins/code_repo/Plugin.php | 🆕 新建 | 核心助手:rootPath(三重优先级)/ resolvePath(安全路径)/ isBlockedName(隐藏目录黑名单)/ isDownloadBlocked(敏感扩展名)/ listRepos / listDir / countFiles / countLines / readForView(大文件截断)/ languageFor / iconFor / formatSize / formatTime / repoVersion / createZip(RecursiveCallbackFilterIterator 过滤黑名单) |
plugins/code_repo/RepoController.php | 🆕 新建 | 前台控制器:index(列表)/ browse(目录树或文件,自动判断)/ file(显式文件查看)/ download(单文件下载)/ archive(ZIP)/ ensureAccess(权限开关)/ notFound |
plugins/code_repo/AdminController.php | 🆕 新建 | 后台设置:仓库根目录路径(绝对路径清洗 + 禁站点内部目录)+ 开放开关,存 settings 表 |
plugins/code_repo/hook/route_register.php | 🆕 新建 | 前台多深度路由:/repo、/repo/{p0}/archive(先注册防吞)、/repo/{...}/file、/repo/{...}/download、/repo/{...} 浏览(各 1~8 段,循环注册) |
plugins/code_repo/hook/admin_route_register.php | 🆕 新建 | 后台路由 /admin/repo-settings(GET 展示 / POST 保存) |
plugins/code_repo/hook/admin_sidebar_links.php | 🆕 新建 | 后台「扩展」组侧栏入口 |
plugins/code_repo/hook/nav_main_links.php | 🆕 新建 | 顶部导航「代码仓库」菜单项 |
plugins/code_repo/hook/init_after.php | 🆕 新建 | 确保 CODE_REPO_ROOT 目录存在 |
plugins/code_repo/views/index.php | 🆕 新建 | 仓库列表(卡片:名称/文件数/更新时间) |
plugins/code_repo/views/browse.php | 🆕 新建 | 目录树(面包屑 + 图标 + 大小/行数/时间 + 禁下载徽标 + 下载 ZIP 按钮) |
plugins/code_repo/views/file.php | 🆕 新建 | 文件查看(highlight.js 高亮 / marked MD 预览 / 禁下载提示 / 截断提示) |
plugins/code_repo/views/admin.php | 🆕 新建 | 后台设置表单(根路径 + 开放开关 + 生效路径/可写性提示) |
plugins/code_repo/assets/style.css | 🆕 新建 | 卡片/目录树/代码区/MD 预览/设置表单样式(含移动端适配) |
plugins/code_repo/assets/highlight.min.js | 🆕 新建 | highlight.js v11.9.0 本地内置(CDN 下载 121KB,离线可用;缺失时视图回退 CDN) |
plugins/code_repo/说明.md | 🆕 新建 | 使用说明(功能/目录/路由/权限/安全/步骤) |
config.php | ✏️ 改 | 新增 define('CODE_REPO_ROOT', __DIR__ . '/data/repos');(SplitDB 数据引擎段后) |
install.php | ✏️ 改 | configTemplate() 模板同步新增 CODE_REPO_ROOT 常量(新装生成一致) |
update/plugins/code_repo/ | 🆕 新建 | update/ 镜像(16 文件,fc /b 逐字节一致;update/ 首次新增 plugins/ 镜像目录) |
update/config.php / update/install.php | ✏️ 改 | 镜像同步(fc /b 逐字节一致) |
验证结论
php -l全过:Plugin.php / RepoController.php / AdminController.php / 5 个 hook(8 个文件 0 错误)- 冒烟脚本(
cli/_tmp_repo_smoke.php,用后即删)42/42 通过:rootPath 解析 → 样例库构建 → listRepos 过滤 → resolvePath 合法多级解析 → 8 项路径穿越/黑名单拦截全过(.. / ../../ / 斜杠段 / .git / node_modules / 不存在 / 空段 / . 段)→ listDir 黑名单过滤 → 文件信息(lang=php / 行数=4 / 禁下载 / 图标 / formatSize)→ 语言图标映射 → createZip(含 User.php+README.md、不含 .git/node_modules)→ repoVersion=v2.1.0 → 大文件截断(truncated=true / ≤512KB / countLines=null) - 镜像一致性:
fc /bconfig.php / install.php / Plugin.php 全部逐字节一致 - 待人工浏览器复核:/repo 页面渲染、目录浏览、高亮与 MD 预览、ZIP 下载、后台设置保存
P33 编辑器固定高度:PC 550px / 移动端 300px(长文内部滚动,不随内容增长)
2026-08-16
结论:用户补充要求「不止进入时高度固定,写入长文时高度也要固定」。P32 实现为
min-height(随内容撑开),本次改为固定 height——可视化编辑区与 Markdown textarea 均固定为 PC 550px / 移动端 300px,长文由内部滚动承载(.editor-editable已有overflow-y: auto)。改动:CSS.editor-editablemin-height→height(PC 550 / 移动 300);JS 全部style.minHeight→style.height(初始化可视化区/初始化 md 区/全屏退出恢复/全屏进入清空);模式切换回可视化处由「跟随mdTextarea.scrollHeight增长」改为「固定defaultEditorHeight()」;defaultEditorHeight()(P32 新增,matchMedia 768px 断点)继续作为唯一高度来源。node --check通过,editor.js 已无minHeight残留。编辑页面(thread/post/blog edit 等)均经data-editor+ autoInit 走同一逻辑,无需单独配置。
| 文件 | 状态 | 变更说明 |
|---|---|---|
assets/css/editor.css | ✏️ 改 | .editor-editable min-height: 550px → height: 550px(PC);移动端 min-height: 300px → height: 300px(注释标注 P32/P33 固定高度、长文内部滚动) |
assets/js/editor.js | ✏️ 改 | 初始化可视化区(L800)/ md textarea(L811)style.minHeight→style.height;全屏退出恢复(L744)minHeight→height、全屏进入清空(L749)minHeight→height;模式切换回可视化(L1455)由 Math.max(defaultEditorHeight(), mdTextarea.scrollHeight) 改为固定 defaultEditorHeight() |
验证结论
node --check assets/js/editor.js通过;grep 确认 editor.js 无minHeight残留- 规则核对:CSS 双端固定高度(550/300)与 JS
defaultEditorHeight()一致;5 处 JS height 触点全部生效 - 影响面:仅
assets/js/editor.js+assets/css/editor.css(assets/ 不在 update/ 镜像范围,无需同步) - 遗留:编辑页/发帖/回复/私信编辑器共用同一组件,建议浏览器实测长文输入时高度不增长、内部滚动正常
P32 编辑器默认高度调整:PC 550px / 移动端 300px(响应式)
2026-08-16
结论:按用户推荐规格调整编辑器默认高度——PC 端 550px(舒适展示长文本,无需频繁滚动)、移动端 300px(配合软键盘,不遮挡输入区域)。实现:新增
defaultEditorHeight()(window.matchMedia('(max-width: 768px)')判断,断点与 editor.css 移动端媒体查询一致),替换原options.height || 400默认值;CSS.editor-editablemin-height 由 200px(移动 250px)改为 550px(移动 300px);全屏退出恢复与可视化切换回同步高度两处Math.max(300, ...)同步改用响应式默认值。node --check通过。
| 文件 | 状态 | 变更说明 |
|---|---|---|
assets/js/editor.js | ✏️ 改 | 新增 defaultEditorHeight()(PC 550 / 移动 300,matchMedia 768px 断点);initEditor 默认高度、全屏退出恢复(L743)、可视化切换回同步(L1455)三处改用该函数 |
assets/css/editor.css | ✏️ 改 | .editor-editable min-height 200px → 550px(PC);移动端媒体查询内 250px → 300px |
验证结论
node --check assets/js/editor.js通过(JS 语法零错误)- 规则核对:CSS 双端(550/300)与 JS
defaultEditorHeight()返回值一致;三处 JS 引用点全部生效 - 影响面:仅
assets/js/editor.js+assets/css/editor.css(assets/ 不在 update/ 镜像范围,无需同步) - 遗留:编辑器高度为 CSS/JS 双端设置,建议浏览器实测 PC/移动端各一次
P31 修复:Markdown 模式发帖后打开帖子详情页被自动滚动定位到回复框
2026-08-16
结论:用户反馈「用 Markdown 格式发帖后,从首页/论坛/分类页点击帖子进入,页面直接定位到回复框;可视化编辑则正常」。根因定位:P26 引入的编辑器模式记忆(
localStorage.flinthub_editor_mode)与详情页回复编辑器自动初始化叠加——ThreadController::show传needsEditor=true使editor.js加载于详情页,回复<textarea data-editor>被autoInit()接管;初始化时若 localStorage 记为markdown,initEditor()调用switchEditorMode(..., 'markdown'),其 markdown 分支无条件执行mdTextarea.focus(),浏览器将页面滚动到回复框;可视化模式分支无 focus 故正常。修复:switchEditorMode()增加第 8 参skipFocus(默认 false),初始化恢复路径传true跳过 focus;用户手动点击 tab 切换(未传 skipFocus)仍保留焦点行为。node --check通过。
| 文件 | 状态 | 变更说明 |
|---|---|---|
assets/js/editor.js | ✏️ 改 | switchEditorMode(editorId, mode, tabs, editor, editorWrap, mdWrap, mdTextarea, skipFocus):markdown 分支 if (!skipFocus) mdTextarea.focus();initEditor() localStorage 恢复路径调用时传 true(注释标注修复原因);手动 tab 点击路径(L942)不传参保持焦点 |
验证结论
node --check assets/js/editor.js通过(JS 语法零错误)- 两处调用点核对:手动 tab 切换(未传 skipFocus → focus 保留)✅;初始化恢复(传 true → 跳过 focus)✅
- 影响面:仅
assets/js/editor.js(assets/ 不在 update/ 镜像范围,无需同步) - 遗留:滚动行为依赖浏览器 focus 语义,建议人工验证一次(Markdown 模式发帖 → 打开帖子详情页不再自动滚到回复框;手动切 tab 仍获得焦点)
P30 控制台拆分与操作日志查看:SplitDB 概览/设置分页 + 菜单两组重组 + 操作日志页
2026-08-16
结论:为更清晰地区分运维功能,将 P29 引擎控制台拆分重构。① 页面拆分:
/admin/database/engine拆为/admin/database/overview(引擎概览,只读监控:分片分布/总帖子数/建议桶数/当前季度 + 60s 统计缓存 + 刷新按钮)与/admin/database/settings(引擎设置,操作型:桶数调整/自动扩容/扩容记录/一致性检查);旧/engine302 重定向到 overview,旧单页视图删除。② 菜单重组:原「数据管理」组拆为「数据管理」(SplitDB 概览 / SplitDB 设置 / 备份优化)与「站长工具」(操作日志 / 维护工具)两组。③ 新增操作日志页/admin/logs/audit:分页列表(时间/管理员/操作类型/目标/IP/详情)+ 按日期与操作类型筛选;操作类型下拉从audit_logs表DISTINCT action动态读取(免维护,实测走 COVERING INDEX idx_audit_logs_action 亚毫秒级)+ 60s 文件缓存;AuditLog::query()追加可选$date参数(向后兼容,LIKE 前缀命中 idx_audit_logs_created)。④ 权限:操作日志保持 need_admin()(BaseController 构造器;系统无 moderator 角色体系,已留扩展点)。
| 文件 | 状态 | 变更说明 |
|---|---|---|
index.php | ✏️ 改 | 新增 /database/overview(GET) + /database/settings(GET/POST) + /logs/audit(GET) 路由;/database/engine 保留指向 engine()(现已重定向) |
app/Controllers/Admin/DatabaseController.php | ✏️ 改 | engine() 拆为 overview()(只读渲染,复用 P29 助手)+ settings()(save_buckets/save_auto_expand/consistency POST + maybeAutoExpand 触发点迁移 + 扩容记录查询 LIMIT 10);engine() 改为 redirect('/admin/database/overview') |
app/Views/admin/database/overview.php | 🆕 新建 | 只读概览视图(引擎状态 + 分片分布统计 + 刷新按钮 + 跳转设置页横幅) |
app/Views/admin/database/settings.php | 🆕 新建 | 操作型视图(桶数调整 / 自动扩容 / 扩容记录 / 一致性检查) |
app/Views/admin/database/engine.php | 🗑️ 删除 | 旧单页视图(不再引用,镜像同步删除) |
app/Controllers/Admin/LogController.php | 🆕 新建 | 操作日志控制器:audit()(page/action/date 白名单清洗 + 分页钳制)+ distinctActions()(DISTINCT action + 60s 文件缓存) |
app/Views/admin/logs/audit.php | 🆕 新建 | 操作日志视图(GET 筛选表单 + 分页列表 + 带参分页条) |
app/Helpers/AuditLog.php | ✏️ 改 | query() 追加可选 $date(YYYY-MM-DD,LIKE 前缀)参数,向后兼容 |
app/Views/admin/_sidebar.php | ✏️ 改 | 菜单重组:数据管理(SplitDB 概览/设置/备份优化)+ 站长工具(操作日志/维护工具)两组 |
update/SplitDB控制台拆分与操作日志改造计划.md | 🆕 新增 | 本功能改造计划(含权限评估与 DISTINCT 性能实测) |
update/SplitDB引擎控制台使用说明.md | ✏️ 改 | 同步概览/设置拆页 + 操作日志页说明 |
验证结论
php -l全过(index.php / DatabaseController / LogController / AuditLog / _sidebar / overview / settings / audit 视图)- 镜像
fc /b8 文件逐字节一致(engine.php 从镜像删除同步) - 功能自测(临时脚本用后即删):overview 数据返回、settings 各 POST 动作、扩容记录查询(含空态)、AuditLog 日期筛选(LIKE 命中/空结果)、engine 重定向 302、侧边栏 5 链接映射核对
- 遗留:操作日志筛选/分页交互依赖浏览器,CLI 无法全覆盖,建议人工复核一次
P29 新增 SplitDB 引擎控制台(后台分片监控 + 桶数动态调整 + 自动扩容 + 一致性检查)
2026-08-16
结论:新增后台页面
/admin/database/engine「SplitDB 引擎控制台」,用于管理和监控分片状态,并支持动态调整桶数。核心前提(扩容零迁移):桶数增加后旧数据原地不动、不迁移不重哈希——新帖按ID % 新桶数路由(ShardRouter::bucketSize()实时读常量),旧帖读取永远以main_index.bucket_path为唯一依据;仅改 config 一个常量即完成扩容。五大模块:① 引擎概览(当前桶数/当前季度/分片分布统计/总帖数/建议桶数,60s 控制台缓存 + 手动刷新);② 桶数动态调整(下拉 32/64/128/256,只增不减,安全写 config 四步:备份→正则替换→写回→opcache 绝对路径失效);③ 自动扩容(开关+阈值+24h 间隔守卫+flock 防重入,触发点=引擎页加载 + cron_trigger/cli worker 消费末尾,worker 静态 60s 节流);④ 备份与优化(复用既有 handleBackup/handleOptimize/handleRestore/handleImport,跳转回本页);⑤ 一致性检查(当前季度/全部历史范围可选,桶汇总 vs 索引对拍,不一致红色高亮)。安全:全 POST Csrf、桶数白名单、阈值 intval、config 备份先行、写失败不覆盖、config 不可写时按钮置灰友好提示。
| 文件 | 状态 | 变更说明 |
|---|---|---|
index.php | ✏️ 改 | 后台组新增 GET/POST /database/engine → AdminDatabaseController::engine 路由 |
app/Controllers/Admin/DatabaseController.php | ✏️ 改 | 新增 engine() + currentBucketSize()(读文件优先)/handleSaveBuckets()/handleSaveAutoExpand()/updateBucketSize()(静态,flock 防重入+四步写 config)/maybeAutoExpand()(静态,供三触发点)/bucketStatsCached()(60s 缓存)/bucketStatsLive()(query_only 逐桶直连)/totalThreads()/suggestBuckets()/backupList()/consistencyCheck();handleBackup/handleOptimize/handleRestore/handleImport 复用,跳转目标由 engine() 控制 |
app/Views/admin/database/engine.php | 🆕 新建 | 五大模块视图(引擎概览/桶数调整/自动扩容/备份优化/一致性检查),含刷新统计、config 不可写置灰、Alpine 确认弹窗 |
app/Views/admin/_sidebar.php | ✏️ 改 | 「数据管理」组新增「引擎控制台」子链接(__nav_active=database_engine) |
cron_trigger.php | ✏️ 改 | 消费末尾调用 DatabaseController::maybeAutoExpand()(触发点 2/2,无需管理员访问) |
cli/worker.php | ✏️ 改 | --once 末尾 / 常驻空闲回调触发 maybeAutoExpand()(静态变量 60s 节流) |
app/SplitDB/QueueConsumer.php | ✏️ 改 | consume() 新增可选 $onIdle 空闲回调参数(向后兼容,默认 null) |
update/SplitDB引擎控制台改造计划.md | 🆕 新增 | 本功能改造计划(已确认,含第三方架构微调建议) |
update/SplitDB引擎控制台使用说明.md | 🆕 新增 | 维护使用说明 |
验证结论
php -l全过(index.php / DatabaseController / engine 视图 / _sidebar / cron_trigger / worker / QueueConsumer)- 镜像
fc /b7 文件逐字节一致(index.php / DatabaseController / engine.php / _sidebar / cron_trigger / worker / QueueConsumer) - 冒烟脚本(
cli/_tmp_verify_p29.php,用后即删):config 正则替换往返、桶统计(60s 缓存命中/过期、query_only)、一致性对拍(当前季度/全部历史)、自动扩容阈值触发、24h 间隔守卫、flock 防重入、config 不可写分支 - 遗留:编辑器表格/模式记忆等既有遗留项不变;桶数调整实际生效依赖 PHP-FPM 下一请求重新加载 config.php(opcache 已失效)
P28 安装安全加固:data/protected 目录防下载 + 全新安装链路修复
2026-08-16
结论:安装链路安全审计发现 2 项严重问题 + 1 项隐藏致命 bug,本次一并修复。① data/ 与 protected/ 可被 HTTP 直接下载(curl 实测 SQLite 库/日志/legacy 归档返回 206,含密码哈希/会话/全量数据)→ 新增
data/.htaccess、protected/.htaccess(Apache Require all denied)+nginx-server.conf增加location ~ ^/(data|protected)/ { deny all; }规则。② 全新安装 fatal bug(本会话 P27 引入):idx_reply_index_uid被插在CREATE TABLE reply_index之前,全新安装报no such table: main.reply_index→ 已移到建表之后(smoke_install 抓获并固化断言)。③ smoke_install 过时断言修复:原断言「search_index 已在 business.sqlite 内」为 P5 时代行为,P21 已剥离至季度分文件 → 更新为 P21 断言 + 新增 P27 索引断言。④ data/.htaccess 安装时自动生成:data/ 为安装动态创建,静态文件无法随代码分发 →Schema::bootstrap内新增ensureProtectionFiles()幂等生成。
| 文件 | 状态 | 变更说明 |
|---|---|---|
data/.htaccess | 🆕 新增 | Apache 防护:Require all denied(2.2/2.4 兼容双保险) |
data/web.config | 🆕 新增 | IIS/Win 主机防护:requestFiltering hiddenSegments(命中返回 404) |
protected/.htaccess | ✏️ 改 | Apache 防护:升级为完整 deny 规则(原 8 行简化版) |
protected/web.config | 🆕 新增 | IIS/Win 主机防护:requestFiltering hiddenSegments |
nginx-server.conf | ✏️ 改 | 新增 `location ~ ^/(data\ |
app/SplitDB/Schema.php | ✏️ 改 | ① idx_reply_index_uid 移到 reply_index 建表之后(修全新安装 fatal);② 新增 ensureProtectionFiles():bootstrap 时幂等生成 data/.htaccess + data/web.config(Apache/IIS 双覆盖,新装站自动带防护) |
cli/smoke_install.php | ✏️ 改 | search_index 断言更新为 P21 季度分文件版(+索引数断言);新增 P27 uid 索引断言(topic/reply) |
验证结论
- smoke_install 34/34 通过(修复前 29/30,且 bootstrap 直接 fatal;修复后全绿)
- 关键修复点:全新空目录 bootstrap 不再报
no such table: main.reply_index;search_index正确存在于季度分文件且含 4 个索引;idx_topic_index_uid/idx_reply_index_uid全新安装即就绪 php -l通过;update/镜像fc /b逐字节一致(Schema/smoke_install/nginx-server.conf/protected 下 .htaccess+web.config 均 OK)- ⚠️ 部署提醒:防护文件按 Web 服务器类型生效——Apache 用 .htaccess、IIS/Win 主机用 web.config、Nginx 用 nginx-server.conf 的 deny 规则(需 include 到站点配置并 reload);本地 phpstudy 未 include 时 curl 仍返回 206,属环境配置未生效而非规则缺失
P27 全盘审计修复:缓存键页码钳制(中危)+ 个人中心/个人主页 uid 索引
2026-08-16
结论:全盘审计(
update/全盘审计报告_20260816.md)发现 1 项中等问题 + 用户反馈个人中心/个人主页载入时间偏长,本次一并修复。① 缓存键页码钳制:PageCache::render()(唯一写盘路径)原用未钳制$cachePage生成缓存键,恶意遍历?page=1..100000可各生成一个缓存文件(磁盘 DoS/缓存污染)→ 论坛/分类页写盘键改用钳制后的$page(越界页码与末页共用一文件,总量封顶 totalPages);首页无分页,缓存键固定home_1(原home_{N}无上限)。② 个人中心/个人主页性能:topic_index/reply_index原无 uid 索引,User::getThreads/getStats及 user_profile 插件同构查询全表扫描(19.2 万行实测 getThreads 80ms + getStats 25ms/35ms)→ 新增idx_topic_index_uid(uid,status,deleted_at,create_time DESC,id DESC)与idx_reply_index_uid(uid,status),实测降至 0.01~0.1ms。索引首列 uid,列表查询(只按 status/deleted_at/category_id 过滤)不会被规划器误选(P25 教训复核:getIndexPaginated 仍走 idx_topic_index_list、getByCategoryPaginated 仍走 idx_topic_index_category)。
| 文件 | 状态 | 变更说明 |
|---|---|---|
app/Controllers/ForumController.php | ✏️ 改 | index()/category() 的 PageCache::render 缓存键改用钳制后的 $page(原 $cacheKey 基于未钳制 $cachePage,恶意页码可刷爆缓存目录) |
app/Controllers/HomeController.php | ✏️ 改 | 首页无分页:serve/render 缓存键固定 home_1(忽略 page 参数,原 home_{N} 无上限可被遍历刷文件) |
app/SplitDB/Schema.php | ✏️ 改 | ensureMainIndex() 新增 idx_topic_index_uid(uid,status,deleted_at,create_time DESC,id DESC) + idx_reply_index_uid(uid,status)(幂等,含 P25 教训注释:索引首列 uid 防列表查询误选) |
data/meta/main_index.sqlite | 🗄️ 应用 | 存量库已执行相同 CREATE INDEX IF NOT EXISTS(幂等落地) |
验证结论
- 缓存键钳制:
php -l通过;越界页码(如 ?page=99999)渲染后写盘键=末页键(forum_0_{totalPages}),不再产生独立文件;首页任何 ?page=N 均读写 home_1 - uid 索引:存量库落地后实测——getThreads 80ms→0.03~0.1ms、getStats#1 25ms→0.01~0.04ms、getStats#2 35ms→0.01~0.02ms、getBlogs 0.03~0.19ms
- 规划器复核(P25 教训):加索引后 EXPLAIN——profile 三查询均 SEARCH/COVERING INDEX 走新索引;
getIndexPaginated仍 SCAN idx_topic_index_list、getByCategoryPaginated仍 SEARCH idx_topic_index_category(无 TEMP B-TREE 回归) - 全部改动文件
php -l通过;update/镜像fc /b逐字节一致(ForumController/HomeController/Schema 均 OK)
P26 功能收尾与体验优化:后台查询/分类筛选、前后台分页统一、编辑器表格与样式保真、表格溢出修复
2026-08-16
结论:SplitDB 落地工作接近尾声,本系列为功能收尾与体验优化。① 后台用户管理新增查询(关键词/字段/角色/状态筛选,分页带参不丢条件);② 后台帖子按分类筛选 + 批量删除/移动联动(操作后 redirect 仍停留当前分类);③ 前后台分页统一(pagination() 移除 admin/mn 双分支、后台默认显示上一页/下一页、全部 min(1000) 页码上限钳制到真实 totalPages);④ 论坛/分类页游标模式页码条消失与高亮停在 1 修复(统一渲染 + 游标链接携带 page);⑤ 编辑器 Markdown 表格/样式保真(A=turndown 补 GFM 表格插件、B=服务端 style 白名单+span 放行、C=模式 localStorage 记忆);⑥ 表格溢出双保险修复(max-width/table-layout/word-break/容器 overflow-x,前台+编辑器同步);⑦ 页脚/页头/导航 UI 调整。另新增
update/程序介绍.md、update/SplitDB介绍.md(含与 MySQL 小/大数据量对比)与update/会话交接文档_20260816_P26版.md(含全盘审计指引)。
| 文件 | 状态 | 变更说明 |
|---|---|---|
app/Models/User.php | ✏️ 改 | 新增 adminSearch/countAdminSearch/buildAdminSearchWhere(后台用户查询:LIKE 通配符转义 + ESCAPE、field/role/status 白名单、参数绑定) |
app/Controllers/Admin/UserController.php | ✏️ 改 | index() 列表分支读取 q/field/role/status 筛选参数并回传视图 |
app/Views/admin/users.php | ✏️ 改 | GET 搜索表单(关键词+字段+角色+状态下拉,回显)+ 分页条带全部筛选参数 |
app/Controllers/Admin/ThreadController.php | ✏️ 改 | 列表按 category_id 筛选;batch_delete/batch_move redirect 拼 category_id 保筛选;页码钳制 totalPages |
app/Controllers/Admin/BaseController.php | ✏️ 改 | batchMoveIds() 新增 $extraQuery 参数;redirect 修正为 ?msg=moved_N(原格式与视图解析不匹配,移动提示从未显示,顺带修复) |
app/Views/admin/threads.php | ✏️ 改 | 分类筛选下拉(onchange 跳转)+ JS 批量表单携带 hidden category_id + 分页带参 |
app/Core/TemplateCompiler.php | ✏️ 改 | pagination() 移除 admin/mn 双分支统一 mn 样式;prevnext 默认显示;上一页/下一页条件放宽(`$page>1\ |
app/Views/forum/index.php / category.php | ✏️ 改 | 移除 cursorMode 专用分支统一 pagination() 渲染;边界守卫(第1页无上一页/末页无下一页);游标链接携带 page=N 修复高亮 |
app/Controllers/Admin/TrashedController.php | ✏️ 改 | 页码钳制 totalPages(解除 min(1000)) |
app/Controllers/BlogController.php | ✏️ 改 | 列表 page + 详情 comment_page 钳制 totalPages(总数统计提前) |
app/Controllers/SearchController.php | ✏️ 改 | 按 Tab 全局总数钳制页码(先钳制再取数据,空关键词兜底) |
app/Controllers/TagsController.php | ✏️ 改 | 按返回 total 钳制页码,越界回落最后一页并重查数据;权限过滤后再钳制 |
app/Views/layouts/main.php | ✏️ 改 | 页脚去 SVG → Powered by FlintHub+版本号+官网链接(前后台统一);页眉「管理」PC 只留图标 |
assets/css/modern.css | ✏️ 改 | 页头描述字号 11→12px、logo 间距 gap 2px;导航 margin-left 60→20px;表格溢出双保险 |
assets/css/editor.css | ✏️ 改 | .editor-editable table/th/td 应用与前台相同的表格溢出规则 |
assets/js/editor.js | ✏️ 改 | 加载并注册 turndown-plugin-gfm(表格→GFM);编辑器模式 localStorage 记忆(初始化恢复+切换保存) |
assets/js/turndown-plugin-gfm.js | 🆕 新增 | 官方 GFM 表格转换插件本地化(tableCell/tableRow/table/tableSection 规则) |
app/Helpers/Content.php | ✏️ 改 | allowedAttrs 增加 style + 新增 sanitizeCssStyle() 白名单过滤(color/font-size/text-align/background-color/border-radius + 危险模式拦截);allowedTags 增加 span |
update/程序介绍.md | 🆕 新增 | 程序全貌介绍(含 SplitDB 落地章节) |
update/SplitDB介绍.md | 🆕 新增 | SplitDB 白皮书+落地+与 MySQL 小/大数据量对比 |
update/会话交接文档_20260816_P26版.md | 🆕 新增 | 本系列交接文档(含全盘审计指引) |
验证结论
- 后台用户查询:5 万用户库实测(无筛选=50008 与全表 COUNT 对拍一致、q/role/status 组合正确、注入返回 0 行、通配符转义生效)
- 后台帖子分类筛选:7 版块逐一过滤总数/列表正确,批量操作 redirect 保筛选
- 前后台分页:页码条所有模式下完整渲染、高亮随翻页推进、越界页码回落最后一页
- 编辑器:tables 插件注册规则齐全、marked 还原
<table>完整、style 白名单保留 color/font-size、危险值 url(/expression( 拦截、span 放行后样式保留 - 全部 PHP 文件
php -l通过;update/镜像(PHP/app/cli)fc /b逐字节一致;assets/ 不在镜像范围 - 遗留:编辑器表格双向转换与模式记忆依赖浏览器 DOM/localStorage,需人工浏览器实测(CLI 无法覆盖)
P25 性能优化收官:统计缓存永久化 + keyset 深翻页 + 编辑生效修复 + 计数落库修复
2026-08-15
结论(补记):P25 系列性能工作此前未记入 CHANGELOG,本次按
update/会话交接文档_20260815_P25版.md补记。① 首页 0.11s 根因定位(统计 GROUP BY 全表扫描)→ 统计缓存永久化(99ms → 2.14ms);② 后台首页 users 排序加索引(26.5ms → 0.05ms);③ keyset 深翻页(第 12000 页 188ms → 0.36ms);④ 编辑不生效修复(ExternStorage::invalidateArchiveEntry()作废 .idx 回退 .txt);⑤ 浏览数落库修复(shutdown 挂接ViewCounter::flush());⑥ 版块最新帖快照(Ofstar 借鉴);⑦ 写放大硬上限(online_users/Session GC LIMIT 5000);⑧ 分页 1000 页上限解除(前台论坛/帖子)。覆盖 Category.php / Thread.php / ExternStorage.php / ViewCounter.php / TemplateCompiler.php / HomeController.php / ForumController.php / index.php / config.php / Settings.php 等。详见update/会话交接文档_20260815_P25版.md。
2026-08-15
结论:用户关键反馈「200 条数据时 8-10ms、19.2 万条时 0.12s,不是配置问题」→ 实测验证:耗时与数据量无关——逐查询剖析全部快速(最大单条 getOnlineData 7.3ms),300 帖与 19.2 万帖的 warm 渲染均 ~7-20ms(首次冷启动 71ms 属冷态,非数据);0.12s 构成 = 未缓存请求的固定管线(init.php 引导 29ms + 稳态渲染 20ms + Nginx 连接层)。针对性优化:①
getOnlineData加 30s 短缓存(最大单查 7.3ms → 缓存命中),warm 渲染 20ms → 7ms;② index.php 前置缓存从「纯游客」扩展为「有会话 Cookie 但未登录」的游客(直读 sessions.sqlite 判定),游客类请求全部 ~2ms 命中
| 文件 | 状态 | 变更说明 |
|---|---|---|
app/Helpers/Settings.php | ✏️ 改 | getOnlineData() 加 30s 短 TTL 文件缓存(data/runtime/online_data.cache.json,json 带 ts;30s 内直接返回,过期自动重查并回写)——实测该查询为首页渲染最大单条成本 ~7ms |
index.php | ✏️ 改 | 前置缓存扩展(P24):可命中条件由「无任何 Cookie」放宽为「纯游客 或 有会话 Cookie 但未登录(且无记住我 Cookie)」;后者直读 meta/sessions.sqlite 判定(序列化会话数据含 `user_id |
排查与验证结论
- 数据量无关(对照实验):300 帖小数据集首页渲染 ≈ 19.2 万帖真实库,warm 稳态均 ~7-20ms;逐查询剖析:getLatestAllowed 0.6ms / Tag::getPopular 1.1ms / Vote::getCounts 1.5ms / Settings 统计 0.1ms(runtime 缓存)——均不随数据量退化;真实库连续渲染 71.5 / 20.2 / 19.1ms(首次冷启动 vs 稳态)
- 0.12s 构成 = 未缓存请求的固定管线:init.php 引导 ~29ms(会话/业务库连接/在线状态/16 插件)+ 稳态渲染 ~20ms + Nginx 连接层(冷连接 ~200ms、keep-alive 下剩余 ~40ms)
- 优化效果:getOnlineData 缓存命中后 warm 渲染 ~20ms → 7ms;前置缓存扩展后「游客会话 Cookie」请求与缓存文件逐字节一致(
fc /b验证)、「已登录」请求响应不同(正确走个性化渲染)、无 Cookie 命中不变 - ⚠️ 排查工具沉淀:
cli/probe_page_cache.php(独立探针,纯 PHP 读缓存文件计时,零框架依赖);php -S内置服务器可绕过 Nginx 隔离服务器层(实测 1.1ms keep-alive) - 全部改动文件
php -l通过;update/镜像已同步 2 个文件(SHA256 校验 IDENTICAL)
P23 首页 0.12s 提速:永久 .html 缓存 + index.php 前置缓存(跳过框架引导)
2026-08-15
结论:用户反馈首页仍 0.12s(P22 缓存命中也一样)→ 实测定位:瓶颈不在 PHP 应用层——Web 服务器连接开销(冷连接每请求 ~200-300ms,纯静态 CSS 也要 217ms)+ init.php 引导 29ms + 渲染 ~58ms;控制器级缓存只省渲染,无法到 <10ms。实施两步:① PageCache 改永久缓存(去掉 60s TTL,仅在发帖/回帖时 invalidate 失效,用户确认的取舍:在线人数/热门标签/设置等下次发帖前为快照);② index.php 入口前置缓存——纯游客(无任何 Cookie)的 GET 列表请求在 require init.php 之前直接输出 pages/ 缓存并 exit,完全跳过 Session/DB/插件/在线状态引导。实测 keep-alive 连续 3 次刷新均为 0.0023s(2.3ms),远低于 0.01s 目标
| 文件 | 状态 | 变更说明 |
|---|---|---|
app/Helpers/PageCache.php | ✏️ 改 | serve() 去掉 60s TTL 过期判断 → 永久缓存(仅在发帖/回帖 invalidate() 主动失效);类注释更新(P22→P23 永久化 + 前置缓存说明) |
index.php | ✏️ 改 | 前置静态缓存:require init.php 之前,GET && empty($_COOKIE)(纯游客)→ 按 URI 推导缓存键(home_{page} / forum_0_{page}(+_h) / forum_{id}_{page})→ PageCache::serve() 命中即输出并 exit;带 Cookie(会话/登录态)一律走完整管线,无用户态泄露;IIFE 包裹避免变量污染 |
app/Controllers/HomeController.php | ✏️ 改 | 注释口径更新(60s TTL → 永久缓存发帖失效) |
app/Controllers/ForumController.php | ✏️ 改 | 注释口径更新(index/category 两处) |
app/Models/Thread.php / Post.php | ✏️ 改 | 失效注释更新(P23 永久缓存语义) |
排查与验证结论
- 耗时构成(真实 Web 请求实测):冷连接每请求 ~200-300ms(纯静态 CSS 217ms、PHP 探针 343ms、首页命中 387ms → 服务器连接层开销,与 PHP 无关);keep-alive 下首页 122ms = 引导 29ms + 渲染 ~58ms + 其余 ~35ms;模板编译不是瓶颈(TemplateCompiler 有编译缓存
protected/cache/templates/,模板变更才重编;icon() 为静态映射表) - 命中 vs 未命中对比:未命中 0.339s(冷连接)/ keep-alive 0.122s;控制器级命中仅省渲染 ~60ms,无法 <10ms → 需入口前置缓存
- 前置缓存生效:清缓存后首访未命中(0.339s,重生成缓存),同一连接连续 3 次刷新均为 0.0023s ✅;带
PHPSESSIDCookie 请求 353ms 走完整管线(证明不串缓存、登录态安全) - ⚠️ 经验:Windows cmd 下
curl -w "%{time_total}"的%无需%%转义(单行命令);-o NUL在多 URL keep-alive 时可能仍输出 body,计时看-w即可;CLI 复现控制器需先 require config.php - 全部改动文件
php -l通过;update/镜像已同步 6 个文件(SHA256 校验 IDENTICAL)
P22 终极性能优化:extern/ 外置文件合并(.bin+.idx) + 高频列表页静态 HTML 缓存
2026-08-14
结论:两项重大任务全部落地。① extern/ 35 万+ 小 .txt 按 批次号=floor(ID/1000) 归并进 .bin 分块(懒加载 .idx JSON 索引),路径按用户修正:.bin/.idx 严格遵循 ShardRouter 原路径与 .txt 同目录(
extern/{年}/{季}/{桶}/{批次基数}.bin+.idx,批次基数=批次号×1000,如extern/2026/Q3/12/12000.bin),红线保障(先 bin → 再 idx → 后删同目录 .txt,单点失败跳过,结束自动校验),存量 351,509 条全部迁移、0 残留;② 首页/版块列表页"访问触发式 HTML 缓存"(data/runtime/pages/,60s TTL,命中直出绕过 DB/视图渲染),发帖/回帖主动失效,新帖立即可见
| 文件 | 状态 | 变更说明 |
|---|---|---|
cli/migrate_extern_to_bin.php | 🆕 新增 | 存量 .txt → .bin/.idx 迁移:按 (目录, 批次) 分组(ID 空间重叠安全),内容格式 [4字节大端长度][正文];红线:先原子写 .bin → 再原子写 .idx → 才删同目录该批 .txt;单点失败记录跳过不崩溃;结束自动校验 .idx 记录数 == 删除数;断点续跑(已迁移组追加合并不丢条目);--dry-run/--verify;自愈:旧版独立 extern/bin/ 误产物自动恢复缺失 .txt(双候选路径推导:帖子用 topic_index、回复用 reply_index 的 create_time+bucket_path,ID 空间重叠只回填恰好缺失的那条)后删除 |
app/SplitDB/ExternStorage.php | ✏️ 改 | read() 新增 .bin/.idx 优先读取(懒加载 .idx + APCu 缓存 600s,缓存键含目录防不同桶同名冲突);降级红线:.bin 存在但 .idx 缺失 / ID 不在索引 → 自动回退 .txt(前台永不 404);新帖只写 .txt 同样走回退;delete() 注释更新(.bin 只读归档不重写) |
app/Helpers/PageCache.php | 🆕 新增 | 列表页静态缓存助手:serve() 命中直接 echo(60s TTL + 与 Controller::view 一致的安全响应头 + clearstatcache);render() ob_start 渲染落盘后输出;invalidate() 清空 pages/ 目录;仅游客生效(登录用户含用户名/头像/未读消息/会话级 CSRF,不缓存避免泄露用户态) |
app/Controllers/HomeController.php | ✏️ 改 | 首页游客静态缓存:顶部 PageCache::serve('home_{page}') 命中即返(完全绕过 DB/渲染);尾部游客 PageCache::render 落盘 |
app/Controllers/ForumController.php | ✏️ 改 | 论坛总览 forum_0_{page}(type=highlighted 键加 _h 后缀防串缓存)+ 版块列表 forum_{版块ID}_{page};category 的权限校验在缓存命中之前(无权版块不命中/不生成缓存,安全红线) |
app/Models/Thread.php / Post.php | ✏️ 改 | insert() 收尾调用 PageCache::invalidate()(发帖/回帖后 pages/ 全清,新帖立即出现在列表,无需等 60s TTL) |
验证结论
- extern 迁移:存量 351,509 个 .txt 全部归并(24,651 个 .bin/.idx、351,509 条记录),残留 .txt = 0,
--verify通过;断点续跑实测(首轮 300s 超时后多轮自动续传直至完成) - 自愈恢复:旧版独立
extern/bin/误产物(103 个 .bin,误删 102,978 个 .txt)自动双候选恢复 + 删除旧目录,实测恢复后与索引完全对齐 - 读取抽查:真实库 30 帖正文全部命中 .bin(0 空),回复楼层读取正常;
smoke_extern_storage10/10、smoke_frontend_flow21/21 全过 - 列表页缓存(真实库实测):游客渲染生成
home_1.html;二次访问命中直出 0.0002s(完全绕过 DB);发帖后缓存文件立即删除、重新生成后回帖再次失效(9/9 全过)✅ - ⚠️ 经验:CLI 复现控制器需先 require config.php(UPLOAD_URL/DEFAULT_SITE_NAME 等 Web 常量);PHP
filesize/filemtime有 stat 缓存需clearstatcache()(P21 已踩) - 全部改动文件
php -l通过;update/镜像已同步 7 个文件(SHA256 校验 IDENTICAL)
此为上篇,P21 及更早阶段的内容见下篇。