打通前后端联调链路

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fish
2026-04-26 15:15:19 +08:00
parent 91226fa976
commit 83d9a08b97
8 changed files with 156 additions and 68 deletions

View File

@@ -10,8 +10,9 @@ RUN mkdir -p /var/log/nginx /var/www/certbot
COPY nginx/nginx.conf /etc/nginx/nginx.conf COPY nginx/nginx.conf /etc/nginx/nginx.conf
COPY nginx/conf.d/ /etc/nginx/conf.d/ COPY nginx/conf.d/ /etc/nginx/conf.d/
# 创建自签名证书(仅用于开发,生产环境应挂载真实证书) # 创建 SSL 目录并生成自签名证书(仅用于开发,生产环境应挂载真实证书)
RUN apk add --no-cache openssl && \ RUN mkdir -p /etc/nginx/ssl && \
apk add --no-cache openssl && \
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/key.pem \ -keyout /etc/nginx/ssl/key.pem \
-out /etc/nginx/ssl/cert.pem \ -out /etc/nginx/ssl/cert.pem \

View File

@@ -0,0 +1,29 @@
version: "3.8"
services:
gateway:
build:
context: .
dockerfile: Dockerfile
container_name: api-gateway
ports:
- "80:80"
- "443:443"
volumes:
# 开发环境:挂载配置便于热更新,生产环境应内嵌在镜像中
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
networks:
- default
- frontend_asset-helper-network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 3s
start_period: 5s
retries: 3
networks:
frontend_asset-helper-network:
external: true

View File

@@ -3,25 +3,48 @@ server {
listen 80 default_server; listen 80 default_server;
listen [::]:80 default_server; listen [::]:80 default_server;
server_name _; server_name _;
return 444; return 444;
} }
# HTTP 重定向到 HTTPS # HTTP 重定向到 HTTPS(生产域名)
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name api.example.com; server_name api.example.com;
location /.well-known/acme-challenge/ { location /.well-known/acme-challenge/ {
root /var/www/certbot; root /var/www/certbot;
} }
location / { location / {
return 301 https://$server_name$request_uri; return 301 https://$server_name$request_uri;
} }
} }
# 开发环境 - 直接代理,不重定向到 HTTPS
server {
listen 80;
listen [::]:80;
server_name localhost api-gateway host.docker.internal;
# 开发环境直接代理,不强制 HTTPS
include /etc/nginx/conf.d/services/*.conf;
# 健康检查
location /health {
access_log off;
return 200 '{"status":"healthy","timestamp":"$time_iso8601"}\n';
add_header Content-Type application/json;
}
# 根路径
location / {
return 200 '{"status":"ok","service":"api-gateway","timestamp":"$time_iso8601"}\n';
add_header Content-Type application/json;
}
}
# API 网关主配置 # API 网关主配置
server { server {
listen 443 ssl http2; listen 443 ssl http2;

View File

@@ -1,36 +1,62 @@
# 用户服务路由 # 用户服务路由
location /api/v1/users {
# 限流 # 账号登录(严格限流
location /api/v1/auth/login/account {
limit_req zone=api_strict burst=5 nodelay;
limit_conn addr 3;
rewrite ^/api/v1/auth/login/account$ /login break;
proxy_pass http://user_login_account;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
}
# 邮箱登录(严格限流)
location /api/v1/auth/login/email {
limit_req zone=api_strict burst=5 nodelay;
limit_conn addr 3;
rewrite ^/api/v1/auth/login/email$ /login break;
proxy_pass http://user_login_email;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
}
# 账号注册(通用限流)
location /api/v1/users/register/account {
limit_req zone=general burst=20 nodelay; limit_req zone=general burst=20 nodelay;
limit_conn addr 10; limit_conn addr 10;
# 代理设置 rewrite ^/api/v1/users/register/account$ /register break;
proxy_pass http://user_service; proxy_pass http://user_register_account;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id; proxy_set_header X-Request-ID $request_id;
# WebSocket 支持(如果需要)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 缓存控制
proxy_cache_bypass $http_upgrade;
proxy_no_cache 1;
} }
# 认证相关接口(严格限流) # 邮箱注册(通用限流)
location /api/v1/auth { location /api/v1/users/register/email {
limit_req zone=api_strict burst=5 nodelay; limit_req zone=general burst=20 nodelay;
limit_conn addr 3; limit_conn addr 10;
proxy_pass http://user_service; rewrite ^/api/v1/users/register/email$ /register break;
proxy_pass http://user_register_email;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -44,22 +44,42 @@ http {
# 连接限制 # 连接限制
limit_conn_zone $binary_remote_addr zone=addr:10m; limit_conn_zone $binary_remote_addr zone=addr:10m;
# 上游服务 # 上游服务 —— 通过宿主机端口访问各微服务(开发环境)
upstream user_service { # 生产环境应改为容器名:端口,并确保同网络
upstream user_login_account {
least_conn; least_conn;
server user-service:8080 max_fails=3 fail_timeout=30s; server host.docker.internal:20111 max_fails=3 fail_timeout=30s;
keepalive 32; keepalive 32;
} }
upstream user_register_account {
least_conn;
server host.docker.internal:20112 max_fails=3 fail_timeout=30s;
keepalive 32;
}
upstream user_login_email {
least_conn;
server host.docker.internal:20113 max_fails=3 fail_timeout=30s;
keepalive 32;
}
upstream user_register_email {
least_conn;
server host.docker.internal:20114 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# 以下服务尚未实现,临时标记为 down避免启动时 DNS 解析失败
upstream order_service { upstream order_service {
least_conn; least_conn;
server order-service:8080 max_fails=3 fail_timeout=30s; server 127.0.0.1:9999 down;
keepalive 32; keepalive 32;
} }
upstream payment_service { upstream payment_service {
least_conn; least_conn;
server payment-service:8080 max_fails=3 fail_timeout=30s; server 127.0.0.1:9999 down;
keepalive 32; keepalive 32;
} }

View File

@@ -28,18 +28,12 @@ struct LoginRequest {
password: String, password: String,
} }
// 统一响应包装 // 登录/认证类接口扁平响应(与前端约定对齐)
#[derive(Serialize)] #[derive(Serialize)]
struct ApiResponse<T> { struct LoginResponse {
success: bool, success: bool,
token: Option<String>,
message: String, message: String,
data: Option<T>,
}
// 登录业务数据
#[derive(Serialize)]
struct LoginData {
token: String,
} }
// JWT Claims // JWT Claims
@@ -98,7 +92,7 @@ async fn main() {
async fn login_handler( async fn login_handler(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(payload): Json<LoginRequest>, Json(payload): Json<LoginRequest>,
) -> (StatusCode, Json<ApiResponse<LoginData>>) { ) -> (StatusCode, Json<LoginResponse>) {
info!("Login attempt for user: {}", payload.username); info!("Login attempt for user: {}", payload.username);
// 查询用户账号与密码 // 查询用户账号与密码
@@ -120,16 +114,16 @@ async fn login_handler(
match verify(&payload.password, &password_hash) { match verify(&payload.password, &password_hash) {
Ok(true) => { Ok(true) => {
info!("User {} logged in successfully", payload.username); info!("User {} logged in successfully", payload.username);
// 生成 JWT // 生成 JWT
let token = generate_token(&user_id.to_string(), &state.jwt_secret); let token = generate_token(&user_id.to_string(), &state.jwt_secret);
( (
StatusCode::OK, StatusCode::OK,
Json(ApiResponse { Json(LoginResponse {
success: true, success: true,
token: Some(token),
message: "Login successful".to_string(), message: "Login successful".to_string(),
data: Some(LoginData { token }),
}), }),
) )
} }
@@ -137,10 +131,10 @@ async fn login_handler(
warn!("Invalid password for user {}", payload.username); warn!("Invalid password for user {}", payload.username);
( (
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
Json(ApiResponse { Json(LoginResponse {
success: false, success: false,
token: None,
message: "Invalid credentials".to_string(), message: "Invalid credentials".to_string(),
data: None,
}), }),
) )
} }
@@ -148,10 +142,10 @@ async fn login_handler(
warn!("Password verification error: {:?}", e); warn!("Password verification error: {:?}", e);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse { Json(LoginResponse {
success: false, success: false,
token: None,
message: "Internal error".to_string(), message: "Internal error".to_string(),
data: None,
}), }),
) )
} }

View File

@@ -28,18 +28,12 @@ struct LoginRequest {
password: String, password: String,
} }
// 统一响应包装 // 登录/认证类接口扁平响应(与前端约定对齐)
#[derive(Serialize)] #[derive(Serialize)]
struct ApiResponse<T> { struct LoginResponse {
success: bool, success: bool,
token: Option<String>,
message: String, message: String,
data: Option<T>,
}
// 登录业务数据
#[derive(Serialize)]
struct LoginData {
token: String,
} }
// JWT Claims // JWT Claims
@@ -98,7 +92,7 @@ async fn main() {
async fn login_handler( async fn login_handler(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(payload): Json<LoginRequest>, Json(payload): Json<LoginRequest>,
) -> (StatusCode, Json<ApiResponse<LoginData>>) { ) -> (StatusCode, Json<LoginResponse>) {
info!("Login attempt for email: {}", payload.email); info!("Login attempt for email: {}", payload.email);
// 查询用户邮箱与密码 // 查询用户邮箱与密码
@@ -120,16 +114,16 @@ async fn login_handler(
match verify(&payload.password, &password_hash) { match verify(&payload.password, &password_hash) {
Ok(true) => { Ok(true) => {
info!("Email {} logged in successfully", payload.email); info!("Email {} logged in successfully", payload.email);
// 生成 JWT // 生成 JWT
let token = generate_token(&user_id.to_string(), &state.jwt_secret); let token = generate_token(&user_id.to_string(), &state.jwt_secret);
( (
StatusCode::OK, StatusCode::OK,
Json(ApiResponse { Json(LoginResponse {
success: true, success: true,
token: Some(token),
message: "Login successful".to_string(), message: "Login successful".to_string(),
data: Some(LoginData { token }),
}), }),
) )
} }
@@ -137,10 +131,10 @@ async fn login_handler(
warn!("Invalid password for email {}", payload.email); warn!("Invalid password for email {}", payload.email);
( (
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
Json(ApiResponse { Json(LoginResponse {
success: false, success: false,
token: None,
message: "Invalid credentials".to_string(), message: "Invalid credentials".to_string(),
data: None,
}), }),
) )
} }
@@ -148,10 +142,10 @@ async fn login_handler(
warn!("Password verification error: {:?}", e); warn!("Password verification error: {:?}", e);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse { Json(LoginResponse {
success: false, success: false,
token: None,
message: "Internal error".to_string(), message: "Internal error".to_string(),
data: None,
}), }),
) )
} }

View File

@@ -18,7 +18,8 @@ export default defineConfig(({ mode }) => ({
}, },
proxy: { proxy: {
'/api': { '/api': {
target: process.env.VITE_API_BASE_URL || 'http://host.docker.internal:80', // 开发环境:通过 Docker 网络直接访问网关容器
target: process.env.VITE_API_BASE_URL || 'http://api-gateway',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },