[开发] SoBBS(FlintHub 1.5.0 → Go)开发规划方案

👑Lv.11 元老 🌞 骨干会员
2026-11-03 15:32:49
项目内容
文档版本v0.1(规划稿,待评审)
编写日期2026-11-03
代码基线fh_reference/flinthub1.5.0(只读参考,不修改)
架构依据sobbs-whitepaper/ARCHITECTURE.md
目标产物单文件 Go 程序 sobbs,直接接管 FH 1.5.0 的 data/ 目录
文档状态需评审通过后,方可进入 B0

0. 文档导读

0.1 本方案要解决的问题

用 Go 重写 FlintHub 1.5.0,并满足一条硬性承诺:

同一份 data/ 目录,PHP 版与 Go 版可以交替启动,读写结果一致;切换只需改 Nginx upstream,不需要数据迁移。

这不是"重写一个论坛",而是"换一个进程去读写同一套已有数据"。因此本方案的重心不是功能列表,而是兼容契约、数据真相源、阶段验收。

0.2 阅读路径

角色建议章节
项目负责人 / 评审人第 1、2、6、9、10 章
后端开发第 3、4、5、6、7 章
前端 / 模板开发第 3.3、5.3、6 章(B7)、7.3
测试 / 运维第 7、8、9 章及附录 A/B/E
插件开发者第 5.5、6 章(B8)、附录 C

0.3 阶段推进规则

  1. 严格按 B0 → B1 → … → B10 顺序推进,前一阶段验证报告未通过,不进入下一阶段。
  2. 每个阶段结束必须产出 docs/reports/Bx-verify.md,内容至少包含:
    • 本阶段任务完成清单(逐条对应任务 ID);
    • 执行的测试命令与结果;
    • 兼容性用例结果(涉及数据层的阶段必填);
    • 已知问题与是否阻断下一阶段;
    • 下一阶段准入结论。
  3. 每个阶段只允许修改本阶段范围内的文件;跨阶段改动必须回到对应阶段补齐验收。

1. 项目定义

1.1 一句话目标

以 Go 标准库为主体重写 FlintHub 1.5.0,保持 data/、Cookie、会话、设置缓存、页面缓存、搜索索引、任务队列逐字节/逐语义兼容,并在此之上提供更安全的运行时与更清晰的模块边界。

1.2 核心目标

编号目标判定方式
G1数据 100% 兼容同一 data/ 下,PHP ↔ Go 交替启动,第 7.3 节 10 条用例全绿
G2功能对等前台 50 条路由、后台 84 条路由、API 7 条路由全部有 Go 实现或明确的下线决策
G3视觉与交互对等64 个视图转写后,关键页面截图对比通过;CSS/JS 复用不破坏
G4单文件零依赖部署CGO_ENABLED=0 交叉编译,Linux 单二进制可启动
G5安全基线达标第 7.7 节安全检查表全绿,无高危项
G6可回滚同主版本内只换二进制;切换失败 5 分钟内切回 PHP 且数据不受损
G7可演进插件、主题、迁移、升级都有明确扩展点,不把分片写成硬依赖

1.3 非目标(本版本明确不做)

项说明
不做数据库结构重构不合并表、不改字段类型、不新增必填字段
不做数据迁移脚本不把现有数据搬到新结构;只做"同构接管"
不支持运行时加载 Go 插件插件后端编译期注入;运行时只安装模板/静态资源
不重写前端框架继续复用 FH 的 CSS/JS/字体/图标与页面结构
不追求 API 全 JSON 化前台仍以 HTML 渲染为主,/api 保持 JSON
不做多租户 / 多站点单站点单实例

1.4 发布阻断红线(任何一条不满足即不得上线)

  1. 向现有表增加必填字段,或删除/改名/改类型。
  2. 把 sessions.data 改成 JSON 或其他格式。
  3. 用 SQLite AUTOINCREMENT 取代 global_id.sqlite 发号。
  4. 正文写到 extern/ 之外的位置,或改变 t/r 前缀语义。
  5. 改变时间字段单位(秒 ↔ 毫秒),或把 business 表的本地时间字符串改成整数。
  6. 重命名/移动 data/meta/ 下的库文件。
  7. 升级或部署时只拷 .sqlite 不带 -wal / -shm。
  8. 允许未验签的插件包安装。
  9. 响应体出现 err.Error()、绝对路径、SQL 片段。
  10. data/、protected/、logs/ 可被 Web 直接访问。

2. 现状盘点

2.1 FH 1.5.0 代码规模(实测)

项数量 / 值
PHP 文件192 个,约 2.97 MB
控制器文件33 个(前后台含同名类)
Helper 类34 个
Model 类7 个(Category / Message / Post / Setting / Tag / Thread / User)
视图文件64 个(含 7 个 _components 组件、layouts、errors)
语言包3 套(zh.php / en.php / zh_tw.php)
核心插件钩子49 个(以 docs/插件开发规范.md §9.1 为准)
路由注册141 条(前台 50、后台 84、API 7,见附录 A)
前端资源CSS 8 个、JS 10 个、lib 3 个、字体 4 个、奖章 10 个、PWA 图标 5 个

2.2 必须理解的 8 个机制

#机制关键事实对 Go 的要求
1SplitDB 分片季度目录 + 32 桶;读路径以 main_index.bucket_path 为准不做二次哈希;支持 32→64/128 扩容路由
2全局发号global_id.sqlite 的 id_generator;BEGIN IMMEDIATE 事务禁止 AUTOINCREMENT;取号必须原子
3正文外置extern/{年}/{季}/{桶}/t|r{id}.txt,另有 .bin/.idx 归档块读优先 .bin/.idx,回退 .txt;写入原子
4会话sessions(id,data,expires);data 是 PHP 序列化串;Cookie 名 PHPSESSID;TTL 1800必须实现 PHP session 编解码,读改写保留未知键
5设置缓存protected/settings_cache.php = PHP 包装头 + JSONGo 写设置后必须原子重建该文件
6页面缓存data/runtime/pages/{key}.html,按主题/语言/摘要偏好分键共用或独立命名空间需显式配置;发帖/回帖必须失效
7搜索索引data/meta/search/search_{YYYYQn}.sqlite,token/type/target_id/weight分词结果必须与 PHP 侧一致,否则索引漂移
8任务队列meta/task_queue/queue_0..2.sqlite,租约 + 重试与 PHP worker 共享时,锁文件与租约语义必须一致

2.3 白皮书已确定、本方案直接继承的结论

  1. 纯标准库:net/http + html/template + database/sql + modernc.org/sqlite;不引入 Gin/Echo/ORM。
  2. 模板外置:禁止 embed;themes/ 目录 + mtime 缓存失效。
  3. 插件编译期注入:后端静态编译,运行时只装视图与资源;签名校验(Ed25519 + SHA-256)。
  4. 只加不删的迁移铁律,migrations/NNNN_*.sql 幂等、可中断恢复。
  5. 请求生命周期复刻 FH,钩子名沿用 FH 命名。
  6. 安全基线:路径穿越、SQL 注入、XSS、CSRF、会话固定、资源耗尽逐项防护。

2.4 事实冲突清单(白皮书 vs FH 1.5.0 实际代码)

这一节是本方案与"直接照抄白皮书"的区别所在。以下冲突必须在 B0 阶段固化为测试夹具,后续实现以实际代码/实际库为准,白皮书作为原则依据。

编号冲突点白皮书说法FH 1.5.0 实际本方案处置
C1时间单位全库秒级 int64business.sqlite 多为 TEXT Y-m-d H:i:s(users/messages/attachments/notifications/audit_logs 等);main_index 与桶为 INTEGER 秒双轨制:business 按本地时间字符串读写;索引/桶按秒级整数。详见 4.4
C2博客表实测 23 张表,含 blogs、blog_categories、blog_comments1.5.0 Schema.php 核心 DDL 不创建这 3 张表(为原站遗留/插件数据)不删除、不改写;B0 采样确认是否真实存在;由 D1 决策归属
C3表数量23 张核心 DDL 20 张;+3 张遗留博客表 = 23Go 只"确保"20 张核心表;3 张遗留表按只读/插件归属处理
C4配置来源新增 config.json现网真实配置在 config.php(define() 常量),且 index.php 在连库前就读取 settings_cache.phpGo 必须同时支持 config.php 只读解析与 config.json 进程配置,优先级见 3.8
C5设置缓存未展开settings_cache.php 是 <?php /* ... */ ?> + JSON;Settings::get() 与 index.php 都读它实现同格式解析与原子重建;详见 4.8
C6主题新增 themes/ 模板层FH 主题是 CSS 变体 + 选项,页面结构写死在 PHP 视图;Cookie site_theme 保存键Go 新增模板层,必须保留原主题键、CSS 类名与 Cookie 语义;详见 B7
C7插件Go 插件 + 钩子PHP 插件(49 钩子,运行时 include)静态 Go 插件;钩子名对齐;PHP 插件不执行,给出迁移边界
C8分片扩容只提 32 桶有 SPLITDB_LAST_EXPANSION_AT / SPLITDB_PREV_BUCKET_SIZE 扩容路由必须实现 bucketForWrite,新帖落 [PREV, NEW) 范围
C9发号实现建议 UPDATE ... RETURNING为兼容 SQLite 3.33,改用 BEGIN IMMEDIATE + SELECT/UPDATEGo 沿用 BEGIN IMMEDIATE,不依赖 RETURNING
C10页面缓存只提游客列表页缓存键含主题/语言/摘要偏好,另有 category_stats.cache、category_<id>_latest.json一并纳入兼容清单;详见 4.9
C11上传目录未细述assets/uploads/,白名单后缀,MAX_FILE_SIZE=5MB沿用路径与 URL;上传走 Go 安全校验
C12时区未提config.php 执行 date_default_timezone_set('Asia/Shanghai')Go 用站点时区解析 business 时间;编译内置 time/tzdata
C13根目录占位库站点根 data/business.sqlite 为 0 字节真实库在 data/meta/启动自检识别 0 字节占位并拒绝误连
C14退役表threads/posts 0 行Post/Thread 模型仍存在,但真相源为索引+桶+externGo 不映射这两张表,不删除不写入

3. 总体架构

3.1 设计原则

原则落地要求
兼容优先任何"更优雅"的重构,若会改变磁盘字节、Cookie、缓存文件,一律不做
真相源唯一读正文只走 topic_index/桶/extern;threads/posts 不参与
薄基础设施不引入框架;路由/上下文/中间件自研且可单测
失败可见数据库/路径/格式异常必须记日志并返回稳定错误码,不静默降级
读写分离读请求直接走连接池;写操作可走单写者队列,读绝不排队
可回滚迁移只加不删;升级失败可用备份 + 旧二进制恢复

3.2 技术选型

层选型约束
语言Go 1.22+只用标准库 + modernc.org/sqlite + golang.org/x/crypto
HTTPnet/http + 自研 lib/router支持 {id} 命名参数、.html 后缀、子目录 BASE_PATH
模板html/template 外置 themes/禁止 embed 模板;mtime 失效;FuncMap 提供 raw/asset/url/pagination/t
数据库modernc.org/sqliteCGO_ENABLED=0;DSN 带 _pragma=busy_timeout(5000)、_pragma=journal_mode(WAL)
会话自研 lib/session/phpsess双向 PHP session 序列化;PHPSESSID
日志log/slogJSON 结构化 + request_id;文件按天/大小轮转(外部 logrotate 或自研)
时区time/tzdata 内置解析 business 本地时间不依赖系统 tzdata
前端原样复用 FH assets/CSS/JS/字体/图标不重写;模板只转写 HTML 结构

3.3 源码目录结构(建议)

d:\sobbs\
├── cmd/sobbs/main.go              # 入口:加载配置 → 自检 → 启动 HTTP
├── lib/
│   ├── router/                    # 路由、参数、方法、BASE_PATH
│   ├── context/                   # 请求上下文、UserID/Role/IP/Lang/RequestID
│   ├── middleware/                # Recovery/RequestID/RealIP/SecurityHeaders/BodyLimit/Timeout/CSRF/Auth/RateLimit
│   ├── config/                    # config.json + config.php 解析
│   ├── db/                        # 连接管理、PRAGMA、LRU、事务、重试
│   ├── splitdb/                   # schema/shardrouter/id/extern/pagecache/settings/search/queue
│   ├── session/                   # session store + phpsess 编解码
│   ├── cache/                     # 进程内 KV(TTL + 主动失效)
│   ├── template/                  # 加载器、FuncMap、mtime 缓存
│   ├── i18n/                      # 三语包加载、键集合自检
│   ├── security/                  # SafeJoin、HTML 净化、密码、token、限流
│   ├── upload/                    # 上传校验、缩略图、随机命名
│   ├── mail/                      # SMTP(异步发送)
│   ├── plugin/                    # 注册表、钩子总线、动态资源包
│   └── compat/                    # PHP 时间/序列化/配置/缓存格式兼容
├── app/
│   ├── handlers/                  # 前台 handler(forum/thread/post/auth/...)
│   ├── admin/                     # 后台 handler
│   ├── api/                       # /api handler
│   ├── services/                  # 业务服务(事务编排)
│   └── models/                    # 数据访问(SQL 逐字段对齐)
├── themes/modern/                 # 外置模板(layout + partials + pages;沿用 FH 默认主题键 modern)
├── plugins/                       # 静态插件(plugin.go + plugin.json + views/assets)
├── assets/                        # 复用 FH 静态资源(css/js/fonts/img/medals/pwa/uploads)
├── migrations/                    # 只加不删的 SQL 迁移
├── tools/                         # compat/dbcheck/langimport/bench/doctor
├── testdata/fixtures/             # FH 只读样本(脱敏)+ 校验清单
├── docs/                          # 本方案、兼容清单、阶段验证报告
└── go.mod

3.4 生产部署布局(与 FH 同构)

/var/www/sobbs/
├── sobbs                    # 单文件二进制
├── config.json              # 进程级配置(监听、日志、超时)
├── config.php               # 存量站原有配置(Go 只读解析,不执行)
├── data/                    # 与 FH 完全同构,直接接管
│   ├── meta/{business,main_index,sessions,global_id}.sqlite
│   ├── meta/search/search_YYYYQn.sqlite
│   ├── meta/task_queue/queue_0..2.sqlite
│   ├── bucket/{active,archive}/YYYYQn/<0..31>.sqlite
│   ├── extern/YYYY/Qn/<bucket>/t|r<id>.txt
│   ├── runtime/pages/*.html
│   ├── runtime/category_stats.cache
│   ├── runtime/category_<id>_latest.json
│   ├── lock/cron_trigger.lock
│   └── log/
├── themes/modern/
├── plugins/
├── assets/                  # 含 assets/uploads/
└── protected/               # 仅存量兼容:settings_cache.php、level_config_cache.php、rate_cache

3.5 分层与依赖规则

cmd → app/handlers, app/admin, app/api
app/* → app/services → app/models → lib/splitdb → lib/db
app/* → lib/template, lib/i18n, lib/security, lib/upload
lib/plugin ← 只依赖 lib/context、lib/router、lib/splitdb 的公开接口

硬性规则:

  1. lib/splitdb 不允许 import app/*。
  2. handler 不直接写 SQL;写操作必须经 service。
  3. 插件只能通过公开接口访问数据库与钩子,不得直接引用 handler。
  4. 任何跨层错误必须用哨兵错误包装(%w),出口统一翻译。

3.6 请求生命周期(对齐 FH index.php)

① 加载 config.json(失败即启动失败,输出可读原因)
② 读取 config.php 兼容常量(data 路径、桶大小、上传、邮件、CRON_KEY、时区)
③ 初始化 slog + 生成/透传 request_id
④ 打开 business/main_index/sessions/global_id;分片桶按需懒加载
⑤ 会话解析:Cookie → sessions 表 → PHP 反序列化 → 还原身份
⑥ 构建 Context(UserID/Role/IP/Lang/Theme/RequestID)
⑦ 全局中间件:Recovery → RequestID → SecurityHeaders → BodyLimit → Auth → RateLimit → CSRF
⑧ 游客页面缓存前置命中(仅 GET、无登录 Cookie)
⑨ 路由分发:静态资源 → 前台 → /api → 后台(鉴权)→ 插件路由
⑩ 业务处理 → 模板渲染 / JSON
⑪ 收尾:计数批量回写、缓存刷新、会话续期、释放连接

与 FH 的差异(必须记录并在验收中说明):

项FHGo原因
route_before_dispatch历史行为会派发两次只派发一次白皮书附录 D 已确认;插件迁移需注意
会话写入lazy_write 下只 updateTimestamp显式续期 expires避免 30 分钟无故掉线
计数回写register_shutdown_function响应完成后批量 flush等价语义

3.7 并发与写模型

场景方案
读每个库独立 *sql.DB;MaxOpenConns:business/index 8、bucket 4、global_id 1、sessions 4
写默认直接写 + SQLITE_BUSY 指数退避重试;高并发阶段启用单写者队列(可选开关)
发号global_id 连接串行,MaxOpenConns=1 + BEGIN IMMEDIATE
桶连接路径级 LRU(默认 8 个常驻),归档桶连接设 PRAGMA query_only=1
后台任务独立 goroutine / 独立进程 sobbs worker,带优雅退出与上下文超时
禁止请求内同步 HTTP/邮件/对象存储外呼;无缓冲 channel 永久阻塞

3.8 配置体系

三层配置,优先级从高到低:

环境变量(FLINTHUB_SITE_URL / FLINTHUB_CRON_KEY 等)
  > 站点根 config.json(Go 进程级)
  > 站点根 config.php(存量兼容,只读解析)
  > 内置默认值

config.json 建议字段(只放进程级):

{
  "listen": "127.0.0.1:8080",
  "base_path": "",
  "data_dir": "./data",
  "theme": "modern",
  "session_cookie": "PHPSESSID",
  "session_ttl": 1800,
  "page_cache_shared": false,
  "timezone": "Asia/Shanghai",
  "log_level": "info",
  "log_dir": "./logs",
  "trusted_proxies": ["127.0.0.1"],
  "read_timeout": 15,
  "write_timeout": 30,
  "queue_mode": "auto"
}

config.php 必须解析的常量(只允许标量字面量,不执行 PHP):

常量用途
SPLITDB_DATA_PATH / SPLITDB_BUCKET_SIZE数据根、桶数量
SPLITDB_LAST_EXPANSION_AT / SPLITDB_PREV_BUCKET_SIZE扩容路由
UPLOAD_PATH / UPLOAD_URL / MAX_FILE_SIZE / ALLOWED_EXTENSIONS上传
MAIL_DRIVER / MAIL_HOST / MAIL_PORT / MAIL_USER / MAIL_PASS / MAIL_ENCRYPTION / MAIL_FROM_ADDR / MAIL_FROM_NAME / MAIL_PASS_KEY邮件
CRON_KEYcron 触发密钥
DEFAULT_THEME / POSTS_PER_PAGE / THREADS_PER_PAGE展示
BASE_PATH / SITE_URL子目录部署与绝对链接
date_default_timezone_set(...)站点时区

解析失败策略:不静默兜底。缺失关键常量时启动失败并打印缺失项;MAIL_PASS 等敏感值只进内存,不写日志。

3.9 错误、日志与可观测性

项规范
错误判定哨兵错误 ErrNotFound/ErrForbidden/ErrUnauthorized/ErrConflict/ErrServiceUnavailable
对外稳定 code + 人话提示;HTML 页面沿用 FH 404/429 视图
对内slog.Error 带 request_id、路径、用户 ID、耗时、错误链
request_id接受合法 X-Request-ID,否则 crypto/rand 生成 16 字节;响应头回写
日志红线不记录明文密码、token、完整 Cookie、SQL 参数中的敏感值
健康检查GET /healthz(仅本机或内网白名单):DB 可读、队列可写、模板可加载
慢请求>500ms 记 WARN;>2s 记 ERROR

3.10 安全基线(摘要,详细见 7.7)

  • 统一 SafeJoin,所有模板名/主题名/插件名/附件名白名单。
  • 全部 SQL 参数化;排序/筛选走白名单映射。
  • 默认 html/template 转义;raw 仅用于服务端净化后的正文。
  • CSRF:表单隐藏字段 + JSON X-CSRF-Token;30 分钟滑动续期。
  • 会话:PHPSESSID、HttpOnly、SameSite=Lax、HTTPS 自动 Secure、登录换 SID。
  • 响应头按白皮书 11.6 固定;CSP 不含 blob:。
  • 请求体 2MB(上传单独限制)、Header 1MB、超时 5/15/30/60s。

4. 数据兼容设计(核心章节)

4.1 兼容契约

层次契约验收方式
文件布局目录名、库名、桶编号、正文命名完全一致文件树 diff + 路径单测
表结构表名/字段/类型/默认值与 FH 一致PRAGMA table_info 快照对比
字符编码UTF-8 无 BOM读库后逐值 utf8.Valid
时间business=TEXT 本地时间;index/bucket=秒级整数双端写入后逐字段比对
布尔0/1模型层禁止 bool 直写
会话PHP session 序列化编解码往返 + 双端登录
正文外置 extern/,索引库只存摘要字节级比对
缓存设置/页面/统计快照格式一致文件内容比对
队列表结构 + 租约 + 重试语义双端消费交叉测试
搜索季度库 + token/weight 语义同一帖分词集合对比

4.2 库与表清单

4.2.1 data/meta/business.sqlite(主库)

核心 20 张(FH 1.5.0 Schema::ensureBusiness 创建):

表用途时间格式Go 写入
users用户、角色、组、积分、等级、记住登录 token、邮箱验证TEXT是
categories版块TEXT是
threads退役表(0 行)TEXT否
posts退役表(0 行)TEXT否
messages私信TEXT是
attachments附件元数据TEXT是
settingsKV 设置-是
online_users在线心跳TEXT是
password_resets重置 tokenTEXT是
points_log积分流水TEXT是
viewed_replies回复浏览去重TEXT是
level_config等级规则-是
user_groups用户组-是
category_permissions版块×组权限(含 can_attach)-是
tags标签TEXT是
thread_tags主题标签关联-是
thread_votes主题投票TEXT是
post_votes回复投票TEXT是
notifications通知TEXT是
audit_logs审计TEXT是

遗留 3 张(实测库存在,1.5.0 核心 DDL 不创建):

表处置
blogs保留不动;由 D1 决定是 Go 博客插件接管还是只读
blog_categories同上
blog_comments同上

Schema 策略:Go 的 ensureBusiness 必须逐字复刻 FH DDL 与列补丁(如 category_permissions.can_attach),不得新增 FH 不认识的列;只允许新增可选列/新表,且有默认值。

4.2.2 data/meta/main_index.sqlite

  • topic_index:id/uid/title/create_time/last_reply_time/view_count/reply_count/status/category_id/is_pinned/is_highlighted/color/reply_to_view/deleted_at/bucket_path/is_archived/excerpt/excerpt_images,时间 INTEGER。
  • reply_index:id/pid/uid/create_time/update_time/status/bucket_path。
  • 索引必须与 FH 同名同列序(idx_topic_index_list、idx_topic_index_pinned_ct、idx_topic_index_hot、idx_topic_index_uid、idx_reply_index_pid、idx_reply_index_uid、idx_reply_index_status 等),否则查询计划劣化。
  • WAL 模式必须校验;非 WAL 拒绝启动(fail-closed)。

4.2.3 桶库 bucket/{active,archive}/YYYYQn/<n>.sqlite

topic:id/uid/category_id/title/create_time/update_time/last_reply_time/view_count/reply_count/status/is_pinned/is_highlighted/color/reply_to_view/deleted_at/extern_path/excerpt/excerpt_images。

reply:id/pid/uid/create_time/update_time/status/deleted_at/extern_path。

索引:idx_topic_category、idx_reply_pid、idx_reply_pid_status_ct;建表后写 PRAGMA user_version = 2(与 FH BUCKET_SCHEMA_VERSION 一致),命中即跳过 DDL。

归档桶:PRAGMA query_only=1,只读;禁止回复/编辑。

4.2.4 sessions.sqlite

CREATE TABLE sessions (id TEXT PRIMARY KEY, data TEXT, expires INTEGER NOT NULL);
CREATE INDEX idx_sessions_expires ON sessions (expires);

4.2.5 global_id.sqlite

CREATE TABLE id_generator (table_name TEXT PRIMARY KEY, last_id INTEGER DEFAULT 0);
INSERT OR IGNORE ... ('topic',0),('reply',0);

4.2.6 meta/task_queue/queue_0..2.sqlite

CREATE TABLE task_queue (
  id INTEGER PRIMARY KEY,
  type TEXT,
  data TEXT,
  status TEXT DEFAULT 'pending',
  create_time INTEGER,
  update_time INTEGER DEFAULT 0,
  retry_count INTEGER DEFAULT 0,
  lease_token TEXT,
  lease_until INTEGER,
  dedupe_key TEXT
);
-- idx_status, idx_update_time

状态机:pending → processing(租约) → done | failed;未知类型记录日志后丢弃(与 FH 一致,避免无限重试堆积)。

4.2.7 meta/search/search_YYYYQn.sqlite

CREATE TABLE search_index (
  token TEXT NOT NULL,
  type INTEGER NOT NULL DEFAULT 1,   -- 1=帖子 2=博客
  target_id INTEGER NOT NULL,
  weight INTEGER NOT NULL DEFAULT 1,
  created_at TEXT                    -- 'Y-m-d H:i:s'(注意:不是整数)
);
-- idx_search_index_token(token)、idx_search_index_target(type,target_id)、
-- idx_search_index_created(created_at)、UNIQUE(token,type,target_id,weight)

写入按目标创建时间所在季度路由;删除按 target 扫描季度;查询逐 token 等值查询后归并(避免 IN (...) 走错索引)。

4.3 时间双轨制(最重要的兼容细节)

数据面存储格式Go 读写规则
business 表(users/messages/...)TEXT 2006-01-02 15:04:05按站点时区(默认 Asia/Shanghai)格式化/解析;禁止写 Unix 整数
main_index / 桶INTEGER 秒一律 time.Unix() 秒;禁止毫秒
extern 文件文件名含 ID,不含时间不解析
sessions.expiresINTEGER 秒同 FH
queue.create_time/update_time/lease_untilINTEGER 秒同 FH
search.created_atTEXT Y-m-d H:i:s与 business 同一本地时间格式;按此路由季度

实现建议:

// lib/compat/dbtime
func FormatBusiness(t time.Time, loc *time.Location) string // "2006-01-02 15:04:05"
func ParseBusiness(s string, loc *time.Location) (time.Time, error)
func UnixSeconds(t time.Time) int64

验收:同一操作在 PHP 与 Go 写入后,对同一行做字段级 diff,时间字符串逐字节一致(时区一致时)。

4.4 分片路由

函数规则
quarter(ts)YYYY + Q + ceil(month/3)
bucket(id,size)id % size
bucketForWrite(id, created)扩容生效(LAST_EXPANSION_AT>0 && created>=LAST 且 PREV<SIZE)→ PREV + id%(SIZE-PREV);否则 id%SIZE;非法配置回落 id%SIZE 并记日志
bucketRel(q,b)bucket/active/{q}/{b}.sqlite
bucketArchiveRel(q,b)bucket/archive/{q}/{b}.sqlite
bucketFromPath(p)严格正则解析;非法抛错
读取路由只以 topic_index.bucket_path / reply_index.bucket_path 为准,绝不重算

4.5 外置正文(Extern)

路径:

extern/{YYYY}/{Qn}/{bucket}/t{topicID}.txt
extern/{YYYY}/{Qn}/{bucket}/r{replyID}.txt

归档块(同目录):

t|r{floor(id/1000)*1000}.bin   # [4字节大端长度][正文]...
t|r{floor(id/1000)*1000}.idx   # JSON { "id": offset }

读写顺序与红线:

  1. 路径安全校验(拒绝 NUL、..、越出 data/)。
  2. 读:进程内 LRU → .bin/.idx → .txt;.bin 存在但 .idx 缺失、ID 不在 idx → 回退 .txt,前台不得 404。
  3. .bin 长度异常(<=0 或 >16MB)或短读 → 记 ERROR 并回退 .txt。
  4. 写:先写同目录临时文件 → 原子 rename;再更新 DB;旧路径按需删除。
  5. 删除:删 .txt + 在 .idx.lock 下从 idx 移除并原子重写;.bin 内死字节保留(不可变归档)。
  6. 局部读(摘要链路):只读前 N 字节,不污染完整正文缓存。
  7. 新帖只写 .txt,不写 .bin/.idx;归档任务另行处理。

4.6 全局 ID

  • 库:meta/global_id.sqlite,表 id_generator(table_name,last_id)。
  • 算法:BEGIN IMMEDIATE → INSERT OR IGNORE 种子 → SELECT last_id → UPDATE last_id+1 → COMMIT。
  • busy_timeout:取号期间 1000ms,结束恢复 5000ms;锁竞争重试 3 次,5–20ms 抖动。
  • 禁止:AUTOINCREMENT、内存自增、按桶独立发号。

4.7 会话(PHP 兼容)

项值/规则
库meta/sessions.sqlite
表sessions(id TEXT PK, data TEXT, expires INTEGER)
TTL1800 秒;每次读/写续期 expires=now+1800
有效判定expires > now
SIDGo 侧生成 32 位十六进制(属于 PHP 允许字符集);PHP 侧创建的历史 SID 不做长度硬校验
校验严格模式:SID 必须存在于库且未过期,否则丢弃并重新生成(validateId 语义)
登录换新 SID,删除旧行
CookiePHPSESSID、Path=/(子目录为 BASE_PATH)、HttpOnly、SameSite=Lax、HTTPS 下 Secure
数据格式PHP session 序列化:`user_id

编解码要求:

  • 支持 s(字符串,长度按字节)、i、b、d、N、a(嵌套数组)。
  • 双向可逆:解析 → 重编码 → 与原文逐字节一致(未知键必须保留原顺序)。
  • 读改写:只修改需要的键,未识别键原样保留;禁止整表重写为 JSON。
  • 遇到不支持的标记:不重写该会话(返回只读),记 WARN;不得破坏用户登录态。

4.8 设置缓存与运行时缓存

文件格式Go 动作
protected/settings_cache.php<?php /* settings cache — do not include */ ?>\n{JSON}\n启动先解析;写设置后原子重建(tmp+rename)
data/runtime/pages/*.htmlHTML见 4.9
data/runtime/category_stats.cacheJSON/自定义发帖/回帖/删帖后失效或重建
data/runtime/category_<id>_latest.jsonJSON同上
protected/level_config_cache.phpPHP 包装 + 数据等级配置变更后重建(与 FH 同步)

设置写流程:

DB UPDATE/UPSERT settings
→ 清进程内设置缓存
→ 从 DB 全量读取 → 生成 JSON → 原子写 settings_cache.php
→ 失效相关页面缓存

4.9 页面缓存

项规则
目录data/runtime/pages/
键实际构造顺序:{页面名}[_pref{N}][_{lang}][_t{主题}];默认语言/默认主题不追加后缀(保证存量缓存零影响);?preview_theme= 单独命名
生效范围仅游客 GET;登录用户不缓存
失效发帖、回帖、删帖、置顶/高亮、版块变更、设置变更、主题变更
响应头Cache-Control: private, no-cache + 统一安全头
共存模式page_cache_shared=false 时 Go 写 pages_go/,避免两端模板差异互相污染;B10 视觉对齐后切 true

4.10 搜索索引

  • 分词:必须移植 FH Segmenter(含词典与停用词),以"同一文本 → token+weight 集合完全一致"为验收。
  • 写入:新帖/编辑/删除后写 rebuild_search 队列任务;消费者调用索引重建。
  • 重建:cli/search_rebuild 可全量重建;季度文件缺失时按目标时间创建。
  • 双端共存:B2 完成后如分词一致性未达标,Go 只入队、不自行消费,交由 PHP worker 处理(配置项 search_consumer=php|go)。

4.11 任务队列与定时任务

项规则
队列数3(queue_0..2.sqlite)
入队随机选队列;dedupe_key 去重
消费认领时写 lease_token/lease_until;租约超时可重领;retry_count 上限 3
锁共享 data/lock/cron_trigger.lock(flock 语义)与 PHP cron 互斥
触发GET /cron/trigger 校验 X-Cron-Key(或 ?key= 过渡),空 key 返回 503
维护会话 GC、审计清理、限流缓存清理、计数器校准、搜索重建、归档

已知任务类型(以 FH consumer 注册为准):rebuild_search、stats;B0 必须枚举全部类型并落表,Go consumer 对未知类型"记录 + 丢弃"。

4.12 附件与上传

项规则
目录assets/uploads/(UPLOAD_PATH)
URL/assets/uploads/(UPLOAD_URL)
命名随机名 + 白名单后缀;支持 YYYY-MM/ 与 seed_avatars/、covers/、pwa/ 命名空间
限制MAX_FILE_SIZE=5MB;后缀 jpg/jpeg/png/gif/webp/pdf/zip/doc/docx
校验扩展名白名单 + MIME 嗅探 + 图片解码校验;禁止 SVG 上传(种子头像除外)
下载filepath.Base + SafeJoin + 登录/权限校验

4.13 兼容红线(实现期逐条检查)

  1. 不写 threads / posts。
  2. 不新增 FH 不认识的必填列。
  3. 不改 bucket_path、extern_path 的语义。
  4. 不改 .idx 的 JSON 结构(键必须是字符串 ID,值必须是整数偏移)。
  5. 不把会话数据写成 JSON/Go gob。
  6. 不在 data/ 外存储正文。
  7. 不用 AUTOINCREMENT。
  8. 不在启动时自动"修复/重排"现有数据。
  9. 不在 PHP 共存期轮换 remember_token(默认只校验)。
  10. 不在未对齐分词前让 Go 消费搜索重建任务。

5. 模块映射与功能清单

5.1 FH Helper → Go 包映射

FH HelperGo 位置阶段
Auth / RememberMe / Csrf / Permissionlib/session, lib/security, app/services/authB3
Settings / Cache / PageCachelib/splitdb/settings, lib/cache, lib/splitdb/pagecacheB2
Upload / Content / Emojilib/upload, lib/security/content, app/services/contentB4
Search / Segmenter / SearchRebuildlib/splitdb/search, lib/compat/segmenterB2/B5
Mailerlib/mailB5
Notification / Points / Vote / Tagapp/services/*B4/B5
Theme / UserPrefs / I18nlib/template, lib/i18n, app/services/prefsB1/B7
Url / RewriteRules / RewriteProbelib/router(BASE_PATH、伪静态)B1
Captcha / PoW / RateLimiter / KeyGuardlib/security/*B3
AuditLogapp/services/auditB3
Plugin / PluginUpdater / Market / Updater / release_keyslib/plugin, app/services/market, app/services/updaterB8/B9

5.2 路由与控制器映射

完整路由清单见附录 A。分组如下:

分组数量Go 包
前台页面50app/handlers/*
后台管理84app/admin/*
API7app/api/*
插件路由动态lib/plugin 注册

5.3 视图清单(64 个)

分组文件阶段
布局/错误/离线layouts/main.php、errors/404.php、errors/429.php、offline.phpB1/B3
认证auth/login、auth/register、auth/forgot_password、auth/reset_password、auth/messageB3
论坛/主题/发帖forum/index、forum/category、thread/show、thread/edit、post/create、post/editB4
消息/通知/搜索/标签/用户/主题设置message/*、notifications/index、search/index、tags/*、user/show、theme/settingsB5
后台admin/*(31 个)B6/B9
组件_components/*(7 个)B7 收口

5.4 通用组件(必须优先转写)

left_sidebar、right_sidebar、mobile_sidebar、notification_bell、attachment_card、image_lightbox、footer_beian。

它们被大量页面引用,先完成组件再铺页面,能显著降低返工。

5.5 插件与钩子

  • 49 个钩子按生命周期分组(完整表在 B0 从 FH 插件开发规范.md §9.1 复制为 docs/COMPAT_HOOKS.md):
    • 初始化/路由:init_after、route_register、route_before_dispatch;
    • 布局/导航:layout_head_end、nav_plugin_links、footer_links、controller_view_before;
    • 内容:topic_created、post_created、upload_url 等;
    • 用户/后台/搜索等其余分组。
  • Go 钩子总线要求:注册顺序 + priority 稳定排序;单个监听器 panic 隔离;同步执行;同一事件可重试,监听器需幂等。
  • 插件形态:
    • 静态插件:plugins/<id>/plugin.go + plugin.json + views/ + assets/,编译期空白导入;
    • 动态资源包:仅视图/静态资源,可在线安装;Ed25519 验签 + SHA-256 + ZIP Slip 防护。
  • PHP 插件兼容边界:不执行 PHP;列出可迁移插件(blog/checkin 优先)、暂不支持插件(依赖 PHP 运行时的 AI/统计类需重写)。

6. 分阶段实施计划

说明:以下任务按依赖排序,不含工期估算。每个阶段结束必须提交验证报告并评审通过。

B0 工程基线与兼容样本

目标:在不写业务代码的前提下,把"什么是兼容"固化成可执行资产。

ID任务实现要点验收
B0-T1初始化 Go 模块与目录go mod init sobbs;cmd/sobbs、lib、app、themes、plugins、tools、testdatago build ./... 通过
B0-T2编码规范与 CIgofmt、go vet、staticcheck、errcheck、go test -race;依赖方向检查脚本CI 全绿;新增依赖需评审
B0-T3FH 只读样本库采集 data/ 全目录(含 -wal/-shm)、extern .txt/.bin/.idx、sessions 样本、settings/page/level caches;脱敏后放 testdata/fixtures样本校验和清单;关键表行数快照
B0-T4兼容工具链tools/dbdump(表结构+行校验和)、tools/dualrun(PHP/Go 交替启停)、tools/externcheck、tools/phpsess工具可一键跑通;输出可比对报告
B0-T5config.php 只读解析器仅解析标量 define() 与 date_default_timezone_set;不 eval;缺失项报错单测覆盖常量、引号、数组(ALLOWED_EXTENSIONS)
B0-T6启动自检 doctor检查数据目录、0 字节占位库、关键表、WAL、时区、写权限、CRON_KEY、上传目录sobbs doctor 对样本库输出逐项 OK/FAIL

退出条件:样本库可被工具完整读取;配置解析与自检通过;兼容清单(C1–C14)有测试用例占位。


B1 基础设施层

目标:路由、上下文、中间件、数据库、缓存、会话、模板骨架可独立运行并通过单测。

ID任务实现要点验收
B1-T1日志与 request_idslog JSON;request_id 生成/透传;字段规范单测 + 日志样例评审
B1-T2路由{id} 参数、方法匹配、.html 后缀兼容、BASE_PATH、404/405、路由组路由表单测;与 FH 路径逐条对照
B1-T3ContextUserID/Role/GroupID/IP/Lang/Theme/RequestID;登录态只读快照单测
B1-T4中间件链Recovery、RequestID、SecurityHeaders、BodyLimit、Timeout、Auth、RateLimit、CSRF 挂载点集成测试:异常不泄内部错误
B1-T5数据库管理连接池、PRAGMA(WAL/busy_timeout/synchronous/cache_size/temp_store/foreign_keys=OFF)、非 WAL fail-closed、LRU 桶连接、归档 query_only并发读写测试无 database is locked
B1-T6进程内缓存KV + TTL + 主动失效 + singleflight单测;失效路径覆盖
B1-T7PHP session 编解码s/i/b/d/N/a;字节长度;未知键保留;往返逐字节往返测试 + fuzz + 样本会话
B1-T8会话存储读/校验/续期/写/换 SID/GC;Cookie 属性与 PHP 交替登录用例
B1-T9模板骨架与 i18n外置 themes/modern;layout/partials;FuncMap(raw/asset/url/pagination/t);mtime 失效;三语键集合自检热更新测试;三语键差集为空
B1-T10静态资源服务/assets、/themes、/plugins 安全直出;MIME、缓存头、禁目录列表路径穿越测试全拒绝

退出条件:go test -race ./lib/... 全绿;最小站点(首页占位、404、静态资源)可启动。


B2 SplitDB 数据层

目标:读写 FH 数据目录的每一个字节级对象,并通过双端校验。

ID任务实现要点验收
B2-T1business schema ensure逐字复刻 20 张表 DDL 与列补丁;不新增必填列与样本库 PRAGMA table_info 逐列 diff 为空
B2-T2main_index ensure2 表 + 全部索引同名同列序索引快照 diff 为空
B2-T3桶 schema ensuretopic/reply + 3 索引 + user_version=2新桶创建与老桶幂等
B2-T4ShardRouterquarter/bucket/bucketForWrite/路径/前缀/校验表驱动单测覆盖扩容边界
B2-T5IDGeneratorBEGIN IMMEDIATE、种子、重试、busy_timeout 还原并发取号 10k 次无重复/无跳号异常
B2-T6Extern 读.bin/.idx → .txt、limit 局部读、LRU、异常回退样本 .bin/.idx 与 .txt 字节一致
B2-T7Extern 写/删原子写、旧路径清理、idx 锁内移除、死字节保留写后 PHP 可读;删后两端均不可读
B2-T8PageCache键组合、游客命中、失效、响应头与 FH 缓存键逐名对比
B2-T9Settings 缓存解析/重建 settings_cache.php;统计快照与计数器Go 改设置 → PHP 读到新值
B2-T10搜索存储与分词季度库、写入/删除/查询、Segmenter 移植同文本 token/weight 集合完全一致
B2-T11队列存储3 库、入队、租约认领、完成/重试、去重与 PHP worker 交叉消费无重复执行
B2-T12迁移与 bootstrapschema_migrations、只加不删、幂等探测、目录树创建重复执行结果一致

退出条件:用 FH 样本库启动 Go,完成"读主题/回复/设置/会话"与"写新主题/回复/设置";PHP 端逐项读回一致;报告附字节级比对。


B3 认证与用户

ID任务实现要点验收
B3-T1注册用户名/邮箱规则、唯一性、初始组/等级/积分、审计正反向用例
B3-T2登录bcrypt 校验($2y$/$2a$/$2b$)、封禁判定、换 SID、审计与 PHP 账号互通
B3-T3记住登录共存期只校验不轮换;完全切换后按 FH 规则轮换双端交替不掉线
B3-T4CSRFtoken 生成/校验/滑续期/轮换/一次性消费;403 友好页表单+JSON 用例
B3-T5权限模型role/group_id、user_groups、category_permissions、管理员组=4与 FH 判定逐用例一致
B3-T6个人中心资料、头像、签名、偏好、主题/布局读写与视图对等
B3-T7找回密码token 一次性、短时效、邮件异步、重置后下线所有会话正向 + 重放拒绝
B3-T8邮箱验证发送/验证/重发节流与 FH 语义一致
B3-T9限流/验证码/PoWIP+账号双维度;文件或独立存储;PoW 校验触发限流返回 429 视图
B3-T10审计与封禁审计表写入、封禁拦截、节流封禁用户所有入口均失效
B3-T11认证视图login/register/forgot/reset/message与 FH 截图对比

退出条件:PHP 建会话 → Go 识别;Go 建会话 → PHP 识别;密码/CSRF/权限用例全绿。


B4 论坛核心读写

ID任务实现要点验收
B4-T1首页统计、可见版块、最新/热门、置顶、在线排序与 FH 完全一致
B4-T2版块页分页、筛选、排序白名单、权限过滤边界页码/空版块
B4-T3主题详情topic_index + 桶 + extern + 回复分页 + 附件 + 投票状态正文逐字节一致
B4-T4发帖事务取号 → 建桶写行 → extern → 索引 → 计数/标签/队列;失败补偿断电式中断测试无半成品
B4-T5回帖事务父帖桶写 reply → extern → reply_index → 计数/通知/积分楼层顺序与时间正确
B4-T6编辑主题/回复权限、版本、extern 重写、索引更新编辑后两端一致
B4-T7删除/恢复/回收站软删、硬删、归档只读、级联清理无残留正文/索引
B4-T8上传与附件类型/大小/MIME、随机名、缩略图、下载鉴权恶意文件名/伪造 MIME 拒绝
B4-T9标签关联、热门、详情、清理计数一致
B4-T10投票/收藏/浏览唯一约束、去重、计数原子更新并发重复提交不重复计数
B4-T11版务操作置顶/高亮/颜色/移动版块/归档索引与桶同步
B4-T12计数一致性增量更新 + 全量校准 + 每日兜底与 FH runtimeBuild 结果一致
B4-T13论坛视图forum/index、forum/category、thread/show、post/create、post/edit、thread/edit截图 + 交互对比

退出条件:完成 7.3 兼容用例第 3–6、8–10 条;发帖/回帖在 PHP 端可正常展示。


B5 消息、通知、搜索、用户主页、PWA

ID任务实现要点验收
B5-T1私信收件箱/已发/撰写/详情/已读与 FH 字段一致
B5-T2通知类型、聚合、已读、铃铛未读数一致
B5-T3搜索查询解析、跨季度归并、过滤、分页、重建任务与 PHP 搜索结果前 N 一致
B5-T4标签页列表/详情/分页对等
B5-T5用户主页资料、帖子、回复、统计对等
B5-T6主题/布局偏好site_theme Cookie、布局切换、侧栏开关切换即时生效
B5-T7PWAmanifest、offline、图标、SW 头安装/离线可用
B5-T8在线状态心跳、在线数、清理与 FH 阈值一致
B5-T9邮件SMTP、异步、失败重试、日志脱敏测试邮箱收到验证/重置信

退出条件:搜索索引读写双向可用;通知/私信/主页视图通过。


B6 后台核心

ID任务实现要点验收
B6-T1仪表盘站点概览、版本、告警、快捷入口数据与 FH 一致
B6-T2数据库概览/引擎/设置库大小、WAL、表行数、维护入口只读安全
B6-T3站点设置(基础/系统/显示/邮件/搜索/队列/PWA)写 DB + 重建 settings_cache + 失效缓存PHP 端即时读到
B6-T4用户管理列表、搜索、编辑、封禁、删除、积分/等级权限与审计
B6-T5主题管理列表、编辑、置顶、高亮、移动、删除与桶/索引同步
B6-T6版块/标签管理CRUD、排序、权限入口前台即时生效
B6-T7回收站恢复/硬删/批量无残留
B6-T8等级/用户组/权限规则、组、版块权限矩阵前台判定一致
B6-T9审核/审计日志待审内容、日志查询与清理审计完整
B6-T10维护重建统计、清理缓存、搜索重建、会话 GC幂等、可重复执行
B6-T11后台视图31 个 admin 视图文件转写截图对比

退出条件:后台所有"会写数据"的操作在 PHP 端可见;设置缓存文件一致。


B7 视图收口、主题与响应式

ID任务实现要点验收
B7-T1模板规范layout/define/partial、命名、上下文结构文档化所有页面统一布局
B7-T2组件转写7 个 _components 优先完成复用页面无重复代码
B7-T3剩余页面逐页对照 64 个视图清单销项清单 100% 关闭
B7-T4CSS/JS 复用原样拷贝 assets;类名/ID/数据属性不改视觉回归通过
B7-T5编辑器marked/turndown/lightbox/预览/上传发帖链路可用
B7-T6响应式/暗色三栏、移动侧栏、主题变量、夜间模式多断点截图
B7-T7三语zh/en/zh_tw 键一致;页面缓存语言分键切换无空白/无英文兜底
B7-T8资源指纹与缓存?v=mtime;CSP 兼容改 CSS 立即生效
B7-T9页面缓存共享视觉对齐后切 page_cache_shared=true双端命中同一缓存无差异

退出条件:64 个视图销项完成;关键页面(首页/版块/主题/发帖/登录/后台首页)截图对比通过。


B8 插件体系与生态

ID任务实现要点验收
B8-T1钩子总线49 钩子注册/派发/优先级/panic 隔离/幂等钩子表单测
B8-T2插件接口与注册表Name/Version/Init/RegisterRoutes;编译期空白导入示例插件可开关
B8-T3plugin.json 兼容字段语义与 FH 一致;ID=目录名;activated 状态读写与 FH 清单互读
B8-T4动态资源包安装ZIP entry 校验、体积上限、原子移动、类型白名单ZIP Slip/炸弹测试拒绝
B8-T5签名与信任Ed25519 + SHA-256 + min_core;无签名仅开发者模式篡改包拒绝
B8-T6后台插件页列表、编辑配置、启用/停用、卸载、权限强制操作落盘一致
B8-T7博客插件兼容遗留 blogs 表(D1 决策)或自有表文章读写/评论
B8-T8签到插件积分、连续签到、审计与 FH 规则一致
B8-T9插件迁移指南49 钩子映射、PHP→Go 改造示例、限制清单文档评审通过

退出条件:至少 2 个静态插件可启停;动态资源包可安装/卸载;钩子派发不破坏主流程。


B9 高级后台、异步与运维

ID任务实现要点验收
B9-T1队列 worker长驻进程、租约、退避、优雅退出、指标与 cron 互斥
B9-T2cron 触发X-Cron-Key、空 key 503、flock 兼容与 PHP cron 共用锁不重入
B9-T3归档任务18 个月冷数据移入 archive、只读、路径更新归档后前台可读、不可回复
B9-T4备份/恢复 CLI整目录(含 wal/shm)、校验行数、恢复演练备份可恢复
B9-T5升级器版本语义、预检、备份、锁、应用、回滚同 MAJOR 可回滚
B9-T6插件市场清单同步、授权、下载、验签、安装与 FH 市场协议兼容
B9-T7主题上传/编辑路径白名单、ZIP 安全、CSS 保存上传/删除安全
B9-T8日志与监控轮转、慢请求、队列积压、5xx 指标指标可导出
B9-T9热升级syscall.Exec 或 fork+exec;Connection: close低峰升级可回滚
B9-T10运维 CLIdoctor/backup/restore/verify-index/rebuild-search/migrate命令帮助与退出码规范

退出条件:队列/归档/备份/升级四类演练通过;无数据丢失。


B10 全量兼容验收与发布

ID任务实现要点验收
B10-T110 条双端用例按白皮书 10.3 逐条执行(本方案 7.3 已展开)全绿
B10-T2极端样本超长标题、emoji、引号/script、中文用户名、空正文、超大页码两端一致
B10-T3字节级校验extern 内容、settings/page cache、session datadiff 为空
B10-T4性能压测2C2G 目标;静态页/主题页/写入P95 达标,无 5xx,无锁错误
B10-T5安全检查路径/SQL/XSS/CSRF/上传/限流/头部高危项清零
B10-T6灰度切换演练0→5%→50%→100%,回退秒级演练报告
B10-T7打包发布Linux amd64/arm64、Windows 开发包;校验和单文件可启动
B10-T8文档交付部署、运维、插件开发、兼容说明、已知差异评审通过

退出条件:发布检查表全绿;评审会签通过;可进入灰度。


7. 测试策略

7.1 测试分层

层范围工具
单元router、phpsess、shardrouter、config、safejoin、模板函数go test + table-driven
集成handler + 临时 data 目录 + 真实 SQLitehttptest + fixtures
兼容与 FH 样本/PHP 双端tools/dualrun + dbdump
端到端浏览器关键路径browser automation
性能列表/主题/写入/搜索k6 或 wrk
安全gosec、govulncheck、fuzzCI

7.2 兼容夹具要求

  1. 覆盖所有库与目录(含 -wal/-shm)。
  2. 至少包含:正常主题、超长正文、纯 emoji 标题、已删除主题、归档主题、.bin/.idx 归档块、损坏 .idx、缺失桶文件。
  3. 会话样本:匿名、登录、管理员、封禁、过期。
  4. 每次样本变更必须更新校验和清单,禁止手工改样本。

7.3 双端切换用例(发布门槛)

按白皮书 10.3 执行,并补充 Go→PHP 方向:

#用例期望
1PHP 注册登录,Go 携带 Cookie仍登录
2Go 登录,PHP 携带 Cookie仍登录
3Go 发帖(中文+富文本)PHP 正确显示
4PHP 回帖,Go 读取楼层/时间正确
5两端改设置另一端即时生效
6Go 删除主题,PHP 查看已删除、无残留
7归档帖 Go 回复拒绝且提示一致
8分页/排序/置顶/高亮顺序完全一致
9remember_token 交替登录不掉线(共存期不轮换)
10页面缓存命中两端渲染无差异(共享模式下)

7.4 PHP 会话编解码专项

  • 单元往返:解析 → 编码 → 逐字节比较。
  • Fuzz:随机键/嵌套数组/超长字符串/非法序列化串,要求不 panic、不丢未知键。
  • 双端:Go 改写 csrf_time 后 PHP 能继续读取。

7.5 数据一致性专项

  • dbdump:表结构、行数、关键列校验和。
  • 写入后:桶行、topic_index、reply_index、extern、搜索索引、计数器六处一致。
  • 中断测试:事务中杀进程,重启后无半成品(schema_migrations、队列租约)。
  • 并发测试:同帖并发回复、并发投票、并发取号。

7.6 性能门槛(建议)

场景目标
游客首页(命中页面缓存)P95 < 50ms
版块列表(1000 帖)P95 < 150ms
主题详情(50 回复)P95 < 200ms
发帖/回帖写入P95 < 250ms
搜索(一年数据)P95 < 500ms
2 小时压测无 5xx、无 database is locked 持续增长

7.7 安全检查表

项要求
路径穿越所有名字输入过 SafeJoin;fuzz 全拒绝
SQL全参数化;排序白名单
XSS默认转义;净化器白名单标签/协议
CSRF所有状态变更校验;登录/登出轮换
会话固定防护、换 SID、Cookie 属性
上传后缀+MIME+大小+随机名;禁可执行类型
限流登录/注册/找回/搜索/上传
信息泄露错误响应无内部细节;目录不可列
依赖govulncheck 无已知高危
日志无密码/token/完整 Cookie

7.8 CI 流水线

lint → unit → integration → race → compat(fixtures) →
nightly: dualrun(PHP) + 性能基准 + 安全扫描

8. 迁移与上线

8.1 上线前数据准备

  1. 整目录备份 data/(含 -wal/-shm),校验关键表行数。
  2. 确认 data/meta/business.sqlite 存在且非 0 字节;根目录 0 字节占位库忽略。
  3. 确认 protected/settings_cache.php 可写;data/lock、data/runtime 可写。
  4. 记录当前 PHP 版本、FH 版本、桶大小、扩容时间戳。

8.2 灰度切换

阶段流量观察回退
0内网接口正确性、数据 diff切回 PHP
15% 读5xx、P95、缓存命中切回 PHP
250% 读写数据一致性抽检、队列切回 PHP
3100%全量指标切回 PHP

切换动作只改 Nginx upstream;不迁移数据。

8.3 回滚

  • 同 MAJOR:直接换回旧二进制,不碰 data/。
  • 升级失败:用备份覆盖 data/ + 放回旧二进制。
  • 不允许用 rsync --delete 同步 data/。

8.4 运行方式

  • systemd:ExecStart=/var/www/sobbs/sobbs,Restart=on-failure,工作目录站点根。
  • Nginx:静态 /assets、/themes、/plugins 直出,其余反代 127.0.0.1:8080。
  • 定时任务:系统 cron 调用 curl -H "X-Cron-Key: ..." /cron/trigger,或 sobbs worker 常驻。

8.5 监控指标

指标告警阈值
5xx 率> 0.5% 持续 5 分钟
P95 延迟超 7.6 门槛 2 倍
SQLITE_BUSY 日志> 10 次/分钟
队列积压pending > 1000 或最老任务 > 10 分钟
搜索索引漂移重建后 target 数差 > 0
页面缓存失效失败任意一次写失败

9. 风险登记表

ID风险影响概率缓解措施检测信号
R1漏拷 -wal/-shm空库、数据"消失"中备份/部署脚本强制整目录;启动校验行数doctor FAIL
R2时间格式混用排序错、显示 1970高双轨制封装;字段级 diff 测试双端对比不一致
R3会话格式不兼容切换掉登录高phpsess 往返 + 双端登录双端用例 1/2 失败
R4settings_cache 不同步后台设置不生效中写 DB 后原子重建;启动对账PHP 读旧值
R5页面缓存跨端污染换主题/语言不生效中默认独立命名空间;对齐后共享截图差异
R6SQLite 写竞争502/超时中WAL + busy_timeout + 重试 + 单写者BUSY 日志
R7扩容路由错误新帖进旧桶低bucketForWrite 表驱动测试桶分布异常
R8extern 路径/idx 漂移正文串读/丢失中SafeJoin + idx 锁 + 回退 .txt字节 diff
R9分词不一致搜索结果漂移高移植 + token 集合验收;未达标交 PHP搜索对比
R1064 视图转写回归视觉/交互破损高组件优先 + 截图回归 + 逐页销项视觉 diff
R11后台范围蔓延延误主线高B6 核心 / B9 高级分阶段;市场/升级可延期里程碑评审
R12PHP 插件不可用站长功能缺失高明确边界;优先重写 blog/checkin插件清单
R13bcrypt 兼容切换后无法登录低校验多前缀;生成 PHP 可验格式登录测试
R14config.php 解析脆弱启动失败/错路径中仅标量解析 + 明确报错 + config.json 覆盖doctor
R15modernc SQLite 行为差异锁/事务异常低固定版本;禁用 RETURNING 依赖;并发测试race/并发用例
R16缓存快照文件漏维护列表陈旧中纳入写路径失效清单内容对比

10. 待决策项(需评审确认)

ID决策点选项建议默认
D1遗留博客 3 表归属A. Go 博客插件接管;B. 只读保留;C. 上线前迁移到插件自有库A(保持数据可读,表名不变)
D2PHP 插件兼容范围A. 仅核心功能;B. 尽力兼容全部;C. 提供迁移工具A + C
D3市场/升级器是否进 v1A. 进;B. 延后到 v1.1B(先保证核心稳定)
D4页面缓存共存策略A. 立即共享;B. 独立目录,B10 后共享B
D5设置缓存写权A. Go 写;B. 仅 PHP 写,Go 只读A(否则 Go 改设置 PHP 不生效)
D6remember_token 轮换时机A. 共存期不轮换;B. 立即轮换A
D7会话 TTLA. 保持 1800;B. 改为更长A
D8时区来源A. config.php;B. config.json 覆盖A + B 可覆盖
D9写队列默认A. 默认直写;B. 默认单写者A,高并发再开
D10上传直出A. Nginx;B. GoA(生产),Go 仍支持下载鉴权
D11分词器实现A. 完全移植;B. 简化分词 + 重建索引A(否则索引漂移)
D12前台是否 JSON 化A. 保持 HTML;B. API 化A

11. 附录

附录 A:路由清单(来自 index.php)

前台(50)

GET  /                                   GET  /index.php
GET  /__rewrite_probe                    GET  /manifest.webmanifest
GET  /offline                            GET  /forum
GET  /forum/category/{id}                GET  /thread/{id}
POST /thread/{id}                        POST /thread/{id}/delete
GET  /thread/{id}/edit                   POST /thread/{id}/edit
GET  /post/new                           POST /post/new
GET  /post/{id}/edit                     POST /post/{id}/edit
GET  /attachment/{id}                    GET/POST /login
GET/POST /register                       POST /logout
GET/POST /forgot-password                GET/POST /reset-password/{token}
GET  /verify-email/{token}               GET/POST /resend-verification
GET/POST /notifications                  GET/POST /profile
GET  /message                            GET/POST /message/inbox
GET/POST /message/sent                   GET/POST /message/compose
GET  /message/view/{id}                  GET/POST /search
GET  /tags                               GET  /tag/{id}
GET/POST /theme-settings                 POST /layout-toggle
POST /toggle-sidebar                     GET  /u/{id}

后台(84,按模块)

模块路由数说明
仪表盘2/、/dismiss-key-warn
数据库7/database、engine、overview、settings
站点设置17basic/system/display/mail/search/queue/pwa + test-rewrite
用户2列表/编辑
主题2列表/编辑
版块2列表/编辑
标签2列表/编辑
回收站2列表/操作
等级2列表/编辑
审核2列表/操作
维护2列表/操作
用户组/权限4groups、permissions
审计日志1/logs/audit
主题外观6默认/保存 CSS/选项/上传/删除
插件7列表/编辑/启停/卸载/权限强制
升级15check/preflight/fetch/unlock/allow-unsigned/backup/apply/rollback/...
市场9sync/callback/install/activate/auth-start/auth-clear/selfcheck/prune

API(7)

POST /vote          POST /upload        POST /fetch-image
GET  /captcha       GET  /pow           GET  /favorites-count
GET  /post/{id}/body

附录 B:数据库对象清单

库表/对象关键约束
business.sqlite20 核心 + 3 遗留不删不改;只加可选列
main_index.sqlitetopic_index、reply_index索引同名同列序
bucket/activetopic、reply秒级时间;user_version=2
bucket/archive同上query_only=1
sessions.sqlitesessionsid 主键;expires 索引
global_id.sqliteid_generator种子 topic/reply
queue_0..2task_queue租约 + 重试
search_YYYYQnsearch_indexcreated_at 为 TEXT;唯一(token,type,target_id,weight)

附录 C:49 钩子分组(B0 落表)

分组代表钩子
初始化/路由init_after、route_register、route_before_dispatch
布局/渲染layout_head_end、controller_view_before、footer_links
导航/入口nav_plugin_links 等 nav_*
内容生命周期topic_created、post_created 等
上传/资源upload_url 等
用户/权限user_* 等
后台admin_* 等
搜索/其他search_* 等

完整 49 条表由 B0 从 fh_reference/flinthub1.5.0/docs/插件开发规范.md §9.1 复制生成 docs/COMPAT_HOOKS.md,并要求"钩子数/调用点数/双向差集"自检脚本通过。

附录 D:视图清单(64)

目录文件数说明
layouts/1main.php
_components/7侧栏、通知铃、附件卡、灯箱、备案页脚
auth/5登录/注册/找回/重置/提示
forum/2首页/版块
thread/2详情/编辑
post/2新建/编辑
message/4收件/已发/撰写/详情
notifications/1通知列表
profile/1个人中心
search/1搜索
tags/2列表/详情
theme/1主题设置
user/1用户主页
errors/2404/429
根1offline.php
admin/31后台页面与子目录(含 _sidebar、_market_action、database/、logs/)

附录 E:关键常量与 Cookie 对照

对象FH 名Go 处理
会话 CookiePHPSESSID原样
记住登录remember_token / remember_user_id共存期只校验
主题site_theme原样
语言flinthub_lang原样(zh/en/zh_tw)
上传UPLOAD_PATH=assets/uploads/原样
分页POSTS_PER_PAGE/THREADS_PER_PAGE=20原样,可配置
桶SPLITDB_BUCKET_SIZE=32原样,支持扩容
定时密钥CRON_KEY + X-Cron-Key原样

附录 F:术语

术语含义
主库business.sqlite
索引库main_index.sqlite
桶 / 分片季度目录下的 *.sqlite
外置正文extern/ 下的 t/r*.txt 与 .bin/.idx
真相源读取内容时唯一可信的数据来源
兼容夹具用于验证 FH 数据读写的只读样本与脚本
阶段门每阶段结束的验证报告评审
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 59 次 | 回复 1 次

全部回复 (1)

🌴Lv.5 高级 🌛 见习会员
2026-11-03 15:58:03
验收标准建议加一条:同一操作序列分别在 PHP 与 Go 下执行后做逐表 diff,尤其关注 SQLite 的 WAL、PHP session 文件和上传目录的命名规则;Nginx upstream 切换必须保证单写者,不能双活。
#1 楼

请 登录