From 2796d7c88f5e9720e011baac1db94a518fa070ef Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:04:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(infra):=20api-gateway=20=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E8=A1=A5=E5=85=A8=20+=20build=20context=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20+=20docker-compose.test=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/deploy.env.example | 7 +- infra/docker-compose.deploy.yml | 27 ++++++- infra/docker-compose.test.yml | 134 ++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 infra/docker-compose.test.yml diff --git a/infra/deploy.env.example b/infra/deploy.env.example index 3802d28..c6f2254 100644 --- a/infra/deploy.env.example +++ b/infra/deploy.env.example @@ -32,8 +32,11 @@ ADMIN_PORTAL_PORT=4003 KAFKA_BROKERS= # ============ push-gateway(WebSocket 推送服务)============ -# /internal/push 鉴权 token(msg 服务调用时需携带 X-Internal-Token 头) -INTERNAL_API_TOKEN=CHANGE_ME_TO_STRONG_INTERNAL_TOKEN +# /internal/push 鉴权 token(msg 服务调用时需携带 X-Internal-Key 头,ARB-013) +# PUSH_INTERNAL_TOKEN 是 v2 规范环境变量;INTERNAL_API_TOKEN 作为向后兼容别名仍被接受 +PUSH_INTERNAL_TOKEN=CHANGE_ME_TO_STRONG_INTERNAL_TOKEN +# 向后兼容别名(留空则使用 PUSH_INTERNAL_TOKEN;设置后会回退到此值) +INTERNAL_API_TOKEN= # WebSocket 允许的 Origin 白名单(逗号分隔) WS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:4000,http://localhost:4001,http://localhost:4002,http://localhost:4003 # push-gateway 对外端口 diff --git a/infra/docker-compose.deploy.yml b/infra/docker-compose.deploy.yml index 42d8ab8..e086e19 100644 --- a/infra/docker-compose.deploy.yml +++ b/infra/docker-compose.deploy.yml @@ -23,25 +23,31 @@ services: # ============================================================ api-gateway: build: - context: ./repo/services/api-gateway - dockerfile: Dockerfile + context: ./repo + dockerfile: services/api-gateway/Dockerfile container_name: edu-api-gateway restart: unless-stopped environment: API_GATEWAY_PORT: ${API_GATEWAY_PORT:-8080} + ENV: production JWT_SECRET: ${JWT_SECRET} JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud} JWT_AUDIENCE: ${JWT_AUDIENCE:-next-edu-cloud} + # RS256 公钥端点(非 DevMode 下必填,config.go W7 防护) + IAM_JWKS_URL: http://iam:3002/v1/iam/.well-known/jwks.json # 生产环境强制关闭 dev-token 旁路 DEV_MODE: "false" CLASSES_SERVICE_URL: http://classes:3001 IAM_SERVICE_URL: http://iam:3002 TEACHER_BFF_URL: http://teacher-bff:3003 + STUDENT_BFF_URL: http://student-bff:3009 + PARENT_BFF_URL: http://parent-bff:3010 CORE_EDU_SERVICE_URL: http://core-edu:3004 CONTENT_SERVICE_URL: http://content:3005 DATA_ANA_SERVICE_URL: http://data-ana:3006 MSG_SERVICE_URL: http://msg:3007 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} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318} LOG_LEVEL: ${LOG_LEVEL:-info} ports: @@ -116,9 +122,19 @@ services: restart: unless-stopped environment: PORT: 3003 + TEACHER_BFF_DEV_MODE: "false" + REDIS_URL: ${REDIS_URL} + IAM_GRPC_TARGET: iam:50052 IAM_SERVICE_URL: http://iam:3002 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} LOG_LEVEL: ${LOG_LEVEL:-info} NODE_ENV: production depends_on: @@ -280,9 +296,12 @@ services: JWT_SECRET: ${JWT_SECRET} REDIS_URL: ${REDIS_URL} KAFKA_BROKERS: ${KAFKA_BROKERS:-kafka:29092} - KAFKA_NOTIFICATION_TOPIC: edu.notification.requested + # ARB-013 canonical topic name (v2 alignment with msg Outbox publisher). + KAFKA_NOTIFICATION_TOPIC: edu.notify.notification.sent KAFKA_CONSUMER_GROUP: push-gateway - INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-edu-internal-token} + # PUSH_INTERNAL_TOKEN is the canonical env var (ARB-013 alignment with + # msg). INTERNAL_API_TOKEN kept as backward-compat alias. + PUSH_INTERNAL_TOKEN: ${PUSH_INTERNAL_TOKEN:-${INTERNAL_API_TOKEN:-edu-internal-token}} JWKS_URL: http://iam:3002/v1/iam/.well-known/jwks.json WS_ALLOWED_ORIGINS: ${WS_ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:4000,http://localhost:4001,http://localhost:4002,http://localhost:4003} MAX_CONNS_PER_USER: "5" diff --git a/infra/docker-compose.test.yml b/infra/docker-compose.test.yml new file mode 100644 index 0000000..d6b9584 --- /dev/null +++ b/infra/docker-compose.test.yml @@ -0,0 +1,134 @@ +# 本地测试用 Docker Compose(iam + teacher-bff + ai 联调) +# 使用方式: +# docker compose -f infra/docker-compose.test.yml up -d --build +# +# 依赖:edu-full_default 网络已存在(MySQL/Redis/Kafka 已在该网络) +# 网络:复用 edu-full_default,使各服务可直连 edu-mysql/edu-redis/edu-kafka +name: edu-test + +services: + iam: + build: + context: .. + dockerfile: services/iam/Dockerfile + container_name: edu-iam-test + restart: unless-stopped + environment: + # 生产模式:避免 pino-pretty 依赖(pruned by --prod) + # DEV_MODE=true 仍可旁路鉴权 + NODE_ENV: production + PORT: "3002" + GRPC_PORT: "50052" + DATABASE_URL: mysql://edu:changeme@edu-mysql:3306/next_edu_cloud + REDIS_URL: redis://edu-redis:6379 + IAM_PRIVATE_KEY_PATH: /app/keys/iam-private.pem + IAM_PUBLIC_KEY_PATH: /app/keys/iam-public.pem + JWT_ISSUER: next-edu-cloud + JWT_AUDIENCE: next-edu-cloud + JWT_KEY_ID: iam-rs256-v1 + KAFKA_BROKERS: kafka:29092 + KAFKA_CLIENT_ID: iam-service + LOG_LEVEL: info + # 开发模式:接受 dev-token 旁路鉴权 + DEV_MODE: "true" + ports: + - "3002:3002" + - "50052:50052" + volumes: + - ../keys:/app/keys:ro + healthcheck: + test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3002/healthz"] + interval: 10s + timeout: 5s + start_period: 30s + retries: 5 + networks: + - edu-full + + teacher-bff: + build: + context: .. + dockerfile: services/teacher-bff/Dockerfile + container_name: edu-teacher-bff-test + restart: unless-stopped + environment: + NODE_ENV: production + PORT: "3003" + GRAPHQL_PATH: /graphql + TEACHER_BFF_DEV_MODE: "true" + REDIS_URL: redis://edu-redis:6379 + IAM_GRPC_TARGET: iam:50052 + IAM_SERVICE_URL: http://iam:3002 + CORE_EDU_GRPC_TARGET: core-edu:50053 + 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 + LOG_LEVEL: info + ports: + - "3003:3003" + depends_on: + iam: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3003/healthz"] + interval: 10s + timeout: 5s + start_period: 20s + retries: 5 + networks: + - edu-full + + ai: + build: + context: ../services/ai + dockerfile: Dockerfile + container_name: edu-ai-test + restart: unless-stopped + environment: + # HTTP/gRPC 端口 + HTTP_PORT: "3008" + GRPC_PORT: "50058" + # 开发模式(旁路鉴权,dev-token) + DEV_MODE: "true" + LOG_LEVEL: info + # 基础设施 + REDIS_URL: redis://edu-redis:6379/0 + KAFKA_BOOTSTRAP_SERVERS: kafka:29092 + KAFKA_AI_USAGE_TOPIC: edu.ai.usage + # 下游 gRPC 端点(edu-full_default 网络内服务名解析) + # content 服务未启动 → connect 失败但不阻断启动,调用时抛 AIError + CONTENT_GRPC_ENDPOINT: content:50054 + DATA_ANA_GRPC_ENDPOINT: data-ana:50055 + IAM_GRPC_ENDPOINT: iam:50052 + # LLM 配置(测试环境不配 API key,LLM 功能降级,HTTP 端点仍可调) + OPENAI_API_KEY: "" + OPENAI_BASE_URL: https://api.openai.com/v1 + ANTHROPIC_API_KEY: "" + BAICHUAN_API_KEY: "" + OLLAMA_BASE_URL: "" + LLM_PROVIDER_PRIORITY: openai,anthropic,baichuan,local_ollama + DEFAULT_CHAT_MODEL: gpt-4o-mini + DEFAULT_QUESTION_MODEL: gpt-4o-mini + # 可观测性(jaeger 已在 edu-full_default 网络中) + OTEL_EXPORTER_OTLP_ENDPOINT: http://edu-jaeger:4318 + ports: + - "3008:3008" + - "50058:50058" + depends_on: + iam: + condition: service_healthy + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3008/healthz')"] + interval: 10s + timeout: 5s + start_period: 30s + retries: 5 + networks: + - edu-full + +networks: + edu-full: + external: true + name: edu-full_default