feat: ship loose-offline dbsync (validate, agent push, LWW audit)
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>
This commit is contained in:
119
platform/internal/handler/agent_sync.go
Normal file
119
platform/internal/handler/agent_sync.go
Normal file
@@ -0,0 +1,119 @@
|
||||
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
|
||||
}
|
||||
@@ -281,6 +281,141 @@ paths:
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/admin/sync/channels:
|
||||
get:
|
||||
operationId: listSyncChannels
|
||||
summary: 列出同步通道
|
||||
parameters: [{ $ref: "#/components/parameters/Authorization" }]
|
||||
responses: { "200": { description: OK } }
|
||||
post:
|
||||
operationId: createSyncChannel
|
||||
summary: 创建同步通道(UUID PK + FK 闭包校验)
|
||||
parameters: [{ $ref: "#/components/parameters/Authorization" }]
|
||||
responses: { "200": { description: OK }, "400": { description: Bad Request } }
|
||||
/api/v1/admin/sync/channels/{id}:
|
||||
get:
|
||||
operationId: getSyncChannel
|
||||
summary: 获取同步通道
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
put:
|
||||
operationId: updateSyncChannel
|
||||
summary: 更新同步通道
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
delete:
|
||||
operationId: deleteSyncChannel
|
||||
summary: 删除同步通道
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/admin/sync/test:
|
||||
post:
|
||||
operationId: testSyncEndpoints
|
||||
summary: 测试本地/线上库连接
|
||||
parameters: [{ $ref: "#/components/parameters/Authorization" }]
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/admin/sync/channels/{id}/prepare:
|
||||
post:
|
||||
operationId: prepareSyncChannel
|
||||
summary: 准备同步(outbox/触发器)
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/admin/sync/channels/{id}/start:
|
||||
post:
|
||||
operationId: startSyncChannel
|
||||
summary: 启动同步
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/admin/sync/channels/{id}/stop:
|
||||
post:
|
||||
operationId: stopSyncChannel
|
||||
summary: 停止同步
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/admin/sync/channels/{id}/reconcile:
|
||||
post:
|
||||
operationId: reconcileSyncChannel
|
||||
summary: 同步修复(对账,有限流)
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK }, "429": { description: Too Many Requests } }
|
||||
/api/v1/admin/sync/channels/{id}/ingest:
|
||||
post:
|
||||
operationId: ingestSyncRows
|
||||
summary: 外部行写入通道 local
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/admin/sync/bindings:
|
||||
get:
|
||||
operationId: listSyncBindings
|
||||
summary: 列出本机库↔线上库绑定
|
||||
parameters: [{ $ref: "#/components/parameters/Authorization" }]
|
||||
responses: { "200": { description: OK } }
|
||||
post:
|
||||
operationId: ensureSyncBinding
|
||||
summary: 登记/更新绑定
|
||||
parameters: [{ $ref: "#/components/parameters/Authorization" }]
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/admin/sync/conflicts:
|
||||
get:
|
||||
operationId: listSyncConflicts
|
||||
summary: 已废弃(公司侧 403)
|
||||
parameters: [{ $ref: "#/components/parameters/Authorization" }]
|
||||
responses: { "403": { description: Forbidden } }
|
||||
/api/v1/agent/sync/channels/{id}/whitelist:
|
||||
get:
|
||||
operationId: agentSyncWhitelist
|
||||
summary: 本机 agent 拉取表白名单
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/agent/sync/channels/{id}/push:
|
||||
post:
|
||||
operationId: agentSyncPush
|
||||
summary: 本机 agent 推变更到线上 A
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/agent/sync/channels/{id}/push/batch:
|
||||
post:
|
||||
operationId: agentSyncPushBatch
|
||||
summary: 本机 agent 批量推送
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK } }
|
||||
/api/v1/platform/dbsync/lww-overrides:
|
||||
get:
|
||||
operationId: platformLwwOverrides
|
||||
summary: 超管 LWW 覆盖审计
|
||||
parameters: [{ $ref: "#/components/parameters/Authorization" }]
|
||||
responses: { "200": { description: OK }, "403": { description: Forbidden } }
|
||||
/api/v1/platform/dbsync/lww-overrides/{id}/rollback:
|
||||
post:
|
||||
operationId: platformLwwRollback
|
||||
summary: 超管按落败快照回滚线上单行
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/Authorization" }
|
||||
- { $ref: "#/components/parameters/Id" }
|
||||
responses: { "200": { description: OK }, "400": { description: Bad Request } }
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Authorization:
|
||||
|
||||
68
platform/internal/handler/platform_dbsync.go
Normal file
68
platform/internal/handler/platform_dbsync.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// GET /api/v1/platform/dbsync/lww-overrides — 仅平台超级管理员。
|
||||
func platformLwwOverridesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if svcCtx.DBSync == nil {
|
||||
authx.WriteError(w, http.StatusServiceUnavailable, "dbsync not enabled")
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
var tenantID int64
|
||||
if s := q.Get("tenant_id"); s != "" {
|
||||
tenantID, _ = strconv.ParseInt(s, 10, 64)
|
||||
}
|
||||
channelID := q.Get("channel_id")
|
||||
limit := 200
|
||||
if s := q.Get("limit"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil && n > 0 {
|
||||
limit = n
|
||||
if limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
list, err := svcCtx.DBSync.Store().ListLwwOverrides(tenantID, channelID, limit)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{
|
||||
"items": list,
|
||||
"hint": "LWW 自动覆盖审计;公司管理员不可见;默认 TTL 90 天;可对 applied_source 回滚单行",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/v1/platform/dbsync/lww-overrides/:id/rollback — 按落败快照回滚线上单行。
|
||||
func platformLwwRollbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if svcCtx.DBSync == nil {
|
||||
authx.WriteError(w, http.StatusServiceUnavailable, "dbsync not enabled")
|
||||
return
|
||||
}
|
||||
id := pathvar.Vars(r)["id"]
|
||||
rec, err := dbsync.RollbackLwwOverride(r.Context(), svcCtx.DBSync.Store(), id)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{
|
||||
"ok": true,
|
||||
"record": rec,
|
||||
"hint": "已按落败快照写回线上 A,并追加一条 rollback 审计",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,8 @@ func RegisterHandlers(server *rest.Server, svcCtx *svc.ServiceContext) {
|
||||
{Method: http.MethodGet, Path: "/api/v1/platform/perm-modules", Handler: chain(platformPermModulesHandler(svcCtx), rl, authMW, platformAdmin)},
|
||||
{Method: http.MethodGet, Path: "/api/v1/platform/tenants/:id/permissions", Handler: chain(platformGetTenantPermsHandler(svcCtx), rl, authMW, platformAdmin)},
|
||||
{Method: http.MethodPut, Path: "/api/v1/platform/tenants/:id/permissions", Handler: chain(platformSetTenantPermsHandler(svcCtx), rl, authMW, platformAdmin)},
|
||||
{Method: http.MethodGet, Path: "/api/v1/platform/dbsync/lww-overrides", Handler: chain(platformLwwOverridesHandler(svcCtx), rl, authMW, platformAdmin)},
|
||||
{Method: http.MethodPost, Path: "/api/v1/platform/dbsync/lww-overrides/:id/rollback", Handler: chain(platformLwwRollbackHandler(svcCtx), rl, authMW, platformAdmin)},
|
||||
})
|
||||
|
||||
// —— 鉴权:需已加入租户 ——
|
||||
@@ -131,6 +133,13 @@ func RegisterHandlers(server *rest.Server, svcCtx *svc.ServiceContext) {
|
||||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/conflicts/:id/resolve", Handler: chain(syncResolveConflictHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/reconcile", Handler: chain(syncReconcileHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/ingest", Handler: chain(syncIngestHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodGet, Path: "/api/v1/admin/sync/bindings", Handler: chain(syncBindingsListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/bindings", Handler: chain(syncBindingsEnsureHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
|
||||
// —— 本机 sync agent(形态 B):白名单拉取 + 推线上 A;需「数据同步」权限 ——
|
||||
{Method: http.MethodGet, Path: "/api/v1/agent/sync/channels/:id/whitelist", Handler: chain(agentSyncWhitelistHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/push", Handler: chain(agentSyncPushHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/push/batch", Handler: chain(agentSyncPushBatchHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
|
||||
// 存储:POST 创建对象,GET 读取(无 /upload 动词路径;旧路径保留别名防断裂)
|
||||
{Method: http.MethodPost, Path: "/api/v1/storage", Handler: chain(uploadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm上传文件))},
|
||||
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/dbsync"
|
||||
@@ -20,6 +21,14 @@ func requireDBSync(svcCtx *svc.ServiceContext, w http.ResponseWriter) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func reconcileMinInterval(svcCtx *svc.ServiceContext) time.Duration {
|
||||
sec := 300
|
||||
if svcCtx != nil && svcCtx.Config.DBSync.ReconcileMinSec > 0 {
|
||||
sec = svcCtx.Config.DBSync.ReconcileMinSec
|
||||
}
|
||||
return time.Duration(sec) * time.Second
|
||||
}
|
||||
|
||||
// sync 仅公司顶级权限(管理员 /「数据同步」);智能体与编辑不可配。
|
||||
func syncTenantID(r *http.Request) int64 {
|
||||
return authx.TenantID(r.Context())
|
||||
@@ -74,6 +83,15 @@ func syncSaveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
// 强制归属当前公司,禁止客户端伪造 tenant_id
|
||||
ch.TenantID = syncTenantID(r)
|
||||
if err := dbsync.ValidateChannelConfig(&ch); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
// 对可达端做 UUID 主键 + 外键闭包校验(remote 通常为线上库)
|
||||
if err := dbsync.ValidateChannelAgainstDB(r.Context(), &ch); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
saved, err := svcCtx.DBSync.Store().SaveChannel(ch)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
@@ -189,37 +207,14 @@ func syncStopHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
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})
|
||||
// M3:LWW/冲突追溯仅平台超级管理员;公司 top 403
|
||||
authx.WriteError(w, http.StatusForbidden, "冲突/LWW 覆盖日志仅平台超级管理员可查")
|
||||
}
|
||||
}
|
||||
|
||||
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})
|
||||
authx.WriteError(w, http.StatusForbidden, "冲突/LWW 覆盖日志仅平台超级管理员可操作")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,11 +228,19 @@ func syncReconcileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
if ok, wait := dbsync.CanReconcile(ch, reconcileMinInterval(svcCtx)); !ok {
|
||||
authx.WriteError(w, http.StatusTooManyRequests, dbsync.ReconcileTooSoonError(wait).Error())
|
||||
return
|
||||
}
|
||||
res, err := dbsync.ReconcileChannel(r.Context(), ch)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
_ = svcCtx.DBSync.Store().PatchStats(ch.ID, func(c *dbsync.Channel) {
|
||||
c.LastReconcileAt = &now
|
||||
})
|
||||
httpx.OkJson(w, res)
|
||||
}
|
||||
}
|
||||
|
||||
50
platform/internal/handler/sync_binding.go
Normal file
50
platform/internal/handler/sync_binding.go
Normal file
@@ -0,0 +1,50 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func syncBindingsListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !requireDBSync(svcCtx, w) {
|
||||
return
|
||||
}
|
||||
localID := r.URL.Query().Get("local_database_id")
|
||||
list, err := svcCtx.DBSync.Store().ListBindings(syncTenantID(r), localID)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"items": list})
|
||||
}
|
||||
}
|
||||
|
||||
func syncBindingsEnsureHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !requireDBSync(svcCtx, w) {
|
||||
return
|
||||
}
|
||||
var body dbsync.Binding
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
body.TenantID = syncTenantID(r)
|
||||
if body.UserID == 0 {
|
||||
body.UserID = authx.UserID(r.Context())
|
||||
}
|
||||
saved, err := svcCtx.DBSync.Store().EnsureBinding(body)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, saved)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user