Files
tamabox/outputs/TamaBox-v4-feature-control-fix-review-2026-08-31.md
T
2026-09-08 10:10:44 +08:00

145 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`:
```go
// 修复前(BUG)
Enabled bool `gorm:"default:true"`
// 修复后
Enabled bool // 不要加 default:true 标签(注释已标注原因)
```
**机制**:GORM 在 `Create` 时对带 `default` 标签的字段,若值为零值(`false`)会替换为 `DefaultValueInterface`(`true`)再写库。实测生成 SQL:
```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 定向调整:
```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 环境运行(本地沙箱无凭据,未执行);逻辑层测试已全部通过。