Files
rond-with-you/internal/web/templates/admin/backup-preview.html
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

311 lines
16 KiB
HTML
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.
{{define "content"}}
<div class="wrap">
<div class="page-head">
<div>
<h1>备份差异预览</h1>
<div class="sub">
这份备份与当前展示的「{{.DatasetName}}」逐条比对的结果。<strong>还没有导入</strong>,站内数据一个字节都没动。
</div>
</div>
<div class="row"><a class="btn" href="/admin">← 返回后台</a></div>
</div>
{{if .Identical}}
<div class="card">
<div class="alert ok">这份备份与当前数据集<strong>完全一致</strong>:没有新增、没有被删、也没有冲突。</div>
</div>
{{end}}
{{if .Truncated}}
<div class="card"><div class="alert err">差异较多,下面每类只列出前 200 条,确切数量看概览表。</div></div>
{{end}}
<div class="card">
<h2>概览</h2>
<div class="tw">
<table class="tb tb-stack">
<thead>
<tr>
<th>实体</th><th class="num">备份</th><th class="num">站内</th>
<th class="num">备份新增</th><th class="num">仅站内有</th><th class="num">冲突</th><th class="num">一致</th>
</tr>
</thead>
<tbody>
<tr>
<td>到访</td>
<td class="num" data-label="备份">{{num .BackupVisits}}</td>
<td class="num" data-label="站内">{{num .StoredVisits}}</td>
<td class="num" data-label="备份新增">{{num .VisitCounts.Added}}</td>
<td class="num" data-label="仅站内有">{{num .VisitCounts.Removed}}</td>
<td class="num" data-label="冲突">{{num .VisitCounts.Changed}}</td>
<td class="num" data-label="一致">{{num .VisitCounts.Same}}</td>
</tr>
<tr>
<td>地点</td>
<td class="num" data-label="备份">{{num .BackupPlaces}}</td>
<td class="num" data-label="站内">{{num .StoredPlaces}}</td>
<td class="num" data-label="备份新增">{{num .PlaceCounts.Added}}</td>
<td class="num" data-label="仅站内有">{{num .PlaceCounts.Removed}}</td>
<td class="num" data-label="冲突">{{num .PlaceCounts.Changed}}</td>
<td class="num" data-label="一致">{{num .PlaceCounts.Same}}</td>
</tr>
<tr>
<td>活动类型</td>
<td class="num" data-label="备份">{{num .BackupActs}}</td>
<td class="num" data-label="站内">{{num .StoredActs}}</td>
<td class="num" data-label="备份新增">{{num .ActCounts.Added}}</td>
<td class="num" data-label="仅站内有">{{num .ActCounts.Removed}}</td>
<td class="num" data-label="冲突">{{num .ActCounts.Changed}}</td>
<td class="num" data-label="一致">{{num .ActCounts.Same}}</td>
</tr>
</tbody>
</table>
</div>
<div class="card-sub" style="margin-top:10px">
对齐按每条记录的 <strong>UUID</strong> 做:rond 每次导出都会重排记录编号(同样的 822 条到访,两次导出的编号 100% 不同,UUID 却逐条一模一样),
所以只有 UUID 认得出来是同一条;地点没有 UUID,退到「名称 + 坐标」。
站内补录的记录不会出现在真机备份里,一律归到「仅站内有」。
</div>
</div>
<div class="card">
<h2>到访</h2>
<div class="card-sub"><strong>备份里多出来的({{num .VisitCounts.Added}})</strong> —— 目前站内没有这些记录</div>
{{if .VisitAdded}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>到达</th><th>离开</th><th>地点</th><th>活动</th><th>备注</th></tr></thead>
<tbody>
{{range .VisitAdded}}
<tr>
<td class="mono">{{dtt .Arrival}}</td>
<td class="mono" data-label="离开">{{dtt .Departure}}</td>
<td data-label="地点">{{if .Place}}{{.Place}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="活动">{{if .Activity}}{{.Activity}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="备注">{{if .Remark}}{{.Remark}}{{else}}<span class="muted">—</span>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted" style="margin-bottom:14px">没有</div>{{end}}
<div class="card-sub" style="margin-top:16px">
<strong>只在站内有({{num .VisitCounts.Removed}})</strong> —— 备份里找不到;可能是你在这里补录的,也可能是实机上删掉的。
<strong>默认一条都不删</strong>;确实想清掉的,勾上左列的「合并时删除」。
</div>
{{if .VisitGone}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>到达</th><th>离开</th><th>地点</th><th>活动</th><th>备注</th><th>来源</th><th>合并时删除</th></tr></thead>
<tbody>
{{range .VisitGone}}
<tr>
<td class="mono">{{dtt .Arrival}}</td>
<td class="mono" data-label="离开">{{dtt .Departure}}</td>
<td data-label="地点">{{if .Place}}{{.Place}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="活动">{{if .Activity}}{{.Activity}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="备注">{{if .Remark}}{{.Remark}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="来源">{{if .UserAdded}}<span>站内补录</span>{{else}}<span class="muted">备份里已无</span>{{end}}</td>
<td data-label="合并时删除">
<input type="checkbox" form="merge-form" name="dv_{{.SrcPK}}" value="1" title="勾上=合并时删掉这条到访">
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted" style="margin-bottom:14px">没有</div>{{end}}
<div class="card-sub" style="margin-top:16px"><strong>两边都有但不一样({{num .VisitCounts.Changed}})</strong> —— 左边是站内当前的值,右边是备份里的值</div>
{{if .VisitClash}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>到达</th><th>地点</th><th>活动</th><th>差异</th><th>合并时</th></tr></thead>
<tbody>
{{range .VisitClash}}
<tr>
<td class="mono">{{dtt .Arrival}}</td>
<td data-label="地点">{{if .Place}}{{.Place}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="活动">{{if .Activity}}{{.Activity}}{{else}}<span class="muted">—</span>{{end}}</td>
<td data-label="差异">
{{range .Fields}}
<div style="font-size:12.5px;line-height:1.7">
<span class="muted">{{.Label}}</span>
<span style="text-decoration:line-through;opacity:.65">{{if .From}}{{.From}}{{else}}(空){{end}}</span>
<span style="opacity:.5">→</span>
<strong>{{if .To}}{{.To}}{{else}}(空){{end}}</strong>
</div>
{{end}}
</td>
<td data-label="合并时" style="white-space:nowrap">
<label style="margin-right:8px"><input type="radio" form="merge-form" name="vc_{{.Key}}" value="backup" checked> 用实机</label>
<label style="margin-right:8px"><input type="radio" form="merge-form" name="vc_{{.Key}}" value="db"> 用站内</label>
<label><input type="radio" form="merge-form" name="vc_{{.Key}}" value="skip"> 跳过</label>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted">没有</div>{{end}}
</div>
<div class="card">
<h2>地点</h2>
<div class="card-sub"><strong>备份里多出来的({{num .PlaceCounts.Added}})</strong></div>
{{if .PlaceAdded}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>名称</th><th>省 / 市</th><th>分类</th><th class="num">坐标</th></tr></thead>
<tbody>
{{range .PlaceAdded}}
<tr>
<td>{{if .Name}}{{.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td data-label="省 / 市">{{.Province}} {{.City}}</td>
<td data-label="分类">{{if .POICategory}}{{.POICategory}}{{else}}<span class="muted">—</span>{{end}}</td>
<td class="num mono" data-label="坐标">{{printf "%.5f" .Lat}}, {{printf "%.5f" .Lon}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted" style="margin-bottom:14px">没有</div>{{end}}
<div class="card-sub" style="margin-top:16px">
<strong>只在站内有({{num .PlaceCounts.Removed}})</strong>
—— 名下还有到访的地点删不掉(会连带删掉那些到访),会直接报错让你先处理。
</div>
{{if .PlaceGone}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>名称</th><th>省 / 市</th><th class="num">到访</th><th class="num">坐标</th><th>合并时删除</th></tr></thead>
<tbody>
{{range .PlaceGone}}
<tr>
<td>{{if .Name}}{{.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td data-label="省 / 市">{{.Province}} {{.City}}</td>
<td class="num" data-label="到访">{{num .VisitCount}}</td>
<td class="num mono" data-label="坐标">{{printf "%.5f" .Lat}}, {{printf "%.5f" .Lon}}</td>
<td data-label="合并时删除">
{{if gt .VisitCount 0}}
<span class="muted" style="font-size:12.5px">还有到访,不能删</span>
{{else}}
<input type="checkbox" form="merge-form" name="dp_{{.SrcPK}}" value="1">
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted" style="margin-bottom:14px">没有</div>{{end}}
<div class="card-sub" style="margin-top:16px"><strong>两边都有但地址不一样({{num .PlaceCounts.Changed}})</strong></div>
{{if .PlaceClash}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>名称</th><th>差异</th><th>合并时</th></tr></thead>
<tbody>
{{range .PlaceClash}}
<tr>
<td>{{if .DB.Name}}{{.DB.Name}}{{else}}<span class="muted">(未命名)</span>{{end}}</td>
<td data-label="差异">
{{range .Fields}}
<div style="font-size:12.5px;line-height:1.7">
<span class="muted">{{.Label}}</span>
<span style="text-decoration:line-through;opacity:.65">{{if .From}}{{.From}}{{else}}(空){{end}}</span>
<span style="opacity:.5">→</span>
<strong>{{if .To}}{{.To}}{{else}}(空){{end}}</strong>
</div>
{{end}}
</td>
<td data-label="合并时" style="white-space:nowrap">
<label style="margin-right:8px"><input type="radio" form="merge-form" name="pc_{{.DB.Key}}" value="backup" checked> 用实机</label>
<label style="margin-right:8px"><input type="radio" form="merge-form" name="pc_{{.DB.Key}}" value="db"> 用站内</label>
<label><input type="radio" form="merge-form" name="pc_{{.DB.Key}}" value="skip"> 跳过</label>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<div class="muted">没有</div>{{end}}
</div>
<div class="card">
<h2>活动类型</h2>
<div class="card-sub">
新增 {{num .ActCounts.Added}} · 仅站内有 {{num .ActCounts.Removed}} · 冲突 {{num .ActCounts.Changed}} · 一致 {{num .ActCounts.Same}}
</div>
<div class="row" style="gap:24px;align-items:flex-start;flex-wrap:wrap">
<div style="flex:1 1 220px">
<div class="muted" style="font-size:12.5px;margin-bottom:4px">备份新增</div>
{{range .ActAdded}}<div>{{.Name}}</div>{{else}}<div class="muted">没有</div>{{end}}
</div>
<div style="flex:1 1 220px">
<div class="muted" style="font-size:12.5px;margin-bottom:4px">仅站内有(勾上=合并时删除)</div>
{{range .ActGone}}
<label style="display:block"><input type="checkbox" form="merge-form" name="da_{{.SrcPK}}" value="1"> {{.Name}}</label>
{{else}}<div class="muted">没有</div>{{end}}
</div>
<div style="flex:1 1 260px">
<div class="muted" style="font-size:12.5px;margin-bottom:4px">名称不同</div>
{{range .ActClash}}
<div style="margin-bottom:6px">
{{range .Fields}}<div><span style="text-decoration:line-through;opacity:.65">{{.From}}</span> → <strong>{{.To}}</strong></div>{{end}}
<div style="margin-top:4px">
<label style="margin-right:8px"><input type="radio" form="merge-form" name="ac_{{actKey .DB}}" value="backup" checked> 用实机</label>
<label style="margin-right:8px"><input type="radio" form="merge-form" name="ac_{{actKey .DB}}" value="db"> 用站内</label>
<label><input type="radio" form="merge-form" name="ac_{{actKey .DB}}" value="skip"> 跳过</label>
</div>
</div>
{{else}}<div class="muted">没有</div>{{end}}
</div>
</div>
</div>
<div class="card">
<h2>其它实体(只列数量)</h2>
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>实体</th><th class="num">备份</th><th class="num">站内</th></tr></thead>
<tbody>
<tr><td>位移</td><td class="num" data-label="备份">{{num .BackupMovements}}</td><td class="num" data-label="站内">{{num .StoredMovements}}</td></tr>
<tr><td>小时天气</td><td class="num" data-label="备份">{{num .BackupWeather}}</td><td class="num" data-label="站内">{{num .StoredWeather}}</td></tr>
<tr><td>原始定位点</td><td class="num" data-label="备份">{{num .BackupRaw}}</td><td class="num" data-label="站内"><span class="muted">不单独落库</span></td></tr>
</tbody>
</table>
</div>
<div class="card-sub" style="margin-top:10px">
位移跟着它的起止到访走、天气跟着到访走,所以这两项按到访对齐结果一并处理,不单独做逐条比对。
</div>
</div>
<div class="card">
<h2>执行合并</h2>
<div class="card-sub">
把选中的改动写进当前数据集「{{.DatasetName}}」。整个过程跑在一个事务里,任何一步失败都会<strong>整体回滚</strong>,数据不会半改。
<br>「仅站内有」的记录<strong>默认一条都不删</strong>;确实要清掉的,在上面各表里逐条勾「合并时删除」。
名下有到访的地点会被拒绝删除(否则会连带删掉那些到访),先处理到访再来。
</div>
<form id="merge-form" method="post" action="/admin/preview/{{.Token}}/merge"></form>
<label style="display:flex;align-items:flex-start;gap:8px;margin:4px 0 12px">
<input type="checkbox" form="merge-form" name="add_new" value="1" checked style="margin-top:3px">
<span>把备份里多出来的
<strong>{{num .VisitCounts.Added}}</strong> 条到访、
<strong>{{num .PlaceCounts.Added}}</strong> 个地点、
<strong>{{num .ActCounts.Added}}</strong> 个活动并进来
<span class="muted">(新到访会连它的地点、活动、标签、位移与天气一起带过来)</span>
</span>
</label>
<button class="btn primary" form="merge-form" type="submit"
onclick="return confirm('确认把选中的改动写进当前数据集?出错会自动整体回滚。')">执行合并</button>
<div class="card-sub" style="margin:10px 0 0">
合并后再传一次同一份备份,差异应当变成「完全一致」——那说明对齐键(UUID 与坐标)都写进去了。
</div>
</div>
</div>
{{end}}