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