feat(api-gateway): retire teacher/student/parent-bff, route GraphQL to apollo-router
v2.1 M9+M10: BFF layer and old portals retired.
- api-gateway: remove TeacherBffURL/StudentBffURL/ParentBffURL config
- api-gateway: add ApolloRouterURL config and /api/graphql route
- api-gateway: /api/admin/graphql now proxies to apollo-router
- api-gateway: health checks now ping apollo-router instead of BFF
- deploy.yml: replace teacher-bff service block with apollo-router
- deploy.yml: add config-service service block (M3 dependency)
- deploy.yml: remove teacher-portal and admin-portal service blocks
- source code in services/{teacher,student,parent}-bff/ and apps/*-portal/ retained for rollback
This commit is contained in:
@@ -39,15 +39,14 @@ services:
|
|||||||
DEV_MODE: "false"
|
DEV_MODE: "false"
|
||||||
CLASSES_SERVICE_URL: http://classes:3001
|
CLASSES_SERVICE_URL: http://classes:3001
|
||||||
IAM_SERVICE_URL: http://iam:3002
|
IAM_SERVICE_URL: http://iam:3002
|
||||||
TEACHER_BFF_URL: http://teacher-bff:3003
|
# v2.1 M9:apollo-router 替代 teacher-bff / student-bff / parent-bff
|
||||||
STUDENT_BFF_URL: http://student-bff:3009
|
APOLLO_ROUTER_URL: http://apollo-router:3000
|
||||||
PARENT_BFF_URL: http://parent-bff:3010
|
|
||||||
CORE_EDU_SERVICE_URL: http://core-edu:3004
|
CORE_EDU_SERVICE_URL: http://core-edu:3004
|
||||||
CONTENT_SERVICE_URL: http://content:3005
|
CONTENT_SERVICE_URL: http://content:3005
|
||||||
DATA_ANA_SERVICE_URL: http://data-ana:3006
|
DATA_ANA_SERVICE_URL: http://data-ana:3006
|
||||||
MSG_SERVICE_URL: http://msg:3007
|
MSG_SERVICE_URL: http://msg:3007
|
||||||
AI_SERVICE_URL: http://ai:3008
|
AI_SERVICE_URL: http://ai:3008
|
||||||
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:4000,http://localhost:4001,http://localhost:4002,http://localhost:4003}
|
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:4000,http://localhost:4001,http://localhost:4002,http://localhost:4003,http://localhost:4010}
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||||
ports:
|
ports:
|
||||||
@@ -55,6 +54,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
classes:
|
classes:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
apollo-router:
|
||||||
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8080/healthz"]
|
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8080/healthz"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -114,39 +115,75 @@ services:
|
|||||||
- edu-net
|
- edu-net
|
||||||
- edu-shared
|
- edu-shared
|
||||||
|
|
||||||
teacher-bff:
|
# v2.1 M3:config-service 从 iam 拆出(插件配置 + 布局 + 用户偏好)
|
||||||
|
config-service:
|
||||||
build:
|
build:
|
||||||
context: ./repo
|
context: ./repo
|
||||||
dockerfile: services/teacher-bff/Dockerfile
|
dockerfile: services/config-service/Dockerfile
|
||||||
container_name: edu-teacher-bff
|
container_name: edu-config-service
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
PORT: 3003
|
PORT: 3011
|
||||||
TEACHER_BFF_DEV_MODE: "false"
|
GRPC_PORT: 50059
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
REDIS_URL: ${REDIS_URL}
|
REDIS_URL: ${REDIS_URL}
|
||||||
IAM_GRPC_TARGET: iam:50052
|
# GraphQL 子图信任凭证(ADR-036)
|
||||||
IAM_SERVICE_URL: http://iam:3002
|
ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-edu-router-secret-change-in-production}
|
||||||
CLASSES_SERVICE_URL: http://classes:3001
|
|
||||||
CORE_EDU_GRPC_TARGET: core-edu:50053
|
|
||||||
CORE_EDU_SERVICE_URL: http://core-edu:3004
|
|
||||||
CONTENT_GRPC_TARGET: content:50054
|
|
||||||
DATA_ANA_GRPC_TARGET: data-ana:50055
|
|
||||||
MSG_GRPC_TARGET: msg:50056
|
|
||||||
MSG_SERVICE_URL: http://msg:3007
|
|
||||||
AI_GRPC_TARGET: ai:50058
|
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
depends_on:
|
depends_on:
|
||||||
iam:
|
iam:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
classes:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3003/healthz"]
|
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3011/healthz"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
start_period: 20s
|
start_period: 30s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- edu-net
|
||||||
|
- edu-shared
|
||||||
|
|
||||||
|
# v2.1 M9:teacher-bff / student-bff / parent-bff 已下线,由 apollo-router 替代
|
||||||
|
# 源码保留在 services/{teacher,student,parent}-bff/ 以便回滚
|
||||||
|
apollo-router:
|
||||||
|
build:
|
||||||
|
context: ./repo
|
||||||
|
dockerfile: infra/apollo-router/Dockerfile
|
||||||
|
container_name: edu-apollo-router
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# 路由配置文件位于 /dist/router.yaml(Dockerfile COPY)
|
||||||
|
APOLLO_ROUTER_CONFIG_PATH: /dist/router.yaml
|
||||||
|
# 信任凭证(子图 RouterAuthGuard 校验,ADR-036)
|
||||||
|
ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-edu-router-secret-change-in-production}
|
||||||
|
# 子图 URL 覆盖(通过 router.yaml override_subgraph_url 配置)
|
||||||
|
IAM_SUBGRAPH_URL: http://iam:3002/graphql
|
||||||
|
CONFIG_SERVICE_SUBGRAPH_URL: http://config-service:3011/graphql
|
||||||
|
CORE_EDU_SUBGRAPH_URL: http://core-edu:3004/graphql
|
||||||
|
CONTENT_SUBGRAPH_URL: http://content:3005/graphql
|
||||||
|
MSG_SUBGRAPH_URL: http://msg:3007/graphql
|
||||||
|
DATA_ANA_SUBGRAPH_URL: http://data-ana:3006/graphql
|
||||||
|
AI_SUBGRAPH_URL: http://ai:3008/graphql
|
||||||
|
ports:
|
||||||
|
- "${APOLLO_ROUTER_PORT:-3000}:3000"
|
||||||
|
depends_on:
|
||||||
|
iam:
|
||||||
|
condition: service_healthy
|
||||||
|
core-edu:
|
||||||
|
condition: service_healthy
|
||||||
|
content:
|
||||||
|
condition: service_healthy
|
||||||
|
msg:
|
||||||
|
condition: service_healthy
|
||||||
|
config-service:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8088/healthz"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
networks:
|
networks:
|
||||||
- edu-net
|
- edu-net
|
||||||
@@ -318,64 +355,9 @@ services:
|
|||||||
- edu-net
|
- edu-net
|
||||||
- edu-shared
|
- edu-shared
|
||||||
|
|
||||||
teacher-portal:
|
# v2.1 M10:teacher-portal / admin-portal 已下线,由 portal-shell 替代
|
||||||
build:
|
# 源码保留在 apps/{teacher,student,parent,admin}-portal/ 以便回滚
|
||||||
context: ./repo
|
# portal-shell 部署在 infra/docker-compose.yml(开发环境)中,端口 4010
|
||||||
dockerfile: apps/teacher-portal/Dockerfile
|
|
||||||
container_name: edu-teacher-portal
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
PORT: 3000
|
|
||||||
API_GATEWAY_URL: http://api-gateway:8080
|
|
||||||
ports:
|
|
||||||
- "${TEACHER_PORTAL_PORT:-3000}:3000"
|
|
||||||
depends_on:
|
|
||||||
api-gateway:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3000/"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
start_period: 20s
|
|
||||||
retries: 3
|
|
||||||
networks:
|
|
||||||
- edu-net
|
|
||||||
- edu-shared
|
|
||||||
|
|
||||||
admin-portal:
|
|
||||||
build:
|
|
||||||
context: ./repo
|
|
||||||
dockerfile: apps/admin-portal/Dockerfile
|
|
||||||
container_name: edu-admin-portal
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
PORT: 4003
|
|
||||||
API_GATEWAY_URL: http://api-gateway:8080
|
|
||||||
# 生产环境强制关闭 mock,使用真实后端
|
|
||||||
NEXT_PUBLIC_API_MOCKING: "disabled"
|
|
||||||
NEXT_PUBLIC_MF_ENABLED: "false"
|
|
||||||
NEXT_PUBLIC_SHELL_URL: http://admin-portal:4003
|
|
||||||
NEXT_PUBLIC_WS_URL: ws://push-gateway:8081/ws
|
|
||||||
NEXT_PUBLIC_OTEL_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
|
||||||
NEXT_PUBLIC_WEB_VITALS_ENDPOINT: /api/admin/web-vitals
|
|
||||||
ports:
|
|
||||||
- "${ADMIN_PORTAL_PORT:-4003}:4003"
|
|
||||||
depends_on:
|
|
||||||
api-gateway:
|
|
||||||
condition: service_healthy
|
|
||||||
teacher-bff:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:4003/api/ready"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
start_period: 20s
|
|
||||||
retries: 3
|
|
||||||
networks:
|
|
||||||
- edu-net
|
|
||||||
- edu-shared
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
# 应用服务内部网络
|
# 应用服务内部网络
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import (
|
|||||||
// Config 持有 api-gateway 运行时配置。
|
// Config 持有 api-gateway 运行时配置。
|
||||||
// P2 起 JWT 验签改 RS256(IAM 签发,Gateway 用 JWKS 公钥校验),
|
// P2 起 JWT 验签改 RS256(IAM 签发,Gateway 用 JWKS 公钥校验),
|
||||||
// JWTSecret 仅在 DevMode 下作为 mock 密钥保留。
|
// JWTSecret 仅在 DevMode 下作为 mock 密钥保留。
|
||||||
|
//
|
||||||
|
// v2.1 M9:teacher-bff / student-bff / parent-bff 已下线,
|
||||||
|
// 由 apollo-router(GraphQL 联邦)替代 BFF 聚合层。
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port string
|
Port string
|
||||||
JWKSURL string // RS256 公钥端点(IAM GET /.well-known/jwks.json)
|
JWKSURL string // RS256 公钥端点(IAM GET /.well-known/jwks.json)
|
||||||
@@ -18,9 +21,7 @@ type Config struct {
|
|||||||
CORSOrigins string
|
CORSOrigins string
|
||||||
ClassesServiceURL string
|
ClassesServiceURL string
|
||||||
IamServiceURL string
|
IamServiceURL string
|
||||||
TeacherBffURL string
|
ApolloRouterURL string // v2.1 M9:apollo-router GraphQL 联邦入口
|
||||||
StudentBffURL string
|
|
||||||
ParentBffURL string
|
|
||||||
CoreEduServiceURL string
|
CoreEduServiceURL string
|
||||||
ContentServiceURL string
|
ContentServiceURL string
|
||||||
DataAnaServiceURL string
|
DataAnaServiceURL string
|
||||||
@@ -77,9 +78,7 @@ func Load() *Config {
|
|||||||
CORSOrigins: getEnv("CORS_ORIGINS", ""),
|
CORSOrigins: getEnv("CORS_ORIGINS", ""),
|
||||||
ClassesServiceURL: getEnv("CLASSES_SERVICE_URL", "http://localhost:3001"),
|
ClassesServiceURL: getEnv("CLASSES_SERVICE_URL", "http://localhost:3001"),
|
||||||
IamServiceURL: getEnv("IAM_SERVICE_URL", "http://localhost:3002"),
|
IamServiceURL: getEnv("IAM_SERVICE_URL", "http://localhost:3002"),
|
||||||
TeacherBffURL: getEnv("TEACHER_BFF_URL", "http://localhost:3003"),
|
ApolloRouterURL: getEnv("APOLLO_ROUTER_URL", "http://localhost:3000"),
|
||||||
StudentBffURL: getEnv("STUDENT_BFF_URL", "http://localhost:3009"),
|
|
||||||
ParentBffURL: getEnv("PARENT_BFF_URL", "http://localhost:3010"),
|
|
||||||
CoreEduServiceURL: getEnv("CORE_EDU_SERVICE_URL", "http://localhost:3004"),
|
CoreEduServiceURL: getEnv("CORE_EDU_SERVICE_URL", "http://localhost:3004"),
|
||||||
ContentServiceURL: getEnv("CONTENT_SERVICE_URL", "http://localhost:3005"),
|
ContentServiceURL: getEnv("CONTENT_SERVICE_URL", "http://localhost:3005"),
|
||||||
DataAnaServiceURL: getEnv("DATA_ANA_SERVICE_URL", "http://localhost:3006"),
|
DataAnaServiceURL: getEnv("DATA_ANA_SERVICE_URL", "http://localhost:3006"),
|
||||||
|
|||||||
@@ -29,20 +29,20 @@ type downstreamCheck struct {
|
|||||||
// GET /readyz:并行 ping 下游服务 /healthz,超时 2s。
|
// GET /readyz:并行 ping 下游服务 /healthz,超时 2s。
|
||||||
//
|
//
|
||||||
// 软失败规则(president-final-rulings.md §3.3):
|
// 软失败规则(president-final-rulings.md §3.3):
|
||||||
// - 必需依赖(iam / teacher-bff,P2 已就绪)失败 → 503
|
// - 必需依赖(iam / apollo-router)失败 → 503
|
||||||
// - 可选依赖(P3-P5 未就绪服务)失败 → 200 + degraded 列表
|
// - 可选依赖(core-edu / content / msg / ai / data-ana)失败 → 200 + degraded 列表
|
||||||
// - 全部可达 → 200 {"status":"ok"}
|
// - 全部可达 → 200 {"status":"ok"}
|
||||||
|
//
|
||||||
|
// v2.1 M9:teacher-bff / student-bff / parent-bff 已下线,由 apollo-router 替代。
|
||||||
func Readyz(cfg *config.Config) gin.HandlerFunc {
|
func Readyz(cfg *config.Config) gin.HandlerFunc {
|
||||||
checks := []downstreamCheck{
|
checks := []downstreamCheck{
|
||||||
{name: "iam", url: cfg.IamServiceURL + "/healthz", required: true},
|
{name: "iam", url: cfg.IamServiceURL + "/healthz", required: true},
|
||||||
{name: "teacher-bff", url: cfg.TeacherBffURL + "/healthz", required: true},
|
{name: "apollo-router", url: cfg.ApolloRouterURL + "/healthz", required: true},
|
||||||
{name: "core-edu", url: cfg.CoreEduServiceURL + "/healthz", required: false},
|
{name: "core-edu", url: cfg.CoreEduServiceURL + "/healthz", required: false},
|
||||||
{name: "content", url: cfg.ContentServiceURL + "/healthz", required: false},
|
{name: "content", url: cfg.ContentServiceURL + "/healthz", required: false},
|
||||||
{name: "msg", url: cfg.MsgServiceURL + "/healthz", required: false},
|
{name: "msg", url: cfg.MsgServiceURL + "/healthz", required: false},
|
||||||
{name: "ai", url: cfg.AiServiceURL + "/healthz", required: false},
|
{name: "ai", url: cfg.AiServiceURL + "/healthz", required: false},
|
||||||
{name: "data-ana", url: cfg.DataAnaServiceURL + "/healthz", required: false},
|
{name: "data-ana", url: cfg.DataAnaServiceURL + "/healthz", required: false},
|
||||||
{name: "student-bff", url: cfg.StudentBffURL + "/healthz", required: false},
|
|
||||||
{name: "parent-bff", url: cfg.ParentBffURL + "/healthz", required: false},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Timeout: 2 * time.Second}
|
client := &http.Client{Timeout: 2 * time.Second}
|
||||||
|
|||||||
@@ -79,15 +79,9 @@ func main() {
|
|||||||
registerProxy(api, "classes", cfg.CoreEduServiceURL)
|
registerProxy(api, "classes", cfg.CoreEduServiceURL)
|
||||||
// iam 服务路由(身份与访问管理)
|
// iam 服务路由(身份与访问管理)
|
||||||
registerProxy(api, "iam", cfg.IamServiceURL)
|
registerProxy(api, "iam", cfg.IamServiceURL)
|
||||||
// teacher-bff 路由(教师聚合层 GraphQL)
|
// v2.1 M9:teacher-bff / student-bff / parent-bff 已下线,
|
||||||
// BFF 在 /graphql 提供服务(@Controller("graphql")),需剥离 /api/v1/teacher 前缀
|
// 前端 GraphQL 查询统一通过 apollo-router 聚邦层。
|
||||||
// 契约 student-portal_contract.md §2.3:/api/v1/{bff}/* → {bff}:port/*
|
// /api/v1/teacher/* /api/v1/student/* /api/v1/parent/* 路由已移除。
|
||||||
registerBffProxy(api, "teacher", cfg.TeacherBffURL)
|
|
||||||
// student-bff 路由(学生聚合层 GraphQL,P3)
|
|
||||||
registerBffProxy(api, "student", cfg.StudentBffURL)
|
|
||||||
// parent-bff 路由(家长聚合层 GraphQL,P4)
|
|
||||||
// parent-bff 在 /v1/graphql 提供服务(ARB-022 §24.4 ISSUE-003 方案 A)
|
|
||||||
registerBffProxy(api, "parent", cfg.ParentBffURL)
|
|
||||||
// core-edu 域路由(考试/作业/成绩)—— 下游 controller 在 /v1/{domain}/*,仅需剥离 /api
|
// core-edu 域路由(考试/作业/成绩)—— 下游 controller 在 /v1/{domain}/*,仅需剥离 /api
|
||||||
registerProxy(api, "exams", cfg.CoreEduServiceURL)
|
registerProxy(api, "exams", cfg.CoreEduServiceURL)
|
||||||
registerProxy(api, "homework", cfg.CoreEduServiceURL)
|
registerProxy(api, "homework", cfg.CoreEduServiceURL)
|
||||||
@@ -110,25 +104,43 @@ func main() {
|
|||||||
registerProxy(api, "dashboard", cfg.DataAnaServiceURL)
|
registerProxy(api, "dashboard", cfg.DataAnaServiceURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// admin 路由组:admin-portal 入口(契约 admin-portal_contract.md §2.3)
|
// v2.1 M9:GraphQL 联邦入口(替代旧 BFF 聚合层)
|
||||||
// POST /api/admin/graphql → teacher-bff:3003/graphql(admin 命名空间)
|
// /api/graphql → apollo-router:3000/graphql(统一前端 GraphQL 入口)
|
||||||
// 中间件链:熔断 → JWT 鉴权 → admin 角色强制 → 指标 → 反向代理
|
// /api/admin/graphql → apollo-router:3000/graphql(admin 命名空间)
|
||||||
|
// 中间件链:熔断 → JWT 鉴权 → 指标 → 反向代理(admin 路由额外强制 admin 角色)
|
||||||
|
graphqlGroup := r.Group("/api/graphql")
|
||||||
|
graphqlGroup.Use(middleware.CircuitBreaker("apollo-router"))
|
||||||
|
graphqlGroup.Use(middleware.AuthMiddleware(cfg, fetcher))
|
||||||
|
graphqlGroup.Use(observability.Metrics())
|
||||||
|
{
|
||||||
|
// 路径重写:/api/graphql → /graphql(apollo-router supergraph.path)
|
||||||
|
graphqlProxy, err := proxy.NewProxyRewrite(cfg.ApolloRouterURL, func(p string) string {
|
||||||
|
return strings.TrimPrefix(p, "/api/graphql")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to create apollo-router graphql proxy", "target", cfg.ApolloRouterURL, "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
graphqlGroup.Any("", proxy.ProxyHandler(graphqlProxy))
|
||||||
|
graphqlGroup.Any("/", proxy.ProxyHandler(graphqlProxy))
|
||||||
|
}
|
||||||
|
|
||||||
|
// admin GraphQL 入口(admin-portal 仍走独立路径以强制 admin 角色)
|
||||||
admin := r.Group("/api/admin")
|
admin := r.Group("/api/admin")
|
||||||
admin.Use(middleware.CircuitBreaker("teacher-bff-admin"))
|
admin.Use(middleware.CircuitBreaker("apollo-router-admin"))
|
||||||
admin.Use(middleware.AuthMiddleware(cfg, fetcher))
|
admin.Use(middleware.AuthMiddleware(cfg, fetcher))
|
||||||
admin.Use(middleware.AdminRoleMiddleware())
|
admin.Use(middleware.AdminRoleMiddleware())
|
||||||
admin.Use(observability.Metrics())
|
admin.Use(observability.Metrics())
|
||||||
{
|
{
|
||||||
// /api/admin/graphql 是唯一入口(契约 §2.3 要求),
|
// 路径重写:/api/admin/graphql → /graphql(apollo-router)
|
||||||
// 路径重写:/api/admin/graphql → /graphql(teacher-bff @Controller("graphql"))
|
adminGraphqlProxy, err := proxy.NewProxyRewrite(cfg.ApolloRouterURL, func(p string) string {
|
||||||
graphqlProxy, err := proxy.NewProxyRewrite(cfg.TeacherBffURL, func(p string) string {
|
|
||||||
return strings.TrimPrefix(p, "/api/admin")
|
return strings.TrimPrefix(p, "/api/admin")
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to create admin graphql proxy", "target", cfg.TeacherBffURL, "error", err)
|
slog.Error("failed to create admin graphql proxy", "target", cfg.ApolloRouterURL, "error", err)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
admin.Any("/graphql", proxy.ProxyHandler(graphqlProxy))
|
admin.Any("/graphql", proxy.ProxyHandler(adminGraphqlProxy))
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
@@ -180,22 +192,5 @@ func registerProxy(api *gin.RouterGroup, prefix, targetURL string) {
|
|||||||
api.Any("/"+prefix+"/*path", handler)
|
api.Any("/"+prefix+"/*path", handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerBffProxy 创建带路径重写的反向代理,用于 BFF 路由(teacher/student/parent)。
|
// registerBffProxy 已在 v2.1 M9 移除:teacher-bff / student-bff / parent-bff 由 apollo-router 替代。
|
||||||
// BFF 在 /graphql 或 /v1/graphql 提供服务(@Controller),与下游 controller 在 /v1/{domain}/* 的
|
// 前端 GraphQL 查询统一通过 /api/graphql → apollo-router:3000/graphql。
|
||||||
// 非 BFF 服务不同,需剥离 /api/v1/{prefix} 前缀,仅转发剩余路径到下游。
|
|
||||||
// 例:/api/v1/teacher/graphql → /graphql(teacher-bff:3003/graphql)。
|
|
||||||
// 例:/api/v1/parent/v1/graphql → /v1/graphql(parent-bff:3010/v1/graphql,ARB-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)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user