备份与迷雾的差异分析、合并执行,附属数据改挂坐标键
备份(.rondbackup)与迷雾(.fwss)各加一层「先看差异再动手」: - 迷雾:上传先出差异页,说清叠加能补多少、整体替换会丢多少,并在真实底图上 用绿/灰/红标出会新增 / 不变 / 会丢的区域 - 备份:与当前数据集按 UUID 逐条比对(rond 每次导出都会重排 Z_PK,编号认不出人), 新增合入、冲突逐条选、仅站内有逐条勾选删除,全在一个事务里执行; 合入的行写回 entity_raw,保证再传同一份备份时差异归零 - 地点按坐标对齐:站点的「补全地址」会把地点改名,拿名称当键会让一整个地点的 到访全变成假冲突 - 附属数据(备注/结论/访客票/图片)从 dataset_id+src_pk 换成坐标键,换数据集不再失联; 访客票没有 dataset_id,回填按活跃数据集认领 - 修正 contentHash 拿 Z_PK 当指纹的隐患,以及两处「src_pk 跨快照稳定」的错误注释 导出格式(.rondbackup / .fwss)保持原样,往返测试通过。
This commit is contained in:
+150
-13
@@ -171,38 +171,50 @@ CREATE TABLE IF NOT EXISTS fog_blocks (
|
||||
PRIMARY KEY (user_id, gbx, gby)
|
||||
);
|
||||
|
||||
-- 地点备注与别名。按 src_pk(rond 内部地点 ID,跨快照稳定)而不是 places.id 存,
|
||||
-- 这样重新上传备份、数据集换了新 id 之后备注依然挂着。
|
||||
-- dataset_id 是后加的:src_pk 只在单个数据集内唯一,缺了它切数据集时同编号的
|
||||
-- 地点会把别人的备注/别名认领过去。
|
||||
-- 地点备注与别名。挂**坐标键**(place_key)而不是 src_pk:
|
||||
--
|
||||
-- src_pk(rond 的 Z_PK)只在单个数据集内有效——rond 每次导出/同步都会重排编号
|
||||
-- (实测两份备份里 822 条到访的 UUID 完全一致、Z_PK 100% 不同),所以换一份备份、
|
||||
-- 换个数据集之后,同编号的完全是另一个地点,备注就再也挂不回去了。
|
||||
-- 坐标是地点的地理事实,跨备份稳定:place_key = 经纬度各取 6 位小数拼起来
|
||||
-- (约 0.1m 精度),与 Go 侧的 rond.PlaceCoordKey 必须逐位一致。
|
||||
-- dataset_id / src_pk 保留下来只作「最后一次写入时挂在哪儿」的排查线索。
|
||||
CREATE TABLE IF NOT EXISTS place_notes (
|
||||
user_id BIGINT NOT NULL,
|
||||
place_key TEXT NOT NULL,
|
||||
dataset_id BIGINT NOT NULL DEFAULT 0,
|
||||
src_pk INT NOT NULL,
|
||||
src_pk INT NOT NULL DEFAULT 0,
|
||||
alias TEXT NOT NULL DEFAULT '',
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
rond_note TEXT NOT NULL DEFAULT '',
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, dataset_id, src_pk)
|
||||
PRIMARY KEY (user_id, place_key)
|
||||
);
|
||||
|
||||
-- 站长对地点的「官方结论」:推荐(1) / 踩雷(-1),与自由文本备注分开存,同样按 src_pk。
|
||||
-- 站长对地点的「官方结论」:推荐(1) / 踩雷(-1),与备注一样按坐标键存。
|
||||
CREATE TABLE IF NOT EXISTS place_votes (
|
||||
user_id BIGINT NOT NULL,
|
||||
place_key TEXT NOT NULL,
|
||||
dataset_id BIGINT NOT NULL DEFAULT 0,
|
||||
src_pk INT NOT NULL,
|
||||
src_pk INT NOT NULL DEFAULT 0,
|
||||
verdict SMALLINT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, dataset_id, src_pk)
|
||||
PRIMARY KEY (user_id, place_key)
|
||||
);
|
||||
|
||||
-- 访客对地点的投票:一人一票、可改可撤。voter_key 由匿名 cookie + IP 派生,
|
||||
-- 与数据集无关(换快照不丢),故不挂 dataset_id。
|
||||
-- 访客对地点的投票:一人一票、可改可撤。voter_key 由匿名 cookie + IP 派生。
|
||||
--
|
||||
-- 这是站点自己的数据(rondbackup 里没有这一项),但同样要认得出「是哪个地点」:
|
||||
-- 挂 place_key 而不是 src_pk,理由与备注、结论一样。src_pk 只留作排查线索。
|
||||
-- 迁移时这张表没有 dataset_id 可依,只能按**活跃数据集**认领那个编号属于哪个地点
|
||||
-- (见文件末尾的迁移块)。
|
||||
CREATE TABLE IF NOT EXISTS place_visitor_votes (
|
||||
src_pk INT NOT NULL,
|
||||
place_key TEXT NOT NULL,
|
||||
voter_key TEXT NOT NULL,
|
||||
vote SMALLINT NOT NULL,
|
||||
src_pk INT NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (src_pk, voter_key)
|
||||
PRIMARY KEY (place_key, voter_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pvv_src ON place_visitor_votes(src_pk);
|
||||
@@ -309,9 +321,17 @@ UPDATE place_notes SET dataset_id = COALESCE((SELECT id FROM datasets WHERE is_a
|
||||
UPDATE place_votes SET dataset_id = COALESCE((SELECT id FROM datasets WHERE is_active ORDER BY id DESC LIMIT 1), 0)
|
||||
WHERE dataset_id = 0;
|
||||
|
||||
-- ⚠️ 这一段只在「还没搬到坐标键」的库上跑:文件末尾那次迁移会把主键换成
|
||||
-- (user_id, place_key),这里要是也跟着改回去,两张表就会在每次启动时来回翻,
|
||||
-- 永远停在最后一个执行的块上。
|
||||
DO $$
|
||||
DECLARE cols text;
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'place_notes' AND column_name = 'place_key') THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT string_agg(a.attname, ',' ORDER BY array_position(c.conkey, a.attnum))
|
||||
INTO cols
|
||||
FROM pg_constraint c
|
||||
@@ -395,6 +415,7 @@ ALTER TABLE places ADD COLUMN IF NOT EXISTS src_lon DOUBLE PRECISION;
|
||||
-- SQL 里写不出来。启动时由 ingest.BackfillAllCoordSource 统一处理,那里同时能识别出
|
||||
-- 「站点导出的备份给新建地点合成的 ZLOCATION 行」,不会把它误当成真机的原始 GPS。
|
||||
|
||||
|
||||
-- 地点图片:站长给地点配的图。字节由**浏览器**转成 WebP 后再上传(服务端只负责收下与转存),
|
||||
-- 1 核机器不用扛解码。storage 记这一张实际放在哪、object_key 是本地相对路径或对象存储 key:
|
||||
-- 换了存储后端之后旧图仍按各自那一列去取,不会因为改了设置就全变破图。
|
||||
@@ -402,6 +423,9 @@ CREATE TABLE IF NOT EXISTS place_images (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dataset_id BIGINT NOT NULL REFERENCES datasets(id) ON DELETE CASCADE,
|
||||
src_pk INT NOT NULL,
|
||||
-- place_key 与备注、结论共用同一套坐标键:换数据集后图片还挂得回原地点。
|
||||
-- 对象键里仍然带 dataset_id(那只是存储路径,与「认不认得出是同一个地点」无关)。
|
||||
place_key TEXT NOT NULL DEFAULT '',
|
||||
storage TEXT NOT NULL DEFAULT 'local',
|
||||
object_key TEXT NOT NULL,
|
||||
width INT NOT NULL DEFAULT 0,
|
||||
@@ -412,3 +436,116 @@ CREATE TABLE IF NOT EXISTS place_images (
|
||||
UNIQUE (dataset_id, src_pk, object_key)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_place_images_place ON place_images(dataset_id, src_pk, position);
|
||||
|
||||
-- ---------- 附属数据改挂坐标键(一次性迁移)----------
|
||||
-- 备注 / 结论 / 图片原本挂 (dataset_id, src_pk),换个数据集就全看不见了。
|
||||
-- 这里把它们搬到坐标键上;只在「还没有 place_key 列」时执行,所以新库直接走上面的
|
||||
-- CREATE TABLE,老库走这一段。
|
||||
--
|
||||
-- 坐标键的算法必须与 Go 侧 rond.PlaceCoordKey 逐位一致:
|
||||
-- fmt.Sprintf("%.6f|%.6f", lat, lon) ↔ to_char(lat,'FM999990.000000')||'|'||to_char(lon,'FM999990.000000')
|
||||
-- 已在真实数据上逐条比对过(368/368 相同)。改任何一边都要重新比。
|
||||
DO $$
|
||||
DECLARE
|
||||
cols text;
|
||||
BEGIN
|
||||
-- 判据是**主键**而不是「列在不在」:半迁移过的库(列补上了、主键还是老的三列)
|
||||
-- 也得能修好,否则这张表会一直卡在中间状态。
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'place_notes' AND column_name = 'place_key') THEN
|
||||
ALTER TABLE place_notes ADD COLUMN place_key TEXT;
|
||||
END IF;
|
||||
SELECT string_agg(a.attname, ',' ORDER BY array_position(c.conkey, a.attnum))
|
||||
INTO cols FROM pg_constraint c
|
||||
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
||||
WHERE c.conname = 'place_notes_pkey';
|
||||
IF cols IS DISTINCT FROM 'user_id,place_key' THEN
|
||||
UPDATE place_notes n SET place_key = (
|
||||
SELECT to_char(p.lat, 'FM999990.000000') || '|' || to_char(p.lon, 'FM999990.000000')
|
||||
FROM places p
|
||||
WHERE p.dataset_id = n.dataset_id AND p.src_pk = n.src_pk
|
||||
AND p.lat IS NOT NULL AND p.lon IS NOT NULL)
|
||||
WHERE n.place_key IS NULL;
|
||||
-- 地点已经不在库里的(被删过),留一个不会撞的孤儿键,宁可留着也别丢
|
||||
UPDATE place_notes SET place_key = 'orphan:' || dataset_id || ':' || src_pk
|
||||
WHERE place_key IS NULL;
|
||||
-- 同一坐标上有多条时只留最新的一条,否则加不上主键
|
||||
DELETE FROM place_notes a USING place_notes b
|
||||
WHERE a.user_id = b.user_id AND a.place_key = b.place_key
|
||||
AND (a.updated_at, a.ctid) < (b.updated_at, b.ctid);
|
||||
ALTER TABLE place_notes ALTER COLUMN place_key SET NOT NULL;
|
||||
ALTER TABLE place_notes DROP CONSTRAINT IF EXISTS place_notes_pkey;
|
||||
ALTER TABLE place_notes ADD PRIMARY KEY (user_id, place_key);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'place_votes' AND column_name = 'place_key') THEN
|
||||
ALTER TABLE place_votes ADD COLUMN place_key TEXT;
|
||||
END IF;
|
||||
SELECT string_agg(a.attname, ',' ORDER BY array_position(c.conkey, a.attnum))
|
||||
INTO cols FROM pg_constraint c
|
||||
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
||||
WHERE c.conname = 'place_votes_pkey';
|
||||
IF cols IS DISTINCT FROM 'user_id,place_key' THEN
|
||||
UPDATE place_votes n SET place_key = (
|
||||
SELECT to_char(p.lat, 'FM999990.000000') || '|' || to_char(p.lon, 'FM999990.000000')
|
||||
FROM places p
|
||||
WHERE p.dataset_id = n.dataset_id AND p.src_pk = n.src_pk
|
||||
AND p.lat IS NOT NULL AND p.lon IS NOT NULL)
|
||||
WHERE n.place_key IS NULL;
|
||||
UPDATE place_votes SET place_key = 'orphan:' || dataset_id || ':' || src_pk
|
||||
WHERE place_key IS NULL;
|
||||
DELETE FROM place_votes a USING place_votes b
|
||||
WHERE a.user_id = b.user_id AND a.place_key = b.place_key
|
||||
AND (a.updated_at, a.ctid) < (b.updated_at, b.ctid);
|
||||
ALTER TABLE place_votes ALTER COLUMN place_key SET NOT NULL;
|
||||
ALTER TABLE place_votes DROP CONSTRAINT IF EXISTS place_votes_pkey;
|
||||
ALTER TABLE place_votes ADD PRIMARY KEY (user_id, place_key);
|
||||
END IF;
|
||||
|
||||
-- 图片自己有 id 主键,只需补列 + 回填;查询走文件末尾那个索引
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'place_images' AND column_name = 'place_key') THEN
|
||||
ALTER TABLE place_images ADD COLUMN place_key TEXT NOT NULL DEFAULT '';
|
||||
END IF;
|
||||
UPDATE place_images i SET place_key = COALESCE((
|
||||
SELECT to_char(p.lat, 'FM999990.000000') || '|' || to_char(p.lon, 'FM999990.000000')
|
||||
FROM places p
|
||||
WHERE p.dataset_id = i.dataset_id AND p.src_pk = i.src_pk
|
||||
AND p.lat IS NOT NULL AND p.lon IS NOT NULL),
|
||||
'orphan:' || i.dataset_id || ':' || i.src_pk)
|
||||
WHERE i.place_key = '';
|
||||
|
||||
-- 访客票:这张表没有 dataset_id,回填时只能按**活跃数据集**认领那个编号属于哪个地点。
|
||||
-- 两个数据集里都有同一编号时也只认活跃的那个——这是唯一说得通的猜法,
|
||||
-- 认不出来的留孤儿键,票不丢。
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'place_visitor_votes' AND column_name = 'place_key') THEN
|
||||
ALTER TABLE place_visitor_votes ADD COLUMN place_key TEXT;
|
||||
END IF;
|
||||
SELECT string_agg(a.attname, ',' ORDER BY array_position(c.conkey, a.attnum))
|
||||
INTO cols FROM pg_constraint c
|
||||
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
||||
WHERE c.conname = 'place_visitor_votes_pkey';
|
||||
IF cols IS DISTINCT FROM 'place_key,voter_key' THEN
|
||||
UPDATE place_visitor_votes vv SET place_key = COALESCE((
|
||||
SELECT to_char(p.lat, 'FM999990.000000') || '|' || to_char(p.lon, 'FM999990.000000')
|
||||
FROM places p JOIN datasets d ON d.id = p.dataset_id
|
||||
WHERE d.is_active AND p.src_pk = vv.src_pk
|
||||
AND p.lat IS NOT NULL AND p.lon IS NOT NULL
|
||||
ORDER BY p.id LIMIT 1),
|
||||
'orphan:' || vv.src_pk)
|
||||
WHERE vv.place_key IS NULL;
|
||||
-- 同一个访客对同一坐标只剩一条:留最新的
|
||||
DELETE FROM place_visitor_votes a USING place_visitor_votes b
|
||||
WHERE a.voter_key = b.voter_key AND a.place_key = b.place_key
|
||||
AND (a.updated_at, a.ctid) < (b.updated_at, b.ctid);
|
||||
ALTER TABLE place_visitor_votes ALTER COLUMN place_key SET NOT NULL;
|
||||
ALTER TABLE place_visitor_votes DROP CONSTRAINT IF EXISTS place_visitor_votes_pkey;
|
||||
ALTER TABLE place_visitor_votes ADD PRIMARY KEY (place_key, voter_key);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_place_images_key ON place_images(place_key, position);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pvv_key ON place_visitor_votes(place_key);
|
||||
|
||||
Reference in New Issue
Block a user