feat(api-gateway): admin 路由组 + BFF 路径重写 + announcements 路由 + Dockerfile 修复 + nextstep v2 文档

This commit is contained in:
SpecialX
2026-07-14 16:03:38 +08:00
parent 9fd7c018c2
commit a70977ad4b
8 changed files with 1121 additions and 14 deletions

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
@@ -79,12 +80,15 @@ func main() {
// iam 服务路由(身份与访问管理)
registerProxy(api, "iam", cfg.IamServiceURL)
// teacher-bff 路由(教师聚合层 GraphQL
registerProxy(api, "teacher", cfg.TeacherBffURL)
// BFF 在 /graphql 提供服务(@Controller("graphql")),需剥离 /api/v1/teacher 前缀
// 契约 student-portal_contract.md §2.3/api/v1/{bff}/* → {bff}:port/*
registerBffProxy(api, "teacher", cfg.TeacherBffURL)
// student-bff 路由(学生聚合层 GraphQLP3
registerProxy(api, "student", cfg.StudentBffURL)
registerBffProxy(api, "student", cfg.StudentBffURL)
// parent-bff 路由(家长聚合层 GraphQLP4
registerProxy(api, "parent", cfg.ParentBffURL)
// core-edu 域路由(考试/作业/成绩)
// parent-bff 在 /v1/graphql 提供服务ARB-022 §24.4 ISSUE-003 方案 A
registerBffProxy(api, "parent", cfg.ParentBffURL)
// core-edu 域路由(考试/作业/成绩)—— 下游 controller 在 /v1/{domain}/*,仅需剥离 /api
registerProxy(api, "exams", cfg.CoreEduServiceURL)
registerProxy(api, "homework", cfg.CoreEduServiceURL)
registerProxy(api, "grades", cfg.CoreEduServiceURL)
@@ -93,9 +97,12 @@ func main() {
registerProxy(api, "chapters", cfg.ContentServiceURL)
registerProxy(api, "knowledge-points", cfg.ContentServiceURL)
registerProxy(api, "questions", cfg.ContentServiceURL)
// msg 域路由(通知/消息)
// msg 域路由(通知/消息/公告
registerProxy(api, "notifications", cfg.MsgServiceURL)
registerProxy(api, "messages", cfg.MsgServiceURL)
// announcements 路由msg 公告 REST APImsg nextstep.md §2.5 要求)
// /api/v1/announcements/* → msg:3007/v1/announcements/*
registerProxy(api, "announcements", cfg.MsgServiceURL)
// ai 服务路由AI 聊天/生成/优化)
registerProxy(api, "ai", cfg.AiServiceURL)
// data-ana 域路由(学情诊断/错题本/仪表盘)
@@ -103,6 +110,27 @@ func main() {
registerProxy(api, "dashboard", cfg.DataAnaServiceURL)
}
// admin 路由组admin-portal 入口(契约 admin-portal_contract.md §2.3
// POST /api/admin/graphql → teacher-bff:3003/graphqladmin 命名空间)
// 中间件链:熔断 → JWT 鉴权 → admin 角色强制 → 指标 → 反向代理
admin := r.Group("/api/admin")
admin.Use(middleware.CircuitBreaker("teacher-bff-admin"))
admin.Use(middleware.AuthMiddleware(cfg, fetcher))
admin.Use(middleware.AdminRoleMiddleware())
admin.Use(observability.Metrics())
{
// /api/admin/graphql 是唯一入口(契约 §2.3 要求),
// 路径重写:/api/admin/graphql → /graphqlteacher-bff @Controller("graphql")
graphqlProxy, err := proxy.NewProxyRewrite(cfg.TeacherBffURL, func(p string) string {
return strings.TrimPrefix(p, "/api/admin")
})
if err != nil {
slog.Error("failed to create admin graphql proxy", "target", cfg.TeacherBffURL, "error", err)
panic(err)
}
admin.Any("/graphql", proxy.ProxyHandler(graphqlProxy))
}
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
@@ -139,6 +167,8 @@ func main() {
// registerProxy 创建反向代理并注册无尾斜杠与通配符两条路由。
// RedirectTrailingSlash=false 时 Gin 不会自动跳转,故两条路由都要显式注册。
// 适用于下游 controller 路径含 /v1 前缀的服务iam/core-edu/content/msg/ai/data-ana
// /api/v1/{prefix}/* → 剥离 /api → /v1/{prefix}/* 转发下游。
func registerProxy(api *gin.RouterGroup, prefix, targetURL string) {
p, err := proxy.NewProxy(targetURL)
if err != nil {
@@ -149,3 +179,23 @@ func registerProxy(api *gin.RouterGroup, prefix, targetURL string) {
api.Any("/"+prefix, handler)
api.Any("/"+prefix+"/*path", handler)
}
// registerBffProxy 创建带路径重写的反向代理,用于 BFF 路由teacher/student/parent
// BFF 在 /graphql 或 /v1/graphql 提供服务(@Controller与下游 controller 在 /v1/{domain}/* 的
// 非 BFF 服务不同,需剥离 /api/v1/{prefix} 前缀,仅转发剩余路径到下游。
// 例:/api/v1/teacher/graphql → /graphqlteacher-bff:3003/graphql
// 例:/api/v1/parent/v1/graphql → /v1/graphqlparent-bff:3010/v1/graphqlARB-022 §24.4 ISSUE-003 方案 A
// 契约依据student-portal_contract.md §2.3 /api/v1/{bff}/* → {bff}:port/*
func registerBffProxy(api *gin.RouterGroup, prefix, targetURL string) {
stripPrefix := "/api/v1/" + prefix
p, err := proxy.NewProxyRewrite(targetURL, func(p string) string {
return strings.TrimPrefix(p, stripPrefix)
})
if err != nil {
slog.Error("failed to create bff proxy", "prefix", prefix, "target", targetURL, "error", err)
panic(err)
}
handler := proxy.ProxyHandler(p)
api.Any("/"+prefix, handler)
api.Any("/"+prefix+"/*path", handler)
}