Files
ai_site/platform/internal/handler/routes.go
whm cb56e6847e feat: add Z12/Z13 bind APIs, stock import, and sync docs
Enable auto default sync channels on agent activate, bind-code/phone confirm flows, publish ALTER, and align admin/yuheng docs with the production bind path.
2026-08-05 11:47:20 +08:00

1173 lines
49 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"aijianzhan/platform/internal/apidef"
"aijianzhan/platform/internal/audit"
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/agentcap"
"aijianzhan/platform/internal/logic/applogic"
"aijianzhan/platform/internal/meta"
"aijianzhan/platform/internal/svc"
"aijianzhan/platform/internal/types"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx"
"github.com/zeromicro/go-zero/rest/pathvar"
)
func chain(h http.HandlerFunc, mws ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
func RegisterHandlers(server *rest.Server, svcCtx *svc.ServiceContext) {
authMW := authx.Middleware(svcCtx.JWT, svcCtx.Config.DevAuth)
rl := svcCtx.Limiter.Middleware
perm := authx.RequirePermission
tenant := authx.RequireTenant()
platformAdmin := authx.RequirePlatformAdmin()
syncPush := authx.RequireSyncPushAccess()
appGrant := requireAgentAppGrant(svcCtx)
// —— 公开 ——
server.AddRoutes([]rest.Route{
{Method: http.MethodPost, Path: "/api/v1/auth/token", Handler: rl(tokenHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/auth/agent/register", Handler: rl(agentSelfRegisterHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/auth/bind-code/redeem", Handler: rl(bindCodeRedeemHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/auth/bind/phone-lookup", Handler: rl(bindPhoneLookupHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/auth/bind/phone-confirm", Handler: rl(bindPhoneConfirmHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/auth/register", Handler: rl(registerHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/auth/login", Handler: rl(loginHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/auth/sms/send", Handler: rl(sendLoginSMSHandler(svcCtx))},
{Method: http.MethodGet, Path: "/api/v1/meta/apis", Handler: rl(apiCatalogHandler())},
{Method: http.MethodGet, Path: "/api/v1/meta/openapi.yaml", Handler: rl(openapiHandler())},
// 授权租约:过期后仍可调用(中间件放行),供远端续费 / 安全狗延期
{Method: http.MethodGet, Path: "/api/v1/license/status", Handler: rl(licenseStatusHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/license/renew", Handler: rl(licenseRenewHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/license/extend", Handler: rl(licenseExtendHandler(svcCtx))},
{Method: http.MethodPut, Path: "/api/v1/license/lease", Handler: rl(licensePutLeaseHandler(svcCtx))},
{Method: http.MethodPost, Path: "/api/v1/license/import", Handler: rl(licenseImportHandler(svcCtx))},
// 已发布模块公开展示(看板预览 / 真页面截图,无需登录)
{Method: http.MethodGet, Path: "/api/v1/public/apps/:slug/blueprint", Handler: rl(publicBlueprintHandler(svcCtx))},
{Method: http.MethodGet, Path: "/api/v1/public/apps/:slug/:resource", Handler: rl(publicListHandler(svcCtx))},
// 按用户 ID 加密后的模块路径访问(宿主「访问地址」)
{Method: http.MethodGet, Path: "/api/v1/public/m/:token/blueprint", Handler: rl(publicModulePathBlueprintHandler(svcCtx))},
})
// —— 鉴权pending 可调用(入驻) ——
server.AddRoutes([]rest.Route{
{Method: http.MethodPost, Path: "/api/v1/auth/invites/accept", Handler: chain(inviteAcceptHandler(svcCtx), rl, authMW)},
{Method: http.MethodPost, Path: "/api/v1/auth/password", Handler: chain(changePasswordHandler(svcCtx), rl, authMW)},
{Method: http.MethodGet, Path: "/api/v1/auth/me", Handler: chain(meHandler(svcCtx), rl, authMW)},
{Method: http.MethodPut, Path: "/api/v1/auth/phone", Handler: chain(bindPhoneHandler(svcCtx), rl, authMW)},
{Method: http.MethodPost, Path: "/api/v1/tenants", Handler: chain(tenantCreateHandler(svcCtx), rl, authMW)},
// —— 平台超级管理员(仅公司一级:列表/新建)——
// —— 平台超级管理员(公司一级 + 权限额度)——
{Method: http.MethodGet, Path: "/api/v1/platform/tenants", Handler: chain(platformListTenantsHandler(svcCtx), rl, authMW, platformAdmin)},
{Method: http.MethodPost, Path: "/api/v1/platform/tenants", Handler: chain(platformCreateTenantHandler(svcCtx), rl, authMW, platformAdmin)},
{Method: http.MethodPut, Path: "/api/v1/platform/tenants/:id", Handler: chain(platformUpdateTenantHandler(svcCtx), rl, authMW, platformAdmin)},
{Method: http.MethodPost, Path: "/api/v1/platform/tenants/:id/enter", Handler: chain(platformEnterTenantHandler(svcCtx), rl, authMW, platformAdmin)},
{Method: http.MethodPost, Path: "/api/v1/platform/exit", Handler: chain(platformExitTenantHandler(svcCtx), rl, authMW, platformAdmin)},
{Method: http.MethodPost, Path: "/api/v1/platform/tenants/:id/admin-invite", Handler: chain(platformAdminInviteHandler(svcCtx), rl, authMW, platformAdmin)},
{Method: http.MethodPost, Path: "/api/v1/platform/tenants/:id/admin-account", Handler: chain(platformAdminAccountHandler(svcCtx), rl, authMW, platformAdmin)},
{Method: http.MethodGet, Path: "/api/v1/platform/tenants/:id/admins", Handler: chain(platformListAdminsHandler(svcCtx), rl, authMW, platformAdmin)},
{Method: http.MethodPut, Path: "/api/v1/platform/tenants/:id/admins/:uid", Handler: chain(platformUpdateAdminHandler(svcCtx), rl, authMW, platformAdmin)},
{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)},
{Method: http.MethodPost, Path: "/api/v1/platform/apps/ensure-import", Handler: chain(platformEnsureImportHandler(svcCtx), rl, authMW, platformAdmin)},
})
// —— 鉴权:需已加入租户 ——
server.AddRoutes([]rest.Route{
{Method: http.MethodGet, Path: "/api/v1/apps", Handler: chain(listAppsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块))},
{Method: http.MethodPost, Path: "/api/v1/admin/apps/ensure-import", Handler: chain(adminEnsureImportHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm发布模块))},
{Method: http.MethodPut, Path: "/api/v1/apps/:slug/draft", Handler: chain(saveDraftHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm写入模块), appGrant)},
{Method: http.MethodPost, Path: "/api/v1/apps/:slug/publish", Handler: chain(publishHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm发布模块), appGrant)},
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/blueprint", Handler: chain(getBlueprintHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块), appGrant)},
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/agent-capsule", Handler: chain(capsuleHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块), appGrant)},
{Method: http.MethodGet, Path: "/api/v1/audit/logs", Handler: chain(auditListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查看审计))},
{Method: http.MethodGet, Path: "/api/v1/admin/agents", Handler: chain(agentListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodPost, Path: "/api/v1/admin/agents", Handler: chain(agentCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodGet, Path: "/api/v1/admin/agents/:id", Handler: chain(agentGetHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodPut, Path: "/api/v1/admin/agents/:id", Handler: chain(agentUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodPost, Path: "/api/v1/admin/agents/:id/rotate-secret", Handler: chain(agentRotateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodDelete, Path: "/api/v1/admin/agents/:id", Handler: chain(agentDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
// Z12b智能体自查无需管理智能体
{Method: http.MethodGet, Path: "/api/v1/agents/me", Handler: chain(agentMeHandler(svcCtx), rl, authMW)},
{Method: http.MethodGet, Path: "/api/v1/admin/bind-codes", Handler: chain(bindCodeListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodPost, Path: "/api/v1/admin/bind-codes", Handler: chain(bindCodeCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodDelete, Path: "/api/v1/admin/bind-codes/:code", Handler: chain(bindCodeRevokeHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodGet, Path: "/api/v1/admin/roles", Handler: chain(roleListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodGet, Path: "/api/v1/admin/entitlements", Handler: chain(companyEntitlementsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块))},
{Method: http.MethodGet, Path: "/api/v1/admin/members", Handler: chain(memberListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
{Method: http.MethodPost, Path: "/api/v1/admin/members", Handler: chain(memberCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
{Method: http.MethodPut, Path: "/api/v1/admin/members/:id", Handler: chain(memberUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
{Method: http.MethodPost, Path: "/api/v1/admin/roles", Handler: chain(roleCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodGet, Path: "/api/v1/admin/roles/:id", Handler: chain(roleGetHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodPut, Path: "/api/v1/admin/roles/:id", Handler: chain(roleUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodDelete, Path: "/api/v1/admin/roles/:id", Handler: chain(roleDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
{Method: http.MethodGet, Path: "/api/v1/admin/invites", Handler: chain(inviteListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
{Method: http.MethodPost, Path: "/api/v1/admin/invites", Handler: chain(inviteCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
{Method: http.MethodDelete, Path: "/api/v1/admin/invites/:id", Handler: chain(inviteRevokeHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
{Method: http.MethodGet, Path: "/api/v1/admin/org-units", Handler: chain(orgUnitListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
{Method: http.MethodPost, Path: "/api/v1/admin/org-units", Handler: chain(orgUnitCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
{Method: http.MethodPut, Path: "/api/v1/admin/org-units/:id", Handler: chain(orgUnitUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
{Method: http.MethodDelete, Path: "/api/v1/admin/org-units/:id", Handler: chain(orgUnitDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
// 跨库同步中间件:本地 SQLite ↔ 线上 MySQL/Postgres后台可配线上地址
{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels", Handler: chain(syncListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels", Handler: chain(syncSaveHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels/:id", Handler: chain(syncGetHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodPut, Path: "/api/v1/admin/sync/channels/:id", Handler: chain(syncSaveHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodDelete, Path: "/api/v1/admin/sync/channels/:id", Handler: chain(syncDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodPost, Path: "/api/v1/admin/sync/test", Handler: chain(syncTestHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/prepare", Handler: chain(syncPrepareHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/start", Handler: chain(syncStartHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/stop", Handler: chain(syncStopHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodGet, Path: "/api/v1/admin/sync/conflicts", Handler: chain(syncConflictsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{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/channels/:id/inspect", Handler: chain(syncInspectHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/preview", Handler: chain(syncPreviewHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/drop-table", Handler: chain(syncDropTableHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
{Method: http.MethodGet, Path: "/api/v1/admin/sync/bindings", Handler: chain(syncBindingsListHandler(svcCtx), rl, authMW, tenant, syncPush)},
{Method: http.MethodPost, Path: "/api/v1/admin/sync/bindings", Handler: chain(syncBindingsEnsureHandler(svcCtx), rl, authMW, tenant, syncPush)},
// —— 本机 sync agent形态 B白名单 + push/pull「数据同步」租户级或登录用户按 Binding 自助 ——
{Method: http.MethodGet, Path: "/api/v1/agent/sync/channels/:id/whitelist", Handler: chain(agentSyncWhitelistHandler(svcCtx), rl, authMW, tenant, syncPush)},
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/push", Handler: chain(agentSyncPushHandler(svcCtx), rl, authMW, tenant, syncPush)},
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/push/batch", Handler: chain(agentSyncPushBatchHandler(svcCtx), rl, authMW, tenant, syncPush)},
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/pull", Handler: chain(agentSyncPullHandler(svcCtx), rl, authMW, tenant, syncPush)},
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/bootstrap", Handler: chain(agentSyncBootstrapHandler(svcCtx), rl, authMW, tenant, syncPush)},
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/schema", Handler: chain(agentSyncSchemaDescribeHandler(svcCtx), rl, authMW, tenant, syncPush)},
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/schema/ensure", Handler: chain(agentSyncSchemaEnsureHandler(svcCtx), rl, authMW, tenant, syncPush)},
// 存储POST 创建对象GET 读取(无 /upload 动词路径;旧路径保留别名防断裂)
{Method: http.MethodPost, Path: "/api/v1/storage", Handler: chain(uploadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm上传文件))},
{Method: http.MethodPost, Path: "/api/v1/storage/upload", Handler: chain(uploadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm上传文件))},
{Method: http.MethodGet, Path: "/api/v1/storage/:1/:2/:3", Handler: chain(downloadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm下载文件))},
// 动态 CRUD仅通用行数据传输
{Method: http.MethodPost, Path: "/api/v1/apps/:slug/:resource/import", Handler: chain(importHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm导入数据), appGrant)},
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource/export", Handler: chain(exportHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm导出数据), appGrant)},
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource/aggregate", Handler: chain(aggregateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查询数据), appGrant)},
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource", Handler: chain(listHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查询数据), appGrant)},
{Method: http.MethodPost, Path: "/api/v1/apps/:slug/:resource", Handler: chain(createHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm新增数据), appGrant)},
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource/:id", Handler: chain(getHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查询数据), appGrant)},
{Method: http.MethodPut, Path: "/api/v1/apps/:slug/:resource/:id", Handler: chain(updateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm更新数据), appGrant)},
{Method: http.MethodDelete, Path: "/api/v1/apps/:slug/:resource/:id", Handler: chain(deleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm删除数据), appGrant)},
})
}
func requireAgentAppGrant(svcCtx *svc.ServiceContext) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if authx.Role(r.Context()) != authx.RoleAgent {
next(w, r)
return
}
vars := pathvar.Vars(r)
slug := vars["slug"]
if slug == "" {
next(w, r)
return
}
if svcCtx.Agents == nil {
authx.WriteError(w, http.StatusForbidden, "agent store unavailable")
return
}
ok, err := svcCtx.Agents.HasAppAccess(r.Context(), authx.AgentID(r.Context()), slug)
if err != nil {
authx.WriteError(w, http.StatusForbidden, err.Error())
return
}
if ok {
next(w, r)
return
}
// 新建:目标模块尚不存在时,允许已启用智能体直接 publish/draft发布成功后自动写入 app_slugs
if svcCtx.Meta != nil && (r.Method == http.MethodPost || r.Method == http.MethodPut) {
existing, gerr := svcCtx.Meta.GetBySlug(r.Context(), authx.TenantID(r.Context()), slug)
if gerr != nil || existing == nil {
next(w, r)
return
}
}
authx.WriteError(w, http.StatusForbidden, "app not granted to agent: "+slug)
}
}
}
func agentListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
items, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).List()
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"items": items})
}
}
func agentGetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
acc, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Get(id)
if err != nil {
authx.WriteError(w, http.StatusNotFound, err.Error())
return
}
httpx.OkJson(w, acc)
}
}
func agentCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AgentCreateReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Create(&req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func agentUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
var req types.AgentUpdateReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
acc, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Update(id, &req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, acc)
}
}
func agentRotateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
resp, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Rotate(id)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func agentDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
if err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Delete(id); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"ok": true})
}
}
func agentMeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
acc, err := applogic.NewAuthLogic(r.Context(), svcCtx).AgentMe()
if err != nil {
authx.WriteError(w, http.StatusForbidden, err.Error())
return
}
httpx.OkJson(w, map[string]any{
"agent_id": acc.AgentID,
"tenant_id": acc.TenantID,
"name": acc.Name,
"client_id": acc.ClientID,
"status": acc.Status,
"channel_id": acc.ChannelID,
"online_db_id": acc.OnlineDBID,
"database_name": acc.DatabaseName,
"sync_bound": strings.TrimSpace(acc.ChannelID) != "" && strings.TrimSpace(acc.OnlineDBID) != "",
"app_slugs": acc.AppSlugs,
"permissions": acc.Perms,
})
}
}
func bindCodeListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
items, err := applogic.NewAuthLogic(r.Context(), svcCtx).ListBindCodes()
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"items": items})
}
}
func bindCodeCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req applogic.BindCodeCreateReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err != io.EOF {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
bc, err := applogic.NewAuthLogic(r.Context(), svcCtx).CreateBindCode(req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, bc)
}
}
func bindCodeRevokeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
code := pathvar.Vars(r)["code"]
if err := applogic.NewAuthLogic(r.Context(), svcCtx).RevokeBindCode(code); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"ok": true})
}
}
func bindCodeRedeemHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req applogic.BindCodeRedeemReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).RedeemBindCode(req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func bindPhoneLookupHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req applogic.PhoneLookupReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).PhoneLookup(req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func bindPhoneConfirmHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req applogic.PhoneConfirmReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).PhoneConfirm(req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func roleListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
items, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).List()
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"items": items})
}
}
func roleGetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
role, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Get(id)
if err != nil {
authx.WriteError(w, http.StatusNotFound, err.Error())
return
}
httpx.OkJson(w, role)
}
}
func roleCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RoleCreateReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
role, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Create(&req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, role)
}
}
func roleUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
var req types.RoleUpdateReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
role, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Update(id, &req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, role)
}
}
func roleDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
if err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Delete(id); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"ok": true})
}
}
func apiCatalogHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
httpx.OkJson(w, map[string]any{
"platform": apidef.Catalog,
"ai": apidef.AICatalog,
"verbs": []string{"GET", "POST", "PUT", "DELETE"},
"note": "业务数据只走 apps/{slug}/{resource} CRUD行业字段由蓝图定义不在此增删路由。",
})
}
}
func tokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.TokenReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).IssueToken(&req)
if err != nil {
authx.WriteError(w, http.StatusUnauthorized, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func agentSelfRegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AgentSelfRegisterReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).SelfRegisterAgent(&req)
if err != nil {
msg := err.Error()
code := http.StatusBadRequest
if strings.Contains(msg, "invalid register secret") {
code = http.StatusUnauthorized
}
if strings.Contains(msg, "already registered") {
code = http.StatusConflict
}
authx.WriteError(w, code, msg)
return
}
httpx.OkJson(w, resp)
}
}
func registerHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RegisterReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).Register(&req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "auth.register", req.Username)
httpx.WriteJson(w, http.StatusCreated, resp)
}
}
func loginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.LoginReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).Login(&req)
if err != nil {
msg := err.Error()
code := http.StatusUnauthorized
if strings.Contains(msg, "请填写") || strings.Contains(msg, "格式") || strings.Contains(msg, "未开启") || strings.Contains(msg, "未启用") {
code = http.StatusBadRequest
}
authx.WriteError(w, code, msg)
return
}
label := req.Username
if label == "" {
label = req.Phone
}
_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "auth.login", label)
httpx.OkJson(w, resp)
}
}
func sendLoginSMSHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SMSSendReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
res, err := applogic.NewAuthLogic(r.Context(), svcCtx).SendLoginSMS(req.Phone)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, types.SMSSendResp{
OK: true,
ExpiresIn: res.ExpiresIn,
RetryAfter: res.RetryAfter,
Message: res.Message,
DebugCode: res.DebugCode,
})
}
}
func changePasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var body struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
if err := applogic.NewAuthLogic(r.Context(), svcCtx).ChangePassword(body.OldPassword, body.NewPassword); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()), "auth.change_password", "ok")
httpx.OkJson(w, map[string]any{"ok": true})
}
}
func meHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
u, err := applogic.NewAuthLogic(r.Context(), svcCtx).Me()
if err != nil {
authx.WriteError(w, http.StatusUnauthorized, err.Error())
return
}
httpx.OkJson(w, map[string]any{
"user_id": u.UserID,
"username": u.Username,
"phone": u.Phone,
"username_login_disabled": u.UsernameLoginDisabled,
"display_name": u.DisplayName,
"role": u.Role,
"tenant_id": u.TenantID,
"org_unit_id": u.OrgUnitID,
"status": u.Status,
"policy": map[string]any{
"phone_login_only": applogic.NewAuthLogic(r.Context(), svcCtx).PhoneLoginOnlyPolicy(),
"license_enabled": svcCtx.Config.License.Enabled,
"disable_username_login_if_phone_bound": svcCtx.Config.Auth.DisableUsernameLoginIfPhoneBound,
"require_phone_bound": svcCtx.Config.Auth.RequirePhoneBound,
},
})
}
}
func bindPhoneHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var body struct {
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
}
l := applogic.NewAuthLogic(r.Context(), svcCtx)
u, err := l.BindPhone(body.Phone)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
if body.DisableUsernameLogin != nil {
u, err = l.SetUsernameLoginDisabled(*body.DisableUsernameLogin)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
}
_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()),
"auth.bind_phone", u.Phone)
httpx.OkJson(w, map[string]any{
"user_id": u.UserID,
"username": u.Username,
"phone": u.Phone,
"username_login_disabled": u.UsernameLoginDisabled,
"display_name": u.DisplayName,
})
}
}
func inviteAcceptHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.InviteAcceptReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).AcceptInvite(&req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "auth.invite.accept", req.Code)
httpx.OkJson(w, resp)
}
}
func tenantCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.TenantCreateReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).CreateTenant(&req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "tenant.create", req.Name)
httpx.WriteJson(w, http.StatusCreated, resp)
}
}
func inviteListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
items, err := applogic.NewInviteAdminLogic(r.Context(), svcCtx).List()
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"items": items})
}
}
func inviteCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.InviteCreateReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
inv, err := applogic.NewInviteAdminLogic(r.Context(), svcCtx).Create(&req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.WriteJson(w, http.StatusCreated, inv)
}
}
func inviteRevokeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
if err := applogic.NewInviteAdminLogic(r.Context(), svcCtx).Revoke(id); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]string{"status": "ok"})
}
}
func orgUnitListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
items, err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).List()
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"items": items, "max_depth": 5})
}
}
func orgUnitCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.OrgUnitCreateReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
ou, err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).Create(&req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.WriteJson(w, http.StatusCreated, ou)
}
}
func orgUnitUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
var req types.OrgUnitUpdateReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
ou, err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).Update(id, &req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, ou)
}
}
func orgUnitDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
if err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).Delete(id); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]string{"status": "ok"})
}
}
func auditListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
size, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if page <= 0 {
page = 1
}
if size <= 0 {
size = 50
}
items, total, err := svcCtx.Audit.List(r.Context(), authx.TenantID(r.Context()), size, (page-1)*size)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"items": items, "total": total, "page": page, "page_size": size})
}
}
func uploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(32 << 20); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
file, hdr, err := r.FormFile("file")
if err != nil {
authx.WriteError(w, http.StatusBadRequest, "file required")
return
}
defer file.Close()
ct := hdr.Header.Get("Content-Type")
if ct == "" {
ct = "application/octet-stream"
}
meta, err := svcCtx.Objects.Put(r.Context(), authx.TenantID(r.Context()), hdr.Filename, ct, file, hdr.Size)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()), "storage.upload", audit.DetailJSON(meta))
httpx.OkJson(w, types.UploadResp{
Key: meta.Key, URL: meta.URL, Filename: meta.Filename, ContentType: meta.ContentType, Size: meta.Size,
})
}
}
func downloadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// path: /api/v1/storage/t1/20260101/xxx.png → key from URL after /storage/
idx := strings.Index(r.URL.Path, "/storage/")
if idx < 0 {
authx.WriteError(w, http.StatusNotFound, "not found")
return
}
key := r.URL.Path[idx+len("/storage/"):]
rc, meta, err := svcCtx.Objects.Open(r.Context(), key)
if err != nil {
authx.WriteError(w, http.StatusNotFound, err.Error())
return
}
defer rc.Close()
if meta.ContentType != "" {
w.Header().Set("Content-Type", meta.ContentType)
}
if meta.Filename != "" {
w.Header().Set("Content-Disposition", "inline; filename="+meta.Filename)
}
_, _ = io.Copy(w, rc)
}
}
func capsuleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
slug := pathvar.Vars(r)["slug"]
resp, err := applogic.NewCapsuleLogic(r.Context(), svcCtx).Build(slug)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func importHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := pathvar.Vars(r)
if err := r.ParseMultipartForm(8 << 20); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
file, hdr, err := r.FormFile("file")
if err != nil {
authx.WriteError(w, http.StatusBadRequest, "file required")
return
}
defer file.Close()
filename := "import.xlsx"
if hdr != nil && hdr.Filename != "" {
filename = hdr.Filename
}
resp, err := applogic.NewCrudLogic(r.Context(), svcCtx).ImportRows(vars["slug"], vars["resource"], filename, file)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func exportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := pathvar.Vars(r)
format := strings.ToLower(r.URL.Query().Get("format"))
if format == "csv" {
raw, err := applogic.NewCrudLogic(r.Context(), svcCtx).ExportCSV(vars["slug"], vars["resource"])
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename="+vars["resource"]+".csv")
_, _ = w.Write(raw)
return
}
raw, name, err := applogic.NewCrudLogic(r.Context(), svcCtx).ExportExcel(vars["slug"], vars["resource"])
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", "attachment; filename="+name)
_, _ = w.Write(raw)
}
}
func aggregateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := pathvar.Vars(r)
q := r.URL.Query()
resp, err := applogic.NewCrudLogic(r.Context(), svcCtx).Aggregate(
vars["slug"], vars["resource"], q.Get("group_by"), q.Get("sum"),
)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func listAppsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
resp, err := applogic.NewPublishLogic(r.Context(), svcCtx).ListApps()
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func saveDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
slug := pathvar.Vars(r)["slug"]
var req types.DraftReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewPublishLogic(r.Context(), svcCtx).SaveDraft(slug, &req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func publishHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
slug := pathvar.Vars(r)["slug"]
var req types.PublishReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
resp, err := applogic.NewPublishLogic(r.Context(), svcCtx).Publish(slug, &req)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func getBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
slug := pathvar.Vars(r)["slug"]
bp, err := applogic.NewCrudLogic(r.Context(), svcCtx).GetBlueprint(slug)
if err != nil {
authx.WriteError(w, http.StatusNotFound, err.Error())
return
}
httpx.OkJson(w, bp)
}
}
func publicBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
slug := pathvar.Vars(r)["slug"]
app, err := svcCtx.Meta.FindPublishedBySlug(r.Context(), slug)
if err != nil {
authx.WriteError(w, http.StatusNotFound, err.Error())
return
}
if app.Blueprint == nil {
authx.WriteError(w, http.StatusNotFound, "blueprint missing")
return
}
httpx.OkJson(w, app.Blueprint)
}
}
func publicModulePathBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := pathvar.Vars(r)["token"]
secret := svcCtx.Config.Agent.CapsuleSecret
if secret == "" {
secret = svcCtx.JWT.AccessSecret
}
claim, err := agentcap.OpenModulePath(secret, token)
if err != nil {
authx.WriteError(w, http.StatusNotFound, "invalid access path")
return
}
app, err := svcCtx.Meta.GetBySlug(r.Context(), claim.TenantID, claim.Slug)
if err != nil || app == nil || app.Blueprint == nil || app.Status != meta.StatusPublished {
authx.WriteError(w, http.StatusNotFound, "module not found")
return
}
httpx.OkJson(w, app.Blueprint)
}
}
func publicListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := pathvar.Vars(r)
slug, resource := vars["slug"], vars["resource"]
if apidef.IsReservedResource(resource) {
authx.WriteError(w, http.StatusNotFound, "not found")
return
}
app, err := svcCtx.Meta.FindPublishedBySlug(r.Context(), slug)
if err != nil {
authx.WriteError(w, http.StatusNotFound, err.Error())
return
}
q := r.URL.Query()
page, size := applogic.ParsePage(q.Get("page"), q.Get("page_size"))
filters := map[string]string{}
for k, vs := range q {
if strings.HasPrefix(k, "filter.") && len(vs) > 0 {
filters[strings.TrimPrefix(k, "filter.")] = vs[0]
}
}
// 以应用所属租户读取数据,无需登录态
ctx := authx.WithClaims(r.Context(), app.TenantID, 0, authx.Role只读)
resp, err := applogic.NewCrudLogic(ctx, svcCtx).List(slug, resource, page, size, filters, q.Get("sort"))
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func listHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := pathvar.Vars(r)
slug, resource := vars["slug"], vars["resource"]
if apidef.IsReservedResource(resource) {
authx.WriteError(w, http.StatusNotFound, "not found")
return
}
q := r.URL.Query()
page, size := applogic.ParsePage(q.Get("page"), q.Get("page_size"))
filters := map[string]string{}
for k, vs := range q {
if strings.HasPrefix(k, "filter.") && len(vs) > 0 {
filters[strings.TrimPrefix(k, "filter.")] = vs[0]
}
}
resp, err := applogic.NewCrudLogic(r.Context(), svcCtx).List(slug, resource, page, size, filters, q.Get("sort"))
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, resp)
}
}
func createHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := pathvar.Vars(r)
if apidef.IsReservedResource(vars["resource"]) {
authx.WriteError(w, http.StatusBadRequest, "reserved resource name")
return
}
body, err := readJSONObject(r)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
row, err := applogic.NewCrudLogic(r.Context(), svcCtx).Create(vars["slug"], vars["resource"], body)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.WriteJson(w, http.StatusCreated, row)
}
}
func getHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := pathvar.Vars(r)
row, err := applogic.NewCrudLogic(r.Context(), svcCtx).Get(vars["slug"], vars["resource"], vars["id"])
if err != nil {
authx.WriteError(w, http.StatusNotFound, err.Error())
return
}
httpx.OkJson(w, row)
}
}
func updateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := pathvar.Vars(r)
body, err := readJSONObject(r)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
row, err := applogic.NewCrudLogic(r.Context(), svcCtx).Update(vars["slug"], vars["resource"], vars["id"], body)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, row)
}
}
func deleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := pathvar.Vars(r)
if err := applogic.NewCrudLogic(r.Context(), svcCtx).Delete(vars["slug"], vars["resource"], vars["id"]); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
}
func readJSONObject(r *http.Request) (map[string]any, error) {
defer r.Body.Close()
raw, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
return nil, err
}
var body map[string]any
if err := json.Unmarshal(raw, &body); err != nil {
return nil, err
}
return body, nil
}