package handler import ( "encoding/json" "net/http" "aijianzhan/platform/internal/authx" "aijianzhan/platform/internal/dbsync" "aijianzhan/platform/internal/svc" "github.com/zeromicro/go-zero/rest/httpx" "github.com/zeromicro/go-zero/rest/pathvar" ) func requireDBSync(svcCtx *svc.ServiceContext, w http.ResponseWriter) bool { if svcCtx.DBSync == nil { authx.WriteError(w, http.StatusServiceUnavailable, "dbsync not enabled") return false } return true } // sync 仅公司顶级权限(管理员 /「数据同步」);智能体与编辑不可配。 func syncTenantID(r *http.Request) int64 { return authx.TenantID(r.Context()) } func syncListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !requireDBSync(svcCtx, w) { return } list, err := svcCtx.DBSync.Store().ListChannelsByTenant(syncTenantID(r)) if err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.OkJson(w, map[string]any{"items": list}) } } func syncGetHandler(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 } httpx.OkJson(w, ch) } } func syncSaveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !requireDBSync(svcCtx, w) { return } var ch dbsync.Channel if err := json.NewDecoder(r.Body).Decode(&ch); err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } if id := pathvar.Vars(r)["id"]; id != "" { ch.ID = id existing, err := svcCtx.DBSync.Store().GetChannelForTenant(id, syncTenantID(r)) if err != nil { authx.WriteError(w, http.StatusNotFound, err.Error()) return } _ = existing } // 强制归属当前公司,禁止客户端伪造 tenant_id ch.TenantID = syncTenantID(r) saved, err := svcCtx.DBSync.Store().SaveChannel(ch) if err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.OkJson(w, saved) } } func syncDeleteHandler(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 } svcCtx.DBSync.StopChannel(id) if err := svcCtx.DBSync.Store().DeleteChannel(id); err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.OkJson(w, map[string]any{"ok": true}) } } func syncTestHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !requireDBSync(svcCtx, w) { return } var body struct { Local *dbsync.Endpoint `json:"local"` Remote *dbsync.Endpoint `json:"remote"` Side string `json:"side"` // local|remote|both } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } out := map[string]any{} side := body.Side if side == "" { side = "both" } if (side == "local" || side == "both") && body.Local != nil { out["local"] = dbsync.TestEndpoint(r.Context(), *body.Local) } if (side == "remote" || side == "both") && body.Remote != nil { out["remote"] = dbsync.TestEndpoint(r.Context(), *body.Remote) } httpx.OkJson(w, out) } } func syncPrepareHandler(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 } if err := dbsync.PrepareChannel(r.Context(), ch); err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.OkJson(w, map[string]any{"ok": true, "message": "outbox + triggers ready"}) } } func syncStartHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !requireDBSync(svcCtx, w) { return } id := pathvar.Vars(r)["id"] ch, err := svcCtx.DBSync.Store().GetChannelForTenant(id, syncTenantID(r)) if err != nil { authx.WriteError(w, http.StatusNotFound, err.Error()) return } if err := dbsync.PrepareChannel(r.Context(), ch); err != nil { authx.WriteError(w, http.StatusBadRequest, "prepare: "+err.Error()) return } if err := svcCtx.DBSync.StartChannel(id); err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.OkJson(w, map[string]any{"ok": true, "running": true}) } } func syncStopHandler(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 } svcCtx.DBSync.StopChannel(id) httpx.OkJson(w, map[string]any{"ok": true, "running": false}) } } func syncConflictsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !requireDBSync(svcCtx, w) { return } only := r.URL.Query().Get("unresolved") != "0" list, err := svcCtx.DBSync.Store().ListConflictsByTenant(syncTenantID(r), only) if err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.OkJson(w, map[string]any{"items": list}) } } func syncResolveConflictHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !requireDBSync(svcCtx, w) { return } var body struct { Resolution string `json:"resolution"` // apply_source | keep_target | discard } _ = json.NewDecoder(r.Body).Decode(&body) if body.Resolution == "" { body.Resolution = "discard" } id := pathvar.Vars(r)["id"] if err := svcCtx.DBSync.Store().ResolveConflictForTenant(id, syncTenantID(r), body.Resolution); err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.OkJson(w, map[string]any{"ok": true, "resolution": body.Resolution}) } } func syncReconcileHandler(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 } res, err := dbsync.ReconcileChannel(r.Context(), ch) if err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.OkJson(w, res) } } func syncIngestHandler(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 { Table string `json:"table"` Source string `json:"source"` // 如 c / excel / api Rows []map[string]any `json:"rows"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } n, err := dbsync.IngestRows(r.Context(), ch, body.Table, body.Rows, body.Source) if err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.OkJson(w, map[string]any{"ok": true, "ingested": n, "hint": "已写入本地并进入 outbox,将同步到线上"}) } }