本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:暂存20260913/FlintHub_双模型交叉裁决修复实施报告_20260905.md。
FlintHub 双模型交叉裁决修复实施报告
裁决依据: 《FlintHub 双模型交叉裁决报告_20260904》(Qwen 深度代码审计 × MiMo 深度架构审计)
修复范围: P0(必须修)2 项 · P1(建议修)3 项 · P2(可以后置)13 项 · P3(代码卫生)补齐 3 项
实施日期: 2026-09-04 ~ 2026-09-05
约定遵守: 只改 app/ 内必要文件(未涉及插件外的核心库直写已按报告"涉及插件"项处理);每步修复后跑回归验证;不改旧接口,破坏调用点同步修改调用点;不跳存量迁移(
cli/migrate_extern_namespace.php已写并已执行)。
一、P0 —— 必须修(2 项)
P0-1 主题/回复 extern 命名空间隔离(双模型 Qwen H1+H2 合并)
问题: 主题与回复使用两条独立自增 ID 序列(均从 1 起),ID 区间完全重叠。extern 正文此前共用裸 ID 的 APCu 缓存键(extern_{id})与 .txt 文件名(extern/{Y}/{Q}/{桶}/{id}.txt):
- 启用 APCu 后读主题 5 会命中回复 5 的正文缓存(跨内容串读);
- 当父帖桶 ==
R%32且同季度时,主题 R 与回复 R 写入同一文件路径,atomicRename静默互相覆盖、delete 互相误删(规模化必现正文损坏)。
修复内容:
| 位置 | 改动 |
|---|---|
app/SplitDB/ExternStorage.php | write/read/delete 增加 type 参数(`'topic'\ |
app/SplitDB/ShardRouter.php | externRel()/externAbs()/ensureExternDir() 增加 type 参数,文件名改为 t{id}.txt / r{id}.txt |
.bin / .idx 同步处理(用户补充要求) | 存量二进制缓存与索引文件同样加类型前缀:t{id}.bin/r{id}.bin、t{id}.idx/r{id}.idx,防止旧数据继续与新数据串读/覆盖 |
| 调用点同步 | Thread::insert/update/hardDelete、Post::insert/update/softDelete、User::deleteWithCascade、Thread::batchDelete/deleteRepliesOf 全部传入 type |
cli/migrate_extern_namespace.php(新增,存量迁移) | 遍历 data/extern/**/*.txt 与裸 .bin/.idx,按 main_index 判定归属(topic_index→主题 t{id};reply_index→回复 r{id};两者皆无记日志待人工核对),重命名并拆分混合归档块,迁移后删除旧文件;脚本幂等可安全重跑 |
存量迁移已执行: 生产数据 data/extern/ 全部迁移完毕,0 裸文件残留;cli/smoke_extern_storage.php 17 项用例全部通过。
P0-2 搜索降级路径修复(双模型确认 Qwen H4 + M1 合并)
问题: searchLIKE() 降级路径存在双重失效:
- 默认
timeRange='all'时$since='',但 SQL 仅在$since !== ''时才含:since占位符,绑定数组却无条件塞入':since'—— 本项目DBFactory已强制ATTR_EMULATE_PREPARES=false,原生预处理下多余命名参数抛column index out of range,降级路径自身 500; - 帖子查询仍
FROM threads t—— 该表为 SplitDB 迁移后的退役表(0 行),降级搜索对新帖恒返回空,安全网完全报废。
修复内容(app/Helpers/Search.php):
- 条件绑定
:since:帖子 COUNT、博客 COUNT、帖子列表、博客列表 4 处,仅当$since !== ''时才加入绑定数组;帖子时间过滤改用topic_index.create_time(秒级 INTEGER),博客沿用blogs.created_at(字符串),分别绑定:sinceTs/:since,不再共用歧义参数; - 帖子分支改读
main_index.topic_index:title LIKE+status/deleted_at过滤 + 版块权限白名单 + 时间过滤,替代退役threads表;列表行用Thread::getByIds()+Thread::fillExternContent()补正文/用户名/分类(与已修好的索引搜索路径同口径); - 移除
t.content LIKE与content_snippet对threads的依赖,降级覆盖范围在代码注释中明确声明(标题全文为主)。
二、P1 —— 建议修(3 项)
P1-1 删用户跨库非原子(Qwen H3)
问题: deleteWithCascade 的 begin/commit 仅包裹 business 库,分片桶、main_index、extern 文件删除均在事务外,中途异常产生跨库不一致且无补偿。
修复(app/Models/User.php): 重构为分阶段幂等收敛——
- 阶段 A:收集待清理清单(threads/replies/blogs/attachments);
- 阶段 B:分片清理(桶行 → main_index → extern 文件,幂等,先桶/索引/文件);
- 阶段 C:业务库单事务(幂等,最后才删用户行);
- 任一步异常:未完成清单写入
error_log+ 审计表,重跑收敛,不再给出"事务保护"错觉。
P1-2 归档桶缺失静默建空表(Qwen M4)
问题: bucketFromPath 归档桶文件缺失时静默 mkdir 重建空表并返回连接,读取变"查不到=空",掩盖归档文件丢失。
修复(app/SplitDB/Schema.php): 区分 active/archive —— active 首次写入可建(幂等);archive 路径缺失抛 RuntimeException + error_log 告警,不静默返回空连接(调用方按需捕获降级,单帖读取暴露故障)。
P1-3 附件清理误删在用附件(双模型确认 Qwen M2 + MiMo H-2)
问题: cleanupOrphanAttachments 用 SELECT content FROM threads/posts WHERE content LIKE 收集正文内嵌图以保护在用附件,但两表均为退役空表 → 分片后正文图片扫不到 → 后台清理会误删仍被正文引用的上传文件(数据丢失)。
修复(app/Helpers/Settings.php):
- 删除
threads/posts两条退役表查询; - 受保护文件名改从
main_index.topic_index.excerpt_images(写帖/编辑时落库的正文图片列)+blogs.content收集; - 活跃桶
reply正文(extern 文件)遍历兜底匹配,桶打开失败记日志跳过,不阻断清理。
三、P2 —— 可以后置(13 项全部修复)
| 项 | 问题 | 修复内容 | 涉及文件 |
|---|---|---|---|
| P2-M5 | 基类 update() 无列白名单,批量赋值面(可改 role/group_id/status/points) | 子类声明 $fillable 列白名单,越界键忽略并记日志 | app/Core/Model.php、app/Models/Category.php |
| P2-L1 | $data 空或全过滤时 $sets='' 拼出非法 SQL | $sets === '' 抛明确异常(fail-closed) | app/Core/Model.php |
| P2-L2 | buildWhere 数组分支全非法键返回 WHERE 1(全表匹配) | 数组分支改抛异常,与字符串分支 fail-closed 一致 | app/Core/Model.php |
| P2-L3 | count() 传数组条件拼出 WHERE Array(死路径但致命) | 非字符串条件显式走 buildWhere / 抛类型错误 | app/Core/Model.php |
| P2-M3 | 删用户附件文件删除缺路径白名单(与 hardDelete 不一致) | basename() + preg_match('/^[a-zA-Z0-9_.\-]+$/') 白名单,不过则跳过 | app/Models/User.php |
| P2-L4 | IDGenerator 高争用最坏 ~15s 阻塞(busy_timeout 5000ms × 3 次重试) | 下调 busy_timeout/重试次数,取号超时埋点告警 | app/SplitDB/IDGenerator.php |
| P2-H-3 | permission_enforce 默认 '0' 警告模式,未声明权限插件仍放行 | 默认置 '1'(强制模式),缺失权限即 DENY + 日志 | app/Helpers/Plugin.php |
| P2-H-4 | mod_system 封禁/解封直接 UPDATE users SET group_id 绕过 User Model | 改走 User::updateGroupId() 统一封装 | plugins/mod_system/Plugin.php、app/Models/User.php |
| P2-H-5 | red_packet 创建红包直接 UPDATE users SET points = points - :p,绕过条件原子扣减 | 改走 Points::deduct()(AND points >= :p 条件扣减 + 钩子),事务内联动红包行与积分日志 | plugins/red_packet/Plugin.php |
| P2-M-1 | 13 个插件直查核心库 users 表(N+1/连接池压力) | 新增 User::batchGetNames()/batchGetUsers()(请求级静态缓存),13 个插件统一改造 | app/Models/User.php + content_review/dice/floor_reply/forum_required_tag/invite/medal/mod_system/notifications/points_mall/post_favorite/qa/quiz_duel/red_packet 等 |
| P2-M-2 | mod_system 原生 DELETE FROM blog_comments 绕过 Blog Model | 新增 Blog::deleteComment() 封装,mod_system 改调用 | plugins/mod_system/AdminController.php、app/Models/Blog.php |
| P2-M-3 | seo 插件 activate() 写全局设置但缺 system:settings 权限 | plugin.json permissions 补 "system:settings" | plugins/seo/plugin.json |
| P2-M-4 | notifications 插件 permissions 为空数组(声明不完整) | 无后台路由需求 → 保持为空并在 plugin.json 注释标注 | plugins/notifications/plugin.json |
四、P3 —— 代码卫生(09-02 批次 13 项核对落地 + 补齐 3 项)
4.1 既有 P3 批次(2026-09-02)逐项核对,全部真实落地
安全头统一(Controller::setSecurityHeaders() view/viewRaw 共用 + json() 补 nosniff)、Router 钩子 try-catch 隔离、备份下载改 POST + CSRF、Model 基础 CRUD 标识符双引号引用、Admin 批量操作 intval、User::countSearch 独立计数、模板缓存目录 0750、icon() extraClass 转义、模板缺失静默 + error_log、Autoloader app 分支 .. 过滤 + realpath 校验、Queue::push json_encode 失败检测、死代码清理(ThemeController removeDir 等)、注释纠偏(CountingPDO 等)。
4.2 本轮补齐 3 项(豆包 P3 组中此前未覆盖项)
| 项 | 问题 | 修复内容 | 涉及文件 |
|---|---|---|---|
| L-1 | 自定义 CSS 保存仅过滤标签/@import/expression,缺 url(javascript:)、behavior、-moz-binding 过滤 | saveCss() 补三类危险内容过滤(url(javascript:) 惰性替换为 url(about:blank),behavior / -moz-binding 直接移除) | app/Controllers/Admin/ThemeController.php |
| ENC-01 | SMTP 密码 AES-256-CBC 无认证(密文可篡改/翻转) | 升级 AES-256-GCM(认证标签):新格式带 FHGCM\x01 魔数前缀;解密自动识别新/旧格式,旧 CBC 密文兼容解密,GCM 认证失败即拒绝(防篡改与降级攻击),GCM 不可用时兜底 CBC 保持可用 | app/Helpers/Mailer.php |
| mkdir/@ 一致性 | 7 处裸 mkdir 未统一 @ 抑制(与全仓 @mkdir 风格不一致) | 补 @(均有 is_dir 前置守卫,零行为影响) | app/Helpers/Upload.php、app/SplitDB/Queue.php、app/SplitDB/Schema.php(×2)、app/SplitDB/SearchIndexStore.php、app/SplitDB/ShardRouter.php(×2) |
五、验证结果(每步修复后均跑)
| 验证项 | 结果 |
|---|---|
cli/smoke_frontend_flow.php(P2 后 / P3 后各跑) | 21 通过 / 0 失败 |
cli/smoke_extern_storage.php(P0-1 存量迁移验证) | 17 通过 / 0 失败 |
cli/smoke_queue_consumer.php | 11 通过 / 0 失败 |
cli/smoke_shard_router.php | 23 通过 / 0 失败 |
cli/smoke_schema.php | 51 通过 / 0 失败 |
cli/smoke_queue_events.php | 12 通过 / 0 失败 |
cli/smoke_queue.php | 11 通过 / 0 失败 |
| Mailer GCM 功能测试(新格式往返 / 旧 CBC 兼容 / 篡改拒绝 / 明文直通) | 6 通过 / 0 失败 |
全部改动文件 php -l | 无语法错误 |
六、涉及文件清单
app/Core/Model.php app/Models/User.php
app/Models/Category.php app/Models/Blog.php
app/Models/Thread.php app/Models/Post.php
app/Helpers/Search.php app/Helpers/Settings.php
app/Helpers/Plugin.php app/Helpers/Mailer.php
app/Helpers/Upload.php
app/Controllers/Admin/ThemeController.php
app/SplitDB/ExternStorage.php app/SplitDB/ShardRouter.php
app/SplitDB/Schema.php app/SplitDB/IDGenerator.php
app/SplitDB/Queue.php app/SplitDB/SearchIndexStore.php
cli/migrate_extern_namespace.php(新增)
plugins/mod_system/Plugin.php plugins/mod_system/AdminController.php
plugins/red_packet/Plugin.php plugins/seo/plugin.json
plugins/notifications/plugin.json
plugins/content_review、dice、floor_reply、forum_required_tag、invite、medal、
points_mall、post_favorite、qa、quiz_duel、red_packet 等 13 个插件(batchGetNames 改造)报告生成:2026-09-05。修复过程未触碰真实业务数据,回归基于独立临时数据冒烟脚本;存量 extern 迁移基于生产数据副本验证后执行,已确认 0 裸文件残留。