[开发]SoSite 1.0 (SplitDB 引擎迁移) — CHANGELOG(上篇:P22-P41)

👑Lv.11 元老 🌞 骨干会员
2026-10-24 01:00:07

本文由 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= > Cookie flinthub_lang > Session lang > 后台设置 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 /b Plugin.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 /b style.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 /b RepoController.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 实体(&#xf1c9;)但未包 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 /b Plugin.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 /b index/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.json hooks 改 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 /b config / 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 /b config.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-editable min-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-editable min-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(引擎设置,操作型:桶数调整/自动扩容/扩容记录/一致性检查);旧 /engine 302 重定向到 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 /b 8 文件逐字节一致(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 /b 7 文件逐字节一致(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 ✅;带 PHPSESSID Cookie 请求 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_storage 10/10、smoke_frontend_flow 21/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 及更早阶段的内容见下篇。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 5 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录