tamakyi
|
496c992e5e
|
图片不再随数据集删除:加「图片挂载恢复」,place_images 摘掉外键
用户报「之前上传的照片找不到了,应该是因为清了应用数据换基线」。查下来与 App 侧无关,
是站点自己两套口径打架:
- place_images.dataset_id 带 ON DELETE CASCADE,而读取按 place_key 坐标键、跨数据集。
- 于是删掉旧数据集时图片**行**被级联删了,**文件却留在 media 目录里**。
- 实测(本机):datasets 只剩 155 号、place_images 0 行、盘上 4 个 .webp 好端端躺着。
同一批附属数据里备注(17)与结论(3+1)因为早就迁到坐标键,毫发无损——只有图片倒霉。
三件事:
1) 恢复:后台首页「图片挂载恢复 → 扫描并恢复」(POST /admin/images/recover,
internal/web/image_recovery.go)。只插行、不删文件、不看数据集,按 object_key 判重可反复点。
认地点两处来源:数据集还在就查它的 places;数据集已删就读它留下的 baselines/<id>.sqlite
还原「地点号 → 坐标」。对象键两种历史布局都认(<ds>/<YYYYMM>/<srcPK>-<hash>.webp 与
<ds>/<srcPK>/<hash>.webp);判据先看文件名有没有 `<数字>-` 前缀,再看中间目录——
地点号也可能刚好 6 位,只看位数会认错。
2) 防复发:CREATE TABLE place_images 去掉外键 + 幂等 DROP CONSTRAINT;删图接口不再按
dataset_id 过滤(数据集删了也得能删图);「删除数据集」的二次确认文案改成实话。
3) 报告同类陷阱(未改):topics 仍带 CASCADE,专题随数据集一起删且没有恢复来源。
测试踩到的坑:图片「按坐标键跨数据集读」+「按 object_key 判重」都不看数据集,用例之间会
互相污染两处——盘上文件(报「应当 2 张、实际 10 张」)与库里的行(Already:1 让兜底分支
根本没跑到,用例差点假绿)。加 isolateImageState(换空 DataDir + 按 place_key 清历史行),
并给夹具补收尾清理(外键没了必须自己清)。
新增用例 4 条;验收:./scripts/verify.sh 全量 PASS=323 FAIL=0 SKIP=0。
|
2026-09-30 19:45:51 +08:00 |
|
tamakyi
|
52a82631da
|
换基线:说清「新底座里多出来的到访不会进导出包」,并把它钉成用例
写文案时去核这条,先把一条写错的断言纠正了:导出条数本来就不等于站点条数。
tableWriter.finish() 是两段:先删「基线里有、PG 已没有」的行(known − seen),
再把「导入时进不了 PG 但留档还在」的行(w.skipped × raws)按原样补回——否则
「导入→导出」一趟会凭空少数据。实测 823 条到访的数据集导出包里是 824 条,差值正常。
(第一版成功路径用例断言「导出 == 站点」,红在 824 != 823 就是这个原因;已改成更锋利的
「换底座前后导出包里 ZVISIT 的 UUID 集合逐条一致」。)
同一段代码定下了换基线的语义:新底座里站点没有的行会被清掉(进 known 没进 seen)。
所以换一份更新的手机包**不会**把手机新记录带进导出包,要带得先走「分析差异」合并。
页面提示与 flash 文案都写明了这点,并新增用例
TestSwapBaselineDoesNotCarryRowsTheSiteDoesNotHave 钉住它(站内补一条到访 → 导成包 →
从站内删掉 → 换底座 → 断言导出里既没有那条的派生 UUID、UUID 集合也与换之前逐条一致)。
验收:./scripts/verify.sh 全量 PASS=319 FAIL=0 SKIP=0。
|
2026-09-30 19:15:48 +08:00 |
|
tamakyi
|
6ff6f944a1
|
换基线:把「底座换代」做进后台(站点数据一行不动)
导出是「底座为底 + 只覆盖托管表」,所以底座必须是手机导出的包。底座跟着数据集走,
手机重新导出后想换代,以前只能整包重新导入成新数据集(站点补录的行、主题、备注都得重收拾)。
新动作 /admin/baseline(后台首页加入口)就是给同一份数据的底座换代用的。
三道闸,顺序不能挪:
1. 重合判据:新包里的到访 UUID 与站点侧带 UUID 的到访比。一条都对不上直接拒
(连 force 也不给过)——干跑只验结构,一份不相干的包照样导得出,但换下去站点
每行都在新底座里认不出身份,全按新行插到最大编号之后,真机上就是「这些到访
全变成新的」。重合率不到一半要显式确认;判据没过不当场报错了事:包已经收进
uploads,页面给一个「我确认,强制替换」按钮,不必重传几十 MB。
2. 干跑导出:底座缺表/缺实体编号在这步挡住,不会出现「换完了才发现导不出、
原来那份已经不知去哪」。
3. 旧底座留档 + 记档,换错了可一键回滚。
两个坑:
- 留档名不能只用秒级时间戳。「替换后紧接着回滚」落在同一秒时,回滚那次 rename 会把
刚生成的留档覆盖掉,于是回滚回了个寂寞(TestRollbackBaselineRestoresPrevious 当场红)。
改用 baselineBackupName():20060102-150405 + 冲突自增。
- ensureBaseline 必须先看记档。只认 datasets.sha256(= 最初那份包)的话,底座一丢就静默
重建回旧包,上一次换基线白做且毫无提示。抽出 ensureBaselineTo(ctx, dsID, dst) 共用;
「重建」入口写临时文件再 rename,不做破坏性删除。
回归用例 7 条(internal/web/baseline_swap_test.go):成功路径断言「换底座前后导出包里
ZVISIT 的 UUID 集合逐条一致」——编号会换、身份不该换,谁在底座里没对上立刻亮;
记档优先重建带反证(清掉记档后同一条路必须走不通);部分重合需确认、完全对不上 force
也不给过、非包文件第一步就挡住、回滚还原且当前那份也留档、页面渲染到底。
验收:./scripts/verify.sh 全量 PASS=318 FAIL=0 SKIP=0。
|
2026-09-30 19:00:18 +08:00 |
|
tamakyi
|
11e00a5b60
|
记忆:记下本次抽出的跨项目技能 workbuddy-memory-versioning
|
2026-09-30 18:32:14 +08:00 |
|
tamakyi
|
3f1b737a78
|
记忆:把 .workbuddy/memory 纳入版本控制
用户要求这份记忆跟着仓库走,而不是只留在本机。
.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`)。
|
2026-09-30 18:31:08 +08:00 |
|
tamakyi
|
d99c63fa4f
|
导出:把 ZVISIT.ZRAW 夹到包内采样行数以内(越界会让真机整包拒收)
ZVISIT.ZRAW 是「这次到访由多少个原始采样点归并而来」的缓存计数,而采样点表 ZRAWVISIT 是
**不托管实体**(只从基线原样带过来)。合并进来的到访带着源包的计数、源包的那些采样行站点
没留下 ⇒ 计数会超过包里实际有的采样行数。
2026-09-30 逐行二分定位:生产导出被真机拒收(「备份恢复失败,你的当前数据未被修改」),
罪魁是单条 `ZVISIT #1000`——它声称 2305 个采样点,而包里只有 2246 行 ZRAWVISIT;
把这一列改成 1421 后同一份包立刻能导入。旁证:被真机接受过的三份包(09-14 基线、
09-20 A 包、验收纯净往返包)的 ZRAW 最大值都恰好 ≤ 采样行数,而它们 ZRAW 为 NULL/0 的行
本来就有 39~139 条 ⇒ 往下夹是安全的、夹取只降不升。
顺带:`readDupBaseline` 先 os.Stat 再开库——sql.Open 默认 READWRITE|CREATE,路径不存在时
会静默建一个空库,报出来的是「no such table」而不是「没有基线」,很难查。
用例 `TestExportClampsRawSampleCount`:造一条声称 999999 个采样点的到访,导出后断言
「任何一行的 ZRAW 都不超过包内采样行数」且该行仍在(只改计数、不丢记录);变异验证:
关掉夹取立刻红。
|
2026-09-30 17:35:59 +08:00 |
|
tamakyi
|
9e112d55f9
|
工具:tools/bisect_export.py —— 把被真机拒收的导出包做行级二分
生产导出被拒(「备份恢复失败,你的当前数据未被修改」),但同一台设备导入「纯净往返包」成功
⇒ 导出链路与 App 都没问题,问题在数据集的数据里。静态把技能里记的全部判据都查了一遍
(打包层位标志、Core Data 骨架逐字节、逐列空值画像、关系完整性、类型分布、Z_OPT/UUID/BLOB、
行级时间不变量、取值域与数值范围、SQLite 文件头)——一处违规都没有,只能靠真机二分。
脚本以被拒的包为底生成三个对照包(都清了悬空引用、按 zip.Store 带长度与 CRC 重新打包):
V1 去掉站内新建的到访;V2 = V1 + 去掉站内新建的地点;V3 手机里也有的行按手机原值重置。
README 与记忆里记了怎么读这三个结果(哪一版开始能进,问题就在上一版删/改掉的那一类里)。
|
2026-09-30 16:59:00 +08:00 |
|
tamakyi
|
0e670aa1cf
|
文档:补「重复到访」两类成因(站点侧副本 / 补录行被合并回来)
|
2026-09-30 16:58:59 +08:00 |
|
tamakyi
|
c9ebb3aebd
|
修性能回归:把「已并入 / 子段」判据做成派生列(visits.is_merged)
昨天给「已并入 / 子段」做的收口写成每行去 entity_raw 判
`raw->>'ZMERGEDTO' IS NOT NULL OR raw->>'ZPARENT' IS NOT NULL`,而 entity_raw.raw 是
**TOAST 过的整行 Core Data JSON**:一次探针要取回并解析整行。地点库「每个地点 × 5 个子查询」
的量级下 —— 实测 /places 70.1 秒、首页 43.6 秒才出页面(而库里没有任何一条 SQL 超过 1 秒:
时间全花在成千上万次「取整行再解析」上,进程 CPU 只用了 3s/135s)。
这批「渲染若干页面」的布局用例因此从 20 秒涨到 226~309 秒,整套验收从 6 分钟变 20+ 分钟。
- schema.sql:加派生列 visits.is_merged + 回填 + 给回填用的部分索引
(只索引确实带合并标记的那几十行,不用取回 raw)。
- stats.SoloVisitSQL 改成读这一列(判据仍然只此一份,读侧全部经由它)。
- 写入侧跟着刷:ingest 在 insertEntityRaw 之后、backup_merge 的 recountPlaces 里,
共用 stats.RefreshMergedFlagsSQL。
- 直接改留档里的合并标记(工具脚本 / 用例)之后也要刷一次,schema.sql 与函数注释都写明了。
实测:/places 70084ms → 206ms,/ 43633ms → 698ms(比「关掉判据」的对照还快)。
新增 page_render_budget_test.go:给 7 个关键页卡 10 秒上限的灾难探测器
(功能全对、只是慢的那种退化,之前没有任何用例会红)。
|
2026-09-30 16:18:52 +08:00 |
|
tamakyi
|
5b8699a03a
|
重复到访:第二类(补录行被合并回来)不依赖基线,基线不可用时也照样能清
「补录行被合并回来」这一类的判据全在站点自己这边(派生 UUID 命中),本来不需要基线;
但原先的实现在读基线之前就 return 了,于是基线缺失 / 对不上的数据集连这一类都清不了。
现在先判第二类,基线的可用性检查只挡住第一类(站点侧副本),不可用时把说明补在卡片里
(有第二类就列出来 + 一行说明,没有就只给说明)。
模板相应合并成一个卡片:标题带对数、说明行只在有对时出现、判不了第一类时补一行灰字。
|
2026-09-30 15:19:42 +08:00 |
|
tamakyi
|
aaf65f654a
|
格式:补回 diff_test.go 里被并掉的换行(gofmt)
|
2026-09-30 15:03:20 +08:00 |
|
tamakyi
|
8dec973db1
|
修「补录行被合并回来」的重复:对账时认它的派生 UUID
成因链:站点补录的到访没有留档行(entity_raw 里没有它),也就没有 UUID;导出时会被补上
一个确定性 UUID v5("ZVISIT/<src_pk>") 写进包里,手机于是把它当成一条真记录;这份包再
合并回站点时,站点这侧没有 UUID 可对齐,DiffVisits 直接判成「备份新增」又插一条 ——
时间线上出现两条一模一样的到访(实测数据集 9 有 43 对,生产时间线一屏就有 53 对)。
- rond:把派生 UUID 抽成 DerivedUUID / DerivedVisitUUID / VisitMatchUUID(导出侧 coreDataUUID
复用同一个函数),差异与合并都拿它当对账键(diff.go 的 byUUID、backup_merge 的 storedByUUID)。
- 「时间 + 地点」兜底配对补 used 检查:同一条站点行现在两个索引里都有,不查会被 classify 两次。
- 数据编辑页的「重复到访」卡片认得这类(成因标「你补录的那条被合并回来的一份」),
清理时把副本名下的位移 / 天气**改挂**到保留项——补录行自己名下没有位移,跟着删就白丢里程;
派生 UUID 判定排在「站点侧副本」之前(证据更强)。
用例:rond 两条(正例 + 派生 UUID 不相配的反例)、web 一条端到端(造补录行与副本 → 检出 →
清理 → 断言位移与天气改挂、留档被清)。变异验证:去掉改挂语义、关掉该判定,两条都立刻红。
|
2026-09-30 15:02:45 +08:00 |
|
tamakyi
|
881f2933ff
|
收尾:验收脚本自动清死元组、城市圈首末到访按可见到访算、补使用说明
verify.sh:测试库 ≥ VACUUM_MIN_MB(默认 200MB)时先 VACUUM (ANALYZE)——一轮全量会留几百 MB
死元组,级联删除随之变慢(同一夹具用例实测 13.6s → 10.2s)。只用普通 VACUUM:FULL 要
ACCESS EXCLUSIVE 锁,有别的服务连着测试库时会把验收整段卡住。
/api/cities 的首末到访改成按可见到访的到达时间算(内层 min/max(v.arrival)),不再取
places.first/last_visit_at——那两列是全量的,会被已并入 / 子段的到访推早推晚。
README 补两节:「数据编辑页上的重复到访」(判定口径与清理范围)、「为什么有些到访在页面上看不到」
(已并入 / 子段不计入显示与统计,但导出必须原样带着),并在开发提示里记下验收环境的死元组与
is_active 两个坑。
回归:city_agg_test.go 补了一段能区分的断言——把某地点最晚那次到访标成已并入后,城市最近到访
必须从 09-13 退到 09-08(原先只比对同口径时,两种算法在夹具数据上结果一样,是条假绿的断言)。
|
2026-09-29 23:49:39 +08:00 |
|
tamakyi
|
3d03aae018
|
修地图城市圈的聚合口径:先按地点汇总,再按城市汇总
/api/cities 原来把聚合打在 places ⋈ visits 上:一个地点有 N 条到访,它的 visit_count 与
dwell_minutes 就被累加 N 次,count(*) 数出来的到访条数又被当成「地点数」显示——实测最大的
那个城市气泡写着「到访 72661 次 · 568 个地点」,真值是 568 次 / 86 个地点(差 128 倍)。
改成内层按地点 GROUP BY、外层按城市 sum/count,并补上「已并入 / 子段不算数」的判据,
圈的位置、排序与返回列序都不变。
回归 city_agg_test.go:手写同口径 SQL 对账 + 「地点数不可能多于到访数」的不变量;
变异验证(sum(pa.visits) *2)立刻红。
|
2026-09-29 22:07:23 +08:00 |
|
tamakyi
|
4597047989
|
口径:已并入 / 子段的到访不再计入时间线与统计(导出侧一个字节不动)
真机里两行到访可能「不独立成立」:ZMERGEDTO 指向它被并入的那条、ZPARENT 说明它只是某个
父段的一部分。app 对这两种行既不显示也不计数,本站以前当成普通到访 ⇒ 时间线上出现
「同一时刻同一地点两条」、日停留重复计入(实测数据集 9 有 64 条,26 条到达时间与原件相同)。
这些行**不能删**——导出要靠它们认这次合并发生过,所以判据只立一份 stats.SoloVisitSQL,
在读的一侧收口:visitsWhere / VisitsByPlace / Bookmarked / placeCols 子查询、首次到达城市、
地图极值点、专题(详情 + 列表批量)、三处聚合的 visit_count 与 dwell_minutes,
外加 schema.sql 一段幂等回填把存量库拉正。
天气与位移**不收口**(那几小时、那段路真实发生过);导出侧一个字节不动(rond 只读
places.last_visit_at,保持全量,压根不读 visit_count / dwell_minutes)。编辑页照样列出
这些行,但打上「已并入 #562 / 子段 #374」,免得以为数据丢了。
顺带修两个自测抓出来的问题:
- ingest 的地点聚合回填排在 insertEntityRaw 之前 ⇒ 判据读到空留档 ⇒ 每行都算独立到访
(整整多算一个「已并入」的量)。
- 收口后 last_visit_at 保持全量,会凑出「0 次却有最近到访」(数据集 9 有 4 个地点),
展示侧按可见到访数兜住。
验收:304 PASS / 0 FAIL / 0 SKIP(新增 3 个用例:标记后导出仍带该行、存量回填拉正、
判据只出现在读侧)。
|
2026-09-29 21:15:25 +08:00 |
|
tamakyi
|
152af81f6a
|
编辑页:新增「重复到访」卡片 —— 以基线(手机包)为参照清掉站点侧的历史副本
站点库里有一批「到达时间 + 地点与手机包里某条完全相同、但 UUID 不同」的到访副本,
合并/差异按 UUID 认人会把它们当成备份新增,导出到 rond 就成了成对的两条重复记录。
判定与 tools/dedup_site_visits.py 同一套语义:UUID 是前提(只按时间+地点判会误删手机包
自身合法的成对条目);基线跟本站一条都对不上时只给说明、不给按钮。清理在单事务里删
副本 + 其位移/天气/留档,并把指向副本的 ZMERGEDTO/ZPARENT 改指到保留项——只动本来
就有的那个键,工具那版会凭空补出一个 ZMERGEDTO(真机里是「已并入别处」,会白丢记录)。
顺带修一个「一补录天气就整包导出失败」的洞:rond 导出把 weather.is_daylight 扫进裸 bool,
而站点「新增天气」不写这一列 ⇒ NULL ⇒ sql.Scan 报错。改成 COALESCE(is_daylight,false),
与导入侧读真机包同口径。
验收:301 PASS / 0 FAIL / 0 SKIP(新增 4 个用例,含变异验证:去掉 UUID 前提/改回
create_if_missing=true/不删副本位移,三种改法都被用例抓到)。
|
2026-09-29 19:17:29 +08:00 |
|
tamakyi
|
40441ba900
|
工具:tools/dedup_site_visits.py —— 以手机包为参照清掉站点侧的历史重复到访
站点数据里有一批「到达时间 + 地点与手机包里某条完全相同、但 UUID 不同」的到访副本(站点侧生成的
UUID)。差异页/合并按 UUID 认人,于是它们被当成「备份新增」,导出到 rond 后就是成对的两条重复记录
(2026-09-29 站长实测:导入后 app 里出现成对的重复条目)。
工具以手机导出的包为参照(数据集的基线):
* 站点到访的 UUID 在参照里能找到 → 保留(手机自己也有成对条目,那是合法的,不能动)
* UUID 找不到、且 (到达时间, 地点) 与参照里某条一致 → 判为副本
副本的附属记录(位移/天气/留档)一并删除;若有别的到访的留档指向副本(ZMERGEDTO / ZPARENT),
改指到保留的那条,避免导出时留下悬空引用。**默认只预演,--apply 才动手。**
踩坑记录(都在此提交里修掉):
* `raw->>` 取出的是 text,与数字比较会报「text = integer 操作符不存在」,导致工具在动手前整个退出;
* 同一个 UPDATE 里不能给 raw 赋两次值(「对同一列 raw 进行了多次分配」),jsonb_set 要嵌套。
演练验证(本地真实数据:导入含 72 条副本的包 → 执行 → 核对):
到访 1026 → 954、位移 833 → 809、天气 621 → 589(与手机包一致);
「同时间同地点」重复组 101 → **31**(= 手机包自身)、悬空引用 0/0。
|
2026-09-29 17:38:50 +08:00 |
|
tamakyi
|
d5a011259d
|
导出:按身份把编号对齐到基线(app 只能靠编号解引用地点/活动,编号漂移就「看不到数据」)
背景:Core Data 里**地点与活动没有 UUID**,app 只能靠 Z_PK 解引用(ZVISIT.ZLOCATION /
ZVISIT.ZACTIVITY_)。站点这边的 src_pk 是从「导入→导出」循环里滚出来的,与手机 store 的 Z_PK
不是一套。2026-09-29 实测(真机 09-28 包 vs 站点导出,按到访 UUID 对齐):地点引用 920/920 不同、
活动引用 903/920 不同,而引用到的地点按「名称+坐标」完全一致——语义相同、编号不同。结果就是
「导入成功但看不到数据」(每条到访都指到手机那边的别的地点/活动或不存在的行)。
做法(新增 internal/rond/export_align.go):导出的每张托管表**先按身份建映射**,再按映射后的编号写:
地点:坐标键优先(多值索引),退到「同名且相距 ≤1km」
活动/到访:UUID(读留档 entity_raw 里的 ZUID_ / ZIDENTIFIER_)
标签/交通方式:名称;位移/天气:所属到访(已对齐)
基线里没有身份对应的行 → 取基线最大编号之上的新号;而**身份匹配不上、原编号又没被别的身份占用的行
保持原编号**(这样「基线就是源包」的往返导出一行都不动;被占用时才另取号,避免覆盖基线的别的记录)。
配套改动:引用全部改挂(含自引用 ZMERGEDTO / ZPARENT,它们原本从留档原样带出、对齐后会指错或悬空,
已加入托管列并按映射写回);Z_PRIMARYKEY.Z_MAX 改用实际写入的编号;基线身份查询都 ORDER BY Z_PK 保证
确定性;siteUUIDMap 改成读 base64 后在 Go 里解码(与 web 侧 loadStoredVisits 同一手法)。
验证:
- 真实数据(本地数据集 + 真机 09-28 当基线):按 UUID 对齐的 953 条共同到访里地点引用一致 933、
活动一致 951(改前 0 / 7),悬空引用 0;余下 20 条是基线自己重名重复行的归属歧义。
- 用例 TestExportAlignsPKsToBaseline:把库内编号整体平移 500000 再导出,断言按 UUID 对齐后**地点/
活动的编号**与基线一致率 ≥95% + 7 类引用无悬空 ⇒ 822/822。⚠️ 第一版只比「名称+坐标」,变异验证
不会变红(编号不一致时引用依然自洽),必须比编号本身。
- 往返不变量 TestExportRoundtrip 通过(「匹配不上则保持原编号」这条规则就是为它加的:之前唯一的
未匹配行是那条没有 UUID 的「全空到访」,被改号成 825 才暴露出来)。
- 全量验收 297 PASS / 0 FAIL / 0 SKIP。
注意:对齐的目标是**这份数据集的基线**。要让导出与手机编号一致,数据集的基线就得是手机包
(把手机导出作为新数据集上传、再把旧站点导出包合并进去,然后从它导出)。
|
2026-09-29 16:57:29 +08:00 |
|
tamakyi
|
d5e96edbea
|
导出:撤销「给 ZISOCOUNTRYCODE 补空串」——实测空串会让真机拒绝整包
真机对照实验(同一份内容、同一台设备,唯一差异是这一列):
国家码 = '' → 备份恢复失败
国家码 = NULL → **能导入**
也就是说我 2026-09-29 早些时候那个「按真机形状补空串」的改动是有害的回归:真机包里的确有 19 行是 '',
但**导入侧不接受**它(真机自己写出来的形状不代表它能吃回去)。恢复成按站点当前值原样写(新建地点没有
这一项 → NULL),与「被真机接受过的 A 包」一致。
教训记进注释:这一列不要做任何「形状美化」,保持 NULL。
|
2026-09-29 15:18:28 +08:00 |
|
tamakyi
|
f98d3864b8
|
导出:zip 用 CreateRaw 写长度/CRC,别再带 data descriptor(真机判整包损坏)
真正的病根(09-16 起每一份站点导出都被拒都是它):Go 的 zip.Writer 默认走流式写入——本地头里
的长度与 CRC 留 0、真值挪到条目尾部的数据描述符里,并在 flag 里置 bit3。**真机的读取器只认
「本地头里就写着长度」的条目**,遇到这种包直接判成损坏,报「备份恢复失败,你的当前数据未被更改」。
铁证(直接读文件头):
站点 Go 写的每一份导出 本地头 flags=0x0008 csize/usize=0/0 → 全部被拒
20260920-fixed-A-数据修复 本地头 flags=0x0000 有长度 → 真机接受(Python 重打包)
真机自己的包 本地头 flags=0x0800 有长度 → 正常(只有 UTF-8 位)
这也解释了之前为什么全都修错了方向:09-20 那次「修悬空引用后就能导入」其实是 Python 重打包顺手把
zip 写法修对了;而 09-29 一整天修的内容问题(活动颜色为 NULL、交通方式重名)都不是拦路虎。
修复:zipBackup 改用 zw.CreateRaw,先把三个文件读进内存算出 CRC32 与长度写进本地头,
不再用会置 bit3 的 zw.CreateHeader。
用例:TestZipBackupWritesNoDataDescriptor —— 直接读第 0 个条目的**本地头**,断言 bit3 为 0、
压缩/原始长度非 0、CRC 正确(变异验证:改回 CreateHeader → 立刻变红,且精确复现 0x0008 + 长度 0)。
真实数据验证:修复后新导出的本地头 flags=0x0000、长度正确。
全量验收 296 PASS / 0 FAIL / 0 SKIP。
|
2026-09-29 15:04:07 +08:00 |
|
tamakyi
|
9c1413bb39
|
导出/合并:交通方式编号空间串了,导出会吐出两条同名方式(真机整包拒收)
继上一轮修「活动颜色为 NULL」之后,生产重新导出 `data/20260929.rondbackup` 依旧导不回 rond。
这次拿**最后一次被真机接受的包**(20260920-fixed-A-数据修复)当参照逐项比对:SQLite 头、31 表 /
47 索引 / 建表语句 / 列定义、`Z_METADATA.Z_PLIST` 与 `Z_MODELCACHE`(逐字节)、值类型分布、文本合法
字节、BLOB 长度、UUID 唯一性、关系完整性、`Z_OPT`、行级时间不变量——全部一致;唯一差异是
**`ZTRANSPORT` 里出现了两条同名「公交」(Z_PK 2 与 4),而真机与可导入的 A 包都只有一条**。
根因:`mergeVisits` 把实机的 `ZTRANSPORT` 编号原样写进 `movements.transport_src`。站点没有交通方式表,
方式只活在位移的 transport_* 四列里,两套编号空间同一个数字含义不同(实测 src=4:站点=电🐔、实机=公交,
src 3/5/6 同样撞车)。于是同一个 src 带上两种含义:导出按 src 聚合只能取一个名字 → 另一种含义的位移挂错
方式;更糟的是吐出两条同名方式,真机那边是模型唯一约束级别的违规 → 整包拒收。
修复两处:
1. 合并侧 `mergeVisits`:按名称把实机方式归一到站点编号(新增 `(*Server).transportsByName`:同名沿用
站点已有的编号,没有才 `nextSrcPK(transportSeq)` 新发一个)。
2. 导出侧 `queryTransports`:改为按**名称**归组(名称就是身份),用量最多的名称优先认领编号,其余编号的
位移改挂到它上面;并清掉「基线里同名但编号已被归并掉」的旧行——`keepMissing` 会把它原样带回,于是又是
两条同名。这段清理必须排在所有托管表写完之后,否则兜底判据看到的还是基线里未重写的 ZMOVEMENT。
真实数据效果(本地库,与生产同一形状):方式恢复成站点原本那套 1火车/2公交/3高铁/4电🐔/5地铁/6步行、
无重名;位移各挂回自己的方式(电🐔 336、地铁 25、高铁 23、公交 19、火车 9、步行 3——修复前只有 6 条能对上)。
用例:`TestExportMergesDuplicateTransportNames`(同名归并 + 位移挂回自己那条 + 无悬空引用)、
`TestMergeKeepsTransportNumberingConsistent`(合并后同一 transport_src 不许有两种名称,附「位移确实被补
回来了」的哨兵断言避免写成空用例)。三处都做了变异验证:去掉改挂 / 去掉旧行清理 / 合并照抄实机编号 →
对应用例分别变红。
全量验收 295 PASS / 0 FAIL / 0 SKIP。
|
2026-09-29 13:25:50 +08:00 |
|
tamakyi
|
f26537b1e9
|
导出:新建活动没选颜色时补占位色,别再写 ZCOLOR_=NULL(真机拒收整包)
症状:生产导出的包导不回 rond,提示「错误,备份恢复失败,你的当前数据未被更改」。
根因链:站内「地点补全」新建活动时写的是 color=''、icon=''(fillup.go 的 INSERT),而导出侧
ns() 把空串也当空(返回 nil)——图标那侧早就有 📍 兜底(注释还写着「站点自己渲染时缺省用 📍」),
颜色这侧漏了,于是写出 ZACTIVITY.ZCOLOR_ = NULL。真机这一列在任何包里都是 0 NULL/0 空串
(扫了 data/ 与 storage/ 下全部 15 个包,只有出事的那份有 1 个空),Core Data 拿到非可选属性上的
NULL 会判成损坏数据 → 整包拒收。
修复:加 activityPlaceholderColor = "gray"(与 transportPlaceholderColor 同一套写法),
活动颜色为空时兜底。
用例:TestExportNeverNullsColumnsTheDeviceAlwaysFills 补上「没选颜色的活动」场景——它守的通用
不变量是「基线里 0 个 NULL 的列,导出新行也不许 NULL」,但此前只造过交通方式,没覆盖活动。
变异验证:去掉兜底 → 两条断言都红(提示语正好复刻生产现象)。
另新增 tools/audit_export_nulls.py:拿参照包做逐列空值画像 + 全部关系列悬空 + Z_MAX 检查,
是排查「导出被拒收」最快的一步(这次就是它一击命中的)。注意判据必须是「参照里 0 个 NULL」——
ZLOCATION.ZISOCOUNTRYCODE 真机自己就漏过 1 个 NULL,说明它容忍 NULL,不要当违规去修。
全量验收 293 PASS / 0 FAIL / 0 SKIP。
|
2026-09-29 11:50:06 +08:00 |
|
tamakyi
|
8763b850be
|
合并:地点配对只留一份实现(rond.MatchPlaces),合并侧不再自己配一遍
- 抽出 rond.MatchPlaces(stored, backup) PlaceMatch:坐标优先、退到「同名且相距 ≤1km」、
多值索引 + 认领即标记已用;DiffPlaces 与 web.mergePlaces 共用它。
- mergePlaces 原来是单槽位 byCoord/byName 自己再配一遍:同坐标/同名的多条备份地点全并到同一行
站内地点,到访被挂到错的分店(实测「茶颜悦色」两家分店相隔 8.3km,到访却指着同一条)。
现在直接用配对结果,再叠加 plan.AddPlaces 的新插入。
- loadStoredPlaces 与 mergePlaces 的地点查询都加 ORDER BY id:候选多于一个时靠顺序定谁配谁,
两侧必须一致,否则配对会飘。
- 删掉只写不读的 PlacePatch.CoordKey、ActivityPatch.Key(容易被误当成查找键)。
- 用例:TestMatchPlacesKeepsDuplicateNameGroupsDistinct(同一坐标/邻近同名都要一条配一条)、
TestMergePlacesMapsSameNameLocationsOneToOne(真库 + 真事务,两条同名备份地点必须映射到两条不同
地点);两处都做了变异验证(认领不标记已用 / 恢复单槽位映射 → 对应用例变红)。
- 探针库复测(从 dump 恢复、不动本地真库,49 组冲突全「用实机」):到访挂错地点(>1km)0/953 条、
地点被搬动 >1km 0 个;合并后再比对三类全 0(到访 0/47/0、地点 0/4/0、活动 0/0/0),
再点合并「按当前的选择没有要写入的改动」。全量验收 293 PASS / 0 FAIL / 0 SKIP。
已知取舍:勾「用实机」但没勾「并进来」时,若该到访在实机上的地点站内没有、且按 1km 规则也配不上,
place_id 会被置空(旧行为是挂到一条远方的同名地点上——那正是这次要去掉的错配)。
|
2026-09-29 10:46:54 +08:00 |
|
tamakyi
|
370c4285f8
|
合并:按名称兜底配对加 1km 上限,冲突行在名称相同时把坐标印出来
- DiffPlaces 的 takeByName 只在两点相距 ≤1km 时才认领:名称不是唯一键,连锁店同名是常态
(真机里「中国体育彩票」3 条、「长沙南站」2 条、「茶颜悦色」2 条)。没有上限时,坐标配不上
的两条同名地点会被强行配成一对——实测把佛山的「中国体育彩票」配给了贺州那条(直线 236.8km),
站长勾「用实机」就把站内那条地点的坐标搬到另一座城市(实测 src_pk=71 从贺州搬到佛山)。
注意 geo.Haversine 返回的是公里,常量按公里写。
- visitFieldChanges 的「地点」在两边名称相同时用 coordSuffix 把坐标印出来:这一项比的是坐标,
只印名称会渲染成「X → X」,站长看不出差在哪(连锁店重名、同一地点两边坐标略有出入都会走到这里)。
- 新增用例 3 条(相隔 236.8km 不该配对、665m 仍要配对、名称相同时必须带坐标)+ 变异验证。
- 复测(回滚后按浏览器默认全选「用实机」重跑):被搬动超过 1km 的地点从 1 个变 0 个;
合并后再比对 到访 0/47/1、地点 0/4/1、活动 0/0/0;953 条 UUID 到访全命中、到达时间 0 偏差。
|
2026-09-29 10:18:22 +08:00 |
|
tamakyi
|
10d23e03cf
|
合并:重复地点不再反复判「备份新增」,到访的活动改按身份比
- DiffPlaces 的 byCoord/byName 改为多值索引 + take() 逐条认领未用项:真机一个坐标下会有
多条同名同坐标的地点(实测「柳州站」3 条),单槽位时同一坐标最多只配上 2 条,第 3 条
永远判「备份新增」⇒ 每次合并都再插一条重复地点、差异永远归不了零(实测 368→438→439)。
修完:地点新增 70→67、合并后再比对归零、再点合并直接「没有要写入的改动」。
- 到访的「活动」由按名称比改为按身份(ZUID_)比,两边都缺 UUID 才退回名称:16 个活动改名
曾把 355 条到访全变成同因冲突,且逐条选「用实机」只写回 activity_id(映射回同一个活动)
改不掉名字。loadStoredVisits 为此多 JOIN 一次 entity_raw 取 ZACTIVITY.ZUID_。
- 回归用例 4 条(重复地点一一配上、活动改名不算冲突、换活动仍要报、缺 UUID 退回名称)+
「合并后地点差异归零」断言 3 处;两处改动都做过变异验证。
- 实测(09-28 备份并进 09-14 数据集):到访冲突 253→6,合并两次地点数 438→439 变为 435→435。
|
2026-09-28 19:31:21 +08:00 |
|
tamakyi
|
f2ce6646c4
|
报告:补 P2 批次(覆盖率口径与关键页冒烟)记录
|
2026-09-24 22:48:32 +08:00 |
|
tamakyi
|
256620ee3d
|
验收:覆盖率改用 -coverpkg 口径,新增关键页冒烟
|
2026-09-24 22:48:31 +08:00 |
|
tamakyi
|
05c6e970db
|
测试:补 rond 合并冲突与 gpx 解析用例,删两处死代码
|
2026-09-24 22:48:22 +08:00 |
|
tamakyi
|
c001e64ee0
|
报告:补稳定性基线与 P0/P1 收尾记录
- 稳定性基线(覆盖率 ≈51%、不设环境变量时 95 PASS / 86 SKIP 的假绿、无 CI)
- scripts/verify.sh 的能力与实测闸门
- 全新库冒烟的变异验证(迁移块顺序错了只有新库会炸)
- web 整包 -race:161 PASS / 0 DATA RACE
- 收尾验收:244 用例 / 0 FAIL / 0 SKIP
|
2026-09-24 20:55:52 +08:00 |
|
tamakyi
|
ff594121da
|
验收脚本:SKIP/FAIL 也把子测试数进去
只数顶层 `^--- SKIP` 会把子测试里的跳过漏掉 —— 那正是「看起来全绿、其实没跑」的形态。
换成 `^[[:space:]]*--- SKIP`,失败明细同理。
|
2026-09-24 20:47:23 +08:00 |
|
tamakyi
|
d2e4392a88
|
验收脚本、工具整理与排查报告
- scripts/verify.sh:把「带齐六个环境变量、且必须 0 skip」这条纪律固化下来
* DSN 自己从 conf/app-test.ini 取,不把口令写进脚本
* 库名不含 test 就拒跑(指到维护库会多出「users 不存在」+ 一堆假 skip)
* 数 SKIP,非 0 直接失败 —— 不带环境变量时 go test 依然回 ok,最容易被当成通过
* 支持 --fast / --race / --cover / --pkg / --run
- tools/:删掉 6 个一次性探针(git 历史里可翻)和误入库的 __pycache__,
只留在用的 4 个工具并补 README;.gitignore 补 __pycache__ / *.pyc
- audit-2026-09-21.md:全量排查报告(含 11 批修复记录与收尾审查)
|
2026-09-24 20:38:18 +08:00 |
|
tamakyi
|
d5f6dfa381
|
审计修复:前端(模板 / CSS / JS 与无障碍)
- 地图脚本:localStorage 读写走 lsGet/lsSet 兜底(隐私模式下裸访问会打断整段脚本);
preview() 在 /api/map-config 还没回来时不丢点(previewQueue 在 if (live) 之前排队);
异步回包一律发序号作废,取消后回来的包不再画
- photo-exif.js:EXIF 时间全 0 不再变成 1899-11-30 写进到访时间;读 count 前先按版本卡宽度
- 数据编辑页:三个筛选表单都把其余条件用隐藏域原样带上,提交不再把别的列表的筛选清掉
- 补全页:sessionStorage 的读写一律包 try(脏数据/隐私模式都会抛)
- 窄屏:删掉被后面同特异性规则盖掉的死内边距;宽表堆叠类名成对、判据扩到所有 tb 表
- 无障碍:图片删除按钮补 aria-label,访客画廊图片补 alt
- report.html 补上漏掉的 </tr>,fog-mask 的宽表带 tb-stack
本批不碰数据库与导出路径:往返包 823/323/8/30/791/557/2246 全等、
坐标偏差 0.000000000、时间偏差 0s、迷雾 8701 块一致。
|
2026-09-24 20:37:57 +08:00 |
|
tamakyi
|
b4c2a8452f
|
审计修复:后端(安全 / 数据一致性 / 性能)
audit-2026-09-21.md 里的后端条目落地。每条都配了回归守卫,并逐条做过变异验证
(注入原 bug → 确认按正确理由变红 → 还原 → 变绿)。
安全
- CSRF 只加同源 Origin/Referer 守卫(csrf.go),不动模板
- 会话 cookie 的 Secure 按请求判断;两步验证两闸(loginGuard + 验证码一次性)
- 跳转收口到 safeInternalPath;clientIP 只认 XFF 最后一段
- GPX 按属主圈定且查 RowsAffected;下载名走 downloadDisposition
- 上传口统一 MaxBytesReader;落盘后的失败路径清掉半成品
数据一致性
- 区域判定两端各过一遍再 AND(apiTrack 与 movementsWhere 对齐)
- Filter.Verdict 全域一致(下推到 visitsWhere);聚合刷新必须返回 error
- 附属数据改挂坐标键,Go 与 SQL 两套表达式逐位一致
- 脏备份的伪时间/越界坐标不再进领域模型;rows 循环统一 drainRows
- 合并与导出:writeRawRow 上抛错误、悬空引用清理、原生列补全
性能
- placeCols 相关子查询带 dataset_id 前导列;findSourceArchive 先按大小筛
- 批量勾选改 ANY($3::bigint[]);fillupCache 加上限;同一查询不再查两遍
另:新增「全新库」冒烟测试 —— 空库跑完 Migrate 再用应用侧读查询真打一遍,
迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
|
2026-09-24 20:37:16 +08:00 |
|
tamakyi
|
78865fc736
|
导出:删掉到访后,别把它指向的原始采样点留成悬空引用
真机报「备份恢复失败,你的当前数据未被更改」——这句说明它是先校验、整体回滚。
往下查的第一个醒悟:**Core Data 的 SQLite 表里根本没有外键约束**(关系由 Core Data
自己维护),所以之前跑的 PRAGMA foreign_key_check 报 0 违例毫无意义,它压根没得可查。
按关系列手查悬空引用,结果:
原版:悬空 0
导出:ZRAWVISIT.ZVISIT 有 2 条指向不存在的 ZVISIT —— (2051→到访46, 465→到访200)
正是用户在站点上删掉的那两条到访。ZRAWVISIT(原始 GPS 采样点)是不托管实体、
导出时原样保留,于是删到访就留下了指向已删记录的悬空关系;而真机删到访时
采样点是跟着级联删的,所以这是它自己绝不会产生的形状。
修法:visit.finish() 之后,删掉 ZVISIT 指向不存在到访的 ZRAWVISIT 行。
选「删」而不是「把 ZVISIT 置空」:真机的「未归并采样点」虽有 1461/2246 条(置空合法),
但留成未归并会让 app 重新推出一条到访,把用户删掉的记录复活。
顺带加固:
- verify_export_structure.py 新增 1.45 节「关系完整性」,手查 8 组关系列的悬空引用,
有悬空就把结论判成「有差异」——这条 foreign_key_check 查不出来的坑进了标准验收清单。
- TestExportDropsOrphanedRawVisits:删一条带采样点的到访 → 导出 → 断言没有悬空;已做变异验证。
|
2026-09-20 18:05:47 +08:00 |
|
tamakyi
|
56c7283e38
|
导出:新建的交通方式别把颜色/图标写成 NULL
排查「导出包导不回真机」时逐列比对发现的唯一实质缺陷:
站点新建的交通方式「步行」导出来是 ZCOLOR_=NULL / ZICON_=NULL,而真机原有 5 条
(火车/公交/高铁/电🐔/地铁)全部有值 —— 真机这两列从不写 NULL、也不写空串。
成因链:站点不单独维护交通方式表(颜色/图标是从 movements 上聚合的),
建方式时没选就存成 NULL,queryTransports 的 FILTER (WHERE ... IS NOT NULL) 聚成 NULL,
ns() 又把空串和 NULL 一起变成 nil,写进包里就是 NULL。
修法:只给**新建的行**补占位(gray / circle.fill),基线里已有的行不碰 ——
那种情况 keepNilCols 会保留真机原值,擅自补占位反而会把真值盖掉。
与「新建活动补 blue/📍」同一套做法。
同时加一条通用的守卫(export_placeholder_test.go):判据是
「基线里 100% 有值的列,导出里的新行也不许为空」,对所有托管表逐列扫。
只修 ZTRANSPORT 是治标,这条才是同类问题的根治办法。已做变异验证。
|
2026-09-20 17:37:04 +08:00 |
|
tamakyi
|
693ee82ab8
|
修「填了显示名却说还没有中文名」,并给枚举拼错拦一道
用户报:把某行的导出值改成 MKPOICategoryCarRent、又手填了显示名「租车」,
保存成功、框里也回填了,页面却仍挂着「还没有中文名」。
两件事叠在一起:
1. 判断错了。Missing 只看内置表,没看有没有自定义覆盖 ——
填过了还照样提示。改成 categoryLabelMissing(v) && 没有覆盖。
补 TestCategoryLabelMissingRespectsOverride,并做变异验证(去掉那半个条件即报错)。
2. 值本身是拼错的。MKPOICategoryCarRent 不是真机的枚举,正确是
MKPOICategoryCarRental(少了个 al)——站内看不出任何异常,但导回手机后
那个地点显示不出类别。这正是加「显示名」时提醒过的风险,所以补一道防线:
编辑页把内置枚举名渲染给脚本,往 poi_category 写值的两个入口
(类别台账的「改名」、地点行内编辑的「自定义类别」)都过 okEnum(),
不在已知枚举里就 confirm,还能猜「你是不是想写 MKPOICategoryCarRental」。
中文值和已知枚举不打扰。
另外:这次用脚本往模板写 JS 时,\n 被多转义一层成了真换行,JS 单引号字符串
含裸换行 ⇒ 整块 <script> 语法错误、页面交互全哑,而模板解析测试照样通过。
新增 inline_script_test.go 扫所有内联脚本块(现在 31 个),字符串跨行就报错
并指出文件行号,已做变异验证。
|
2026-09-20 12:22:11 +08:00 |
|
tamakyi
|
82e63412c0
|
类别显示名可自己维护,顺手把两套对不上的中文名合成一套
你要改的那个「海滩」原来根本不在数据里——它是 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)。
|
2026-09-20 10:54:51 +08:00 |
|
tamakyi
|
cbb45b6d6b
|
补全页的类别候选取「库里在用的 ∪ 静态表」
这一页原来只列静态表(30 个 MapKit 枚举),库里真在用的值不在候选里——
真机自己写的中文类别(一般道路 / 彩票店 / 租车点 / 美食街)只能手打,
一不小心就挑个名字相近的静态枚举,跟已有数据并存成两种,同一个意思分两处统计。
现在三段拼,同一个值只出现一次:
1. 数据集里在用的,按地点数降序,标「中文名(在用 N)」
2. 真机用过的 12 种(当前数据里还没有的),标「真机用过」
3. 其余常见 MapKit 类别
在用的那份信息更准,所以顶掉静态表里的说明——既在用又在静态表里的只出现一次。
真实数据实测:36 项 = 16 在用 + 18 静态 +(不动)+(其它「自己写」),零重复;
排在前面的是 餐厅(在用 51)/ 商店(在用 29)/ … / 一般道路(在用 5)。
|
2026-09-20 10:07:00 +08:00 |
|
tamakyi
|
da6fe54ab4
|
数据编辑页加「活动与类别」卡片:改名 / 删除 / 新增 / 合并
活动的管理能力其实后端早就有(create/update/delete 三个端点),但入口只藏在
「到访记录」卡片里——要先在下拉里选中某个活动,再点按钮弹 prompt,看不见
「哪些活动一条到访都没挂」(实测有 0 条的活动);类别则完全没有集中入口。
现在页首多了一张卡片(唯一默认展开的),两个表格都带用量:
- 活动:名称行内改、删除(有引用时二次确认并报条数)、新增,
另有一条「把[A]并到[B]」——改名解决不了「两个名字指同一件事」,
合并会把到访转过去、把 is_home/is_work 抄成目标的,再删源活动、清它的原始行留档。
- 类别:类别没有独立的表,值就长在 places.poi_category 上,所以「改名 / 清空」
都是按值批量作用到地点;改成另一个已有值就是合并,不另做按钮。
新增端点:/admin/edit/category/rename、/admin/edit/category/clear、
/admin/edit/activity/merge。编辑页那条 DISTINCT poi_category 查询顺手改成
GROUP BY 带计数,一份数据两用。
顺带修一处会静默失效的写法:新表原来只写了 tb-stack,漏了配对的 tb,
而 CSS 全是 `.tw > table.tb.tb-stack …` ⇒ 一条堆叠规则都没命中,窄屏下表仍是
640px 底宽。横滑是在 .tw 里滚的、页面不溢出,肉眼很难发现,守卫测试也只查
「有没有 tb-stack」——已补上「有 tb-stack 必须有 tb」并做了变异验证。
|
2026-09-20 09:51:43 +08:00 |
|
tamakyi
|
ffee082fad
|
补全页:手工填过的值自动进下拉候选,后面同类地点不用再打一遍
下拉的候选是渲染时定好的,这一页手工新填的活动名/类别进不去候选,
后面遇到同类地点只能照着再敲一遍——这正是「其它(自己写)」最费事的地方。
现在填过的值会被记住并补进同类下拉,标成「新填:X」,插在建议之后、
真正的候选之前,一眼就能挑到;本页实时同步(打字即生效),
翻页和点「用高德分析」重载也留得住,提交成功后清掉(那之后它们已经是真候选了)。
顺带两处:
- 草稿保存改成按 name 合并,翻页不再把上一页填的覆盖掉;分页链接也接了保存。
- 重画候选前先把「这一格已经选中的新填值」留住再还回去,
否则改一下文本框就会把别的行选好的值悄悄抹成空。
文本框的监听直接挂在框上而不是委托到 document:`new Event('input')` 默认不冒泡,
探针第一次就是这么假绿的——直接挂不依赖冒泡,也少一层隐式约定。
|
2026-09-20 09:32:07 +08:00 |
|
tamakyi
|
bf23fc62d7
|
补全页:活动改成和类别同款下拉,两列样式一致
活动原本是 input+datalist、类别是 select,同在一行列里长得不一样,看着别扭。
现在两者共用同一套写法:下拉 + 末尾「其它(自己写)…」+ 选中后露出的文本框
(后端 act_<id>==__custom__ 时读 act_<id>__custom,与类别同一套约定)。
CSS 类也合成一个 fill-sel,脚本只需遍历一次。
顺带:删掉不再需要的 datalist;采纳建议、草稿存取都改走 select.fill-sel。
草稿恢复时对下拉派发 change,所以「其它」的文本框也能跟着恢复显示。
|
2026-09-19 23:46:02 +08:00 |
|
tamakyi
|
5aeded2d51
|
补全页恢复手工填写,把「不动」变成真正的默认
上一版改成 select 之后有两个问题:活动和类别都只能从列表里挑,填不进去的写不了;
而且建议被强制预选,用户选「不动」再点「用高德分析」(会整页重载)就白选了,
看起来像被锁死。
现在:活动是可输入的输入框,带已有类型提示,填新名字会新建类型;
类别下拉末尾加「其它(自己写)…」,选中后露出文本框,写什么存什么——
真机自己也存中文类别,写中文一样认得,不必强记 MKPOICategory 那串枚举名。
「不动」改成真正的默认:什么都不预选,想批量采纳就点「采纳本页建议」。
点「用高德分析」前会把本页填过的存进 sessionStorage,回来接着填,不会白改。
|
2026-09-19 20:13:06 +08:00 |
|
tamakyi
|
6b023fb9fd
|
fix
|
2026-09-19 19:50:27 +08:00 |
|
tamakyi
|
824c1e3b3d
|
补全页加分页,每页 30 行
218 行在手机上堆成一屏一行有五万多像素长,分页后第 1 页只有八千多。
页码会夹紧:?page=999 停在最后一页而不是给空白页,解析不出来回第一页。
提交后留在同一页——处理完的行会从列表里消失,停在这儿才看得见剩下的。
翻页前若这一页有改过还没提交的下拉,先确认一次(预选的建议不算「改过」)。
|
2026-09-19 18:25:16 +08:00 |
|
tamakyi
|
b311ba202b
|
补全页的两个下拉改成 select,类别显示中文
input + datalist 只在下拉里显示中文,框里永远是 MKPOICategoryPark 这种枚举名,
站长根本看不懂。改成 select 之后选中项直接是「公园(真机用过)」。
活动下拉同理:建议项排在最前并预选,后面跟全部现有活动;选「(不动)」就跳过。
页面原始体积 940KB,但 gzip 后只有 22KB(选项高度重复),不必操心。
顺带把建议项的值简化成活动名本身——处理函数本来就按名字查、查不到就新建,
不需要额外的 __new__ 前缀。
|
2026-09-19 17:05:34 +08:00 |
|
tamakyi
|
9c7e152361
|
补全页显示高德找到的 POI 与类型,类别标出真机用过的
原先没映射出建议的行就是一片空白,可高德其实早就说了「这里是什么」——
「中国邮政 / 生活服务;邮局」「沙县小吃 / 餐饮服务;中餐厅」,
这种信息看着就能判断该填什么。现在多一列「高德参考」把它显示出来。
类别下拉带上中文,并标出哪几种是**真机确实用过**的:
ZCATEGORY_ 那列存的就是这些 MKPOICategory 枚举串,那 12 种导回 rond 一定认得,
其余是 MapKit 支持但你的设备还没用过的,写错只会显示不出类别、不会坏数据。
|
2026-09-19 16:46:34 +08:00 |
|
tamakyi
|
6c061a0548
|
地点补全:按地名与坐标猜活动与类别,只填空
后台新增「地点补全」页,把「名下到访没设活动」和「类别为空」的地点列出来,
给出建议、由站长确认后再写。判断分三层:
- 本地关键词表:零延迟、离线,命中即高置信(「星巴克咖啡」→ 奶茶和甜甜水)
- 高德周边搜索:按坐标问「这里是什么 POI」,比按名字搜准;
拿到的类型再映射成活动与 MapKit 类别
- 人工:都猜不出的留空,站长自己填;填了列表里没有的名字会自动建成新活动类型
两条底线:**只填空**(已有活动或类别的一律不动);**活动按地点批量设**,
一次把它名下所有没设活动的到访一起填上,否则同一个地点会在统计里分裂成两种活动。
类别写的是 Apple MapKit 的枚举名(真机确认用过 12 种),不是高德分类。
规则上踩过两个坑,都写进注释了:匹配前要先掐掉括号里的地址(否则
「尚优居客栈(大理古城步行街店)」会命中「步行街」),并且按词长优先
(否则「马鞍山公园」会命中「山」);单字关键词一律不要,误命中率太高。
|
2026-09-19 16:31:01 +08:00 |
|
tamakyi
|
30c502ce7b
|
备份与迷雾的差异分析、合并执行,附属数据改挂坐标键
备份(.rondbackup)与迷雾(.fwss)各加一层「先看差异再动手」:
- 迷雾:上传先出差异页,说清叠加能补多少、整体替换会丢多少,并在真实底图上
用绿/灰/红标出会新增 / 不变 / 会丢的区域
- 备份:与当前数据集按 UUID 逐条比对(rond 每次导出都会重排 Z_PK,编号认不出人),
新增合入、冲突逐条选、仅站内有逐条勾选删除,全在一个事务里执行;
合入的行写回 entity_raw,保证再传同一份备份时差异归零
- 地点按坐标对齐:站点的「补全地址」会把地点改名,拿名称当键会让一整个地点的
到访全变成假冲突
- 附属数据(备注/结论/访客票/图片)从 dataset_id+src_pk 换成坐标键,换数据集不再失联;
访客票没有 dataset_id,回填按活跃数据集认领
- 修正 contentHash 拿 Z_PK 当指纹的隐患,以及两处「src_pk 跨快照稳定」的错误注释
导出格式(.rondbackup / .fwss)保持原样,往返测试通过。
|
2026-09-19 16:04:00 +08:00 |
|
tamakyi
|
9e01923244
|
宽表在窄屏真的不横滑了:堆叠判据退回媒体查询,后台三张表补上
真机反馈「手机上还是要左右滑」,两个根因:
1. 上一版把堆叠判据写成了 @container。它按卡片自己的宽度判断更准,但要
iOS 16 / Chrome 105 才支持——旧手机把整块规则当不认识的东西丢掉,表格
完全不堆叠、退化成横滑 328~388px,比改之前还糟。改回 @media,阈值从
620 抬到 800:卡片内容宽 ≈ 视口 − 93,最宽的表 min-width 700,视口 793
以上才放得下,620 那档会让 621~792 两头不靠。
2. /admin/edit 的地点/到访/行程三张表压根没加 tb-stack。它们默认折叠在
data-card 里,探针在默认态量到的是 display:none,前两轮审计都没扫到。
顺带修掉三处被这次改动顶出来的问题:
- 宽表卡片不放 g2:/stats /report 有几张表被塞进半宽卡片,视口 1440 时
卡片只有 494px,桌面端照样要横滑 100+px。改成整行(只动含表的那些网格,
「推荐 & 踩雷」「类型分布 + 一天中的分布」这类没有表的仍保持两列)。
- 操作格的 inline white-space:nowrap 会继承进行内编辑表单,堆叠态下
「已选:#403 …」那一行不换行、把卡片顶破。挪成 .ops 类,窄屏放开。
- 行内编辑表单的 min-width 改成 --efw 变量(inline 的 min-width 压不过
CSS 覆盖),堆叠态置 0 让它自然铺满。
导航装不下时右缘渐隐一下,提示还能往右滑;滑到末端自动收掉。用 mask 而不是
盖一层渐变伪元素——伪元素会把链接文字一起遮住。
回归:tb_stack_test.go 补 /admin/edit(12 张表),表头为空的列(如末尾操作格)
不再要求 data-label;新增 wide_table_layout_test.go 守住「宽表卡片必须整行」+
「凡被 .tw 包住的表必须带 tb-stack」+「堆叠规则必须在 max-width≥792 的 @media 里」。
|
2026-09-19 02:04:03 +08:00 |
|
tamakyi
|
7236c7a9d5
|
窄屏宽表不再横向滚动:改成一屏一行的「字段名 + 值」
移动端有 7 张表要横滑 300+px 才看得全,滑着还对不上表头(地点库 8 列、统计 3 张、专题详情、
首页天气、后台数据集表与数据说明、专题管理、报告页 2 张)。给这些表加 tb-stack:
窄屏隐藏 thead、每行一屏一块、每格「字段名 + 值」竖排,值多行时用悬挂缩进。
- 字段名取自 td 的 data-label(首格是这行主角,故意不带标签;操作格给「操作」)——
这样改列顺序也不会串标签,比把名字写进 CSS 的 nth-child 稳。
- 表格结构、链接、表单、按钮一律没动(只是隐藏 thead + 改 display),桌面与平板仍是原表。
- 断点只用 @media (max-width:620px):试过容器查询(能顺手治掉「宽屏里 g2 窄卡片也要横滑」),
但实测会把 /stats 三个卡片撑高 1300px,太臃肿,不划算。
实测:390/620 十张表需横滑全部为 0、每行每格都有标签;621/700/1440 的横滑量与改动前一致
(桌面零变化);128 个链接/按钮滚动到可见后逐个点测均可点中,无裁切;全站审计 390 站长/访客 +
320 + 1440 溢出全 0、0 JS 报错,小触控与小字数与改动前逐页一致(320 下露外元素 /places 277→2、
/stats 114→12、/admin 46→4,剩下的都是有意的横滑容器)。
新增回归 internal/web/tb_stack_test.go:渲染 7 个页面、逐行核对「每个非首格都有 data-label」
且「格子数 == 表头列数」(变异验证过:去掉一个标签立刻 FAIL)。
|
2026-09-18 22:24:12 +08:00 |
|