用户要求这份记忆跟着仓库走,而不是只留在本机。 .gitignore 里挡路的是 `.workbuddy/`。改成反忽略六行,并且**只放行 .md**: 别用 `!.workbuddy/memory/**`——它会把 memory 下任何文件都放行,本项目真发生过 `web.test.exe` 混进暂存。复核用 `git check-ignore -q --no-index` 逐条打靶: memory/*.png、memory/sub/x.md、memory/MEMORY.md.bak、plans/a.md 全 IGNORED, 17 个 .md 全 included;`git add -An .workbuddy/` 恰好 16 个文件。 其余 .workbuddy 内容(计划、本地设置、临时产物)继续忽略。记忆里没有真凭据 (唯一命中是 2026-09-14.md 里的 `password=rond-admin`,那是项目默认口令)。 顺带做两件整理: 1. 取证了注入上限。以前按「字节数」判断 MEMORY.md 是否超限是错的: WorkBuddy 的 `getWorkingMemoryContent()` 用 `MAX_MEMORY_CHARS = 1e4` 比 JS `content.length`,即**字符数**(中文 1 字算 1);超了 substring 截断、尾部静默丢失。 本 details 与每日日志都不参与注入,只有 MEMORY.md 受约束。 现已从 6604 字符收到上限的 66%,并把这条上限写进索引抬头。 2. 把「备份合并」「导出/恢复三条铁律」两段长文迁进 MEMORY-details.md 的卅一/卅二节, 索引只留关键词指针。行尾统一为 LF(仓库 .gitattributes 是 `* text=auto eol=lf`)。
19 KiB
2026-09-14
rond-with-you:把 rond 备份变成足迹站点(从零建成可运行版本)
背景
用户提供 data/20260914_151649.rondbackup(3.4MB),要求做一个后台可上传该文件、
前台展示足迹地图 / 类型 / 日期筛选的站点。技术栈由用户指定为 PostgreSQL(本地测试环境
postgres / 127.0.0.1:5432),后端语言 Python 或 Go 自选。
备份结构(已实测确认)
.rondbackup 是 zip,内含 Core Data 库 LifeEasy.sqlite + -wal + -shm。
关键点:数据几乎全在 WAL 里(主库 348KB / WAL 3MB),必须三个文件一起解出再打开,
否则只能读到极少量记录。Core Data 时间戳基准为 2001-01-01 UTC。
主要表与本次数据量:ZVISIT 824、ZLOCATION 323、ZRAWVISIT 2246、ZMOVEMENT 791、
ZACTIVITY 30、ZHOURLYWEATHER 557、ZTAG 8、ZTRANSPORT 5、ACHANGE 4810(未导入)。
标签关联表命名规则为 Z_<实体号>TAGS_,如 Z_11VISITS_ = Visit↔Tag(11=Tag,18=Visit)。
导入结果:823 条到访(跳过 1 条到达时间缺失的空记录)、323 地点、8 标签关联、791 位移、 557 天气;跨度 2025-02-10 ~ 2026-09-13,估算里程 16861.9km,城市 9 座 / 省份 2 个。
技术选型
- Go 1.25 + 标准库 net/http + html/template(未引入 Web 框架)
- PostgreSQL +
pgx/v5;备份解析用纯 Go 的modernc.org/sqlite(无 cgo) - 前端手写 CSS,Leaflet + markercluster 本地内置(无 CDN 依赖)
- 口令 PBKDF2-SHA256(标准库
crypto/pbkdf2);会话为 HMAC 签名 Cookie, 签名混入口令摘要前缀,改密即让旧会话失效
踩过的坑(都已修复)
//go:embed static的资源路径相对包目录,原放在仓库根的web/下会编译失败, 已移动到internal/web/{templates,static}。- 地点查询用
WHERE ... EXISTS (SELECT ... visits v+inner.SQL()[7:]拼接, 切片把WHERE关键字一起去掉了,导致SQLSTATE 42601。改为inner.expr()并给 子查询占位符加偏移(where.off),避免与外层$1冲突。 - 模板函数
dur形参写死 int64,而VisitRow.DurationMin是 int,渲染时报 "wrong type for value",改成接受any。 - 页面模板若共用
templates/*.html一次性 ParseFS,各页{{define "content"}}会互相覆盖;改为每个页面与 layout 单独组成模板集。 - 月度图表只查有数据的月份,会把空档压缩掉;改为补齐成连续月份序列并标注年份。
- 移动端导航会折行撑坏布局,窄屏改为隐藏站点名、导航横向滚动、按钮文案缩短。 地图缩放/图层控件在窄屏改挂右下角,避免与全宽筛选面板重叠。
环境备忘
GOPROXY=https://goproxy.cn可用;proxy.golang.org不通- jsdelivr 可下载前端库;高德瓦片
webrd0X/webst0X.is.autonavi.com可直连 - playwright-cli 通过
npx --yes @playwright/cli@latest调用;浏览器进程会在两次 bash 调用之间被回收,open / goto / screenshot 必须放在同一次调用里
第二轮:地图偏移 + 重复导入 + 隐私默认 + 底图配置(P0/P1 收尾与回归)
坐标结论(已实测,写进 README 了)
rond 入库的 ZLOCATION / ZRAWVISIT.ZLATITUDE 本来就是 GCJ-02,不是 WGS-84。
证据两条:① 2246 个采样点「解析坐标 − 原始 GPS」中位差 lat −0.002823 / lon +0.004187,
与 GCJ-02 理论偏移(−0.002825 / +0.004187)残差 0 米;② 与世界迷雾覆蓋比对,
逆 GCJ→WGS 后命中率 92.3%,直接用原始值只有 32.8%。
→ 高德 / 天地图这类 GCJ-02 底图直接画,不要再纠偏;只有 WGS-84 底图才做 GCJ→WGS 逆变换。
旧代码对每个点都套了一次 gcj(),所以叠了约 1.1km 的双重偏移。
底图配置
conf/app.ini:map_provider = amap(默认免密钥)/ tianditu(需 map_key)/ custom
(需 map_tile_url + map_crs)。填了 map_key 后瓦片走服务端代理 /tiles/{layer}/{z}/{x}/{y},
密钥不出现在页面源码;无密钥则浏览器直连。前端读 /api/map-config 拿 crs 决定要不要逆变换,
切换底图时重新投影已有标记。
本次修掉的三个 bug(都是实测复现过的)
- 瓦片代理子域写死 0 → 高德子域只有 01~04,
webrd00/wprd00根本不解析。 改为按(x+y)%count轮转,高德 1..4、天地图 0..7。代理模式实测 4 张瓦片全部 200。 Stats.Skipped从未赋值(ingest.go构造 Stats 时漏了字段)→ 「已跳过 N 条」 与「内容完全重合」两个提示全是死代码。补上后实测提示正常。movementsINSERT 不写user_id→ 位移永远无法去重(重导入 791 条变 1582 条), 且下次启动时迁移的回填 UPDATE 会撞上idx_movements_dedupe,整个站点起不来 (SQLSTATE 23505,实测复现)。已补 user_id +ON CONFLICT DO NOTHING, 并把迁移里三个回填 UPDATE 各自包进EXCEPTION WHEN unique_violation,保证迁移再也不会拖垮启动。
数据集模型:已定「快照式」并落地(用户在两个方向里选了快照式)
「自然键跨数据集全局去重」与「一次上传 = 一份完整数据集」互相矛盾:全局去重下,更新备份里的
新记录只会落进未激活的数据集,页面上完全看不到(已实测:删 1 条后重导,新记录进了
is_active=f 的数据集 #2,页面仍显示 822 条)。
最终实现(快照式 B+):
- 一份备份 = 一份完整快照,行级唯一性只靠各表自带的
UNIQUE (dataset_id, src_pk) schema.sql:DROP INDEX idx_visits/movements/weather_dedupe(迁移里清理旧索引),datasets加content_hash并建(user_id, content_hash)索引ingest.Import:先按contentHash(b)(到访自然键排序后 sha256)查重,命中就复用旧数据集 并返回Stats.Identical=true;否则建新快照,最后UPDATE datasets SET is_active=(id=$1) WHERE user_id=$2让最新快照接管展示handlers_admin.go:同 SHA 重复上传改为「已导入过,未重复导入」并顺手删掉刚落盘的文件 (顺带修了删除数据集不删上传文件的 P2);提示语明确写「已切换为当前展示」- 回归全绿(一次性库实测):首导 823 / 同文件复用 / 内容同字节不同的复用 / 含 1 条新到访的 备份 → 新快照 #2 824 条接管展示,页面上新记录可见
- 已知小尾巴:存量数据集的
content_hash是 NULL,无法识别与它的内容重复,只影响第一次 重传;后续上传正常 - 交叉编译产物
dist/rond-linux-amd64已同步重编
回归验证配方(可复用,别动真实库)
- 建一次性库:
CREATE DATABASE rond_dedup_test - 写
conf/app.test.ini指向它、listen = :8090、独立 upload/data 目录 - 首次启动自动建表并生成
admin / rond-admin curl -X POST -d "username=admin&password=rond-admin" .../admin/login拿 cookiecurl -b <jar> -F "[email protected]" .../admin/upload,提示语在 303 的Location头里(URL 编码,urllib.parse.unquote解一下)- 造「内容相同但摘要不同」的备份:用 python
zipfile按原条目重打包即可换 SHA - 收尾:kill rond.exe →
DROP DATABASE→ 删conf/app.test.ini与测试目录
其他备忘
go build ... | head -20 && echo OK会掩盖编译失败(管道退出码来自 head),别这么判成功- Windows 上用 python 写
/tmp/x会落到C:\tmp(和 Git Bash 的 /tmp 不是一处), 尽量写项目内路径 - 后台删除数据集只删 DB 行,
storage/uploads/里的原始文件会留在磁盘上(P2,待清理逻辑)
世界迷雾叠加(已全部落地并验证)
internal/fog(fog.go 解码/编码、store.go 内存索引+Import(merge)、render.go 瓦片渲染)- 端到端:Go 解码与 Python 原型一致(52 tile / 8701 块 / 575753 格),编码回环无损(299112B)
- 路由:
POST /admin/fog(mode=overlay|cover)、GET /admin/fog/export、GET /tiles/fog/{z}/{x}/{y}?crs= - 前端:mapConfig 下发 fog 统计,rond-map.js 图层控件加「世界迷雾」,切底图自动换 crs 重拉
- 渲染两坑:① 低缩放级按全球块范围双循环 = z0 卡死 43 亿次 → 改为遍历块表+粗裁剪
②
image.RGBA是预乘 alpha,直写颜色被 png 编码器除回 alpha 变绿 → 用image.NRGBA - 对齐验证:足迹点 WGS 格位图已点亮;gcj02 渲染像素与足迹点像素偏差 0.0px(数学); z15 近景合成图迷雾沿道路分布、红点落在迷雾格上(视觉)
- 测试:overlay 删块恢复 ✓、cover 清空重导 ✓、导出→再导入格数不变 ✓
- 测试配置留在
conf/app-test.ini(已 gitignore);主站 8080 还是旧二进制,重启后才有迷雾功能 - 19:07 主站已用新二进制重启(后台任务),真实库迁移出 fog_blocks ✓,fog 瓦片/配置接口正常; 真实库尚未导入迷雾,等用户在后台传 .fwss
- 整体检查:build/vet 零问题;仓库历史文件是 CRLF 行尾(gofmt 全文件 diff),只有 internal/fog 做了 gofmt;fog.go 里多余的 filepath_Base 已换成 filepath.Base
P1+P2 六个功能(19:18-19:38 全部落地并冒烟通过)
- 迷雾覆盖统计:fog.CellOf/AreaKm2/Index.CellLit + web/fogCoverage,stats 页新区块; 坑:SQL 少写一列但 Scan 五个目标 → 静默返回 nil(区块不显示),已修
- 行程轨迹:/api/track(movements+places 起终点,5000 段上限),map.html 行程轨迹开关, rond-map.js setTrack,随底图坐标系重投影
- 到访热力:vendor leaflet-heat.js(unpkg 0.2.0),map.html 热力开关 + setHeat,权重=到访次数
- 年度报告:/report?y=,Nav 新增「报告」,report.html(KPI+城市榜+南北两端+交通)
- 数据导出:/admin/export/places.{geojson,gpx},全量地点含家/工作(站长本人数据), GCJ-02 原样导出并在 properties 注明 crs
- PWA:manifest.webmanifest + PIL 生成的爪印图标 192/512 + layout head meta
- 验证:测试库(rond_exp_test,已删)验导出 323 features/323 wpt、迷雾覆盖 47.8km²、 河池 43/93 等;主站 6 条路由全 200
- 所有 Go 文件已 gofmt(含 LF 行尾归一,为将来 git init 首提交做准备)
三个增强功能(20:00-20:45 全部落地并验证,主站 8080 已用最终二进制重启)
- OSM 免密钥兜底:高德瓦片 tileerror≥8 次前端自动切 OSM + toast; clarification:Leaflet 是渲染库 不是瓦片源,高德本来就是免 key 直连默认底图
- 公开页面开关:settings 保存
pages(map/places/timeline/stats/report),guardPage 中间件 对匿名访客 404,管理员放行,导航同步隐藏;实测 anon /stats=404、admin /stats=200 - TOTP 两步验证(internal/auth/totp.go + handlers_admin.go):RFC 6238 HMAC-SHA1 30s ±1 窗口;
两步登录中间令牌
2fa|uid|exp|mac(绑定口令摘要,改密失效,5 分钟);关闭 2FA 也要求新鲜验证码。 全流程实测通过:绑定→确认→登出→密码登录→动态码→/admin 200;负例(错码/篡改令牌)均被拒; 关闭后登录免第二步 ✓ - 两个坑:① TOTP 计数器曾用
t.Unix()/int64(30*time.Second)(除以 300 亿),改unixSec/30; ② login2FA 曾用SplitN(pending,"|",2)[0]取 uid(取到的是 "2fa" 前缀),改parts[1] - 测试方法坑(重要):html/template 会把属性值里的
+转义成+(防 UTF-7),curl 从 HTML 抠隐藏字段直接回传会验签失败——浏览器会解码实体,这不是产品 bug。测试脚本要先sed 's/+/+/g' - 验证用测试账号:首次启动自动生成 admin/rond-admin(README 有写)
- 清理:rond_2fa_test 库已 DROP、conf/app-2fa.ini 与 .tmp-test/2fa 已删;conf/app-test.ini 保留 (gitignored,回归配方用)
- venv 的 python.exe 在
envs/<name>/Scripts/python.exe(不在根目录),用户记忆里写的 sss 环境可用
口令重置(21:05)
- 主站真实库(rond_with_you)的 admin 口令早已改过,默认 rond-admin 只对空表新库生效 (EnsureDefaultUser 见 users 非空即跳过);用户忘了旧口令,用项目内 auth.HashPassword 写了一次性 重置程序(.tmp-test/resetpw,用后已删)把口令恢复为 rond-admin,curl 实测登录 303→/admin 200 ✓
- 改口令会同时让旧会话失效(session MAC 混入口令摘要前缀)——实测无异常
- 遗留建议(P2,未实施,等批准):默认账号首次登录时页面没有任何「请修改默认口令」的提示, README 有写但站点不强制;可考虑 EnsureDefaultUser 建的账号加个首登 flash 提示
默认口令警示 + 迷雾后台管理增强(22:30-22:45,主站已重启生效)
- adminHome 检测 admin 口令仍为 rond-admin 时,后台首页顶部渲染警示条(含改密/2FA 链接); 实现:AdminData.DefaultPW = auth.VerifyPassword(p.User.Hash, "rond-admin"),仅后台首页每次渲染时验证一次
- 迷雾卡片增强:空态引导(尚未导入提示)、块/格/估算面积/最近更新时间(fog_blocks.max(updated_at), sql.NullTime + 模板函数 dtt 接受 *time.Time)、「清空迷雾」按钮(POST /admin/fog/clear,confirm 后 DELETE FROM fog_blocks + fogResetIndex() 换掉内存单例索引)
- 模板函数注意:text/template 调函数时指针参数不会自动解引用,可空时间字段要么加 *T 版本函数, 要么模板里 if 判空后用 deref
- 安全全链路实测:导出(299112B)→清空→空态→cover 重导→8701 块/575753 格/47.8km² 完整还原,瓦片 200
- 用户真实库迷雾是用户 22:26 自己在后台导入的(说明迷雾 UI 其实可用,只是缺状态展示让人困惑)
迷雾可视化 A+C + 传输层性能优化(23:50-00:25,主站已重启生效)
- 迷雾配色改高对比:填充 #307ae0 a=132、边界 #103484 a=232;render.go 改为两遍渲染 (先填 mask,再按四邻域判定填充/描边),瓦片外邻格按「已探索」处理以免接缝处出现网格伪影。 实测贵阳 z12 瓦片:填充 5572px / 轮廓 663px ✓
- 前端加「迷雾浓度」滑杆(L.Control + localStorage rond.fogOpacity,默认沿用图层 opacity 0.9)
- 性能:新增 internal/web/gzip.go(gzip 池 + 首字节判 content-type,PNG 透传); assets.go 计算静态文件哈希作 assetVersion,cacheStatic 对 ?v=匹配者给 immutable 一年; /map 不再内联 points(58.6KB→12.5KB,gzip 3.1KB)改走 /api/points;leaflet-heat 改为 首次勾选热力图时动态加载(ensureHeat)
- 实测:/stats 38.3→5.4KB、/api/track 38.9→6.7KB、/report 6.3→2.0KB
- 坑:模板函数传 *time.Time 不会自动解引用(另见上条);server.go 里 gzip 是包级函数不是 Server 方法
访客可见区域白名单 + 地点别名/备注(01:00-01:30,主站已重启生效)
- 区域白名单:setting.Settings.Regions(省/市/区任意层级);stats.Filter.Regions 在 visitsWhere 里
生成三段 IN 条件(province/city/district 各一份占位符);filterOf 里仅
!p.IsAdmin()时注入, 因此地图/地点库/时间线/统计/报告全站生效、管理员全量。 实测:设「贵阳市」→ 匿名 73 点全为贵阳,管理员 283 点 ✓(测完已清空) - 地点备注:新表 place_notes(user_id, src_pk, alias, note),按 src_pk 而非 places.id 存, 重传备份不丢;internal/notes 包(All/Save,空则删记录)。Place 加 SrcPK/Alias/Note 三个字段, placeCols 末尾追加 p.src_pk(scanPlace 同步)
- 显示约定:别名对外替代原名(applyNotes 里直接改 Name,各页面/地图气泡自动生效), 备注仅管理员可见(applyNotes 传 admin 标志)
- 坑:applyNotes 传切片,单地点场景必须
ps := []stats.Place{place}; ...; place = ps[0], 直接传字面量切片改不到原值 - 后台设置页新增「访客可见区域」textarea(一行一个),保存时按行 trim 去空
备注可见性开关 + 后台备注管理页(01:25-01:35,主站已重启生效)
- setting.Settings.NotesPublic(notes_public,默认 false);applyNotes 参数由 admin 改为 showNote = p.IsAdmin() || p.Settings.NotesPublic,别名始终对外、备注按开关
- 新增 GET /admin/notes(notesPage + templates/admin/notes.html):LEFT JOIN places 取地点名/城市/到访数, 支持 ?q= 搜索(别名/备注/地点名 ILIKE),每行就地保存/删除(删除=提交空值,Save 会自动删记录), 按 Referer 判断保存后回管理页还是后台首页
- 后台首页导航与地点详情页都已挂入口
- 事故与修复:用 curl 单字段 POST /admin/settings 测区域白名单时,未提交 pages 字段导致 「公开页面」被清空(/map 等全部 404)。教训:settingsSubmit 是整体覆盖,任何脚本化测试都必须 把 pages/modules 等字段一并回传;已用全字段 POST 恢复
- Git Bash 里
curl -o /dev/null偶发 exit 23(写入失败),改成 -o 文件更稳
四个增强功能 F1–F4(20:5x 收尾,主站已重启实测)
- F1 出行方式配色:apiTrack 返回
modes图例(空方式归「步行/其他」灰色 #8e8e93), rond-map.js 渲染图例;实测modes:[步行/其他 370, 电🐔 317, 地铁 22, 高铁 21, 公交 17, 火车 9] - F2 城市级聚合 + 首次到达里程碑:
- 地图
/api/cities按城市聚合到访/时长/首次(last)日期,圈大小随到访量;已剔除家/工作 - 报告页「🏅 首次到达的城市」里程碑:firstArrivalCities 查「有史以来首次到访落在本年」的城市 (HAVING min(arrival) in [from,to)),按日期升序;区域白名单对访客同样生效
- 实测发现并修复的 bug:apiCityAgg 原 SQL 写
AND NOT p.is_home AND NOT p.is_work, 但 is_home/is_work 是 visits 列、places 表没有 →SQLSTATE 42703直接 500。 改为NOT EXISTS (SELECT 1 FROM visits vh WHERE vh.place_id=p.id AND (vh.is_home OR vh.is_work)) - 报告页已验证 2025 年出现里程碑、/api/cities 返回 9 城带 first/last 日期
- 地图
- F3 年度报告长图/PDF 分享:ReportData 加 Cities []CityMilestone;报告页加「复制链接」(
navigator.share优先、否则 clipboard) + 「保存为 PDF」(window.print()),app.css 加@media print隐藏导航/页脚/按钮、 卡片 break-inside:avoid。注:长图用浏览器打印生成 PDF(未接 html2canvas),README 已注明该取舍 - F4 从 GPX 补充轨迹:internal/gpx 解析 trk/trkseg/rte(WGS-84);adminGpxUpload 存 gpx_tracks,
apiGpx 按 crs=gcj02 把 WGS-84 转 GCJ-02 对齐高德底图;地图橙色叠加
- 修复:gpx.Parse 原来
if len(out)>0 return在已有 trk 时直接返回、丢掉 rte;改为 trk 与 rte 都追加 - 实测:登录 admin/rond-admin → 上传含 trk+rte 的测试 gpx → /api/gpx 返回两条轨迹(测试数据已删除)
- 修复:gpx.Parse 原来
- 验证手段:
go build+go vet全过;主站后台起、curl 实测各 API 与报告页;测试轨迹上传后清理 - README 已把 F1–F4 从「后续可以做的」挪到已实现章节,并补 GPX/城市聚合/报告分享说明