feat: add sync checkpoint restore for last successful sync

Capture rolling online DB snapshots after push/drain/reconcile and expose SyncPage 数据恢复 plus checkpoint/restore APIs; mark Z12h and restore done in coop docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-06 11:49:00 +08:00
parent 75622a728e
commit 680d2b8cde
11 changed files with 860 additions and 16 deletions

View File

@@ -262,6 +262,7 @@ func syncReconcileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
_ = svcCtx.DBSync.Store().PatchStats(ch.ID, func(c *dbsync.Channel) {
c.LastReconcileAt = &now
})
dbsync.ScheduleCheckpoint(svcCtx.DBSync.Store(), ch, "reconcile")
httpx.OkJson(w, res)
}
}
@@ -418,3 +419,62 @@ func syncDropTableHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
httpx.OkJson(w, res)
}
}
func syncCheckpointMetaHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !requireDBSync(svcCtx, w) {
return
}
id := pathvar.Vars(r)["id"]
if _, err := svcCtx.DBSync.Store().GetChannelForTenant(id, syncTenantID(r)); err != nil {
authx.WriteError(w, http.StatusNotFound, err.Error())
return
}
meta, err := dbsync.LoadCheckpointMeta(svcCtx.DBSync.Store(), id)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, meta)
}
}
func syncRestoreHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !requireDBSync(svcCtx, w) {
return
}
ch, err := svcCtx.DBSync.Store().GetChannelForTenant(pathvar.Vars(r)["id"], syncTenantID(r))
if err != nil {
authx.WriteError(w, http.StatusNotFound, err.Error())
return
}
var body struct {
Which string `json:"which"`
Confirm bool `json:"confirm"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
if !body.Confirm {
authx.WriteError(w, http.StatusBadRequest, "请确认恢复confirm=true")
return
}
which := strings.TrimSpace(body.Which)
if which == "" {
which = "latest"
}
res, err := dbsync.RestoreCheckpoint(r.Context(), svcCtx.DBSync.Store(), ch, which)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
if svcCtx.Audit != nil {
_ = svcCtx.Audit.Log(r.Context(), syncTenantID(r), authx.UserID(r.Context()), "dbsync.restore", audit.DetailJSON(map[string]any{
"channel_id": ch.ID, "which": which, "synced_at": res.SyncedAt, "upserted": res.Upserted, "deleted": res.Deleted,
}))
}
httpx.OkJson(w, res)
}
}