# 2026-09-28 ## rondbackup「差异分析 → 合并执行」实测(真机导出包 20260928_183908) 环境 `conf/app.ini`(库 `rond_with_you`、`data_dir=storage/data`、:8080),数据集 id=9; 把真机新导出的 `data/20260928_183908.rondbackup`(955 到访 / 432 地点)合并进已有 867 到访的数据集。 **主链路生效**(真跑路由 + 站长登录):`POST /admin/preview` → `GET 差异页` → `POST /admin/preview/{token}/merge`, `add_new=1`、冲突留默认 ⇒ 新增 70 地点 / 1 活动 / 134 到访,随到访带入 24 位移 / 32 天气; 到访 867→1001、地点 368→438、位移 791→815、天气 557→589;**孤儿位移 0、无地点到访 0、重复 UUID 0 组**。 逐条核对:备份里 953 条带 UUID 的到访全部命中且**到达时间微秒级零误差**;站内 2 条 app 已删的 UUID 到访被正确保留。 合并后 12 个关键页全 200。报告:`storage/.mergetest/报告-rondbackup合并实测-20260928.md` **三个问题** 1. 🟠 同名同坐标的重复地点**不幂等**:`DiffPlaces` 的 `byCoord`/`byName` 是单槽位 map + `used[]`, 同一坐标下 N 条备份地点最多只能配 2 条 ⇒ 第 3 条永远判「备份新增」,每次合并多插一条。 实测备份里同名同坐标共 10 组、只有「柳州站」count=3 ⇒ **每次合并固定多 1 个重复地点**,差异永不归零 (438→439→…)。修法:多值 map 逐条消费 + `mergePlaces` INSERT 前按 `PlaceCoordKey` 去重兜底。 2. 🟠 到访冲突被「活动改名」放大:`visitFieldChanges` 比的是**活动名称**不是身份;16 个活动改名 (餐厅→吃饭…)⇒ 355 条到访冲突全部同因(库内挂在改名活动下的 378 条 − 站内独有 23 条 = 355,精确吻合), 活动类型本身只报 16 条。且对到访选「用实机」**不会**改活动名(`activity_id` 按 UUID 映射回同一个) ⇒ 冲突永不收敛。修法:到访层面按活动身份(`ZUID_`/id)比对,改名只在活动层报一次。 3. 🟡 21 个**长沙**地点两边坐标差 661~748m,实测 20 条满足「站内坐标 == 反纠偏(备份坐标)」 ⇒ 坐标系不一致(站内那侧被多做了一次 GCJ→WGS 反向换算);**差异页冲突的默认选项是「用实机」**, 直接点合并会把这批坐标覆盖、地图上跳 ~660m。另有 分类 12 处(站内中文标签 vs 备份 `MKPOICategory*` 原始枚举)。 **备份 / 回滚**:`storage/.mergetest/rond_with_you-before-20260928.sql`(整库 pg_dump); 差异页快照 `diff-before-merge.html` / `diff-after-merge.html`。库内最终状态 = 合并一次(我重复跑测试 多插的 1 个重复地点已回退)。 **方法(可复用)**:临时 Go 测试(`package web`,才能拿到非导出的 `srv.captcha`)—— `httptest.NewServer(srv.Handler())` + cookie jar + **`CheckRedirect: http.ErrUseLastResponse`** (站点用 303,必须关掉自动跟随才能读到 Location);验证码答案直接读 `captcha.Store.Answer(id)`, 绕开图像识别;管理员口令是默认 `rond-admin`(pbkdf2 可离线核对)。跑完删掉测试文件,`git status` 保持干净。 ## 同日晚:修复 + 复测(回档数据库后重跑) 按上面前两条问题改了代码,**5 个文件 +172/−22**: 1. `internal/rond/diff.go` `DiffPlaces`:`byCoord`/`byName` 由单槽位 map 改为**多值索引**(`map[string][]StoredPlace`), 抽出 `take()` 逐条认领未使用的候选(认领即置 `used`)。`classify` 不再自己置 used。 2. `internal/rond/diff.go` `visitFieldChanges`:到访的「活动」改为**按身份**比(新增参数 `actUUID`), 两边都缺 UUID 才退回名称;`DiffVisits` 传入 `b.ActivityUUID`。`StoredVisit` 新增 `ActivityUUID`。 3. `internal/web/backup_diff.go` `loadStoredVisits`:多 JOIN 一次 `entity_raw`(`entity='ZACTIVITY'`,按 `a.src_pk`)取 `ZUID_`。 **复测结果**(回档 → 重跑同一份备份,同环境):到访冲突 **253 → 6**(247 条活动改名假冲突消失); 地点新增 **70 → 67**,合并后再比对 **地点新增 1 → 0**;再点合并由「再插 1 个重复地点」变成 「**按当前的选择没有要写入的改动,数据未变**」,地点 435→435、「柳州站」稳定 3 条。 到访 1001 / 位移 815 / 天气 589 与修复前一致;953 条 UUID 到访全命中、到达时间 0 偏差、孤儿位移 0。 **新增守护**:`TestDiffPlacesAlignsDuplicateNameCoordGroups`(含「站内真少两条仍报新增 2」反向断言)、 `TestDiffVisitsIgnoresActivityRename`、`TestDiffVisitsReportsActivityChange`、 `TestDiffVisitsActivityWithoutUUIDFallsBackToName`;`TestBackupDiffIdentical` 补地点新增断言、 两个 merge 幂等用例补「地点差异归零」。**变异验证**:把两处改回旧逻辑,对应用例确实全挂 (⚠️ 变异要忠于旧行为:旧 `byCoord` 是 map「后者覆盖前者」=取最后一条;我第一次写成「取第一条」, 误报 8 条,与真实旧行为不符,改用 `byCoord[k] = []StoredPlace{sp}` + `byName` 只留第一条才复现出正确的 1 条)。 **仍未动**:21~24 个长沙地点坐标系差 ~660m(默认「用实机」会覆盖站内;现连带 25 条到访「地点」冲突)、 分类 12 处中文标签 vs `MKPOICategory*` 枚举、16 个活动改名需选「用实机」才同步。 报告已更新为「实测 → 修复 → 复测」完整版。 ## 收尾:回滚 + 推送 + 重建(19:35) - **回滚**:`DROP/CREATE rond_with_you` + 恢复 `storage/.mergetest/rond_with_you-before-20260928.sql`, 核对回到合并前基线 867 到访 / 368 地点 / 31 活动 / 791 位移 / 557 天气 / 2538 留档 / 柳州站 3。 - **推送**:提交 `10d23e0`(5 文件 +172/−22),`git push origin master`(远端 `git.shiroko.one`)。 本地原本就**领先远端 8 个提交**,一并推上去了(`78865fc..10d23e0`)。 ⚠️ **推送凭据陷阱**:`~/.git-credentials`(store)与 GCM 里缓存的 token **都已失效** (`remote: Failed to authenticate user`),SSH 22 端口 `Network is unreachable`(只有 HTTP 代理 `http.proxy=http://100.104.48.63:11882` 能通)。**别加 `GIT_TERMINAL_PROMPT=0`**——它会让 GCM 直接放弃认证;去掉这个变量、让 GCM 自己走一遍交互认证即可成功(13 秒完成)。 - **重建**:`go build -o bin/rond.exe .`(`bin/rond.exe` 原本还是 9-20 的旧构建,不含修复; 旧文件留了副本 `bin/rond.exe.bak-20260920`)。只读冒烟:`/healthz` `/` `/admin/login` 均 200、 `/admin` 303,随后 `taskkill //F //IM rond.exe` 停掉、8080 释放,库内计数不变。 - 注意:`go build` 那次 `bin/rond.exe~`(9-20 12:14 的更旧副本)不见了;原因未查明, 两个都是修复前的旧产物,无实际损失。