# 2026-09-14 ## rond-with-you:把 rond 备份变成足迹站点(从零建成可运行版本) ### 背景 用户提供 `data/20260914_151649.rondbackup`(3.4MB),要求做一个后台可上传该文件、 前台展示足迹地图 / 类型 / 日期筛选的站点。技术栈由用户指定为 PostgreSQL(本地测试环境 `postgres / 127.0.0.1:5432`),后端语言 Python 或 Go 自选。 ### 备份结构(已实测确认) `.rondbackup` 是 zip,内含 Core Data 库 `LifeEasy.sqlite` + `-wal` + `-shm`。 **关键点:数据几乎全在 WAL 里**(主库 348KB / WAL 3MB),必须三个文件一起解出再打开, 否则只能读到极少量记录。Core Data 时间戳基准为 2001-01-01 UTC。 主要表与本次数据量:`ZVISIT` 824、`ZLOCATION` 323、`ZRAWVISIT` 2246、`ZMOVEMENT` 791、 `ZACTIVITY` 30、`ZHOURLYWEATHER` 557、`ZTAG` 8、`ZTRANSPORT` 5、`ACHANGE` 4810(未导入)。 标签关联表命名规则为 `Z_<实体号>TAGS_`,如 `Z_11VISITS_` = Visit↔Tag(11=Tag,18=Visit)。 导入结果:823 条到访(跳过 1 条到达时间缺失的空记录)、323 地点、8 标签关联、791 位移、 557 天气;跨度 2025-02-10 ~ 2026-09-13,估算里程 16861.9km,城市 9 座 / 省份 2 个。 ### 技术选型 - Go 1.25 + 标准库 net/http + html/template(未引入 Web 框架) - PostgreSQL + `pgx/v5`;备份解析用纯 Go 的 `modernc.org/sqlite`(无 cgo) - 前端手写 CSS,Leaflet + markercluster 本地内置(无 CDN 依赖) - 口令 PBKDF2-SHA256(标准库 `crypto/pbkdf2`);会话为 HMAC 签名 Cookie, 签名混入口令摘要前缀,改密即让旧会话失效 ### 踩过的坑(都已修复) 1. `//go:embed static` 的资源路径相对包目录,原放在仓库根的 `web/` 下会编译失败, 已移动到 `internal/web/{templates,static}`。 2. 地点查询用 `WHERE ... EXISTS (SELECT ... visits v` + `inner.SQL()[7:]` 拼接, 切片把 `WHERE` 关键字一起去掉了,导致 `SQLSTATE 42601`。改为 `inner.expr()` 并给 子查询占位符加偏移(`where.off`),避免与外层 `$1` 冲突。 3. 模板函数 `dur` 形参写死 int64,而 `VisitRow.DurationMin` 是 int,渲染时报 "wrong type for value",改成接受 `any`。 4. 页面模板若共用 `templates/*.html` 一次性 ParseFS,各页 `{{define "content"}}` 会互相覆盖;改为每个页面与 layout 单独组成模板集。 5. 月度图表只查有数据的月份,会把空档压缩掉;改为补齐成连续月份序列并标注年份。 6. 移动端导航会折行撑坏布局,窄屏改为隐藏站点名、导航横向滚动、按钮文案缩短。 地图缩放/图层控件在窄屏改挂右下角,避免与全宽筛选面板重叠。 ### 环境备忘 - `GOPROXY=https://goproxy.cn` 可用;`proxy.golang.org` 不通 - jsdelivr 可下载前端库;高德瓦片 `webrd0X/webst0X.is.autonavi.com` 可直连 - playwright-cli 通过 `npx --yes @playwright/cli@latest` 调用;**浏览器进程会在两次 bash 调用之间被回收**,open / goto / screenshot 必须放在同一次调用里 --- ## 第二轮:地图偏移 + 重复导入 + 隐私默认 + 底图配置(P0/P1 收尾与回归) ### 坐标结论(已实测,写进 README 了) rond 入库的 `ZLOCATION` / `ZRAWVISIT.ZLATITUDE` **本来就是 GCJ-02**,不是 WGS-84。 证据两条:① 2246 个采样点「解析坐标 − 原始 GPS」中位差 lat −0.002823 / lon +0.004187, 与 GCJ-02 理论偏移(−0.002825 / +0.004187)残差 0 米;② 与世界迷雾覆蓋比对, 逆 GCJ→WGS 后命中率 92.3%,直接用原始值只有 32.8%。 → 高德 / 天地图这类 GCJ-02 底图**直接画,不要再纠偏**;只有 WGS-84 底图才做 GCJ→WGS 逆变换。 旧代码对每个点都套了一次 `gcj()`,所以叠了约 1.1km 的双重偏移。 ### 底图配置 `conf/app.ini`:`map_provider` = amap(默认免密钥)/ tianditu(需 `map_key`)/ custom (需 `map_tile_url` + `map_crs`)。填了 `map_key` 后瓦片走服务端代理 `/tiles/{layer}/{z}/{x}/{y}`, 密钥不出现在页面源码;无密钥则浏览器直连。前端读 `/api/map-config` 拿 `crs` 决定要不要逆变换, 切换底图时重新投影已有标记。 ### 本次修掉的三个 bug(都是实测复现过的) 1. **瓦片代理子域写死 0** → 高德子域只有 01~04,`webrd00` / `wprd00` 根本不解析。 改为按 `(x+y)%count` 轮转,高德 1..4、天地图 0..7。代理模式实测 4 张瓦片全部 200。 2. **`Stats.Skipped` 从未赋值**(`ingest.go` 构造 Stats 时漏了字段)→ 「已跳过 N 条」 与「内容完全重合」两个提示全是死代码。补上后实测提示正常。 3. **`movements` INSERT 不写 `user_id`** → 位移永远无法去重(重导入 791 条变 1582 条), 且下次启动时迁移的回填 UPDATE 会撞上 `idx_movements_dedupe`,**整个站点起不来** (`SQLSTATE 23505`,实测复现)。已补 user_id + `ON CONFLICT DO NOTHING`, 并把迁移里三个回填 UPDATE 各自包进 `EXCEPTION WHEN unique_violation`,保证迁移再也不会拖垮启动。 ### 数据集模型:已定「快照式」并落地(用户在两个方向里选了快照式) 「自然键跨数据集全局去重」与「一次上传 = 一份完整数据集」互相矛盾:全局去重下,更新备份里的 新记录只会落进未激活的数据集,页面上完全看不到(已实测:删 1 条后重导,新记录进了 `is_active=f` 的数据集 #2,页面仍显示 822 条)。 最终实现(快照式 B+): - 一份备份 = 一份完整快照,行级唯一性只靠各表自带的 `UNIQUE (dataset_id, src_pk)` - `schema.sql`:`DROP INDEX idx_visits/movements/weather_dedupe`(迁移里清理旧索引), `datasets` 加 `content_hash` 并建 `(user_id, content_hash)` 索引 - `ingest.Import`:先按 `contentHash(b)`(到访自然键排序后 sha256)查重,命中就复用旧数据集 并返回 `Stats.Identical=true`;否则建新快照,最后 `UPDATE datasets SET is_active=(id=$1) WHERE user_id=$2` 让最新快照接管展示 - `handlers_admin.go`:同 SHA 重复上传改为「已导入过,未重复导入」并顺手删掉刚落盘的文件 (顺带修了删除数据集不删上传文件的 P2);提示语明确写「已切换为当前展示」 - 回归全绿(一次性库实测):首导 823 / 同文件复用 / 内容同字节不同的复用 / 含 1 条新到访的 备份 → 新快照 #2 824 条接管展示,页面上新记录可见 - 已知小尾巴:存量数据集的 `content_hash` 是 NULL,无法识别与它的内容重复,只影响第一次 重传;后续上传正常 - 交叉编译产物 `dist/rond-linux-amd64` 已同步重编 ### 回归验证配方(可复用,别动真实库) 1. 建一次性库:`CREATE DATABASE rond_dedup_test` 2. 写 `conf/app.test.ini` 指向它、`listen = :8090`、独立 upload/data 目录 3. 首次启动自动建表并生成 `admin / rond-admin` 4. `curl -X POST -d "username=admin&password=rond-admin" .../admin/login` 拿 cookie 5. `curl -b -F "backup=@xxx.rondbackup" .../admin/upload`,提示语在 303 的 `Location` 头里(URL 编码,`urllib.parse.unquote` 解一下) 6. 造「内容相同但摘要不同」的备份:用 python `zipfile` 按原条目重打包即可换 SHA 7. 收尾:kill rond.exe → `DROP DATABASE` → 删 `conf/app.test.ini` 与测试目录 ### 其他备忘 - `go build ... | head -20 && echo OK` 会**掩盖编译失败**(管道退出码来自 head),别这么判成功 - Windows 上用 python 写 `/tmp/x` 会落到 `C:\tmp`(和 Git Bash 的 /tmp 不是一处), 尽量写项目内路径 - 后台删除数据集只删 DB 行,`storage/uploads/` 里的原始文件会留在磁盘上(P2,待清理逻辑) ### 世界迷雾叠加(已全部落地并验证) - `internal/fog`(fog.go 解码/编码、store.go 内存索引+Import(merge)、render.go 瓦片渲染) - 端到端:Go 解码与 Python 原型一致(52 tile / 8701 块 / 575753 格),编码回环无损(299112B) - 路由:`POST /admin/fog`(mode=overlay|cover)、`GET /admin/fog/export`、`GET /tiles/fog/{z}/{x}/{y}?crs=` - 前端:mapConfig 下发 fog 统计,rond-map.js 图层控件加「世界迷雾」,切底图自动换 crs 重拉 - 渲染两坑:① 低缩放级按全球块范围双循环 = z0 卡死 43 亿次 → 改为遍历块表+粗裁剪 ② `image.RGBA` 是预乘 alpha,直写颜色被 png 编码器除回 alpha 变绿 → 用 `image.NRGBA` - 对齐验证:足迹点 WGS 格位图已点亮;gcj02 渲染像素与足迹点像素偏差 0.0px(数学); z15 近景合成图迷雾沿道路分布、红点落在迷雾格上(视觉) - 测试:overlay 删块恢复 ✓、cover 清空重导 ✓、导出→再导入格数不变 ✓ - 测试配置留在 `conf/app-test.ini`(已 gitignore);主站 8080 还是旧二进制,重启后才有迷雾功能 - 19:07 主站已用新二进制重启(后台任务),真实库迁移出 fog_blocks ✓,fog 瓦片/配置接口正常; 真实库尚未导入迷雾,等用户在后台传 .fwss - 整体检查:build/vet 零问题;仓库历史文件是 CRLF 行尾(gofmt 全文件 diff),只有 internal/fog 做了 gofmt;fog.go 里多余的 filepath_Base 已换成 filepath.Base ### P1+P2 六个功能(19:18-19:38 全部落地并冒烟通过) - 迷雾覆盖统计:fog.CellOf/AreaKm2/Index.CellLit + web/fogCoverage,stats 页新区块; 坑:SQL 少写一列但 Scan 五个目标 → 静默返回 nil(区块不显示),已修 - 行程轨迹:/api/track(movements+places 起终点,5000 段上限),map.html 行程轨迹开关, rond-map.js setTrack,随底图坐标系重投影 - 到访热力:vendor leaflet-heat.js(unpkg 0.2.0),map.html 热力开关 + setHeat,权重=到访次数 - 年度报告:/report?y=,Nav 新增「报告」,report.html(KPI+城市榜+南北两端+交通) - 数据导出:/admin/export/places.{geojson,gpx},全量地点含家/工作(站长本人数据), GCJ-02 原样导出并在 properties 注明 crs - PWA:manifest.webmanifest + PIL 生成的爪印图标 192/512 + layout head meta - 验证:测试库(rond_exp_test,已删)验导出 323 features/323 wpt、迷雾覆盖 47.8km²、 河池 43/93 等;主站 6 条路由全 200 - 所有 Go 文件已 gofmt(含 LF 行尾归一,为将来 git init 首提交做准备) ### 三个增强功能(20:00-20:45 全部落地并验证,主站 8080 已用最终二进制重启) - **OSM 免密钥兜底**:高德瓦片 tileerror≥8 次前端自动切 OSM + toast; clarification:Leaflet 是渲染库 不是瓦片源,高德本来就是免 key 直连默认底图 - **公开页面开关**:settings 保存 `pages`(map/places/timeline/stats/report),guardPage 中间件 对匿名访客 404,管理员放行,导航同步隐藏;实测 anon /stats=404、admin /stats=200 - **TOTP 两步验证**(internal/auth/totp.go + handlers_admin.go):RFC 6238 HMAC-SHA1 30s ±1 窗口; 两步登录中间令牌 `2fa|uid|exp|mac`(绑定口令摘要,改密失效,5 分钟);关闭 2FA 也要求新鲜验证码。 全流程实测通过:绑定→确认→登出→密码登录→动态码→/admin 200;负例(错码/篡改令牌)均被拒; 关闭后登录免第二步 ✓ - **两个坑**:① TOTP 计数器曾用 `t.Unix()/int64(30*time.Second)`(除以 300 亿),改 `unixSec/30`; ② login2FA 曾用 `SplitN(pending,"|",2)[0]` 取 uid(取到的是 "2fa" 前缀),改 `parts[1]` - **测试方法坑(重要)**:html/template 会把属性值里的 `+` 转义成 `+`(防 UTF-7),curl 从 HTML 抠隐藏字段直接回传会验签失败——浏览器会解码实体,这不是产品 bug。测试脚本要先 `sed 's/+/+/g'` - 验证用测试账号:首次启动自动生成 admin/rond-admin(README 有写) - 清理:rond_2fa_test 库已 DROP、conf/app-2fa.ini 与 .tmp-test/2fa 已删;conf/app-test.ini 保留 (gitignored,回归配方用) - venv 的 python.exe 在 `envs//Scripts/python.exe`(不在根目录),用户记忆里写的 sss 环境可用 ### 口令重置(21:05) - 主站真实库(rond_with_you)的 admin 口令早已改过,默认 rond-admin 只对空表新库生效 (EnsureDefaultUser 见 users 非空即跳过);用户忘了旧口令,用项目内 auth.HashPassword 写了一次性 重置程序(.tmp-test/resetpw,用后已删)把口令恢复为 rond-admin,curl 实测登录 303→/admin 200 ✓ - 改口令会同时让旧会话失效(session MAC 混入口令摘要前缀)——实测无异常 - 遗留建议(P2,未实施,等批准):默认账号首次登录时页面没有任何「请修改默认口令」的提示, README 有写但站点不强制;可考虑 EnsureDefaultUser 建的账号加个首登 flash 提示 ### 默认口令警示 + 迷雾后台管理增强(22:30-22:45,主站已重启生效) - adminHome 检测 admin 口令仍为 rond-admin 时,后台首页顶部渲染警示条(含改密/2FA 链接); 实现:AdminData.DefaultPW = auth.VerifyPassword(p.User.Hash, "rond-admin"),仅后台首页每次渲染时验证一次 - 迷雾卡片增强:空态引导(尚未导入提示)、块/格/估算面积/最近更新时间(fog_blocks.max(updated_at), sql.NullTime + 模板函数 dtt 接受 *time.Time)、「清空迷雾」按钮(POST /admin/fog/clear,confirm 后 DELETE FROM fog_blocks + fogResetIndex() 换掉内存单例索引) - 模板函数注意:text/template 调函数时指针参数不会自动解引用,可空时间字段要么加 *T 版本函数, 要么模板里 if 判空后用 deref - 安全全链路实测:导出(299112B)→清空→空态→cover 重导→8701 块/575753 格/47.8km² 完整还原,瓦片 200 - 用户真实库迷雾是用户 22:26 自己在后台导入的(说明迷雾 UI 其实可用,只是缺状态展示让人困惑) ### 迷雾可视化 A+C + 传输层性能优化(23:50-00:25,主站已重启生效) - 迷雾配色改高对比:填充 #307ae0 a=132、边界 #103484 a=232;render.go 改为两遍渲染 (先填 mask,再按四邻域判定填充/描边),瓦片外邻格按「已探索」处理以免接缝处出现网格伪影。 实测贵阳 z12 瓦片:填充 5572px / 轮廓 663px ✓ - 前端加「迷雾浓度」滑杆(L.Control + localStorage rond.fogOpacity,默认沿用图层 opacity 0.9) - 性能:新增 internal/web/gzip.go(gzip 池 + 首字节判 content-type,PNG 透传); assets.go 计算静态文件哈希作 assetVersion,cacheStatic 对 ?v=匹配者给 immutable 一年; /map 不再内联 points(58.6KB→12.5KB,gzip 3.1KB)改走 /api/points;leaflet-heat 改为 首次勾选热力图时动态加载(ensureHeat) - 实测:/stats 38.3→5.4KB、/api/track 38.9→6.7KB、/report 6.3→2.0KB - 坑:模板函数传 *time.Time 不会自动解引用(另见上条);server.go 里 gzip 是包级函数不是 Server 方法 ### 访客可见区域白名单 + 地点别名/备注(01:00-01:30,主站已重启生效) - 区域白名单:setting.Settings.Regions(省/市/区任意层级);stats.Filter.Regions 在 visitsWhere 里 生成三段 IN 条件(province/city/district 各一份占位符);filterOf 里仅 `!p.IsAdmin()` 时注入, 因此地图/地点库/时间线/统计/报告全站生效、管理员全量。 实测:设「贵阳市」→ 匿名 73 点全为贵阳,管理员 283 点 ✓(测完已清空) - 地点备注:新表 place_notes(user_id, src_pk, alias, note),按 src_pk 而非 places.id 存, 重传备份不丢;internal/notes 包(All/Save,空则删记录)。Place 加 SrcPK/Alias/Note 三个字段, placeCols 末尾追加 p.src_pk(scanPlace 同步) - 显示约定:别名对外替代原名(applyNotes 里直接改 Name,各页面/地图气泡自动生效), 备注仅管理员可见(applyNotes 传 admin 标志) - 坑:applyNotes 传切片,单地点场景必须 `ps := []stats.Place{place}; ...; place = ps[0]`, 直接传字面量切片改不到原值 - 后台设置页新增「访客可见区域」textarea(一行一个),保存时按行 trim 去空 ### 备注可见性开关 + 后台备注管理页(01:25-01:35,主站已重启生效) - setting.Settings.NotesPublic(notes_public,默认 false);applyNotes 参数由 admin 改为 showNote = p.IsAdmin() || p.Settings.NotesPublic,别名始终对外、备注按开关 - 新增 GET /admin/notes(notesPage + templates/admin/notes.html):LEFT JOIN places 取地点名/城市/到访数, 支持 ?q= 搜索(别名/备注/地点名 ILIKE),每行就地保存/删除(删除=提交空值,Save 会自动删记录), 按 Referer 判断保存后回管理页还是后台首页 - 后台首页导航与地点详情页都已挂入口 - **事故与修复**:用 curl 单字段 POST /admin/settings 测区域白名单时,未提交 pages 字段导致 「公开页面」被清空(/map 等全部 404)。教训:settingsSubmit 是整体覆盖,任何脚本化测试都必须 把 pages/modules 等字段一并回传;已用全字段 POST 恢复 - Git Bash 里 `curl -o /dev/null` 偶发 exit 23(写入失败),改成 -o 文件更稳 ### 四个增强功能 F1–F4(20:5x 收尾,主站已重启实测) - F1 出行方式配色:apiTrack 返回 `modes` 图例(空方式归「步行/其他」灰色 #8e8e93), rond-map.js 渲染图例;实测 `modes:[步行/其他 370, 电🐔 317, 地铁 22, 高铁 21, 公交 17, 火车 9]` - F2 城市级聚合 + 首次到达里程碑: - 地图 `/api/cities` 按城市聚合到访/时长/首次(last)日期,圈大小随到访量;已剔除家/工作 - 报告页「🏅 首次到达的城市」里程碑:firstArrivalCities 查「有史以来首次到访落在本年」的城市 (HAVING min(arrival) in [from,to)),按日期升序;区域白名单对访客同样生效 - **实测发现并修复的 bug**:apiCityAgg 原 SQL 写 `AND NOT p.is_home AND NOT p.is_work`, 但 is_home/is_work 是 visits 列、places 表没有 → `SQLSTATE 42703` 直接 500。 改为 `NOT EXISTS (SELECT 1 FROM visits vh WHERE vh.place_id=p.id AND (vh.is_home OR vh.is_work))` - 报告页已验证 2025 年出现里程碑、/api/cities 返回 9 城带 first/last 日期 - F3 年度报告长图/PDF 分享:ReportData 加 Cities []CityMilestone;报告页加「复制链接」(`navigator.share` 优先、否则 clipboard) + 「保存为 PDF」(`window.print()`),app.css 加 `@media print` 隐藏导航/页脚/按钮、 卡片 break-inside:avoid。注:长图用浏览器打印生成 PDF(未接 html2canvas),README 已注明该取舍 - F4 从 GPX 补充轨迹:internal/gpx 解析 trk/trkseg/rte(WGS-84);adminGpxUpload 存 gpx_tracks, apiGpx 按 crs=gcj02 把 WGS-84 转 GCJ-02 对齐高德底图;地图橙色叠加 - **修复**:gpx.Parse 原来 `if len(out)>0 return` 在已有 trk 时直接返回、丢掉 rte;改为 trk 与 rte 都追加 - 实测:登录 admin/rond-admin → 上传含 trk+rte 的测试 gpx → /api/gpx 返回两条轨迹(测试数据已删除) - 验证手段:`go build` + `go vet` 全过;主站后台起、curl 实测各 API 与报告页;测试轨迹上传后清理 - README 已把 F1–F4 从「后续可以做的」挪到已实现章节,并补 GPX/城市聚合/报告分享说明