Files
rond-with-you/README.md
T
tamakyi 6b665b6056 图片恢复:坐标来源全部合并,认不出时说清已查过哪些来源
真数据实测(扫描 40 个对象、只挂回 7 张)暴露了第一版的漏洞:
报错「数据集 #1 里没有编号 389 的地点」——来源能读出来,却不是上传那会儿的编号空间。

根因:srcPKCoords 是「第一个能读到的来源就返回」,而**换基线会覆盖 baselines/<id>.sqlite**,
当前那份未必是当年上传时用的编号;旧编号往往还躺在同一份底座的 .bak 留档里。
教训很直白:**「能读出数据」不等于「读对了那一版」**。

改了三处:
1. 来源全部读入合并(先读的优先):库里 places → entity_raw 里 ZLOCATION 的整行留档
   (站点上删掉的地点,places 没了、留档可能还在)→ baselines/<id>.sqlite 及其每一份 .bak-*。
   读不动的文件跳过换下一个,不再「第一份成功就返回」。
2. 同一个编号保留多组候选坐标,逐个试到能在当前数据集里对上为止
   (站点纠过偏、或不同来源算出的坐标不一样,只认第一组会白白认不出)。
3. 认不出要能排查:理由写明「已查来源 …」与试过的坐标;提示条按**数据集归并**上报
   (「数据集 #1:31 张(编号 389、390…),已查来源 …」),不再刷「另有 31 张」;
   完整清单(前 30 条)进服务端日志。

新增用例:当前底座缺编号、换基线留档里才有(正是用户遇到的形状)⇒ 必须救得回;
以及「真的认不出」时要带出已查来源。全量验收 PASS=332 FAIL=0 SKIP=0。
2026-10-01 00:11:04 +08:00

375 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# rond-with-you
把 rond(iOS 应用 LifeEasy)导出的 `.rondbackup` 备份,变成一张可浏览、可编辑、可对外分享的个人足迹地图。
数据全在自己的 PostgreSQL 里,站点无外部 CDN 依赖,单文件二进制约 19MB,1 核 1G 的小机器就能跑。
## 前台功能
- **主页**:地点 / 城市 / 省份 / 记录天数 / 停留时长 / 出行里程等核心数字,足迹地图预览、类型分布、
城市排行、月度活跃度、活跃日历、出行方式、作息分布、标签、天气足迹、最近足迹——每个模块都能在后台开关
- **地图**:全屏地图,支持聚合、底图切换(街道 / 纯路网 / 卫星影像 / OSM)、
按时间范围 / 类型 / 城市 / 标签 / 最短停留 / 关键词筛选;
另可按需叠加**行程轨迹**、**到访热力**、**城市聚合**、**GPX 轨迹**
- **地点库**:全部地点的表格与搜索、排序、分页
- **地点详情**:位置、到访次数、累计停留、逐次到访记录、**图片画廊**、访客投票
- **时间线**:按天分组的到访流水
- **统计**:月度分布、类型分布、作息分布、城市 / 省份排行、星期分布、出行方式、标签、天气,
以及世界迷雾覆盖与各城市点亮率
- **年度报告**:`/report` 按年汇总,含「🏅 首次到达的城市」里程碑,可一键复制链接或打印成 PDF
- **专题**:把「一次旅行」这类线索单独成页——按时间段和 / 或「中心点 + 半径」圈定范围,
可附带迷雾层,适合分享某次出行的专属地图
- **PWA**:带 manifest 与图标,手机浏览器可「添加到主屏幕」独立窗口打开
## 后台功能
| 入口 | 能做什么 |
| --- | --- |
| 数据总览 | 上传 `.rondbackup`、切换/删除数据集、导入导出世界迷雾、导出数据 |
| 数据编辑 | 地点 / 到访 / 行程 / 活动类型 / 天气的增删改;带筛选与分页;坐标来源标记与筛选 |
| 照片导入 | 从相册批量补录地点(前端读 EXIF,可顺手把照片存为该地点的图片) |
| 地点备注 | 别名、备注、「推荐 / 踩雷」结论,支持批量操作 |
| 专题 | 新建 / 编辑 / 启停专题 |
| 设置 | 站点标题与头像、主题色、主页模块开关、访客可见页面、访客可见区域、隐家/隐工作、图片存储、默认底图 |
| 账号 | 改密码、两步验证(TOTP) |
### 数据编辑页上的「重复到访」
站点里可能多出一条一模一样的到访,成因有两种,卡片上会分别标出来:
- **站点侧副本**:同一次到访在站点存了两份,差别只在 UUID(站点侧生成的那份在手机包里找不到)。
合并与差异页都是按 UUID 认人的,于是它被当成「备份新增」。
- **补录行被合并回来**:站点**补录**的到访没有留档、也就没有 UUID;导出时会被补上一个确定性的
派生 UUID(`v5("ZVISIT/<编号>")`)写进包里,手机于是把它当成一条真记录。这份包再合并回站点时,
站点这侧没有 UUID 可对齐,又插了一条。
判定以**这份数据集的基线(导入时留下的手机包)**为参照(第二类不用基线)。卡片只在检出时出现在
数据编辑页,默认全选 + 二次确认;清理会连它的位移 / 天气 / 留档一起清——「补录行被合并回来」那类
会把位移 / 天气**改挂**到保留项(补录行自己名下没有位移,跟着删就白丢里程),并把指向它的合并
标记改指到保留项。基线不可用或与本站一条都对不上时,只判第二类,另给一行说明。
命令行有一条等价的工具 **只覆盖第一类**:`tools/dedup_site_visits.py`(默认只预演,加 `--apply` 才动手)。
### 为什么有些到访在页面上看不到
手机包里有两种「不独立成立」的到访:被别的到访**并入**了的(`ZVISIT.ZMERGEDTO`),
以及某个长驻记录下的**子段**(`ZVISIT.ZPARENT`)。rond 自己既不显示、也不把它们算进次数与停留,
本站因此也不算——时间线、统计、地点计数、地图城市圈都不含它们。
⚠️ 这些行**不删**:导出包必须原样带着它们,rond 要靠它们认这次合并发生过。
数据编辑页会照常列出,并打上「已并入 #562 / 子段 #374」,方便核对。
## 技术栈
| 层 | 选型 |
| --- | --- |
| 后端 | 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. 准备数据库
```bash
createdb rond_with_you
# 或:psql -U postgres -c "CREATE DATABASE rond_with_you ENCODING 'UTF8'"
```
表结构在程序启动时自动创建(`internal/db/schema.sql`,全部 `IF NOT EXISTS`,老库会自动增量升级)。
### 2. 配置
```bash
cp conf/app.example.ini conf/app.ini
vi conf/app.ini # 至少需要填 dsn
```
配置项全部在 `conf/app.example.ini` 里有注释说明;只有 `dsn` 必填。
### 3. 运行
```bash
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。导出走「基线库 + 覆盖」,不重建库
## 换基线(让导出底稿跟着手机换代)
导出**不是**从零重建数据库,而是以一份「底座」(`storage/baselines/<数据集>.sqlite`)为底,
只覆盖站点托管的那几张实体表。Core Data 元数据、索引、持久化历史、`ZRAWVISIT` 原始采样点、
`ZTRIP*` 等一律原样带出来——**所以底座必须是手机导出的包**,拿本站导出的包当底座会越导越失真。
底座一旦定下就跟着数据集走。手机那边重新导出后想换代,以前只能整包重新导入成新数据集
(站点侧补录的行、主题、备注都得重新收拾)。后台「换基线」就是给同一份数据的底座换代用的:
- **站点数据一行不动**,只换底稿;换完导出立刻以新底座为准。
- 两道判据:① 这份包与当前数据集得是同一批数据(按到访 UUID 认,低于一半要显式确认,
一条都对不上直接拒绝);② 这份底座得真的能导出(先用它干跑一次导出,失败就放弃替换)。
- 替换前旧底座自动留档(最近 3 份),换错了在页面上一键回滚;回滚本身也留档,可再滚回来。
- 换过的底座记档在 `datasets.baseline_name/baseline_sha256`:底座文件丢了会**按记档重建**,
而不是退回最初那份旧包(否则上一次换基线会静默失效)。
## 地点图片
- **字节在浏览器里转成 WebP** 再上传,服务端只做形状校验、不重新编码(1 核机器扛不住解码大图)。
- **存哪边**由后台设置决定:服务器本地磁盘(`<data_dir>/media/`)或对象存储(S3 / R2)。
对象键是 `<数据集>/<上传月份>/<地点号>-<内容哈希>.webp`,按月份分目录、带内容哈希天然去重。
- **图片认地点靠坐标键**,跟备注、结论同一套:换数据集之后同一个地点照样看到同一张图。
- ⚠️ **早期版本图片记录挂在数据集上**(`place_images.dataset_id` 带 `ON DELETE CASCADE`),
于是「删掉旧数据集」会把图片记录一起删掉,**而文件还留在磁盘上**——读取按坐标键、行却按数据集级联,
两套口径打架,实测丢过一批图。现在外键已摘掉,删数据集不再带走图片;已经丢掉的那批可以恢复:
后台首页「图片挂载恢复 → 扫描并恢复」,它按坐标把还在的对象挂回当前数据集的地点上
(只插记录、不动对象、可反复执行)。**本地磁盘与对象存储都扫**:
本地走目录遍历,对象存储走 ListObjectsV2 列桶——行没了就只剩桶里有那些键,不列桶无从得知它们叫什么。
⚠️ 列桶需要凭据上有 **`s3:ListBucket`** 权限(R2 的 API Token 默认只给对象读写),
没权限时本地那半照救,页面会把该开哪个权限说明白。
认地点要用旧数据集留下的底座库(`<data_dir>/baselines/<id>.sqlite`)来还原「地点号 → 坐标」,
**所以别删那个文件**,换基线留下的 `.bak-*` 留档也别清——换过的底座未必还是上传那会儿的编号空间,
旧编号往往只在留档里(来源会全部读进来合并,逐个试到对上为止)。都没有才认不出,报告里会列出来。
- **删除数据集**只清掉从备份导来的记录与原始行留档;备注、结论、图片按坐标键保留。
- **专题归人不归数据集**:专题是「一段时间 + 一块区域」的定义,换数据集之后照旧成立
(内容照当前生效数据集算),删掉旧数据集不会再把它带走。
## 照片导入(补录没有轨迹的时间段)
手机里的照片带着拍摄时间和 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 小机器)
```bash
# 交叉编译 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` 可以做导入→导出→逐表对账,检查改动有没有破坏备份保真度。
- 全量验收 `scripts/verify.sh`(要 **0 FAIL / 0 SKIP**)。每个夹具都会导入一整包再删掉,
一轮下来测试库能留几百 MB 死元组,级联删除随之变慢(实测 16MB 时约 6 分钟跑完、
几百 MB 时半小时都跑不完,看着像卡死)。脚本会在库超过 `VACUUM_MIN_MB`(默认 200MB)
时先清一次死元组;手动来一次 `VACUUM (FULL, ANALYZE)` 还能把文件也缩回去。
另外要确认库里有生效数据集(`datasets.is_active`),否则 `internal/stats` 的对照用例会假 skip。