112 lines
15 KiB
Markdown
112 lines
15 KiB
Markdown
# rond-with-you 硬规矩(索引)
|
||
|
||
> 全文:`.workbuddy/memory/MEMORY-details.md`(47 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。
|
||
>
|
||
> ⚠️ 本文件**按字符数**注入,硬上限 1e4(`MAX_MEMORY_CHARS`;中文 1 字算 1,不是按字节)。超了会被 `substring` 截断=**尾部静默丢失**。日常日志与 details **不参与注入**,可随便写。现值见 `wc -m`,留 20% 余量。
|
||
|
||
## 落库 / 精度(details 一、四、五、十六、十九、廿八A)
|
||
- 建地点+到访**同一事务**;新行号只走 `nextSrcPK`+`clearStaleRaw`,**禁 `MAX(src_pk)+1`**。`seq_test.go`
|
||
- 行程派生列同导入(`validateMovementForm`):`from/to_place_id`(留空=绕过区域过滤直露访客)、`distance_km`、`duration_min`。
|
||
- 表单精度低于真值 ⇒ 只改备注也**静默改时间/坐标**:`keepStoredTime`/`resolveMovementEnd`/`keepStoredCoord`;改到访时间须 `clampVisitMovements` 夹行程。
|
||
- `src_pk` 只单数据集内有效;跨备份键=`ZIDENTIFIER_`/`ZUID_`;`contentHash` 别用 `SrcPK|Arrival`。
|
||
- 坐标留档 `coord_source/coord_sys/src_lat/src_lon`;换算只走 geo 包与 `/api/coords`。
|
||
- 脏数据:`appleTime` 超范围浮点转 int64 **回绕非饱和**(先卡秒数);`validCoord` 非法归 `(0,0)`。`dirty_test.go`
|
||
- 聚合刷新(`saveVisitTags`/`refreshPlaceAgg`/`refreshDatasetAgg`/`activityFlags`)**必须返回 error**。`agg_error_test.go`
|
||
- 收藏切换走单语句 `UPDATE … RETURNING bookmarked`。`visit_bookmark_test.go`
|
||
|
||
## rond 导出(details 二、廿六)
|
||
- 铁律:**PG 覆盖管理列、`entity_raw` 还原其余列**;基线已有行保真机原值(`keepNilCols`+`known[pk]`)。
|
||
- 新行只补 `ZRAWLATITUDE/LONGITUDE`、`ZVISIT.ZIDENTIFIER_`/`ZACTIVITY.ZUID_`(确定性=表名+src_pk)、`ZICON_`、`ZLASTARRIVALDATE_` 哨兵 `-63114076800`。
|
||
- `ZMOVEMENT.ZTYPE_` 不能 NULL、**别当管理列**(会洗掉 type=4);`queryTransports` 必须 `GROUP BY transport_src`。
|
||
- rows 收尾必须 `drainRows`(先 `Err()` 再 `Close()`)。`parse_rows_test.go`
|
||
- **扫进裸 `bool` 的列必须先 `COALESCE`**:`weather.is_daylight` 漏写 ⇒ NULL ⇒ 整包拒收(details 卅C)。
|
||
- 核对导出包**别按留档 UUID 找行**:站点新建行会被补派生 UUID(`coreDataUUID`)。
|
||
- **换基线**(`/admin/baseline`):站点数据一行不动,只换底稿。判据=到访 UUID 重合率(`matched==0` 硬拒)+ 干跑导出;记档 `datasets.baseline_*`,`ensureBaseline` **必须先看记档**;留档名**必须防重名**。`baseline_swap_test.go`(卅三)
|
||
|
||
## 迷雾 / 遮罩(details 三、九、十、廿二)
|
||
- `.fwss`=zip、条目 zlib;`Model/*/`=头 32768B+每块 515B;`Model/~/`、`Model/#/` 原样写回;清空/替换都要连 `fog_raw_files`。
|
||
- 导入三段式(preview 暂存 → 差异页现算 → `?stash=` 落地);带 stash=urlencoded、无 stash=multipart 两条都留。
|
||
- `fog_masks` 圆心 GCJ-02+半径,判定前换 WGS-84 网格;遮罩只对访客;`stats.Filter` 有**两套 where**(`visitsWhere`+`movementsWhere`),漏一个=「到访藏了里程还在」。
|
||
- 瓦片须与底图同 `crs`;带地图页须 `p.HasMap=true`。
|
||
- `tileDigits` 补前导 0;`tileName` 尾缀 `len(s)-2` 单数字编号时为 -1 ⇒ panic;解码守卫 `len(name)<8`。
|
||
- `fogIndexV` 必须 `atomic.Pointer`(普通指针=真 data race)。`fog_race_test.go`(-race)
|
||
- **专题显示区=一组圆(并集)**:存 `topics.areas` jsonb(不单开表);SQL 是 N 个圆的 **OR**(每圆 3 个占位符,编号须恰好覆盖 `[prefix+1,len(args)]`)。裁剪=**单独 pane**(不能裁 `tilePane`)+ `clip-path: path()`,每圆一段**同绕向**子路径(反了重叠处会挖成洞);圆心须 `project()`;⚠️ 注册必须排在 `applyFit()` **之后**(视图未定会抛「Set map center and zoom first.」并打断后续图层=迷雾整片消失且不报错)。后台=地图点选(`topic-areas.js`);⚠️ areas 初值必须渲染进隐藏字段(脚本没加载就静默清空)、上限两边写死须一致(四十五、四十六)
|
||
|
||
## 前端 / 模板 / CSS(details 六~八、十七、廿三、廿八D、卅七)
|
||
- ⚠️ **对访客公开的块,措辞必须按身份分支**(`{{if .IsAdmin}}我的…{{else}}站长…{{end}}`):
|
||
写模板时人站在站长视角下笔,「我的结论 / 你自己 / 我推荐的地点」露给访客就没人读得懂。
|
||
照 `place.html` 投票区与 `rond-map.js` 气泡的写法。门禁 `TestVisitorPagesUseThirdPersonCopy`
|
||
(以访客 ctx 渲染 `/`、`/stats`、`/report` 逐个断言禁用词,并确认站长那份仍说「我的」)。
|
||
- 改 Go 必须重启;模板/CSS/JS 只在 `debug=true` 热加载(`AssetV` 启动哈希)。⚠️ **新模板+旧二进制**=「渲染到一半仍 200」;启动自检 `smokeRender` 会拒启(四十一)。
|
||
- 字阶/圆角/阴影**只走令牌**,裸 `font-size: 13px` 有静态守卫 `TestNoBareFontSizePx`;卡片三档 `.card--hero/--quiet`。⚠️ 改**根字号**要查 Leaflet(`.75rem` 会跟着漂),(四十二)。
|
||
- 类型色走 `csscolor/csssoft`(**必须返回 `template.CSS`**,否则 `var()` 被换成 `ZgotmplZ` 且不报错);这套色连 3:1 都不到,**只能当圆点标记**。窄屏导航收进 `.nav-menu`,顶栏 `--z-nav` 必须高于地图浮层(四十三)。
|
||
- 日期控件一律走 `attachAll`(原生 `type=date` 年段会吞 6 位数字 ⇒ 敲 20260606 变 `202606/06` 且值为空),模板里有守卫。`.grid` 必须 `align-items:start`(四十四)。前端单测(`static/js/*.test.js`)已挂进 verify.sh,**node 缺失直接失败不跳过**;可测的纯函数要抽成独立文件(`rond-map.js` 是大 IIFE,取不出函数)。数字框 `step` 须 `any` —— 它相对 `min` 递增,写死小数会把正常值判非法,且那是**浏览器原生**提示、JS/后端都拦不到。(四十六)
|
||
- 拼查询串走返回 `template.URL` 的方法(`PlaceLink`/`EditPlaceLink`/`MapLink`/`EditLink`),勿手工转义。
|
||
- 窄屏覆盖写 `app.css` **末尾那块**;宽表堆叠类名**成对** `class="tb tb-stack"`;触控 44px。`tb_stack_test.go`
|
||
- 加表格列:`PlaceCols`/`scanPlace` **成对**、新列追加最后;CJK 挤换行只抬该表 `min-width`。
|
||
- ⚠️ 模板 JS 别用「heredoc+python」写 `\n`(`<script>` 语法错、交互全哑而模板测试照过);用 Edit,改完 CDP 验 JS 报错为空。`inline_script_test.go`
|
||
- 只在访客复现的 bug:先比「无 cookie 原始 HTML vs CDP 后 DOM」;别让 JS 从 `{{if .IsAdmin}}` 块读 id。
|
||
- **异步回包一律发序号作废**:`var seq=++xxxSeq;` 在 `if(!on) return;` **之前**,回包首行 `if(seq!==xxxSeq) return;`。`map_async_cancel_test.go`
|
||
- `window.localStorage` 一律经 `lsGet`/`lsSet`(隐私模式裸访问会抛);`previewQueue[id]` 须在 `if (live)` 之前排队、回调消费后删。
|
||
|
||
## 编辑页 / 重复到访(details 三十)
|
||
- 「重复到访」卡片:判定与 `tools/dedup_site_visits.py` 同语义、**UUID 是前提**、基线对不上就不给按钮;改指合并标记只动**本来就有**的键(`create_if_missing=false`)。`dup_visits_test.go`
|
||
- **已并入 / 子段(`ZMERGEDTO`/`ZPARENT`)只在读侧收口**,判据唯一一份 `stats.SoloVisitSQL`;天气与位移**不收口**;导出侧一字节不动。收口点清单见 details 卅G。
|
||
- 相对计数断言会**假绿**(变异让基准值一起膨胀),必须写定义性不变量;`render` 是流式直写,`code==200` 不等于渲染完。
|
||
- ⚠️ ingest 地点聚合回填必须排在 `insertEntityRaw` **之后**(判据读 entity_raw)。
|
||
|
||
## 活动 / 类别(details 廿四、廿五)
|
||
- **活动是表,类别不是**——类别只是 `places.poi_category` 字符串列;显示名**绝不写进该列**(`TestCategoryLabelNeverTouchesValue`),走 `categoryLabel(v,custom)`,Go 侧算好塞。
|
||
- 站内新建行 `entity_raw.raw IS NULL`;`ZCATEGORY_` 真机只有 `MKPOICategoryXxx` 枚举。
|
||
- 类别候选:编辑/照片页=库里在用 `GROUP BY`;补全页=在用 ∪ 静态表;下拉留「其它(自己写)」,默认「不动」。
|
||
|
||
## 归属口径 / 跨备份 / 附属数据(details 十九~廿一、卅四、卅五)
|
||
- ⚠️ **铁律:这份记录能不能从备份重新导入?** 能 ⇒ 可挂 `dataset_id … ON DELETE CASCADE`
|
||
(places/visits/movements/weather/activities/tags/entity_raw);**不能 ⇒ 只留 dataset_id 当出处、不许挂外键**
|
||
(place_images/place_notes/place_votes/topics)。挂在备份里没有的表上 = 删数据集就永久丢失。
|
||
- 两道门禁都钉着它:运行时查 `pg_constraint`(表必须在白名单里,反向也查)+ **静态扫 `schema.sql`**
|
||
(新建的库走 CREATE TABLE,只看运行时会有盲点)。`dataset_ownership_test.go`(卅五)
|
||
- **体检常驻后台首页**:库里 N 张 / 本地盘上 M 个 / 落单 K 个(`imageHealth`)——
|
||
事故的本质是「行与对象悄悄对不上、没人发现」,让它当场可见而不是等照片消失才追。
|
||
- 预演按 `VisitUUID`/`ActivityUUID` 对齐、只读;`raw #>> '{ZIDENTIFIER_,$blob}'` 是 base64 须转 hex 比;地点只认坐标(`PlaceCoordKey` 6 位)。
|
||
- 附属数据(备注/投票/图片)主键=`place_key` 坐标键(非 dataset+src_pk);Go 与 SQL 两套须逐位一致。
|
||
- **站长结论(`place_votes`)与访客票(`place_visitor_votes`)是两码事**:各存各表、各看各榜(`votes.TopVisitorVoted`,`HAVING` 只算有票的方向)。
|
||
⚠️ 票表也按坐标键跨数据集读 ⇒ 用例必须按 `place_key` 清历史票,否则票数越跑越多。(卅六)
|
||
- ⚠️ **附属数据不能挂数据集外键**(`place_images` 曾按 `dataset_id` CASCADE ⇒ 删数据集=删行、**文件还在盘上**);
|
||
恢复走后台「图片挂载恢复」,认地点靠 `baselines/<id>.sqlite`(**别清该目录**);
|
||
⚠️ 来源必须**全部读进来合并**(places → entity_raw 留档 → 该 sqlite 及**每份 `.bak`**),只看当前那份会整片认不出。`image_recovery_test.go`
|
||
- ⚠️ 恢复**同时扫对象存储**(R2/S3):`media.ListObjects`(ListObjectsV2 + SigV4,查询串先编码再签),
|
||
需 `s3:ListBucket`;列不动时本地照救 + warning。**专题同理**:`topics` 已改成归人不归数据集(`user_id` 过滤,内容仍照生效数据集算)。(卅四)
|
||
- ⚠️ `schema.sql` 迁移块必须收敛且排在依赖表之后(插在 `place_images` 前:开发库无症、**新库起不来**)。
|
||
- 合并 `backup_merge.go`:整个 `withTx`、不删除;新行 `*_visit_src` 写**库里的 src_pk**;必须 `writeRawRow`;删地点前先查有无到访(CASCADE)。
|
||
|
||
## 后端 / 性能(details 十、廿八C)
|
||
- `findSourceArchive` 先按 `size_bytes` 筛再算 sha256。池 8/8 + `db.WarmUp(d,6)`;页面内查询 `runJobs`(≤6);别用 `fog.Index.All()`;改遮罩须 `masks.invalidate()`+`fogTiles.clear()`。
|
||
- `placeCols` 子查询必须带 `vN.dataset_id=p.dataset_id`。`stats/placecols_test.go`(按子查询分段判)
|
||
- multipart handler 直接 `ParseMultipartForm`(`r.ParseForm()` 毒化字段);**之前必须 `MaxBytesReader`**。五个上传口:备份/GPX/迷雾/照片/地点图片。
|
||
- 包了 `ResponseWriter` 必须实现 `Unwrap()`(`gzipWriter`/`headerSent`);`recover` 写错误页前须看 `headerSent`。`server_error_test.go`
|
||
- 「先落盘再解析」的上传 handler,落盘后每条失败路径都要删半成品。`upload_cleanup_test.go`
|
||
- 批量勾选走 `*Bulk` + `p.src_pk = ANY($3::bigint[])`;**全局内存缓存必须带上限**(`fillupCache` FIFO 淘汰最旧 1/4);**同一查询别在一个 handler 里查两遍**。`admin_notes_bulk_test.go`+`fillup_cache_test.go`+`stats_weekday_once_test.go`
|
||
- 加 `setting` 字段改三处:`Default()`/`Load()`/`settingsSubmit()`。
|
||
|
||
## 安全 / 跳转(details 廿八B)
|
||
- 跳转唯一判据 `safeInternalPath`(`view.go`);四处共用。`redirect_target_test.go`(含禁弱判据静态扫描)
|
||
- cookie `Secure` 按请求判断(`cookieSecure`),否则本机 http 与 CDP 都登不进。
|
||
- CSRF 只加同源 Origin/Referer 守卫(`csrf.go`):仅写方法、`Origin: null` 拒、两头都缺放行。
|
||
- `clientIP` 只认 XFF **最后一段**(须 `netip.ParseAddr`);⚠️ 投票匿名身份=`hash(cookie,IP)`⇒改取法即改身份。
|
||
- GPX 按属主收口:`/api/gpx` 取生效数据集 `user_id` 再 `WHERE user_id=$1`;`adminGpxDelete` 查 `RowsAffected()==0`。`gpx_owner_test.go`
|
||
- 下载名走 `downloadDisposition`(ASCII 兜底+`filename*=UTF-8''`);四出口都经此。`download_name_test.go`
|
||
- `writeRawRow` 必须上抛错误让 `withTx` 回滚。**两步验证两闸都要**:`loginGuard`(键 `"2fa:"+用户名`)+`CodeReplay.Consume`。`twofa_guard_test.go`+`code_replay_test.go`
|
||
|
||
## 口径 / 404 / 验收(details 十一~十三、廿七、廿八E、卅二)
|
||
- ⚠️ **跑全量验收前**:① `VACUUM (FULL, ANALYZE)`(死元组涨到 3.3GB 后级联 DELETE 变 IO 爬虫,看着像 hang);② 库内必须有 `is_active` 数据集,否则 `internal/stats` 6 用例**假 skip**。跑着时别并发跑定点测试。
|
||
- 区域判定**两端各过一遍再 AND**(同 `regionSQL`);`movementsWhere` 也必须两端各判,不能用 `EXISTS(... IN (from,to) ...)`;比较列一律 `COALESCE(col,'')=$n`。`region_consistency_test.go`
|
||
- 其余(累计停留口径、`Filter.Verdict` 下沉、404 分档、验收四件套、覆盖率 `-coverpkg`)见 details 卅二。
|
||
|
||
## 备份合并 / 导出链路(details 卅一 + 廿一)
|
||
- 对齐键:到访=`ZIDENTIFIER_`、地点=`PlaceCoordKey`(坐标 6 位,**不认名称**)、活动=`ZUID_`;合并前必须**重算差异**。
|
||
- 地点配对只有一份实现 `rond.MatchPlaces`(差异与合并共用);同名兜底必须有距离上限(`nameMatchMaxKm=1.0`)。
|
||
- 差异页默认 radio 是「**用实机**」;写合并测试要照这个提交(抓 `value="backup"`),否则测的不是真实点击行为。
|
||
- 自检不变量:合并后没有任何地点被搬动 >1km、也没有任何到访离它的实机地点 >1km。
|
||
- **三条铁律**:① app 只靠 `Z_PK` 解引用 ⇒ 导出编号必须与**数据集基线**同一套 ⇒ **基线必须是手机包**;② rond 导入是**整库替换** ⇒ 导入前先清空 app 数据;③ 站点数据是超集 ⇒ 用「手机导出作新数据集 + 旧站点导出当备份合并进去」;`ZRAW` 这类从源包抄来的计数会越界,靠导出侧夹取兜住。
|