# rond-with-you 项目长期记忆
> 只留「改代码前必须知道的结论」,一句话一条。**细节别在这儿找**:rond 包/迷雾/导出验收见技能 `rond-backup-parse`,量法与探针见技能 `rond-web-verify`,推导过程见 `.workbuddy/memory/YYYY-MM-DD.md`。
## 一、站点侧落库规矩
- **「建地点 + 建到访」必须同一事务**(`Server.withTx` + `execer`):两步写留下孤儿地点,导进 rond 永远「无数据」。到访时段选填,但**校验必须先于落库**。
- **行程派生字段必须与导入路径一致**(`validateMovementForm`):`from_place_id`/`to_place_id`(轨迹靠它画线,**区域黑名单也按两端 places 判定**,留空 = 绕过过滤直接露给访客)、`distance_km`、`duration_min`。
- **新行编号一律走 `nextSrcPK` + `clearStaleRaw`**:取三处最大 +1 —— 库内该列、`coredata_meta.entities.<实体>.max`、`entity_raw.raw->>'Z_PK'`(留档存着 skipped 行,占号但无实体行)。只看库内 `MAX` 会复用 → 新行被当旧行、继承对方 rond 写的列。**绝不再写 `SELECT MAX(src_pk)+1`**;pk 分配在事务内。回归 `internal/web/seq_test.go`。
## 二、rond 备份包导出(细节见技能 `rond-backup-parse`)
真机包 `data/20260914_151649.rondbackup` 是唯一标准答案。
- **两条铁律**:① PG 是真值来源、`entity_raw` 是原值来源——管理列由 PG 覆盖,其余列只能从导入时留的原始整行还原;② 区分**基线已有行**与**站点新建行**(`keepNilCols` + `known[pk]`),前者永远保持真机原值。
- **只给新行补的列**:`ZLOCATION.ZRAWLATITUDE/LONGITUDE` = 原始 WGS-84(`ZLATITUDE/ZLONGITUDE` 是 GCJ-02),用 `geo.GCJ02ToWGS84` 反算;`ZVISIT.ZIDENTIFIER_`/`ZACTIVITY.ZUID_` **必须确定性**(表名 + src_pk),随机则每次导出都变、rond 当新记录;`ZACTIVITY.ZICON_` 缺省 `📍`。
- **派生缓存列** `Z*_.ZLASTARRIVALDATE_` = 该对象最晚一次到访到达时间;没用过写哨兵 `-63114076800` 或 NULL(判空不能只看 `IS NULL`),**别留空**否则 rond 显示「无数据」。
- **ZMOVEMENT**:`ZTYPE_` 791/791 非空、**不能写 NULL**(2 = 无交通方式、5 = 有);进 `keepNilCols` 且**只给新建行**补,**别做成管理列**(否则 23 条 `type=4` 被洗掉)。硬不变量 `ZSTART_ >= 起点离开`、`ZEND_ <= 终点到达`(0 违例)。`ZTRANSPORT` 聚合(`queryTransports`)必须 `GROUP BY transport_src`(`DISTINCT` 会给重复 Z_PK → 撞主键、整次导出失败)。
- **不是缺口**:`ZVISIT.ZRAW`(采样点**数量**)、`ZMERGEDTO`/`ZPARENT`、`ZLOCATION.ZNOTE_`;`place_notes` 仅站长可见,单向从 `ZVISIT.ZREMARK_` 汇总、不回写 rond。
## 三、迷雾(Fog of World).fwss
zip 包,**条目内容本身是 zlib 流**;`Model/*/` = 头 32768B + 每块 515B,**已探索层由 `fog_blocks` 重建**;`Model/~/`、`Model/#/` **不解析只原样写回**(少了快照残缺约 55%)。块尾 3 字节 = 大端 24 位数 = 置位数×2+1(真机 8701/8701 精确满足)、上限 4096 ⇒ **最高字节恒 0**。
- `Decode→Encode` 除压缩级别与块体槽位顺序外完全一致;回归 `internal/fog/fog_test.go`(需 `FOG_SRC`)。**清空迷雾**与**整体替换(cover)**都要连 `fog_raw_files` 一起处理,否则残留「无对应 `Model/*` 瓦片」的图层条目。
## 四、精度陷阱:时间与坐标都不能「原样回贴」
表单精度低于库里真值,**只改备注也会悄悄改掉时间和坐标**:**时间** `datetime-local` 只到分钟、真机带小数秒(`20:03:21.323088`,截断后还会早于起点离开时间被行程窗口拒掉——线上「改方式提示开始时间不能早于离开时间」就是这个);**坐标**控件 `%.6f`、真机长小数(`26.46800422211455`),只改名称就把点挪走。规矩:**与库里已有值落在同一精度上就沿用库里那份**(`keepStoredTime`/`resolveMovementEnd`/`keepStoredCoord`),行程、到访、地点三条编辑路径都要用。
## 五、坐标来源留档(`places` 四列)
`coord_source`(rond/photo/manual,空 = 历史行)、`coord_sys`(wgs84/gcj02)、`src_lat`/`src_lon`(**换算前**)。不变式:**显示坐标 == 按 `coord_sys` 换算 src_lat/src_lon**。
- 写入点:`ingest/coord.go` 的 `BackfillCoordSource`(启动全量跑;判据要 geo 往返换算,**不写进 schema.sql**)、`adminPhotosImport`、`editPlaceCreate`;`editPlaceUpdate` 走 `updatePlaceRow`——表单没提交的列不动、坐标动过才更新留档、走换算时记换算前那份。
- **「有 ZLOCATION 留档」≠「rond 导入」**:`export.go` 给站点新建点**合成** ZLOCATION 行(`ZRAWLATITUDE = GCJ02ToWGS84(导出那一刻的显示坐标)`),与真机行无异。**唯一判据**:`ZRAWLATITUDE == GCJ02ToWGS84(该行自己的 ZLATITUDE)` 位级相等 ⇒ 合成行(基线 321 行零命中);**境外点跳过**(换算是恒等)。
- `ZISPHOTOIMPORT` 由 `coord_source` 推;换算统一走 `GET /api/coords?mode=wgs2gcj|gcj2wgs`,**geo 包是全站唯一实现,前端别另抄**。**高德 POI 就是 GCJ-02**;`/api/regeo` 返回 `street`/`township`(表单里叫 `thoroughfare`,`edit.html` 曾读错键导致街道填不进去)。
- **存量坐标裁决**:同名 POI 对称距离、迷雾栅格两条判据**已否证别再试**;用**批次同一定律**(一批 = 一坐标系 + 一台手机、批内同向,凡几个旧坐标距同名 POI >200m 的点换算后落回地物几十米内即成立;**名称无证据力**)。修库 `tools/photo_coord_audit.py --apply`,修完拉 `/api/points` 复核;`/api/points` 带 `sp`(=src_pk),**基线 sp ≤ 323 用长小数、照片导入 > 323 是 6 位小数**。
## 六、前端 / 模板 / CSS 坑
- **改 Go 必须重启**(旧进程占 8080 时新进程报 `bind: Only one usage`,看着像构建失败);**模板走 `//go:embed`,正式态改 `.html` 要重编二进制**,但 `debug=true` 时每次从磁盘重解析、刷新即生效;CSS/JS 只在 `debug=true` 直读磁盘。**带地图的页面必须 `p.HasMap = true`**,否则 Leaflet 不引入 →「地图容器在、整块不出现」。
- **模板拼查询串 href 必须走返回 `template.URL` 的方法**(`Page.PlaceLink/EditPlaceLink/MapLink`):`?{{.FilterQS}}` 被 URL 上下文转义(`=`→`%3d`)→ 筛选**静默失效**(翻页最易忽略),`{{safe}}` 挡不住;**返回 template.URL 时不要再手工做 HTML 属性转义**(`&` 会被再转成 `&`)。
- **CSS 顺序陷阱**:同特异性靠后的赢,而 `.chip`/`.btn`/`.check input` 的**基础规则在 `app.css` 中部** ⇒ 写在文件前半的 `@media (max-width:620px)` 覆盖是**死代码**(`.check input{width:20px}` 一直没生效)。窄屏尺寸/内边距覆盖**一律写进文件末尾那个块**(已标注「必须放最后」);基础规则里没有的属性(`min-height`/`display`/`--field-fs`)写前面没事。
- **控件字号**:`input:not([type=file]):not([type=color])…,select,textarea{font-size:var(--field-fs,inherit)}`,窄屏 `--field-fs:16px`、表格里 14px。**被 `:not()` 排除的类型(file/color/range/checkbox)吃不到**,要单独给字号——iOS 聚焦 <16px 控件会自放大整页(file/color 已提 16px;**range 是审计误报**,滑块不弹键盘)。窄屏下限:触控 44px(`.btn/.chip/.nav-links a` + 导航 logo `.brand`)、`select{min-height:44px}`(表格里除外)、`checkbox/radio{20px}`、`.tag{12px}`。
- **表格别让 CJK 把列榨干**:`table.tb{width:100%}` + CJK 逐字可断 ⇒ 窄屏列压到 1~2 字宽;靠 `.tw > table.tb{min-width:…}` + `.tw` 横滚。**展开行与主行等宽靠 `colSpan`**(主行 9 列 ⇒ `colSpan=9`),浅底用 `var(--bg-2)` 别用 `--line-soft`。
- **`rond-map.js`**:`full()` 首屏用 `points: []` 再靠 `setPoints` 填,**按需构建的图层必须读 `st.points`**;`preview()` 的「点击进全屏」要传 `opts.clickToFull`(只有主页该有);**同容器二次 `L.map()` 抛 `already initialized`**(被 catch = 假失败),`_state[id]` 已有 `setPoints` 就复用实例;时间轴增量更新判据是**对象引用**、拖动传 `{keepView:true}`;markerCluster 子标记**异步**渲染,动画类要挂 `add`。
- **`map.html` 筛选面板**:三个坑(内边距须挂 `#filter-form` 而非面板、sticky 元素**后面不能还挂内容**、「查询/重置」按钮要 `width:100%`)与量法见技能 `rond-web-verify` 的 `probe_map_panel.js`;`act`/`hw` 两个 hidden **必须留在 `details` 外面**否则收起即丢筛选。
## 七、访客 vs 站长:双身份差异(新 bug 都容易出在这)
**访客页与站长页的 DOM 不一样**——只在访客视角复现、站长自己点一遍一切正常。遇到「某功能只有站长有」先查这类。
- **定性手法**:用**不带 cookie 拉原始 HTML**(urllib)与 CDP 求值后的 DOM 比,能区分「服务端没渲染」与「渲染了被 JS 删掉」,比看截图快。
- **已修**(`96e7631`):详情页画廊 `syncGallery()` 从 `#imgs-grid` 反推地址,而缩略图墙整块在 `{{if .IsAdmin}}` 里 ⇒ 访客页数到 0 张,把服务端渲染好的「图片 N」按钮 `remove` 掉。修法:`PlaceData.ImageURLs` 随页面下发;`cellURLs()` 判据改成**「缩略图墙在不在」而非「墙里有几张」**(站长删光图时墙还在,按张数回退会把删掉的图数回来)。回归 `TestVisitorPlacePageCarriesImageURLs`。
- **同类扫描**(条件块内声明的 `id` 被 JS `getElementById` 读)目前**只有 place.html 命中**,其余是「本页元素被本页脚本读」的正常形态;改模板时别新增这种耦合。
## 八、地点图片与转码
- **对象名 = `<数据集>/<上传月份>/<地点号>-<内容哈希>.webp`**(按月分目录便于备份、带地点号便于认领、哈希天然去重);**不要去地点号**(删图按 key 精确删)。key 存 DB 行,**改规则不影响旧图**。排序 `ORDER BY position, id`。
- **转码在浏览器 canvas 做**(`place-image.js`),服务端只校验文件头、原样写盘、**不解码** ⇒ 任何平台都不需要 libwebp;**`canvas.toBlob` 在不支持 WebP 的浏览器会静默返回 PNG**,必须判 `blob.type`。
- **压缩参数(2026-09-18 重定,别退回旧值)**:长边 **1440**、质量阶梯 `[0.72, 0.58]`、命中 **220KB** 即停,走到底仍超标就整张缩到 **1080** 重编(≤3 次编码)。依据:压质量掉 19% 画面就糊,缩尺寸同 q 掉 55% ⇒ **超标该缩尺寸**。透明 PNG 先铺白底。
- 存哪边由后台设置决定(local/s3),凭据只在 `conf/app.ini` 的 `s3_*`;`internal/media` 手写 SigV4、零新依赖。**`s3_public_base` 必须带协议**(裸域名 = 相对地址 = **图全变破图且不报错**)。**上传不能只放 ``**:用 `