From e6a9145cd4d3eded0f4fb1ea9a327ffab613739a Mon Sep 17 00:00:00 2001 From: ayou <550244300@qq.com> Date: Fri, 29 Aug 2025 23:37:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=92=8C=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新后端端口号和环境配置,添加Redis支持 - 改进错误处理,添加带消息的未授权和禁止访问错误 - 优化前端登录流程和错误提示 - 更新前端页面标题和欢迎信息 - 清理未使用的代码模块 --- backend/.env | 2 +- backend/.env.example | 28 ++++++++++---------------- backend/.env.prod | 7 ++++++- backend/migration/Cargo.toml | 2 +- backend/src/error.rs | 4 ++++ backend/src/main.rs | 26 +++++++++++++++++------- backend/src/services/auth_service.rs | 17 +++++++++++----- frontend/src/pages/Dashboard.tsx | 4 ++-- frontend/src/pages/Login.tsx | 15 +++++++------- frontend/src/utils/axios.ts | 30 ++++++++++++++++++++++++++-- frontend/src/utils/config.ts | 4 ++-- 11 files changed, 93 insertions(+), 46 deletions(-) diff --git a/backend/.env b/backend/.env index a401836..b248e85 100644 --- a/backend/.env +++ b/backend/.env @@ -1,7 +1,7 @@ RUST_LOG=info,udmin=debug APP_ENV=development APP_HOST=0.0.0.0 -APP_PORT=9891 +APP_PORT=9898 DB_URL=mysql://root:123456@127.0.0.1:3306/udmin JWT_SECRET=dev_secret_change_me JWT_ISS=udmin diff --git a/backend/.env.example b/backend/.env.example index 418a5af..b248e85 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,22 +1,14 @@ -# 数据库配置 -DATABASE_URL=sqlite://udmin.db?mode=rwc - -# JWT配置 -JWT_SECRET=your-super-secret-jwt-key-change-this-in-production +RUST_LOG=info,udmin=debug +APP_ENV=development +APP_HOST=0.0.0.0 +APP_PORT=9898 +DB_URL=mysql://root:123456@127.0.0.1:3306/udmin +JWT_SECRET=dev_secret_change_me JWT_ISS=udmin -JWT_ACCESS_EXP_SECS=1800 # 30分钟 -JWT_REFRESH_EXP_SECS=1209600 # 14天 +JWT_ACCESS_EXP_SECS=1800 +JWT_REFRESH_EXP_SECS=1209600 +CORS_ALLOW_ORIGINS=http://localhost:5173,http://localhost:5174,http://localhost:5175 # Redis配置 REDIS_URL=redis://:123456@127.0.0.1:6379/9 -REDIS_TOKEN_VALIDATION=true # 是否启用Redis token验证 - -# 服务器配置 -APP_HOST=0.0.0.0 -APP_PORT=8080 - -# CORS配置 -CORS_ALLOW_ORIGINS=http://localhost:5173 - -# 日志级别 -RUST_LOG=info \ No newline at end of file +REDIS_TOKEN_VALIDATION=true \ No newline at end of file diff --git a/backend/.env.prod b/backend/.env.prod index 97abd73..317b908 100644 --- a/backend/.env.prod +++ b/backend/.env.prod @@ -1,8 +1,13 @@ -RUST_LOG=info,udmin=debug +RUST_LOG=info APP_ENV=prod APP_HOST=0.0.0.0 +# db APP_PORT=9898 DB_URL=mysql://root:109ysy!@149.104.0.182:3306/udmin +# Redis配置 +REDIS_URL=redis://:109ysy!@149.104.0.182:6379/9 +REDIS_TOKEN_VALIDATION=true # 是否启用Redis token验证 +# jwt JWT_SECRET=prod_secret_change_me JWT_ISS=udmin JWT_ACCESS_EXP_SECS=1800 diff --git a/backend/migration/Cargo.toml b/backend/migration/Cargo.toml index e7fa36b..5842fe8 100644 --- a/backend/migration/Cargo.toml +++ b/backend/migration/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "migration" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] sea-orm-migration = { version = "1.1.14" } diff --git a/backend/src/error.rs b/backend/src/error.rs index 4d61288..66451ea 100644 --- a/backend/src/error.rs +++ b/backend/src/error.rs @@ -4,7 +4,9 @@ use crate::response::ApiResponse; #[derive(thiserror::Error, Debug)] pub enum AppError { #[error("unauthorized")] Unauthorized, + #[error("unauthorized: {0}")] UnauthorizedMsg(String), #[error("forbidden")] Forbidden, + #[error("forbidden: {0}")] ForbiddenMsg(String), #[error("bad request: {0}")] BadRequest(String), #[error("not found")] NotFound, #[error(transparent)] Db(#[from] sea_orm::DbErr), @@ -16,7 +18,9 @@ impl IntoResponse for AppError { fn into_response(self) -> axum::response::Response { let (status, code, msg) = match &self { AppError::Unauthorized => (StatusCode::UNAUTHORIZED, 401, "unauthorized".to_string()), + AppError::UnauthorizedMsg(m) => (StatusCode::UNAUTHORIZED, 401, m.clone()), AppError::Forbidden => (StatusCode::FORBIDDEN, 403, "forbidden".to_string()), + AppError::ForbiddenMsg(m) => (StatusCode::FORBIDDEN, 403, m.clone()), AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, 400, m.clone()), AppError::NotFound => (StatusCode::NOT_FOUND, 404, "not found".into()), _ => (StatusCode::INTERNAL_SERVER_ERROR, 500, "internal error".into()), diff --git a/backend/src/main.rs b/backend/src/main.rs index 2bc1b01..ad3c01d 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -7,7 +7,7 @@ pub mod models; pub mod services; pub mod routes; pub mod utils; -pub mod workflow; +//pub mod workflow; use axum::Router; use axum::http::{HeaderValue, Method}; @@ -17,12 +17,12 @@ use axum::middleware; #[tokio::main] async fn main() -> anyhow::Result<()> { - // 增强:支持通过 ENV_FILE 指定要加载的环境文件 + // 增强:支持通过 ENV_FILE 指定要加载的环境文件,并记录实际加载的文件 // - ENV_FILE=prod 或 production => .env.prod // - ENV_FILE=dev 或 development => .env // - ENV_FILE=staging => .env.staging // - ENV_FILE=任意字符串 => 视为显式文件名或路径 - if let Ok(v) = std::env::var("ENV_FILE") { + let env_file_used: Option = if let Ok(v) = std::env::var("ENV_FILE") { let filename = match v.trim() { "" => ".env".to_string(), "prod" | "production" => ".env.prod".to_string(), @@ -30,10 +30,16 @@ async fn main() -> anyhow::Result<()> { "staging" | "pre" | "preprod" | "pre-production" => ".env.staging".to_string(), other => other.to_string(), }; - dotenvy::from_filename(&filename).ok(); + match dotenvy::from_filename_override(&filename) { + Ok(_) => Some(filename), + Err(_) => Some(format!("{} (not found)", filename)), + } } else { - dotenvy::dotenv().ok(); - } + match dotenvy::dotenv_override() { + Ok(path) => Some(path.to_string_lossy().to_string()), + Err(_) => None, + } + }; tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::from_default_env()).init(); @@ -91,7 +97,13 @@ async fn main() -> anyhow::Result<()> { .layer(cors) .layer(middleware::from_fn_with_state(db.clone(), middlewares::logging::request_logger)); - let addr = format!("{}:{}", std::env::var("APP_HOST").unwrap_or("0.0.0.0".into()), std::env::var("APP_PORT").unwrap_or("8080".into())); + // 读取并记录最终使用的主机与端口(默认端口改为 9898) + let app_host = std::env::var("APP_HOST").unwrap_or("0.0.0.0".into()); + let app_port = std::env::var("APP_PORT").unwrap_or("9898".into()); + if let Some(f) = &env_file_used { tracing::info!("env file loaded: {}", f); } else { tracing::info!("env file loaded: "); } + tracing::info!("resolved APP_HOST={} APP_PORT={}", app_host, app_port); + + let addr = format!("{}:{}", app_host, app_port); tracing::info!("listening on {}", addr); axum::serve(tokio::net::TcpListener::bind(addr).await?, app).await?; Ok(()) diff --git a/backend/src/services/auth_service.rs b/backend/src/services/auth_service.rs index 8863991..263c68c 100644 --- a/backend/src/services/auth_service.rs +++ b/backend/src/services/auth_service.rs @@ -7,10 +7,17 @@ use sea_orm::ActiveValue::NotSet; pub struct LoginResult { pub user: user::Model, pub access: String, pub refresh: String } pub async fn login(db: &Db, username: String, password_plain: String) -> Result { - let u = user::Entity::find().filter(user::Column::Username.eq(username.clone())).one(db).await?.ok_or(AppError::Unauthorized)?; - if u.status != 1 { return Err(AppError::Forbidden); } - let ok = password::verify_password(&password_plain, &u.password_hash).map_err(|_| AppError::Unauthorized)?; - if !ok { return Err(AppError::Unauthorized); } + let u = user::Entity::find() + .filter(user::Column::Username.eq(username.clone())) + .one(db) + .await? + .ok_or(AppError::UnauthorizedMsg("用户名或密码错误".into()))?; + + if u.status != 1 { return Err(AppError::ForbiddenMsg("账户已禁用".into())); } + + let ok = password::verify_password(&password_plain, &u.password_hash) + .map_err(|_| AppError::UnauthorizedMsg("用户名或密码错误".into()))?; + if !ok { return Err(AppError::UnauthorizedMsg("用户名或密码错误".into())); } let access_claims = crate::middlewares::jwt::new_access_claims(u.id, &u.username); let refresh_claims = crate::middlewares::jwt::new_refresh_claims(u.id, &u.username); @@ -79,7 +86,7 @@ pub async fn rotate_refresh(db: &Db, uid: i64, old_refresh: String) -> Result<(S let existing = refresh_token::Entity::find().filter(refresh_token::Column::UserId.eq(uid)).filter(refresh_token::Column::TokenHash.eq(token_hash.clone())).one(db).await?; if !is_valid_redis && existing.is_none() { - return Err(AppError::Unauthorized); + return Err(AppError::Unauthorized); } let u = user::Entity::find_by_id(uid).one(db).await?.ok_or(AppError::Unauthorized)?; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 4cec9f4..aa33423 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -90,7 +90,7 @@ export default function Dashboard() { return (
- + {/* */} @@ -163,7 +163,7 @@ export default function Dashboard() { - 欢迎使用 道友Admin。当前首页展示了若干概览与示例报表,真实数据以接口返回为准。 + 欢迎使用 道友管理后台。当前首页展示了若干概览与示例报表,真实数据以接口返回为准。
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index f7d5eed..f410c3e 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,7 +1,7 @@ import { LockOutlined, UserOutlined } from '@ant-design/icons' import { Button, Card, Form, Input, message, Typography } from 'antd' import { useNavigate } from 'react-router-dom' -import api from '../utils/axios' +import api, { type ApiResp } from '../utils/axios' import { setToken, setUser } from '../utils/token' import './login.css' @@ -11,26 +11,27 @@ export default function Login() { const onFinish = async (values: any) => { try { - const { data } = await api.post('/auth/login', values) + const { data } = await api.post>('/auth/login', values) if (data?.code === 0) { - const token = data.data.access_token + const token = data.data!.access_token setToken(token) - setUser(data.data.user) + setUser(data.data!.user) message.success('登录成功') navigate('/', { replace: true }) } else { throw new Error(data?.message || '登录失败') } } catch (e: any) { - message.error(e.message || '登录失败') + const serverMsg = e?.response?.data?.message + message.error(serverMsg || e.message || '登录失败') } } return (
- Udmin 管理后台 -
+ 道友 管理后台 + } placeholder="用户名" /> diff --git a/frontend/src/utils/axios.ts b/frontend/src/utils/axios.ts index bb8f1af..61f57ac 100644 --- a/frontend/src/utils/axios.ts +++ b/frontend/src/utils/axios.ts @@ -33,8 +33,34 @@ api.interceptors.response.use( (r: AxiosResponse) => r, async (error: AxiosError>) => { const original = (error.config || {}) as RetryConfig - if (error.response?.status === 401 && !original._retry) { + const status = error.response?.status + if (status === 401) { + const reqUrl = (original?.url || '').toString() + const resp = error.response?.data as ApiResp | undefined + + // 登录接口返回 401:不做 refresh,不跳转,直接把服务端消息抛给调用方 + if (reqUrl.includes('/auth/login')) { + const msg = resp?.message || '用户名或密码错误' + return Promise.reject(new Error(msg)) + } + + // 已经重试过了,直接拒绝 + if (original._retry) { + return Promise.reject(error) + } original._retry = true + + const hasToken = !!getToken() + if (!hasToken) { + // 没有 token 的 401:如果不在登录页,则跳转到登录页;否则仅抛错以便界面提示 + if (typeof window !== 'undefined' && window.location.pathname !== '/login') { + window.location.href = '/login' + } + const msg = resp?.message || '未登录或登录已过期' + return Promise.reject(new Error(msg)) + } + + // 有 token 的 401:尝试刷新 if (!isRefreshing) { isRefreshing = true try { @@ -50,7 +76,7 @@ api.interceptors.response.use( pendingQueue.forEach(p => p.reject(e)) pendingQueue = [] clearToken() - if (typeof window !== 'undefined') { + if (typeof window !== 'undefined' && window.location.pathname !== '/login') { window.location.href = '/login' } return Promise.reject(e) diff --git a/frontend/src/utils/config.ts b/frontend/src/utils/config.ts index 6bb961e..03f85b9 100644 --- a/frontend/src/utils/config.ts +++ b/frontend/src/utils/config.ts @@ -1,8 +1,8 @@ // 前端系统配置常量 export const APP_CONFIG = { // 网站标题配置 - SITE_NAME: '道友管理系统', - SITE_NAME_SHORT: '道友', + SITE_NAME: '道友', + SITE_NAME_SHORT: '道', // 其他系统配置可以在这里添加 // 如: API_VERSION: 'v1', DEFAULT_PAGE_SIZE: 10 等