用户要求这份记忆跟着仓库走,而不是只留在本机。 .gitignore 里挡路的是 `.workbuddy/`。改成反忽略六行,并且**只放行 .md**: 别用 `!.workbuddy/memory/**`——它会把 memory 下任何文件都放行,本项目真发生过 `web.test.exe` 混进暂存。复核用 `git check-ignore -q --no-index` 逐条打靶: memory/*.png、memory/sub/x.md、memory/MEMORY.md.bak、plans/a.md 全 IGNORED, 17 个 .md 全 included;`git add -An .workbuddy/` 恰好 16 个文件。 其余 .workbuddy 内容(计划、本地设置、临时产物)继续忽略。记忆里没有真凭据 (唯一命中是 2026-09-14.md 里的 `password=rond-admin`,那是项目默认口令)。 顺带做两件整理: 1. 取证了注入上限。以前按「字节数」判断 MEMORY.md 是否超限是错的: WorkBuddy 的 `getWorkingMemoryContent()` 用 `MAX_MEMORY_CHARS = 1e4` 比 JS `content.length`,即**字符数**(中文 1 字算 1);超了 substring 截断、尾部静默丢失。 本 details 与每日日志都不参与注入,只有 MEMORY.md 受约束。 现已从 6604 字符收到上限的 66%,并把这条上限写进索引抬头。 2. 把「备份合并」「导出/恢复三条铁律」两段长文迁进 MEMORY-details.md 的卅一/卅二节, 索引只留关键词指针。行尾统一为 LF(仓库 .gitattributes 是 `* text=auto eol=lf`)。
37 KiB
2026-09-19
真机反馈「移动端还是要左右滑」:容器查询是错的,改回媒体查询(阈值 800)
站长 00:09 反馈:在本地环境用移动端测试,还是得左右滑动,否则只能看到一部分信息。
根因:我上一轮把堆叠判据换成 @container 是个错误决定
无头 Chrome(新版)里容器查询工作正常,390 下表格都堆叠了;但容器查询要 iOS 16 / Chrome 105
起才支持,不支持的浏览器会把整块 @container 规则当不认识的东西整块丢弃 ⇒ 表格完全不堆叠、
8 列要横滑 328388px。站长的手机浏览器大概率就落在这一档,于是「还是得左右滑、只能看到一部分」。
(顺带:媒体查询 620 那个旧版本同样有漏 —— 视口 621792 的卡片既放不下表格又没窄到 620 以下。)
改法
app.css 末尾:@container (max-width: 699px) + .tw{container-type:inline-size} →
@media (max-width: 800px)。800 是从「卡片能不能放下表格」换算来的:卡片内容宽 ≈ 视口 − 93,
最宽的表 min-width 700 ⇒ 视口 793 以上才放得下,取 800 留几像素余量(注释里写清了换算依据)。
wide_table_layout_test.go 的 CSS 断言同步改成:每条 tb-stack 规则都必须落在 max-width ≥ 792
的 @media 里(容器查询会被判为「不在任何 @media 里」,退回 620 会被判「阈值太小」)。
实测
- 形态切换点:360/390/620/800 → 堆叠;801/900/1440 → 表格。全部横滑 0(/places、/stats、/report)。
- 禁用容器查询的环境下依然堆叠(注入脚本删 CSSContainerRule:删到 0 条、横滑仍 0)⇒ 旧手机不会再退化。
- 两次变异验证:阈值退回 620 → FAIL(「视口 621~792 的卡片放不下表格」); 改回 @container → FAIL(「不在任何 @media 里」)。
- 四轮全站审计(390 站长 / 390 访客 / 1440 / 320)与改动前逐值一致(溢出全 0、0 JS 报错)。
- 两段式全量:纯净包 PASS → 11 包 ok、0 FAIL。
移动端还剩哪三处能左右滑(普查全站所有 overflow-x:auto 的元素)
| 位置 | 需滑 | 性质 |
|---|---|---|
.nav-links(每个页面顶部) |
107~137px | 7 个导航项在 390 里放不下(内容 321 / 可视 214),overflow-x:auto + 隐藏滚动条 |
.heat(首页「活跃日历」) |
513px | 有意的横向滚动(一周 53 格) |
.temp-strip(统计页「天气与气温」逐月气温条) |
32px | 有意的横向滚动 |
表格已全部不滑。导航那处每个页面都有、且用户看不出「还能滑」,是下一个候选(方案:换行两行 / 「更多」菜单 / 保持滑动但加渐隐提示),等站长定。
教训
"更精确"的技术特性要先看兼容性代价:容器查询在无头 Chrome 里完美,我却没评估旧浏览器会 整块失效——这是把「自己环境的验证通过」当成了「用户环境可用」。凡是写 CSS/JS 判据, 先问一句「不支持这个特性时会发生什么」,退化路径比精确性更重要。
站长补充定位:「后台数据编辑页面,比如地点列表等地方」
我把问题定位到 /admin/edit:三张表(地点列表 / 到访记录 / 行程记录)从来没有打过 tb-stack,
390 下 5 列要横滑 316px。之所以上两轮全站审计没发现,是因为它们默认折叠在 data-card 卡片里、
探针量到的是 display:none —— 默认态测不出隐藏内容的横滑,这是漏页的真正原因。
改动
admin/edit.html:三张表加tb-stack,非首格补data-label(城市/坐标/到访/地点/类型/备注/路线/方式/里程); 末尾那个只放「编辑」的操作格不加标签(表头本来就是空的<th></th>,硬加会把按钮/表单挤窄)。- 三处行内编辑表单的
style="min-width:280/300/320px"→style="--efw:280/300/320px"; CSS 里.inline-edit form { min-width: var(--efw, 280px) },放不下宽表时再放开成 0。 用 CSS 变量而不是!important:inline 的 min-width 压不过后面的媒体查询覆盖,变量没有优先级问题。 - 操作格的
white-space:nowrap从 inline style 挪成类.ops(table.tb td.ops):white-space会继承到格内的编辑表单,堆叠后「已选:#403 2026-09-13 19:53 · 某某地」这一行不换行、把卡片顶出 44px。 同样的 nowrap 还出现在admin/index.html与admin/topics.html,一并改成类。 - 堆叠态放开:
td:not([data-label])与td.ops都white-space: normal。
测试(重点:让「漏页」不再可能)
tb_stack_test.gotargets 加/admin/edit(12 张表);标签豁免规则改成跟着表头走: 表头为空的列不需要data-label(正好覆盖操作格,规则自洽)。wide_table_layout_test.gotargets 加/admin/edit、/admin/photos;div 栈同时跟踪.tw, 新增判据 「凡被 .tw 包住的表格都必须带 tb-stack」(照片表按既有方案单独处理,显式豁免)。 这条是通用的:以后任何新页面漏加,测试直接报出来。
实测
- 展开三张表后:360/390/620/801/1440 横滑全部 0(改前 390 是 316)。
- 每张表各展开一行编辑表单:28 个可见控件逐个
elementFromPoint点测 0 个点不到, 表单不超出卡片(390 下 302px 放进 324px);0 JS 报错。桌面 1440 仍是表格、td.ops仍 nowrap。 - 后台各页(edit / photos / admin / topics)展开后全站可横滑元素只剩
.nav-links(导航,107px)。 - 两段式全量:纯净包 PASS → 11 包 ok、0 FAIL(
tb_stack_test12 张表、wide_table_layout_test12 张表 13 条规则)。 - 四轮审计与上一版逐值一致(表格改的都是折叠区内容,默认态审计看不出来)。
两个坑
- 折叠/隐藏区域里的内容,默认态探针测不到 —— 审计脚本跑的是页面默认态,
/admin/edit三张表 一直是display:none。凡是"某个区块要展开/切换才出现"的页面,必须先展开再量。 - CDP 独占:我在审计脚本运行期间并发跑了截图命令,两边抢同一个 Chrome/9333,
审计数字当场就歪了(
/timeline小触控 181 → 6、/map13 → 6)。审计/探针/截图一律串行。
导航栏:保持可滑动 + 右缘渐隐提示(站长选的方案)
站长的第二问选了「保持滑动,加渐隐提示」。7 个导航项在 390 下内容 321 / 可视 214,硬塞不下 (每项 2~3 个汉字 + 44px 触控高度,缩字号内边距也省不出 107px),换行或「更多」菜单都动结构, 所以按他选的做提示。
layout.html的nav定义里加 9 行脚本(跟在</nav>后):按scrollWidth - clientWidth - scrollLeft > 2给.nav-links加/去.overflow-right, 监听scroll(passive)与resize。app.css620 块里加.nav-links.overflow-right { mask-image: linear-gradient(...) }。 用 mask 而不是盖一层渐变伪元素——伪元素会把链接文字一起遮住,mask 是让内容本身渐隐。 不支持 mask 的浏览器只是少个提示,滑动照旧(退化安全)。
实测:390 溢出 107 → 有类、mask 生效;滑到末端类自动消失、滑回开头回来;1440 无溢出无类、mask=none。
像素级验证渐隐可见:同一页面 A/B(注入脚本摘掉类)截图对比,差异区 x 516~548(scale 2)
正好是 .nav-links 右缘那 28px——注意别拿整图最右侧去采样,那里是「后台/退出」按钮、不在 mask 范围内
(我第一次就这么采的,差 0.0 差点误判成"渐隐没生效")。
全量测试(含 layout.html/CSS 改动):纯净包 PASS → 11 包 ok、0 FAIL。改动仍未提交。
收尾复查与提交(01:30~01:55)
复查清单与结论:
go build ./.../go vet ./.../gofmt -l全清;CSS 花括号 485/485 平衡;CSS 里@container只出现在注释里(stripCSSComments那条判据就是为它写的:注释里的@media/@container会带偏解析)。- 导出验收两段式:
ROND_CLEAN_OUT=… go test ./internal/web -run TestMovementCleanExport→ROND_EXPORT=… go test ./internal/rond -run TestExportRoundtrip,visits 823/places 323/moves 791 全对上, 坐标偏差 0.000000000、时间偏差 0s。注意:这两个测试的相对路径是相对包目录(internal/rond), 从仓库根传ROND_SRC=data/…会报「找不到文件」,必须给绝对路径C:/…/data/…。 - 全量
go test -count=1 ./...(带ROND_TEST_CONF+ROND_SRC):11 包 ok、0 FAIL 0 SKIP。 - CDP 实测(debug=true,模板/CSS 热加载):390 下
/admin/edit/stats/report整页溢出 0、 每个.tw横滑 0、__errs=0;1440 下同样 0 溢出,且 nav 无 overflow-right 类(桌面不该有渐隐), 操作格white-space=桌面 nowrap / 堆叠态 normal。三张行内编辑表的字段逐一核对无缺失。
修了一处测试的逻辑瑕疵:checkWideTablesOutsideGrid 原来「凡在 g2 里的表都报错」,
但 g2 里放窄表是合法的 → 会误报。改成只判 tb-stack(那才是「宽表」的定义),窄表走 .tw 那条判据。
做了变异验证:把 /stats 的 grid 改回 g2 → 测试确实报 3 条 FAIL → 还原。
⚠️ 变异验证差点污染工作区:我图省事用 b.replace(b'<div class="grid">', b'<div class="grid g2">')
全量替换,而 stats.html 里本来就有 2 处没动过的 grid g2(「推荐&踩雷」「类型分布+一天中的分布」),
还原时又被全量换回去 ⇒ 凭空多出 2 处我不想要的改动(diff 从 3 行变 5 行)。
教训:变异只改一处、且改完用 git diff --stat 跟改前的行数对账(这次是看到 6→10 才发现)。
按行号改(不是按内容 replace)才安全。
提交 9e01923 并 push(96e7631..9e01923,一次带上了前三次未推的提交)。
评估「导入新备份 → 分析差异 → 合并」(未动生产代码)
用户诉求:data/20260919_111819.rondbackup 与运行中的库有明显差异、两边各有对方没有的数据;
fwss 虽然能选叠加/覆盖,但看不到差异。
先做实测,结论推翻了朴素设想。临时探针 tools/{diffprobe,idprobe,colprobe,fogprobe}/(一次性,可删):
diffprobe 20260919 … 9:867 条到访「同号且一致 0 / 仅备份有 5 / 同号冲突 867」; 但按到达时刻对齐能命中 866/871 ⇒ 是同一批数据,Z_PK 被整体重排(配对全是 1→555、2→276 这种乱序)。- 对照
diffprobe 20260914 … 9:同号一致 819/824、1→1 2→2… ⇒ 库内那一份就是 0914 备份 (外加 44 条站点补录is_user_added+ 4 处站点改过的时间)。所以 0914 时 PK 还是稳的,是这 5 天里变的。 idprobe:ZVISIT.ZIDENTIFIER_两次备份 822 条 UUID 完全交集(A 独有 0), 但 822 条里 Z_PK 全变;ZACTIVITY.ZUID_同理(30/31)。⇒ UUID 才是跨备份稳定键。colprobe:ZLOCATION.ZPLACEID322/323 为空、ZEVENTIDENTIFIER全空 ⇒ 地点没有 UUID 可用, 只能「名称+坐标(1e-5)」当键(B 的 367 个地点命中 A 323 个,44 个是新的)。fogprobe:库内 10542 块/670565 格。0914 快照 8701 块对库 overlay 新增 0 格(全重复), 而 cover 会静默丢弃 1841 块 / 94401 格(≈14%)。fog-export-fixed.fwss与库完全一致(0 差异)。
副产品发现:schema.sql 里「src_pk(rond 内部地点 ID,跨快照稳定)」的注释是错的;
ingest.contentHash 用 SrcPK|Arrival 做指纹,被 PK 重排骗过 ⇒ 同批数据重导会被当成新内容、新建数据集。
给出的方案(待批准,未写代码):上传先 dry-run 出差异页(新增/冲突/站点独有,UUID 优先对齐)→ 确认后单事务合并;fwss 先补数字差异面板(重点是 cover 会丢多少),再考虑差异地图预览。
用户拍板 + 落地迷雾差异(阶段 1)
三个回答:①没重装/换设备/恢复 iCloud,"最后一次是从本项目正式环境导出数据覆盖到 rond 里" ②冲突策略=全部逐条 ③先做差异。
①是决定性的:Z_PK 重排不是 iCloud 的锅,是「把站点导出的包导回 rond」这个动作本身,
即每次往返都会发生。于是补做了链路验证 tools/exportprobe:调 ExportRondbackup 导一份,
再与 0914 原始备份比 —— 822 条 UUID 全部一致、Z_PK 也全部一致(B 独有 44 条正是站点补录的)。
⇒ 站点侧导出完好,UUID 是贯穿全链路的唯一稳定键,合并方案的地基成立。
已实现(internal/fog/diff.go + internal/web/fog*.go + templates/admin/fog-preview.html):
fog.DiffBlocks(cur, inc)纯函数,出 GainBlocks/GainCells/GainArea、Overlap/SameBlocks、 LostBlocks/LostCells/LostArea;面积按块中心纬度 cos 修正。- 导入改三段式:
POST /admin/fog/preview(暂存到data_dir/fog-staging/,令牌 32 位 hex, TTL 2h)→GET /admin/fog/preview/{token}(现算差异,不落库)→POST /admin/fog?stash=…(此时是 urlencoded,PostFormValue能读到 mode;无 stash 才走原 multipart,向后兼容)。 - 差异页把两件事摆出来:叠加能补多少;整体替换会丢多少块/格/面积(快照缺 Model/~、Model/# 时另给警示)。
真实数据实测(开发库 10542 块 / 670565 格 vs data/Snapshot-20260914T154839+0800.fwss):
叠加 +0(全是重复)⇒ 文案报「已被完全覆盖」;整体替换会丢 1841 块 / 94401 格 / 7.7 km²。
页面数字与独立探针 tools/fogprobe 逐项吻合。CDP 量过 1440/390 均无横向溢出、按钮 84×44 达标。
验证:fog/diff_test.go(5 例)+ web/fog_preview_test.go(预览不落库、叠加真写、替换丢数据、
令牌挡路径穿越)。变异验证:把 ix.Import(..., mode=="overlay") 改成 true,
TestFogPreviewWarnsBeforeCoverLosesData 立刻失败("应有 3 格,实际 5")。
go test ./internal/web/ ./internal/fog/ 全绿;全量 ./... 也绿(internal/rond 那条
TestBaselineKeepsCoreData 要传绝对路径的 ROND_SRC,相对路径会报"不是有效的 zip")。
探针仍在 tools/{diffprobe,idprobe,colprobe,fogprobe,exportprobe}/,后续做 rondbackup 合并还要用。
下一步(未做):rondbackup 差异预演页(UUID 对齐 + 地点用名称/坐标兜底),冲突全部逐条选。
rondbackup 差异预演(本次)
internal/rond/diff.go(纯函数:DiffVisits/DiffPlaces/DiffActivities + Counts)
rond.Parse新增Backup.VisitUUID/ActivityUUID(从 SQLitehex(ZIDENTIFIER_)读)web/backup_diff.go(库侧加载 +POST /admin/preview暂存 +GET /admin/preview/{token}渲染)templates/admin/backup-preview.html。后台上传区改成两个按钮:分析差异(primary)/ 直接导入(普通)。 暂存逻辑同时抽成通用stashPut/stashRead/stashSweep(迷雾与备份共用),迷雾那套测试原样通过。
过程中撞到并修掉的两个真问题
- 无 UUID 的到访被双算:真机 824 条到访里有 2 条没写 UUID,只按 UUID 配对 ⇒ 同一条既进
「备份新增」又进「仅站内有」。修法是加「到达时间(到秒)+ 地点坐标」兜底配对
(
timeKey;只到秒是因为苹果时间戳是浮点秒、PG 存微秒)。 - 地点改名造成 17 条假冲突:首版地点键是「名称+坐标」,而站点的
backfillAfterImport把「体育馆美食街」反查成了「河池市国民体质监测中心」(坐标一字未动),于是一整个地点的 17 条到访全被报成「换了地点」。改成地点只认坐标(PlaceCoordKey),名称降级为比较字段; 配不上再按名称兜底(覆盖站点把坐标纠偏过的情况)。改完真实数据上冲突 17 → 0。
真实数据实测(0919 备份 vs 库内 0914 数据集)
| 实体 | 备份 | 站内 | 备份新增 | 仅站内有 | 冲突 | 一致 |
|---|---|---|---|---|---|---|
| 到访 | 872 | 867 | 49 | 45 | 0 | 822 |
| 地点 | 367 | 368 | 1 | 2 | 4 | 362 |
| 活动 | 31 | 31 | 0 | 0 | 0 | 31 |
账目(tools/keyprobe 逐条核过):0914 备份 824 行 = 822 有 UUID + 2 无(#16、#191);
其中 #191 无到达时间,导入时就被跳过且不写 visits 但写了留档,所以库里是 821 条有 UUID 的到访。
0919 备份 872 行 = 870 有 UUID + 2 无(#365、#372 无到达时间)。
于是:821 条按 UUID 配对、49 条是 0919 新出现的 UUID(870−821)、
#365 靠「到达时间+地点坐标」兜底配上库里的 #16、
库里 45 条站点补录无 UUID 也没留档 ⇒ 仅站内有 45。四个数字全部自洽。
(一度以为「有 1 条无 UUID 没配上」,是把补录数记成 44 了 —— is_user_added 实际是 45 条。)
地点 4 条冲突正是反查补全留下的: 改名 1 条、站内比备份更全 1 条、坐标被纠偏 2 条(28.141772→28.144964 这类)。
验证
rond/diff_test.go 11 例(UUID 对齐/字段差异/编号撞号不得误配/兜底配对/改名/坐标纠偏/活动重命名)
web/backup_diff_test.go3 例端到端(同一份备份报一致、站内改动静默识别、渲染前后行数不变)。 变异验证:去掉used[i] = true,fog 与 web 两层同时失败。 完整验收(带ROND_SRC/ROND_EXPORT/ROND_DSN/FOG_SRC/ROND_CLEAN_OUT)见当次结果。
按建议补做的三项
ingest.contentHash换稳定键(原为SrcPK|Arrival):改成按到访 UUID / 活动 UUID / 地点「名称+坐标」排序后取摘要,没有 UUID 的老备份退回「到达时间+地点坐标」。 回归ingest_test.go六例,变异验证(把 UUID 换回fmt.Sprintf("V%d", pk))当场失败。- 修正两处声称「src_pk 跨快照稳定」的注释(
db/schema.sql、rond/export.go), 改成「只在单个数据集内稳定」并写明跨备份要靠 UUID。 - 复查「1 条配对不上」:用
tools/keyprobe把两边的键逐条打出来,结论是没有 bug, 是补录条数记错(45 而非 44)。兜底配对实际工作正常。
顺带确认:「站点补录行占用 nextSrcPK 号段」这个隐患不影响正确性 —— 跨备份对齐走 UUID,导出时新行是 INSERT(基线里没有该号),回导实机后 pk 重排也无所谓。
合并执行(本次)
rond/merge.go(纯逻辑:Choices + BuildMergePlan,把「差异 + 页面上逐条的选择」翻成改动清单)
web/backup_merge.go(真执行)+ 差异页的合并表单(form=属性把各表格里的 radio 关联到同一个#merge-form) →POST /admin/preview/{token}/merge。
语义:新增类勾选才并;「仅站内有」一律保留;冲突逐条选(用实机/用站内/跳过,默认用站内,没提交或怪值也退回用站内)。 不做删除 —— 删地点会级联删掉它的全部到访。
合并顺序:地点 → 活动 → 标签 → 到访 → 位移/天气 → 重算地点聚合与数据集汇总,全在一个 withTx 里。
三个实测撞出来的坑(都已修)
- 位移/天气被整份复制:一开始遍历
b.Movements只要两端能映射就插,而映射表里包含所有备份到访 ⇒ 库里已有的位移又插了一遍(791 → 1547)。改成只处理「至少一端是这一轮新插入的到访」。 - 天气重复:站内删到访时是把
weather.visit_src置 NULL 而保留记录(editVisitDelete), 合并把到访找回来时又插一条同at的天气。改成插入前先认领「同一时刻、visit_src IS NULL」的那条。 - 测试自己的手法问题:直接
DELETE FROM visits会留下孤儿天气/位移(weather.visit_src没有外键), 数字对不上。改成按站点的方式删(删位移、天气置 NULL、删到访)。
验证
rond/merge_test.go 4 例(选择翻译、怪值退回用站内、跳过、活动键优先 UUID)
web/backup_merge_test.go3 例端到端:删两条到访 → 合并 → 到访/位移/天气全部复原 → 再比对「零差异」(幂等)、 冲突选「用实机」真覆盖 / 选「用站内」不动、什么都不勾则一个字节都不写。 变异验证:把新行的writeRawRow去掉,留档变 821/823 且幂等性当场破(合并后再比对报「新增 1 / 仅站内有 1」)。
真实数据只验证了渲染(radio 分组正确、390 宽无溢出),没有在开发库上真执行合并——那会改动你 0914 那套真实数据。
附属数据改挂坐标键(迁移)
动因:备注/结论/图片原本挂 (dataset_id, src_pk),换个数据集就全看不见——这是用户最初的痛点之一。
用户这次明确要求:迁移可以,但导出出去的 .rondbackup 与 .fwss 必须还是原有结构。
核对过:这次只动 PG 的三张附属表,ExportRondbackup 走的 visits/places/movements/weather/entity_raw
一个没碰,迷雾那几张表也没碰,所以导出结构不受影响(并用往返测试实测确认)。
做法:place_key = 经纬度各 6 位小数(约 0.1m)。place_notes/place_votes 主键换成
(user_id, place_key);place_images 加列 + 索引查。Store 的对外签名一律不变(仍是
userID/datasetID/srcPK),内部改成按 key 存查,所以调用方一处没改。
关键前提验证:Go 的 %.6f 与 PG 的 FM999990.000000 必须逐位一致——先在真实数据上
比了 368/368 全同才敢动。
撞出来的一个大坑:迁移块互相打架
改完后测试报 没有匹配 ON CONFLICT 说明的唯一或者排除约束。查测试库发现:place_key 列在、
主键却还是老的 (user_id, dataset_id, src_pk)。
根因:schema.sql 里早就有一段把主键改回三列的 DO 块(当年加 dataset_id 时写的),
守卫是「有 src_pk 列」——而 src_pk 永远存在,于是它每次启动都执行,把我刚改成 place_key 的主键
又改回去。每次都停在最后执行的那个块上。
修法两条:① 老块先判「没有 place_key 列」再动手(让位);② 新块的判据从「列在不在」改成 「主键列组合对不对」,这样「列已补、主键还老」的半迁移库也能自愈 (测试库正好卡在这个状态,改完立刻验证通过)。
结论:schema.sql 里所有改同一约束的块都会互相覆盖,必须让老块能被新块取代。
访客票一并迁移(用户确认了猜法)
用户明确:访客投票只属于本站,rondbackup 里没有这一项;但它同样要认得出是哪个地点,
「如果两边都有就跟着活跃的走」。于是 place_visitor_votes 也改挂坐标键,
主键换成 (place_key, voter_key);这张表没有 dataset_id,回填时按活跃数据集认领
(同编号在多个数据集里也只认活跃的那个),认不出的留 orphan:<pk> 不丢票。
连带把所有按这两张表统计的地方都改成按坐标键 join places:voteTotals、
verdictPlaces、备注页的排序与两处关联、stats 的 Verdict 过滤。
按它们自己的 dataset_id 过滤会漏掉「从别的数据集写进来的票」。
apiVote 原本没有数据集上下文,补了 datasetFor。
开发库实测:1 条访客票迁到 26.471574|106.673434,与活跃数据集里 src_pk=162
(孔学堂-礼仪广场)的坐标完全一致;/api/votes 返回 tallies:{"162":{"up":1}}、
verdicts 4 条都对得上。新增 TestVisitorVotesFollowPlaceAcrossDatasets
(换数据集票还在、从任一侧点两次都能撤掉同一张票)。
验证
- 开发库:迁移后行数不变(notes 14 / votes 3 / images 5)、无空键、无孤儿键、
place_key与实际坐标 0 条不符;重启两次主键稳定(收敛) - 新增
place_identity_test.go两例:换数据集后备注与结论照旧跟在同一个地点、 坐标差 0.1 度的地点不会互相认领 - 全量复测(含
TestExportRoundtrip与TestNativeSnapshotRoundtrip)见当次结果
合并的删除(上一轮)
「仅站内有」的记录默认一条都不删,要清的逐条勾「合并时删除」(dv_<src_pk> / dp_<src_pk> / da_<src_pk>)。
键用 src_pk 而不是 UUID —— 站点补录的行压根没有 UUID。
顺序:到访 → 活动 → 地点,删到访时连 movements、entity_raw 一起清、weather.visit_src 置空(与站点
editVisitDelete 一致)。名下有到访的地点直接拒绝(外键是 ON DELETE CASCADE,真删会带走到访),
拒绝后整体回滚。
测试:TestBackupMergeDeletesOnlyChecked(勾一条只删一条、没勾的不动、留档一起清、再合并不勾仍不删)
TestBackupMergeRefusesDeletingPlaceInUse(空地点可删、有到访的地点被拒且数据不变)。 变异验证:把「有到访就拒绝」改成n > 100000,测试当场失败。
页面实测:45 个 dv_ 勾选框默认全未勾、12 个冲突 radio、add_new 默认勾选;
那两个「仅站内有」的地点因为 visit_count > 0,按设计不给删除框(显示「还有到访,不能删」)。
踩的坑:TestBackupDiffIdentical 一开始断言整页含「完全一致」四个字——结果被说明文字里的
「UUID 完全一致」命中,成了假阳性;且断言整页 data-label="冲突">0</td> 会被地点行张冠李戴。
改用 visitCounts() 正则取「到访」那一行。另外「上传同一份备份」在地点层面本来就有差异
(导入后反查补全改过名称/地址),所以不能拿整页 Identical 当断言。
差异地图(用户要求「差异部分在地图上展示」)
新增 internal/fog/render_diff.go:RenderDiffTile(cur, inc, z, x, y, crs) 出三色瓦片
(绿=叠加会新增 {36,164,90}、灰=两边都有 {126,134,146}、红=替换会丢 {214,56,56})。
精度分两级:z≥14(scale≥1)逐格精确;更粗的级别下一个块还不到一像素,按块内多数类整块涂
(dominantKind,绘制顺序 both→gain→lost,让绿/红压住灰)。不做描边(三色已够,粗级别会糊)。
internal/web/fog_diff.go:瓦片端点 + 两边块集的单槽缓存(按 token,TTL 10 分钟,
否则每张瓦片都要重解一遍 680KB 的 .fwss)。diffBounds 算两边包围盒给前端 fitBounds
(按底图 crs 换算,否则视野偏几百米)。
踩的坑(值得记):第一次 CDP 测出 container=NO、diffTiles=0 且控制台零报错 ——
原因是页面没设 p.HasMap=true,layout 因此不引 Leaflet,而脚本里 if(!window.L) return
静默退出。记忆里本来就写着这条,还是漏了。判据要认 leaflet-container 类,不能只看有没有报错。
真实数据验证(0914 快照 vs 开发库):container=ok diffTiles=15 ok=15 bad=0,
三色像素 gain=0 / lost=557 / both=2473 —— 绿色为 0 与页面上「叠加 +0」完全自洽。
390/1440 均无横向溢出(地图 324/1042 宽,高 400)。
测试:render_diff_test.go(逐格三色、单边整块、粗级别多数类、边界、dominantKind、计数)
fog_preview_test.go的TestFogDiffTileColors(端点颜色语义 + 越界/无效令牌 404; 第一版用例把(0,0)当越界,被测试自己纠正成x=262144)。变异验证:把cellKind的 gain/lost 反过来,fog 与 web 两层测试同时失败。
后台服务任务的假失败(12:44)
bin/rond.exe 的后台任务报 failed,查下来是重复启动撞端口:旧实例还占着 8080,新进程只打一行
「bind: Only one usage」就退出(日志写进的是新进程那个文件,真正在服务的实例日志在
storage/_rond_new.log)。判据:用唯一 URL(/probe-<pid>-<rand>)打一下,看哪个日志文件出现这条记录,
那个文件对应的才是活着的实例。别急着重编——先看日志确认是不是 bind 冲突。
本次实例是 12:44:09 起的、一直在正常服务(各页 200),templates/CSS 在 debug=true 下热加载,不受影响。
地点补全(本轮)
用户要的:新增/活动和类别为空的地点,根据地名自动判断填充;匹配不上就新增或手工填。
摸底:dataset 9 有 368 个地点,217 个没类别、52 个完全没活动(71 条到访无活动)、41 个零到访空地点。
poi_category 存的是 Apple MapKit 枚举(真机用过 12 种),不是高德分类 —— 补的时候必须写这种形状。
实现:fillup.go(页面/列表/写入)+ fillup_rules.go(词表与映射)+ fillup.html。
三层判断:本地词表 → 高德周边搜索(按坐标,比按名字准)→ 人工。只填空,活动按地点批量设。
撞出来的三件事
- 规则匹配要先掐括号、再按词长优先:否则「尚优居客栈(大理古城步行街店)」命中「步行街」、 「马鞍山公园」命中「山」。
- 单字关键词必须删掉:「独山路28-17号」被猜成「山」——误命中率太高,这类交给高德更稳。
- 页面一度 927KB:每行两个 30+ 选项的
<select>,218 行就是 13000 个 option。 改成input + datalist后降到 169KB。
验证
真实数据跑十批高德:218 个待补里 71 个拿到建议(活动 25、类别 47), 其中词表 10 个、其余来自高德。剩下的高德查过但类型映射不上(邮局、公司、地名地址这类), 缓存记下不再重试,所以批次跑几轮就收敛。
测试:fillup_rules_test.go(用真实地名做用例)+ fillup_test.go
(按地点批量设活动 / 已有值不被覆盖 / 填陌生名字自动建活动类型)。
变异验证:去掉 AND activity_id IS NULL,TestFillupNeverOverwrites 当场失败。
完整验收(含导出往返)全绿。
补:高德参考列 + 类别安全标注
没映射出建议的行原先一片空白,但高德其实已经说了「这里是什么」。多了一列「高德参考」 (POI 名 + 类型原文),「中国邮政 / 生活服务;邮局」这种一眼就能判断该填什么。 类别下拉加了中文标签,并标出哪 12 种是真机确实用过的。
顺带实测回答了一个重要问题:poi_category 改动后导出的 rondbackup 还能不能导回 rond ——
能。用 exportprobe 导一份再读 ZLOCATION.ZCATEGORY_,值与真机备份同格式
(都是 MKPOICategoryXxx 字符串)。当时还写了「真机自己也会写中文类别」——
⚠️ 这条 2026-09-20 已证伪,见下面那次的更正:真机从没写过非枚举值。
补:下拉改回 select
input + datalist 只在下拉展开时显示 label,框里永远显示 value
(MKPOICategoryPark 这种枚举名),站长看不懂。改成 <select> 后选中项直接是中文。
页面原始 940KB、gzip 后 22KB(选项高度重复),体积不是问题,不必再为它绕路。
datalist 这个坑值得记:要用它就得接受「框里显示 value」,想显示中文只能用 select。
全量回归(今天所有改动 + 原有功能)
三层都跑了,没发现回归,也没改任何代码。
1. 代码层 go test ./...(带齐 ROND_TEST_CONF / ROND_SRC / ROND_EXPORT / ROND_DSN / FOG_SRC):
所有包 ok、零 FAIL。关键细节:fwss 往返核对 8701 块 / 106 个其它层条目;
rondbackup 往返逐项一致(visits 823 / places 323 / tags 8 / acts 30 / moves 791 / weather 557 / rawvisits 2246),
坐标偏差 0.000000000、时间偏差 0s;tb_stack_test 检查了 12 张表;访客页面 6 个子测试全过。
2. HTTP 层(curl 全站扫):站长 18 页全 200;访客 9 页全 200; 访客访问 /admin/* 四个都 303 跳登录(权限边界正常);API 五支全 200 且都有数据 (points 64KB / track 108KB / cities / votes / map-config);导出四支全 200 (geojson 108KB / gpx 62KB / rondbackup 2.9MB / fwss 693KB);迷雾瓦片三档全 200 且访客也能取。 新增的三个页面(迷雾差异 / 备份差异 / 地点补全)也各 200,备份差异页数字与之前一致(49/45/0)。
3. 渲染层(CDP,390 宽):run_all.py 扫的 14 个页面横向溢出全部为 0;
新增三页 × 两种视口也是溢出 0、零 JS 报错。
两个改进点(不是 bug):
/admin/fillup在 390 下页高 54870px —— 218 行堆叠确实很长,该加分页(每页 30 行)。/stats小字 40、/mapiOS 自放大 1,都是既有的(技能笔记里就记着),与本次改动无关。
补:补全页分页
每页 30 行。390 宽下页高 54870 → 8126。页码夹紧(?page=999 停在末页、解析不出来回第一页),
提交后留在同一页;翻页前若有改过还没提交的下拉会先 confirm(预选的建议不算改过,靠记录初始值判断)。
测试:TestClampPage(8 个边界)+ TestFillupPagination(造 35 个地点验两页与越界)。
变异验证:去掉夹紧逻辑,两个测试同时失败。
⚠️ 测试基础设施的已知脆弱点(不是代码 bug)
go test ./...(默认并行跑包)时 internal/stats 偶发 SKIP —— 它的 setup 读全局的
datasets WHERE is_active LIMIT 1,而 internal/web 的 fixture 会在导入期间临时把 is_active
指向自己的数据集、cleanup 时再删掉。两者撞上就是「没有生效数据集」或「找不到可用省份」而 skip。
单独跑 stats 包 5 个测试全过,-p 1 串行跑也不会有这问题。
以后做「完整验收」建议用 go test -p 1,或者把 stats 测试也改成自带数据集。
补:修 UI 回归(用户报的)
用户报「活动和类别变得无法手工填写」+「选不动后点用高德分析就锁死了」。根因都是我上一版改 select 引入的:
- 纯 select 把手工填写的路堵死了 —— 活动改回可输入的 input(活动名本来就是中文,没有显示问题);
类别保留 select 但末尾加「其它(自己写)…」,选中露出文本框(后端认
cat_<id>__custom)。 (当时以为「真机自己也存中文类别」所以让用户随便写中文 —— 那条判断是错的,见 2026-09-20 的更正: 真机只写过MKPOICategoryXxx,中文全是站内自己写进去的。) - 建议被强制预选 ⇒ 用户表达不了「不动」 —— 而且点「用高德分析」会整页重载, 刚选的全丢,看起来就像锁死。改成默认什么都不选,批量采纳走「采纳本页建议」按钮; 分析前把本页填过的存进 sessionStorage,回来恢复,提交时清掉。
教训:select 只适合「选项可穷举」的字段。活动是用户可自由新增的,就绝不能做成纯下拉。
顺带被测试抓到一次:分页测试数 name="cat_ 时把新加的 cat_X__custom 也算进去了(30 行变 60),
口径改成数 <select name="cat_。
补:活动与类别统一样式(用户报的)
用户说「类别和活动的选择样式好像不一样」。上一版我为了恢复手工填写,把活动做成 input+datalist、 类别做成 select+「其它」,同一行两个格子长得不一样。
现在活动也改成 select + 「其它(自己写)…」+ 文本框,与类别完全同款:
- 模板两处结构一字不差,只换
act_/cat_前缀;CSS 类合并成fill-sel(原cat-sel全部替换) - 后端
act_<id>读到__custom__就读act_<id>__custom - 删掉
datalist#fill-act;「采纳本页建议」与草稿存取改走select.fill-sel一次遍历 - 草稿恢复对 SELECT 派发
change⇒ 「其它」文本框能跟着恢复显示(否则值在、框被隐藏)
实测(CDP):390 宽下两者完全一致 229×37;1440 宽下同字号/内边距/边框,宽度由内容撑开略有差。
新增两个测试:TestFillupCustomActivity(写文本框里的名字)、TestFillupCustomActivityEmptyIsSkipped(空名不建类型)。
验收插曲:第一次串行跑出现 --- SKIP: TestMovementCleanExport —— 不是回归,是我把
ROND_CLEAN_OUT 只写在第一条命令的前缀里(前缀 env 只作用于那一条),整链跑时没导出。
改成 export 全部环境变量后重跑:12 个包全 ok,零 FAIL 零 SKIP。教训:验收链要用 export,
不要用命令前缀,否则导出验收会在完整跑里静默跳过、看着像全绿其实没跑。
提交 bf23fc6。