feat(data-ana): python graphql federation subgraph with strawberry

- strawberry-graphql[asgi] dependency added

- 13 Federation 2 types: ClassPerformance/StudentWeakness/Dashboards/Mastery/ErrorBook

- 11 Query resolvers delegate to existing analytics/mastery services

- RouterAuthMiddleware validates Router-Authorization header on /graphql

- GraphQL endpoint mounted at /graphql alongside existing REST endpoints
This commit is contained in:
SpecialX
2026-07-15 00:57:02 +08:00
parent 1dcdcf23fd
commit a3f4fd013e
7 changed files with 818 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
"""数据分析服务入口FastAPI HTTP :3006.
端点清单3 基础 + 11 业务 = 14 个):
端点清单3 基础 + 11 业务 + 1 GraphQL = 15 个):
基础:
GET / 根信息
GET /healthz 活性检查liveness
@@ -20,12 +20,18 @@
GET /analytics/class/{class_id}/mastery-distribution 班级掌握度分布
GET /analytics/student/{student_id}/mastery 学生掌握度明细
GraphQLv2.1 M1Apollo Federation 2 子图):
POST /graphql GraphQL 端点Router-Authorization 校验)
- Resolver 委托给 analytics_service / mastery_service
- 由 Apollo Router 组合不直接对外暴露dev_mode 除外)
设计要点:
- 所有业务端点返回 ActionState[T]coord-cross-review §5.3 P0 整改)
- 降级标记在顶层 details.degraded不放 error.details
- /readyz 检查 4 依赖clickhouse / cdc_consumer / redis / iam_grpc
- gRPC server :50055 在 lifespan 启动
- CDC 消费者在 lifespan 启动
- /graphql 由 strawberry-graphql 提供RouterAuthMiddleware 拦截
"""
from collections.abc import AsyncGenerator
@@ -44,6 +50,8 @@ from prometheus_client import make_asgi_app
from . import analytics_service, cdc_consumer, grpc_server, warning_service
from .config import settings
from .graphql.router_auth import RouterAuthMiddleware
from .graphql.schema import graphql_app
from .repository import (
clickhouse_repository,
iam_client,
@@ -181,6 +189,13 @@ app = FastAPI(
FastAPIInstrumentor.instrument_app(app)
app.mount("/metrics", make_asgi_app())
# GraphQL Federation 2 子图v2.1 M1
# RouterAuthMiddleware 仅对 /graphql 端点校验 Router-Authorization Header
# REST 路由放行(已有 PermissionGuard / DataScope 校验)
app.add_middleware(RouterAuthMiddleware)
# 挂载 strawberry ASGI handler 到 /graphqlApollo Router 访问入口)
app.mount(settings.graphql_path, graphql_app)
router = APIRouter()
@@ -200,6 +215,7 @@ async def root() -> dict[str, Any]:
"healthz": "/healthz",
"readyz": "/readyz",
"business": "/analytics/*",
"graphql": settings.graphql_path,
},
}