tamakyi b4c2a8452f 审计修复:后端(安全 / 数据一致性 / 性能)
audit-2026-09-21.md 里的后端条目落地。每条都配了回归守卫,并逐条做过变异验证
(注入原 bug → 确认按正确理由变红 → 还原 → 变绿)。

安全
- CSRF 只加同源 Origin/Referer 守卫(csrf.go),不动模板
- 会话 cookie 的 Secure 按请求判断;两步验证两闸(loginGuard + 验证码一次性)
- 跳转收口到 safeInternalPath;clientIP 只认 XFF 最后一段
- GPX 按属主圈定且查 RowsAffected;下载名走 downloadDisposition
- 上传口统一 MaxBytesReader;落盘后的失败路径清掉半成品

数据一致性
- 区域判定两端各过一遍再 AND(apiTrack 与 movementsWhere 对齐)
- Filter.Verdict 全域一致(下推到 visitsWhere);聚合刷新必须返回 error
- 附属数据改挂坐标键,Go 与 SQL 两套表达式逐位一致
- 脏备份的伪时间/越界坐标不再进领域模型;rows 循环统一 drainRows
- 合并与导出:writeRawRow 上抛错误、悬空引用清理、原生列补全

性能
- placeCols 相关子查询带 dataset_id 前导列;findSourceArchive 先按大小筛
- 批量勾选改 ANY($3::bigint[]);fillupCache 加上限;同一查询不再查两遍

另:新增「全新库」冒烟测试 —— 空库跑完 Migrate 再用应用侧读查询真打一遍,
迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
2026-09-24 20:37:16 +08:00
2026-09-18 13:05:49 +08:00
2026-09-18 13:05:49 +08:00
fix
2026-09-19 19:50:27 +08:00
2026-09-18 13:05:49 +08:00
2026-09-18 13:05:49 +08:00
2026-09-18 13:05:49 +08:00
2026-09-18 13:05:49 +08:00
2026-09-18 13:05:49 +08:00
2026-09-18 15:49:54 +08:00

rond-with-you

把 rond(iOS 应用 LifeEasy)导出的 .rondbackup 备份,变成一张可浏览、可编辑、可对外分享的个人足迹地图。

数据全在自己的 PostgreSQL 里,站点无外部 CDN 依赖,单文件二进制约 19MB,1 核 1G 的小机器就能跑。

前台功能

  • 主页:地点 / 城市 / 省份 / 记录天数 / 停留时长 / 出行里程等核心数字,足迹地图预览、类型分布、 城市排行、月度活跃度、活跃日历、出行方式、作息分布、标签、天气足迹、最近足迹——每个模块都能在后台开关
  • 地图:全屏地图,支持聚合、底图切换(街道 / 纯路网 / 卫星影像 / OSM)、 按时间范围 / 类型 / 城市 / 标签 / 最短停留 / 关键词筛选; 另可按需叠加行程轨迹、到访热力、城市聚合、GPX 轨迹
  • 地点库:全部地点的表格与搜索、排序、分页
  • 地点详情:位置、到访次数、累计停留、逐次到访记录、图片画廊、访客投票
  • 时间线:按天分组的到访流水
  • 统计:月度分布、类型分布、作息分布、城市 / 省份排行、星期分布、出行方式、标签、天气, 以及世界迷雾覆盖与各城市点亮率
  • 年度报告:/report 按年汇总,含「🏅 首次到达的城市」里程碑,可一键复制链接或打印成 PDF
  • 专题:把「一次旅行」这类线索单独成页——按时间段和 / 或「中心点 + 半径」圈定范围, 可附带迷雾层,适合分享某次出行的专属地图
  • PWA:带 manifest 与图标,手机浏览器可「添加到主屏幕」独立窗口打开

后台功能

入口 能做什么
数据总览 上传 .rondbackup、切换/删除数据集、导入导出世界迷雾、导出数据
数据编辑 地点 / 到访 / 行程 / 活动类型 / 天气的增删改;带筛选与分页;坐标来源标记与筛选
照片导入 从相册批量补录地点(前端读 EXIF,可顺手把照片存为该地点的图片)
地点备注 别名、备注、「推荐 / 踩雷」结论,支持批量操作
专题 新建 / 编辑 / 启停专题
设置 站点标题与头像、主题色、主页模块开关、访客可见页面、访客可见区域、隐家/隐工作、图片存储、默认底图
账号 改密码、两步验证(TOTP)

技术栈

层 选型
后端 Go 1.25(标准库 net/http + html/template,无 Web 框架)
数据库 PostgreSQL(pgx/v5)
备份解析 纯 Go SQLite 驱动 modernc.org/sqlite,零 cgo
前端 手写 CSS + Leaflet(本地内置),零外部 CDN 依赖
地图底图 高德(AutoNavi)栅格瓦片,可切换为天地图 / 自备瓦片
图片存储 服务器本地磁盘,或任意 S3 兼容对象存储(Cloudflare R2 等)
图片转码 浏览器 canvas 转 WebP,服务端只收成品、不解码
口令 PBKDF2-SHA256(21 万次迭代,标准库实现)
会话 HMAC-SHA256 签名 Cookie,改密后旧会话自动失效

快速开始

1. 准备数据库

createdb rond_with_you
# 或:psql -U postgres -c "CREATE DATABASE rond_with_you ENCODING 'UTF8'"

表结构在程序启动时自动创建(internal/db/schema.sql,全部 IF NOT EXISTS,老库会自动增量升级)。

2. 配置

cp conf/app.example.ini conf/app.ini
vi conf/app.ini     # 至少需要填 dsn

配置项全部在 conf/app.example.ini 里有注释说明;只有 dsn 必填。

3. 运行

go build -o rond .
./rond -c conf/app.ini

首次启动会创建管理员账号,用户名取环境变量 ROND_ADMIN_USER(默认 admin), 密码取 ROND_ADMIN_PASSWORD(默认 rond-admin)。登录后请立即在后台修改密码。

打开 http://localhost:8080,进入 /admin 上传 .rondbackup 文件即可。 /healthz 是给探活用的轻量健康检查。

备份解析说明

.rondbackup 本质是一个 zip,内含 Core Data 的 SQLite 库 LifeEasy.sqlite 以及同名 -wal / -shm。 数据绝大部分写在前端 WAL 里,因此解析时三个文件必须一起解出再打开,否则会丢掉几乎全部记录。

导入时会做这些转换:

来源表 内容 落到
ZVISIT 到访(到达 / 离开、类型、地点、备注、是否收藏) visits
ZLOCATION 地点(名称、经纬度、城市、省份、POI 分类) places
ZACTIVITY 自定义类型(颜色、图标、是否「家 / 工作」) activities
ZTAG / Z_11VISITS_ 标签与访问标签关联 tags / visit_tags
ZMOVEMENT 位移段(交通方式、起止时间) movements,里程按 haversine 估算
ZHOURLYWEATHER 逐小时天气 weather

其他处理:

  • Core Data 时间戳以 2001-01-01 UTC 为基准,解析时统一转为标准时间
  • 到达 / 离开时间恰好缺失的空记录会被跳过
  • 一次上传 = 一份完整快照(一个数据集),页面始终只展示其中一份
  • 两道判重:同一个文件按 SHA-256 直接复用已有数据集;内容相同但文件字节不同的 (rond 每次重新导出,WAL 与时间戳都会变)按内容指纹识别,同样不会新建快照
  • 新导入的快照自动接管展示,旧快照留在后台可以随时切回或删除;删除数据集会连同其下 全部记录一并移除
  • 每一行的原始记录都留着(entity_raw 表)。这是导出能还原的前提:站点的管理列由站点 覆盖,其余列一律按导入时那一行的原值写回,不会因为中间改过备注就丢掉 rond 自己写的字段
  • 回导:后台可导出修改过的 .rondbackup(含补录的记录,坐标为 GCJ-02 原样写回), 可直接导回 rond App。导出走「基线库 + 覆盖」,不重建库

照片导入(补录没有轨迹的时间段)

手机里的照片带着拍摄时间和 GPS,可以批量补成「地点 + 到访」。

  • 照片不用上传:EXIF 在浏览器里读(static/js/photo-exif.js),只把坐标与时间提交上来。 一次几十张、几百 MB 的相册全传一遍既慢又占磁盘——早期版本真这么干过,51 张 334MB 直接把请求撑爆。
  • 坐标系:EXIF 的 GPS 按标准是 WGS-84,导入页默认勾「WGS-84」,服务端转成 GCJ-02 再落库。 如果照片里的坐标已经是纠偏过的(某些安卓相册导出),要改选「已经是 GCJ-02」,否则整批会偏 500 米左右。
  • 存图(可选):每行可勾「存图」,把这张照片转成 WebP 存为该地点的图片(见下节)。 默认不勾——一次几十张全存会占不少空间,也容易把相册里的截图一起带进来。

地图与坐标

  • rond 存入库的经纬度已经是 GCJ-02(火星坐标)。 iPhone 在国内走的是高德数据源, ZLOCATION / ZRAWVISIT.ZLATITUDE 都是纠偏后的坐标(实测 2246 个采样点与 GCJ-02 理论偏移的 残差为 0 米),原始 WGS-84 GPS 另存于 ZRAWVISIT.ZRAWLATITUDE。
  • 因此底图只要是 GCJ-02(高德 / 天地图),直接绘制即可,不要再做任何纠偏;只有当底图是 WGS-84 时,才需要 GCJ-02 → WGS-84 逆变换。static/js/rond-map.js 会读 /api/map-config 返回的 crs 自动决定是否变换,切换底图时会重新投影已有标记。
  • 底图在 conf/app.ini 里配:map_provider = amap(默认,免密钥)/ tianditu(需 map_key)/ custom(自备 map_tile_url 与 map_crs)。填了 map_key 后瓦片改由服务端代理 (/tiles/{layer}/{z}/{x}/{y}),密钥不会出现在页面源码里。
  • 内置底图:高德街道(免密钥)、高德纯路网(只画道路,不带 POI 与路名标注)、高德卫星影像、 OpenStreetMap(免密钥全球兜底)。高德瓦片连续加载失败时前端会自动切到 OSM 并弹提示, 出境数据也能正常显示;自定义瓦片源请自行确认符合国家地图数据规定,以保证国界、 台湾省、南海诸岛等要素的绘制符合国家标准。
  • 默认铺哪张底图在后台「设置 → 地图底图」里选,不在 app.ini 里。 点位密集时换成纯路网最清爽(实测同一块瓦片的文字像素占比从 1.5% 降到 0.2%); 访客在地图上手动切换过的话,浏览器会记住他自己的选择,优先于后台默认值。
  • 个人位置数据保存在自己部署的数据库里,站点也提供了「隐藏家 / 隐藏工作 / 过滤短停留」的隐私开关; 默认隐藏「家 / 工作」,仅站长登录后可通过「含家/工作」开关临时显示。
  • 坐标来源留档:每个地点都记着它是从哪来的——coord_source(rond 导入 / photo 照片导入 / manual 后台新增)、coord_sys(提交时是哪个坐标系)、以及换算前的原始坐标。 不变式是「显示坐标 = 按 coord_sys 换算原始坐标」,随时可复算、可核对; 后台地点列表带来源标记与筛选,行内有「按坐标刷新地址」按钮。 这一栏是踩过坑才加的:2026-09 有一次照片导入把坐标系选错,63 个点整体偏 500~670 米, 当时库里没有任何线索,只能逐张翻原图才查出来。

地点图片

给地点配图,访客在地点详情页点「图片」即可全屏浏览。

  • 转码在浏览器里做:选中的原图先经 canvas 转成 WebP(长边 2000px、质量 0.82), 再把成品传给服务端。1 核的服务器解一张 4000px 的原图要好几秒,几张就能卡住整站; 用户自己的手机转一张只是几十毫秒的事。服务端只做文件头校验后原样写出去,不做解码。
  • 存哪儿可选:后台「设置 → 图片存储」切换
    • 服务器本地磁盘(默认):落在 data_dir 下的 media/,由站点自己的 /media/ 直出
    • 对象存储:S3 兼容协议(Cloudflare R2、MinIO、AWS S3 等),凭据写在 conf/app.ini 的 s3_*,不进数据库、不下发前端(服务端签名后直接 PUT)
    • 每张图各自记着自己实际存在哪,换存储后端不影响已有图片(旧的仍按原位置取,不会变破图)
  • 对象命名:<数据集>/<上传月份>/<地点号>-<内容哈希>.webp
    • 按月份分目录——一次出行传几十张,按月归堆最好找、也最好整月备份或迁移; 若按地点分目录,几百个地点会摊出几百个大多只有零张图的空夹
    • 文件名带地点号,在对象存储控制台里一眼看出这张是谁的
    • 带内容哈希,同一张图重复上传落到同一个 key,天然去重
    • 本地存储删掉最后一张时会顺手收掉空的月份目录
  • 上限:单张 6MB、单个地点 12 张;照片导入整包请求上限 60MB(勾超过 60 张会先提示分两次)
  • 上传即生效,没有「保存」这一步:面板里会实时显示转码 / 上传进度、当前张数, 传完缩略图和页头的「图片 N」按钮会立刻出现。页头那个按钮是访客入口,站长自己也能点开预览

备注、别名与表态

  • 地点别名与备注:地点详情页可填「别名」和「备注」。别名会替代原名显示在所有页面上 (把「未命名地点」改成「外婆家」);备注默认只有站长自己能看到,可在「站点设置 → 地点备注对访客公开」打开对外展示。 两者按 rond 的地点 ID(src_pk)单独存表,重新上传备份、地点换了数据库 id 也不会丢。
  • 推荐 / 踩雷:与自由文本备注分开的两类表态,用按钮交互。
    • 站长结论(place_votes,按 src_pk 存):详情页「我的结论」一键 👍推荐 / 👎踩雷, 再点已选按钮取消;后台备注页也能逐行或批量打标。
    • 访客投票(place_visitor_votes):详情页「大家怎么说」可点赞 / 点踩,一人一票、可改可撤; 匿名身份由 cookie + IP 派生并哈希,清 cookie 也会被识别为同一来源,起轻度防刷作用。
    • 展示:地点详情、地点库(行内徽章 + 「推荐 / 踩雷」筛选)、地图气泡;统计页有 「推荐 & 踩雷」区块(站长结论计数、访客票数、两边榜单)。是否对访客公开由 「站点设置 → 推荐 / 踩雷标记对访客公开」控制(默认公开)。
  • 备注批量管理:后台「地点备注」页列出当前数据集的全部地点(不只已写备注的), 支持关键词 / 城市 / 状态(全部 / 有备注 / 无备注)/ 排序筛选与分页;每行可就地编辑别名、 备注并快速打标。顶部批量条可对勾选项批量设置结论 / 清空备注(保留别名)/ 删除备注; 给原本没有备注的地点直接填写保存即等于新增。挂在当前数据集已不存在地点上的「孤儿备注」 单独列在底部便于清理。

轨迹、热力与年度报告

  • 行程轨迹:地图页勾选「行程轨迹」,按时间顺序把位移段画成连线,颜色跟随 rond 的 交通方式配色(没记录交通方式的段归「步行 / 其他」灰色);右下角自动出现图例, 数据来自 /api/track,随筛选条件联动。
  • 城市级聚合:地图页勾选「城市聚合」,按到访量加权画出城市圈,圈越大表示到访越多, 点开 popup 可见该城市的首次 / 末次到访日期;数据来自 /api/cities(已自动剔除「家 / 工作」)。
  • 到访热力:地图页勾选「到访热力」,按到访频次渲染热力图(内置 leaflet-heat)。
  • 年度报告:/report 按年展示到访、新地点、城市、活跃天数、里程、南北两端与交通方式, 迷雾面积为累计口径;并单列「🏅 首次到达的城市」里程碑——该城市有史以来第一次到访落在本年的, 按到达日期排序。报告页右上角有「复制链接」与「保存为 PDF」(调用浏览器打印,已配 @media print 样式,隐藏导航与按钮、卡片避免跨页,直接生成清爽的长图 / PDF 便于分享)。
  • GPX 补充轨迹:后台上传 .gpx(或 .xml)文件,解析其中的 trk(轨迹段)与 rte(路线点), 按 haversine 估算里程后存入 gpx_tracks,地图上勾选「GPX 轨迹」即用橙色叠加真实路径。 库里存的是 WGS-84(GPX 标准),高德 / 天地图等 GCJ-02 底图会按点做 WGS-84 → GCJ-02 正变换, 和足迹点对齐;可在后台删除已导入轨迹。

世界迷雾(Fog of World)

后台支持导入 Fog of World 导出的 .fwss 快照,把「已探索区域」作为半透明图层叠加在足迹地图上。

  • 格式:.fwss 是 zip,Model/*/ 下每个文件是一块瓦片(全球 512×512 块瓦片 × 128×128 块 × 64×64 格位图),内容 zlib 压缩;文件名由瓦片编号经 md5 前缀与两张置换表编码而成。
  • 坐标系:迷雾网格是 Web Mercator 投影的 WGS-84(实测确认:与 rond 地点做 GCJ→WGS 转换后 90% 落差在 50m 内、中位 7m)。因此画在 GCJ-02 底图(高德)上时,服务端按块中心做 WGS→GCJ 正变换再渲染(/tiles/fog/{z}/{x}/{y}?crs=gcj02);WGS-84 底图则原样绘制。前端切换底图坐标系时 会自动换 URL 重新拉取迷雾瓦片。
  • 导入模式:叠加(默认)把新快照按位或合并进现有迷雾,适合逐步追加新路线;覆盖先清空 再整体替换,适合迁移或纠错。同一块内的位图按格合并,不会互相抹掉。后台卡片会显示当前 已入库的块/格数、估算面积与最近更新时间,并提供导出与「清空迷雾」(需二次确认)操作。
  • 导出:后台「导出当前迷雾」把库里的迷雾还原成标准 .fwss(仅 Model/*/),可直接导回 Fog of World 或迁移到其他部署。已验证导出→再导入格数无损。
  • 迷雾遮罩区:可以按「圆心 + 半径」定义若干对访客隐藏的区域,比如把常住地那片抹掉。 遮罩只对访客生效,站长永远看到完整迷雾;被遮的格子不画填充、也不当成「未探索」的邻居, 免得遮罩边缘描出轮廓反而暴露范围。
  • 迷雾浓度:地图右下(窄屏左下)的图层控件旁有「迷雾浓度」滑杆,可按底图明暗调节透明度, 选择会记在本地浏览器。
  • 统计页有「世界迷雾覆盖」区块:估算已探索面积(格边长 9.55m × cos(纬度) 修正)与 各城市「点亮率」(地点落在已探索格内的比例)。

账号安全与公开页面

  • 两步验证(TOTP):后台「两步验证」页扫码或手输密钥绑定 Google/Microsoft Authenticator、 1Password、Aegis 等验证器(RFC 6238,±1 窗口容错)。开启后登录分两步:口令通过后先签发 5 分钟有效的中间令牌(绑定口令摘要,改密即失效),再输入 6 位动态码才发正式会话。 关闭两步验证同样要求新鲜的动态码,防止会话被劫持后直接拆掉 2FA。
  • 登录验证码:登录页带图形验证码,防脚本撞库。
  • 公开页面开关:后台「站点设置」可勾选对外展示的页面(地图 / 足迹 / 时间线 / 统计 / 报告)。 关闭的页面对未登录访客不开放,导航栏同步隐藏;管理员始终可见。
  • 访客可见区域:后台「访客可见区域」一行填一个,省 / 市 / 区任意层级都认 (如「广东省」「贵阳市」「南明区」),并可选择白名单(只放行这些区域)或 黑名单(藏起这些区域)。地图、地点库、时间线、统计、报告、地点详情全套生效, 管理员登录后仍是全量。 访客点到被隐藏的地点详情时不再直接甩 404,而是给一页说明「这个地点没有对访客开放」 并引导回首页(4 秒后自动跳转,也可手动点按钮)——404 会让人以为站点坏了。 代价是能反推出该 ID 确实存在;如果更在意这一点,把 internal/web/notice.go 里的 placeHiddenPage 改回 http.NotFound 即可。

目录结构

main.go                     启动入口
conf/app.example.ini        配置样例(真实的 app.ini 请勿提交)
internal/
  config/                   配置解析
  db/                       连接、迁移、schema.sql
  rond/                     .rondbackup 解压与 Core Data 解析
  ingest/                   解析结果写入 PostgreSQL;坐标来源留档
  stats/                    全部页面查询(统一筛选条件)
  auth/                     口令、签名会话与 TOTP
  setting/                  站点设置(JSONB)
  notes/                    地点别名 / 备注
  votes/                    地点「推荐 / 踩雷」结论与访客投票
  media/                    地点图片存储(本地磁盘 / S3 兼容对象存储 + SigV4 签名)
  gpx/                      GPX 轨迹解析
  geo/                      haversine 与坐标系纠偏(全站唯一的坐标换算实现)
  fog/                      世界迷雾 .fwss 解码 / 编码 / 入库 / 瓦片渲染
  web/                      路由、处理器、模板与静态资源
    static/js/photo-exif.js 浏览器端读 EXIF
    static/js/place-image.js 浏览器端转 WebP 并上传
storage/                    上传文件、解析中间目录与本地图片(运行时生成)
tools/                      维护脚本(备份往返校验、坐标审计等)

部署建议(1C1G 小机器)

# 交叉编译 Linux 单文件
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o rond .

配合 deploy/rond-with-you.service 用 systemd 托管即可,程序本身无其他运行时依赖, 也不需要 libwebp 之类的原生库(图片转码在浏览器端完成,见上文)。

传输层与运行时优化(已内置)

  • gzip:HTML / CSS / JS / JSON 在线压缩(图片本身已压缩,直接透传)。 实测 /stats 38KB → 5.4KB、/api/track 39KB → 6.7KB。
  • 静态资源强缓存:页面链接都带 ?v=<静态文件内容哈希>,版本一致才返回 max-age=31536000, immutable;重新编译后哈希变化,旧缓存自动失效。
  • 地图数据按需加载:/map 首屏只出页面骨架(12.5KB,gzip 后 3.1KB),地点数据走 /api/points 并行拉取;轨迹与热力图都是勾选后才请求,热力图库也是首次勾选时才下载。
  • 连接池:PostgreSQL 建连接比查询贵两个数量级,因此 MaxIdleConns 与 MaxOpenConns 相等(8/8)且启动时预热;页面内的独立查询并发跑(上限 6)。
  • debug = true 时会打印超过 30ms 的慢查询,便于定位。

若前面还挂了 Nginx,注意不要重复 gzip,且需把 Accept-Encoding 透传给后端。

开发提示

  • 模板走 //go:embed,改了 .html 必须重新编译;debug = true 时 CSS / JS 直读磁盘,改样式免重编。
  • 时间、坐标这类「原样回贴」的字段有讲究:表单精度(分钟 / 6 位小数)往往低于库里真值, 保存时若与库里已有值落在同一精度上要沿用库里那份,否则只改个备注也会把时间或坐标悄悄改掉。
  • tools/verify_rondbackup.py 可以做导入→导出→逐表对账,检查改动有没有破坏备份保真度。
S
Description
把你的Rond和Fog of World数据进行整合,分享,展现你的一生轨迹。
https://life.shiroko.one
Readme
5.6 MiB
Languages
Go 69.2%
HTML 17.6%
JavaScript 5.6%
CSS 3.7%
Python 3.4%
Other 0.5%