用户表拆分为主从模式并调整登录注册逻辑

This commit is contained in:
fish
2026-04-13 20:23:35 +08:00
parent e3550fedac
commit 49af914f9b
3 changed files with 138 additions and 51 deletions

View File

@@ -1,15 +1,51 @@
-- 用户表初始化 -- 用户
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS user_main (
id SERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL, deleted BOOLEAN NOT NULL DEFAULT FALSE,
password_hash VARCHAR(255) NOT NULL, createdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
email VARCHAR(100) UNIQUE, modifydate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
-- 用户登录账号表
CREATE TABLE IF NOT EXISTS user_login_account (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
account VARCHAR(100) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
createdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
modifydate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_user_login_account_user_main FOREIGN KEY (user_id) REFERENCES user_main(id)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_login_account_active
ON user_login_account(account)
WHERE deleted = FALSE;
-- 用户密码表
CREATE TABLE IF NOT EXISTS user_login_password (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
password VARCHAR(255) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
createdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
modifydate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_user_login_password_user_main FOREIGN KEY (user_id) REFERENCES user_main(id)
);
CREATE INDEX IF NOT EXISTS idx_user_login_password_user_id
ON user_login_password(user_id);
-- 插入测试用户(密码: 123456 -- 插入测试用户(密码: 123456
-- bcrypt hash: $2b$12$REwMlLDCbzR4UpL6MWnzE.AacihwpFvQhGs7vDKTwwyNMb1qBWOTm -- bcrypt hash: $2b$12$REwMlLDCbzR4UpL6MWnzE.AacihwpFvQhGs7vDKTwwyNMb1qBWOTm
INSERT INTO users (username, password_hash, email) DO $$
VALUES ('admin', '$2b$12$REwMlLDCbzR4UpL6MWnzE.AacihwpFvQhGs7vDKTwwyNMb1qBWOTm', 'admin@example.com') DECLARE
ON CONFLICT (username) DO NOTHING; v_user_id BIGINT;
BEGIN
INSERT INTO user_main DEFAULT VALUES RETURNING id INTO v_user_id;
INSERT INTO user_login_account (user_id, account)
VALUES (v_user_id, 'admin');
INSERT INTO user_login_password (user_id, password)
VALUES (v_user_id, '$2b$12$REwMlLDCbzR4UpL6MWnzE.AacihwpFvQhGs7vDKTwwyNMb1qBWOTm');
END $$;

View File

@@ -90,9 +90,12 @@ async fn login_handler(
) -> (StatusCode, Json<LoginResponse>) { ) -> (StatusCode, Json<LoginResponse>) {
info!("Login attempt for user: {}", payload.username); info!("Login attempt for user: {}", payload.username);
// 查询用户 // 查询用户账号与密码
let user: Option<(String,)> = sqlx::query_as( let user: Option<(String,)> = sqlx::query_as(
"SELECT password_hash FROM users WHERE username = $1" "SELECT 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) .bind(&payload.username)
.fetch_optional(&state.db) .fetch_optional(&state.db)

View File

@@ -6,7 +6,6 @@ use axum::{
Router, Router,
}; };
use bcrypt::hash; use bcrypt::hash;
use chrono::Utc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
use std::env; use std::env;
@@ -32,7 +31,7 @@ struct RegisterRequest {
#[derive(Serialize)] #[derive(Serialize)]
struct RegisterResponse { struct RegisterResponse {
success: bool, success: bool,
user_id: Option<i32>, user_id: Option<i64>,
message: String, message: String,
} }
@@ -89,13 +88,15 @@ async fn register_handler(
); );
} }
// 检查用户名是否存在 // 检查账号是否存在
let existing: Option<(i32,)> = sqlx::query_as("SELECT id FROM users WHERE username = $1") let existing: Option<(i64,)> = sqlx::query_as(
"SELECT id FROM user_login_account WHERE account = $1 AND deleted = FALSE"
)
.bind(&payload.username) .bind(&payload.username)
.fetch_optional(&state.db) .fetch_optional(&state.db)
.await .await
.unwrap_or(None); .unwrap_or(None);
if existing.is_some() { if existing.is_some() {
return ( return (
StatusCode::CONFLICT, StatusCode::CONFLICT,
@@ -106,25 +107,7 @@ async fn register_handler(
}), }),
); );
} }
// 检查邮箱是否存在
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) { let password_hash = match hash(&payload.password, bcrypt::DEFAULT_COST) {
Ok(h) => h, Ok(h) => h,
@@ -140,22 +123,87 @@ async fn register_handler(
); );
} }
}; };
// 插入用户 // 插入用户(主从表事务)
let result = sqlx::query_as::<_, (i32,)>( let mut tx = match state.db.begin().await {
"INSERT INTO users (username, password_hash, email, created_at, updated_at) Ok(t) => t,
VALUES ($1, $2, $3, $4, $4) Err(e) => {
RETURNING id" warn!("Transaction start failed: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(RegisterResponse {
success: false,
user_id: None,
message: "Internal error".to_string(),
}),
);
}
};
let result: Result<(i64,), sqlx::Error> = sqlx::query_as(
"INSERT INTO user_main DEFAULT VALUES RETURNING id"
) )
.bind(&payload.username) .fetch_one(&mut *tx)
.bind(&password_hash)
.bind(&payload.email)
.bind(Utc::now())
.fetch_one(&state.db)
.await; .await;
match result { let user_id = match result {
Ok((user_id,)) => { Ok((id,)) => id,
Err(e) => {
warn!("Insert user_main failed: {}", e);
let _ = tx.rollback().await;
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(RegisterResponse {
success: false,
user_id: None,
message: "Registration failed".to_string(),
}),
);
}
};
if let Err(e) = sqlx::query(
"INSERT INTO user_login_account (user_id, account) VALUES ($1, $2)"
)
.bind(user_id)
.bind(&payload.username)
.execute(&mut *tx)
.await
{
warn!("Insert user_login_account failed: {}", e);
let _ = tx.rollback().await;
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(RegisterResponse {
success: false,
user_id: None,
message: "Registration failed".to_string(),
}),
);
}
if let Err(e) = sqlx::query(
"INSERT INTO user_login_password (user_id, password) VALUES ($1, $2)"
)
.bind(user_id)
.bind(&password_hash)
.execute(&mut *tx)
.await
{
warn!("Insert user_login_password failed: {}", e);
let _ = tx.rollback().await;
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(RegisterResponse {
success: false,
user_id: None,
message: "Registration failed".to_string(),
}),
);
}
match tx.commit().await {
Ok(()) => {
info!("User {} registered with id {}", payload.username, user_id); info!("User {} registered with id {}", payload.username, user_id);
( (
StatusCode::CREATED, StatusCode::CREATED,