Files
rond-with-you/internal/web/templates/admin/baseline.html
T
tamakyi 6ff6f944a1 换基线:把「底座换代」做进后台(站点数据一行不动)
导出是「底座为底 + 只覆盖托管表」,所以底座必须是手机导出的包。底座跟着数据集走,
手机重新导出后想换代,以前只能整包重新导入成新数据集(站点补录的行、主题、备注都得重收拾)。
新动作 /admin/baseline(后台首页加入口)就是给同一份数据的底座换代用的。

三道闸,顺序不能挪:
1. 重合判据:新包里的到访 UUID 与站点侧带 UUID 的到访比。一条都对不上直接拒
   (连 force 也不给过)——干跑只验结构,一份不相干的包照样导得出,但换下去站点
   每行都在新底座里认不出身份,全按新行插到最大编号之后,真机上就是「这些到访
   全变成新的」。重合率不到一半要显式确认;判据没过不当场报错了事:包已经收进
   uploads,页面给一个「我确认,强制替换」按钮,不必重传几十 MB。
2. 干跑导出:底座缺表/缺实体编号在这步挡住,不会出现「换完了才发现导不出、
   原来那份已经不知去哪」。
3. 旧底座留档 + 记档,换错了可一键回滚。

两个坑:
- 留档名不能只用秒级时间戳。「替换后紧接着回滚」落在同一秒时,回滚那次 rename 会把
  刚生成的留档覆盖掉,于是回滚回了个寂寞(TestRollbackBaselineRestoresPrevious 当场红)。
  改用 baselineBackupName():20060102-150405 + 冲突自增。
- ensureBaseline 必须先看记档。只认 datasets.sha256(= 最初那份包)的话,底座一丢就静默
  重建回旧包,上一次换基线白做且毫无提示。抽出 ensureBaselineTo(ctx, dsID, dst) 共用;
  「重建」入口写临时文件再 rename,不做破坏性删除。

回归用例 7 条(internal/web/baseline_swap_test.go):成功路径断言「换底座前后导出包里
ZVISIT 的 UUID 集合逐条一致」——编号会换、身份不该换,谁在底座里没对上立刻亮;
记档优先重建带反证(清掉记档后同一条路必须走不通);部分重合需确认、完全对不上 force
也不给过、非包文件第一步就挡住、回滚还原且当前那份也留档、页面渲染到底。

验收:./scripts/verify.sh 全量 PASS=318 FAIL=0 SKIP=0。
2026-09-30 19:00:18 +08:00

162 lines
6.6 KiB
HTML

{{define "content"}}
<div class="wrap">
<div class="page-head">
<div>
<h1>换基线</h1>
<div class="sub">
「底座」是导出 rondbackup 时铺数据的底稿:导出只覆盖我们托管的那几张实体表,
<strong>其余内容(Core Data 元数据、索引、ZRAWVISIT 原始采样点…)全部从底座原样带出来</strong>。
所以底座必须是手机导出的包;换底座<strong>不动站点里的任何数据</strong>,只换底稿。
</div>
</div>
<div class="row"><a class="btn" href="/admin">← 返回后台</a></div>
</div>
{{if not .Dataset}}
<div class="card">
<div class="empty"><div class="big">📦</div>还没有导入任何数据集,先上传一份备份再说。</div>
</div>
{{else}}
<div class="card">
<h2>当前底座</h2>
<div class="card-sub">数据集「{{trunc .Dataset.Name 40}}」(#{{.Dataset.ID}})的导出底稿</div>
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>项目</th><th>值</th></tr></thead>
<tbody>
<tr>
<td>状态</td>
<td data-label="状态">
{{if .Status.Exists}}
<span class="tag">已就绪</span>
<span class="muted">{{size .Status.Size}} · {{dtt .Status.ModTime}}</span>
{{else}}
<span class="tag">文件缺失</span>
<span class="muted">导出时会按记档自动重建;也可以点下面的「重建」。</span>
{{end}}
</td>
</tr>
<tr>
<td>来源</td>
<td data-label="来源">
{{if .Status.SHA}}
<span class="mono">{{trunc .Status.Name 40}}</span>
<span class="muted">· {{dtt .Status.At}}</span>
{{if .FromOriginal}}<span class="tag">与数据集原始包同一份</span>{{end}}
{{else}}
<span>数据集最初上传的那份包</span>
<span class="mono muted">{{trunc .OrigName 40}}</span>
{{end}}
</td>
</tr>
</tbody>
</table>
</div>
{{if .Status.Exists}}
<div class="card-sub" style="margin-top:14px">底座里有什么(换底座前后可以对着看)</div>
{{if .CountsErr}}
<div class="alert err">{{.CountsErr}}</div>
{{else}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>表</th><th>内容</th><th class="num">行数</th></tr></thead>
<tbody>
{{range .Counts}}
<tr>
<td><span class="mono">{{.Table}}</span></td>
<td data-label="内容">{{.Label}}</td>
<td class="num" data-label="行数">{{num .N}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{else}}
<div class="row" style="margin-top:12px">
<form method="post" action="/admin/baseline/rebuild">
<button class="btn" type="submit">按记档重建底座</button>
</form>
</div>
{{end}}
</div>
{{if .PendingSHA}}
<div class="card">
<div class="alert err">
刚上传的 <span class="mono">{{trunc .PendingName 40}}</span> 与当前数据集只对得上
<strong>{{num .PendingMatched}}/{{num .PendingWith}}</strong> 条带 UUID 的到访(正常应当有九成以上)。
它可能不是同一批数据的手机包——换下去会让导出包整个错位:站点每一行都在新底座里
认不出身份,于是全部按「新行」插到最大编号之后,真机上就是「这些到访全变成新的、老记录还在」。
</div>
<div class="row">
<form method="post" action="/admin/baseline/confirm"
onsubmit="return confirm('确认这份包确实是同一批数据的手机包?换错会让导出的备份在真机上错位。')">
<input type="hidden" name="sha" value="{{.PendingSHA}}">
<input type="hidden" name="name" value="{{.PendingName}}">
<button class="btn danger" type="submit">我确认,强制替换</button>
</form>
<a class="btn" href="/admin/baseline">放弃</a>
</div>
</div>
{{end}}
<div class="card">
<h2>换成另一份包当底座</h2>
<div class="card-sub">
在 rond App 里<strong>重新导出</strong>一份备份,传到这里。换完之后:
到访数、地点、备注、主题都不变;导出 rondbackup 立刻以新底座为准。
判据有两条——这份包得跟当前数据集是同一批数据(按到访 UUID 认),而且得真的能导出成功。
换之前旧底座会留档,随时可以回滚。
</div>
<form method="post" action="/admin/baseline" enctype="multipart/form-data">
<div class="row" style="align-items:center;flex-wrap:wrap">
<input type="file" name="backup" accept=".rondbackup,.zip" required>
<button class="btn primary" type="submit">上传并换底座</button>
</div>
<label class="muted" style="display:block;margin-top:10px;font-size:13px">
<input type="checkbox" name="force" value="1">
即使与当前数据集对不上也强制替换(重合率偏低时会再让你确认一次)
</label>
<div class="card-sub" style="margin-top:8px">单份上限 {{size .MaxSize}}。</div>
</form>
</div>
<div class="card">
<h2>旧底座留档</h2>
<div class="card-sub">每次替换前都会把旧底座改名留一份(最近 3 份)。换错了就回滚。</div>
{{if .Status.Backups}}
<div class="tw">
<table class="tb tb-stack">
<thead><tr><th>留档</th><th>时间</th><th class="num">大小</th></tr></thead>
<tbody>
{{range .Status.Backups}}
<tr>
<td><span class="mono">{{trunc .Name 48}}</span></td>
<td class="muted" data-label="时间">{{dt .At}}</td>
<td class="num" data-label="大小">{{size .Size}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="row" style="margin-top:12px">
<form method="post" action="/admin/baseline/rollback"
onsubmit="return confirm('回滚到最近一份留档的旧底座?当前这份也会被留档,可以再滚回来。')">
<button class="btn" type="submit">回滚到最近一份</button>
</form>
</div>
<div class="card-sub" style="margin-top:8px">
回滚后底座的「来源」记档会清空(表示它来自数据集最初那份包),导出照常可用。
</div>
{{else}}
<div class="empty"><div class="big">🗂️</div>还没有留档——这个数据集还没换过底座。</div>
{{end}}
</div>
{{end}}
</div>
{{end}}