chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
271
platform/internal/handler/sync.go
Normal file
271
platform/internal/handler/sync.go
Normal file
@@ -0,0 +1,271 @@
|
||||
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,将同步到线上"})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user