统计页把「我的结论」与「访客点赞」拆成两块

用户问「数据统计处的推荐地点和踩雷地点只针对站长吗?应该分开,用户的点赞点踩单独做一块」。
他看到的没错:那张卡的**列表全是站长结论**(verdictPlaces = place_votes),访客票只在副标题里
露了个总数(👍 N · 👎 M)——既看不出哪几个地点被顶上去,也容易被当成两者合并的榜。

- 新增 votes.TopVisitorVoted:按访客票数列地点榜。HAVING 只算**至少有一票的那个方向**,
  否则 0 票的地点会把榜塞满;同票数按 visit_count DESC, p.id 排,刷新之间顺序不跳。
  票按坐标键挂,所以 join 回当前数据集的地点才定位得到(与 Tallies/verdictPlaces 同一口径)。
- 「推荐 & 踩雷(我的结论)」只讲站长,副标题只留「推荐 N · 踩雷 M」,标题与列表都改成「我」的口吻。
- 新增「访客点赞」卡:副标题总票数,两列列 👍/👎 票最多的地点并带票数,各有空态。
- 加载拆成两个 pageJob(verdicts / visitorvotes)并行,两块互不依赖就不该串着等。
- 两块共用同一个可见性开关(IsAdmin || Settings.MarksPublic),与地点页投票区一致。

用例(stats_votes_split_test.go)按卡片标题切出两段 HTML,断言「我标的地点只出现在站长块、
访客顶的地点只出现在访客块」,并核对两边各自的计数——比单看总数更能证明没串台。

⚠️ 又一次同一个坑:两张票表都按坐标键读、与数据集无关,place_visitor_votes 还没有 user_id、
不随删用户清理 ⇒ 上一轮跑剩的票会被算进来、票数越堆越多。新增 isolateVoteState 按 place_key
清两张表(前置 + cleanup 各一次)。

顺带订正 README:那里还写着站长结论「按 src_pk 存」,其实早就是坐标键了。
全量验收 PASS=338 FAIL=0 SKIP=0。
This commit is contained in:
tamakyi
2026-10-01 01:47:48 +08:00
parent 4c3032cf48
commit ff46a9cbb8
9 changed files with 340 additions and 13 deletions
+16
View File
@@ -34,3 +34,19 @@
改成整段排在 `place_images` 建表之后(它是五张里最后一张),并把这个坑写进 schema.sql 的注释里。
教训:**凡是 ALTER 某个表的迁移块,先确认那张表在文件里已经建过了**——
只在本机库上跑一遍是测不出来的,必须跑 fresh-database 用例。
## 统计页:站长结论与访客点赞拆成两块(01:40)
用户问「数据统计处的推荐地点和踩雷地点只针对站长吗?应该分开,用户的点赞点踩单独做一块」。
他看到的没错:那张卡的**列表全是站长结论**,访客票只在副标题露了个总数。
- 新增 `votes.TopVisitorVoted`(`HAVING` 只算至少有一票的方向,否则 0 票地点塞满榜;
同票按 `visit_count DESC, p.id` 保证顺序稳定)。
- 「推荐 & 踩雷(我的结论)」只讲站长;另起一张卡「访客点赞」,列 👍/👎 票最多的地点并带票数。
- 两个 pageJob 并行加载;两块共用 `IsAdmin || MarksPublic`(与地点页投票区一致)。
- 用例按卡片标题切 HTML 断言「A 只在站长块、B 只在访客块」,比数总数更能证明没串台。
⚠️ 又踩同一个坑:票表按坐标键跨数据集读、`place_visitor_votes` 还没有 user_id ⇒
上一轮跑剩的票会被算进来。新增 `isolateVoteState` 按 place_key 清两张表(前置 + cleanup)。
顺带订正 README:那里还写着站长结论「按 src_pk 存」,其实早就是坐标键了。
+28
View File
@@ -725,3 +725,31 @@ Core Data 元数据、索引、持久化历史、`ZRAWVISIT` 采样点、`ZTRIP*
(更早那次改坐标键时摘的,老库/生产库上未必摘干净)**迁移并不会摘**——
原来只写了 `place_images` 与 `topics` 两句 DROP。已补成四张自建数据表全覆盖
(含 `place_visitor_votes` 兜底)。**部署出去的库要能自愈,DROP 必须覆盖全部同类表,不是只覆盖这次动过的那两张。**
---
## 三十六、统计页:站长结论与访客点赞拆成两块(2026-10-01)
用户问「数据统计处的推荐地点和踩雷地点只针对站长吗?我觉得应该分开,用户的点赞点踩单独做一块」。
查下来他看到的没错:`/stats` 那张卡**列表全是站长结论**(`verdictPlaces` = `place_votes`),
访客票只在副标题里露了个总数(`👍 N · 👎 M`)——既看不出哪几个地点被顶上去,也容易被当成两者合并的榜。
改动(`internal/votes` + `internal/web` + `templates/stats.html`):
- 新增 `votes.TopVisitorVoted(ctx, datasetID, vote, limit)`:按访客票数列地点榜。
`HAVING count(*) FILTER (WHERE vv.vote=$2) > 0` —— **只算至少有一票的那个方向**,
否则 0 票的地点会把榜塞满;同票数按 `visit_count DESC, p.id` 排,保证刷新之间顺序不跳。
票按坐标键挂,所以要 join 回当前数据集的地点才定位得到(与 `Tallies`/`verdictPlaces` 同一口径)。
- 「推荐 & 踩雷(我的结论)」:只讲站长,副标题只留 `推荐 N · 踩雷 M`,标题与列表都改成「我」的口吻。
- 新增「访客点赞」:副标题总票数(复用 `voteTotals` 的 `VoteUp/VoteDown`),两列分别列
👍/👎 票最多的地点(带票数),空态「还没有访客点赞 / 点踩」。
- 加载拆成**两个 pageJob**(`verdicts` / `visitorvotes`):两者互不依赖,串在一个任务里只会白等。
- 两块共用同一个可见性开关(`IsAdmin || Settings.MarksPublic`),与地点页的投票区一致。
⚠️ 用例隔离(又一次同一个坑):**两张票表都按坐标键读,与数据集无关**,
`place_visitor_votes` 还没有 `user_id`、不随删用户清理 ⇒ 上一轮跑剩的票会被算进来、票数越堆越多。
新增 `isolateVoteState`(按 place_key 清两张表,前置 + `t.Cleanup` 各一次)。
断言手法:按卡片标题切出两段 HTML(`htmlSection`),再断言「A 只在站长块、B 只在访客块」——
比数总数更能证明两块真的没串台。
顺带订正 README:那里还写着站长结论「按 `src_pk` 存」,其实早就是**坐标键**了。
+4 -1
View File
@@ -1,6 +1,6 @@
# rond-with-you 硬规矩(索引)
> 全文:`.workbuddy/memory/MEMORY-details.md`(36 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。
> 全文:`.workbuddy/memory/MEMORY-details.md`(37 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。
>
> ⚠️ 本文件**按字符数**注入,硬上限 `MAX_MEMORY_CHARS = 1e4`(WorkBuddy `app.asar` 里 `getWorkingMemoryContent`;中文 1 字算 1,不是按字节)。超了会被 `substring(0,1e4)` 截断并写明「先整理再干活」——即整段尾部静默丢失。日常日志与本 details **不参与注入**,可以随便写。现值见 `wc -m`,留 20% 余量。
@@ -64,6 +64,9 @@
## 跨备份 / 附属数据(details 十九~廿一、卅四)
- 预演按 `VisitUUID`/`ActivityUUID` 对齐、只读;`raw #>> '{ZIDENTIFIER_,$blob}'` 是 base64 须转 hex 比;地点只认坐标(`PlaceCoordKey` 6 位)。
- 附属数据(备注/投票/图片)主键=`place_key` 坐标键(非 dataset+src_pk);Go 与 SQL 两套须逐位一致。
- **站长结论(`place_votes`)与访客票(`place_visitor_votes`)是两码事**:各存各表、各看各榜;
统计页拆成「我的结论」与「访客点赞」两块(`votes.TopVisitorVoted`,`HAVING` 只算有票的那个方向)。
⚠️ 票表也按坐标键跨数据集读 ⇒ 用例必须按 `place_key` 清历史票,否则票数越跑越多。(details 卅六)
- ⚠️ **附属数据不能挂数据集外键**:`place_images` 曾按 `dataset_id` CASCADE ⇒ 删旧数据集=删图片行、**文件却留在盘上**(读取按坐标键、行按数据集级联,两套口径打架)。已摘掉外键;恢复走后台「图片挂载恢复」,认地点靠旧数据集留下的 `baselines/<id>.sqlite`(**别清 baselines 目录**);⚠️ 来源必须**全部读进来合并**(places → entity_raw 留档 → 该 sqlite 及**每份 `.bak`**)——换基线会覆盖底座文件,只看当前那份会整片认不出(实测 40 张只挂回 7 张)。`image_recovery_test.go`(details 卅四)
- ⚠️ 恢复**同时扫对象存储**(R2/S3):`media.ListObjects`(ListObjectsV2 + SigV4,查询串先编码再签),
需 `s3:ListBucket` 权限;列不动时本地照救 + warning。**专题同理**:`topics` 已改成归人不归数据集