Files
tamabox/outputs/TamaBox-v4-feature-control-fix-review-2026-08-31.md
T
tamakyi e3e59e7c69 chore: baseline snapshot before moderation-strictness feature
Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
2026-09-10 10:56:27 +08:00

11 KiB
Raw Blame History

TamaBox-v4 功能管理修复 + 代码审阅报告

  • 日期:2026-08-31(更新于同日 16:00,追加 §三 遗留项修复)
  • 范围:4 个已报告问题的修复 + 全链路回归审阅 + 审阅发现项二次修复
  • 状态:✅ 4 个问题已修复;✅ 审阅发现的 P1-2 / P2-1 已一并修复;仅剩已知边界 P2-2(时区约定,非 bug)

一、修复总览

# 用户报告的问题 根因 修复 验证
1 🔴「开启该功能」取消勾选保存后自动恢复勾选 Enabled bool 带 gorm:"default:true",GORM 在 Create 时把零值 false 替换为默认值 true 写入(INSERT 值与 ON CONFLICT 的 excluded.enabled 都是 true) 移除该标签,GORM 原样写入显式值 生成的 SQL 已确认写入 false;单元测试通过
2 🟠 勾选「仅允许注册用户向我提问」后,二维码下仍显示「谁都可以以匿名的形式提问」 templates/question/list.html:165 与 question-not-found.html:86 硬编码匿名文案,与 HarassmentSetting 无关 文案随设置联动:新增 ask.registerOnly 词条,Pager 注入 RegisterOnlyAsk,模板二选一 新增 3 个模板渲染测试
3 🟠 功能关闭时只显示「提问功能尚未开放」,缺少上次关闭/下次开放时间 关闭提示仅有 FeatureClosedMessage 一行 FeatureState 新增 NextOpenAt / LastCloseAt,关闭提示下追加两行时间信息 新增 2 个渲染测试 + 逻辑单元测试
4 🟡 希望支持每周几 / 每天几点到几点循环开放 原模型只有一次性 OpenAt/CloseAt 新增 ScheduleDays(周几列表)+ ScheduleStart/End(每日时段),Evaluate 综合判定 新增 20+ 个单元测试用例

验证结果:internal/db、internal/i18n、internal/template、templates、internal/shared、internal/security/censor 全部测试通过;go vet ./... 无新增告警(仅 Windows 沙箱 webp CGO 缺库的已知噪音);模板解析/跨引用/渲染测试全绿。


二、修复细节

1. 问题 1 根因(P0,已修复)

internal/db/feature_control.go:

// 修复前(BUG)
Enabled bool `gorm:"default:true"`

// 修复后
Enabled bool  // 不要加 default:true 标签(注释已标注原因)

机制:GORM 在 Create 时对带 default 标签的字段,若值为零值(false)会替换为 DefaultValueInterface(true)再写库。实测生成 SQL:

-- 修复前:Enabled=false 传入 → VALUES 里写入 true
INSERT INTO feature_controls (..., enabled, ...) VALUES (..., true, ...)
  ON CONFLICT (...) DO UPDATE SET enabled = excluded.enabled  -- 也是 true

-- 修复后:Enabled=false 传入 → VALUES 里写入 false ✓
INSERT INTO feature_controls (..., enabled, ...) VALUES (..., false, ...)

连带修复:backfillFeatureControls 为私聊(默认关闭)补行时同样会把 false 写成 true——即历史上所有私聊默认关闭的用户可能被静默开启。同一处修复同时解决。

2. 问题 2(P1,已修复)

  • route/question/page.go:Pager 注入 ctx.Data["RegisterOnlyAsk"] = pageUser.HarassmentSetting == register_only。
  • templates/question/list.html / question-not-found.html:二维码下文案改为 {{ if .RegisterOnlyAsk }}仅允许注册用户向TA提问{{ else }}谁都可以以匿名的形式提问{{ end }}。
  • 新增 i18n 词条 ask.registerOnly(4 种语言)。

3. 问题 3(P1,已修复)

  • FeatureState 新增 NextOpenAt / LastCloseAt(由 Evaluate 计算)。
  • Pager 格式化为 AskNextOpenAtStr / AskLastCloseAtStr。
  • new-question-template.html 在关闭提示下方追加:
    • 下次开放时间:2026-09-02 10:00(未开始 / 循环定时未来再开时)
    • 上次关闭时间:2026-08-31 18:00(曾经开放过时)
  • 手动关闭 / 定量上限两类与时间无关的状态不展示时间行。

4. 问题 4(P1,新功能)

模型(feature_controls 新增 3 列,AutoMigrate 自动加列):

列 语义
schedule_days 逗号分隔周几,0=周日…6=周六(与 time.Weekday 一致);空 = 每天都开放
schedule_start / schedule_end 每日时段 HH:MM;都为空 = 开放日全天

语义:

  • 只填开放日 → 该天全天开放(每周循环);只填时间 → 每天该时段开放;都填 → 按周循环的每日时段开放。
  • 与一次性 OpenAt/CloseAt 叠加(AND):一次性窗口是硬性外边界,循环定时仅在其内生效。
  • Evaluate 判定优先级:手动关闭 > 未到一次性开放 > 已过一次性关闭 > 不在循环窗口 > 定量上限。
  • 「下次开放时间」按日向前扫描 8 天(覆盖全部周几组合),与一次性窗口取交集。

UI(feature-control-form.html):新增「循环定时(每周循环)」区块——7 个开放日胶囊复选框 + 每日开始/结束 time 输入;「只填开放日=全天 / 只填时间=每天」提示写入 hint。

校验(route/user/features.go):开始/结束必须成对填写;HH:MM 合法;结束须晚于开始;开放日仅接受 0-6 并去重排序存储。


三、审阅发现的其他潜在问题

🟠 P1-1 一次性时间解析的时区偏移(已修复)

  • 位置:route/user/vote.go parseTimeInput。
  • 问题:time.Parse("2006-01-02T15:04", s) 解析结果落在 UTC,而 Evaluate 用 time.Now()(服务器本地时区)比较——服务器非 UTC 时,「定时开放/关闭」与「投票起止」会整体偏移(如东八区偏移 8 小时)。
  • 修复:改用 time.ParseInLocation(..., time.Local),与 time.Now() 比较基准一致。
  • 注意:修复前已保存的时间值按 UTC 解释,部署后若服务器本地时区非 UTC,历史设定会有一致性位移(UTC 服务器无感知)。

🟠 P1-2 历史数据:旧版 backfill 可能已把私聊错误开启(✅ 已修复)

  • 位置:internal/db/feature_control.go 新增 reconcileLegacyChatEnabled(db.Init 中在 backfill 之后调用)。
  • 问题:旧版本(Enabled gorm:"default:true")下,为 allow_chat=false 的用户补私聊行时,false 被写成 true,私聊被被动开启;且 UpdateChatSettings 会反向把 users.allow_chat 同步为 true,形成持久化。
  • 修复:启动期幂等纠偏,同时命中以下条件才复位(绝不误伤箱主配置过的行):
    • feature='chat' 且 enabled=true;
    • 对应 users.allow_chat=false(箱主从未在面板开启私聊——开启会同步 allow_chat=true);
    • 该行无任何自定义配置(一次性时间为空、quota_limit=0、循环定时为空);
    • updated_at <= created_at(从未被面板保存过)。
    • 命中即复位 feature_controls.enabled=false 并同步 users.allow_chat=false;每次启动只处理未修复行,直到清零,失败仅告警不阻断启动。
  • 说明:对于「allow_chat 已被旧同步逻辑改写为 true」的更深度历史污染,无法与箱主主动开启区分,不在自动纠偏范围内;如需处理请人工核对后按下方 SQL 定向调整:
    -- PostgreSQL(仅当确认某账号私聊被错误开启时,替换 <user_id>)
    UPDATE feature_controls SET enabled = false WHERE feature = 'chat' AND user_id = <user_id>;
    UPDATE users SET allow_chat = false WHERE id = <user_id>;
    

🟡 P2-1 Vote.RequireCaptcha 同类 GORM 陷阱(✅ 已修复)

  • 位置:internal/db/votes.go RequireCaptcha bool(原带 gorm:"default:true")。
  • 问题:与问题 1 同源——若底层 Votes.Create 未来允许 RequireCaptcha=false,GORM 会把 false 替换为默认 true。
  • 修复:去掉 default:true 标签。业务层 route/user/vote.go 仍硬编码 requireCaptcha := true(决策 #3),线上行为不变;已同步更新 votes_test.go 的 testVotesCreate 断言(Create 未显式传参时按 false 落库,注释说明业务层强制 true)。实测生成的 SQL:显式 true 写入 true、未传写入 false。

🟡 P2-2 循环定时的时区与跨日限制(已知边界,建议知晓)

  • 时段按服务器本地时区解释(与一次性时间一致)。若服务器与箱主时区不同,每日时段会整体偏移;如需支持,可后续增加箱主时区字段。
  • 不支持跨午夜的时段(如 22:00-02:00,结束必须晚于开始),校验会拦截并给出中文提示。
  • 「下次开放时间」扫描窗口为 8 天,覆盖任意周几组合;一次性窗口 >8 天的场景由一次性分支单独处理,不受影响。

四、回归检查(原问题是否复现 / 影响面)

检查项 结果
问题 1(开关复位):Save 生成的 SQL 正确写入 false,ON CONFLICT 更新列含 enabled ✅
问题 2(文案):匿名/注册专属两种状态的渲染测试 ✅ 3 个用例通过
问题 3(关闭信息):未开始 / 已结束(永久)/ 循环定时三种关闭形态的模板渲染 ✅ 2 个用例通过
问题 4(循环定时):14 组 Evaluate 判定 + 8 组 NextOpenAt/LastCloseAt 边界 ✅ 全部通过
P1-2 纠偏:SELECT(带 fc. 别名)/ UPDATE(无前缀)生成的 PostgreSQL SQL 均正确;软删除行已过滤 ✅ SQL 生成验证
P2-1 Vote:Create(RequireCaptcha=true) 写入 true、未传写入 false(不再被默认替换) ✅ SQL 生成验证 + 断言更新
模板:新增 hasDay 函数与 printf 组合无解析错误,全部模板解析/跨引用测试通过 ✅
i18n:新增 12 个词条 × 4 语言齐全,无缺失(缺失会回退 zh-CN,但已全部补齐) ✅
代码质量:gofmt 干净、go vet ./... 无新增告警 ✅
存量行为:未改 CanAsk 计算、未改 FeatureOpen 语义、投票业务流(硬编码 requireCaptcha=true)不变 ✅

部署提醒:

  1. feature_controls 新增 3 列由 AutoMigrate 自动加列,无手工迁移;上线后在功能管理页重新保存一次各功能即可看到循环定时区块。
  2. 私聊纠偏为启动期自动执行:首次启动时若命中旧数据会输出 Warn "私聊开关纠偏:旧版数据把未开启私聊的账号置为开启,已复位为关闭"(附数量),属预期行为;未命中则无日志。
  3. TestVotes 等 DB 集成测试需在配置了 DB_TYPE 的 PostgreSQL/MySQL 环境运行(本地沙箱无凭据,未执行);逻辑层测试已全部通过。