你要改的那个「海滩」原来根本不在数据里——它是 Go 里硬编码的 map,所以改不动。
更糟的是这样的 map 有**两张**,而且对不上:
MKPOICategoryFoodMarket 菜市场(编辑页) / 食品市场(补全页、GPX)
MKPOICategoryCafe 咖啡馆 / 咖啡厅
MKPOICategoryFitnessCenter 健身房 / 健身中心
外加 ATM / Atm 大小写各写一份、一条永远匹配不上的 MKPOICategoryMarina2
现在合成一张内置表(42 条,冲突处取编辑页那套),并在它上面加了一层站点覆盖:
- 新表 category_labels(value, label),站点级、不挂 dataset——值本身才是稳定键
- 端点 POST /admin/edit/category/label;传空 label = 删掉覆盖行、恢复内置默认名
- 类别台账那行改成两个带标签的输入框:
导出值 [MKPOICategoryBeach] ← 会跟着导出写回手机
显示名 [ ] ← 只影响站内显示,placeholder 写着默认值
是 MKPOICategory 枚举但内置表里没有的,会提示「填一个中文名」,这就是你要的补全入口
**显示名绝不写进 places.poi_category**——中文跟着写进去就把导出数据污染了,
专门加了 TestCategoryLabelNeverTouchesValue 钉这条。显示名会贯穿到所有中文名的地方:
编辑页/照片页/补全页的下拉、补全页「在用」标签、访客地点页、GPX 的 desc。
连带删掉两个绑在旧表上的模板函数 catName / poiname:模板里算不出显示名(要读站点覆盖),
改成在 Go 侧算好塞进数据(FillupRow.CatLabel、PlaceData.CategoryLabel)。
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 在线压缩(图片本身已压缩,直接透传)。
实测
/stats38KB → 5.4KB、/api/track39KB → 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可以做导入→导出→逐表对账,检查改动有没有破坏备份保真度。