This commit is contained in:
2025-08-28 00:55:35 +08:00
commit 410f54a65e
93 changed files with 9863 additions and 0 deletions

View File

@ -0,0 +1,72 @@
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter, ActiveModelTrait, Set};
use crate::{db::Db, models::{user, refresh_token}, utils::password, error::AppError};
use chrono::{Utc, Duration, FixedOffset};
use sha2::{Sha256, Digest};
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<LoginResult, AppError> {
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 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);
let secret = std::env::var("JWT_SECRET").unwrap();
let access = crate::middlewares::jwt::encode_token(&access_claims, &secret)?;
let refresh = crate::middlewares::jwt::encode_token(&refresh_claims, &secret)?;
// persist refresh token hash
let mut hasher = Sha256::new(); hasher.update(refresh.as_bytes());
let token_hash = format!("{:x}", hasher.finalize());
let exp_secs = std::env::var("JWT_REFRESH_EXP_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(1209600);
let expires = (Utc::now() + Duration::seconds(exp_secs as i64)).with_timezone(&FixedOffset::east_opt(0).unwrap());
let created = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
let am = refresh_token::ActiveModel {
id: NotSet,
user_id: Set(u.id),
token_hash: Set(token_hash),
expires_at: Set(expires),
created_at: Set(created),
};
let _ = am.insert(db).await?;
Ok(LoginResult { user: u, access, refresh })
}
pub async fn logout(db: &Db, uid: i64) -> Result<(), AppError> {
let _ = refresh_token::Entity::delete_many().filter(refresh_token::Column::UserId.eq(uid)).exec(db).await?;
Ok(())
}
pub async fn rotate_refresh(db: &Db, uid: i64, old_refresh: String) -> Result<(String, String), AppError> {
let mut hasher = Sha256::new(); hasher.update(old_refresh.as_bytes());
let token_hash = format!("{:x}", hasher.finalize());
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 existing.is_none() { return Err(AppError::Unauthorized); }
let u = user::Entity::find_by_id(uid).one(db).await?.ok_or(AppError::Unauthorized)?;
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);
let secret = std::env::var("JWT_SECRET").unwrap();
let access = crate::middlewares::jwt::encode_token(&access_claims, &secret)?;
let refresh = crate::middlewares::jwt::encode_token(&refresh_claims, &secret)?;
let _ = refresh_token::Entity::delete_many().filter(refresh_token::Column::UserId.eq(uid)).filter(refresh_token::Column::TokenHash.eq(token_hash)).exec(db).await?;
let mut hasher2 = Sha256::new(); hasher2.update(refresh.as_bytes());
let token_hash2 = format!("{:x}", hasher2.finalize());
let exp_secs = std::env::var("JWT_REFRESH_EXP_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(1209600);
let expires = (Utc::now() + Duration::seconds(exp_secs as i64)).with_timezone(&FixedOffset::east_opt(0).unwrap());
let created = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
let am = refresh_token::ActiveModel {
id: NotSet,
user_id: Set(u.id),
token_hash: Set(token_hash2),
expires_at: Set(expires),
created_at: Set(created),
};
let _ = am.insert(db).await?;
Ok((access, refresh))
}

View File

@ -0,0 +1,84 @@
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter, ActiveModelTrait, Set, QueryOrder, TransactionTrait, QuerySelect};
use sea_orm::prelude::DateTimeWithTimeZone;
use crate::{db::Db, models::{department, user_department}, error::AppError};
use sea_orm::PaginatorTrait;
#[derive(Clone, Debug, serde::Serialize)]
pub struct DepartmentInfo { pub id: i64, pub parent_id: Option<i64>, pub name: String, pub order_no: i32, pub status: i32, pub created_at: DateTimeWithTimeZone }
impl From<department::Model> for DepartmentInfo { fn from(m: department::Model) -> Self { Self { id: m.id, parent_id: m.parent_id, name: m.name, order_no: m.order_no, status: m.status, created_at: m.created_at } } }
pub async fn list_all(db: &Db, keyword: Option<String>) -> Result<Vec<DepartmentInfo>, AppError> {
let mut selector = department::Entity::find();
if let Some(k) = keyword { let like = format!("%{}%", k); selector = selector.filter(department::Column::Name.like(like)); }
let models = selector.order_by_asc(department::Column::OrderNo).order_by_asc(department::Column::Id).all(db).await?;
Ok(models.into_iter().map(Into::into).collect())
}
#[derive(serde::Deserialize)]
pub struct CreateDepartmentInput { pub parent_id: Option<i64>, pub name: String, pub order_no: Option<i32>, pub status: Option<i32> }
pub async fn create(db: &Db, input: CreateDepartmentInput) -> Result<DepartmentInfo, AppError> {
let mut am: department::ActiveModel = Default::default();
am.parent_id = Set(input.parent_id);
am.name = Set(input.name);
am.order_no = Set(input.order_no.unwrap_or(0));
am.status = Set(input.status.unwrap_or(1));
let m = am.insert(db).await?;
Ok(m.into())
}
#[derive(serde::Deserialize)]
pub struct UpdateDepartmentInput { pub parent_id: Option<i64>, pub name: Option<String>, pub order_no: Option<i32>, pub status: Option<i32> }
pub async fn update(db: &Db, id: i64, input: UpdateDepartmentInput) -> Result<DepartmentInfo, AppError> {
let m = department::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let mut am: department::ActiveModel = m.into();
if let Some(v) = input.parent_id { am.parent_id = Set(Some(v)); }
if let Some(v) = input.name { am.name = Set(v); }
if let Some(v) = input.order_no { am.order_no = Set(v); }
if let Some(v) = input.status { am.status = Set(v); }
let m = am.update(db).await?;
Ok(m.into())
}
// user <-> departments
pub async fn get_department_ids_by_user_id(db: &Db, user_id: i64) -> Result<Vec<i64>, AppError> {
let ids = user_department::Entity::find()
.filter(user_department::Column::UserId.eq(user_id))
.select_only()
.column(user_department::Column::DepartmentId)
.into_tuple::<i64>()
.all(db)
.await?;
Ok(ids)
}
#[derive(serde::Deserialize)]
pub struct SetIdsInput { pub ids: Vec<i64> }
pub async fn set_department_ids_by_user_id(db: &Db, user_id: i64, ids: Vec<i64>) -> Result<(), AppError> {
let txn = db.begin().await?;
user_department::Entity::delete_many()
.filter(user_department::Column::UserId.eq(user_id))
.exec(&txn)
.await?;
if !ids.is_empty() {
let mut items: Vec<user_department::ActiveModel> = Vec::with_capacity(ids.len());
for did in ids { let mut am: user_department::ActiveModel = Default::default(); am.user_id = Set(user_id); am.department_id = Set(did); items.push(am); }
user_department::Entity::insert_many(items).exec(&txn).await?;
}
txn.commit().await?;
Ok(())
}
// 新增:删除部门(禁止删除存在子部门的项),并清理用户关联
pub async fn delete(db: &Db, id: i64) -> Result<(), AppError> {
let _ = department::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let child_count = department::Entity::find().filter(department::Column::ParentId.eq(Some(id))).count(db).await?;
if child_count > 0 { return Err(AppError::BadRequest("请先删除子部门".into())); }
let txn = db.begin().await?;
user_department::Entity::delete_many().filter(user_department::Column::DepartmentId.eq(id)).exec(&txn).await?;
let _ = department::Entity::delete_by_id(id).exec(&txn).await?;
txn.commit().await?;
Ok(())
}

View File

@ -0,0 +1,71 @@
use crate::{db::Db, models::request_log};
use sea_orm::{ActiveModelTrait, Set, EntityTrait, ColumnTrait, QueryFilter, PaginatorTrait, QueryOrder};
use chrono::{DateTime, FixedOffset, Utc};
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct CreateLogInput {
pub path: String,
pub method: String,
pub request_params: Option<String>,
pub response_params: Option<String>,
pub status_code: i32,
pub user_id: Option<i64>,
pub username: Option<String>,
pub request_time: DateTime<FixedOffset>,
pub duration_ms: i64,
}
pub async fn create(db: &Db, input: CreateLogInput) -> anyhow::Result<i64> {
let am = request_log::ActiveModel {
id: Default::default(),
path: Set(input.path),
method: Set(input.method),
request_params: Set(input.request_params),
response_params: Set(input.response_params),
status_code: Set(input.status_code),
user_id: Set(input.user_id),
username: Set(input.username),
request_time: Set(input.request_time),
duration_ms: Set(input.duration_ms),
created_at: Set(Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap())),
};
let m = am.insert(db).await?;
Ok(m.id)
}
#[derive(serde::Serialize, Clone, Debug)]
pub struct LogInfo {
pub id: i64,
pub path: String,
pub method: String,
pub request_params: Option<String>,
pub response_params: Option<String>,
pub status_code: i32,
pub user_id: Option<i64>,
pub username: Option<String>,
pub request_time: chrono::DateTime<FixedOffset>,
pub duration_ms: i64,
}
impl From<request_log::Model> for LogInfo {
fn from(m: request_log::Model) -> Self {
Self { id: m.id, path: m.path, method: m.method, request_params: m.request_params, response_params: m.response_params, status_code: m.status_code, user_id: m.user_id, username: m.username, request_time: m.request_time, duration_ms: m.duration_ms }
}
}
#[derive(serde::Serialize)]
pub struct PageResp<T> { pub items: Vec<T>, pub total: u64, pub page: u64, pub page_size: u64 }
#[derive(serde::Deserialize)]
pub struct ListParams { pub page: Option<u64>, pub page_size: Option<u64>, pub path: Option<String>, pub start_time: Option<String>, pub end_time: Option<String> }
pub async fn list(db: &Db, p: ListParams) -> anyhow::Result<PageResp<LogInfo>> {
let page = p.page.unwrap_or(1); let page_size = p.page_size.unwrap_or(10);
let mut selector = request_log::Entity::find();
if let Some(path) = p.path { let like = format!("%{}%", path); selector = selector.filter(request_log::Column::Path.like(like)); }
if let Some(start) = p.start_time.as_deref() { if let Ok(dt) = start.parse::<chrono::DateTime<FixedOffset>>() { selector = selector.filter(request_log::Column::RequestTime.gte(dt)); } }
if let Some(end) = p.end_time.as_deref() { if let Ok(dt) = end.parse::<chrono::DateTime<FixedOffset>>() { selector = selector.filter(request_log::Column::RequestTime.lte(dt)); } }
let paginator = selector.order_by_desc(request_log::Column::Id).paginate(db, page_size);
let total = paginator.num_items().await? as u64;
let models = paginator.fetch_page(if page>0 { page-1 } else { 0 }).await?;
Ok(PageResp { items: models.into_iter().map(Into::into).collect(), total, page, page_size })
}

View File

@ -0,0 +1,84 @@
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter, ActiveModelTrait, Set, QueryOrder};
use sea_orm::prelude::DateTimeWithTimeZone;
use crate::{db::Db, models::menu, error::AppError};
use sea_orm::PaginatorTrait;
#[derive(Clone, Debug, serde::Serialize)]
pub struct MenuInfo { pub id: i64, pub parent_id: Option<i64>, pub name: String, pub path: Option<String>, pub component: Option<String>, pub r#type: i32, pub icon: Option<String>, pub order_no: i32, pub visible: bool, pub status: i32, pub keep_alive: bool, pub perms: Option<String>, pub created_at: DateTimeWithTimeZone }
impl From<menu::Model> for MenuInfo { fn from(m: menu::Model) -> Self { Self { id: m.id, parent_id: m.parent_id, name: m.name, path: m.path, component: m.component, r#type: m.r#type, icon: m.icon, order_no: m.order_no, visible: m.visible, status: m.status, keep_alive: m.keep_alive, perms: m.perms, created_at: m.created_at } } }
pub async fn list_all(db: &Db, keyword: Option<String>) -> Result<Vec<MenuInfo>, AppError> {
let mut selector = menu::Entity::find();
if let Some(k) = keyword { let like = format!("%{}%", k); selector = selector.filter(menu::Column::Name.like(like)); }
let models = selector.order_by_asc(menu::Column::OrderNo).order_by_asc(menu::Column::Id).all(db).await?;
Ok(models.into_iter().map(Into::into).collect())
}
#[derive(serde::Deserialize)]
pub struct CreateMenuInput { pub parent_id: Option<i64>, pub name: String, pub path: Option<String>, pub component: Option<String>, pub r#type: i32, pub icon: Option<String>, pub order_no: Option<i32>, pub visible: Option<bool>, pub status: Option<i32>, pub keep_alive: Option<bool>, pub perms: Option<String> }
pub async fn create(db: &Db, input: CreateMenuInput) -> Result<MenuInfo, AppError> {
let mut am: menu::ActiveModel = Default::default();
am.parent_id = Set(input.parent_id);
am.name = Set(input.name);
am.path = Set(input.path);
am.component = Set(input.component);
am.r#type = Set(input.r#type);
am.icon = Set(input.icon);
am.order_no = Set(input.order_no.unwrap_or(0));
am.visible = Set(input.visible.unwrap_or(true));
am.status = Set(input.status.unwrap_or(1));
am.keep_alive = Set(input.keep_alive.unwrap_or(true));
am.perms = Set(input.perms);
let m = am.insert(db).await?;
Ok(m.into())
}
#[derive(serde::Deserialize)]
pub struct UpdateMenuInput { pub parent_id: Option<i64>, pub name: Option<String>, pub path: Option<String>, pub component: Option<String>, pub r#type: Option<i32>, pub icon: Option<String>, pub order_no: Option<i32>, pub visible: Option<bool>, pub status: Option<i32>, pub keep_alive: Option<bool>, pub perms: Option<String> }
pub async fn update(db: &Db, id: i64, input: UpdateMenuInput) -> Result<MenuInfo, AppError> {
let m = menu::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let mut am: menu::ActiveModel = m.into();
if let Some(v) = input.parent_id { am.parent_id = Set(Some(v)); }
if let Some(v) = input.name { am.name = Set(v); }
if let Some(v) = input.path { am.path = Set(Some(v)); }
if let Some(v) = input.component { am.component = Set(Some(v)); }
if let Some(v) = input.r#type { am.r#type = Set(v); }
if let Some(v) = input.icon { am.icon = Set(Some(v)); }
if let Some(v) = input.order_no { am.order_no = Set(v); }
if let Some(v) = input.visible { am.visible = Set(v); }
if let Some(v) = input.status { am.status = Set(v); }
if let Some(v) = input.keep_alive { am.keep_alive = Set(v); }
if let Some(v) = input.perms { am.perms = Set(Some(v)); }
let m = am.update(db).await?;
Ok(m.into())
}
pub async fn list_by_ids(db: &Db, ids: Vec<i64>) -> Result<Vec<MenuInfo>, AppError> {
if ids.is_empty() { return Ok(vec![]); }
let models = menu::Entity::find()
.filter(menu::Column::Id.is_in(ids))
.filter(menu::Column::Visible.eq(true))
.filter(menu::Column::Status.eq(1))
.order_by_asc(menu::Column::OrderNo)
.order_by_asc(menu::Column::Id)
.all(db)
.await?;
Ok(models.into_iter().map(Into::into).collect())
}
// 新增:删除菜单(禁止删除存在子菜单的项),并清理角色关联
use crate::models::role_menu;
use sea_orm::TransactionTrait;
pub async fn delete(db: &Db, id: i64) -> Result<(), AppError> {
let _ = menu::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let child_count = menu::Entity::find().filter(menu::Column::ParentId.eq(Some(id))).count(db).await?;
if child_count > 0 { return Err(AppError::BadRequest("请先删除子菜单".into())); }
let txn = db.begin().await?;
role_menu::Entity::delete_many().filter(role_menu::Column::MenuId.eq(id)).exec(&txn).await?;
let _ = menu::Entity::delete_by_id(id).exec(&txn).await?;
txn.commit().await?;
Ok(())
}

View File

@ -0,0 +1,8 @@
pub mod auth_service;
pub mod user_service;
pub mod role_service;
pub mod menu_service;
pub mod department_service;
pub mod log_service;
// 新增岗位服务
pub mod position_service;

View File

@ -0,0 +1,87 @@
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter, PaginatorTrait, ActiveModelTrait, Set, QueryOrder, TransactionTrait, QuerySelect};
use sea_orm::prelude::DateTimeWithTimeZone;
use crate::{db::Db, models::{position, user_position}, error::AppError};
#[derive(Clone, Debug, serde::Serialize)]
pub struct PositionInfo { pub id: i64, pub name: String, pub code: String, pub description: Option<String>, pub status: i32, pub order_no: i32, pub created_at: DateTimeWithTimeZone }
impl From<position::Model> for PositionInfo { fn from(m: position::Model) -> Self { Self { id: m.id, name: m.name, code: m.code, description: m.description, status: m.status, order_no: m.order_no, created_at: m.created_at } } }
#[derive(Clone, Debug, serde::Serialize)]
pub struct PageResp<T> { pub items: Vec<T>, pub total: u64, pub page: u64, pub page_size: u64 }
pub async fn list(db: &Db, page: u64, page_size: u64, keyword: Option<String>) -> Result<PageResp<PositionInfo>, AppError> {
let mut selector = position::Entity::find();
if let Some(k) = keyword { let like = format!("%{}%", k); selector = selector.filter(position::Column::Name.like(like.clone()).or(position::Column::Code.like(like))); }
let paginator = selector.order_by_desc(position::Column::Id).paginate(db, page_size);
let total = paginator.num_items().await? as u64;
let models = paginator.fetch_page(if page>0 { page-1 } else { 0 }).await?;
Ok(PageResp { items: models.into_iter().map(Into::into).collect(), total, page, page_size })
}
#[derive(serde::Deserialize)]
pub struct CreatePositionInput { pub name: String, pub code: String, pub description: Option<String>, pub status: Option<i32>, pub order_no: Option<i32> }
pub async fn create(db: &Db, input: CreatePositionInput) -> Result<PositionInfo, AppError> {
if position::Entity::find().filter(position::Column::Code.eq(input.code.clone())).one(db).await?.is_some() {
return Err(AppError::BadRequest("position code already exists".into()));
}
let mut am: position::ActiveModel = Default::default();
am.name = Set(input.name);
am.code = Set(input.code);
am.description = Set(input.description);
am.status = Set(input.status.unwrap_or(1));
am.order_no = Set(input.order_no.unwrap_or(0));
let m = am.insert(db).await?;
Ok(m.into())
}
#[derive(serde::Deserialize)]
pub struct UpdatePositionInput { pub name: Option<String>, pub description: Option<String>, pub status: Option<i32>, pub order_no: Option<i32> }
pub async fn update(db: &Db, id: i64, input: UpdatePositionInput) -> Result<PositionInfo, AppError> {
let m = position::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let mut am: position::ActiveModel = m.into();
if let Some(v) = input.name { am.name = Set(v); }
if let Some(v) = input.description { am.description = Set(Some(v)); }
if let Some(v) = input.status { am.status = Set(v); }
if let Some(v) = input.order_no { am.order_no = Set(v); }
let m = am.update(db).await?;
Ok(m.into())
}
pub async fn delete(db: &Db, id: i64) -> Result<(), AppError> {
let _ = position::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let txn = db.begin().await?;
user_position::Entity::delete_many().filter(user_position::Column::PositionId.eq(id)).exec(&txn).await?;
let _ = position::Entity::delete_by_id(id).exec(&txn).await?;
txn.commit().await?;
Ok(())
}
pub async fn get_position_ids_by_user_id(db: &Db, user_id: i64) -> Result<Vec<i64>, AppError> {
let ids = user_position::Entity::find()
.filter(user_position::Column::UserId.eq(user_id))
.select_only()
.column(user_position::Column::PositionId)
.into_tuple::<i64>()
.all(db)
.await?;
Ok(ids)
}
pub async fn set_position_ids_by_user_id(db: &Db, user_id: i64, position_ids: Vec<i64>) -> Result<(), AppError> {
let txn = db.begin().await?;
user_position::Entity::delete_many().filter(user_position::Column::UserId.eq(user_id)).exec(&txn).await?;
if !position_ids.is_empty() {
let mut items: Vec<user_position::ActiveModel> = Vec::with_capacity(position_ids.len());
for pid in position_ids {
let mut am: user_position::ActiveModel = Default::default();
am.user_id = Set(user_id);
am.position_id = Set(pid);
items.push(am);
}
user_position::Entity::insert_many(items).exec(&txn).await?;
}
txn.commit().await?;
Ok(())
}

View File

@ -0,0 +1,136 @@
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter, PaginatorTrait, ActiveModelTrait, Set, QueryOrder};
use sea_orm::prelude::DateTimeWithTimeZone;
use crate::{db::Db, models::role, error::AppError};
#[derive(Clone, Debug, serde::Serialize)]
pub struct RoleInfo { pub id: i64, pub name: String, pub code: String, pub description: Option<String>, pub status: i32, pub created_at: DateTimeWithTimeZone }
impl From<role::Model> for RoleInfo { fn from(m: role::Model) -> Self { Self { id: m.id, name: m.name, code: m.code, description: m.description, status: m.status, created_at: m.created_at } } }
#[derive(Clone, Debug, serde::Serialize)]
pub struct PageResp<T> { pub items: Vec<T>, pub total: u64, pub page: u64, pub page_size: u64 }
pub async fn list(db: &Db, page: u64, page_size: u64, keyword: Option<String>) -> Result<PageResp<RoleInfo>, AppError> {
let mut selector = role::Entity::find();
if let Some(k) = keyword { let like = format!("%{}%", k); selector = selector.filter(role::Column::Name.like(like.clone()).or(role::Column::Code.like(like))); }
let paginator = selector.order_by_desc(role::Column::Id).paginate(db, page_size);
let total = paginator.num_items().await? as u64;
let models = paginator.fetch_page(if page>0 { page-1 } else { 0 }).await?;
Ok(PageResp { items: models.into_iter().map(Into::into).collect(), total, page, page_size })
}
#[derive(serde::Deserialize)]
pub struct CreateRoleInput { pub name: String, pub code: String, pub description: Option<String>, pub status: Option<i32> }
pub async fn create(db: &Db, input: CreateRoleInput) -> Result<RoleInfo, AppError> {
if role::Entity::find().filter(role::Column::Code.eq(input.code.clone())).one(db).await?.is_some() {
return Err(AppError::BadRequest("role code already exists".into()));
}
let mut am: role::ActiveModel = Default::default();
am.name = Set(input.name);
am.code = Set(input.code);
am.description = Set(input.description);
am.status = Set(input.status.unwrap_or(1));
let m = am.insert(db).await?;
Ok(m.into())
}
#[derive(serde::Deserialize)]
pub struct UpdateRoleInput { pub name: Option<String>, pub description: Option<String>, pub status: Option<i32> }
pub async fn update(db: &Db, id: i64, input: UpdateRoleInput) -> Result<RoleInfo, AppError> {
let m = role::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let mut am: role::ActiveModel = m.into();
if let Some(v) = input.name { am.name = Set(v); }
if let Some(v) = input.description { am.description = Set(Some(v)); }
if let Some(v) = input.status { am.status = Set(v); }
let m = am.update(db).await?;
Ok(m.into())
}
// --- Role assignments: menus only ---
use crate::models::{role_menu, user_role};
use sea_orm::{TransactionTrait, QuerySelect};
pub async fn get_menu_ids(db: &Db, role_id: i64) -> Result<Vec<i64>, AppError> {
let ids = role_menu::Entity::find()
.filter(role_menu::Column::RoleId.eq(role_id))
.select_only()
.column(role_menu::Column::MenuId)
.into_tuple::<i64>()
.all(db)
.await?;
Ok(ids)
}
pub async fn set_menu_ids(db: &Db, role_id: i64, ids: Vec<i64>) -> Result<(), AppError> {
let txn = db.begin().await?;
role_menu::Entity::delete_many()
.filter(role_menu::Column::RoleId.eq(role_id))
.exec(&txn)
.await?;
if !ids.is_empty() {
let mut items: Vec<role_menu::ActiveModel> = Vec::with_capacity(ids.len());
for mid in ids { let mut am: role_menu::ActiveModel = Default::default(); am.role_id = Set(role_id); am.menu_id = Set(mid); items.push(am); }
role_menu::Entity::insert_many(items).exec(&txn).await?;
}
txn.commit().await?;
Ok(())
}
pub async fn get_role_ids_by_user_id(db: &Db, user_id: i64) -> Result<Vec<i64>, AppError> {
let ids = user_role::Entity::find()
.filter(user_role::Column::UserId.eq(user_id))
.select_only()
.column(user_role::Column::RoleId)
.into_tuple::<i64>()
.all(db)
.await?;
Ok(ids)
}
pub async fn get_menu_ids_by_user_id(db: &Db, user_id: i64) -> Result<Vec<i64>, AppError> {
let role_ids = get_role_ids_by_user_id(db, user_id).await?;
if role_ids.is_empty() { return Ok(vec![]); }
let mut ids = role_menu::Entity::find()
.filter(role_menu::Column::RoleId.is_in(role_ids))
.select_only()
.column(role_menu::Column::MenuId)
.into_tuple::<i64>()
.all(db)
.await?;
ids.sort_unstable();
ids.dedup();
Ok(ids)
}
pub async fn set_role_ids_by_user_id(db: &Db, user_id: i64, role_ids: Vec<i64>) -> Result<(), AppError> {
let txn = db.begin().await?;
user_role::Entity::delete_many()
.filter(user_role::Column::UserId.eq(user_id))
.exec(&txn)
.await?;
if !role_ids.is_empty() {
let mut items: Vec<user_role::ActiveModel> = Vec::with_capacity(role_ids.len());
for rid in role_ids {
let mut am: user_role::ActiveModel = Default::default();
am.user_id = Set(user_id);
am.role_id = Set(rid);
items.push(am);
}
user_role::Entity::insert_many(items).exec(&txn).await?;
}
txn.commit().await?;
Ok(())
}
// 新增:删除角色(清理用户与权限、菜单关联)
pub async fn delete(db: &Db, id: i64) -> Result<(), AppError> {
let _ = role::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let txn = db.begin().await?;
role_menu::Entity::delete_many().filter(role_menu::Column::RoleId.eq(id)).exec(&txn).await?;
user_role::Entity::delete_many().filter(user_role::Column::RoleId.eq(id)).exec(&txn).await?;
let _ = role::Entity::delete_by_id(id).exec(&txn).await?;
txn.commit().await?;
Ok(())
}

View File

@ -0,0 +1,74 @@
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter, PaginatorTrait, ActiveModelTrait, Set, QueryOrder};
use sea_orm::prelude::DateTimeWithTimeZone;
use crate::{db::Db, models::user, utils::password, error::AppError};
#[derive(Clone, Debug, serde::Serialize)]
pub struct UserInfo { pub id: i64, pub username: String, pub nickname: Option<String>, pub status: i32, pub created_at: DateTimeWithTimeZone }
impl From<user::Model> for UserInfo { fn from(m: user::Model) -> Self { Self { id: m.id, username: m.username, nickname: m.nickname, status: m.status, created_at: m.created_at } } }
#[derive(Clone, Debug, serde::Serialize)]
pub struct PageResp<T> { pub items: Vec<T>, pub total: u64, pub page: u64, pub page_size: u64 }
pub async fn list(db: &Db, page: u64, page_size: u64, keyword: Option<String>) -> Result<PageResp<UserInfo>, AppError> {
let mut selector = user::Entity::find();
if let Some(k) = keyword { let like = format!("%{}%", k); selector = selector.filter(user::Column::Username.like(like.clone()).or(user::Column::Nickname.like(like))); }
let paginator = selector.order_by_desc(user::Column::Id).paginate(db, page_size);
let total = paginator.num_items().await? as u64;
let models = paginator.fetch_page(if page>0 { page-1 } else { 0 }).await?;
Ok(PageResp { items: models.into_iter().map(Into::into).collect(), total, page, page_size })
}
#[derive(serde::Deserialize)]
pub struct CreateUserInput { pub username: String, pub password: String, pub nickname: Option<String>, pub status: Option<i32> }
pub async fn create(db: &Db, input: CreateUserInput) -> Result<UserInfo, AppError> {
if user::Entity::find().filter(user::Column::Username.eq(input.username.clone())).one(db).await?.is_some() {
return Err(AppError::BadRequest("username already exists".into()));
}
let hash = password::hash_password(&input.password)?;
let mut am: user::ActiveModel = Default::default();
am.username = Set(input.username);
am.password_hash = Set(hash);
am.nickname = Set(input.nickname);
am.status = Set(input.status.unwrap_or(1));
let m = am.insert(db).await?;
Ok(m.into())
}
#[derive(serde::Deserialize)]
pub struct UpdateUserInput { pub nickname: Option<String>, pub status: Option<i32> }
pub async fn update(db: &Db, id: i64, input: UpdateUserInput) -> Result<UserInfo, AppError> {
let m = user::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let mut am: user::ActiveModel = m.into();
if let Some(n) = input.nickname { am.nickname = Set(Some(n)); }
if let Some(s) = input.status { am.status = Set(s); }
let m = am.update(db).await?;
Ok(m.into())
}
#[derive(serde::Deserialize)]
pub struct ResetPasswordInput { pub password: String }
pub async fn reset_password(db: &Db, id: i64, input: ResetPasswordInput) -> Result<(), AppError> {
let m = user::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let mut am: user::ActiveModel = m.into();
am.password_hash = Set(password::hash_password(&input.password)?);
let _ = am.update(db).await?;
Ok(())
}
// 新增:删除用户(包含清理关联关系)
use crate::models::{user_role, user_department, refresh_token};
use sea_orm::TransactionTrait;
pub async fn delete(db: &Db, id: i64) -> Result<(), AppError> {
let _ = user::Entity::find_by_id(id).one(db).await?.ok_or(AppError::NotFound)?;
let txn = db.begin().await?;
user_role::Entity::delete_many().filter(user_role::Column::UserId.eq(id)).exec(&txn).await?;
user_department::Entity::delete_many().filter(user_department::Column::UserId.eq(id)).exec(&txn).await?;
refresh_token::Entity::delete_many().filter(refresh_token::Column::UserId.eq(id)).exec(&txn).await?;
let _ = user::Entity::delete_by_id(id).exec(&txn).await?;
txn.commit().await?;
Ok(())
}