Files
rond-with-you/internal/rond/export.go
T
2026-09-18 13:05:49 +08:00

1382 lines
45 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package rond
import (
"archive/zip"
"context"
"crypto/sha1"
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strings"
"time"
"rond-with-you/internal/geo"
_ "modernc.org/sqlite"
)
// managedTables 是「由 PostgreSQL 全量重建」的实体表。
// 其余表一律保持基线原样——包括 Z_METADATA / Z_MODELCACHE / 持久化历史
// (ACHANGE/ATRANSACTION*)、全部索引、以及我们不落库的实体
// (ZRAWVISIT 原始 GPS 采样点、ZKEYWORD、ZPOICATEGORY、ZTRIP* 等)。
var managedTables = []string{"ZVISIT", "ZMOVEMENT", "ZHOURLYWEATHER", "ZTRANSPORT", "ZLOCATION", "ZACTIVITY", "ZTAG"}
// managedCols 是「由 PostgreSQL 负责」的列。这些列之外的字段只能从原始行留档还原
// (留档缺失时再退回基线里的原值),否则会像早期版本那样把 rond 自己写的字段洗成 NULL。
var managedCols = map[string][]string{
"ZLOCATION": {"ZNAME_", "ZCATEGORY_", "ZLATITUDE", "ZLONGITUDE", "ZISOCOUNTRYCODE",
"ZADMINISTRATIVEAREA", "ZLOCALITY", "ZSUBLOCALITY", "ZTHOROUGHFARE", "ZTIMEZONE",
"ZLASTARRIVALDATE_", "ZRAWLATITUDE", "ZRAWLONGITUDE"},
"ZACTIVITY": {"ZNAME_", "ZCOLOR_", "ZICON_", "ZISHOME", "ZISWORK", "ZISEXCLUDED", "ZISARCHIVED",
"ZLASTARRIVALDATE_", "ZUID_"},
"ZTAG": {"ZNAME_", "ZCOLOR_", "ZGROUP", "ZLASTARRIVALDATE_"},
"ZTRANSPORT": {"ZNAME_", "ZCOLOR_", "ZICON_"},
"ZVISIT": {"ZLOCATION", "ZACTIVITY_", "ZARRIVALDATE_", "ZDEPARTUREDATE_",
"ZBOOKMARKED", "ZUSERADDED", "ZREMARK_", "ZEMOJINAME_", "ZWEATHERSYMBOL_", "ZIDENTIFIER_"},
"ZMOVEMENT": {"ZTRANSPORT_", "ZVISITFROM_", "ZVISITTO_", "ZSTART_", "ZEND_", "ZTYPE_"},
"ZHOURLYWEATHER": {"ZVISIT", "ZDATE_", "ZISDAYLIGHT", "ZTEMPERATURE_", "ZAPPARENTTEMPERATURE_", "ZHUMIDITY_",
"ZPRECIPITATIONAMOUNT_", "ZPRECIPITATIONCHANCE_", "ZWINDSPEED_", "ZVISIBILITY_", "ZUVINDEXVALUE_",
"ZCONDITION_", "ZSYMBOLNAME_", "ZUVINDEXCATEGORY_", "ZWINDCOMPASSDIRECTION_"},
}
// newRowDefaults 是「编辑页新建、没有原始留档」的行要补的值,照真机包里的取值分布抄。
// 置 NULL 会让 rond 把地点显示成「固定」这类特殊状态。
var newRowDefaults = map[string]map[string]any{
"ZLOCATION": {"ZBOOKMARKED": 0, "ZISPHOTOIMPORT": 0, "ZNEEDREFRESH": 0, "ZPINSTYLE_": 0,
"ZTYPE_": 0, "ZUSERADDED": 0, "ZUSERIGNORED": 0, "ZCOUNTDOWNINSECONDS": 0.0, "ZRATING": 0.0,
"ZRADIUS": 100.0},
"ZACTIVITY": {"ZISCALENDAREXCLUDED": 0, "ZISDEFAULT": 0, "ZPINSTYLE_": 0,
"ZCA_": 0.0, "ZCB_": 0.0, "ZCG_": 0.0, "ZCR_": 0.0, "ZG_": 0.0},
"ZTAG": {"ZISARCHIVED": 0, "ZISAUTOMARK": 0, "ZPINSTYLE_": 0},
"ZTRANSPORT": {"ZISDEFAULT": 0},
"ZVISIT": {"ZASSOCIATIONBITMASK_": 0, "ZISACTIVITYTAGEXCLUDED": 0, "ZISPHOTOIMPORT": 0, "ZISPLACETAGEXCLUDED": 0, "ZUSERIGNORED": 0},
"ZMOVEMENT": {"ZISMODIFIED": 0},
}
// keepNilCols 是「PG 侧算不出值就保持基线原值」的管理列——它们要么是 rond 的派生缓存列,
// 要么是原生包里有、站点这边没有对应字段的列。都只在站点确实有值可写时才动,否则原样保留:
//
// - ZLOCATION/ZACTIVITY/ZTAG 的 ZLASTARRIVALDATE_(各页面里的「最近活跃时间」):值为该
// 地点/活动/标签最晚一次到访的到达时间。新建的行不写,rond 里就永远显示「无数据」;
// 但从没被用过的那几个,app 写的是 distantPast 哨兵(真机包里 ZLOCATION 有 38 个),
// 站点复现不出来,就别用 NULL 去覆盖。
// - ZLOCATION.ZRAWLATITUDE/ZRAWLONGITUDE:真机存的是原始 GPS(WGS-84),而
// ZLATITUDE/ZLONGITUDE 是它经 GCJ-02 偏移后的显示坐标(实测 293/323 精确满足)。
// 站点新建的地点没有原始 GPS 可写时,才用显示坐标反算一个补上。
// - ZVISIT.ZIDENTIFIER_ / ZACTIVITY.ZUID_:真机给每条记录存一个 16 字节 UUID v4
// (822/824、30/30 全有),站点这边没有对应字段,新建的行需要补一个确定性 UUID。
// - ZMOVEMENT.ZTYPE_:真机 791/791 都有值,取值集中在 2(无交通方式,实测中位速度
// 5.0km/h ≈ 步行)与 5(有交通方式,中位 17km/h),另有 23 条 4、1 条 0。
// 站点复现不了 rond 自己的判定,只给新建的行补一个(见 patchSQLite),
// 已有行一律保持真机原值。
// - ZTRANSPORT 的名称/颜色/图标:站点不单独维护交通方式表,导出时从位移里聚合。
// 聚合不到(用同一方式的所有行程这几列都为空)就保留基线原值,
// 否则会把真机的模式名洗成 NULL,rond 里变成一个没有名字的方式。
var keepNilCols = map[string][]string{
"ZLOCATION": {"ZLASTARRIVALDATE_", "ZRAWLATITUDE", "ZRAWLONGITUDE"},
"ZACTIVITY": {"ZLASTARRIVALDATE_", "ZUID_"},
"ZTAG": {"ZLASTARRIVALDATE_"},
"ZVISIT": {"ZIDENTIFIER_"},
"ZMOVEMENT": {"ZTYPE_"},
"ZTRANSPORT": {"ZNAME_", "ZCOLOR_", "ZICON_"},
}
// movementTypeDefault 给站点新建的位移补 ZTYPE_:真机里这条规律覆盖 757/791 行
// (有交通方式→5、没有→2),剩下的是用户在 app 里手改过的不规则取值。
func movementTypeDefault(transportSet bool) int {
if transportSet {
return 5
}
return 2
}
// ExportRondbackup 以导入时留下的基线库为底,把 PostgreSQL 里的当前数据覆盖写回,
// 再打包成与 rond 原始格式一致的 .rondbackup(LifeEasy.sqlite / -wal / -shm)。
//
// 坐标是 GCJ-02,与 rond 的 ZLOCATION 同系,原样写回、无需任何坐标变换。
func ExportRondbackup(ctx context.Context, db *sql.DB, datasetID int64, baselinePath string, dst io.Writer) error {
if _, err := os.Stat(baselinePath); err != nil {
return fmt.Errorf("缺少基线库(%s):请重新上传一次原始 .rondbackup,以便重建基线后导出", filepath.Base(baselinePath))
}
work, err := os.MkdirTemp("", "rond-export-")
if err != nil {
return err
}
defer os.RemoveAll(work)
sqlitePath := filepath.Join(work, "LifeEasy.sqlite")
if err := copyFile(baselinePath, sqlitePath); err != nil {
return fmt.Errorf("复制基线库失败: %w", err)
}
if err := patchSQLite(ctx, db, datasetID, sqlitePath); err != nil {
return err
}
return zipBackup(sqlitePath, dst)
}
// patchSQLite 在基线库副本上覆盖写入 PostgreSQL 的当前数据。
//
// 对基线里已存在的行走 UPDATE、只动我们管理的列——rond 自己写的其余列
// (ZLOCATION.ZPINSTYLE_/ZRADIUS、ZVISIT.ZRAW/ZIDENTIFIER_、ZMOVEMENT.ZTYPE_ 等)
// 原样保留;此前用 DELETE+INSERT 全量重写会把它们统统置 NULL,rond 里新建的地点
// 会因此显示成「固定」这类特殊状态。真正新增的行才 INSERT,并补上 rond 自己
// 会写的标志位默认值。
func patchSQLite(ctx context.Context, db *sql.DB, datasetID int64, sqlitePath string) error {
sdb, err := sql.Open("sqlite", "file:"+filepath.ToSlash(sqlitePath))
if err != nil {
return fmt.Errorf("打开基线库失败: %w", err)
}
defer sdb.Close()
sdb.SetMaxOpenConns(1)
if _, err := sdb.ExecContext(ctx, "PRAGMA journal_mode=WAL"); err != nil {
return fmt.Errorf("切换 WAL 模式失败: %w", err)
}
if _, err := sdb.ExecContext(ctx, "PRAGMA foreign_keys=OFF"); err != nil {
return fmt.Errorf("关闭外键检查失败: %w", err)
}