用户要求这份记忆跟着仓库走,而不是只留在本机。 .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`)。
627 lines
49 KiB
Markdown
627 lines
49 KiB
Markdown
# 2026-09-17
|
||
|
||
## 迷雾遮罩:生效时机、重叠语义、访客默认页(提问)回复
|
||
|
||
用户三问:①遮罩生效需要时间吗 ②重叠遮罩有效吗 ③能否设置访客默认页面。
|
||
|
||
**① 生效时机:服务端即时,访客端原来最长 1 小时**
|
||
`handleFogTile` 每个请求都 `LoadMasks` 实时读库(遮罩**不在** `fogIndex` 的 sync.Once 缓存里),
|
||
所以保存/删除后下一次请求即生效、无需重启。**但**访客瓦片是 `public, max-age=3600`,
|
||
而瓦片 URL 的 `?v=` 来自 `ix.Version()`(只跟**迷雾数据**走)→ 设完遮罩 URL 不变,
|
||
访客浏览器/CDN 继续用旧瓦片,看起来像「遮罩没生效」。
|
||
修:新增 `fog.MaskFingerprint(masks)`(FNV-1a 64,按 id|lat|lon|radius 求哈希),
|
||
`mapConfig` 的 `fog.ver` 变成 `"<ix.Version()>-<指纹>"`(string)。遮罩一变 URL 就变,缓存自然失效。
|
||
实测:无遮罩 `1789578442-0` → 加一块 `…-7e72b20aa455d2f9` → 再加一块 `…-5cc45ddb03dc0230` → 删光回到 `…-0` ✓
|
||
|
||
**② 重叠遮罩:渲染有效,但统计原来会重复扣数(已修)**
|
||
- 渲染/判定本来就是并集:`MaskSet.Hit` 遍历所有圆、任一命中即隐藏 ✓,重叠无副作用。
|
||
- **但 `MaskedCells` 是「逐遮罩累加命中格数」**——重叠区域被统计多遍,`cov.Cells` 扣多
|
||
(面积走的是另一套逐格算法 `AreaKm2Excluding`,于是两个数字互相矛盾)。
|
||
修:改成**逐格判一次 `Hit`** + 用 `done` 集合对「遮罩包围盒覆盖到的块」去重。
|
||
- 新增 `internal/fog/mask_test.go`:
|
||
`TestMaskedCellsUnion`(重叠时 == 暴力并集)、`TestMaskedAreaMatchesCells`(面积差换算成格数与 MaskedCells 一致,1e-6 m²)、
|
||
`TestMaskFingerprint`(增删改都会换指纹)。
|
||
**验证测试有效**:退回旧算法 → 303019 vs 并集 189470(多扣 60%)→ FAIL ✓
|
||
- 实测(测试库 8701 块迷雾):无遮罩访客=站长;一块 2000m 遮罩 → 访客像素 77959→71689、格数 575753→571692,站长不变;
|
||
再加一块**完全相同**的遮罩 → 访客数字**一点不变**(旧实现会扣两倍 8122 格)✓;删光后双方复原 ✓
|
||
|
||
**③ 访客默认页面:目前没有这个功能**
|
||
`/` 固定走 `home`;页面级控制只有 `setting.AllPages`(map/places/timeline/stats/report 的可见性开关)。
|
||
已在回复里给方案让用户选(后台下拉 / .ini 配置 / 不做)。
|
||
|
||
## 新增「访客默认页面」(后台设置下拉)
|
||
|
||
需求确认后按「后台设置加下拉」实现:
|
||
- `setting.Settings.VisitorHome`(json `visitor_home`,缺键/非法值一律回落 `VisitorHomeMain="home"`),
|
||
`VisitorHomePath()` 只在**目标页已对访客开放**时返回 `/<key>`,否则返回空串不跳。
|
||
收敛用 `NormalizeVisitorHome()`(表单被改坏时回落主页)。
|
||
- `home` handler:在「没导入数据」分支之后,`!IsAdmin && VisitorHomePath()!=""` → 302。
|
||
刻意不跳的三种情况:站长自己访问、没配、目标页没开放;另外**没数据时不跳**
|
||
(让访客留在主页看引导,比落到空地图有用)。
|
||
- 后台「公开页面」卡片下方加下拉(主页 + AllPages 五项)。
|
||
- 新增 `internal/setting/setting_test.go`:纯逻辑用例 + `TestVisitorHomeStore`(ROND_DSN 门控,
|
||
**库名不含 test 时自动跳过**,避免动到正式站的 settings;用例内先备份原值、defer 还原)。
|
||
|
||
实测(测试库 HTTP):
|
||
| 场景 | 访客 / | 站长 / |
|
||
|---|---|---|
|
||
| 默认(home) | 200 主页 | 200 主页 |
|
||
| 设为 map(页面全开) | **302 → /map** | 200 主页(不受影响) |
|
||
| 设为 map 但 map 未对访客开放 | 200 不跳(/map 本身 404) | 200 |
|
||
| 库里被塞非法值 `//evil.com` | 200 不跳 | 200 |
|
||
CDP 复核下拉:6 个选项、正确回填(map 时选中「足迹地图」)、无 JS 报错。
|
||
生产库没有这个键 → 缺键回落默认,访客行为不变 ✓
|
||
|
||
## 到访热力无效 + CPU/内存优化 + 点地图跳 /map(+ 回归)
|
||
|
||
### 1. 「到访热力」无效:热力图恒为空
|
||
`full()` 里 `var points = opts.points || []`,而地图页首屏是 `full({points: []})`(地点随后走
|
||
`/api/points` 拉回),`setPoints` 只写了 `st.points`,**没同步闭包里的 `points`**;
|
||
`buildHeat` 用的正是闭包那个 → 恒为空数组。
|
||
修:`buildHeat` 改用 `st.points`,并让 `setPoints` 同时写 `points = st.points = pts`
|
||
(与 `preview()` 里已有的写法一致)。
|
||
**实测(awaitPromise 探针 + canvas 像素统计)**:旧写法画布在但**非透明像素 0**;
|
||
修复后 59258 像素(生产站访客视角 51049)✓ 这就是「勾了没反应」。
|
||
|
||
### 2. 点击地图跳 /map:改成按需启用
|
||
`preview()` 里硬编码 `map.on('click', () => location.href='/map')`,而 preview 被 4 处复用:
|
||
主页(要)、地点详情页(不想要,上面本来就有「在地图中查看」按钮)、后台照片页 2 处
|
||
(**最不该有,点一下就被甩出后台**)。
|
||
修:加 `opts.clickToFull`,只有主页传 true。实测:主页 `home-map` 有 1 个 click 监听;
|
||
地点页 `place-map` 为 **0** ✓
|
||
|
||
### 3. CPU / 内存优化(先测量后动手)
|
||
**基线**:`/stats` 379ms(访客)/ 505ms(站长),fog 瓦片 7.9ms,其余 <50ms。
|
||
**根因(测出来的,不是猜的)**:
|
||
- 单个统计查询只要 **2–3ms**(psql 也是 3ms),但页面 379ms → 逐段计时日志显示
|
||
`weather` 任务 400–700ms。测出**建一条 PG 连接要 307ms**(psql 建连同样 0.55–0.7s)——
|
||
Windows 上新建 backend 进程就是这么贵。
|
||
- 池配置 `MaxOpenConns(12)` + **`MaxIdleConns(4)`**:串行页面只需 1 条连接(首次付 300ms,
|
||
之后复用),**并发超过 4 条时反复重建连接**,于是「并行反而更慢」。
|
||
- 另外 `fogCoverage` 每次要 `ix.All()` **三次**,而 `All()` 会复制 10542 个块并**排序**。
|
||
|
||
**改动**:
|
||
1. `internal/db/db.go`:`MaxIdleConns` 提到与 `MaxOpenConns` 相同(8/8),
|
||
新增 `WarmUp(d, 6)` 在启动时并发预建连接(main.go 调用)。
|
||
2. `internal/web/parallel.go`:`runJobs` 并发执行页面里互不依赖的查询(上限 6),
|
||
debug 下打印 >30ms 的慢查询(**这条日志就是定位到建连问题的关键**);statsPage 的 13 个查询改用它。
|
||
3. `internal/fog`:`Index` 预存 `areaM2`(写入索引时顺手算),新增 `Index.AreaKm2()`;
|
||
`maskedStats`/`Index.MaskedStats` 直接在索引上按遮罩包围盒算(不再复制整张块表),
|
||
删掉旧的 `MaskedCells` 与 `AreaKm2Excluding`。
|
||
4. `internal/web/mask_cache.go`:缓存遮罩列表 + 指纹(30s TTL,保存/删除时**主动失效**),
|
||
瓦片渲染结果也进 `fogTiles` 缓存(key 含 crs 与遮罩指纹,站长用 "-")。
|
||
|
||
**结果**:`/stats` **379→38ms / 505→29ms**;fog 瓦片 **7.9→1.8ms**;
|
||
rond.exe RSS 42–44MB(压测 200 次请求后不涨,无泄漏);
|
||
单次 `/stats` 约 15.6ms CPU。剩下的 2 条慢查询是启动后第一次请求的冷连接。
|
||
|
||
### 4. 回归检查又发现两处区域限制遗漏(同「轨迹」那类)
|
||
`Overview` 的**总里程**与 `TransportStats`(出行方式)都重建了 `where` 而**丢掉 `f.Regions`**
|
||
→ 访客看到的里程/出行里程里含黑名单区域。新增 `Filter.movementsWhere()` 统一(按两端的
|
||
`places` 判定,黑白名单语义与 `visitsWhere` 一致),两处都换过去。
|
||
实测生产站:访客里程 **16861.9 → 4678.8 km**,站长不变 ✓
|
||
新增 `internal/stats/stats_test.go`(ROND_DSN 门控、**只读**):
|
||
与手工 SQL 逐项对账 + 区域过滤对 visits/movements 双向生效 + 时间范围仍有效。
|
||
**验证测试有效**:退回旧写法 → 立刻报「白名单 广西壮族自治区 后里程没减少」✓
|
||
|
||
其余巡检:8 个公开页 + 7 个后台页全部 200;遮罩保存/删除**立即生效**(28ms 内访客数字与瓦片
|
||
同步变化,站长不受影响);全量测试通过。
|
||
|
||
### 主实例「退出」事件排查:不是崩溃,是端口被另一个进程接管
|
||
|
||
收到后台任务完成通知(我启动的 8080 实例结束),排查结论:
|
||
- **事件日志没有任何 rond 崩溃记录**(Application 日志里 Application Error / WER 均无);
|
||
rond 自己的日志也没有 panic → 是**被正常终止**,不是崩溃。
|
||
- 进程清单(Get-CimInstance Win32_Process)给出了真相:
|
||
| PID | 启动时间 | 命令行 |
|
||
|---|---|---|
|
||
| 11992 | 01:33:08 | `bin\rond.exe -c conf/app-test.ini`(我起的 8081 测试实例) |
|
||
| 14556 | **02:36:42** | `bin\rond.exe -c conf/app.ini` —— **没有日志重定向** |
|
||
- 日志里的 `02:36:46 已启动` + `02:36:47 服务异常退出: listen tcp :8080: bind ...`
|
||
是**我的重启尝试**:它在 02:36:42 那个进程占住端口后才启动,于是 bind 失败退出。
|
||
(main.go 是先打印「已启动」再 ListenAndServe,所以会看到这两行连在一起。)
|
||
- 结论:**用户在 02:36 自己重启了服务**(命令行没有 `tee` 重定向,所以它的日志不落在
|
||
storage/rond-main.log 里,这正是「日志里看不到我这次请求」的原因)。我停止抢占 8080。
|
||
- 已验证当前实例是**最新构建**(`/api/map-config` 的 `fog.ver` 带 `-0` 遮罩指纹后缀,
|
||
这是本轮才有的),/stats 实测 59ms、8081 对照 45ms(整机当时有浏览器等负载,属正常波动)。
|
||
|
||
**运维提示(值得记住)**:`main.go` 先打印「足迹站点已启动」再真正 ListenAndServe,
|
||
所以端口被占时会出现「已启动」+「服务异常退出: bind」两行连在一起 —— 看到这对组合
|
||
就是**有另一个实例抢着占了端口**,而不是启动成功后又崩了。
|
||
|
||
---
|
||
|
||
## 出行(ZMOVEMENT)后台增改删 + 导出保真(本轮新增功能)
|
||
|
||
需求:编辑页能对「出行区间的方式 / 时长 / 时间」做新增、修改、删除,且导回 rond 正常可用。
|
||
|
||
**先反推真机语义**(拿 `data/20260914_151649.rondbackup` 逐列实测,脚本跑完即删):
|
||
- `ZMOVEMENT` 11 列,`ZTYPE_` **791/791 非空**:2=无交通方式(中位速度 5.0km/h≈步行)、
|
||
5=有交通方式(中位 17km/h),另有 23 条 4、1 条 0 → **不能写 NULL**,站点只给新行补 2/5。
|
||
- 时间窗口硬不变量:`ZSTART_ >= 起点到访离开`、`ZEND_ <= 终点到访到达`(760/756 条 0 违例)。
|
||
- `ZTRANSPORT` 是全局表,站点侧从位移聚合;`ZVISITFROM_/ZVISITTO_` 真机就有 35 条为空。
|
||
|
||
**改动**(`internal/rond/export.go`、`internal/web/handlers_edit.go`、`view.go`、`server.go`、
|
||
`templates/admin/edit.html`、`static/css/app.css`):
|
||
1. 编辑页新增「行程记录」卡片:搜索(地点/方式/日期/#编号)+ 分页 + 行内改(起止到访、
|
||
时间区间、方式)+ 删除;「新增行程」表单重做(时间留空自动按两端到访推断、方式可内联新建)。
|
||
2. 落库补齐派生字段:`from_place_id`/`to_place_id`(**区域黑名单按两端 places 判定,留空会绕过过滤**)、
|
||
`distance_km`、`duration_min`,与导入路径同源。
|
||
3. 编号复用防护 `nextSrcPK`(取库内/coredata_meta/留档三者上限 +1)。
|
||
4. 导出侧:`ZMOVEMENT.ZTYPE_` 进 `keepNilCols` + 新行补值;`queryTransports` 改 `GROUP BY`
|
||
(原来 `DISTINCT` 会在同名冲突时产出两个同 `Z_PK` → 撞主键让整次导出失败);
|
||
`ZTRANSPORT` 的名称/颜色/图标「空则保留基线」+ `keepMissing`(没有行程引用的方式不再被删)。
|
||
|
||
**验证(可复现,全部实测)**
|
||
- 新增 `internal/web/movement_e2e_test.go`:真导入真机包 → 调真实处理函数 → 导出 → 读导出 SQLite 核对。
|
||
门控 `ROND_TEST_CONF` + `ROND_SRC`,在测试库建一次性账号、跑完连数据集一起删。
|
||
实测:删基线最大位移 #791 后新建拿到 #792(防复用生效);新行 `ZTYPE_`=5/2、`ZTRANSPORT` 指向新方式、
|
||
原名保真;`tools/verify_rondbackup.py` 比对 → **ZMOVEMENT/ZTRANSPORT valuediff=0**,只有我动过的行在差。
|
||
- 纯净往返(不增删改)对照组:新旧代码的比对输出**逐行相同**(总差异 3,全是 `ZLOCATION.
|
||
ZLASTARRIVALDATE_` 的「站点重算最近活跃时间」,属既有行为)→ 证明改动没波及别处。
|
||
- 项目自带断言全过:`TestExportRoundtrip`(坐标 0 偏差、时间 0 偏差)、`TestBaselineKeepsCoreData`、
|
||
`TestProgramRowsFillNativeColumns`(新行 2 条 `ZTYPE_` 全有值)、新加 `TestMovementTypeNeverNull`。
|
||
- 无头 Chrome(`rond-web-verify` 技能)实测编辑页:**0 JS 报错**,6 张卡片顺序正确,40 行行内表单、
|
||
41 个方式下拉、165 个 flatpickr 全初始化;方式联动(选已有→预填名称/颜色/图标、新建→清空、
|
||
无→禁用)与时长实时提示(「时长 1 小时 35 分钟」/「结束时间必须晚于开始时间」)均按预期。
|
||
- 移动端:发现行程行高 206px(到访行 115px),把时间压成一行 + 窄屏隐藏城市名后降到 165px
|
||
(卡片 10607→7332px),320/390 无横向溢出;桌面 61px vs 到访 57px,基本齐平。
|
||
|
||
**注意**:8080 主实例已用新构建重启(`bin/rond.exe~` 是改动前的旧二进制,可回滚)。
|
||
|
||
### 反馈三点:改方式不生效(真 bug)、卡片折叠、筛选改选项框
|
||
|
||
**1) 「改方式保存不了」的根因(我上一轮引入的)**
|
||
页面时间输入只到分钟,回填后原样提交 → 服务端把 `20:03:21.323` 截成 `20:03:00`:
|
||
① 顺带改掉了时间(数据漂移);② 截断值早于起点到访的离开时间(`20:03:21`),被上一轮
|
||
新加的时间窗口校验直接拒掉。用户那行是 **movement #385(09-13 20:03→20:31,电🐔)**,
|
||
起点到访 #403 的离开时间正是 `20:03:21.323088`——卡在秒上。
|
||
修法:**与库里同分钟就沿用库里原值**(`keepStoredTime`;行程走 `resolveMovementEnd`),
|
||
手填越界仍给硬校验(带 1 分钟取整余量)。到访的 `editVisitUpdate` 是同一类病,一起修了
|
||
(改备注也会磨掉 `ZARRIVALDATE_` 的秒)。回归用例:
|
||
`TestKeepStoredTime`、`TestMovementWindowKeepsStoredSeconds`,E2E 的「只改方式」用例
|
||
专门挑「截到分钟就会越界」的行,并断言导出包里 `ZSTART_/ZEND_` 与原始包逐位一致。
|
||
|
||
**2) 卡片折叠**:编辑页 6 张卡片各加 `data-card` 键 + 标题右侧箭头(标题整行可点),
|
||
状态记 localStorage,页头加「全部折叠/展开」。选点地图做了延迟初始化
|
||
(折叠着就不建,展开时经 `window.__pickerMapInit` 补建)——否则 Leaflet 在
|
||
`display:none` 容器里初始化会拿到 0 尺寸。
|
||
|
||
**3) 行程筛选**:把手输关键词换成三个选项框——交通方式(全部/无方式/各方式含段数)、
|
||
地点(起或止,取前 300 个地点)、开始时间从/到(date)。分页链接改由 Go 生成
|
||
(`EditData.PageLink` 返回 `template.URL`)——直接拼在 href 里会被转义成 `mt%3d4`,
|
||
翻页会静默丢筛选。
|
||
|
||
**实测**:`mt=4`→332 段(与下拉里的「332 段」一致)、`mt=none`→390 段(= 791−401 ✓)、
|
||
日期区间 2026-09-01~30→37 段;`?mt=4&mpage=2` 第 2/9 页且选中态保持;折叠/展开/全部折叠
|
||
与 localStorage 读取均正常;桌面筛选行底边极差 0(按钮不歪)、390/320 无横向溢出;
|
||
全程 0 JS 报错。8080 已用最新构建重启。
|
||
|
||
### 再反馈:默认折叠 + 选择器收不回/被覆盖
|
||
|
||
**默认折叠**:折叠脚本的默认值从「未记录=展开」翻成「未记录=折叠」(`state[k] !== false`),
|
||
只有明确记过展开的卡片才展开。实测 6/6 折叠、页面高度 = 视口高(不再一屏拉不到底)。
|
||
选点地图的延迟初始化因此成了常态路径:折叠时 `startMap()` 不执行、也不建 Leaflet,
|
||
展开时经 `window.__pickerMapInit` 补建(实测展开瞬间就发出 `/api/map-config` 请求)。
|
||
|
||
**选择器「只能选不能收回 + 被其他元素覆盖」**——量出来的根因是 CSS 不是 z-index:
|
||
`.picker-results` 有 `max-height:200px` 但 `overflow:visible`(flex 子项不会因上限收缩),
|
||
30 条候选实际溢出 **1020px**,溢出部分被后面 DOM 里的表单元素压住
|
||
(`elementFromPoint` 在框下方 20px 处拿到的是 `BUTTON.geo-item`,即「看得见点不着」)。
|
||
修法:`.picker-results{overflow-y:auto}` → 200px 内滚(框下方 20px 处已变回 `DIV.row`)。
|
||
交互上补:点空白处 / Esc / 失焦都收回候选列表;行内编辑表单里隐藏域已有值时
|
||
直接渲染成「已选: #173 2026-09-13 20:40 家 + 重选」,用户能看到当前值、也能清空重选
|
||
(原来预填了隐藏域却不显示,看着像「选了就收不回」)。
|
||
|
||
### 地点坐标换算(有些点没做过 GCJ-02 偏移)
|
||
|
||
需求:地点列表每个点能就地做坐标系换算,并能看到前后变动,不用删了重填。
|
||
|
||
**先反推判据**(实测 `data/20260914_151649.rondbackup` 的 323 个地点):
|
||
`gcj(raw)==lat` **305** 个(正常)、`raw` 与 `lat` **一字不差 18 个**(= 当初没偏移,显示坐标
|
||
其实还是 WGS-84,偏 500~540m)、其他 0 个。⇒ 「哪个点没偏移」可自动判定:`raw == lat`
|
||
且在中国外接矩形内(境外的点本就不该偏移,必须排除,否则误报)。
|
||
顺带纠正了记忆里「293/323」的旧数字(实测 305)。
|
||
|
||
**实现**:`GET /api/coords?mode=wgs2gcj|gcj2wgs`(站长限定,geo 包是唯一实现,实测
|
||
21.428836,109.116822 → 21.426460,109.121113 / 517m 往东南,与离线 Python 分析逐位一致);
|
||
编辑页每行加「坐标系换算」下拉 + 前后对照行(`原 → 新(移动 N 米 · 往X)`),
|
||
换算前先记 base,来回切方向不会越换越偏;地点列表加「疑似 WGS-84」橙标 + 「坐标系」筛选
|
||
(实测生产站正好筛出 18 个)+ 「比导入时移动了 N 米」;保存后的提示带上移动距离与方位。
|
||
「导入时的原值」取自 entity_raw 留档里的 ZLATITUDE/ZLONGITUDE,所以改动后能一直对照。
|
||
|
||
**实测(端到端)**:换算 亚平村二巷 #33 后,导出包里 `ZRAWLATITUDE/ZRAWLONGITUDE` **一字未动**、
|
||
只有 `ZLATITUDE/ZLONGITUDE` 变了,且 `gcj(原始GPS) == 显示坐标` 重新成立(rond 的坐标模型)。
|
||
`verify_rondbackup.py` 差异明细:`PK 33 differs: [('ZLATITUDE', 21.428836, 21.42646),
|
||
('ZLONGITUDE', 109.116822, 109.121113)]` —— 正是要动的那两列。
|
||
|
||
另外给验证脚本 `site_cdp.py` 加了 `--await`(async 表达式等 resolve 再取值),
|
||
这次要验 fetch 驱动的换算界面才发现原来不支持;SKILL.md 补了用法。
|
||
|
||
### 站长给的实例:IMG_20260418_181404.jpg 的坐标到底对不对
|
||
|
||
站长截图给了「源图 GPS」,我读不了图,但照片本体在 `Downloads/temp2/` 找到了 —— 直接读 EXIF:
|
||
`22°51'11.58"N 113°15'11.04"E` = **22.853216, 113.253067(与站点里存的完全相同)**,
|
||
相机 HUAWEI Mate 80 Pro Max。结论:**导入时没做偏移**(导入页默认会转,所以是当时选了
|
||
「已经是 GCJ-02」)。EXIF 按标准就是 WGS-84 → 这个点在 GCJ 底图上偏了 **626m**。
|
||
|
||
**怎么裁决**(不靠猜):站长说手机地图上显示在「银星雅苑西北门/柏景楼二期/翠庭轩/咖研社
|
||
这个十字路口」。用这些地物名走 `/api/geocode` 取坐标再比距离:
|
||
- 银星雅苑 22.849159,113.258707 → 距现状 733m、距换算后的点 **144m**
|
||
- 翠庭轩 22.850376,113.259255 → 距现状 708m、距换算后的点 **91m**
|
||
⇒ 手机是对的,站点偏了。已按真实保存路径换算:**22.853216,113.253067 → 22.850420,113.258373**,
|
||
提示「坐标移动到 22.850420, 113.258373(626 米,东南方向)」,复测新位置距地物 91~144m。
|
||
|
||
**补了回归断言**(`movement_e2e_test.go` 5d):站点新建(= 照片导入同类)的地点换算后,
|
||
导出包必须 `ZRAWLATITUDE=22.853216,113.253067`(换算前输入值)+ `ZLATITUDE=22.850420,113.258373`
|
||
(换算后),且 `gcj(原始GPS)==显示坐标`。这条覆盖了「新行 ZRAW 由显示坐标反算」的分支。
|
||
|
||
站长选了**先不补**「照片导入的坐标来源」记录 → 这类点仍然不在「疑似 WGS-84」判据里,
|
||
只能按上头的「地物名裁决」人工核。这条限制已写进 MEMORY.md。
|
||
|
||
### 生产环境「这两天导入的照片全是错坐标」——排查与补救清单
|
||
|
||
站长在生产(life.shiroko.one)导了一批照片,都是按「已经是 GCJ-02」导的 → 坐标没转换。
|
||
|
||
**排查手段(不登录也能做)**:公开 `GET /api/points` 带 `sp`(=src_pk) 与全精度经纬度。
|
||
生产 281 个可见点里:195 个长小数全部 sp≤323(= rond 导入基线),85 个 ≤6 位小数全部 sp>323
|
||
(= 站点新建,即照片导入)。**站长从没手工点选新增过地点**,所以这 85 个就是全量受影响面。
|
||
(生产中还有被区域黑名单/迷雾遮罩挡住的点,完整清单要登录后取。)
|
||
|
||
**判定命中**:拿仓库 `data/images/` 的 51 张长沙原图 EXIF 对账生产数据,24 个可判定的点
|
||
**全部**是 `stored == round(exif,6)`(没转换),**0 个**是 `gcj(exif)`。本地环境同样 24/24。
|
||
偏移 351~673m,方向南/东南(随纬度变化)。清单:`tmpfog/photo-coord-fix-preview.csv`。
|
||
|
||
**关键坑**:不能靠库内字段自证——逆地理用的是同一个错坐标,省市区/街道/名称自洽;
|
||
`places`/`visits` 无 created_at,也没记录导入时的坐标系选择。只有原图 EXIF 能定罪。
|
||
|
||
**生产环境事实**:跑的是 v7(`/api/coords` 在、`app.css` 含 `.tag.warn`),所以现成接口能用;
|
||
只走 IPv4(curl 必须 `-4`,IPv6 超时);登录有验证码,cookie 形如 `uid|token`(HMAC,不绑
|
||
IP/UA),站长复制过来即可用 curl 操作。
|
||
|
||
待站长定:会话怎么给 / 照片目录是否提供(把 61 个未匹配的点也逐张核实)/ 是否补「导入来源」记录。
|
||
|
||
### 照片错坐标:86 个站点新建地点的最终裁定(含判据标定)
|
||
|
||
对生产 86 个 `sp>323` 的点做了三路证据对账,结论 **63 修 / 23 不动**。
|
||
|
||
**判据 A(铁证)· 47 个**:库内坐标与某张原图 EXIF **六位小数逐位相等** → 当时没换算。
|
||
长沙 24(sp344-367)+ 佛山 23(sp387-409)。原图在本机 Downloads/桌面等 4 个目录下。
|
||
|
||
**判据 B(列签名)· 16 个**:`places.country_code='CN'` 且 `timezone='Asia/Shanghai'`。
|
||
机制:`photos.html` 的 `tInput(name,value,...)` 把 country/timezone 写成**表单默认值**,
|
||
而 `edit.html` 的同名字段只有 placeholder(空 value),且 `search()` 只回填
|
||
name/province/city/district/坐标,**不回填** country/timezone/街道。
|
||
标定(60 个已知答案):47 个照片导入**全部**命中;13 个按名搜索只有 1 个例外
|
||
(324 昆明站,坐标与 POI 完全一致、且与 325 是重复点 → 归为「不动」)。→ 判据可信。
|
||
命中的是荔波 368-383(荔波原图不在本机,无法用 A 复核)。
|
||
|
||
**判据 C(不动)· 23 个**:昆明 2 + 大理 18 + 荔波 384/385 + 南宁 386。创建于「新增地点」页,
|
||
坐标来自高德 POI 或地图点选,本来就是 GCJ-02。
|
||
|
||
**被否证的两条判据(别再捡起来)**:
|
||
- **「按名搜索同名 POI」的对称距离判据不可用**。在 47 个已知「需修」点上标定:
|
||
只有 17/31 有候选的点判对(55%),**14 个把需修的点判成「正确」**(如 sp409 柠季
|
||
same=16.7/fix=630 → 判「正确」但实际是 EXIF 原值);16 个连候选都搜不到。
|
||
只有 `same<=10m`(坐标就是 POI 坐标本身)这一档是可信的——47 个需修点里**没有一个**
|
||
落在该档。即「same≈0 ⇒ 按名搜索」成立,其余档位一律不可信。
|
||
- **迷雾栅格不可用**。荔波 16 个点里 12 个两侧都是 0%;大理更是出现与已知正确点
|
||
(sp333 喜洲古镇北区停车场,same=0.0)**相反**的结果(C=0%、gcj(C)=23%)。作废。
|
||
|
||
**其他相关事实**:
|
||
- 导出的 `ZISPHOTOIMPORT` 恒为 0(站点导出不给新建行补这个值),所以 rond 里分不出
|
||
「照片导入」与「手工新增」——这是继「坐标来源」之后第二个该补的缺口。
|
||
- `editPlaceUpdate` 的 UPDATE 不含 `thoroughfare`,所以「只改坐标」不会动到街道字段(好消息);
|
||
但省/市/区/街道是按**错坐标**反查来的,改完坐标后会与新位置差约 600m(多为同区,可接受)。
|
||
- 生产备份里 `ZVISIT.ZUSERADDED` 85 个新建到访**全部为 1**,`ZISPHOTOIMPORT` 全 0。
|
||
|
||
产物:`tmpfog/fix-list-final.csv`(63 行)、`tmpfog/keep-list-final.csv`(23 行)、
|
||
`tmpfog/fix-review.html`(前后对照表)、`tmpfog/live-preview.csv`(对生产只读试运行,
|
||
已验证会话有效、线上值与备份一致)。写库走 `tools/photo_coord_audit.py --apply`,
|
||
换算调服务端 `/api/coords?mode=wgs2gcj`(与错导入正好互逆)。**尚未写库,等站长确认范围**。
|
||
|
||
### 已执行:63 个坐标全部换算回 GCJ-02(生产)
|
||
|
||
站长选「全部 63 个 / 只改坐标」。用 `tools/photo_coord_audit.py --include-unverified --only <63> --apply`
|
||
写库(换算走服务端 `/api/coords?mode=wgs2gcj`):**成功 63 / 失败 0**。随后重新拉 `/api/points` 逐点复核,
|
||
`63 一致 / 0 不符 / 最大偏差 0.00m`,23 个「本来正确」的点坐标一字未动。
|
||
|
||
**执行后才发现并解决的重大疑点(差点误判成「改错了」)**:用同名高德 POI 当标尺时,有 6 个点显示
|
||
「旧值离 POI 只有 17~78m」→ 像是在说旧值本来就是 GCJ。**真相是循环论证**:这些点的**名称本身就是
|
||
「用那个错坐标做逆地理」生成的**(`/api/regeo` 会把最近的 POI 名回填到名称框),所以名称必然贴着旧坐标。
|
||
用 `/api/regeo` 在旧坐标上重跑一遍即可判定:名称出现在反查结果里 ⇒ 名称无证据力。
|
||
逐点跑完:**63 个里 13 个名称是反查来的、50 个是站长自己命名的**。
|
||
- 6 个「疑似改错」的点(349 天心阁、364、372、387 钟楼公园、394 顺德体育中心、409 柠季)**全部**落在
|
||
「反查来的」这 13 个里 → 全是伪证。
|
||
- 而「修后离同名 POI 4~39m」的(348 茶颜悦色、352 长沙南站、354 肉丸店、390 萨莉亚、407 大地醉鹅、
|
||
375 小七孔古桥…)**全部**落在「站长自己命名」那 50 个里 —— 站长按自己当时所在命名,新坐标落在同名
|
||
地物上,这才是有效证据。两组完全互斥,逻辑自洽。
|
||
|
||
**EXIF 基准的决定性证明(回答「照片 EXIF 到底是不是 WGS-84」)**:13 个按名搜索的点,坐标与高德 POI
|
||
六位小数完全一致 ⇒ 它们是已知 GCJ 真值。拿大理原图去对,3 个真值点各有一张照片,**EXIF 换算成 GCJ 后
|
||
落在 5.2 / 19.0 / 31.4m 内,且拍摄时刻与到访时刻相差 8~78 分钟**(`IMG_20260815_101254.heic` → 九孃
|
||
豌豆粉店 5.2m、晚 8 分钟)。不换算则离真值 238~370m。⇒ **EXIF 确为 WGS-84,换算方向正确**。
|
||
换算函数另在基线真机数据上验证 **305/323 逐位一致**,各地偏移量与真值吻合(贵阳实测 -0.00354/+0.00373,
|
||
函数给 -0.00353/+0.00377)。
|
||
|
||
**旁证:高德 POI 是 GCJ-02**(别再怀疑):拿贵阳基线的 76 个真值点验证,64 个高德 POI 落在
|
||
「显示坐标(GCJ)」3m 内、离「原始 GPS(WGS)」约 540m。
|
||
|
||
**修复后新出现的瑕疵**:13 个「名称/地址由错坐标反查而来」的点,坐标改对后名称与位置差了约 600m
|
||
(如 sp349 现在在天心阁东南 630m,名字还叫「天心阁」)。已列清单,等站长定是否按新坐标重跑一次逆地理刷新。
|
||
|
||
产物:`tmpfog/fix-report.html`(最终报告,含前后坐标、验证结论、遗留项)、`tmpfog/applied.csv`、
|
||
`tmpfog/_name_origin.json`、`tmpfog/_old_datum.json`、`tmpfog/_exif_dist.json`。
|
||
|
||
### 按新坐标刷新地址(生产)+ 坐标来源留档上线(根治方案)
|
||
|
||
站长指令:**「1、按新坐标重跑一次逆地理刷一致;2、按根治方案动手」**。两件都做完了。
|
||
|
||
#### 一、名称「与位置差 600m」的裁定:**不改名称,只刷省市区街道**
|
||
|
||
上一轮说「13 个点的名称/地址是旧坐标反查产物」用的是**宽松判据**(名称出现在旧坐标 regeo 的 blob 里),
|
||
这轮校准出它**过宽**:`#349` 的 blob 里出现「天心阁」只是因为里面有「8117咖啡(天心阁**店**)」和「天心**公园**」。
|
||
真正的判据只有两条(照片导入页 `applyOne` 只会用这两种方式自动填名):
|
||
① 名称 == 旧坐标 200m 内最近 POI 的店名;② 名称 == 「区县 + 街道/乡镇」兜底拼法。
|
||
按这两条重判:**13 个里只有 `#409`(柠季,旧坐标最近 POI 一字不差、17m)和 `#377`(荔波县瑶山瑶族乡,兜底拼法)是软件填的**,
|
||
其余 11 个是站长自己的标注 → **一律不动**。
|
||
|
||
**「同名地物离旧坐标更近」不是反证**(差点把它当成改错了):13 个里有 7 个被判「名称属于旧坐标」,
|
||
但旧坐标附近往往有一批同名 POI(天心阁古玩城/钟楼公园店/华莱士·钟楼公园店…),
|
||
更多是**站长照着导入页显示的那一行地址(`店名(N米) · 完整地址`)抄下来的**——地址也是错坐标反查出来的。
|
||
站长的 `#409` 就是实例。
|
||
|
||
**同一批次同一定律(这才是决定性的)**:一次导入里所有点用的是同一个坐标系选择、同一台手机。
|
||
长沙批次里 `#348 茶颜悦色`(旧坐标距同名 POI **647m**,远超反查半径 200m ⇒ 不可能是自动填的)、
|
||
`#354 肉丸店`(674m→11m)、`#352 长沙南站`(566m→12m)、`#360 四方坪三十栋饭店`(629m→35m)、
|
||
`#356 坡子街派出所`(322m→32m) 等 **8 个站长自己命名的点,换算后全部落在同名地物 11~383m 内**
|
||
⇒ 换算方向对整个批次成立 ⇒ 剩下那几个「旧坐标更近」的只能是名称伪证。
|
||
|
||
**设备同一性(补完最后一块证据)**:长沙原图 `data/images/IMG_20251219_*.jpg`、大理 `IMG_20260815_*.heic`、
|
||
佛山 `IMG_20260418_181404.jpg` **全是 HUAWEI Mate 80 Pro Max**。大理那 3 张已独立证明 EXIF=WGS-84
|
||
(gcj(EXIF) 落在高德真值 5.2/19.0/31.4m 内),佛山那张经站长肉眼复核(银星雅苑/翠庭轩 91~144m)。
|
||
同机同约定 ⇒ 长沙批次也是 WGS-84 ⇒ 上一轮 63 个点的换算全部正确,**无需回退**。
|
||
|
||
#### 二、地址刷新(已落库,39 行)
|
||
|
||
派生字段(省/市/区/街道)还是按旧坐标反查来的——长沙 24 个、荔波 16 个的**「区县」干脆是空的**
|
||
(顺带说明:这 40 行是**老版**照片导入页建的,那时表单还没有区县字段;佛山那批才带区县——
|
||
所以「区县为空」可以给「老版照片导入」当旁证)。
|
||
以**新坐标**调 `/api/regeo`(coord=gcj02,不再换算),只覆盖反查给出值的字段,反查失败不清空。
|
||
`tmpfog/addr_apply.py --apply` → **成功 39 / 失败 0**;复核 **区县异常 0 | 名称/类别变动 0 | 坐标偏移 0**
|
||
(回退清单 `tmpfog/addr-rollback.csv`)。佛山 23 行本来就对。`#367` 首次反查空返回,单独重试补上(开福区)。
|
||
**街道(thoroughfare)这轮没提交**——线上还是旧二进制会忽略该字段,等部署后再跑一轮。
|
||
|
||
#### 三、根治方案:places 加 4 列坐标来源留档(已实现,待部署)
|
||
|
||
列:`coord_source`(rond / photo / manual,空=历史行无留档)、`coord_sys`(wgs84 / gcj02)、
|
||
`src_lat`/`src_lon`(**换算前**的原始坐标)。三者齐全时 **显示坐标 == 按 coord_sys 换算 src_lat/src_lon**。
|
||
|
||
- `schema.sql`:4 列 + 存量回填(有 ZLOCATION 留档 → rond/wgs84/`ZRAWLATITUDE`)。**不拿启发式猜**——
|
||
猜错比留空更糟,那 45 行(生产 86 行)宁可先空着,标「未留档」。
|
||
本地实测回填 323 行,其中 18 行 src==显示坐标(正是「当初没偏移」的那批)。
|
||
- `ingest.go` 加 `backfillCoordSource`:导入完 rondbackup 后按同一判据补,新导入的包立刻有档。
|
||
- `handlers_photos.go`:写入 `photo` + 用户选的坐标系 + 换算前坐标(换算前先存 `srcLat/srcLon`)。
|
||
- `handlers_edit.go`:新增记 `manual`/`gcj02`;`editPlaceUpdate` 拆出 `updatePlaceRow`,规则:
|
||
**表单没提交的列一个字不动**(批量工具只改坐标时不能把留档/街道抹掉);**坐标动过才更新留档**(否则改个名字就冲掉原始坐标);
|
||
走换算时原始坐标记换算前那份 + `coord_sys` 记来源坐标系;选「未记录」整组清空。
|
||
- `edit.html`:列表加来源标记(`rond 导入 · WGS-84` / `未留档`)+ 来源筛选下拉(带各来源计数);
|
||
行内新增「按坐标刷新地址」按钮、`coord_source`/`coord_sys` 下拉、街道输入框;换算下拉联动修正留档
|
||
(`coord_sys` 设成来源系、`src_lat/src_lon` 填换算前的值),隐藏字段用全精度带出(原先 `%.6f` 会截断留档)。
|
||
- `place_coord_test.go`(新增,6 条断言):导入自动补档、只改名称不动留档与街道、换算带原始坐标且 `显示==gcj(src)`、
|
||
选「未记录」清空、后台新增 manual/gcj02、照片导入 photo/(wgs84|gcj02) + 原始坐标。
|
||
|
||
**实测**:`bin/rond.exe` 重编后本地起站(伪造会话 uid=2)→ 列表标记/筛选正确(368 / rond 323 / 未留档 45 / photo 0);
|
||
用 `rond-web-verify` 的无头 Chrome 真点按钮:反查把空的区县填成「金城江区」、街道保留、提示带地址与最近 POI(13m)、**0 JS 报错**;
|
||
换算下拉实测 `24.689928,108.042479`(WGS)→ `24.687112,108.046675`(527m 往东南),
|
||
`src_lat/src_lon` 变成换算前的值、`coord_sys=wgs84`,切回「保持原样」原值(含全精度)完整还原。
|
||
导出侧回归:`TestMovementCRUDExportE2E` PASS;纯净往返 `verify_rondbackup.py` **总差异 3**
|
||
(全是既有的 `ZLOCATION.ZLASTARRIVALDATE_` 站点重算,与改动前逐行相同)。
|
||
|
||
**部署**:`bash scripts/build-linux.sh` → `dist/rond-linux-amd64` 已生成,替换后 `systemctl restart rond-with-you`
|
||
(启动自动跑增量迁移,幂等)。留档列不参与导出,rond 侧列一字不变。
|
||
|
||
**遗留**:① 街道需部署后再跑一轮(目标值在 `tmpfog/addr-plan.csv`);② 86 个历史行的留档仍为空
|
||
(审计结论已能逐行定性,等站长要就按结论回填);③ `#324–343、#384、#385` 缺区县(新增地点页建的正确点);
|
||
④ 导出 `ZISPHOTOIMPORT` 恒 0(有了 `coord_source` 后很容易补,属导出侧改动,单独一轮)。
|
||
|
||
产物:`tmpfog/report-coordsource.html`(本轮报告)、`tmpfog/addr-plan.csv`、`tmpfog/addr-rollback.csv`、
|
||
`tmpfog/regeo-plan.csv`(13 个名称点的新旧反查对照)、`tmpfog/regeo_refresh.py` / `addr_plan.py` / `addr_apply.py` / `addr_verify.py` / `name_check.py`。
|
||
|
||
### 部署后复测:发现并修掉「导出合成行」这个真 bug(v7 之后)
|
||
|
||
站长指令:**「继续未完成的任务,并检查最近的修改是否引入了什么逻辑错误或者bug」**。
|
||
|
||
#### 一、真 bug:62 个照片导入的点被误标成 `rond 导入 · WGS-84`
|
||
|
||
**根因(两条,都在我上一轮自己写的代码里)**:
|
||
1. **判据错**:回填写的是「有 ZLOCATION 留档 = rond 导入」。但生产这份数据集是**用站点自己
|
||
导出的包回导**出来的,而 `export.go:247` 会给站点新建的地点**合成** ZLOCATION 行
|
||
(`ZRAWLATITUDE = GCJ02ToWGS84(导出那一刻的显示坐标)`)。于是 324–385 这些照片/手工点
|
||
全都"有留档",被整批当成 rond。386–409 是那次导出之后才建的,没有留档行,所以是空的。
|
||
2. **列错位**:`src_lon` 取的是 `ZLONGITUDE`(显示经度),应为 `ZRAWLONGITUDE`(原始经度)。
|
||
纬度取对了、经度复制粘贴错。本地实测 323 行里只有 18 行碰巧对。
|
||
|
||
**判据(唯一可靠的那条)**:合成行的 `ZRAW` 是我们自己写进去的,所以**位级**满足
|
||
`ZRAWLATITUDE == GCJ02ToWGS84(该留档行自己的 ZLATITUDE)`;真机行的 ZRAW 是独立采样值,
|
||
从不满足(基线 321 行**零命中**,Go 实测)。**必须用留档行自己的 ZLAT 比,不能用 `places.lat`**
|
||
——344–383 的显示坐标在导出之后被改过,拿当前值比会漏掉这 40 行。
|
||
|
||
**修法**:`internal/ingest/coord.go`(新)写 `BackfillCoordSource`:
|
||
- 合成行 → 有 `coord_source='rond'` 且 `src_lat==ZRAW` 的**撤回**(清成 NULL,宁可留空不猜)
|
||
- 非合成行 → 空的补 rond/wgs84 + src=ZRAW;src_lon 是显示经度的**顺手改回原始经度**
|
||
- **境外点跳过**:境外不做偏移,`GCJ02ToWGS84` 是恒等,合成行与真机行长得一模一样、判不出来
|
||
启动时 `BackfillAllCoordSource` 跑一遍(`main.go`),schema.sql 里那段 SQL 回填删掉(写不出 geo 判据)。
|
||
|
||
#### 二、生产数据订正(`tmpfog/repair_coord_source.py`,86/86 成功)
|
||
|
||
| 组 | 行数 | 目标留档 | 依据 |
|
||
|---|---|---|---|
|
||
| 344–383、387–409 | 63 | `photo` / `wgs84` / src=换算前的 EXIF | `tmpfog/fix-list-final.csv` |
|
||
| 324–343、384、385、386 | 23 | `manual` / `gcj02` / src=显示坐标 | `tmpfog/keep-list-final.csv` |
|
||
|
||
顺带补完上一轮遗留:**街道**(`addr-plan.csv` 的「新街道」;84/86 有值)+ **324–343、384、385
|
||
的区县**(反查补齐,86 行零缺失)。写库前先验不变式「显示坐标 == 按 coord_sys 换算 src」,
|
||
86 行全过才提交。复核:321 rond / 63 照片导入 / 23 后台新增 / **0 未留档**。
|
||
回退清单 `tmpfog/repair-rollback.csv`(注意:留档那几列回退等于退回已知有 bug 的状态)。
|
||
|
||
#### 三、导出侧补 `ZISPHOTOIMPORT`(`export.go`)
|
||
|
||
rond 靠这一位分「照片导入 / 手工新增」,站点没这个字段,但 `coord_source='photo'` 说得清楚。
|
||
做法:`queryPlaces` 多取 `coord_source`,ZLOCATION 写完统一 `UPDATE ZLOCATION SET
|
||
ZISPHOTOIMPORT=1 WHERE Z_PK IN (...)`(分块 500)——新建行与「走整行还原」的行都覆盖得到,
|
||
而真机行(`coord_source='rond'`)不碰。列不存在时跳过(`loc.typeOf`)。
|
||
|
||
#### 四、顺手发现并修掉的两个 bug
|
||
|
||
1. **「按坐标刷新地址」按钮从来不填街道**(`edit.html`):接口给的是 `street`/`township`,
|
||
JS 却读 `it['thoroughfare']` → 永远 undefined。改成 `it.street || it.township`。
|
||
无头 Chrome 实测:修前街道保持空,修后填出「西环路」,0 JS 报错。
|
||
2. **行内保存会把坐标截到 6 位小数**(`handlers_edit.go`):表单经纬度框是 `%.6f`,真机基线
|
||
的点带长小数。实测只改名称也把 `26.46800422211455` 写成 `26.468004`。照 `keepStoredTime`
|
||
的同一套办法加 `keepStoredCoord`:落在同一个 6 位小数上就沿用库里那份。实测修后不再截断。
|
||
|
||
#### 五、还没动、留给站长拍板的
|
||
|
||
`adminPhotosImport` / `editPlaceCreate` / 天气新增 都用 `SELECT MAX(src_pk)+1`,没走
|
||
`nextSrcPK`(那个还会看 `coredata_meta` 与 `entity_raw` 的最大编号),也没调 `clearStaleRaw`。
|
||
`entity_raw` 里**确实存在 skipped 行**(本地 ZVISIT#191),意味着某个编号可能没有对应实体行却
|
||
一直占着号;删掉最大号的地点后再新建就会复用,导出时被当旧行。位移/交通方式两条路径是走对了的,
|
||
地点与天气没走。当前生产没踩到(places max 409 > entity_raw max 385,且删地点有 pruneEntityRaw 兜底)。
|
||
|
||
产物:`internal/ingest/coord.go`、`tmpfog/repair_coord_source.py`、`tmpfog/repair-regeo.csv`、
|
||
`tmpfog/repair-rollback.csv`、`dist/rond-linux-amd64`。新增测试:
|
||
`TestCoordSourceSkipsExportSynthetic`、`TestExportPhotoImportFlag`、`TestInlineSaveKeepsFullPrecisionCoord`。
|
||
纯净往返差异仍是 **3**(全是既有的 `ZLASTARRIVALDATE_`)。
|
||
|
||
### 站长部署后的复核 + 编号分配收口
|
||
|
||
站长指令:**「我已经在正式环境部署了最新的源码编译版本,你再检查下你在本地测试的结果是否正确。
|
||
另外你发现的地点与天气没走 nextSrcPK 问题,一并收口,做完再次完整测试。」**
|
||
|
||
#### 一、生产复核(`tmpfog/verify_deploy.py`,全绿)
|
||
|
||
| 检查项 | 结果 |
|
||
|---|---|
|
||
| 来源标记分布 | 321 rond / 63 照片导入 / 23 后台新增 / **0 未留档** |
|
||
| 基础行留档自愈(321 行) | `src_lat`、`src_lon` 与导出包 `ZRAWLAT/ZRAWLON` **全精度逐行相等,0 不符** |
|
||
| 导出侧 `ZISPHOTOIMPORT` | 照片行全 = 1、手工行全 = 0 |
|
||
| 不变式「显示 == 按 coord_sys 换算 src」 | 407 行 0 不符 |
|
||
| 新增行地址完整度 | 区县 0 缺失;街道缺 367、386(高德未返回,可接受) |
|
||
|
||
⇒ **启动自愈真的跑了、跑对了**:`src_lon` 那个「取成显示经度」的笔误在生产上被自动改回原始经度,
|
||
且 86 行手工订正的留档一个没被动。本地测试结论在真实环境成立。
|
||
|
||
#### 二、编号分配收口(本轮新改动)
|
||
|
||
原来只有位移/交通方式走 `nextSrcPK`,其余全是 `SELECT MAX(src_pk)+1`。**统一改成
|
||
`nextSrcPK` + `clearStaleRaw`**,新增四个编号空间:
|
||
|
||
```go
|
||
placeSeq = srcPKSeq{"places", "src_pk", "Location", "ZLOCATION"}
|
||
visitSeq = srcPKSeq{"visits", "src_pk", "Visit", "ZVISIT"}
|
||
activitySeq = srcPKSeq{"activities", "src_pk", "Activity", "ZACTIVITY"}
|
||
weatherSeq = srcPKSeq{"weather", "src_pk", "HourlyWeather", "ZHOURLYWEATHER"}
|
||
```
|
||
|
||
改动点:`adminPhotosImport`(原来在循环外算一次、失败还 `nextPlaceSrc--`,现在每行在事务里
|
||
按三处上限取号)、`editPlaceCreate`、`editWeatherCreate`(原来是裸 `s.db` 插入,顺手包进事务)、
|
||
`insertVisit`(新增到访 / 照片导入共用)、活动内联新建。全项目已无 `COALESCE(MAX(src_pk),0)+1`。
|
||
|
||
新增 `internal/web/seq_test.go` 的 `TestNewRowNumberingSkipsArchivedPKs`:造一条
|
||
「只有留档、没有实体行」的 ZLOCATION(`skipped=true`,**raw 里必须带 `Z_PK`**,判据就是靠它算上限),
|
||
再走手工新增与照片导入两条路径,断言编号绕开它、且留档行本身不被误清。
|
||
|
||
**踩过的坑**:第一版测试 fixture 的 raw JSON 忘了写 `Z_PK`,`nextSrcPK` 读不到,
|
||
于是「绕开」断言失败 —— 反过来说明这条测试确实在盯 `entity_raw` 的最大号。
|
||
|
||
#### 三、复测
|
||
|
||
`go test ./internal/...` 全绿;`go vet` 干净;纯净往返 `verify_rondbackup.py` 总差异仍是 **3**。
|
||
产物 `dist/rond-linux-amd64`(19:29)与 `bin/rond.exe` 均已重编。本轮只改编号分配、不涉及数据订正,
|
||
部署替换二进制即可。
|
||
|
||
### 三个新需求:地图底图可选 / 文字折行 / 地点图片(前端转 WebP)
|
||
|
||
站长指令原文:**「一,地图放到最大时会显示很多信息显得杂乱,看不清自己的点,能否只显示街道或
|
||
有其他表现形式可选(可在后台选择);二,不少地方文字显示异常,一行只有一个字就截断到第二行;
|
||
三,加一个展示图片的功能,点开地点用按钮显示管理员维护的图片,图片可存本地或 S3(R2),
|
||
已有地点可编辑添加、新增地点可选择是否保留图片,要转 WebP 且**在前端完成转码**。
|
||
请逐项完成,每项做完测试是否引入新 bug 或与其他功能冲突。」**
|
||
|
||
#### 一、文字「一字一行」:表格被压扁(已修,全站回归 0 处)
|
||
|
||
**取证方式**:写了段 CDP 诊断脚本,用 `Range.getClientRects()` 数**真实行盒**
|
||
(按 top 以半字号为容差聚类,避免同一行里不同字体被算成多行),再算「每行平均字数」,
|
||
扫 10 个页面 × 6 个宽度(360/390/480/620/768/1024)。
|
||
|
||
**发现**:只有 `/places` 与 `/admin` 两处,都在 `table.tb` 上。根因是
|
||
`table.tb { width: 100% }` + CJK 允许逐字断行 ⇒ 窄屏时**列被榨到一两个字宽**而不是溢出
|
||
(`/places` 7 列在 390px 下每列约 68px,「一般公共场所」排成 **6 行、一行一个字**)。
|
||
|
||
**修法**:`.tw > table.tb { min-width: 640px }` —— 给表格一个内容下限,放不下交给 `.tw` 横滚。
|
||
修后 60 个组合全部 `[]`(无挤压)。
|
||
|
||
#### 二、地图底图可选「纯路网」(已做,后台可切 + 访客自选优先)
|
||
|
||
**先确认了「杂乱」来自底图自带的 POI/路名标注**(地图上我们自己的点只有 hover 气泡,没有常驻标注)。
|
||
`style=8`(当前「街道」)与 `style=7` 对比实测:同一块的**近黑像素占比**(≈文字量)
|
||
长沙 1.46% vs 0.21%、香港 1.27% vs 0.82% —— **style=7 是纯路网,几乎不标注**。
|
||
|
||
改动:`tiles.go` 加 `amap-roadnet`(style=7);后台「设置 → 地图底图」加「默认用哪张底图」
|
||
下拉(`setting.MapDefaultBase`,取值按当前实际图层校验,换过 map_provider 的旧值自动失效);
|
||
`/api/map-config` 下发 `default`;`rond-map.js` 的 `pickBase()` 三级回落
|
||
**访客在图上切过的那张(localStorage `rond.base`)→ 后台默认 → 第一张**,
|
||
并监听 `baselayerchange` 记住。无头 Chrome 实测:后台设 `amap-roadnet` 后地图真的铺 style=7;
|
||
同一会话里写 localStorage 为 amap-street/amap-roadnet,铺的分别是 style=8/style=7。
|
||
|
||
**踩过的坑**:`setting.Load()` 是**逐字段显式覆盖**的,新加的字段忘了写那一行 ⇒ 库里存了、
|
||
读出来是默认值。加设置字段时**必须同时改 Default()/Load()/settingsSubmit() 三处**。
|
||
|
||
#### 三、地点图片(已做,前端转 WebP + 本地/R2 双后端)
|
||
|
||
站长拍板:**照片导入时勾选保留** / **经本站后端中转** / **选择放后台、凭据放 app.ini**。
|
||
|
||
- `internal/media`(新):`Save/Delete/PublicURL`,后端 `local`(`data_dir/media/...`,
|
||
由 `GET /media/` 直出、内容寻址可强缓存)或 `s3`(**手写 SigV4**,PUT/DELETE,路径式)。
|
||
零新依赖——go.mod 只有 pgx 与 sqlite,为这个引一个 SDK 不划算。
|
||
- `place_images` 表:`storage` + `object_key` 分别记住每张图**实际**在哪,
|
||
换存储后端后老图仍取得到。key = `<dataset>/<src_pk>/<sha256[:20]>.webp`(内容寻址、天然去重)。
|
||
- `POST /admin/place/images`(multipart)、`POST /admin/place/images/delete`、`GET /media/`。
|
||
- 前端 `static/js/place-image.js`:canvas 转 WebP(长边 2000px、q0.82),
|
||
**`blob.type !== 'image/webp'` 必须显式判错**——不支持编码的浏览器会静默给回 PNG。
|
||
- 地点页:访客一个「图片 N」按钮 → 全屏画廊(深底、Esc/点空白关闭、锁背景滚动);
|
||
站长多一块管理面板(缩略图墙 + 多选上传 + 单张删除)。
|
||
- 照片导入页:新增「存图」列(默认不勾),提交时**拦下 submit → 逐张转码 → 塞进隐藏的
|
||
`<input type=file>`(DataTransfer)→ 再 `form.submit()` 放行**;表单改 `multipart/form-data`。
|
||
|
||
**两个真 bug(都是测试逼出来的)**:
|
||
|
||
1. **`r.ParseForm()` 会毒化后续的 multipart 读取**。前端用 `FormData` 发(multipart),
|
||
`ParseForm` 只解析 urlencoded,却会把 `r.Form` 先填成「不含 multipart 字段」的那一份;
|
||
`FormValue` 只在 `r.Form == nil` 时才去解析 ⇒ 之后再怎么读都是空。图片删除按钮点了没反应
|
||
就是这个。**规则:收 multipart 的 handler 不要先 ParseForm**,直接 `ParseMultipartForm`
|
||
(注意 urlencoded 调用方会收到 `ErrNotMultipart`,要 `errors.Is` 放过,否则测试与脚本全挂)。
|
||
2. **把照片导入表单改成 multipart 后,原 `ParseForm` 直接让 `n` 读成 0**,整批导入静默失败
|
||
(报「没有可导入的照片」)。同一个根因,同一次修。
|
||
|
||
顺带加了防护:导入请求 `http.MaxBytesReader` 60MB(勾了存图才可能有几百 KB×N),
|
||
前端勾超过 60 张先 confirm 提示。
|
||
|
||
**验收**:`TestPlaceImageUploadDelete`(multipart 上传→落盘→`/media` 字节一致→非 WebP 被拒→
|
||
multipart 删除两处都清)、`TestPhotoImportWithImage`(勾选存图→地点与图都建好、宽高记对)、
|
||
`TestPlaceImageS3Backend`(httptest 假装 S3,断言路径式 URL + SigV4 Authorization + 公开域名取址 +
|
||
切 s3 后本地不再落盘)、`internal/media/media_test.go` 的
|
||
**`TestSignV4MatchesAWSExample`(AWS 官方向量,签名逐位相符)** + escape/canonicalURI/取址/路径穿越。
|
||
浏览器实测:CDP 造一张 PNG 塞进文件框,页面自己转成 WebP(落盘确为 `WEBP (800,600) 4954 bytes`)
|
||
并出现在缩略图墙;全站 8 个页面 JS 报错 0。
|
||
|
||
**测试间互相污染**:多个测试共用一个库,S3 那个测试把 `media_backend` 留着没还,
|
||
后面的本地上传就失败。加了 `setMediaBackend()` + `t.Cleanup` 还原本。
|
||
|
||
#### 复测
|
||
|
||
`go test ./internal/...` 全绿;`go vet` 干净;纯净往返总差异仍是 **3**。
|
||
产物 `dist/rond-linux-amd64`(22:58)。本轮**新增表 `place_images`**,启动迁移会自动建;
|
||
`conf/app.ini` 多了 6 个 `s3_*` 空键(不填 = 只能存本地)。
|