Add UUID/FK channel checks, agent whitelist/push APIs, bindings, super-admin LWW audit with rollback, reconcile rate limits, and sync docs. Default customers stay opt-in; company conflict UI is removed. Co-authored-by: Cursor <cursoragent@cursor.com>
120 lines
3.3 KiB
Go
120 lines
3.3 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"
|
||
)
|
||
|
||
// agent 同步只读白名单 + 推远程 A(形态 B);需 JWT 含「数据同步」权限(人类管理员或智能体均可)。
|
||
|
||
func agentSyncWhitelistHandler(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
|
||
}
|
||
tables := uniqueStringSlice(ch.Local.Tables, ch.Remote.Tables)
|
||
httpx.OkJson(w, map[string]any{
|
||
"channel_id": ch.ID,
|
||
"name": ch.Name,
|
||
"enabled": ch.Enabled,
|
||
"direction": ch.Direction,
|
||
"conflict_policy": ch.ConflictPolicy,
|
||
"tables": tables,
|
||
"pk_columns": ch.PKColumns,
|
||
"hint": "本机 agent 缓存此表白名单;仅白名单表走 local_dbsync",
|
||
})
|
||
}
|
||
}
|
||
|
||
func agentSyncPushHandler(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 item dbsync.PushItem
|
||
if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
res, err := dbsync.PushToRemote(r.Context(), ch, svcCtx.DBSync.Store(), item)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"success": true, "result": res})
|
||
}
|
||
}
|
||
|
||
func agentSyncPushBatchHandler(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 {
|
||
Items []dbsync.PushItem `json:"items"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
if len(body.Items) == 0 {
|
||
authx.WriteError(w, http.StatusBadRequest, "items required")
|
||
return
|
||
}
|
||
if len(body.Items) > 100 {
|
||
authx.WriteError(w, http.StatusBadRequest, "items limit 100")
|
||
return
|
||
}
|
||
results, err := dbsync.PushBatchToRemote(r.Context(), ch, svcCtx.DBSync.Store(), body.Items)
|
||
if err != nil {
|
||
httpx.OkJson(w, map[string]any{
|
||
"success": false,
|
||
"error": err.Error(),
|
||
"results": results,
|
||
})
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"success": true, "results": results})
|
||
}
|
||
}
|
||
|
||
func uniqueStringSlice(a, b []string) []string {
|
||
seen := map[string]struct{}{}
|
||
var out []string
|
||
for _, xs := range [][]string{a, b} {
|
||
for _, s := range xs {
|
||
if s == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[s]; ok {
|
||
continue
|
||
}
|
||
seen[s] = struct{}{}
|
||
out = append(out, s)
|
||
}
|
||
}
|
||
return out
|
||
}
|