chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
196
platform/internal/handler/license.go
Normal file
196
platform/internal/handler/license.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/license"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func requireLicenseSecret(svcCtx *svc.ServiceContext, r *http.Request) error {
|
||||
want := strings.TrimSpace(svcCtx.Config.License.ControlSecret)
|
||||
if want == "" {
|
||||
want = strings.TrimSpace(svcCtx.Config.Auth.IssueSecret)
|
||||
}
|
||||
if want == "" {
|
||||
return errMsg("未配置 License.ControlSecret")
|
||||
}
|
||||
got := strings.TrimSpace(r.Header.Get("X-License-Secret"))
|
||||
if got == "" {
|
||||
got = strings.TrimSpace(r.URL.Query().Get("secret"))
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(got), []byte(want)) != 1 {
|
||||
return errMsg("invalid license control secret")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type errMsg string
|
||||
|
||||
func (e errMsg) Error() string { return string(e) }
|
||||
|
||||
func licenseStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if svcCtx.License == nil || !svcCtx.License.Enabled() {
|
||||
httpx.OkJson(w, map[string]any{"enabled": false, "message": "本实例未启用授权租约"})
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, svcCtx.License.Status(time.Now()))
|
||||
}
|
||||
}
|
||||
|
||||
func licenseRenewHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := requireLicenseSecret(svcCtx, r); err != nil {
|
||||
authx.WriteError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
if svcCtx.License == nil || !svcCtx.License.Enabled() {
|
||||
authx.WriteError(w, http.StatusBadRequest, "license 未启用")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
NotAfter string `json:"not_after"`
|
||||
Note string `json:"note"`
|
||||
By string `json:"by"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
na, err := license.ParseNotAfter(body.NotAfter)
|
||||
if err != nil || na.IsZero() {
|
||||
authx.WriteError(w, http.StatusBadRequest, "not_after 无效")
|
||||
return
|
||||
}
|
||||
by := strings.TrimSpace(body.By)
|
||||
if by == "" {
|
||||
by = "remote"
|
||||
}
|
||||
lease, err := svcCtx.License.Renew(na, by, body.Note)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"ok": true, "lease": lease, "status": svcCtx.License.Status(time.Now())})
|
||||
}
|
||||
}
|
||||
|
||||
func licenseExtendHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := requireLicenseSecret(svcCtx, r); err != nil {
|
||||
authx.WriteError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
if svcCtx.License == nil || !svcCtx.License.Enabled() {
|
||||
authx.WriteError(w, http.StatusBadRequest, "license 未启用")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Days int `json:"days"`
|
||||
Note string `json:"note"`
|
||||
By string `json:"by"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if body.Days <= 0 {
|
||||
body.Days = license.DefaultExtensionMaxDays
|
||||
}
|
||||
by := strings.TrimSpace(body.By)
|
||||
if by == "" {
|
||||
by = "remote"
|
||||
}
|
||||
lease, err := svcCtx.License.Extend(body.Days, by, body.Note)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"ok": true, "lease": lease, "status": svcCtx.License.Status(time.Now())})
|
||||
}
|
||||
}
|
||||
|
||||
func licensePutLeaseHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := requireLicenseSecret(svcCtx, r); err != nil {
|
||||
authx.WriteError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
if svcCtx.License == nil || !svcCtx.License.Enabled() {
|
||||
authx.WriteError(w, http.StatusBadRequest, "license 未启用")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Customer string `json:"customer"`
|
||||
NotAfter string `json:"not_after"`
|
||||
ExtensionsUsed int `json:"extensions_used"`
|
||||
ExtensionsMax int `json:"extensions_max"`
|
||||
ExtensionMaxDays int `json:"extension_max_days"`
|
||||
Note string `json:"note"`
|
||||
By string `json:"by"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
na, err := license.ParseNotAfter(body.NotAfter)
|
||||
if err != nil || na.IsZero() {
|
||||
authx.WriteError(w, http.StatusBadRequest, "not_after 无效")
|
||||
return
|
||||
}
|
||||
by := strings.TrimSpace(body.By)
|
||||
if by == "" {
|
||||
by = "watchdog"
|
||||
}
|
||||
lease, err := svcCtx.License.PutLease(&license.Lease{
|
||||
Customer: body.Customer,
|
||||
NotAfter: na,
|
||||
ExtensionsUsed: body.ExtensionsUsed,
|
||||
ExtensionsMax: body.ExtensionsMax,
|
||||
ExtensionMaxDays: body.ExtensionMaxDays,
|
||||
Note: body.Note,
|
||||
}, by)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"ok": true, "lease": lease, "status": svcCtx.License.Status(time.Now())})
|
||||
}
|
||||
}
|
||||
|
||||
func licenseImportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := requireLicenseSecret(svcCtx, r); err != nil {
|
||||
authx.WriteError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
if svcCtx.License == nil || !svcCtx.License.Enabled() {
|
||||
authx.WriteError(w, http.StatusBadRequest, "license 未启用")
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
by := strings.TrimSpace(r.URL.Query().Get("by"))
|
||||
if by == "" {
|
||||
by = "offline"
|
||||
}
|
||||
lease, err := svcCtx.License.ImportSigned(raw, by)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"ok": true, "lease": lease, "status": svcCtx.License.Status(time.Now())})
|
||||
}
|
||||
}
|
||||
326
platform/internal/handler/openapi.yaml
Normal file
326
platform/internal/handler/openapi.yaml
Normal file
@@ -0,0 +1,326 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: AI建站 Platform API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
通用中台契约。业务行数据仅通过 apps/{slug}/{resource} CRUD 传输;
|
||||
行业字段由蓝图定义,不在此增加行业专用路由。
|
||||
动词约定:GET 读 / POST 创建或动作 / PUT 更新 / DELETE 删除。
|
||||
servers:
|
||||
- url: http://127.0.0.1:8180
|
||||
paths:
|
||||
/api/v1/meta/apis:
|
||||
get:
|
||||
operationId: listApis
|
||||
summary: API 目录
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
/api/v1/meta/openapi.yaml:
|
||||
get:
|
||||
operationId: getOpenAPI
|
||||
summary: OpenAPI 原文
|
||||
responses:
|
||||
"200":
|
||||
description: YAML
|
||||
/api/v1/auth/register:
|
||||
post:
|
||||
operationId: authRegister
|
||||
summary: 注册
|
||||
responses:
|
||||
"201": { description: Created }
|
||||
/api/v1/auth/login:
|
||||
post:
|
||||
operationId: authLogin
|
||||
summary: 登录
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/api/v1/auth/token:
|
||||
post:
|
||||
operationId: authToken
|
||||
summary: 服务签发 JWT
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/apps/{slug}/publish:
|
||||
post:
|
||||
operationId: publishApp
|
||||
summary: 发布蓝图
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/api/v1/apps/{slug}/blueprint:
|
||||
get:
|
||||
operationId: getBlueprint
|
||||
summary: 读取蓝图
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/api/v1/apps/{slug}/agent-capsule:
|
||||
get:
|
||||
operationId: getAgentCapsule
|
||||
summary: 智能体胶囊
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/apps/{slug}/{resource}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
get:
|
||||
operationId: listRows
|
||||
summary: 列表
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
schema: { type: integer, minimum: 1, default: 1 }
|
||||
- name: page_size
|
||||
in: query
|
||||
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
|
||||
- name: sort
|
||||
in: query
|
||||
schema: { type: string }
|
||||
- name: filter.*
|
||||
in: query
|
||||
description: 如 filter.status=在售,键须在蓝图 allowed_filters
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PageResult"
|
||||
post:
|
||||
operationId: createRow
|
||||
summary: 创建
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Row"
|
||||
|
||||
/api/v1/apps/{slug}/{resource}/{id}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Id"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
get:
|
||||
operationId: getRow
|
||||
summary: 详情
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Row"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
put:
|
||||
operationId: updateRow
|
||||
summary: 更新
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Row"
|
||||
delete:
|
||||
operationId: deleteRow
|
||||
summary: 删除
|
||||
responses:
|
||||
"204": { description: No Content }
|
||||
|
||||
/api/v1/apps/{slug}/{resource}/import:
|
||||
post:
|
||||
operationId: importRows
|
||||
summary: 导入
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [file]
|
||||
properties:
|
||||
file: { type: string, format: binary }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/apps/{slug}/{resource}/export:
|
||||
get:
|
||||
operationId: exportRows
|
||||
summary: 导出
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- name: format
|
||||
in: query
|
||||
schema: { type: string, enum: [xlsx, csv], default: xlsx }
|
||||
responses:
|
||||
"200": { description: 文件流 }
|
||||
|
||||
/api/v1/apps/{slug}/{resource}/aggregate:
|
||||
get:
|
||||
operationId: aggregateRows
|
||||
summary: 聚合
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- name: group_by
|
||||
in: query
|
||||
schema: { type: string }
|
||||
- name: sum
|
||||
in: query
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/audit/logs:
|
||||
get:
|
||||
operationId: listAuditLogs
|
||||
summary: 审计日志
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/storage:
|
||||
post:
|
||||
operationId: uploadObject
|
||||
summary: 上传
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [file]
|
||||
properties:
|
||||
file: { type: string, format: binary }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/storage/{tenant}/{day}/{name}:
|
||||
get:
|
||||
operationId: downloadObject
|
||||
summary: 下载
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- name: tenant
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- name: day
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200": { description: 文件流 }
|
||||
|
||||
/api/v1/apps/generate:
|
||||
post:
|
||||
operationId: generateBlueprint
|
||||
summary: 生成蓝图(AI 服务,经网关 /ai 前缀)
|
||||
description: 实际请求 /ai/api/v1/apps/generate;勿在 platform 重复实现。
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
prompt: { type: string }
|
||||
excel: { type: string, format: binary }
|
||||
images: { type: string, format: binary }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/llm/providers:
|
||||
get:
|
||||
operationId: listLlmProviders
|
||||
summary: LLM 厂商(AI 服务)
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
in: header
|
||||
required: true
|
||||
schema: { type: string }
|
||||
description: Bearer JWT
|
||||
Slug:
|
||||
name: slug
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, pattern: "^[a-z][a-z0-9_]{1,47}$" }
|
||||
Resource:
|
||||
name: resource
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
description: 蓝图 apis.resources.path(无前导 /);不可为保留名
|
||||
Id:
|
||||
name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
schemas:
|
||||
PageResult:
|
||||
type: object
|
||||
properties:
|
||||
items: { type: array, items: { $ref: "#/components/schemas/Row" } }
|
||||
total: { type: integer }
|
||||
Row:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
NotFound:
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: integer }
|
||||
message: { type: string }
|
||||
18
platform/internal/handler/openapi_serve.go
Normal file
18
platform/internal/handler/openapi_serve.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed openapi.yaml
|
||||
var openapiYAML []byte
|
||||
|
||||
func openapiHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "public, max-age=60")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(openapiYAML)
|
||||
}
|
||||
}
|
||||
319
platform/internal/handler/platform.go
Normal file
319
platform/internal/handler/platform.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/logic/applogic"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"github.com/zeromicro/go-zero/rest/pathvar"
|
||||
)
|
||||
|
||||
func platformListTenantsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
list, err := l.ListTenants()
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusForbidden, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"items": list})
|
||||
}
|
||||
}
|
||||
|
||||
func platformCreateTenantHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
AdminPhone string `json:"admin_phone"`
|
||||
WithAdminInvite *bool `json:"with_admin_invite"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
withInvite := false
|
||||
if body.WithAdminInvite != nil {
|
||||
withInvite = *body.WithAdminInvite
|
||||
}
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
res, err := l.CreateTenant(body.Name, body.Slug, withInvite, body.AdminPhone)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, res)
|
||||
}
|
||||
}
|
||||
|
||||
func platformUpdateTenantHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
t, err := l.UpdateTenant(id, body.Name, body.Slug)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, t)
|
||||
}
|
||||
}
|
||||
|
||||
func platformAdminInviteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
inv, err := l.IssueAdminInvite(id)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, inv)
|
||||
}
|
||||
}
|
||||
|
||||
func platformAdminAccountHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||||
var body struct {
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
acc, err := l.IssueAdminAccount(id, body.Phone)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
_ = svcCtx.Audit.Log(r.Context(), id, authx.UserID(r.Context()), "platform.admin_account", acc.Username)
|
||||
httpx.OkJson(w, map[string]any{"admin_account": acc})
|
||||
}
|
||||
}
|
||||
|
||||
func platformListAdminsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
items, err := l.ListCompanyAdmins(id)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"items": items})
|
||||
}
|
||||
}
|
||||
|
||||
func platformUpdateAdminHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tid, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||||
uid, _ := strconv.ParseInt(pathvar.Vars(r)["uid"], 10, 64)
|
||||
var body struct {
|
||||
ResetPassword *bool `json:"reset_password"`
|
||||
Password string `json:"password"`
|
||||
Phone *string `json:"phone"`
|
||||
DisableUsernameLogin *bool `json:"disable_username_login"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
reset := body.ResetPassword != nil && *body.ResetPassword
|
||||
if !reset && body.Phone == nil && body.DisableUsernameLogin == nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, "请指定重置密码、更新手机号或禁用用户名登录")
|
||||
return
|
||||
}
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
acc, err := l.UpdateCompanyAdmin(tid, uid, reset, body.Password, body.Phone, body.DisableUsernameLogin)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
_ = svcCtx.Audit.Log(r.Context(), tid, authx.UserID(r.Context()), "platform.update_admin", acc.Username)
|
||||
httpx.OkJson(w, map[string]any{"admin_account": acc})
|
||||
}
|
||||
}
|
||||
|
||||
func platformEnterTenantHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
resp, err := l.EnterTenant(id)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
_ = svcCtx.Audit.Log(r.Context(), id, resp.UserID, "platform.enter_tenant", resp.Message)
|
||||
httpx.OkJson(w, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func platformExitTenantHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
resp, err := l.ExitTenant()
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
_ = svcCtx.Audit.Log(r.Context(), 0, resp.UserID, "platform.exit_tenant", resp.Message)
|
||||
httpx.OkJson(w, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func platformPermModulesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !authx.IsPlatformAdmin(authx.Role(r.Context())) {
|
||||
authx.WriteError(w, http.StatusForbidden, "需要超级管理员")
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{
|
||||
"modules": authx.PermModules(),
|
||||
"catalog": authx.CompanyPermCatalog(),
|
||||
"platform": []string{authx.Perm管理租户},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func platformGetTenantPermsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
perms, err := l.GetTenantPerms(id)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"tenant_id": id, "permissions": perms, "total": len(authx.CompanyPermCatalog())})
|
||||
}
|
||||
}
|
||||
|
||||
func platformSetTenantPermsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||||
var body struct {
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
l := applogic.NewPlatformLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
perms, err := l.SetTenantPerms(id, body.Permissions)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"tenant_id": id, "permissions": perms, "total": len(authx.CompanyPermCatalog())})
|
||||
}
|
||||
}
|
||||
|
||||
func companyEntitlementsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := applogic.NewAuthLogic(r.Context(), svcCtx)
|
||||
perms, modules, err := l.CompanyEntitlements()
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusForbidden, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"permissions": perms, "modules": modules})
|
||||
}
|
||||
}
|
||||
|
||||
func memberListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := applogic.NewMemberLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
list, err := l.List()
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
items := make([]map[string]any, 0, len(list))
|
||||
for _, u := range list {
|
||||
items = append(items, map[string]any{
|
||||
"user_id": u.UserID,
|
||||
"username": u.Username,
|
||||
"phone": u.Phone,
|
||||
"display_name": u.DisplayName,
|
||||
"role": u.Role,
|
||||
"org_unit_id": u.OrgUnitID,
|
||||
"status": u.Status,
|
||||
"created_at": u.CreatedAt,
|
||||
})
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"items": items})
|
||||
}
|
||||
}
|
||||
|
||||
func memberCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Password string `json:"password"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Role string `json:"role"`
|
||||
OrgUnitID int64 `json:"org_unit_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
l := applogic.NewMemberLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
u, plain, err := l.Create(body.Password, body.DisplayName, body.Role, body.OrgUnitID)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()),
|
||||
"member.create", "创建成员 "+u.Username)
|
||||
httpx.OkJson(w, map[string]any{
|
||||
"user_id": u.UserID,
|
||||
"username": u.Username,
|
||||
"display_name": u.DisplayName,
|
||||
"role": u.Role,
|
||||
"org_unit_id": u.OrgUnitID,
|
||||
"status": u.Status,
|
||||
"password": plain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func memberUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||||
var body struct {
|
||||
Role string `json:"role"`
|
||||
OrgUnitID int64 `json:"org_unit_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
l := applogic.NewMemberLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||
u, err := l.Update(id, body.Role, body.OrgUnitID, body.Status)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{
|
||||
"user_id": u.UserID,
|
||||
"username": u.Username,
|
||||
"display_name": u.DisplayName,
|
||||
"role": u.Role,
|
||||
"org_unit_id": u.OrgUnitID,
|
||||
"status": u.Status,
|
||||
})
|
||||
}
|
||||
}
|
||||
1035
platform/internal/handler/routes.go
Normal file
1035
platform/internal/handler/routes.go
Normal file
File diff suppressed because it is too large
Load Diff
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