272 lines
8.0 KiB
Go
272 lines
8.0 KiB
Go
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,将同步到线上"})
|
||
}
|
||
}
|