use axum::{ extract::State, http::StatusCode, response::Json, routing::post, Router, }; use bcrypt::hash; use chrono::Utc; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::env; use std::sync::Arc; use tracing::{info, warn}; use validator::Validate; #[derive(Clone)] struct AppState { db: PgPool, } #[derive(Deserialize, Validate)] struct RegisterRequest { #[validate(length(min = 3, max = 50))] username: String, #[validate(length(min = 6))] password: String, #[validate(email)] email: String, } #[derive(Serialize)] struct RegisterResponse { success: bool, user_id: Option, message: String, } #[derive(Serialize)] struct ErrorResponse { error: String, } #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); info!("Starting user-register service..."); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let pool = sqlx::postgres::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); info!("Database connected"); let state = Arc::new(AppState { db: pool }); let app = Router::new() .route("/register", post(register_handler)) .route("/health", axum::routing::get(health_handler)) .with_state(state); let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "8080".to_string()); let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) .await .unwrap(); info!("User-register service listening on port {}", port); axum::serve(listener, app).await.unwrap(); } async fn register_handler( State(state): State>, Json(payload): Json, ) -> (StatusCode, Json) { info!("Registration attempt for user: {}", payload.username); // 参数校验 if let Err(e) = payload.validate() { return ( StatusCode::BAD_REQUEST, Json(RegisterResponse { success: false, user_id: None, message: format!("Validation error: {}", e), }), ); } // 检查用户名是否存在 let existing: Option<(i32,)> = sqlx::query_as("SELECT id FROM users WHERE username = $1") .bind(&payload.username) .fetch_optional(&state.db) .await .unwrap_or(None); if existing.is_some() { return ( StatusCode::CONFLICT, Json(RegisterResponse { success: false, user_id: None, message: "Username already exists".to_string(), }), ); } // 检查邮箱是否存在 let existing_email: Option<(i32,)> = sqlx::query_as("SELECT id FROM users WHERE email = $1") .bind(&payload.email) .fetch_optional(&state.db) .await .unwrap_or(None); if existing_email.is_some() { return ( StatusCode::CONFLICT, Json(RegisterResponse { success: false, user_id: None, message: "Email already exists".to_string(), }), ); } // 密码哈希 let password_hash = match hash(&payload.password, bcrypt::DEFAULT_COST) { Ok(h) => h, Err(e) => { warn!("Password hashing failed: {}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(RegisterResponse { success: false, user_id: None, message: "Internal error".to_string(), }), ); } }; // 插入用户 let result = sqlx::query_as::<_, (i32,)>( "INSERT INTO users (username, password_hash, email, created_at, updated_at) VALUES ($1, $2, $3, $4, $4) RETURNING id" ) .bind(&payload.username) .bind(&password_hash) .bind(&payload.email) .bind(Utc::now()) .fetch_one(&state.db) .await; match result { Ok((user_id,)) => { info!("User {} registered with id {}", payload.username, user_id); ( StatusCode::CREATED, Json(RegisterResponse { success: true, user_id: Some(user_id), message: "User registered successfully".to_string(), }), ) } Err(e) => { warn!("Registration failed: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(RegisterResponse { success: false, user_id: None, message: "Registration failed".to_string(), }), ) } } } async fn health_handler() -> &'static str { "OK" }