diff --git a/.env.example b/.env.example index 507cce2..5df08e8 100644 --- a/.env.example +++ b/.env.example @@ -24,7 +24,4 @@ RUST_LOG=info # ADMIN_WEB_PORT=18888 # USER_POSTGRES_PORT=20101 # USER_REDIS_PORT=20103 -# USER_LOGIN_ACCOUNT_PORT=20111 -# USER_REGISTER_ACCOUNT_PORT=20112 -# USER_LOGIN_EMAIL_PORT=20113 -# USER_REGISTER_EMAIL_PORT=20114 +# USER_SERVICE_PORT=20110 diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 1581b02..3ae50ba 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -4,7 +4,7 @@ ## 项目概述 -这是一个基于 **Rust** 的微服务后端项目,采用 **Axum + Tokio** 技术栈,使用 **Nginx** 作为 API 网关,**PostgreSQL** 作为数据库,**Redis** 作为缓存。服务以 Docker 容器形式部署,每个核心功能拆分为独立的微服务二进制文件。 +这是一个基于 **Rust** 的微服务后端项目,采用 **Axum + Tokio** 技术栈,使用 **Nginx** 作为 API 网关,**PostgreSQL** 作为数据库,**Redis** 作为缓存。服务以 Docker 容器形式部署,按 **DDD 限界上下文(Bounded Context)** 划分服务边界——一个领域对外是一个微服务,内部由 Rust 模块组织。 ## 技术栈 @@ -23,13 +23,24 @@ ``` backend/ ├── services/ # 微服务目录 -│ └── user-service/ # 用户服务(当前唯一实现的服务域) -│ ├── user-login-account/ # 账号登录服务 (port 8001) -│ ├── user-register-account/ # 账号注册服务 (port 8002) -│ ├── user-login-email/ # 邮箱登录服务 (port 8003) -│ ├── user-register-email/ # 邮箱注册服务 (port 8004) -│ ├── migrations/ # 数据库初始化 SQL -│ └── Dockerfile # 通用/遗留构建文件 +│ └── user-service/ # 用户服务(DDD 用户限界上下文,单一服务对外) +│ ├── Cargo.toml # 单 crate +│ ├── Dockerfile # 单镜像 +│ ├── migrations/ # 数据库初始化 SQL +│ │ └── 001_init.sql +│ └── src/ +│ ├── main.rs # 装配 Router、连接池、AppState +│ ├── state.rs # AppState { db, jwt_secret } +│ ├── api.rs # 通用 ApiRequest / ApiResponse +│ ├── jwt.rs # JWT Claims + generate_token +│ ├── auth/ # 认证模块 +│ │ ├── mod.rs +│ │ ├── login_account.rs +│ │ └── login_email.rs +│ └── register/ # 注册模块 +│ ├── mod.rs +│ ├── account.rs +│ └── email.rs ├── gateway/ # API 网关 │ ├── Dockerfile │ └── nginx/ @@ -51,13 +62,21 @@ backend/ ### 服务拆分原则 -每个用户功能(登录/注册)按**认证方式**拆分为独立服务: -- `user-login-account`: 账号密码登录,签发 JWT -- `user-login-email`: 邮箱密码登录,签发 JWT -- `user-register-account`: 账号注册,写入 `user_main` / `user_login_account` / `user_login_password` -- `user-register-email`: 邮箱注册,写入 `user_main` / `user_login_email` / `user_login_password` +按 **DDD 限界上下文(Bounded Context)** 划分:每个独立的业务域对外暴露为单一微服务,内部细分通过 Rust 模块和 axum Router 组合实现,**不再按操作粒度(登录/注册)拆 crate**。 -每个服务都是独立的 Rust Crate,拥有独立的 `Cargo.toml`、`src/main.rs` 和 `Dockerfile`。 +**user-service**(用户域,单一服务)对外提供: + +| 方法 | 网关路径 | 下游路径 | 用途 | +|------|---------|---------|------| +| POST | `/api/v1/auth/login/account` | `/auth/login/account` | 账号密码登录,签发 JWT | +| POST | `/api/v1/auth/login/email` | `/auth/login/email` | 邮箱密码登录,签发 JWT | +| POST | `/api/v1/users/register/account` | `/users/register/account` | 账号注册(写 user_main / user_login_account / user_login_password) | +| POST | `/api/v1/users/register/email` | `/users/register/email` | 邮箱注册(写 user_main / user_login_email / user_login_password) | +| GET | `/health` | `/health` | 健康检查 | + +> 网关 nginx 通过 `rewrite ^/api/v1(/.*)$ $1 break;` 统一去除 `/api/v1` 前缀。 + +服务内部模块化布局见 [项目结构](#项目结构):`auth/` 子模块负责登录认证,`register/` 子模块负责账号注册,共享 `state.rs` / `api.rs` / `jwt.rs`。 ### 数据库模型 @@ -157,7 +176,7 @@ OK - 使用 **Rust 2024 Edition**。 - 注释使用**中文**。 - 服务状态通过 `Arc` 注入到 Axum Handler 中。 -- 注册类服务统一使用包装请求/响应格式: +- 注册类接口统一使用包装请求/响应格式: ```rust struct ApiRequest { device: i32, language: i32, data: T } struct ApiResponse { success: bool, message: String, data: Option } @@ -220,8 +239,8 @@ docker compose up -d --build # 正式 如需仅启动后端栈(不含前端)做联调: ```bash -docker compose -f docker-compose.dev.yml up -d --build user-db user-redis \ - user-login-account user-register-account user-login-email user-register-email gateway +docker compose -f docker-compose.dev.yml up -d --build \ + user-db user-redis user-service gateway ``` ### 网关管理 @@ -239,25 +258,37 @@ docker compose -f docker-compose.dev.yml up -d --build user-db user-redis \ ./scripts/gateway.sh reload ``` -### 本地编译单个服务 +### 本地编译运行 user-service ```bash -cd services/user-service/user-login-account +cd services/user-service cargo run ``` ## 扩展指南 -### 新增微服务 +### 在已有领域内新增功能(推荐) -1. 在 `services//` 下创建新目录,如 `services/order-service/order-create/`。 -2. 编写独立的 `Cargo.toml`、`src/main.rs`、`Dockerfile`。 -3. 在 `gateway/nginx/conf.d/services/` 添加路由配置。 -4. 在 `gateway/nginx/nginx.conf` 添加 `upstream`。 -5. 如需新数据库表,在对应服务域的 `migrations/` 目录添加 SQL 文件。 +属于同一限界上下文(如新增"用户资料修改"接口)时,**不要**新建 crate 或服务,而是在现有 `user-service` 下新增模块: + +1. 在 `services/user-service/src/` 下新增模块文件,或扩展现有 `auth/` / `register/` 子模块。 +2. 在子模块的 `mod.rs` 中通过 `Router::new().route(...)` 注册新路由。 +3. 在 `gateway/nginx/conf.d/services/user-service.conf` 中追加对应 `location` 块(路径仍以 `/api/v1/...` 起始)。 +4. 如需新数据库表或字段,在 `services/user-service/migrations/` 下追加 SQL 文件。 + +### 新增服务域(新限界上下文) + +当业务边界明显独立(如订单 `order-service`、支付 `payment-service`)时再新建独立服务: + +1. 在 `services//` 下创建独立 crate(参考 `services/user-service/` 的目录布局:单 `Cargo.toml` + 单 `Dockerfile` + 模块化的 `src/`)。 +2. 在 `gateway/nginx/conf.d/services/` 添加路由配置文件。 +3. 在 `gateway/nginx/nginx.conf` 添加对应 `upstream`。 +4. 在根目录 `docker-compose.yml` / `docker-compose.dev.yml` 中追加服务定义。 +5. 在 [PORT_ALLOCATION.md](PORT_ALLOCATION.md) 申请新的百位段端口并更新分配表。 ### 共享代码提取 -当前 `shared/` 目录为空。当多个服务需要共用模型、中间件或工具函数时: +当前 `shared/` 目录为空。当多个服务域需要共用模型、中间件或工具函数时: + 1. 在 `shared/` 下创建子模块(如 `shared/models`、`shared/middleware`)。 2. 将共享 crate 以 path dependency 引入各微服务: ```toml @@ -268,6 +299,5 @@ cargo run ## 注意事项 -- `services/user-service/Dockerfile` 是一个通用构建文件,但当前各微服务使用自己的 Dockerfile。修改时请确认影响范围。 -- 当前 `shared/` 为空,Agent 在修改代码时若发现重复逻辑,可提议提取到 `shared/`。 +- 当前 `shared/` 为空,Agent 在修改代码时若发现跨服务域重复逻辑,可提议提取到 `shared/`;同一服务内部的重复逻辑直接抽到模块即可,无需走 `shared/`。 - 网关配置文件中的 `api.example.com` 为占位域名,本地开发需配置 hosts 或使用 `localhost`。 diff --git a/backend/PORT_ALLOCATION.md b/backend/PORT_ALLOCATION.md index 6e1780b..98dbe61 100644 --- a/backend/PORT_ALLOCATION.md +++ b/backend/PORT_ALLOCATION.md @@ -19,8 +19,8 @@ | 子段 | 用途 | 已分配端口 | |------|------|-----------| | `20100-20109` | 数据层 | `20101` Postgres、`20103` Redis | -| `20110-20149` | 认证/登录类 | `20111` 账号登录、`20113` 邮箱登录 | -| `20150-20189` | 注册/管理类 | `20112` 账号注册、`20114` 邮箱注册 | +| `20110-20149` | 用户业务服务 | `20110` user-service(合并后单一服务) | +| `20150-20189` | 预留扩展 | 预留 | | `20190-20199` | 预留/调试 | 预留 | ### user-service 端口明细 @@ -29,10 +29,10 @@ |--------|-----------|---------|------| | user-postgres | `20101` | `5432` | PostgreSQL | | user-redis | `20103` | `6379` | Redis 缓存 | -| user-login-account | `20111` | `8080` | 账号密码登录 | -| user-register-account | `20112` | `8080` | 账号注册 | -| user-login-email | `20113` | `8080` | 邮箱密码登录 | -| user-register-email | `20114` | `8080` | 邮箱注册 | +| user-service | `20110` | `8080` | 用户域统一服务(含账号/邮箱 登录/注册) | + +> **历史端口(已回收,请勿复用)**:`20111` `20112` `20113` `20114` +> 曾分别用于 `user-login-account` / `user-register-account` / `user-login-email` / `user-register-email` 四个独立微服务,现已合并为单一 `user-service:20110`,按 DDD 限界上下文聚合,内部由 axum Router 模块化拆分。 ## 使用方式 @@ -43,11 +43,9 @@ ## 示例 .env ```bash -# user-service/.env +# 数据层 USER_POSTGRES_PORT=20101 USER_REDIS_PORT=20103 -USER_LOGIN_ACCOUNT_PORT=20111 -USER_REGISTER_ACCOUNT_PORT=20112 -USER_LOGIN_EMAIL_PORT=20113 -USER_REGISTER_EMAIL_PORT=20114 +# 用户业务服务(合并后) +USER_SERVICE_PORT=20110 ``` diff --git a/backend/gateway/nginx/conf.d/services/user-service.conf b/backend/gateway/nginx/conf.d/services/user-service.conf index 857ffa3..500727f 100644 --- a/backend/gateway/nginx/conf.d/services/user-service.conf +++ b/backend/gateway/nginx/conf.d/services/user-service.conf @@ -1,12 +1,15 @@ # 用户服务路由 +# +# 全部接口统一打到 user_service upstream(合并后的单一服务) +# rewrite 统一去掉 /api/v1 前缀,下游按 /auth/* 和 /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; + rewrite ^/api/v1(/.*)$ $1 break; + proxy_pass http://user_service; proxy_http_version 1.1; proxy_set_header Host $host; @@ -21,8 +24,8 @@ 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; + rewrite ^/api/v1(/.*)$ $1 break; + proxy_pass http://user_service; proxy_http_version 1.1; proxy_set_header Host $host; @@ -37,8 +40,8 @@ location /api/v1/users/register/account { limit_req zone=general burst=20 nodelay; limit_conn addr 10; - rewrite ^/api/v1/users/register/account$ /register break; - proxy_pass http://user_register_account; + rewrite ^/api/v1(/.*)$ $1 break; + proxy_pass http://user_service; proxy_http_version 1.1; proxy_set_header Host $host; @@ -53,8 +56,8 @@ location /api/v1/users/register/email { limit_req zone=general burst=20 nodelay; limit_conn addr 10; - rewrite ^/api/v1/users/register/email$ /register break; - proxy_pass http://user_register_email; + rewrite ^/api/v1(/.*)$ $1 break; + proxy_pass http://user_service; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/backend/gateway/nginx/nginx.conf b/backend/gateway/nginx/nginx.conf index 09adaed..9291d56 100644 --- a/backend/gateway/nginx/nginx.conf +++ b/backend/gateway/nginx/nginx.conf @@ -45,27 +45,9 @@ http { limit_conn_zone $binary_remote_addr zone=addr:10m; # 上游服务 —— 通过 Docker 内部 DNS(服务名)访问,统一由根目录 docker-compose 编排 - upstream user_login_account { + upstream user_service { least_conn; - server user-login-account:8080 max_fails=3 fail_timeout=30s; - keepalive 32; - } - - upstream user_register_account { - least_conn; - server user-register-account:8080 max_fails=3 fail_timeout=30s; - keepalive 32; - } - - upstream user_login_email { - least_conn; - server user-login-email:8080 max_fails=3 fail_timeout=30s; - keepalive 32; - } - - upstream user_register_email { - least_conn; - server user-register-email:8080 max_fails=3 fail_timeout=30s; + server user-service:8080 max_fails=3 fail_timeout=30s; keepalive 32; } diff --git a/backend/services/user-service/user-login-account/Cargo.toml b/backend/services/user-service/Cargo.toml similarity index 81% rename from backend/services/user-service/user-login-account/Cargo.toml rename to backend/services/user-service/Cargo.toml index 47f3d1d..e06f6c4 100644 --- a/backend/services/user-service/user-login-account/Cargo.toml +++ b/backend/services/user-service/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "user-login-account" +name = "user-service" version = "0.1.0" edition = "2024" @@ -19,10 +19,10 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", # UUID uuid = { version = "1", features = ["v7", "serde"] } -# Redis +# Redis(预留:当前未使用,待引入限流/会话等场景) redis = { version = "0.29", features = ["tokio-comp"] } -# 密码哈希(bcrypt) +# 密码哈希 bcrypt = "0.17" # JWT @@ -39,6 +39,9 @@ dotenvy = "0.15" # 错误处理 thiserror = "2.0" +# 参数校验 +validator = { version = "0.20", features = ["derive"] } + [profile.release] opt-level = 3 lto = true diff --git a/backend/services/user-service/Dockerfile b/backend/services/user-service/Dockerfile index 98d52fd..e8fdb5d 100644 --- a/backend/services/user-service/Dockerfile +++ b/backend/services/user-service/Dockerfile @@ -2,18 +2,18 @@ FROM rust:1.94.1-alpine3.23 AS builder # 安装构建依赖 -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static +RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig # 创建工作目录 WORKDIR /app -# 先复制共享代码和 Cargo 文件以利用缓存 -COPY shared /app/shared +# 先复制 Cargo 文件以利用依赖缓存 COPY services/user-service/Cargo.toml services/user-service/Cargo.lock* ./ # 创建虚拟 main.rs 来缓存依赖 RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release && rm -rf src +RUN cargo build --release 2>/dev/null || true +RUN rm -rf src # 复制真实源代码 COPY services/user-service/src ./src diff --git a/backend/services/user-service/src/api.rs b/backend/services/user-service/src/api.rs new file mode 100644 index 0000000..b2d4331 --- /dev/null +++ b/backend/services/user-service/src/api.rs @@ -0,0 +1,18 @@ +// 注册/业务类接口的统一请求/响应包装格式 +// 与 backend/CLAUDE.md 中的 API 公共约定保持一致 + +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct ApiRequest { + pub device: i32, + pub language: i32, + pub data: T, +} + +#[derive(Serialize)] +pub struct ApiResponse { + pub success: bool, + pub message: String, + pub data: Option, +} diff --git a/backend/services/user-service/src/auth/login_account.rs b/backend/services/user-service/src/auth/login_account.rs new file mode 100644 index 0000000..8f0bdc2 --- /dev/null +++ b/backend/services/user-service/src/auth/login_account.rs @@ -0,0 +1,88 @@ +// 账号密码登录 +// 验证 user_login_account.account + user_login_password.password,签发 JWT + +use axum::{Json, extract::State, http::StatusCode}; +use bcrypt::verify; +use serde::Deserialize; +use std::sync::Arc; +use tracing::{info, warn}; + +use crate::jwt::generate_token; +use crate::state::AppState; + +use super::LoginResponse; + +#[derive(Deserialize)] +pub struct LoginRequest { + username: String, + password: String, +} + +pub async fn handle( + State(state): State>, + Json(payload): Json, +) -> (StatusCode, Json) { + info!("Login attempt for user: {}", payload.username); + + // 查询用户账号与密码 + let user: Option<(uuid::Uuid, String)> = sqlx::query_as( + "SELECT a.user_id, p.password \ + FROM user_login_account a \ + JOIN user_login_password p ON a.user_id = p.user_id \ + WHERE a.account = $1 AND a.deleted = FALSE AND p.deleted = FALSE", + ) + .bind(&payload.username) + .fetch_optional(&state.db) + .await + .unwrap_or(None); + + match user { + Some((user_id, password_hash)) => match verify(&payload.password, &password_hash) { + Ok(true) => { + info!("User {} logged in successfully", payload.username); + let token = generate_token(&user_id.to_string(), &state.jwt_secret); + ( + StatusCode::OK, + Json(LoginResponse { + success: true, + token: Some(token), + message: "Login successful".to_string(), + }), + ) + } + Ok(false) => { + warn!("Invalid password for user {}", payload.username); + ( + StatusCode::UNAUTHORIZED, + Json(LoginResponse { + success: false, + token: None, + message: "Invalid credentials".to_string(), + }), + ) + } + Err(e) => { + warn!("Password verification error: {:?}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(LoginResponse { + success: false, + token: None, + message: "Internal error".to_string(), + }), + ) + } + }, + None => { + warn!("User not found: {}", payload.username); + ( + StatusCode::UNAUTHORIZED, + Json(LoginResponse { + success: false, + token: None, + message: "Invalid credentials".to_string(), + }), + ) + } + } +} diff --git a/backend/services/user-service/src/auth/login_email.rs b/backend/services/user-service/src/auth/login_email.rs new file mode 100644 index 0000000..9b7cb46 --- /dev/null +++ b/backend/services/user-service/src/auth/login_email.rs @@ -0,0 +1,88 @@ +// 邮箱密码登录 +// 验证 user_login_email.email + user_login_password.password,签发 JWT + +use axum::{Json, extract::State, http::StatusCode}; +use bcrypt::verify; +use serde::Deserialize; +use std::sync::Arc; +use tracing::{info, warn}; + +use crate::jwt::generate_token; +use crate::state::AppState; + +use super::LoginResponse; + +#[derive(Deserialize)] +pub struct LoginRequest { + email: String, + password: String, +} + +pub async fn handle( + State(state): State>, + Json(payload): Json, +) -> (StatusCode, Json) { + info!("Login attempt for email: {}", payload.email); + + // 查询用户邮箱与密码 + let user: Option<(uuid::Uuid, String)> = sqlx::query_as( + "SELECT e.user_id, p.password \ + FROM user_login_email e \ + JOIN user_login_password p ON e.user_id = p.user_id \ + WHERE e.email = $1 AND e.deleted = FALSE AND p.deleted = FALSE", + ) + .bind(&payload.email) + .fetch_optional(&state.db) + .await + .unwrap_or(None); + + match user { + Some((user_id, password_hash)) => match verify(&payload.password, &password_hash) { + Ok(true) => { + info!("Email {} logged in successfully", payload.email); + let token = generate_token(&user_id.to_string(), &state.jwt_secret); + ( + StatusCode::OK, + Json(LoginResponse { + success: true, + token: Some(token), + message: "Login successful".to_string(), + }), + ) + } + Ok(false) => { + warn!("Invalid password for email {}", payload.email); + ( + StatusCode::UNAUTHORIZED, + Json(LoginResponse { + success: false, + token: None, + message: "Invalid credentials".to_string(), + }), + ) + } + Err(e) => { + warn!("Password verification error: {:?}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(LoginResponse { + success: false, + token: None, + message: "Internal error".to_string(), + }), + ) + } + }, + None => { + warn!("Email not found: {}", payload.email); + ( + StatusCode::UNAUTHORIZED, + Json(LoginResponse { + success: false, + token: None, + message: "Invalid credentials".to_string(), + }), + ) + } + } +} diff --git a/backend/services/user-service/src/auth/mod.rs b/backend/services/user-service/src/auth/mod.rs new file mode 100644 index 0000000..9716fd6 --- /dev/null +++ b/backend/services/user-service/src/auth/mod.rs @@ -0,0 +1,25 @@ +// auth 模块:登录/认证相关接口 +// 路由:/auth/login/account, /auth/login/email + +use axum::{Router, routing::post}; +use serde::Serialize; +use std::sync::Arc; + +use crate::state::AppState; + +mod login_account; +mod login_email; + +// 登录/认证类接口扁平响应(与前端约定对齐) +#[derive(Serialize)] +pub struct LoginResponse { + pub success: bool, + pub token: Option, + pub message: String, +} + +pub fn router() -> Router> { + Router::new() + .route("/auth/login/account", post(login_account::handle)) + .route("/auth/login/email", post(login_email::handle)) +} diff --git a/backend/services/user-service/src/jwt.rs b/backend/services/user-service/src/jwt.rs new file mode 100644 index 0000000..78579d7 --- /dev/null +++ b/backend/services/user-service/src/jwt.rs @@ -0,0 +1,31 @@ +// JWT Claims 定义与签发 +// 当前账号登录、邮箱登录共用,未来登出/刷新等也走这里 + +use chrono::{Duration, Utc}; +use jsonwebtoken::{EncodingKey, Header, encode}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: usize, + pub iat: usize, +} + +pub fn generate_token(sub: &str, secret: &str) -> String { + let now = Utc::now(); + let exp = now + Duration::days(7); + + let claims = Claims { + sub: sub.to_string(), + iat: now.timestamp() as usize, + exp: exp.timestamp() as usize, + }; + + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret.as_bytes()), + ) + .unwrap() +} diff --git a/backend/services/user-service/src/main.rs b/backend/services/user-service/src/main.rs new file mode 100644 index 0000000..c375259 --- /dev/null +++ b/backend/services/user-service/src/main.rs @@ -0,0 +1,63 @@ +// user-service 装配入口 +// 合并旧 4 个微服务(user-login-account / user-register-account / user-login-email / user-register-email) + +use axum::{Router, routing::get}; +use std::env; +use std::sync::Arc; +use tracing::info; + +mod api; +mod auth; +mod jwt; +mod register; +mod state; + +use state::AppState; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + info!("Starting user-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"); + + sqlx::query("SET TIME ZONE 'Asia/Shanghai'") + .execute(&pool) + .await + .expect("Failed to set timezone"); + + info!("Database connected"); + + // JWT 密钥 + let jwt_secret = env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret".to_string()); + + let state = Arc::new(AppState { + db: pool, + jwt_secret, + }); + + // 路由:合并 auth + register 子路由 + 健康检查 + let app = Router::new() + .merge(auth::router()) + .merge(register::router()) + .route("/health", 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-service listening on port {}", port); + + axum::serve(listener, app).await.unwrap(); +} + +async fn health_handler() -> &'static str { + "OK" +} diff --git a/backend/services/user-service/user-register-account/src/main.rs b/backend/services/user-service/src/register/account.rs similarity index 70% rename from backend/services/user-service/user-register-account/src/main.rs rename to backend/services/user-service/src/register/account.rs index a7905b2..dfe43d2 100644 --- a/backend/services/user-service/user-register-account/src/main.rs +++ b/backend/services/user-service/src/register/account.rs @@ -1,88 +1,29 @@ -use axum::{ - extract::State, - http::StatusCode, - response::Json, - routing::{get, post}, - Router, -}; +// 账号注册 +// 写入 user_main / user_login_account / user_login_password 三表事务 + +use axum::{Json, extract::State, http::StatusCode}; use bcrypt::hash; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use std::env; -use std::sync::Arc; use chrono::Utc; +use serde::Deserialize; +use std::sync::Arc; use tracing::{info, warn}; use uuid::Uuid; use validator::Validate; -#[derive(Clone)] -struct AppState { - db: PgPool, -} +use crate::api::{ApiRequest, ApiResponse}; +use crate::state::AppState; + +use super::RegisterData; #[derive(Deserialize, Validate)] -struct RegisterRequest { +pub struct RegisterRequest { #[validate(length(min = 3, max = 50))] username: String, #[validate(length(min = 6))] password: String, } -#[derive(Deserialize)] -struct ApiRequest { - device: i32, - language: i32, - data: T, -} - -#[derive(Serialize)] -struct ApiResponse { - success: bool, - message: String, - data: Option, -} - -#[derive(Serialize)] -struct RegisterData { - user_id: Uuid, -} - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - info!("Starting user-register-account 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"); - - sqlx::query("SET TIME ZONE 'Asia/Shanghai'") - .execute(&pool) - .await - .expect("Failed to set timezone"); - - info!("Database connected"); - - let state = Arc::new(AppState { db: pool }); - - let app = Router::new() - .route("/register", post(register_handler)) - .route("/health", 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( +pub async fn handle( State(state): State>, Json(req): Json>, ) -> (StatusCode, Json>) { @@ -105,12 +46,12 @@ async fn register_handler( // 检查账号是否已存在 let existing: Option<(Uuid,)> = sqlx::query_as( - "SELECT id FROM user_login_account WHERE account = $1 AND deleted = FALSE" + "SELECT id FROM user_login_account WHERE account = $1 AND deleted = FALSE", ) - .bind(&req.data.username) - .fetch_optional(&state.db) - .await - .unwrap_or(None); + .bind(&req.data.username) + .fetch_optional(&state.db) + .await + .unwrap_or(None); if existing.is_some() { return ( @@ -159,7 +100,7 @@ async fn register_handler( let user_id = Uuid::now_v7(); if let Err(e) = sqlx::query( - "INSERT INTO user_main (id, create_date, modify_date) VALUES ($1, $2, $3)" + "INSERT INTO user_main (id, create_date, modify_date) VALUES ($1, $2, $3)", ) .bind(user_id) .bind(now) @@ -181,7 +122,7 @@ async fn register_handler( let account_id = Uuid::now_v7(); if let Err(e) = sqlx::query( - "INSERT INTO user_login_account (id, user_id, account, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)" + "INSERT INTO user_login_account (id, user_id, account, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)", ) .bind(account_id) .bind(user_id) @@ -205,7 +146,7 @@ async fn register_handler( let password_id = Uuid::now_v7(); if let Err(e) = sqlx::query( - "INSERT INTO user_login_password (id, user_id, password, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)" + "INSERT INTO user_login_password (id, user_id, password, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)", ) .bind(password_id) .bind(user_id) @@ -252,14 +193,3 @@ async fn register_handler( } } } - -async fn health_handler() -> (StatusCode, Json>) { - ( - StatusCode::OK, - Json(ApiResponse { - success: true, - message: "OK".to_string(), - data: None, - }), - ) -} diff --git a/backend/services/user-service/user-register-email/src/main.rs b/backend/services/user-service/src/register/email.rs similarity index 70% rename from backend/services/user-service/user-register-email/src/main.rs rename to backend/services/user-service/src/register/email.rs index 055a2d7..4ed0706 100644 --- a/backend/services/user-service/user-register-email/src/main.rs +++ b/backend/services/user-service/src/register/email.rs @@ -1,88 +1,29 @@ -use axum::{ - extract::State, - http::StatusCode, - response::Json, - routing::{get, post}, - Router, -}; +// 邮箱注册 +// 写入 user_main / user_login_email / user_login_password 三表事务 + +use axum::{Json, extract::State, http::StatusCode}; use bcrypt::hash; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use std::env; -use std::sync::Arc; use chrono::Utc; +use serde::Deserialize; +use std::sync::Arc; use tracing::{info, warn}; use uuid::Uuid; use validator::Validate; -#[derive(Clone)] -struct AppState { - db: PgPool, -} +use crate::api::{ApiRequest, ApiResponse}; +use crate::state::AppState; + +use super::RegisterData; #[derive(Deserialize, Validate)] -struct RegisterRequest { +pub struct RegisterRequest { #[validate(email)] email: String, #[validate(length(min = 6))] password: String, } -#[derive(Deserialize)] -struct ApiRequest { - device: i32, - language: i32, - data: T, -} - -#[derive(Serialize)] -struct ApiResponse { - success: bool, - message: String, - data: Option, -} - -#[derive(Serialize)] -struct RegisterData { - user_id: Uuid, -} - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - info!("Starting user-register-email 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"); - - sqlx::query("SET TIME ZONE 'Asia/Shanghai'") - .execute(&pool) - .await - .expect("Failed to set timezone"); - - info!("Database connected"); - - let state = Arc::new(AppState { db: pool }); - - let app = Router::new() - .route("/register", post(register_handler)) - .route("/health", 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-email service listening on port {}", port); - - axum::serve(listener, app).await.unwrap(); -} - -async fn register_handler( +pub async fn handle( State(state): State>, Json(req): Json>, ) -> (StatusCode, Json>) { @@ -105,12 +46,12 @@ async fn register_handler( // 检查邮箱是否已存在 let existing: Option<(Uuid,)> = sqlx::query_as( - "SELECT id FROM user_login_email WHERE email = $1 AND deleted = FALSE" + "SELECT id FROM user_login_email WHERE email = $1 AND deleted = FALSE", ) - .bind(&req.data.email) - .fetch_optional(&state.db) - .await - .unwrap_or(None); + .bind(&req.data.email) + .fetch_optional(&state.db) + .await + .unwrap_or(None); if existing.is_some() { return ( @@ -159,7 +100,7 @@ async fn register_handler( let user_id = Uuid::now_v7(); if let Err(e) = sqlx::query( - "INSERT INTO user_main (id, create_date, modify_date) VALUES ($1, $2, $3)" + "INSERT INTO user_main (id, create_date, modify_date) VALUES ($1, $2, $3)", ) .bind(user_id) .bind(now) @@ -181,7 +122,7 @@ async fn register_handler( let email_id = Uuid::now_v7(); if let Err(e) = sqlx::query( - "INSERT INTO user_login_email (id, user_id, email, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)" + "INSERT INTO user_login_email (id, user_id, email, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)", ) .bind(email_id) .bind(user_id) @@ -205,7 +146,7 @@ async fn register_handler( let password_id = Uuid::now_v7(); if let Err(e) = sqlx::query( - "INSERT INTO user_login_password (id, user_id, password, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)" + "INSERT INTO user_login_password (id, user_id, password, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)", ) .bind(password_id) .bind(user_id) @@ -252,14 +193,3 @@ async fn register_handler( } } } - -async fn health_handler() -> (StatusCode, Json>) { - ( - StatusCode::OK, - Json(ApiResponse { - success: true, - message: "OK".to_string(), - data: None, - }), - ) -} diff --git a/backend/services/user-service/src/register/mod.rs b/backend/services/user-service/src/register/mod.rs new file mode 100644 index 0000000..c955ab1 --- /dev/null +++ b/backend/services/user-service/src/register/mod.rs @@ -0,0 +1,24 @@ +// register 模块:账号/邮箱注册接口 +// 路由:/users/register/account, /users/register/email + +use axum::{Router, routing::post}; +use serde::Serialize; +use std::sync::Arc; +use uuid::Uuid; + +use crate::state::AppState; + +mod account; +mod email; + +// 注册成功返回的业务数据(账号/邮箱共用) +#[derive(Serialize)] +pub struct RegisterData { + pub user_id: Uuid, +} + +pub fn router() -> Router> { + Router::new() + .route("/users/register/account", post(account::handle)) + .route("/users/register/email", post(email::handle)) +} diff --git a/backend/services/user-service/src/state.rs b/backend/services/user-service/src/state.rs new file mode 100644 index 0000000..d861da7 --- /dev/null +++ b/backend/services/user-service/src/state.rs @@ -0,0 +1,10 @@ +// 应用全局状态:数据库连接池 + JWT 密钥 +// 由 main.rs 在启动时构造,通过 Arc 注入到各 handler + +use sqlx::PgPool; + +#[derive(Clone)] +pub struct AppState { + pub db: PgPool, + pub jwt_secret: String, +} diff --git a/backend/services/user-service/user-login-account/Dockerfile b/backend/services/user-service/user-login-account/Dockerfile deleted file mode 100644 index 9b1434d..0000000 --- a/backend/services/user-service/user-login-account/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# 构建阶段 -FROM rust:1.94.1-alpine3.23 AS builder - -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig - -WORKDIR /app - -# 复制 user-login-account 代码 -COPY services/user-service/user-login-account/Cargo.toml services/user-service/user-login-account/Cargo.lock* ./ - -# 缓存依赖 -RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release 2>/dev/null || true -RUN rm -rf src - -# 复制真实源码 -COPY services/user-service/user-login-account/src ./src - -# 重新构建 -RUN touch src/main.rs && cargo build --release - -# 运行阶段 -FROM alpine:3.23 AS runtime - -RUN apk add --no-cache ca-certificates - -RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser - -WORKDIR /app - -COPY --from=builder /app/target/release/user-login-account /app/user-login-account - -RUN chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -CMD ["./user-login-account"] diff --git a/backend/services/user-service/user-login-account/src/main.rs b/backend/services/user-service/user-login-account/src/main.rs deleted file mode 100644 index d16f9f3..0000000 --- a/backend/services/user-service/user-login-account/src/main.rs +++ /dev/null @@ -1,190 +0,0 @@ -use axum::{ - extract::State, - http::StatusCode, - response::Json, - routing::post, - Router, -}; -use bcrypt::verify; -use chrono::{Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey, Header}; -use serde::{Deserialize, Serialize}; -use sqlx::{Pool, Postgres}; -use std::env; -use std::sync::Arc; -use tracing::{info, warn}; - -// 应用状态 -#[derive(Clone)] -struct AppState { - db: Pool, - jwt_secret: String, -} - -// 登录请求 -#[derive(Deserialize)] -struct LoginRequest { - username: String, - password: String, -} - -// 登录/认证类接口扁平响应(与前端约定对齐) -#[derive(Serialize)] -struct LoginResponse { - success: bool, - token: Option, - message: String, -} - -// JWT Claims -#[derive(Serialize, Deserialize)] -struct Claims { - sub: String, - exp: usize, - iat: usize, -} - -#[tokio::main] -async fn main() { - // 初始化日志 - tracing_subscriber::fmt::init(); - - info!("Starting user-login-account 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"); - - sqlx::query("SET TIME ZONE 'Asia/Shanghai'") - .execute(&pool) - .await - .expect("Failed to set timezone"); - - info!("Database connected"); - - // JWT 密钥 - let jwt_secret = env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret".to_string()); - - let state = Arc::new(AppState { - db: pool, - jwt_secret, - }); - - // 路由 - let app = Router::new() - .route("/login", post(login_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-login service listening on port {}", port); - - axum::serve(listener, app).await.unwrap(); -} - -// 登录处理 -async fn login_handler( - State(state): State>, - Json(payload): Json, -) -> (StatusCode, Json) { - info!("Login attempt for user: {}", payload.username); - - // 查询用户账号与密码 - let user: Option<(uuid::Uuid, String)> = sqlx::query_as( - "SELECT a.user_id, p.password \ - FROM user_login_account a \ - JOIN user_login_password p ON a.user_id = p.user_id \ - WHERE a.account = $1 AND a.deleted = FALSE AND p.deleted = FALSE" - ) - .bind(&payload.username) - .fetch_optional(&state.db) - .await - .unwrap_or(None); - - match user { - Some((user_id, password_hash)) => { - // 验证密码 - tracing::debug!("Verifying password: input_len={}, hash_len={}", payload.password.len(), password_hash.len()); - match verify(&payload.password, &password_hash) { - Ok(true) => { - info!("User {} logged in successfully", payload.username); - - // 生成 JWT - let token = generate_token(&user_id.to_string(), &state.jwt_secret); - - ( - StatusCode::OK, - Json(LoginResponse { - success: true, - token: Some(token), - message: "Login successful".to_string(), - }), - ) - } - Ok(false) => { - warn!("Invalid password for user {}", payload.username); - ( - StatusCode::UNAUTHORIZED, - Json(LoginResponse { - success: false, - token: None, - message: "Invalid credentials".to_string(), - }), - ) - } - Err(e) => { - warn!("Password verification error: {:?}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(LoginResponse { - success: false, - token: None, - message: "Internal error".to_string(), - }), - ) - } - } - } - None => { - warn!("User not found: {}", payload.username); - ( - StatusCode::UNAUTHORIZED, - Json(LoginResponse { - success: false, - token: None, - message: "Invalid credentials".to_string(), - }), - ) - } - } -} - -// 健康检查 -async fn health_handler() -> &'static str { - "OK" -} - -// 生成 JWT Token -fn generate_token(sub: &str, secret: &str) -> String { - let now = Utc::now(); - let exp = now + Duration::days(7); - - let claims = Claims { - sub: sub.to_string(), - iat: now.timestamp() as usize, - exp: exp.timestamp() as usize, - }; - - encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(secret.as_bytes()), - ) - .unwrap() -} diff --git a/backend/services/user-service/user-login-email/Cargo.toml b/backend/services/user-service/user-login-email/Cargo.toml deleted file mode 100644 index d3be127..0000000 --- a/backend/services/user-service/user-login-email/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "user-login-email" -version = "0.1.0" -edition = "2024" - -[dependencies] -# Web 框架 -axum = "0.8" -tokio = { version = "1", features = ["full"] } -tower = "0.5" - -# 序列化 -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -# 数据库 -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "uuid"] } - -# UUID -uuid = { version = "1", features = ["v7", "serde"] } - -# Redis -redis = { version = "0.29", features = ["tokio-comp"] } - -# 密码哈希(bcrypt) -bcrypt = "0.17" - -# JWT -jsonwebtoken = "9.3" - -# 时间和日志 -chrono = { version = "0.4", features = ["serde"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -# 环境变量 -dotenvy = "0.15" - -# 错误处理 -thiserror = "2.0" - -[profile.release] -opt-level = 3 -lto = true -strip = true diff --git a/backend/services/user-service/user-login-email/Dockerfile b/backend/services/user-service/user-login-email/Dockerfile deleted file mode 100644 index 79d9632..0000000 --- a/backend/services/user-service/user-login-email/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# 构建阶段 -FROM rust:1.94.1-alpine3.23 AS builder - -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig - -WORKDIR /app - -# 复制 user-login-email 代码 -COPY services/user-service/user-login-email/Cargo.toml services/user-service/user-login-email/Cargo.lock* ./ - -# 缓存依赖 -RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release 2>/dev/null || true -RUN rm -rf src - -# 复制真实源码 -COPY services/user-service/user-login-email/src ./src - -# 重新构建 -RUN touch src/main.rs && cargo build --release - -# 运行阶段 -FROM alpine:3.23 AS runtime - -RUN apk add --no-cache ca-certificates - -RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser - -WORKDIR /app - -COPY --from=builder /app/target/release/user-login-email /app/user-login-email - -RUN chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -CMD ["./user-login-email"] diff --git a/backend/services/user-service/user-login-email/src/main.rs b/backend/services/user-service/user-login-email/src/main.rs deleted file mode 100644 index 871f9f2..0000000 --- a/backend/services/user-service/user-login-email/src/main.rs +++ /dev/null @@ -1,190 +0,0 @@ -use axum::{ - extract::State, - http::StatusCode, - response::Json, - routing::post, - Router, -}; -use bcrypt::verify; -use chrono::{Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey, Header}; -use serde::{Deserialize, Serialize}; -use sqlx::{Pool, Postgres}; -use std::env; -use std::sync::Arc; -use tracing::{info, warn}; - -// 应用状态 -#[derive(Clone)] -struct AppState { - db: Pool, - jwt_secret: String, -} - -// 登录请求 -#[derive(Deserialize)] -struct LoginRequest { - email: String, - password: String, -} - -// 登录/认证类接口扁平响应(与前端约定对齐) -#[derive(Serialize)] -struct LoginResponse { - success: bool, - token: Option, - message: String, -} - -// JWT Claims -#[derive(Serialize, Deserialize)] -struct Claims { - sub: String, - exp: usize, - iat: usize, -} - -#[tokio::main] -async fn main() { - // 初始化日志 - tracing_subscriber::fmt::init(); - - info!("Starting user-login-email 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"); - - sqlx::query("SET TIME ZONE 'Asia/Shanghai'") - .execute(&pool) - .await - .expect("Failed to set timezone"); - - info!("Database connected"); - - // JWT 密钥 - let jwt_secret = env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret".to_string()); - - let state = Arc::new(AppState { - db: pool, - jwt_secret, - }); - - // 路由 - let app = Router::new() - .route("/login", post(login_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-login-email service listening on port {}", port); - - axum::serve(listener, app).await.unwrap(); -} - -// 登录处理 -async fn login_handler( - State(state): State>, - Json(payload): Json, -) -> (StatusCode, Json) { - info!("Login attempt for email: {}", payload.email); - - // 查询用户邮箱与密码 - let user: Option<(uuid::Uuid, String)> = sqlx::query_as( - "SELECT e.user_id, p.password \ - FROM user_login_email e \ - JOIN user_login_password p ON e.user_id = p.user_id \ - WHERE e.email = $1 AND e.deleted = FALSE AND p.deleted = FALSE" - ) - .bind(&payload.email) - .fetch_optional(&state.db) - .await - .unwrap_or(None); - - match user { - Some((user_id, password_hash)) => { - // 验证密码 - tracing::debug!("Verifying password: input_len={}, hash_len={}", payload.password.len(), password_hash.len()); - match verify(&payload.password, &password_hash) { - Ok(true) => { - info!("Email {} logged in successfully", payload.email); - - // 生成 JWT - let token = generate_token(&user_id.to_string(), &state.jwt_secret); - - ( - StatusCode::OK, - Json(LoginResponse { - success: true, - token: Some(token), - message: "Login successful".to_string(), - }), - ) - } - Ok(false) => { - warn!("Invalid password for email {}", payload.email); - ( - StatusCode::UNAUTHORIZED, - Json(LoginResponse { - success: false, - token: None, - message: "Invalid credentials".to_string(), - }), - ) - } - Err(e) => { - warn!("Password verification error: {:?}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(LoginResponse { - success: false, - token: None, - message: "Internal error".to_string(), - }), - ) - } - } - } - None => { - warn!("Email not found: {}", payload.email); - ( - StatusCode::UNAUTHORIZED, - Json(LoginResponse { - success: false, - token: None, - message: "Invalid credentials".to_string(), - }), - ) - } - } -} - -// 健康检查 -async fn health_handler() -> &'static str { - "OK" -} - -// 生成 JWT Token -fn generate_token(sub: &str, secret: &str) -> String { - let now = Utc::now(); - let exp = now + Duration::days(7); - - let claims = Claims { - sub: sub.to_string(), - iat: now.timestamp() as usize, - exp: exp.timestamp() as usize, - }; - - encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(secret.as_bytes()), - ) - .unwrap() -} diff --git a/backend/services/user-service/user-register-account/Cargo.toml b/backend/services/user-service/user-register-account/Cargo.toml deleted file mode 100644 index 65df885..0000000 --- a/backend/services/user-service/user-register-account/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "user-register-account" -version = "0.1.0" -edition = "2024" - -[dependencies] -axum = "0.8" -tokio = { version = "1", features = ["full"] } -tower = "0.5" - -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "uuid"] } - -# UUID -uuid = { version = "1", features = ["v7", "serde"] } -redis = { version = "0.29", features = ["tokio-comp"] } - -bcrypt = "0.17" - -chrono = { version = "0.4", features = ["serde"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -dotenvy = "0.15" -thiserror = "2.0" -validator = { version = "0.20", features = ["derive"] } - -[profile.release] -opt-level = 3 -lto = true -strip = true diff --git a/backend/services/user-service/user-register-account/Dockerfile b/backend/services/user-service/user-register-account/Dockerfile deleted file mode 100644 index fde8928..0000000 --- a/backend/services/user-service/user-register-account/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM rust:1.94.1-alpine3.23 AS builder - -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig - -WORKDIR /app - -COPY services/user-service/user-register-account/Cargo.toml services/user-service/user-register-account/Cargo.lock* ./ - -RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release 2>/dev/null || true -RUN rm -rf src - -COPY services/user-service/user-register-account/src ./src - -RUN touch src/main.rs && cargo build --release - -FROM alpine:3.23 AS runtime - -RUN apk add --no-cache ca-certificates - -RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser - -WORKDIR /app - -COPY --from=builder /app/target/release/user-register-account /app/user-register-account - -RUN chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -CMD ["./user-register-account"] diff --git a/backend/services/user-service/user-register-email/Cargo.toml b/backend/services/user-service/user-register-email/Cargo.toml deleted file mode 100644 index 4e37e10..0000000 --- a/backend/services/user-service/user-register-email/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "user-register-email" -version = "0.1.0" -edition = "2024" - -[dependencies] -axum = "0.8" -tokio = { version = "1", features = ["full"] } -tower = "0.5" - -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "uuid"] } - -# UUID -uuid = { version = "1", features = ["v7", "serde"] } -redis = { version = "0.29", features = ["tokio-comp"] } - -bcrypt = "0.17" - -chrono = { version = "0.4", features = ["serde"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -dotenvy = "0.15" -thiserror = "2.0" -validator = { version = "0.20", features = ["derive"] } - -[profile.release] -opt-level = 3 -lto = true -strip = true diff --git a/backend/services/user-service/user-register-email/Dockerfile b/backend/services/user-service/user-register-email/Dockerfile deleted file mode 100644 index 8a24809..0000000 --- a/backend/services/user-service/user-register-email/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM rust:1.94.1-alpine3.23 AS builder - -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig - -WORKDIR /app - -COPY services/user-service/user-register-email/Cargo.toml services/user-service/user-register-email/Cargo.lock* ./ - -RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release 2>/dev/null || true -RUN rm -rf src - -COPY services/user-service/user-register-email/src ./src - -RUN touch src/main.rs && cargo build --release - -FROM alpine:3.23 AS runtime - -RUN apk add --no-cache ca-certificates - -RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser - -WORKDIR /app - -COPY --from=builder /app/target/release/user-register-email /app/user-register-email - -RUN chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -CMD ["./user-register-email"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6517b4b..ce2f523 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -51,87 +51,20 @@ services: retries: 5 # ============ 用户微服务 ============ - user-login-account: + user-service: build: context: ./backend - dockerfile: services/user-service/user-login-account/Dockerfile - container_name: user-login-account-dev + dockerfile: services/user-service/Dockerfile + container_name: user-service-dev environment: - RUST_LOG=${RUST_LOG:-debug} - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@user-db:5432/user-db - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-login-account + - SERVICE_NAME=user-service - SERVICE_PORT=8080 - JWT_SECRET=${JWT_SECRET:-dev-secret-key} ports: - - "${USER_LOGIN_ACCOUNT_PORT:-20111}:8080" - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - asset-helper-dev - restart: unless-stopped - - user-register-account: - build: - context: ./backend - dockerfile: services/user-service/user-register-account/Dockerfile - container_name: user-register-account-dev - environment: - - RUST_LOG=${RUST_LOG:-debug} - - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-register-account - - SERVICE_PORT=8080 - ports: - - "${USER_REGISTER_ACCOUNT_PORT:-20112}:8080" - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - asset-helper-dev - restart: unless-stopped - - user-login-email: - build: - context: ./backend - dockerfile: services/user-service/user-login-email/Dockerfile - container_name: user-login-email-dev - environment: - - RUST_LOG=${RUST_LOG:-debug} - - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-login-email - - SERVICE_PORT=8080 - - JWT_SECRET=${JWT_SECRET:-dev-secret-key} - ports: - - "${USER_LOGIN_EMAIL_PORT:-20113}:8080" - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - asset-helper-dev - restart: unless-stopped - - user-register-email: - build: - context: ./backend - dockerfile: services/user-service/user-register-email/Dockerfile - container_name: user-register-email-dev - environment: - - RUST_LOG=${RUST_LOG:-debug} - - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-register-email - - SERVICE_PORT=8080 - ports: - - "${USER_REGISTER_EMAIL_PORT:-20114}:8080" + - "${USER_SERVICE_PORT:-20110}:8080" depends_on: user-db: condition: service_healthy @@ -151,10 +84,7 @@ services: - "${GATEWAY_HTTP_PORT:-18080}:80" - "${GATEWAY_HTTPS_PORT:-18443}:443" depends_on: - - user-login-account - - user-register-account - - user-login-email - - user-register-email + - user-service networks: - asset-helper-dev restart: unless-stopped diff --git a/docker-compose.yml b/docker-compose.yml index bda3985..e59149e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,16 +47,16 @@ services: retries: 5 # ============ 用户微服务 ============ - user-login-account: + user-service: build: context: ./backend - dockerfile: services/user-service/user-login-account/Dockerfile - container_name: user-login-account + dockerfile: services/user-service/Dockerfile + container_name: user-service environment: - RUST_LOG=${RUST_LOG:-info} - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@user-db:5432/user-db - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-login-account + - SERVICE_NAME=user-service - SERVICE_PORT=8080 - JWT_SECRET=${JWT_SECRET:?need JWT_SECRET in .env} depends_on: @@ -68,67 +68,6 @@ services: - asset-helper restart: unless-stopped - user-register-account: - build: - context: ./backend - dockerfile: services/user-service/user-register-account/Dockerfile - container_name: user-register-account - environment: - - RUST_LOG=${RUST_LOG:-info} - - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-register-account - - SERVICE_PORT=8080 - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - asset-helper - restart: unless-stopped - - user-login-email: - build: - context: ./backend - dockerfile: services/user-service/user-login-email/Dockerfile - container_name: user-login-email - environment: - - RUST_LOG=${RUST_LOG:-info} - - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-login-email - - SERVICE_PORT=8080 - - JWT_SECRET=${JWT_SECRET} - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - asset-helper - restart: unless-stopped - - user-register-email: - build: - context: ./backend - dockerfile: services/user-service/user-register-email/Dockerfile - container_name: user-register-email - environment: - - RUST_LOG=${RUST_LOG:-info} - - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-register-email - - SERVICE_PORT=8080 - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - asset-helper - restart: unless-stopped - # ============ API 网关 ============ gateway: build: @@ -139,10 +78,7 @@ services: - "${GATEWAY_HTTP_PORT:-80}:80" - "${GATEWAY_HTTPS_PORT:-443}:443" depends_on: - - user-login-account - - user-register-account - - user-login-email - - user-register-email + - user-service networks: - asset-helper restart: unless-stopped