# 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`)。**登录后请立即在后台修改密码。** 打开 ,进入 `/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 核机器扛不住解码大图)。 - **存哪边**由后台设置决定:服务器本地磁盘(`/media/`)或对象存储(S3 / R2)。 对象键是 `<数据集>/<上传月份>/<地点号>-<内容哈希>.webp`,按月份分目录、带内容哈希天然去重。 - **图片认地点靠坐标键**,跟备注、结论同一套:换数据集之后同一个地点照样看到同一张图。 - ⚠️ **早期版本图片记录挂在数据集上**(`place_images.dataset_id` 带 `ON DELETE CASCADE`), 于是「删掉旧数据集」会把图片记录一起删掉,**而文件还留在磁盘上**——读取按坐标键、行却按数据集级联, 两套口径打架,实测丢过一批图。现在外键已摘掉,删数据集不再带走图片;已经丢掉的那批可以恢复: 后台首页「图片挂载恢复 → 扫描并恢复」,它按坐标把盘上还在的文件挂回当前数据集的地点上 (只插记录、不动文件、可反复执行)。认地点要用旧数据集留下的底座库(`/baselines/.sqlite`) 来还原「地点号 → 坐标」,**所以别删那个文件**;底座也没了就认不出来了,报告里会列出来。 - **删除数据集**只清掉从备份导来的记录与原始行留档;备注、结论、图片按坐标键保留。 专题(`topics`)目前仍旧随数据集删除——同一个数据集删掉后要重建。 ## 照片导入(补录没有轨迹的时间段) 手机里的照片带着拍摄时间和 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。