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() 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/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)}, }) // —— 鉴权:需已加入租户 —— server.AddRoutes([]rest.Route{ {Method: http.MethodGet, Path: "/api/v1/apps", Handler: chain(listAppsHandler(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管理智能体))}, {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数据同步))}, // 存储: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 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 }