用户问「数据统计处的推荐地点和踩雷地点只针对站长吗?应该分开,用户的点赞点踩单独做一块」。 他看到的没错:那张卡的**列表全是站长结论**(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。
53 lines
4.0 KiB
Markdown
53 lines
4.0 KiB
Markdown
# 2026-10-01
|
||
|
||
## 防复发:把「删数据集带走自建数据」做成结构性门禁(00:30)
|
||
|
||
用户放弃继续救那批旧图:「我只想避免类似的事件再次发生」。于是把这次的教训固化成三件东西
|
||
(`internal/web/dataset_ownership_test.go` + `schema.sql` 抬头 + README 新章节):
|
||
|
||
1. **归属铁律**:判断标准只有一句——**这份记录能不能从备份重新导入?**
|
||
能(places/visits/movements/weather/activities/tags/entity_raw)⇒ 可挂 CASCADE;
|
||
不能(place_images/place_notes/place_votes/topics)⇒ 只留 dataset_id 当出处、不许挂外键。
|
||
2. **两道门禁**:运行时查 `pg_constraint`(表必须在白名单里,反向也查)+ 静态扫 `schema.sql`
|
||
(新建的库走 CREATE TABLE,只看运行时有盲点)。做变异验证时确认了静态那条**真的会响**。
|
||
3. **体检常驻后台首页**:库里 N 张 / 本地盘上 M 个 / 落单 K 个(`imageHealth`),落单 >0 直接报警。
|
||
事故的本质不是「删数据集」本身,而是**行与对象悄悄对不上、没人发现**——让它当场可见。
|
||
|
||
变异验证的收获(比测试本身更有价值):
|
||
- 运行时门禁单独看**不够**:夹具每次跑迁移,迁移自己就把坏外键摘了 ⇒ 它永远不会在不健康的库上报警。
|
||
补了 `TestMigrationDropsStaleDatasetForeignKeys`:注入事故原样的外键再跑迁移,断言被摘掉。
|
||
- 这条自愈用例又抓出缺口:`place_notes`/`place_votes` 上同类外键**迁移并不会摘**
|
||
(原先只写了 place_images 与 topics 两句 DROP)⇒ 补成四张自建数据表全覆盖。
|
||
**部署出去的库要能自愈,DROP 必须覆盖全部同类表。**
|
||
- 静态扫描必须先跳过 `--` 注释行:规矩的说明文字里就写着 `REFERENCES datasets(id)`,
|
||
不跳过会把自己写的文档当成违规。
|
||
|
||
顺带订正:后台「删除数据集」的二次确认文案在专题改完后已说过时话(说专题会一起删),改成实话。
|
||
|
||
### 补记:迁移块排序的老规矩我又踩了一次(00:55)
|
||
|
||
把「摘外键」那五句 ALTER 收成一段放在 `topics` 之后,而 `place_images` 的 CREATE 在这段**之后**
|
||
⇒ 开发库(表已存在)毫无症状,**全新库起不来**:`ALTER TABLE place_images …` 报「关系不存在」。
|
||
正是记忆里那条老规矩:「迁移块必须收敛且排在依赖表之后(插在 place_images 前:开发库无症、新库起不来)」。
|
||
被 `TestFreshDatabaseServesAppQueries` 当场抓住(这条门存在的意义)。
|
||
|
||
改成整段排在 `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 存」,其实早就是坐标键了。
|