排查:/admin/settings 渲染到一半就断
- 现象:页面在「统计脚本」那张卡片处截断,没有保存按钮、没有 </form></html>,而 HTTP 仍是 200。
- 根因:跑的是旧二进制(改动前编译),磁盘上的模板却是新的。debug=true 下 pageSet()
每次渲染都从磁盘重解析模板,于是新 settings.html 引用了旧 setting.Settings 里没有的
TrackEnabled —— 执行期报错,而 render 只把错误写进日志,客户端只看到半张页面。
- 判据:strings <二进制> | grep -c cleanTrackScript(旧 0 / 新 2)。别用 strings | grep
TrackScript —— config 里本来就有这个名字,旧二进制也命中,是个假信号。
启动自检(防复发)
- template_smoke.go:pageProbe 给 24 个页面各配一份最小可用数据,New() 里全部渲染一遍,
任一页失败就拒绝启动。pageProbe 与 pageFiles 双向校验,新加页面必须补探针,
漏配按失败处理(不是省事)。
- smokeOne 用 recover 兜住方法里的 panic,报「哪一页坏了」而不是崩栈。
- 实测:往 settings.html 插一个不存在的字段 → 启动即报
「模板自检未通过…admin/settings: can't evaluate field …」,退出码 1。
- 门禁 template_smoke_test.go:一条跑全量自检,一条变异验证(探针换成字段更少的类型
必须报错且点名该页)。
站长导航被页面开关误伤
- guardPage 只拦访客(站长始终能访问全部公开页),但导航写的是 {{if .Settings.HasPage "x"}},
没有 IsAdmin 分支 ⇒ 页面开关一关,站长自己也只剩「主页」一个入口 —— 能访问却点不到。
改成 {{if or .IsAdmin (.Settings.HasPage "x")}}(map/places/timeline/stats/report 五处)。
- 门禁 nav_visibility_test.go:三向不变量(站长仍有全部入口 / 访客一个都没有 /
全开放时访客也看得到),做过变异验证。
- 访客侧「只有主页」是按后台配置应有的状态,没有动数据。
统计脚本关口收紧(cleanTrackScript)
- src= 只在 <script …> 开标签内部找。原来对整段做 strings.Index,脚本体里一句
`img.src=u` 就被命中、整段被判危险而清空 —— 页面照样提示「设置已保存」,站长看不出没挂上。
- 开闭标签从「只拒 shut > open」改成严格配平:末尾未闭合的 <script> 会把页面收尾整段吞进脚本。
- 不再静默:签名改成 (script, warn),说明码经 ?warn= 回显成人话;判为危险时整段不采纳、
库里原脚本与开关都不动(把站长手上好用的脚本一并清掉更糟)。
- 门禁 TestCleanTrackScript 重写:逐条边界 + 定义性不变量(采纳了非空结果时,
里面开闭标签数必定相等)+ TestTrackWarnMessage(未识别码不得回显)。变异验证做过。
- 文案订正:settings.html 的「conf/app.ini 的 track_script 会作为这里的默认值」有歧义 ——
它不回填文本框,而是「这个框留空时、注入那一刻才回落到 ini」。
其余
- bin/ 目录删除(15 个 exe / 412MB),二进制只留仓库根 rond.exe;
技能 rond-web-verify 与 memory details 十八里 bin/rond.exe 的写法同步改掉。
- 验收:./scripts/verify.sh 全绿且 0 skip(PASS=346 FAIL=0 SKIP=0)。
14 KiB
14 KiB
rond-with-you 硬规矩(索引)
全文:
.workbuddy/memory/MEMORY-details.md(38 节);验证机制:技能rond-fix-verify;备份包解析/导出:技能rond-backup-parse。本文件只做关键词索引——新规矩写 details,别堆这里。⚠️ 本文件按字符数注入,硬上限
MAX_MEMORY_CHARS = 1e4(WorkBuddyapp.asar里getWorkingMemoryContent;中文 1 字算 1,不是按字节)。超了会被substring(0,1e4)截断并写明「先整理再干活」——即整段尾部静默丢失。日常日志与本 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 卅三)
迷雾 / 遮罩(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)
前端 / 模板 / CSS(details 六~八、十七、廿三、廿八D、卅七)
- ⚠️ 对访客公开的块,措辞必须按身份分支(
{{if .IsAdmin}}我的…{{else}}站长…{{end}}): 写模板时人站在站长视角下笔,「我的结论 / 你自己 / 我推荐的地点」露给访客就没人读得懂。 照place.html投票区与rond-map.js气泡的写法。门禁TestVisitorPagesUseThirdPersonCopy(以访客 ctx 渲染/、/stats、/report逐个断言禁用词,并确认站长那份仍说「我的」)。 - 改 Go 必须重启;模板/CSS/JS 只在
debug=true热加载(AssetV启动哈希)。⚠️ 新模板+旧二进制=「渲染到一半仍 200」;启动自检smokeRender会拒启(四十一)。 - 拼查询串走返回
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)—— 事故的本质是「行与对象悄悄对不上、没人发现」,让它当场可见而不是等照片消失才追。
跨备份 / 附属数据(details 十九~廿一、卅四)
- 预演按
VisitUUID/ActivityUUID对齐、只读;raw #>> '{ZIDENTIFIER_,$blob}'是 base64 须转 hex 比;地点只认坐标(PlaceCoordKey6 位)。 - 附属数据(备注/投票/图片)主键=
place_key坐标键(非 dataset+src_pk);Go 与 SQL 两套须逐位一致。 - 站长结论(
place_votes)与访客票(place_visitor_votes)是两码事:各存各表、各看各榜; 统计页拆成「我的结论」与「访客点赞」两块(votes.TopVisitorVoted,HAVING只算有票的那个方向)。 ⚠️ 票表也按坐标键跨数据集读 ⇒ 用例必须按place_key清历史票,否则票数越跑越多。(details 卅六) - ⚠️ 附属数据不能挂数据集外键:
place_images曾按dataset_idCASCADE ⇒ 删旧数据集=删图片行、文件却留在盘上(读取按坐标键、行按数据集级联,两套口径打架)。已摘掉外键;恢复走后台「图片挂载恢复」,认地点靠旧数据集留下的baselines/<id>.sqlite(别清 baselines 目录);⚠️ 来源必须全部读进来合并(places → entity_raw 留档 → 该 sqlite 及每份.bak)——换基线会覆盖底座文件,只看当前那份会整片认不出(实测 40 张只挂回 7 张)。image_recovery_test.go(details 卅四) - ⚠️ 恢复同时扫对象存储(R2/S3):
media.ListObjects(ListObjectsV2 + SigV4,查询串先编码再签), 需s3:ListBucket权限;列不动时本地照救 + warning。专题同理:topics已改成归人不归数据集 (user_id过滤,内容仍照生效数据集算)。用户级数据靠Dataset.UserID定位。(details 卅四) - ⚠️
schema.sql迁移块必须收敛且排在依赖表之后(插在place_images前:开发库无症、新库起不来)。 - 合并
backup_merge.go:整个withTx、不删除;新行*_visit_src写库里的 src_pk;必须writeRawRow;删地点前先查有无到访(CASCADE)。
后端 / 性能(details 十、廿八C)
findSourceArchive先按size_bytes筛再算 sha256(pickArchiveBySha)。池 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[])(pgx 直接吃[]int64);全局内存缓存必须带上限(fillupCacheFIFO 淘汰最旧 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/stats6 用例假 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这类从源包抄来的计数会越界,靠导出侧夹取兜住。