chore: initial commit of ai site platform

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-07-31 10:31:17 +08:00
commit 4ca82fb58a
203 changed files with 45745 additions and 0 deletions

View 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将同步到线上"})
}
}