145 lines
11 KiB
Markdown
145 lines
11 KiB
Markdown
# 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 环境运行(本地沙箱无凭据,未执行);逻辑层测试已全部通过。
|