Files
rond-with-you/internal/web/fog_diff.go
T
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

95 lines
2.6 KiB
Go

package web
import (
"bytes"
"image/png"
"net/http"
"strconv"
"sync"
"time"
"rond-with-you/internal/fog"
)
// 差异瓦片要反复读同一份快照、同一份库内迷雾,单槽缓存住两边的块集即可——
// 站长一次只会看一份预览。缓存里的 map 加载完就只读,渲染时并发遍历是安全的。
var (
diffSetsMu sync.Mutex
diffSetsToken string
diffSetsAt time.Time
diffSetsCur map[[2]int][]byte
diffSetsInc map[[2]int][]byte
)
// diffSetsTTL 短于暂存的 2 小时:库内迷雾随时可能被别的操作改掉,
// 缓存太久会让差异图与页面上那组数字对不上。
const diffSetsTTL = 10 * time.Minute
func (s *Server) fogDiffSets(token string) (cur, inc map[[2]int][]byte, err error) {
diffSetsMu.Lock()
defer diffSetsMu.Unlock()
if token == diffSetsToken && time.Since(diffSetsAt) < diffSetsTTL {
return diffSetsCur, diffSetsInc, nil
}
data, err := s.loadStashedFog(token)
if err != nil {
return nil, nil, err
}
blocks, _, err := fog.Decode(data)
if err != nil {
return nil, nil, err
}
ix, err := s.fogIndex()
if err != nil {
return nil, nil, err
}
cur = map[[2]int][]byte{}
ix.View(func(m map[[2]int][]byte) {
for k, bm := range m {
cur[k] = bm
}
})
inc = make(map[[2]int][]byte, len(blocks))
for _, b := range blocks {
inc[[2]int{b.GX, b.GY}] = b.Bitmap
}
diffSetsToken, diffSetsCur, diffSetsInc, diffSetsAt = token, cur, inc, time.Now()
return cur, inc, nil
}
// adminFogDiffTile 渲染一张差异瓦片:绿=叠加会新增、灰=两边都有、红=整体替换会丢。
// 只给站长看,不涉及遮罩。
func (s *Server) adminFogDiffTile(w http.ResponseWriter, r *http.Request) {
z, err1 := strconv.Atoi(r.PathValue("z"))
x, err2 := strconv.Atoi(r.PathValue("x"))
y, err3 := strconv.Atoi(r.PathValue("y"))
if err1 != nil || err2 != nil || err3 != nil || z < 0 || z > 22 ||
x < 0 || y < 0 || x >= 1<<uint(z) || y >= 1<<uint(z) {
http.NotFound(w, r)
return
}
cur, inc, err := s.fogDiffSets(r.PathValue("token"))
if err != nil {
http.NotFound(w, r)
return
}
crs := r.URL.Query().Get("crs")
if crs != "gcj02" && crs != "wgs84" {
crs = s.cfg.MapCRS
}
img := fog.RenderDiffTile(cur, inc, z, x, y, crs)
if img == nil {
http.NotFound(w, r)
return
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
s.serverError(w, r, err)
return
}
w.Header().Set("Content-Type", "image/png")
// 令牌是一次性的,内容也只给站长看,不落共享缓存
w.Header().Set("Cache-Control", "private, max-age=300")
w.Write(buf.Bytes())
}