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
|
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
|
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
|
05c6e970db
|
测试:补 rond 合并冲突与 gpx 解析用例,删两处死代码
|
2026-09-24 22:48:22 +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
|
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
|
b327bc1230
|
first release
|
2026-09-18 13:05:49 +08:00 |
|