1382 lines
45 KiB
Go
1382 lines
45 KiB
Go
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)
|
||
}
|
||