导出是「底座为底 + 只覆盖托管表」,所以底座必须是手机导出的包。底座跟着数据集走, 手机重新导出后想换代,以前只能整包重新导入成新数据集(站点补录的行、主题、备注都得重收拾)。 新动作 /admin/baseline(后台首页加入口)就是给同一份数据的底座换代用的。 三道闸,顺序不能挪: 1. 重合判据:新包里的到访 UUID 与站点侧带 UUID 的到访比。一条都对不上直接拒 (连 force 也不给过)——干跑只验结构,一份不相干的包照样导得出,但换下去站点 每行都在新底座里认不出身份,全按新行插到最大编号之后,真机上就是「这些到访 全变成新的」。重合率不到一半要显式确认;判据没过不当场报错了事:包已经收进 uploads,页面给一个「我确认,强制替换」按钮,不必重传几十 MB。 2. 干跑导出:底座缺表/缺实体编号在这步挡住,不会出现「换完了才发现导不出、 原来那份已经不知去哪」。 3. 旧底座留档 + 记档,换错了可一键回滚。 两个坑: - 留档名不能只用秒级时间戳。「替换后紧接着回滚」落在同一秒时,回滚那次 rename 会把 刚生成的留档覆盖掉,于是回滚回了个寂寞(TestRollbackBaselineRestoresPrevious 当场红)。 改用 baselineBackupName():20060102-150405 + 冲突自增。 - ensureBaseline 必须先看记档。只认 datasets.sha256(= 最初那份包)的话,底座一丢就静默 重建回旧包,上一次换基线白做且毫无提示。抽出 ensureBaselineTo(ctx, dsID, dst) 共用; 「重建」入口写临时文件再 rename,不做破坏性删除。 回归用例 7 条(internal/web/baseline_swap_test.go):成功路径断言「换底座前后导出包里 ZVISIT 的 UUID 集合逐条一致」——编号会换、身份不该换,谁在底座里没对上立刻亮; 记档优先重建带反证(清掉记档后同一条路必须走不通);部分重合需确认、完全对不上 force 也不给过、非包文件第一步就挡住、回滚还原且当前那份也留档、页面渲染到底。 验收:./scripts/verify.sh 全量 PASS=318 FAIL=0 SKIP=0。
354 lines
25 KiB
Markdown
354 lines
25 KiB
Markdown
# 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`:底座文件丢了会**按记档重建**,
|
||
而不是退回最初那份旧包(否则上一次换基线会静默失效)。
|
||
|
||
## 照片导入(补录没有轨迹的时间段)
|
||
|
||
手机里的照片带着拍摄时间和 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。
|