[开发]FlintHub 双模型交叉裁决修复实施报告

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

本文由 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.phpwrite/read/delete 增加 type 参数(`'topic'\
app/SplitDB/ShardRouter.phpexternRel()/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):

  1. 条件绑定 :since:帖子 COUNT、博客 COUNT、帖子列表、博客列表 4 处,仅当 $since !== '' 时才加入绑定数组;帖子时间过滤改用 topic_index.create_time(秒级 INTEGER),博客沿用 blogs.created_at(字符串),分别绑定 :sinceTs / :since,不再共用歧义参数;
  2. 帖子分支改读 main_index.topic_index:title LIKE + status/deleted_at 过滤 + 版块权限白名单 + 时间过滤,替代退役 threads 表;列表行用 Thread::getByIds() + Thread::fillExternContent() 补正文/用户名/分类(与已修好的索引搜索路径同口径);
  3. 移除 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-L2buildWhere 数组分支全非法键返回 WHERE 1(全表匹配)数组分支改抛异常,与字符串分支 fail-closed 一致app/Core/Model.php
P2-L3count() 传数组条件拼出 WHERE Array(死路径但致命)非字符串条件显式走 buildWhere / 抛类型错误app/Core/Model.php
P2-M3删用户附件文件删除缺路径白名单(与 hardDelete 不一致)basename() + preg_match('/^[a-zA-Z0-9_.\-]+$/') 白名单,不过则跳过app/Models/User.php
P2-L4IDGenerator 高争用最坏 ~15s 阻塞(busy_timeout 5000ms × 3 次重试)下调 busy_timeout/重试次数,取号超时埋点告警app/SplitDB/IDGenerator.php
P2-H-3permission_enforce 默认 '0' 警告模式,未声明权限插件仍放行默认置 '1'(强制模式),缺失权限即 DENY + 日志app/Helpers/Plugin.php
P2-H-4mod_system 封禁/解封直接 UPDATE users SET group_id 绕过 User Model改走 User::updateGroupId() 统一封装plugins/mod_system/Plugin.php、app/Models/User.php
P2-H-5red_packet 创建红包直接 UPDATE users SET points = points - :p,绕过条件原子扣减改走 Points::deduct()(AND points >= :p 条件扣减 + 钩子),事务内联动红包行与积分日志plugins/red_packet/Plugin.php
P2-M-113 个插件直查核心库 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-2mod_system 原生 DELETE FROM blog_comments 绕过 Blog Model新增 Blog::deleteComment() 封装,mod_system 改调用plugins/mod_system/AdminController.php、app/Models/Blog.php
P2-M-3seo 插件 activate() 写全局设置但缺 system:settings 权限plugin.json permissions 补 "system:settings"plugins/seo/plugin.json
P2-M-4notifications 插件 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-01SMTP 密码 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.php11 通过 / 0 失败
cli/smoke_shard_router.php23 通过 / 0 失败
cli/smoke_schema.php51 通过 / 0 失败
cli/smoke_queue_events.php12 通过 / 0 失败
cli/smoke_queue.php11 通过 / 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 裸文件残留。

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

全部回复 (0)

暂无回复

请 登录