备份(.rondbackup)与迷雾(.fwss)各加一层「先看差异再动手」: - 迷雾:上传先出差异页,说清叠加能补多少、整体替换会丢多少,并在真实底图上 用绿/灰/红标出会新增 / 不变 / 会丢的区域 - 备份:与当前数据集按 UUID 逐条比对(rond 每次导出都会重排 Z_PK,编号认不出人), 新增合入、冲突逐条选、仅站内有逐条勾选删除,全在一个事务里执行; 合入的行写回 entity_raw,保证再传同一份备份时差异归零 - 地点按坐标对齐:站点的「补全地址」会把地点改名,拿名称当键会让一整个地点的 到访全变成假冲突 - 附属数据(备注/结论/访客票/图片)从 dataset_id+src_pk 换成坐标键,换数据集不再失联; 访客票没有 dataset_id,回填按活跃数据集认领 - 修正 contentHash 拿 Z_PK 当指纹的隐患,以及两处「src_pk 跨快照稳定」的错误注释 导出格式(.rondbackup / .fwss)保持原样,往返测试通过。
95 lines
2.6 KiB
Go
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())
|
|
}
|