tamakyi
0226956feb
fix(stats): 回答耗时改用 answered_at,修测试环境实测出的三处问题
...
在测试环境(box-dev-api)上逐项验算后发现「回答耗时」这一口径是错的。
1. 回答耗时不能用 updated_at - created_at(P0)
GORM 的 Update 会自动刷新 updated_at,而已读、置顶、公开/私密切换全走 Update。
实测:一条 10 天前的提问被 SetRead 标记已读后,updated_at 被推到当下(delta=240h)。
于是「回答耗时」实际测的是「提问到最后一次被碰」:线上那 6 条「超过 7 天」
和偏低的 24 小时回复占比都是这么来的。
修法:新增 questions.answered_at(首次回答时间,可空),只在 answer 由空变非空时
写一次,改写回答不重置;聚合改用 COALESCE(answered_at, updated_at),
首次加列时用 updated_at 冻结历史一次(Migrator().HasColumn 判断是否刚加列),
否则历史值会随着后续的已读/置顶继续变大。
2. 环比句式读不通
中文「上月提问量比前一个月 -69.6%」、英文「...: -69.6% questions.」都是病句。
改为「上月提问量环比 %s%%」/「Month-over-month change for last month: %s%%」。
3. 脚注说「当日 0 点的快照」不实
实际是当天第一次访问时算、服务重启也会重算(实测算出时刻 01:11:50,
基准日却写 2026-10-24,说明它含了 0 点之后的数据)。改成准确描述。
4. 数据库故障时请求会排成 N×30s
ovRefresh 串行化 + 30 秒超时意味着数据库挂起时第 N 个请求要等 N×30 秒。
加 30 秒失败冷却:冷却期内直接降级(有旧快照用旧快照),不再碰数据库。
todayNumbers 失败也盖时间戳,不再每个请求都打两条 COUNT。
测试:新增 internal/db/answered_at_test.go(5 例:只记一次、私密路径也记、
分桶以 answered_at 为准、历史行回退、回填),并做过变异验证 —— 把口径改回
updated_at 后测试立即失败。internal/stats 补 3 例冷却测试。全量构建与测试通过
(internal/storage 的构建失败是 overlay 桩的既有问题)。
2026-10-24 01:45:34 +08:00
tamakyi
0c282914c2
fix(stats): 今日计数不再被每日缓存冻结,日期分组不依赖库会话时区
...
自查部署结果时发现两处正确性问题,都是本页上线时引入的:
1. 「今日提问 / 今日回答」原本随整页聚合一整天缓存,页面却写着「实时」,
实际当天第一次访问后数字就冻住到次日。现拆出 TodayCounts 单独取,
60 秒 TTL、跨日按日期失效;命中整页缓存时返回副本,不原地改缓存指针
(那会被并发读踩成数据竞争)。
2. 日/月/时分组用 to_char/EXTRACT 直接作用在 timestamptz 上,结果跟着数据库
会话时区走,而 PG 官方镜像默认 UTC、Go 侧按容器时区(Asia/Shanghai)切今天。
实测同一条 2026-10-24 01:30+08 的提问,UTC 会话下被算成 10-23 17 点 ——
日报表整体偏 8 小时、热力图时段错位,且与「今日」对不上。
现统一经 analyticsLocalize() 加 AT TIME ZONE INTERVAL 换算,偏移取自
time.Time.Zone()(Go 未设 TZ 时给 "Local",PG 不认该名,故不用 IANA 名)。
过滤条件仍是裸列,索引照用。
另外两处顺手修:
- PeakDay 用 >= 比较,并列时会漂到最近的同值日期,与 busiestSlot 的取值规则不一致。
- 聚合改用 context.WithoutCancel + 30s 超时:第一个访客在聚合途中关掉页面时,
排在后面等锁的请求不会各自重跑一遍全表 GROUP BY。
测试:新增 TestTodayNumbersNotFrozen(缓存命中后今日数仍更新、整页聚合当天只算一次)
与 TestTodayCacheExpiresAcrossMidnight;analytics 连真库通过。
2026-10-24 01:20:02 +08:00
tamakyi
a5441c99fd
feat(stats): 新增公开数据页 /stats,入口收敛到页脚
...
首页底部那张「站点统计」卡片下线,完整数据移到公开只读页 /stats:
今日实时数、概览六格、近 30 天折线与 12 个月柱状、三个占比环、
7×24 时段热力图、提问字数与回答耗时分布。
- 聚合在 internal/db/analytics.go(SUM(CASE WHEN),方言差异收敛在 analyticsDialectFor)
- 结果按自然日缓存,跨日重算一次后全站共用;重算失败降级返回上一份快照
- 图表零前端依赖:服务端算坐标的 SVG + CSS grid,禁用 JS 页面仍完整
- 环比取「上月 vs 上上月」:本月未过完,否则每月 1 号会得出 -95%
- 入口只在页脚「社区规则」与「找站长」之间;route.Home 不再计算统计
- 补 questions(created_at) 索引;启动后异步预热当天快照
- 测试:analytics 连真库验证软删除不计入、分桶下标、DOW 对齐
2026-10-24 00:57:08 +08:00
tamakyi
818fa74160
fix(honeypot): 取证记精确地址、封禁用 /64 键,并补建封与拦截日志
...
线上实测:IPv6 访问蜜罐后收到「你的 IP 已被封禁」,邮件里却是
`2408:…::/64` —— 既不像自己的地址,也说不清封了多大范围;同时用户自己
仍能正常访问,看日志也无从判断到底有没有封。
根因是「取证地址」与「封禁键」混用同一个变量(都取 LimitKey 的折叠值):
- 取证丢掉精确 IPv6 后 64 位,事后无法定位到具体主机、没法和其他日志对账;
- 邮件只给聚合键,运维第一反应是「不是我」,排查方向直接被带偏。
改动:
- Serve 拆 clientIP(ResolveClientIP,精确)与 banKey(LimitKey,/64):
DB / 凭据文件 / 告警邮件用前者,封禁 / 探测采样 / 告警去重用后者;
- autoban.Options 新增 OnBan 回调(保持纯标准库、不引入 logrus),
route.newBanner 注入后每次建封打 WARN:ban_key / reason / duration / expires_at;
中间件 403 分支加 Debug 日志(被封者会持续请求,Info 会被刷屏);
- 告警邮件新增「封禁范围」行,仅 ip != ban_key 时渲染(IPv4 不重复显示),
顺带把过时的「已入库」改为「写入取证文件」——凭据早就外置了;
- honeypot stats 的来源排行改按 LimitKey 合并,否则 IPv6 轮换地址会把
排行碎成几十条各 1 次的记录;合并口径与封禁完全一致。
测试(每项都做了变异验证:改坏→确认变红→恢复):
- TestHoneypotRecordsExactIPv6ButBansByPrefix:精确地址进 DB 与凭据文件、
/64 才是封禁键、同 /64 换地址仍 403;
- TestOnBanCallback:建封回调带键/时长/原因,封禁期内重复命中不重复回调;
- TestHoneypotAlertTemplateRenders:IPv6 渲染封禁范围、IPv4 不重复;
- TestMergeSourcesByKey:同 /64 合并、IPv4 原样、limit 与空输入。
2026-10-23 10:53:03 +08:00
tamakyi
b939d26ff0
fix templates v2
2026-10-22 17:49:25 +08:00
tamakyi
bd02eb7ed2
fix templates v1
2026-10-22 17:04:10 +08:00
tamakyi
c29e1d071f
fix(honeypot): 补齐封禁装配与诱捕语义,凭据外置为文件不再入库
...
一、封禁链路整条没接上(P0,已实测复现)
route.New 的 autoban.New 漏传 HoneypotEnabled/Threshold/Duration,而
RecordHoneypot 首行就是 `if !b.honeypotEnabled { return false }`
→ 蜜罐命中从不封禁、落库 banned 恒 false、IsBanned 永不为真。
同一批改动又把中间件里蜜罐路径的常规计数也跳过了,净效果是蜜罐路径
从「10 次封禁」退化为零计数零封禁 —— 是 regression 不是修复。
处理:抽 newBanner() 作为唯一装配入口并补齐参数;新增 banner_config_test
四个用例(全部经由 newBanner,不再自建 Banner)。变异验证:分别把
HoneypotEnabled 置 false、HoneypotThreshold 置 0,对应用例按预期失败。
二、诱捕语义(审计时漏掉,比上面更影响功能价值)
原实现对**所有方法**计数且阈值 1 → 扫描器 GET 一次假登录页就吃 403,
永远走不到「POST 提交账号密码」那一步,蜜罐的核心价值(拿到字典)归零。
处理:提交凭据 → RecordHoneypot(不受 enable_auto_ban 约束,立即封);
纯探测 → 常规 Record(阈值 10)。新增 TestHoneypotProbeDoesNotBanImmediately,
变异验证:把 GET 也塞进 RecordHoneypot,测试按预期失败。
三、落库边界(实测取证,非推断)
- Path 原样落库而列宽 512 → PG `SQLSTATE 22001 值太长了(512)`,整条证据丢失;
实测 600 字符失败、500 字符成功;
- 三处按字节截断 → `SQLSTATE 22021 无效的 "UTF8" 编码字节顺序`(中文密码/URL
可稳定触发)。统一改 rune 截断并按列宽逐项对齐。
四、资源与噪音
- alerted 无界 map → 带 TTL 的 deduper(上限 2 万,过期清理 + 超限硬裁),
同时承担告警 24h 冷却与纯探测落库采样(每 IP 每分钟一条)——
GET 洪泛不再变成等量 INSERT;
- credentialKeys 去掉 id/name,普通 JSON 探测不再被误判为凭据提交并触发假告警;
- canary 从 meta generator 移到 HTML 注释,generator 伪装成 WordPress,
不再同时向扫描器自曝「这是蜜罐」和「这站是 TamaBox」;
- serveLoginFailure 复用 WriteLoginPage,不再手抄响应头(已开始漂移);
- Describer 死代码 → Prefixes(),接到新命令 honeypot paths;
- honeypot_paths 解析挪到 honeypot.SplitPaths(CLI 与 route 共用),
补前导斜杠、拒绝 "/"(会让所有 404 变假登录页)。
五、凭据外置:数据库不再存用户名/密码
明文账号密码一旦入库就会跟着进备份、主从复制与任何能连库的报表工具,
事后无法彻底清除。改为:
- 新增 honeypot.CredRecord/CredStore:按天文件 creds-YYYY-MM-DD.jsonl,
目录 0700、文件 0600、坏行跳过、PurgeBefore 只删整天过期的文件;
- db.HoneypotHit 删掉 Username/Password/Fields/RawBody,只留 HadFields 元数据;
- HoneypotStore 删掉 TopCredentials/ListSinceByIP(CLI 改读文件后零调用);
- 新增 LegacyCredentialColumns/PurgeLegacyCredentialColumns:AutoMigrate 不删
既有列,老库升级后历史明文仍在 → 启动 WARN + `honeypot purge-creds` 显式 DROP;
- 清理器 HoneypotRetention 增加 CredPurgeable 参数,与 DB 记录共用保留期;
honeypot prune 也一并删过期凭据文件;
- CLI:hits 不再连库(DB 挂了也能查),stats 的凭据排行读文件;
- 配置 honeypot_cred_dir(缺省 data/honeypot-creds),.gitignore 加 /data/。
验证:go build ./... / go vet ./internal/... / go test ./...(含 PostgreSQL)全绿;
新增 credstore_test(追加/倒序/IP 过滤/坏行/Top/Purge/权限)、
TestHoneypotLegacyCredentialColumns(ADD COLUMN 模拟老库 → 检测 → DROP)、
TestHoneypotWritesCredentialsToFile;CLI 端到端手动验证 hits/--ip/--show-password。
2026-10-21 15:43:25 +08:00
tamakyi
d300167a30
fix(conf): 配置键名容错 + 未知键告警 + 蜜罐保留期缺省值修正
...
现场故障:配置写 `honeypot_retention_days <= 30`,日志却报
「蜜罐自动清理未启用:honeypot_retention_days <= 0」。
根因(实测 ini v1.67.0 行为确认):
1. 键名多一个 t(retention vs retain)→ ini 对未知键**静默忽略**,保留期成 0;
2. `<= 30` 这种写法让 ini 把键名解析成 "honeypot_retention_days <"
(带尾随比较符),更加匹配不上;
3. 警告文案里写的也是错键名,等于在误导运维往错的键上配。
修复(三层):
- 键名归一化(internal/conf/keynorm.go):修剪首尾空白、剥掉尾随 `<` `>`
`=` 等比较符残留,并按别名表纠正常见误拼。归一化结果必须命中该段
合法键名白名单才会被采用 —— 不认识的键不猜,交给未知键告警。
- 未知键告警(internal/conf/unknown.go):启动时逐段比对键名,无法识别的
键 WARN 出来并给出最接近的合法键名建议。合法键名由反射从结构体 ini tag
得出(单一事实来源,新增配置项自动纳入,不会脱节)。
- 缺省值语义修正(internal/conf/honeypot_defaults.go):
* 保留期缺省 30 天(原先零值 0 = 永不清理);显式 <= 0 也回退 30。
要关闭自动清理请用 honeypot_auto_clean = false。
* honeypot_auto_clean 缺省 true。原先靠 bool 零值,ini 的 bool 解析是
「有键才赋值」,未配置与显式关闭无法区分 → 老配置升级上来自动清理
实际是关着的。现改用 HasKey 判定。
* honeypot_max_rows 缺省 200000;显式 0 = 不限(会告警提醒只增不减)。
* honeypot_threshold 缺省 1。
实测 ini 行为(探针程序,已删):
- 键缺失或值非法时 MapTo **不覆盖**预置默认值 → 可安全预置缺省值;
- 键名大小写敏感(Honeypot_Auto_Clean 不生效);
- HasKey 能区分「缺失」与「显式 0」;
- 段不存在时 MapTo 返回 nil 且不修改目标。
顺带清理 app.sample.ini 里的两条死配置(app.title / server.xsrf_expire):
代码中从未读取(站点标题走 i18n),留着会让新检测对官方模板误报。
验证:
- 用现场原始配置端到端复现 → 归一化后保留期 30 天、自动清理开启、上限 20 万,
日志明确说明「配置键名已自动纠正」;
- 误拼键(honeypot_autoclean / honeypot_max_row)自动纠正;未知键(totally_bogus)
WARN 告警;
- 显式 honeypot_auto_clean = false 被尊重为关闭;完全未配置时缺省开启;
- 新增 TestSampleIniHasNoUnknownKeys 回归检测:官方样例配置零误报;
- go build ./... + go test -timeout 600s ./... 全绿,go vet 无输出。
2026-10-21 13:41:19 +08:00
tamakyi
63e851ddae
feat(honeypot): 自动保留期清理 + 行数上限 + VACUUM 空间回收 + size 子命令
...
问题:honeypot_hits 是只追加审计表,写入完全由外部扫描流量驱动(不可预期、
不可节流),而原实现只在手动跑 CLI prune 时才清理,站点运行期表体积只增不减。
实测取证(本地 PostgreSQL 建同构表灌 10.5 万行真实数据):
- 典型行 244 字节(含索引),10.5 万行 24 MB
- 保留期清理对照(行数恒定 10 万,每轮删 25% 插 25%):
有 VACUUM 22→24MB 收敛;无 VACUUM 22→52MB 持续膨胀
- VACUUM 不缩小文件,只让空间可复用;二次实测(5 万行、连续 4 轮删插)
行数恒定 5 万、文件稳定 15MB 不再增长
改动:
- 新增 HoneypotRetention 后台清理器(每小时一轮):时间闸 honeypot_retain_days
+ 数量闸 honeypot_max_rows,清理后调 Compact 回收空间
- db: 新增 Compact()(PG 执行 VACUUM (ANALYZE),走 GORM Exec 非事务路径)、
DeleteOldestBeyond()(跨方言通用)、ListSinceByIP()、Size()
- 去掉 honeypot_hits.provider 无用索引(10 万行占 648kB,全代码库零查询使用),
并在启动时显式清理既有库上的旧索引(GORM 去 tag 不会自动删)
- 修 CLI hits --ip 过滤 bug:原先取回最近 N 条再在 Go 侧筛,导致指定 IP 的
历史命中基本查不出来 → 改到 SQL 层过滤
- 新增 CLI honeypot size 子命令:报告行数、磁盘占用、时间跨度,并与两道闸对照
- 新增配置 honeypot_auto_clean / honeypot_max_rows
2026-10-21 13:16:14 +08:00
tamakyi
79051acae8
miguan v1
2026-10-21 12:09:28 +08:00
tamakyi
e9fba8b9ed
theme v1
2026-10-19 15:23:11 +08:00
tamakyi
4592b6ef24
fix v13
2026-10-19 01:58:00 +08:00
tamakyi
7a6417c33d
restrict v8
2026-10-18 23:32:38 +08:00
tamakyi
bd81398a06
restrict v6
2026-10-18 22:18:21 +08:00
tamakyi
6bd90779f8
restrict v1
2026-10-18 21:14:59 +08:00
tamakyi
5ee2dc1540
fix login error
2026-09-15 13:10:57 +08:00
tamakyi
adcc42cc8c
fix anwser token and database issus
2026-09-15 11:06:09 +08:00
tamakyi
8e2325eda5
aliyun sensor server
2026-09-10 18:04:19 +08:00
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