277 lines
21 KiB
Markdown
277 lines
21 KiB
Markdown
# 2026-10-18
|
||
|
||
## 运营侧内容治理:饭圈黑话拉踩/挂人问题(诊断 + 方向)
|
||
|
||
用户观察到部分箱主页面出现拉踩、黑话辱骂、挑动群体对立(样例 `/_/52mxtx1314`,85 条问答,
|
||
内容为「避雷/壁垒/挂人 + 圈内代称」,如咸菜坤/县城坤/猪肝/澄毒/忘羡姐/端水洁癖/蟹脚coser)。
|
||
|
||
### 已核实的事实(代码侧)
|
||
|
||
- `moderation.Check` 已覆盖提问入口(`route/question/page.go:441`)、回答、私聊、投票、追问;七牛只硬拦
|
||
politics/terrorism/porn,`review` 一律放行 → **通用审核对黑话零命中**,样例文本无一个传统脏字。
|
||
- 箱主自建屏蔽词上限:`conf.Limits.MaxBlockWords=10`、`MaxBlockWordLen=10`(`internal/conf/limits.go`),
|
||
对「一人一个代称」的场景容量不匹配。
|
||
- **全站无举报入口**:`grep Report` 无结果;处置能力只有箱主侧 Delete / SetPrivate / SetPublic。
|
||
- 现有成本控制:30 次/分钟限流、数学验证码、同 IP+同内容重复拦截(`shared.DuplicateGuard`)、
|
||
`harassment_setting` 的 register_only 档。`autoban` 只针对后台路径扫描,不涉及内容行为。
|
||
- `Question.FromIP` 已落库但**未被任何运营/聚合逻辑消费**。
|
||
- 单题页/提问箱页已全局 noindex(`internal/context/context.go:243` 的非公开页分支)→ SEO 不是主要传播面,
|
||
扩散主要靠截图外传。
|
||
|
||
### 关键判断
|
||
|
||
根因是**激励结构**而非审核强度:匿名零身份 + 提问进后台默认不公开 + 箱主逐条回答后公开(样例中连「?」这类
|
||
单字回答也算)+ 页面可分享。攻击者收益是「被朗读、被围观」,不是「发出去」。因此加词库/调最严档
|
||
投入产出极低且放大误伤。方向按三层:结构层降低收益(已回答≠自动公开、含第三方指涉的投稿默认私密)、
|
||
成本层用行为信号(来源配额、同 token 聚合)替代词库、处置层补举报-下架-按来源封禁并留痕。
|
||
|
||
本次仅给方向,未改代码。下一步待用户选定:出具体变更清单,或先落「举报 + 批量处置」最小闭环。
|
||
|
||
## 追加:箱主主动引导投稿(处罚口径讨论)
|
||
|
||
用户澄清:本例是**箱主引导用户投稿挂人**,不是被动受害;提议"禁用提问 N 天,多次累计直接封号"。
|
||
|
||
### 新核实的事实
|
||
|
||
- **单题分享卡是公开的一键外传通道**:`QuestionShareCard`(`route/question/share.go:792`)只要
|
||
`answer != "" && !IsPrivate && question.UserID == user.ID` 就渲染整张卡片(含箱主头像/背景/水印/二维码),
|
||
路由 `GET /_/{domain}/{questionID}/share-card` 在公开的 `Questioner` 分组内。挂人内容因此可以变成无痕图片流出站外。
|
||
- **首页/探索分发无任何信誉门槛**:`users.show_in_home` 由箱主自选,`GetShowInHome` / `GetByTag` /
|
||
`getRandomUsers` 只按 created_at 或随机排序,不查举报/处置记录。高互动挂人箱可直接获得平台曝光。
|
||
- **per-user 功能开关已存在**:`feature_controls`(user_id + feature + enabled + 时间窗 + quota),
|
||
`FeatureOpen` / `FeatureStateOf` 是唯一判断点 → 但它是**箱主自设**,`Save` 由本人调用,
|
||
若复用为处罚,箱主可自行 `enabled=true` 解除,语义也混淆。处罚需独立 "平台限制" 层,优先级高于用户设置。
|
||
- `User` 模型无任何限制/处罚字段;现有只有 Deactivate(自停用)、BanUserAndCleanup(物理删)、restore。
|
||
- 「公开可见 = 已回答」(`Questioner` 已挡私密/未回答),所以**「回答」这个动作同时是生产与公开**,处罚打这里最准。
|
||
|
||
### 立场(待用户确认)
|
||
|
||
- 赞同罚箱主:主动引导投稿属滥用产品组织围攻,不是内容治理能覆盖的。
|
||
- 反对罚「提问(收信)」:伤害来自「发布」,禁收信对已公开的 85 条无效,且给箱主"被平台打压"叙事。
|
||
正确梯子是从传播面往下收:停止分发 → 收窄公开 → 冻结发布(回答/公开)→ 停用页面 → 封禁。
|
||
- 反对「多次被禁自动封号」:本场景举报本身是攻击武器,自动级联等于给组织化举报装自动扳机。
|
||
必须"举报成立(带证据快照)+ 人工判定 + 可申诉 + 留痕"。
|
||
- 判据必须用行为指纹而非文本关键词:挂人内容占比、独立指涉对象数、处置后复发率。
|
||
- ⚠️ 本例箱主自述被"澄毒长期辱骂",可能是互殴一方 → 流程必须双向取证,否则单边处罚会激化对立。
|
||
|
||
## 追加:改为运营者人工裁定(用户倾向)
|
||
|
||
用户提出:被举报不自动封号,交由运营者本人判断。此方向认可,但补三条前提,否则会退化成"看心情处置":
|
||
|
||
1. 判定标准必须**先书面化**(什么算挂人:可识别第三方 + 贬损/号召规避/曝光主页 + 无公共利益),
|
||
否则同一人不同时间判出不同结果,比误封更伤公信力。
|
||
2. **证据包自动生成**(内容快照 + 来源 IP 聚合 + 该箱主历史处置 + 同 token 的关联条目),
|
||
人工只做裁决,不做取证的体力活。
|
||
3. 处置必须**留痕 + 可申诉 + 有冷静期**(举报后 24h 再判),这是平台履行"通知-删除"义务的自证材料;
|
||
自动封号反而更难自证。
|
||
|
||
分级矩阵(裁定 × 内容性质)作为处置口径:不成立→不处理(C 类转人工);成立首次→A 提示整改、
|
||
B L1 停止分发、C L1+L2;成立复发→A L1、B L2、C L3;恶意持续→A L2、B L3、C L4→L5。
|
||
|
||
互殴/选边风险:无法判定过错方时**对等处置**(双方同 L1),不选边;运营者本人有圈内立场,
|
||
封号类必须二次复核 + 统一模板公告,不写个人评价。L1 类建议静默生效(不单独通知),
|
||
避免暴露"平台在盯内容"后转为图片/缩写等更隐蔽表达。
|
||
|
||
运营侧还缺一个"处置台":举报列表、证据聚合、一键执行 L1–L5、留痕查询 —— 目前完全没有。
|
||
|
||
## 追加:用户明确要求「不要后台」,改为无 UI 实现
|
||
|
||
审阅后确认项目结构:`/user` 是箱主自助面板(`reqUserSignIn`),**无 admin 角色、无站方后台**;
|
||
CLI 骨架在 `cmd/tamabox.go`(`internal/cmd/*.go` 每个命令一个文件,范式见 `ban.go`:`conf.Init` → 拼 DSN →
|
||
`db.Init` → 操作 → 确认 → 执行)。告警可用 `conf.Security.SecurityAlertEmail`(`internal/conf/static.go:70`,
|
||
autoban 已在用)+ `mail.SendSecurityAlertMail`。
|
||
|
||
因此"处置台"改为三条非 UI 腿:
|
||
1. **举报采集**:单题页举报按钮 → `content_reports` 表;对箱主可见走 `/user/questions?filter=reported`
|
||
(这是他自己的收件箱,不是站方后台),举报的处置第一责任人仍是箱主。
|
||
2. **告警**:达到阈值(同一箱主 24h 内 N 条举报)发邮件到 `security_alert_email`,这是运营者唯一需要的"入口"。
|
||
3. **处置**:`TamaBox reports --domain` 出证据包、`TamaBox restrict --domain --level --days --reason`、
|
||
`TamaBox unrestrict`。状态写 `user_restrictions`(带 `expires_at`,判断时比较时间 → **过期自动失效,不需要 cron**)。
|
||
|
||
生效点(已定位):`QuestionShareCard`(share.go:808)、`GetShowInHome`/`GetByTag`/`getRandomUsers`
|
||
(users.go:620/643/749,注意 GetRandom 有 30s 缓存)、`FeatureOpen`(feature_control.go:364,可一处覆盖箱主自设)、
|
||
`PublishAnswer`/`SetPublic`/`AnswerFollowUp`、`question.Pager`(L4 整页停用)。
|
||
|
||
关键约束(已向用户说明):不能复用 `feature_controls`(箱主可自行 `enabled=true` 解除,语义混淆);
|
||
不用配置文件方案(要重启、无法表达到期时间);无后台 ⇒ 只能**事后处置**,不能事前审核,口径需写成
|
||
"发布后 24–48h 内可被处置";举报必须防刷(同 IP + 同 question 唯一),否则邮箱会被刷爆。
|
||
|
||
## 已落地(第 1–2 步 + L1 两个出口),已实测
|
||
|
||
备份:`C:/OtherFiles/Github/TamaBox-v4-mainline-before-abuse-restrict-20261018.tar.gz`(3.2M,排除 .git/.workbuddy)。
|
||
|
||
新增/改动:
|
||
- `internal/db/restriction.go`(新):`RestrictionLevel` L1–L4、`UserRestriction`、`ContentReport`、
|
||
`RestrictionsStore`(Create 替换语义 / RevokeActive / ActiveLevel / ActiveLevels / ListByUserID)。
|
||
两个模型都**刻意不嵌 dbutil.Model**:处置记录与举报都不软删(撤销走 `revoked_at`),
|
||
且带 `DeletedAt` 的唯一索引会留下"删了仍占槽位"的陷阱。
|
||
- `internal/cmd/restrict.go`(新):`reports` / `restrict` / `unrestrict`,复用 `fmtScan` 做 YES 确认。
|
||
- `internal/db/db.go`:`AllTables` 加两表,`db.Init` 装配 `Restrictions`。
|
||
- `tamabox.go` + `cmd/tamabox.go`:注册三个命令(并给根入口补上一直缺失的 `restore`)。
|
||
- `route/question/share.go`:`QuestionShareCard` 在任何级别处置下都拒绝生成分享图(fail-closed)。**L1 核心出口**。
|
||
- `internal/db/users.go`(CRLF,用 python 脚本改):`GetShowInHome` / `GetByTag` / `getRandomUsers`
|
||
加 `id NOT IN (activeRestrictionsSubQuery(...))`,把被处置箱主从首页/探索/标签/随机位摘除。
|
||
`GetRandom` 有 30s 缓存 → 随机位最多滞后 30s,其余即时。
|
||
- `internal/db/restriction_test.go`(新):覆盖替换 / 升级 / 到期失效 / 撤销幂等 / 曝光排除。**测试全绿**(3.87s)。
|
||
|
||
实测踩到的两个坑:
|
||
1. **只改 `cmd/tamabox.go` 命令不生效** —— 真正入口是根 `tamabox.go`,报 `No help topic for 'reports'`。
|
||
2. 证据包比例首版写成「举报次数 / 公开回答数」,实测出现 **166.7%**(5 次举报 / 3 条回答)。
|
||
已改为「被举报**条目数** / 公开回答数」,举报次数单独展示。
|
||
|
||
CLI 冒烟(临时 PG 库 + 临时 ini):reports 证据包 → restrict L1 → 证据包显示生效 → pending 列表 →
|
||
unrestrict → 显示"已撤销";升级 L1→L3 后旧记录 `revoked_at` 置位、新记录 L3 生效。清理已 DROP 临时库。
|
||
|
||
未做(下一步):L2/L3 发布侧拦截(`PublishAnswer`/`SetPublic`/`AnswerFollowUp` + `FeatureOpen` 覆盖箱主自设)、
|
||
L4 整页停用、举报按钮与箱主面板筛选、邮件告警。分享卡拦截未做 web 端到端验证(仅编译 + 单测覆盖 store 层)。
|
||
|
||
## 已落地(第二批:L2 / L3 / L4 发布侧),编译与单测通过,web e2e 受沙箱限制未做
|
||
|
||
- `internal/db/restriction.go`:加 `restrictionLevelRank` + `RestrictionAtLeast(level, target)`。
|
||
显式权重而非字符串比较(将来加 L10 不会静默排错)。空/未知级别恒 false,防止脏数据锁死箱主。
|
||
- `route/question/question.go`(CRLF,python 改):
|
||
- `PublishAnswer`:L3+ 拒绝(`question.errRestrictedPublish`);L2 **先 SetPrivate 再 AnswerByID**,
|
||
保证任何时刻都不出现「公开 + 已作答」的中间态(不是先答后藏)。
|
||
- `SetPublic`:L2+ 拒绝(`question.errRestrictedPublic`),否则等于箱主自行解除处置。
|
||
- `route/question/follow_up.go`:`AnswerFollowUp` L2+ 拒绝 —— 追问回答同样是公开内容且没有私密档可降。
|
||
- `route/question/page.go`:`Pager` 里 L4 → `Redirect("/restricted")`。此处 **fail-open**
|
||
(DB 出错放行):它不承担外传通道职责,fail-closed 会让所有箱子一起白屏。
|
||
- `route/user/restriction.go`(新)+ `templates/user/restricted.html`(新)+ `internal/route/route.go`
|
||
注册公开路由 `/restricted`(必须在 reqUserSignIn 之外)。
|
||
- i18n 4 语言各加 5 键(`question.errRestricted*` / `question.flash.answeredPrivate` / `restricted.*`),
|
||
`TestDictionaryComplete` 通过。i18n 与 route 文件均为 CRLF:用 python 插行 → `gofmt | 转 CRLF` 写回。
|
||
|
||
修正上一轮的判断:**`FeatureOpen` 不需要改**。L3 的语义是「冻结发布、仍可收信」,而 feature_controls 控制的是
|
||
收集(收信);关掉它反而违背 L3 定义。L4 已经由 Pager 整页挡住。
|
||
|
||
验证:`go build ./...` EXIT=0;`TestRestrictions` / `TestRestrictionLevelValidation` / `TestRestrictionAtLeast`
|
||
/ `TestDictionaryComplete` 全绿。**L4 与分享卡的 HTTP 行为未验证** —— 本沙箱无 redis,
|
||
`TamaBox web` 启动即 panic,详见 MEMORY.md 的构建与测试一节。
|
||
|
||
## 追加:部署自检脚本(用户要部署到测试服务器)
|
||
|
||
新增 `dev/devcheck-restrict.sh`:串起「基线 → 施加 L4(断言 302 + /restricted 200 + 文案)→ 降级 L1
|
||
(断言页面 200 + 分享卡 302)→ 撤销」,默认 dry-run,`RUN=1` 才执行,`trap EXIT` 兜底撤销。
|
||
只依赖 CLI 与 curl,不需要 psql。`bash -n` 通过,LF 行尾。
|
||
|
||
同时核实:`Dockerfile` 的构建命令是 `go build ... ./cmd/`(不是根目录),所以两个入口必须同步 ——
|
||
已写进 MEMORY.md。部署前必须先确认 `./NekoBox --help` 里有 `restrict`,否则是旧产物。
|
||
手册里另注明两项只能人工手测:L2(回答应保存为私密)、L3(回答应被拒绝)。
|
||
|
||
### 首次实跑反馈(用户 dry-run)
|
||
|
||
用户给 `DOMAIN=admin56` 跑 dry-run,结果 2 项 FAIL。日志证据:`SELECT * FROM "users" WHERE domain='admin56'`
|
||
→ **record not found**,说明该 domain 不存在(很可能把账号名当成了 domain)。所有 `/_/admin56` 的 302
|
||
都来自 `GetByDomain` 失败后的 `Redirect("/")`,与处置无关 → **L4 那项 PASS 是假阳性**。
|
||
同时 dry-run 不施加处置,前后状态没变,状态断言本来就无意义 —— 是脚本设计缺陷。
|
||
|
||
已修 `dev/devcheck-restrict.sh`:
|
||
1. 基线不是 200 直接中止并打印排查建议(302 常见原因 + 查真实 domain 的命令);
|
||
2. dry-run 在基线后短路,不再做状态断言(只验命令存在、基线、停用页);
|
||
3. 新增 1b「停用页是否部署」+ 文案断言,dry-run 也能验 i18n/模板。
|
||
|
||
教训(已与既有坑同类):**302 / 200 都不能单独证明处置生效**,必须先确立「基线 200」这个前提。
|
||
|
||
### 二次实跑:7/7 全绿(真实环境)
|
||
|
||
`DOMAIN=admin6`:基线 200 → L4 302 → L1 200 + 分享卡 302(qid=73)→ 撤销后 200。
|
||
证明 L1 分享卡拦截、L4 整页停用、撤销、以及 L4→L1 的**替换语义**在真实库上都按预期工作。
|
||
|
||
**随后发现并修掉一个顺序缺陷(L2)**:原实现把「强制私密」放在 `moderation.Check` 之前,
|
||
一旦审核失败(内容违规或审核源不可用,fail-closed),题目已被改成私密——箱主一次失败的编辑
|
||
会让原有公开内容凭空消失。现已移到审核通过之后、`AnswerByID` 之前,仍满足
|
||
「任何时刻不出现公开+已作答」(此刻它是未作答的私密题)。编译 EXIT=0、gofmt 干净。
|
||
⚠️ 该修复需重新部署才可在测试站手测 L2/L3。
|
||
|
||
L2/L3 的手测断言(比看 flash 可靠):
|
||
- L2 后 `select is_private, answer from questions where id=<qid>` → `t` 且 answer 非空,公开页不显示;
|
||
- L3 后 `answer` 仍为空串(未被写入),`is_read` 不变。
|
||
- 干扰项:回答会走七牛审核,审核不可用会 fail-closed 报「审核服务不可用」,那不是处置问题。
|
||
|
||
### L2 线上实测:功能生效,但「箱主无感知」被误判成没效果
|
||
|
||
用户报「L2 没效果」。我用匿名视角实测(这是我能独立做的部分):
|
||
- 公开页 `/_/admin6` 只出现 73,**不含 88**;
|
||
- 匿名访问 `/_/admin6/88` → **302 到首页**;对比 `/_/admin6/73` → 200。
|
||
|
||
结论:L2 私下里是生效的。用户误判的两个原因:
|
||
1. 他**以箱主身份**看单题页 —— 箱主本来就能看自己的私密内容,页面当然「一切正常」;
|
||
2. **我的 bug**:`question.flash.answeredPrivate` 只写进了 4 个 i18n 字典,**代码里从未使用**,
|
||
成功后仍提示「回答发布成功」。已修:L2 分支改用 `forcedPrivate` 布尔切换 flash 文案。
|
||
|
||
教训:这些 i18n 键写完必须 grep 调用点;「加了文案」不等于「接上了逻辑」。
|
||
|
||
### 用测试账号自测(已跑通登录态全链路,不需要用户点击)
|
||
|
||
用户提供测试箱主 `admin6`(密码不落盘)。用 python + urllib 跑通:
|
||
`GET /login` 取 `_csrf` → `GET /api/captcha/generate` 取 `{id, question}`(形如 `5 × 5 =`,
|
||
注意乘号是 `×` 不是 `*`)→ 算答案后提交 `g-recaptcha-response="<id>:<答案>"` → 登录成功;
|
||
再 `GET /_/admin6` 取表单 csrf → `POST /_/admin6`(字段 `content`)发提问 →
|
||
`POST /_/admin6/{id}/answer`(字段 `answer`)。
|
||
|
||
实测结论(qid=89):
|
||
- **L2 服务端生效**:回答保存成功,但该题变私密 —— 匿名访问 302 到首页,公开页只有 73;
|
||
- **flash 仍是旧文案**「回答发布成功」→ 服务器跑的是 21:22 部署的二进制,我 21:45 的 flash 修复未部署。
|
||
- 88(用户手测)与 89(我自检)都是测试数据,均已被 L2 强制转私密,测完需在后台清理或保持私密。
|
||
|
||
### L3 验证(重新部署 + 切 L3 后)
|
||
|
||
qid=90:发提问成功(L3 允许收信 ✔);`POST /answer` **没有重定向**、URL 停在 `/answer`、
|
||
重新加载单题页仍有回答表单 → **发布确实被拒绝、内容未写入**。
|
||
但响应里**没有任何提示文案**(`uk-alert` 只出现在 CSS 里)→ 又一个 bug:
|
||
|
||
⚠️ **同请求渲染不能用 `SetErrorFlash`**。`ctx.Success("question/item")` 是直接出页面(没有 302),
|
||
而 `SetErrorFlash` 写的是 session、要等下一次请求才读 → 提示静默消失,箱主看到的是「点了没反应」。
|
||
已把 3 处(PublishAnswer L3 / SetPublic L2 / AnswerFollowUp L2)改成 `ctx.SetError(...)`,
|
||
并顺手修了 `follow_up.go` 里同类的 moderation 失败分支(既有缺陷)。编译 EXIT=0。
|
||
**该修复需再部署一次**;部署后当前仍是 L3,可直接验提示是否出现。
|
||
|
||
### 重新部署后 L3 提示验证:通过
|
||
|
||
qid=90 再答一次:`ALERT: 当前处于平台处置期,已暂停发布回答` 已渲染;仍无重定向、回答表单仍在
|
||
(未写入)。L3 全链路闭环。待办:用户要求后续做「`SetErrorFlash + Success` 全量清理」专项。
|
||
|
||
剩余未验:L2 的「已保存为私密」提示(需把 admin6 切到 L2 后自测),以及举报采集链(未开始)。
|
||
|
||
### L2 提示验证:通过(qid=90)
|
||
|
||
```
|
||
post url: /_/admin6/90 (正常 302 回题目页)
|
||
ALERT: 已保存为私密:当前处于平台处置期,回答不会公开
|
||
匿名访问 90 -> 首页;公开页 ids: [73]
|
||
```
|
||
L1/L2/L3/L4 至此全部有真实环境实测证据:
|
||
- L1:页面 200 + 分享卡 302(qid=73)
|
||
- L2:回答成功 + 转私密 + 匿名不可见 + 专用提示
|
||
- L3:回答被拒(无重定向、表单仍在、未写入)+ 拒绝提示
|
||
- L4:整页 302 → /restricted 200 + 四语言文案
|
||
|
||
测试数据:88(用户手测)、89(L2 自检)、90(L3/L2 自检,现已答且私密),均需在测试站清理。
|
||
admin6 当前处于 L2(到期次日),必要时 `unrestrict` 解除。
|
||
|
||
## 举报采集链(已落地,编译 + 单测 + 模板检查通过,未部署验证)
|
||
|
||
新增/改动:
|
||
- `internal/db/report.go`(新):`ReportsStore` —— `Create`(唯一索引冲突静默为幂等成功)、
|
||
`QuestionIDsByUser`(箱主列表标标,排除已删除提问)、`CountByUserSince`(告警阈值)。
|
||
理由枚举 `harassment/doxxing/divisive/other` + `IsValidReportReason`。
|
||
- `internal/form/question.go`:`ReportQuestion{Reason}`(`valid:"required;maxlen:32"`)。
|
||
- `route/question/report.go`(新):`Report` handler。**不要求登录**(目击者是匿名访客);
|
||
理由白名单;同 IP 同题幂等且提示与首次一致(避免变成内容存在性探测接口);只写库不改可见性。
|
||
- `route/question/abuse_notify.go`(新):阈值告警 24h≥8 或 7d≥20,24h 冷却(进程内 map + mutex),
|
||
复用 `conf.Security.SecurityAlertEmail`,`shared.SafeGo` 异步发送。**只提醒不自动处置**。
|
||
- `internal/mail/mail.go` + `templates/mail/abuse-alert.html`:`SendAbuseAlertMail`。
|
||
邮件只给计数与下一步命令,**不贴举报内容原文**(邮件会被转发归档,等于又复制一份挂人内容)。
|
||
- `internal/route/route.go`:`POST /_/{domain}/{questionID}/report`,`rateLimitMiddleware(10, 1*time.Minute)`。
|
||
- `templates/question/item.html`:回答下方 `⚑ 举报这条内容`,`<details>` 折叠 + 固定理由下拉,
|
||
仅 `{{ if not .CanDelete }}`(非箱主)可见。
|
||
- `templates/user/question-list.html` + `route/user/question.go`:箱主列表加「被举报 N 次」红色 badge,
|
||
**只给数量不给举报人 IP**。
|
||
- i18n 4 语言共 11 个新键(report.* / qlist.reported*)。
|
||
|
||
验证:`go build ./...` EXIT=0;`TestReports` PASS(首次写入 / 同 IP 幂等 / 异 IP 计数 /
|
||
时间窗口 / 驳回后不计入);`TestDictionaryComplete` PASS;`go run ./dev/checktpl` → all templates syntax OK
|
||
(项目自带模板语法检查工具,改模板后必跑)。
|
||
|
||
未做:箱主列表的「只看被举报」筛选(目前只有 badge);举报驳回/成立的处置动作(目前 status 只有 pending)。
|