本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:暂存20260913/docs/CHANGELOG (2).md。因篇幅较长拆分为上下两篇。
此为下篇,承接上篇 P20 之后的内容。
P21 搜索索引季度分文件(search_index 剥离 business.sqlite → data/meta/search/)
2026-08-14
结论:按用户确认的完整剥离方案执行——search_index 从 business.sqlite(306.7MB 体积大户)拆出为季度分文件 data/meta/search/search_{YYYYQn}.sqlite;存量 2,357,943 行按 created_at 路由迁移(断点续跑 + 加载期摘索引提速);Search 全部读写路径改走新文件(多文件检索归并 + 时间范围→覆盖季度集合);迁移校验 100% 一致后删除 business.search_index 并 VACUUM,business.sqlite 306.7MB → 19.0MB(释放约 288MB)
| 文件 | 状态 | 变更说明 |
|---|
app/SplitDB/SearchIndexStore.php | 🆕 新增 | 季度分文件存储层:quarterOf() created_at→季度路由(异常/空落当前季度);quartersForSince() 时间范围→覆盖季度集合(滑动窗口跨季度,如 8-14 近一月跨 Q2/Q3);writeIndex() 删旧+批量 INSERT 单事务;deleteIndex/deleteTargets(900/批 IN 跨文件);clearAll/countAll/countByType/maxTargetId/cleanWeightOne;db()/dbBulk()/dropFastIndexes()(bulk 加载期摘非唯一索引提速,收尾 ensureSchema 补回);幂等建表 + 4 索引(与旧表结构一致) |
app/Helpers/Search.php | ✏️ 改 | searchByIndex 改多季度文件归并检索:每文件各取真实总数 + 文件内前 (offset+limit) 名候选 → score 归并排序 → 页内切片(target 只落唯一季度文件,score 无跨文件重复);计数跨文件 SUM;权限过滤取数后统一做。indexThread/indexBlog 改走 SearchIndexStore::writeIndex(顺带修正正文合并词条 shape 隐患);removeFromIndex 改走 deleteIndex;降级 LIKE 路径不变 |
app/SplitDB/Schema.php | ✏️ 改 | bootstrap 目录树新增 meta/search;business.sqlite 移除 search_index 建表与 columnPatches(新装不再建,索引改季度分文件) |
app/Models/Thread.php | ✏️ 改 | delete()/batchDelete() 的倒排索引清理改走 SearchIndexStore::deleteIndex/deleteTargets(原直删 business 表会因表已剥离而报错) |
app/Controllers/Admin/SettingsController.php | ✏️ 改 | 重建索引第 1 页不再 DROP/CREATE business 表 → clearAll()(补上 P20 漏写的 created_at);帖子/博客逐条改 writeIndex;「清理正文索引」改 cleanWeightOne() |
app/Controllers/Admin/DatabaseController.php | ✏️ 改 | sqliteFiles() 显式枚举新增 meta/search/*.sqlite(备份页覆盖季度文件) |
cli/migrate_search_quarters.php | 🆕 新增 | 存量迁移:keyset 游标分块(WHERE id > last 免 OFFSET 全表扫描)→ 按 created_at 分季度 → 各文件单事务 + 单行预编译;断点进度存 runtime/search_migrate_progress.json(被杀/中断自动续传);--dry-run/--verify/--from-scratch |
cli/shrink_business.php | 🆕 新增 | 瘦身收口:迁移校验(季度合计 == business 行数)硬性通过后才 DROP TABLE search_index + VACUUM;--check 仅校验;报告前后体积 |
cli/rebuild_search.php | ✏️ 改 | 清空/计数改 SearchIndexStore::clearAll/countAll |
cli/seed_content.php | ✏️ 改 | flushSearchIndex/phaseSearch 按 created_at 路由季度文件写入(每季度文件事务 + 预编译,20000 行提交);进度起点改 maxTargetId(1/2);收尾计数 countAll |
cli/smoke_schema.php | ✏️ 改 | business 表断言移除 search_index + 新增「business 无 search_index(已剥离)」断言;目录树含 meta/search;新增搜索库季度文件建表/唯一索引/created_at 索引断言 |
cli/smoke_frontend_flow.php | ✏️ 改 | 分类定位改按名称(seedDefaults 已内置 id=1「默认版块」,原按 allOrdered 序取分类会取到默认版块导致断言偏差) |
cli/_debug_search*.php | ✏️ 改 | 直接查询 business.search_index 的调试语句改 dumpSearchIndex() 扫全部季度文件(临时调试脚本) |
验证结论
- 迁移:2,357,943 行全部迁移成功(2025Q4: 681,657 / 2026Q1: 674,378 / 2026Q2: 678,348 / 2026Q3: 323,560),
--verify 与 business 原表行数 100% 一致;断点续跑实测(首跑 300s 超时中断后二次自动续传,最终耗时 103s 完成) - 瘦身:删除 search_index + VACUUM 后 business.sqlite 306.7MB → 19.0MB(释放约 288MB,freelist_count=0 完全压实);⚠️ 实测发现 PHP
filesize() 同一请求内有 stat 缓存,复测需 clearstatcache()(曾误判"未瘦身",实际已缩) - 搜索全链路(真实库实测):关键词「插件」全部 30,999 → 近一年 30,999 → 近半年 17,468 → 近一月 2,771(与 P20 基线逐级递减一致,滑窗边界差属正常);30d 命中帖抽样 100 条窗口外 0 条 ✅;博客「论坛」blog_count=2 ✅
- 多文件归并:30d 窗口 [07-15~08-14] 正确路由 2026Q3 单文件;180d 覆盖 Q1+Q2+Q3 三文件归并计数正确(17,468)
- 冒烟:
smoke_schema 51/51、smoke_frontend_flow 21/21 全通过(发帖→列表→详情→回帖→搜索→投票→标签) - 全部改动文件
php -l 通过;update/ 镜像已同步 15 个文件(fc 校验 IDENTICAL)
P20 搜索时间范围过滤(search_index.created_at)+ 统计缓存主动失效
2026-08-14
结论:按决策采用"第一步"方案——search_index 表加 created_at 冗余列,写入点(indexThread/indexBlog/seed 重建)同步写入;搜索页新增时间范围下拉(全部/近一月/近半年/近一年),searchByIndex 与 searchLIKE 按 created_at >= 过滤;另补发帖/回帖后主动失效版块统计缓存(category_stats.cache),新帖统计立即可见。实测近一月/近半年命中数逐级递减、30d 抽样时间均在窗口内,过滤生效
| 文件 | 状态 | 变更说明 |
|---|
app/SplitDB/Schema.php | ✏️ 改 | search_index 建表新增 created_at TEXT(幂等补列到 columnPatches)+ idx_search_index_created 索引 |
app/Helpers/Search.php | ✏️ 改 | search() 新增 $timeRange 参数(all/30d/180d/365d);timeRangeSince() 换算下限;searchByIndex 帖子/博客两分支加 AND si.created_at >= :since;searchLIKE 降级路径同步支持;indexThread/indexBlog 写入时带 created_at |
app/Controllers/SearchController.php | ✏️ 改 | 解析 time_range 参数(白名单校验),透传所有 Search::search 调用点;视图传 timeRange 回显 |
app/Views/search/index.php | ✏️ 改 | 搜索表单新增时间范围下拉(全部/近一月/近半年/近一年),tab 链接保留 time_range |
app/Models/Category.php | ✏️ 改 | 新增 invalidateCategoryStats():删除 protected/cache/category_stats.cache + 重置请求内缓存 |
app/Models/Thread.php / Post.php | ✏️ 改 | insert() 收尾调用 Category::invalidateCategoryStats()(新帖/回帖统计立即可见) |
cli/seed_content.php | ✏️ 改 | titleTokens/flushSearchIndex/phaseSearch 写入 created_at(帖子取 topic_index.create_time 转 Y-m-d H:i:s,博客取 created_at);phaseSearch 改"事务+单行预编译(20000 行提交)"提速重建 |
验证结论
php -l 全通过(Schema/Search/SearchController/搜索视图/Category/Thread/Post/seed_content 8 个文件)- search_index 全量重建:2,357,943 行,created_at 覆盖率 100%
- 时间范围过滤实测(关键词"插件"):全部 30,999 → 近一年 30,999 → 近半年 17,480 → 近一月 2,778(逐级递减 ✅);近一月抽样命中帖 created_at 均在 2026-07-14~08-10 窗口内 ✅;博客"论坛":全部 2 → 近一月 0(旧博客超窗口,正确 ✅)
- 统计缓存主动失效:
Category::invalidateCategoryStats() 已接入 Thread/Post insert;update/ 镜像已同步 8 个文件
P19 重建标签分批删除修复 + 版块统计合并缓存 + VACUUM 提示文案更新
2026-08-14
结论:① 后台"重建标签"报 too many SQL variables 根因 = Tag::rebuild() 将 19 万帖子 ID 一次性拼入 NOT IN,超过 SQLite 变量上限(实测 32766)→ 改为先算孤儿 ID 再按 900/批 IN 分批删除;② 版块页仍 0.1-0.2s 根因 = 三个全表统计(getThreadCountMap/getPostCountMap/countActive 各扫 19 万行 ~0.08s)→ 合并为一次 GROUP BY 出三份统计 + 60s 文件缓存;③ VACUUM 提示文案更新为动态口径
| 文件 | 状态 | 变更说明 |
|---|
app/Helpers/Tag.php | ✏️ 改 | rebuild() 分批清理孤儿关联:先 SELECT DISTINCT thread_id 计算孤儿集(现存 − 有效),再 array_chunk(900) 按 IN 分批删除(单条 SQL ≤900 占位符,远低于 32766 上限);修复 2026-08-14 误用 NOT IN 分批导致全量误删的问题 |
app/Models/Category.php | ✏️ 改 | 新增 getCategoryStats():一条 SELECT category_id, COUNT(*), SUM(reply_count) ... GROUP BY category_id 同时产出主题数+回复数映射(60s 文件缓存 + 请求内静态缓存);getThreadCountMap() / getPostCountMap() 改为从合并结果取值 |
app/Models/Thread.php | ✏️ 改 | countActive() 常规统计接入 Category::getCategoryStats() 缓存求和(避免版块页全表 COUNT);highlightOnly 精华统计保留原 SQL |
app/Views/admin/database.php | ✏️ 改 | VACUUM 提示文案 (202MB)...非必要情况下执行 → (>100MB)...必要情况下执行(通用口径,不随体积漂移) |
验证结论
- 重建标签:实测 0.13s 成功,保留 16 个标签 / 27 条关联(孤儿清理语义正确,无全量误删);此前误删数据已从
protected/data_backup_20260814/ 恢复 - 版块统计:首次合并
getCategoryStats() 0.1127s(一次 GROUP BY);缓存命中后 getThreadCountMap + getPostCountMap + countActive 0.0000s(原三查合计 ~0.25s);一致性校验:合并主题数 192111 == 全表 COUNT 192111 ✅ php -l 全部通过(Tag.php / Category.php / Thread.php / database.php);update/ 镜像已同步 4 个文件
P18 后台备份页与前台列表性能修复(sqliteFiles 显式枚举 + topic_index 复合索引 + VACUUM 提示)
2026-08-14
结论:① 后台"备份优化"页卡死根因 = sqliteFiles() 全目录递归遍历(data/ 含 extern 数十万 .txt,单次数十秒)→ 改为显式枚举 meta 库/队列库/分片桶,实测 0.029s(原约数十秒);② 前台列表页变慢根因 = topic_index 排序字段无索引(19 万行全表扫描 + 临时 B 树)→ 补复合索引 idx_topic_index_list,首页最新帖 0.0828s → 0.0012s、版块列表 0.0981s → 0.0010s;③ "优化"按钮旁新增 VACUUM 大库耗时提示
| 文件 | 状态 | 变更说明 |
|---|
app/Controllers/Admin/DatabaseController.php | ✏️ 改 | sqliteFiles() 由 RecursiveDirectoryIterator 全目录递归遍历改为显式枚举:meta/.sqlite + meta/task_queue/.sqlite + bucket/active//.sqlite + bucket/archive//.sqlite,跳过 extern 目录(正文外置海量 .txt 所在,不含 sqlite) |
app/Views/admin/database.php | ✏️ 改 | "优化"按钮下方新增提示小字:当前 business.sqlite 体积较大(202MB),VACUUM 操作可能会持续较长时间,建议在低峰期或非必要情况下执行。 |
data/meta/main_index.sqlite(运行时) | ⚙️ 执行 | CREATE INDEX IF NOT EXISTS idx_topic_index_list ON topic_index (is_pinned DESC, last_reply_time DESC, create_time DESC) + ANALYZE(幂等;镜像不含数据文件,重新执行 SQL 即可) |
验证结论
sqliteFiles():实测 0.029s,枚举 136 个库文件(meta 5 + 队列 3 + 分片桶 128),对照旧全量枚举缺失 0,覆盖完整- EXPLAIN:首页最新帖 / 版块列表均由
SCAN TABLE + USE TEMP B-TREE FOR ORDER BY → USING INDEX idx_topic_index_list(TEMP B-TREE 消除) - 耗时实测:首页最新帖 LIMIT 10 0.0012s(原 0.0828s,约 69 倍提速);版块第 1 页 0.0010s(原 0.0981s);版块第 100 页 0.0163s(原 0.1214s)
php -l 通过(DatabaseController.php / database.php);update/ 镜像已同步
P17 后台首页卡死修复(Settings::getDbSize 全目录遍历 → 跳过 extern)
2026-08-14
结论:后台首页 /admin 每次加载都调用 Settings::getDbSize(),旧实现用 RecursiveDirectoryIterator 递归遍历整个 data/ 目录(35 万+ 文件,其中 extern 正文 .txt 约 35 万个),实测单次 143.89s → 后台"非常慢/打不开"。修复后跳过 extern 目录(sqlite 库文件只分布在 meta/、bucket/、task_queue/ 等目录),实测 0.089s,提速约 1600 倍,且统计口径不变(sqlite 总字节数一致)
| 文件 | 状态 | 变更说明 |
|---|
app/Helpers/Settings.php | ✏️ 改 | getDbSize() 改用 RecursiveCallbackFilterIterator:目录跳过 extern/(正文外置海量 .txt 所在,不含 sqlite),文件仅统计 .sqlite 扩展;遍历范围从 35 万文件收敛到数百个库文件 |
验证结论
php -l 通过;修复后 getDbSize() 耗时 0.0892s(旧逻辑同目录实测 143.89s,对比提升约 1600 倍)- 返回值一致:新旧逻辑均统计 sqlite 合计 271.0 MB(data/ 总量)
update/ 镜像已同步
P16 测试内容批量生成(正式内容保留:5 万用户 + 20 万帖 + 16 万回复)
2026-08-14
结论:新增 cli/seed_content.php + cli/seed_content_data.php 测试内容生成器,按用户比例生成并作为正式内容保留;32 桶分布均匀(min=6001 / max=6005),全部内容经真实分片链路落盘(extern → 桶 → main_index → search_index),php -l 通过,占位符/编码零残留
| 文件 | 状态 | 变更说明 |
|---|
cli/seed_content.php | 🆕 新增 | 确定性批量生成器:仿 migrate.php 批量路径(extern 原子写 → 桶 topic/reply 真相源 → main_index 索引 → 推进 global_id),断点续跑(游标驱动);阶段 users / threads / replies / blogs / search / finalize / report,每阶段 --limit 分批可续跑;内容确定性(mt_srand(种子+id));昵称/邮箱/签名/头像(SVG 种子)确定性生成;search_index 仅标题分词(与 search_index_content=0 语义一致),独立 --phase search 事务批处理重建 |
cli/seed_content_data.php | 🆕 新增 | 素材库:昵称池(中文/英文/Emoji/老站长/生活家)、签名池、槽位池({语言}/{框架}/{数字} 等 20+ 词性)、各版块标题池(公告/程序发布/插件模板/精品/灌水 + 博客)、正文段落池、代码块、引用、回复池、博客标题/正文池 |
生成规模(正式内容保留)
- 用户:新增 50000 名(总数 50008,含原有 8),昵称支持中文/英文/数字/下划线/Emoji,统一测试密码
SeedTest@2026,头像为 assets/uploads/seed_avatars/{id}.svg - 论坛帖:新增 192000 条,严格按用户比例分配——站点公告 1%(2000)/ 程序发布 15%(30000)/ 插件模板 20%(40000)/ 精品专区 30%(60000)/ 聊天灌水 30%(60000)
- 博客:新增 8000 篇(4%),按 2 个博客分类分配(开发记录 55% / 日常杂记 45%)
- 回复:新增 160656 条(聊天灌水为主,技术类次之)
- search_index:全量重建 2444184 行(仅标题分词)
验证结论
- 32 桶分布:min=6001 / max=6005(聚合 4 个季度,桶负载近乎均匀)
- 版块统计:站点公告 2006(+6 原有)/ 程序发布 30021 / 插件模板 40005 / 精品专区 60042 / 聊天灌水 60007 / 博客 8005(+5 原有)
- 全站占用:data/ 354.39 MB / 351669 个文件
- 搜索抽样:PHP 1488 / SQLite 6117 / SplitDB 5434 / 插件 30999 / 周末 5265 全部命中
- 占位符残留:标题/正文抽样 0 残留;全部文件 UTF-8 no BOM + LF;
php -l 通过 update/ 镜像已同步 cli/seed_content.php + cli/seed_content_data.php
2026-08-13
结论:后台搜索对 SplitDB 完全有用且工作正常——倒排索引表已并入 business.sqlite(2889 行:帖子 2780 + 博客 109),帖子正文经 Thread 模型读 main_index 分片 + extern;实测 search('小恐') 命中、indexThread() 重建、博客搜索均正常
| 文件 | 状态 | 变更说明 |
|---|
app/SplitDB/Schema.php | ✏️ 改 | 清理死库定义:删除 SEARCH_INDEX 常量、searchIndexDb() 方法、ensureSearchIndex() 方法(bootstrap 从不调用,search_index.sqlite 独立库从未创建;实际倒排索引表已并入 business.sqlite,见 ensureBusiness 注释) |
cli/smoke_schema.php | ✏️ 改 | 断言与实现对齐:库文件清单移除 meta/search_index.sqlite;business 表清单追加 search_index(验证并入业务库) |
验证结论
cli/smoke_schema.php 全量 47/47 通过(含 search_index 在 business.sqlite 中建表断言)- 实测搜索链路:帖子/博客命中正常,索引构建(indexThread)写入 business.sqlite 正常
php -l 通过;update/ 镜像已同步
P14 勋章后台 500 修复(跨库查询 user_medals)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
plugins/medal/AdminMedalController.php | ✏️ 改 | index() 勋章持有统计误用核心库 Database::getInstance() 查 user_medals(该表在 medal 插件独立库)→ 改经 \app\Helpers\Plugin::db('medal') 查询,修复后台勋章管理 500 |
验证结论
php -l 通过;实际调用验证:medal 独立库 user_medals 统计返回 3 行(medal_id 4/5/6 各 1 人)、getMedals() 列表返回 3 枚,后台勋章管理页恢复可用
P13 补索引 + SEO 帖子描述修复(P12 审计收尾)
2026-08-13
决策:执行 P12 审计建议的 3 条幂等索引(P1 + P2 全补);修复 seo 插件读取已退役 threads 表的逻辑残留
| 库 | 表 | 新增索引 | 状态 |
|---|
核心库 data/meta/business.sqlite | points_log | idx_points_log_reason ON points_log (reason) | ✅ 已建(EXPLAIN 复测:SCAN → SEARCH USING INDEX) |
核心库 data/meta/business.sqlite | blogs | idx_blogs_user_created ON blogs (user_id, created_at DESC) | ✅ 已建(EXPLAIN 复测:TEMP B-TREE 消除) |
medal 独立库 plugins/medal/data/medal.sqlite | user_medals | idx_user_medals_user_created ON user_medals (user_id, created_at DESC) | ✅ 已建(EXPLAIN 复测:TEMP B-TREE 消除) |
| 文件 | 状态 | 变更说明 |
|---|
plugins/seo/hook/layout_head_end.php | ✏️ 改 | 帖子详情分支 threads t LEFT JOIN users u(business.sqlite 已退役表,0 行)→ 改经 (new \app\Models\Thread())->find($id) 读 main_index 分片(正文 extern + 用户名),SEO 帖子描述恢复可用 |
验证结论
- 三条索引均已在各自库
PRAGMA index_list 确认存在 - EXPLAIN QUERY PLAN 复测:
medal#1 getUserMedals、user_profile#7 getBlogs 由「USE TEMP B-TREE FOR ORDER BY」→ 全走索引;dice#4 getLeaderboard 由「SCAN idx_points_log_user」→ SEARCH USING idx_points_log_reason(剩余 TEMP B-TREE 为 GROUP BY 聚合排序,属正常开销) Thread::find(160) 实测返回 content / created_at / username 字段齐全;php -l 通过- 本次改动文件(plugins/)不在 update/ 镜像范围,无需镜像同步
P12 插件性能与索引审计(静态扫描 + EXPLAIN QUERY PLAN 实测)
2026-08-13
目标:排查 14 个启用插件是否因 SQLite 下缺索引的联表查询导致性能损耗(页面 0.02s vs 核心 0.0078s)
方法:① 静态扫描 15 个插件目录全部 PHP 的 JOIN 语句 ② PRAGMA index_list 核对实际索引 ③ 对每处联表查询实测 EXPLAIN QUERY PLAN
结论:无致命缺索引联表查询;7 处 JOIN 中 4 处执行计划完全健康,3 处仅存在「排序/过滤字段缺索引」的轻微优化空间(不影响 0.02s 级性能);页面开销主因是 14 个插件 hook 的固定查询次数,而非 JOIN 索引
| # | 插件 | 查询位置 | 联表摘要 | 查询库 | 执行计划判定 |
|---|
| 1 | medal | Plugin.php getUserMedals() | user_medals JOIN medals ON medal_id WHERE user_id ORDER BY created_at DESC | medal 独立库 | WHERE 走索引 ✓ / USE TEMP B-TREE FOR ORDER BY ⚠️ |
| 2 | medal | Plugin.php getWearingMedals() | user_medal_wears JOIN medals ON medal_id WHERE user_id | medal 独立库 | ✓ 全走索引 |
| 3 | medal | hook/controller_view_before.php | user_medal_wears JOIN medals ON medal_id WHERE user_id IN (...) | medal 独立库 | ✓ 全走索引 |
| 4 | dice | Plugin.php getLeaderboard() | points_log JOIN users ON user_id WHERE reason GROUP BY | 核心库 | SCAN idx_points_log_user ⚠️(按 reason 过滤无索引)+ TEMP B-TREE |
| 5 | seo | hook/layout_head_end.php | threads LEFT JOIN users ON user_id WHERE id | 核心库 | ✓ 全走索引(⚠️ 但查的是已退役 threads 表,见附注) |
| 6 | seo | hook/layout_head_end.php | blogs LEFT JOIN users ON user_id WHERE id | 核心库 | ✓ 全走索引 |
| 7 | user_profile | Plugin.php getBlogs() | blogs LEFT JOIN blog_categories ON category_id WHERE user_id ORDER BY created_at | 核心库 | WHERE 走索引 ✓ / USE TEMP B-TREE FOR ORDER BY ⚠️ |
缺失索引汇总与建议修复(待用户决定是否补)
| 优先级 | 库 | 表 | 缺失字段 | 影响查询 | 建议 CREATE INDEX(幂等) |
|---|
| P1 | 核心库 | points_log | reason | #4 骰子排行榜 | CREATE INDEX IF NOT EXISTS idx_points_log_reason ON points_log (reason); |
| P2 | medal 独立库 | user_medals | (user_id, created_at) | #1 勋章列表排序 | CREATE INDEX IF NOT EXISTS idx_user_medals_user_created ON user_medals (user_id, created_at DESC); |
| P2 | 核心库 | blogs | (user_id, created_at) | #7 个人主页博客排序 | CREATE INDEX IF NOT EXISTS idx_blogs_user_created ON blogs (user_id, created_at DESC); |
附注(非性能问题)
seo/hook/layout_head_end.php 帖子详情分支查询的是 business.sqlite 已退役的 threads 表(0 行)→ SEO 帖子描述实际长期为空,建议后续改经 main_index.topic_index 读取(功能修复,非本次范围)- 详细报告见
update/插件性能与索引审计报告.md
P11 品牌重命名(SoSite → FlintHub)+ MySQL 残留清理
2026-08-13
决策确认:① SOSITE_VERSION → FLINTHUB_VERSION ② MAIL_PASS_KEY 不改(AES 密钥,改则旧 SMTP 密码失效)③ 迁移脚本/legacy 归档保留(历史参考)④ 历史文档(README/CHANGELOG/计划书)不追改 ⑤ update/ 镜像与根目录完全同步
| 文件 | 状态 | 变更说明 |
|---|
app/Helpers/Search.php | ✏️ 改 | 删除全部 MySQL FULLTEXT 死代码:isFTSAvailable/initFTS(查 information_schema)、searchMySQLFulltext(无调用方)、rebuildFTS(SQLite 必报错)、syncThreadFTS/syncBlogFTS 空实现 |
app/Controllers/{Post,Thread,Blog,Admin/Thread}Controller.php | ✏️ 改 | 删除 6 处 syncThreadFTS/syncBlogFTS 调用点(倒排索引 indexThread/indexBlog 保留) |
app/Views/admin/index.php | ✏️ 改 | 「数据库: MySQL」→「SplitDB(SQLite 分片)」 |
app/Views/admin/maintenance.php | ✏️ 改 | 移除无用 $dbType 变量(默认值 mysql 已删) |
app/Core/Database.php | ✏️ 改 | 注释去除「原 MySQL 单例 PDO」历史说明 |
config.php / install.php | ✏️ 改 | SOSITE_VERSION → FLINTHUB_VERSION(同步 Dashboard/footer/安装向导引用);MAIL_FROM_NAME/DEFAULT_SITE_NAME → FlintHub;安装向导文案;@package → FlintHub;/sosite 注释 → /flinthub |
app/Controllers/Admin/DashboardController.php | ✏️ 改 | 「SoSite 版本」→「FlintHub 版本」+ FLINTHUB_VERSION |
app/Views/layouts/main.php | ✏️ 改 | footer 徽标 sosite-badge.svg → flinthub-badge.svg + 文案;版本常量引用 |
sosite-badge.svg | 🗑 删除 | 由 flinthub-badge.svg 替代(含 FLINTHUB 文字) |
app/Helpers/{Mailer,Captcha,Theme}.php、app/Controllers/{Api,Auth}Controller.php | ✏️ 改 | SoSiteMailer→FlintHubMailer、SoSiteCaptcha_→FlintHubCaptcha_、X-Mailer/user_agent/站点名回退值 → FlintHub |
app/Views/admin/{mail_settings,settings_basic}.php、app/SplitDB/Schema.php | ✏️ 改 | 发件人名称/部署路径示例/站点默认名 → FlintHub |
| 根目录 181 个 PHP 文件头 | ✏️ 改 | 文件头注释 SoSite — → FlintHub —(逐文件精确替换,UTF-8 no BOM + LF 保持) |
assets/lib/sostate.js → fhstate.js、assets/lib/soajax.js → fhajax.js | 🔄 改名 | 文件头 SoSite→FlintHub、SoState→FhState、SoAjax→FhAjax;同步 layouts/main.php/install.php 引用 |
cli/migrate.php / cli/migrate_plugins.php | ✏️ 改 | 头部加「一次性历史工具,仅供查阅」说明注释(脚本保留) |
update/ 镜像 | 🔄 同步 | app/ + cli/ + index.php + install.php + cron_trigger.php 与根目录哈希一致 |
| 排除项 | — | MAIL_PASS_KEY(AES 密钥不改)、protected/database/legacy/(历史归档)、data/extern/(用户数据)、历史文档(README/CHANGELOG/计划书) |
本阶段验证结论
- 全量
php -l(根目录 + update/ 镜像)0 语法错误 - 根目录与 update/ 活动 PHP 中
SoSite 残留 ZERO(排除 MAIL_PASS_KEY);mysql|FULLTEXT|MATCH|AGAINST|information_schema 活动代码残留 ZERO(排除 data/legacy/文档) sostate/soajax/SoState/SoAjax 活动代码引用 ZERO;SOSITE_VERSION 活动代码引用 ZERO- 前端 FhState(Alpine)/FhAjax(htmx)功能不变(仅文件名与注释品牌化)
P10 插件系统迁移(独立数据库目录 + SQLite 化 + 全量数据迁移)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
app/Helpers/Plugin.php | ✏️ 改 | 新增 db($name)(插件独立库连接:plugins/{插件名}/data/{插件名}.sqlite,复用 DBFactory 统一 PRAGMA/LRU 池)+ ensureSchema($name, $ddl)(SQLite DDL 幂等建表助手) |
app/init.php | ✏️ 改 | 恢复 Plugin::init() 与 init_after 钩子(解除 P0~P6 暂停注释) |
index.php | ✏️ 改 | 恢复 4 处路由钩子(admin_route_register / route_register / route_before_dispatch / route_after_dispatch) |
app/Core/Controller.php、app/Helpers/Points.php、Controllers(Api/Attachment/Auth/Post/Profile/Thread) | ✏️ 改 | 恢复全站 28 处 Plugin::hook 调用(解除临时注释,保留原文) |
app/Views/*(layouts/forum/home/admin/_sidebar/_components 等) | ✏️ 改 | 恢复 11 处视图内插件钩子 |
plugins/(16 个插件) | ✏️ 改 | 全部插件数据库迁移至独立库:Database::getInstance()(全局库)→ Plugin::db($name)(插件独立库);MySQL DDL(AUTO_INCREMENT/VARCHAR/DATETIME/SHOW TABLES/SHOW COLUMNS/NOW()/FOR UPDATE)→ SQLite DDL 与参数化;跨库 JOIN(插件表 JOIN users/threads)拆分为「独立库查询 + 核心库批量补字段」;红包/勋章等并发控制改条件 UPDATE 防透支 |
cli/migrate_plugins.php | 🆕 新增 | 插件数据迁移脚本(旧 MySQL → 各插件独立库):--dry-run 预览;16 张插件表按归属迁移(列白名单 + 显式 ID + 行数校验一致);目标表缺失时自动调用插件 activate() 建表;旧库遗留表(qa_*,当前插件零引用)不迁移 |
本阶段验证结论
- P2 骨架恢复:init.php + 全站 28 处控制器钩子 + 11 处视图钩子恢复,
php -l 全过;首页/论坛/分类页渲染验收 PASS(插件未激活时不触发) - P3 逐插件改造:16 个插件全部独立库化 + SQLite DDL,全部
php -l 通过(PowerShell 批量校验) - P4 数据迁移(真实库实测):
cli/migrate_plugins.php --dry-run 预览 16 张表 177 行 → 全量迁移 16 张表 161 行全部成功、行数校验一致 ✓(announcements 3 / daily_checkin 36 / edit_history 57 / review_notifications 10 / red_packet_claims 10 / red_packets 8 / post_favorites 7 / notifications 7 / friend_links 6 / quiz_duels 4 / medals·user_medals·user_medal_wears 各 3 / invites 2 / user_follows 2) - 迁移中发现并修复:invite 插件 activate() 未建表(补建表逻辑与 hook 一致);旧库 qa_* 遗留表(当前插件零引用)不迁移
cli/migrate_plugins.php 已同步 update/cli/(哈希比对 IDENTICAL)
P9 任务队列独立模块(后台导航 + 独立页面 + 手动立即处理)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
index.php | ✏️ 改 | 注册 GET/POST /admin/settings/queue → AdminSettingsController::queueSettings |
app/Controllers/Admin/SettingsController.php | ✏️ 改 | basic() 保存键表移除 queue_mode(迁至独立页);新增 queueSettings()(保存模式 save_mode / 手动触发 flush)+ queueStatsAggregate()(聚合 3 队列 pending+processing/done/failed)+ flushQueueNow()(复用 worker 同构 handler 对 3 队列各 drain(0),lock/manual_flush.lock LOCK_EX\ |
app/Views/admin/queue_settings.php | 🆕 新增 | 独立「任务队列」页:标题区(任务调度引擎 + 说明文案)/ 三种模式卡片(同步直写/Cron 定时触发/CLI 常驻消费,文案按需求原文)/ 队列面板(⏳待处理 | ✅已完成 | 📦当前模式 | ⚠️失败)/ 防呆弹窗(sync→异步时确认"旧任务由新模式接管")/ 「立即处理待处理任务」按钮 |
app/Views/admin/_sidebar.php | ✏️ 改 | 「站点设置」组搜索设置下方新增「任务队列」菜单项(/admin/settings/queue,sync/🔄 图标,__nav_active='settings_queue') |
app/Views/admin/settings_basic.php | ✏️ 改 | 删除「队列任务调度」区块(迁至独立页,基本信息页恢复纯净) |
update/任务调度独立模块改造计划.md | 🆕 新增 | 独立模块改造计划文档(T1~T5 串行,含手动触发按钮建议) |
本阶段验证结论
- 路由/导航:
/admin/settings/queue 已注册;侧边栏「搜索设置」下方出现「任务队列」菜单(🔄 图标);基本信息页已无队列区块 - 聚合统计(真实库实测):
queueStatsAggregate() 正确汇总 3 队列(pending=0 done=6 起步);压入 2 任务后 pending=2 - 手动立即处理(真实库实测):
flushQueueNow() 消费 2 任务 → pending=0/done=8;二次调用空队列返回 0(文件锁防并发生效) - 保存逻辑:
save_mode 分支经 setValue+buildCache 写入 settings(cli→sync 往返实测正常) - 全部改动文件
php -l 通过;update/ 已同步 5 个改动文件 + 2 份计划文档
P8 队列任务调度模式后台开关(sync / cron / cli)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
app/Models/Thread.php | ✏️ 改 | insert() 步骤5 按 queue_mode 分支:sync → 同步执行 Search::indexThread($id) + Settings::runtimeBuild()(与 worker handler 同构,数据 100% 实时);cron/cli → 保留 Queue::push('rebuild_search', ...) 入队即返回 |
app/Models/Post.php | ✏️ 改 | insert() 步骤6 按 queue_mode 分支:sync → 同步 Settings::runtimeBuild()(实时统计 total_posts);cron/cli → 保留 Queue::push('stats', ...) |
app/SplitDB/Schema.php | ✏️ 改 | seedDefaults() 默认 settings 追加 queue_mode=sync(幂等 INSERT OR IGNORE;老库由 Settings::get('queue_mode','sync') 兜底,无需迁移脚本) |
app/Controllers/Admin/SettingsController.php | ✏️ 改 | basic() 保存键表追加 queue_mode(复用 setValue + buildCache,保存即生效) |
app/Views/admin/settings_basic.php | ✏️ 改 | 新增「队列任务调度」区块:queue_mode 下拉框(同步直写/Cron 定时触发/CLI 常驻消费)+ Alpine(sostate.js)x-show 联动展示模式 B(wget cron_trigger.php 指引)与模式 C(Supervisor 指引) |
cron_trigger.php | 🆕 新增 | 模式 B Web 触发入口:① 模式守卫(queue_mode !== 'cron' 拒绝,防 cli 模式双消费)② lock/cron_trigger.lock LOCK_EX\ |
update/队列调度模式改造执行计划.md | 🆕 新增 | 需求评估与改造执行计划文档(S1~S6 串行) |
本阶段验证结论
- 三种模式冒烟(真实库实测):
sync:发帖后立即入 search_index(YES)、无入队(YES)、统计即时(YES);回帖后 total_posts 即时、无 stats 入队(YES)cron:发帖仅入队(YES),访问 cron_trigger.php 后任务消费、帖子入索引、队列清空(YES);二次访问幂等 0 任务(锁/守卫生效)cli:发帖仅入队(YES),cli/worker.php 消费后帖子入索引(YES)- 测试帖已硬删清理(级联 extern/桶/索引),
queue_mode 复位为默认 sync - 全部改动文件
php -l 通过;update/ 已同步 app/ + cron_trigger.php + 计划文档
P1 核心引擎(连接池 / ID 生成器 / 分片路由 / 外置正文)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
app/SplitDB/DBFactory.php | 🆕 新增 | 白皮书模块一「数据库连接池」:单进程 LRU 池,最大 8 个常驻句柄;ATTR_PERSISTENT=false 禁持久连接;连接建立时强制 6 条统一 PRAGMA(busy_timeout 5000 / WAL / synchronous NORMAL / cache_size -20000 / temp_store MEMORY / foreign_keys OFF);路径归一化防同一库双连接;closeAll() 供请求结束清池 |
app/SplitDB/IDGenerator.php | 🆕 新增 | 白皮书模块二「全局 ID 生成器」:global_id.sqlite 的 id_generator 表原子取号;兼容性适配——白皮书用 UPDATE…RETURNING(需 SQLite ≥3.35),本工程基线 PHP 8.0.2 内置 SQLite 3.33.0 不支持,改用 BEGIN IMMEDIATE 写锁事务(SELECT+UPDATE 原子递增,等价且更兼容);maxRetries=3 + 随机退避重试;首次出现表名自动建种子行;事务控制全程原始 SQL(exec('BEGIN IMMEDIATE')/COMMIT/ROLLBACK),避免与 PDO 内部事务标志混用 |
app/SplitDB/ShardRouter.php | 🆕 新增 | 白皮书第五章「分区与路由」:一级季度时序分区(YYYYQn,如 2026Q3);二级哈希分桶(默认 32,ID % 桶数,支持 32→64→128 平滑升级);路径约定 bucket/active/{季度}/{桶}.sqlite(写入 main_index.bucket_path)、bucket/archive/(18 个月冷数据)、extern/{YYYY}/{Qn}/{桶}/{id}.txt;目录自动创建;DATA_PATH/BUCKET_SIZE 优先取 config.php 常量,缺省回落站点根 /data 与 32 |
app/SplitDB/ExternStorage.php | 🆕 新增 | 白皮书模块三「外置文件存储」:帖子/回复正文剥离 SQLite,落 extern/ .txt;写入「临时文件+LOCK_EX+rename」原子替换;Windows 兼容——rename 无法覆盖已存在目标时先删旧再重试(白皮书风险提示第三条);读取 APCu 优先 + 文件回填缓存,无 APCu 自动降级;delete() 连带清理缓存 |
cli/smoke_db_factory.php | 🆕 新增 | DBFactory 冒烟测试:LRU 上限 8、PRAGMA 生效、同路径复用、无持久连接、closeAll。10/10 通过 |
cli/smoke_id_generator.php | 🆕 新增 | IDGenerator 冒烟测试:种子初始化、100 连续无重复、多表隔离、5 连接并发 100 ID 无重复、未知表自动建种子。9/9 通过 |
cli/smoke_shard_router.php | 🆕 新增 | ShardRouter 冒烟测试:季度边界(含跨年)、哈希分布(32/64 桶)、路径规则、目录自动创建。21/21 通过 |
cli/smoke_extern_storage.php | 🆕 新增 | ExternStorage 冒烟测试:原子写入、覆盖重写(Windows rename 分支)、桶目录隔离、缺失返回空、删除清理、无 APCu 降级。10/10 通过 |
冒烟测试运行方式(PHP 8.0.2)
cd D:\phpstudy_pro\Extensions\php\php8.0.2nts
php.exe D:\phpstudy_pro\WWW\cli\smoke_db_factory.php
php.exe D:\phpstudy_pro\WWW\cli\smoke_id_generator.php
php.exe D:\phpstudy_pro\WWW\cli\smoke_shard_router.php
php.exe D:\phpstudy_pro\WWW\cli\smoke_extern_storage.php
P2 任务队列(队列分发 / CAS 消费者 / CLI worker)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
app/SplitDB/Queue.php | 🆕 新增 | 白皮书 7.6 + 8.1「任务队列」:task_queue 表(id/type/data/status/create_time/update_time/retry_count)+ status/update_time 双索引;随机分发到 3 个队列文件之一(meta/task_queue/queue_0~2.sqlite,打散写锁竞争);data 负载 JSON 编码存储;queuePath 自动建目录;countByStatus 诊断统计 |
app/SplitDB/QueueConsumer.php | 🆕 新增 | 白皮书模块四「队列消费者」:CAS 乐观锁抢占(SELECT pending → 条件 UPDATE WHERE id AND status='pending',rowCount()==0 即被其他 worker 抢占放弃);业务成功后置 done,异常不标记失败保持 processing 等待超时自动恢复;超时恢复(update_time < now-timeout:retry<3 回 pending 且 +1,retry>=3 置 failed);drain() 有界消费供测试/--once;consume() 常驻循环。★ 兼容 SQLite 3.33:update_time 用 PHP time() 传参,不依赖 strftime('%s','now') |
cli/worker.php | 🆕 新增 | 后台消费者 CLI:php cli/worker.php [queue_index] [--once];默认常驻消费 queue_0,--once 有界消费完即退出;业务处理器 P2 为占位实现(P6 挂接真实索引重建/统计);supervisor 可管理 3 实例分别绑定 3 队列 |
cli/smoke_queue.php | 🆕 新增 | Queue 冒烟测试:幂等建表、300 任务三队列随机分布、指定队列写入、JSON 负载往返、统计接口。11/11 通过 |
cli/smoke_queue_consumer.php | 🆕 新增 | QueueConsumer 冒烟测试:drain 正常消费置 done、双消费者 CAS 无重复处理、异常任务保持 processing、超时恢复回 pending+retry+1、重试达上限置 failed。11/11 通过 |
端到端验证(worker.php --once)
压入 5 个 rebuild_search 任务 → php cli/worker.php 0 --once → 全部处理置 done(含负载 JSON 解析、队列索引参数、有界退出)。
P3 数据库门面(Schema 建库骨架 / Database 门面 / Model 去反引号 / init.php Session / MySQL 语法清除)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
app/SplitDB/Schema.php | 🆕 新增 | P0a 建库骨架并入本阶段(白皮书第四章目录树 + 全部 meta 库):bootstrap() 幂等建目录树(meta/task_queue/bucket active+archive/extern/lock/log/runtime);business.sqlite 建 22 张非分片业务表(users 含 group_id/email_verify 列、categories、threads、posts、messages、attachments、博客三表、settings、online_users、password_resets、points_log、viewed_replies、level_config、user_groups、category_permissions、tags、thread_tags、thread_votes、post_votes、audit_logs)+ 唯一/普通索引;main_index.sqlite 建 topic_index(白皮书 7.2,P4 分片后启用);search_index.sqlite 建自研倒排表;sessions.sqlite 独立会话库;global_id/task_queue 复用既有 init。桶分片文件不预建(首次写入自动创建,零启动成本) |
app/Core/Database.php | ✏️ 重写 | MySQL 单例 PDO → SplitDB 门面:业务连接走 Schema::businessDb(经 DBFactory 统一 PRAGMA),Session 连接走独立 sessions.sqlite;保留全部原 API(getInstance/getConnection/query/fetchAll/fetchOne/lastInsertId/begin/commit/rollback/getSessionConnection/getQueryCount/closeLastStatement),Models 调用方零改动迁移;数据目录缺失自动 Schema::bootstrap()(空库可初始化) |
app/Core/Model.php | ✏️ 改 | 去反引号 → 双引号标识符引用(SQLite 标准,兼容保留字列名 key);buildWhere 正则补充 " 白名单;lastInsertId 走门面(SQLite lastInsertRowID) |
app/init.php | ✏️ 改 | Session 存储 MySQL → SQLite:读/写/GC 全部改秒级时间戳比较(expires > :now / UPSERT ON CONFLICT(id) DO UPDATE / expires < :now),原 NOW()/DATE_ADD/ON DUPLICATE/SHOW TABLES 探测移除(sessions 表由 Schema 幂等创建);插件初始化与 init_after 钩子按补充要求 // [SplitDB] 临时注释: 注释保留原文 |
app/Models/Thread.php | ✏️ 改 | GREATEST(reply_count-1,0) → MAX(reply_count-1,0)(SQLite 标量 MAX) |
app/Models/User.php | ✏️ 改 | GREATEST(post_count-1,0) → MAX(...);NOW() → :now 参数绑定 |
app/Models/Setting.php | ✏️ 改 | 反引号 ` key → 双引号 "key"` |
app/Helpers/Settings.php | ✏️ 改 | 7 处 ON DUPLICATE KEY UPDATE → SQLite UPSERT ON CONFLICT(...) DO UPDATE(settings/online_users);CAST(AS SIGNED/CHAR) → CAST(AS INTEGER/TEXT);反引号全清 |
app/Helpers/Vote.php | ✏️ 改 | ON DUPLICATE KEY UPDATE → ON CONFLICT({$idCol}, user_id) DO UPDATE(唯一索引 uk_*_votes) |
app/Helpers/Captcha.php | ✏️ 改 | 反引号 → 双引号;ON DUPLICATE → ON CONFLICT("key") DO UPDATE |
app/Helpers/Permission.php | ✏️ 改 | ON DUPLICATE KEY UPDATE → ON CONFLICT(category_id, group_id) DO UPDATE |
app/Helpers/Tag.php | ✏️ 改 | INSERT IGNORE → INSERT OR IGNORE(SQLite 语法) |
app/Helpers/Points.php | ✏️ 改 | SELECT ... FOR UPDATE 行锁 → 条件 UPDATE 原子扣减(UPDATE users SET points=points-:p WHERE id=:id AND points>=:p,rowCount 判余额,等价防并发透支);level_config 内联 MySQL DDL → SQLite DDL |
app/Helpers/AuditLog.php | ✏️ 改 | 内联 CREATE TABLE ... AUTO_INCREMENT ... ENGINE=InnoDB 移除(改由 Schema 统一建表,幂等兜底探测);DATE_SUB(NOW(), INTERVAL) → PHP 计算截止时间参数 |
app/Helpers/Search.php | ✏️ 改 | INSERT IGNORE INTO search_index ×2 → INSERT OR IGNORE |
cli/smoke_schema.php | 🆕 新增 | Schema 冒烟:目录树 8 项、8 个库文件、business 22 表、main_index/search_index/sessions/global_id 核心表、幂等、可写性。48/48 通过 |
cli/smoke_database.php | 🆕 新增 | Database 门面冒烟:自动建库、原 API 兼容(insert/lastInsertId/fetchOne/fetchAll/update)、事务 commit/rollback、Session 独立连接、查询计数、closeAll。13/13 通过 |
cli/smoke_model.php | 🆕 新增 | Model 冒烟:insert/find/findWhere(字符串+数组)/findAllWhere/update/count/query/queryOne/delete/all、双引号保留字列名、setValue UPSERT。13/13 通过 |
P3 验证结论
- grep 终检:Models/Helpers 目录 ZERO MySQL 残留(排除 [SplitDB] 注释说明后 PHP 全量扫描零命中)
- 残留范围说明:Controllers 层(AuthController 的 ON DUPLICATE/NOW/DATE_SUB/AUTO_INCREMENT、Admin 的 DatabaseController/SettingsController 备份与重建 DDL)属 P5 控制器与视图阶段 处理范围,不在 P3
- 全部改动文件
php -l 语法通过;update/ 已同步 app/ + cli/ 全量
P4 模型层(Thread/Post 分片读写 / 业务模型接入 / ViewCounter 防刷)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
app/Models/Thread.php | ✏️ 重写 | 帖子分片读写(白皮书 8.x):insert() 覆写为 global_id 取号 → 季度+哈希路由 → 写桶 topic 真相源 + 正文 extern 原子写入 → 写 main_index.topic_index 索引行(UPSERT)→ 返回全局 ID;find/getById 读索引 bucket_path → 单桶 + extern + 用户/分类装饰(白皮书 5.3 不二次哈希);列表/分页(getLatestAllowed/getByCategoryPaginated/getLatest*)读 topic_index + 批量补用户/分类(避免 N+1);软删/恢复/回收站/硬删/批量删均桶+索引双写,硬删级联清理 extern + reply + reply_index;回复计数桶+索引同步(MAX 防负);incrementView 接入 ViewCounter |
app/Models/Post.php | ✏️ 重写 | 回复分片读写:★设计决策——回复写入【父帖所在桶】的 reply 表(非按回复 ID 再哈希),保证 getByThread 单桶读取、详情页「单桶」语义一致;新增 main_index.reply_index 定位索引(回复按 ID 读取 O(1));find 经 reply_index → 桶 + extern;update 重写 extern;软删桶+索引双写 |
app/SplitDB/Schema.php | ✏️ 改 | 新增桶库初始化:bucketDb()(建桶文件 + topic/reply 表,幂等)、bucketFromPath()(解析 bucket_path 开桶,读取路由唯一依据);topic/reply 表结构 = 白皮书 7.1 + SoSite 扩展字段(category_id/is_pinned/color/reply_to_view/reply_count 等),时间统一秒级时间戳 INTEGER;main_index 新增 reply_index 表 + pid 索引 |
app/SplitDB/ShardRouter.php | ✏️ 改 | 新增 bucketFromPath():从 bucket_path 正则提取桶编号(供 extern 目录定位) |
app/SplitDB/ViewCounter.php | 🆕 新增 | 白皮书模块五 + 补充要求 #6:有 APCu 内存计数(apcu_inc)+ flush() APCUIterator 批量落库 + 防重复 flush;无 APCu 静默降级为直接 UPDATE topic_index.view_count(不崩站,任何 PHP 环境可运行),极端异常静默记日志 |
app/Models/User.php | ✏️ 改 | getStats/getThreads 改读 main_index(topic_index/reply_index)+ 分类名批量补查;deleteWithCascade 分片感知:主题 ID 从 topic_index 收集、回复 ID 从 reply_index 收集、post_votes 按 reply_id 清理、主题经 Thread::hardDelete 级联清桶/extern/索引 |
app/Models/Category.php | ✏️ 改 | getThreadCountMap/getPostCountMap 改读 main_index.topic_index 聚合(真相源最终一致性视图);deleteWithReset 分片感知:逐桶 UPDATE topic.category_id + 索引行同步,再删版块 |
cli/smoke_thread_write.php | 🆕 新增 | Thread 分片写入冒烟:取号/路由/桶行/extern/索引五处落盘、40 帖 ID 连续散落 32 桶。15/15 通过 |
cli/smoke_thread_read.php | 🆕 新增 | Thread 分片读取冒烟:详情(索引→桶→extern→装饰)、列表权限过滤、分页、软删/恢复/回收站。23/23 通过 |
cli/smoke_post.php | 🆕 新增 | Post 回复分片冒烟:父帖桶写入、reply_index 定位、楼层列表、计数联动、update 重写 extern、软删、硬删级联。21/21 通过 |
cli/smoke_viewcounter.php | 🆕 新增 | ViewCounter 冒烟:无 APCu 静默降级 UPDATE、eval 注入模拟 APCu(内存计数/flush 落库/防重复)、Thread::incrementView 接入。19/19 通过 |
cli/smoke_business_models.php | 🆕 新增 | 业务模型集成冒烟:User/Category/Setting 走 business.sqlite、getStats/getThreads/计数映射读 main_index、deleteWithReset/deleteWithCascade 分片感知。21/21 通过 |
P4 验证结论
- 新增 5 个冒烟脚本共 99/99 通过(Thread 写 15 + Thread 读 23 + Post 21 + ViewCounter 19 + 业务模型 21)
- 关键设计决策:回复跟随父帖桶存储(白皮书「详情读单桶」语义);reply_index 保证回复 O(1) 定位
- 修复过程中发现并解决:
getByThread 未过滤软删回复(补 status 过滤)、decorateList 分类名取整行(改取 ['name'])、User::getThreads 排序断言笔误 - business.sqlite 的
threads/posts 表已无写入方(P3 过渡遗留),读取方(Admin/Dashboard/Forum 控制器、Tag/Search 联动)归 P5 控制器与视图阶段 处理 - 全部文件
php -l 通过;update/ 已同步 app/ + cli/ 全量
P5 控制器与视图(前台流程接分片 / MySQL 语法全清 / 插件钩子临时注释 / 后台适配)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
app/Models/Thread.php | ✏️ 改 | 新增分片 update()(内容变更重写 extern、标题/分类/颜色/置顶等桶行+main_index 双写);countActive()(读 main_index 计数,支持版块/精华过滤);getIndexPaginated()(论坛首页列表,读 topic_index + 批量补用户/分类);★修复 decorate() 用户合并时剔除 id(原 array_merge 用用户 id 覆盖帖子 id,导致详情/搜索返回错误 id) |
app/Models/Post.php | ✏️ 改 | ★同修复:decorate() 用户合并剔除 id,防止覆盖回复 id |
app/Controllers/ForumController.php | ✏️ 改 | 首页/版块列表改接 countActive + getIndexPaginated/getByCategoryPaginated(原直读 threads 表 JOIN 查询移除) |
app/Helpers/Search.php | ✏️ 改 | indexThread 改经 Thread 模型读取标题/正文(分片 extern);searchByIndex 结果取数改经 Thread::find(含版块权限过滤、保持排序) |
app/Helpers/Vote.php | ✏️ 改 | toggle 目标存在性校验改读 main_index(topic_index/reply_index),不再直读 threads/posts |
app/Helpers/Permission.php | ✏️ 改 | canReplyToThread 改读 main_index.topic_index 取 category_id |
app/Helpers/Settings.php | ✏️ 改 | runtimeBuildSingle 的 total_threads/total_posts 改读 main_index 计数 |
app/Helpers/Tag.php | ✏️ 改 | getThreads 改经 Thread 模型读分片(关联走 business.thread_tags,PHP 侧排序) |
app/SplitDB/Schema.php | ✏️ 改 | 自研倒排 search_index 表并入 business.sqlite(Search 助手经 business 门面落库的实际归属),撤销独立 search_index.sqlite 拆分 |
app/Controllers/AuthController.php | ✏️ 改 | MySQL 全清:反引号→双引号、ON DUPLICATE→ON CONFLICT DO UPDATE、NOW()/DATE_SUB→PHP 时间参数、password_resets 内联 MySQL DDL→幂等探测(Schema 统一建表) |
app/Controllers/Admin/DatabaseController.php | ✏️ 重写 | 后台备份/优化全 SQLite 化:备份=逐库 VACUUM INTO 一致性快照(文件名 forum_backup_{ts}__{库相对路径}.sqlite 携带映射);优化=wal_checkpoint(TRUNCATE)+VACUUM;恢复/导入=按文件名映射回 data/ 对应库(路径穿越校验 + SQLite 文件头校验);数据库大小=全部 .sqlite 字节和。原 MySQL 导出/导入(SHOW TABLES/SET FOREIGN_KEY/信息架构)全部移除 |
app/Controllers/Admin/SettingsController.php | ✏️ 改 | 搜索重建 DDL 改 SQLite(INTEGER PRIMARY KEY/TEXT/无 ENGINE);帖子分批读取改接 main_index + Thread 模型(含 extern 正文);INSERT IGNORE→INSERT OR IGNORE ×2 |
app/Views/admin/database.php | ✏️ 改 | 类型显示 SplitDB(SQLite 分片);上传改为 .sqlite 并提示命名规范;确认文案更新 |
app/Views/*(9 个文件) | ✏️ 改 | 视图内 11 处 Plugin::hook 按补充要求 // [SplitDB] 临时注释: 前缀注释(保留原文,<?php … ?> 展开为多行注释避免 ?> 被吞) |
app/Controllers/* + app/Core/Controller.php + app/Helpers/Points.php | ✏️ 改 | 控制器/Core/Helpers 内 13 处 Plugin::hook 全部 // [SplitDB] 临时注释: 前缀注释(vote_after/attachment_download_before/auth_*×3/thread_create_after/post_edit_after/avatar_upload_after/post_create_after/thread_edit_after/controller_view_before/points_award_after/points_deduct_after) |
index.php | ✏️ 改 | 4 处路由钩子(admin_route_register/route_register/route_before_dispatch/route_after_dispatch)// [SplitDB] 临时注释: 注释保留原文 |
cli/smoke_frontend_flow.php | 🆕 新增 | 前台全流程冒烟:发帖→列表/分页/精华→详情→回帖→楼层→搜索索引→投票→标签页。21/21 通过(含搜索命中修复验证) |
P5 验证结论
- MySQL 残留终检:前台 Controllers ZERO、Admin Controllers ZERO(排除注释后 PHP 全量扫描)
- 插件钩子终检:活动源码 ZERO 未注释的
Plugin::hook 调用(全部 // [SplitDB] 临时注释: 保留原文) - 前台全流程冒烟 21/21:发帖/列表/详情/回帖/搜索/投票/标签全链路接通
- 修复的关键缺陷:
decorate() 中 array_merge($row, $user) 用用户 id 覆盖帖子/回复 id(单帖调试中 uid 恰与 id 相同被掩盖,多帖场景暴露)→ 合并时 array_diff_key 剔除用户 id - 后台备份/优化/恢复已全 SQLite 化(VACUUM INTO 快照 + 文件名映射恢复)
- 全部文件
php -l 通过;update/ 已同步 app/ + cli/ + index.php 全量
P6 索引与统计(队列事件闭环 / 全量重建 / 冷数据归档 / worker 真实处理器)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
app/Models/Thread.php | ✏️ 改 | 队列事件接入:insert() 激活 Queue::push('rebuild_search', ['topic_id'=>id])(P2 占位启用);update() 标题/正文变更后同样入队重建索引;hardDelete/batchDelete 补清理 business.search_index(type=1) |
app/Models/Post.php | ✏️ 改 | 回帖 insert() 入队 stats 事件(异步刷新全站统计 total_posts) |
cli/rebuild_search.php | 🆕 新增 | 全量重建搜索索引:清空 search_index → 遍历 main_index.topic_index 调 Search::indexThread(标题/正文经 Thread 模型读 extern)→ 遍历 blogs 调 Search::indexBlog;支持 --threads/--blogs 局部重建;进度输出 |
cli/archive.php | 🆕 新增 | 冷数据归档:季度结束超过 18 个月的 bucket/active/{季度}/ 整体移至 bucket/archive/{季度}/(移动前 DBFactory::closeAll() 释放 Windows 句柄);同步 topic_index/reply_index 的 bucket_path(active→archive,白皮书 5.3 读取以 bucket_path 为准,业务代码零改动);支持 --dry-run 预览;正文 extern 不移动(路径独立仍有效) |
cli/worker.php | ✏️ 改 | 处理器挂接真实业务(P2 占位替换):rebuild_search → Search::indexThread;stats → Settings::runtimeBuild;未知类型记录日志不抛错(避免无限重试堆积) |
cli/smoke_queue_events.php | 🆕 新增 | 队列事件冒烟:发帖→1 个 rebuild_search pending、worker 消费后 search_index 建立且可搜、回帖→stats 事件、编辑→rebuild_search、二次消费统计刷新(total_threads/total_posts 读 main_index)、硬删→search_index/topic_index 零残留。12/12 通过 |
P6 验证结论
- 队列闭环端到端:发帖/回帖入队 → 真实
worker.php --once 消费 → search_index 建立(9 行)、搜索命中、统计刷新(total_threads=1/total_posts=1)、pending=0/done=2 - rebuild_search 端到端:播种 2 帖 + 1 博客 → 清空索引 → 真实脚本重建 → 9 条索引、帖子/博客搜索均命中
- archive 端到端:构造 2024Q1 旧季度桶 → 真实脚本归档 → active 文件移入 archive/、topic_index/reply_index bucket_path 同步、归档后经
bucketFromPath 读取正常、extern 正文可读 - 最终一致性闭环达成:Bucket 分片(真相源)→ main_index/reply_index(索引视图)→ search_index/stats(二级缓存) 全链路可重建、可归档、可异步刷新
- 全部文件
php -l 通过;update/ 已同步 app/ + cli/ + index.php 全量
P0 安装与初始化 + 数据迁移脚本(install.php 重写 / migrate.php)
2026-08-13
| 文件 | 状态 | 变更说明 |
|---|
install.php | ✏️ 重写 | MySQL 安装向导 → SplitDB 向导:移除 testMySQLConnection/initDatabaseSchema/手工 MySQL DDL 补建;runInstall = 写 SplitDB config → Schema::bootstrap()(目录树+全 meta 库)+ Schema::seedDefaults()(默认种子)→ 按 Schema users 列建管理员(role=admin/group_id=4)→ 写安装完成标记;环境检测改 pdo_sqlite + data/ 可写;向导步骤精简为 欢迎→环境检测→管理员→完成(去数据库配置步) |
install.php configTemplate/writeConfig | ✏️ 重写 | 去 DB_TYPE/DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASS/DB_CHARSET;新增 SPLITDB_DATA_PATH(默认 __DIR__.'/data')/ SPLITDB_BUCKET_SIZE(32);SOSITE_VERSION → V1.0.0;writeConfig 全新写模板、升级时含 SPLITDB_DATA_PATH 则跳过否则备份后升级 |
app/SplitDB/Schema.php | ✏️ 改 | 新增 seedDefaults()(幂等):用户组 1~4(含管理员组 id=4)、level_config 1~11 级、默认 settings(site_name/posts_per_page 等)、博客分类 4 类;并入 bootstrap() 尾部 |
index.php | ✏️ 改 | 首访引导:config.php 缺失(非 CLI)→ 302 跳转 /install.php,避免 init.php require 时 fatal |
app/Controllers/Admin/DashboardController.php | ✏️ 改 | 环境信息去 DB_TYPE/DB_NAME 残留:数据库显示「SplitDB(SQLite 分片)」+ 数据目录;getDbVersion() 改 sqlite_version() |
protected/database/legacy/ | 🆕 归档 | init.php/schema.sql/install.php(旧 MySQL 初始化死代码)移入 protected/database/legacy/ 归档保留(用户决策②:保留安全备份供查阅),活动源码零引用 |
cli/migrate.php | 🆕 新增 | 旧 MySQL → SplitDB 迁移脚本:--dry-run 预览各表行数;CLI 参数覆盖连接(默认 fastbb/123456);源抽象 MigrateSourceInterface(MySqlMigrateSource 真实连接 + FixtureMigrateSource 供冒烟);B2 基础表(users/categories/settings/博客/私信/标签等 17 表,显式 ID 保外键 + 目标列白名单适配旧库扩展列 + settings 跳过 _runtime_*);B3 分片(threads/posts → extern 先行原子写 → 桶 topic/reply 真相源 → main_index 索引行,ID 保留、桶路由 id%32、回复随父帖桶、软删映射 status、结束后 global_id 游标推进);B4 附件/投票(attachments 显式 ID + thread_votes/post_votes)+ rebuild_search 自动收口 |
cli/rebuild_search.php | ✏️ 改 | 核心逻辑提取为 runRebuildSearch()(可被 migrate 收口复用),直接运行入口加守卫 |
cli/smoke_install.php | 🆕 新增 | P0 冒烟:空目录 bootstrap 目录树/meta 库/队列库、种子齐全、管理员创建链路、install.php/index.php 源码断言。30/30 通过 |
cli/smoke_migrate.php | 🆕 新增 | 迁移全链路冒烟:fixture 源 B2 基础表 + B3 分片三处落盘 + 游标 + B4 附件投票 + rebuild_search 可搜 + 红线故障注入(extern 写失败 → 抛异常/回滚/无索引残留/无 .tmp)。21/21 通过 |
本阶段验证结论
- P0 端到端:空目录 bootstrap 自动建全目录树 + 8 库 + 22 表 + 默认种子;install.php 全 SQLite 化(MySQL 残留 ZERO);index.php 首访跳转守卫就绪;MySQL 初始化死代码已归档
legacy/(活动源码零引用) - 迁移红线(强制):B3 正文剥离 extern 失败 → 立即抛异常、清理本批已写 extern、提示清空 data/ 重跑;故障注入验证「索引行只在外 extern 成功后才写」,绝无「索引成功正文丢失」
- 迁移完整性:fixture 全链路 21/21——基础表显式 ID/外键/列白名单/
_runtime_ 跳过、分片 extern+桶+索引三处落盘、软删 status 映射、global_id 游标推进(topic=101/reply=200)、附件投票、rebuild_search 收口后搜索命中 - 真实 MySQL 迁移验收命令:
php cli/migrate.php --dry-run(预览)→ php cli/migrate.php(全量)→ 自动 rebuild_search → php cli/worker.php 0 --once(刷统计) - 全部文件
php -l 通过;update/ 已同步 app/ + cli/ + index.php + install.php + legacy 归档
待办(后续阶段,按计划串行执行)
- 浏览器整站跑通测试(空目录安装 → 注册发帖 → 列表/详情/回帖 → 搜索全链路)