use chrono::Utc; use sea_orm::sea_query::Expr; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set, TransactionTrait, }; use uuid::Uuid; use crate::errors::{SpaceError, SpaceResult}; use crate::services::service::SpaceService; use crate::services::types::{ AddMemberBody, JoinRequestSummary, JoinRuleSummary, SYSTEM_ROLE_MEMBER, SYSTEM_ROLE_OWNER, }; use crate::services::{member, service}; use models::space::{ SpaceJoinMethod, SpaceJoinRequestState, SpaceMemberRole, space_join_requests, space_join_rules, space_members, space_roles, }; use models::users::users; fn rule_summary(row: &space_join_rules::Model) -> JoinRuleSummary { JoinRuleSummary { id: row.id, organization_id: row.organization_id, role_id: row.role_id, trigger: row.trigger.clone(), matcher: row.matcher.clone(), enabled: row.enabled, created_at: row.created_at, updated_at: row.updated_at, } } impl SpaceService { async fn join_request_summary( &self, row: &space_join_requests::Model, ) -> SpaceResult { let user = users::Entity::find_by_id(row.user_id) .one(self.db_conn().read()) .await?; Ok(JoinRequestSummary { id: row.id, organization_id: row.organization_id, user_id: row.user_id, username: user .as_ref() .map(|value| value.username.clone()) .unwrap_or_default(), displayname: user.and_then(|value| value.displayname), status: row.status, note: row.note.clone(), decided_by: row.decided_by, decided_at: row.decided_at, created_at: row.created_at, updated_at: row.updated_at, }) } // 加入规则的可选字段本身就多,参数表与 HTTP 请求体一一对应,拆结构体只会增加噪音。 #[allow(clippy::too_many_arguments)] #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))] pub async fn admin_create_join_rule( &self, actor_user_id: i64, organization_id: Uuid, role_id: Uuid, trigger: String, matcher: String, enabled: bool, request_id: Option<&str>, ) -> SpaceResult { self.require_permission(actor_user_id, organization_id, "role.update", request_id) .await?; let trigger = trigger.trim().to_string(); let matcher = matcher.trim().to_string(); if trigger.is_empty() || matcher.is_empty() { return Err(SpaceError::MalformedRequest); } let role = space_roles::Entity::find_by_id(role_id) .filter(space_roles::Column::OrganizationId.eq(organization_id)) .one(self.db_conn().read()) .await? .ok_or(SpaceError::SpaceRoleNotFound)?; let row = space_join_rules::ActiveModel { organization_id: Set(organization_id), role_id: Set(role.id), trigger: Set(trigger), matcher: Set(matcher), enabled: Set(enabled), ..Default::default() } .insert(self.db_conn().write()) .await?; service::bump_version(self.db_conn().write(), organization_id).await?; Ok(rule_summary(&row)) } // 同上:可选字段多,保持与请求体一致的参数顺序。 #[allow(clippy::too_many_arguments)] #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, rule_id = %rule_id, request_id = %request_id.unwrap_or_default()))] pub async fn admin_update_join_rule( &self, actor_user_id: i64, organization_id: Uuid, rule_id: Uuid, role_id: Option, trigger: Option, matcher: Option, enabled: Option, request_id: Option<&str>, ) -> SpaceResult { self.require_permission(actor_user_id, organization_id, "role.update", request_id) .await?; let row = space_join_rules::Entity::find_by_id(rule_id) .filter(space_join_rules::Column::OrganizationId.eq(organization_id)) .one(self.db_conn().read()) .await? .ok_or(SpaceError::SpaceNotFound)?; let mut active: space_join_rules::ActiveModel = row.into(); if let Some(role_id) = role_id { let role = space_roles::Entity::find_by_id(role_id) .filter(space_roles::Column::OrganizationId.eq(organization_id)) .one(self.db_conn().read()) .await? .ok_or(SpaceError::SpaceRoleNotFound)?; active.role_id = Set(role.id); } if let Some(trigger) = trigger { let trigger = trigger.trim().to_string(); if trigger.is_empty() { return Err(SpaceError::MalformedRequest); } active.trigger = Set(trigger); } if let Some(matcher) = matcher { let matcher = matcher.trim().to_string(); if matcher.is_empty() { return Err(SpaceError::MalformedRequest); } active.matcher = Set(matcher); } if let Some(enabled) = enabled { active.enabled = Set(enabled); } let updated = active.update(self.db_conn().write()).await?; service::bump_version(self.db_conn().write(), organization_id).await?; Ok(rule_summary(&updated)) } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, rule_id = %rule_id, request_id = %request_id.unwrap_or_default()))] pub async fn admin_delete_join_rule( &self, actor_user_id: i64, organization_id: Uuid, rule_id: Uuid, request_id: Option<&str>, ) -> SpaceResult { self.require_permission(actor_user_id, organization_id, "role.update", request_id) .await?; let deleted = space_join_rules::Entity::delete_many() .filter(space_join_rules::Column::Id.eq(rule_id)) .filter(space_join_rules::Column::OrganizationId.eq(organization_id)) .exec(self.db_conn().write()) .await?; if deleted.rows_affected > 0 { service::bump_version(self.db_conn().write(), organization_id).await?; } Ok(deleted.rows_affected > 0) } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))] pub async fn admin_list_join_rules( &self, actor_user_id: i64, organization_id: Uuid, request_id: Option<&str>, ) -> SpaceResult> { self.require_permission(actor_user_id, organization_id, "role.view", request_id) .await?; let rows = space_join_rules::Entity::find() .filter(space_join_rules::Column::OrganizationId.eq(organization_id)) .order_by_asc(space_join_rules::Column::CreatedAt) .all(self.db_conn().read()) .await?; Ok(rows.iter().map(rule_summary).collect()) } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))] pub async fn admin_create_join_request( &self, actor_user_id: i64, organization_id: Uuid, note: Option, request_id: Option<&str>, ) -> SpaceResult { let organization = self.load_organization(organization_id).await?; if organization.status != models::space::SpaceStatus::Active { return Err(SpaceError::SpaceInactive); } if let Some(existing) = member::find_any(self.db_conn().read(), organization_id, actor_user_id).await? { if existing.banned_at.is_some() { return Err(SpaceError::SpaceMemberBanned); } if existing.left_at.is_none() { return Err(SpaceError::SpaceMemberExists); } } if let Some(pending) = space_join_requests::Entity::find() .filter(space_join_requests::Column::OrganizationId.eq(organization_id)) .filter(space_join_requests::Column::UserId.eq(actor_user_id)) .filter(space_join_requests::Column::Status.eq(SpaceJoinRequestState::Pending)) .one(self.db_conn().read()) .await? { let mut active: space_join_requests::ActiveModel = pending.into(); active.note = Set(note); let updated = active.update(self.db_conn().write()).await?; return self.join_request_summary(&updated).await; } let row = space_join_requests::ActiveModel { organization_id: Set(organization_id), user_id: Set(actor_user_id), status: Set(SpaceJoinRequestState::Pending), note: Set(note), ..Default::default() } .insert(self.db_conn().write()) .await?; self.join_request_summary(&row).await } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))] pub async fn admin_list_join_requests( &self, actor_user_id: i64, organization_id: Uuid, status: Option, limit: u64, offset: u64, request_id: Option<&str>, ) -> SpaceResult<(Vec, u64)> { self.require_permission(actor_user_id, organization_id, "member.view", request_id) .await?; let mut select = space_join_requests::Entity::find() .filter(space_join_requests::Column::OrganizationId.eq(organization_id)); if let Some(status) = status { select = select.filter(space_join_requests::Column::Status.eq(status)); } let total = select.clone().count(self.db_conn().read()).await?; let rows = select .order_by_desc(space_join_requests::Column::CreatedAt) .limit(limit) .offset(offset) .all(self.db_conn().read()) .await?; let mut out = Vec::with_capacity(rows.len()); for row in rows { out.push(self.join_request_summary(&row).await?); } Ok((out, total)) } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))] pub async fn admin_withdraw_join_request( &self, actor_user_id: i64, organization_id: Uuid, join_request_id: Uuid, request_id: Option<&str>, ) -> SpaceResult { let row = space_join_requests::Entity::find_by_id(join_request_id) .filter(space_join_requests::Column::OrganizationId.eq(organization_id)) .one(self.db_conn().read()) .await? .ok_or(SpaceError::SpaceMemberNotFound)?; if row.user_id != actor_user_id { return Err(SpaceError::Forbidden); } if row.status != SpaceJoinRequestState::Pending { return Err(SpaceError::Forbidden); } let mut active: space_join_requests::ActiveModel = row.into(); active.status = Set(SpaceJoinRequestState::Withdrawn); let updated = active.update(self.db_conn().write()).await?; self.join_request_summary(&updated).await } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))] async fn finish_join_request( &self, actor_user_id: i64, organization_id: Uuid, join_request_id: Uuid, status: SpaceJoinRequestState, note: Option, request_id: Option<&str>, ) -> SpaceResult { self.require_permission(actor_user_id, organization_id, "member.add", request_id) .await?; let row = space_join_requests::Entity::find_by_id(join_request_id) .filter(space_join_requests::Column::OrganizationId.eq(organization_id)) .one(self.db_conn().read()) .await? .ok_or(SpaceError::SpaceMemberNotFound)?; if row.status != SpaceJoinRequestState::Pending { return Err(SpaceError::Forbidden); } if status == SpaceJoinRequestState::Approved { let default_role = space_roles::Entity::find() .filter(space_roles::Column::OrganizationId.eq(organization_id)) .filter(space_roles::Column::Key.eq(SYSTEM_ROLE_MEMBER)) .one(self.db_conn().read()) .await?; self.admin_add_member( actor_user_id, organization_id, AddMemberBody { user_id: row.user_id, role: Some(SpaceMemberRole::Member), role_keys: default_role .filter(|role| role.default_role) .map(|role| vec![role.key]) .unwrap_or_default(), }, request_id, ) .await?; } let mut active: space_join_requests::ActiveModel = row.into(); active.status = Set(status); active.decided_by = Set(Some(actor_user_id)); active.decided_at = Set(Some(Utc::now())); if let Some(note) = note { active.note = Set(Some(note)); } let updated = active.update(self.db_conn().write()).await?; self.join_request_summary(&updated).await } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))] pub async fn admin_approve_join_request( &self, actor_user_id: i64, organization_id: Uuid, join_request_id: Uuid, request_id: Option<&str>, ) -> SpaceResult { self.finish_join_request( actor_user_id, organization_id, join_request_id, SpaceJoinRequestState::Approved, None, request_id, ) .await } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))] pub async fn admin_reject_join_request( &self, actor_user_id: i64, organization_id: Uuid, join_request_id: Uuid, note: Option, request_id: Option<&str>, ) -> SpaceResult { self.finish_join_request( actor_user_id, organization_id, join_request_id, SpaceJoinRequestState::Rejected, note, request_id, ) .await } #[tracing::instrument(skip_all, fields(organization_id = %organization_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))] pub async fn ensure_bot_member( &self, organization_id: Uuid, user_id: i64, role_keys: Vec, role_ids: Vec, request_id: Option<&str>, ) -> SpaceResult<(crate::services::types::MemberSummary, bool)> { let txn = self.db_conn().write().begin().await?; // 机器人成员的角色上限:OWNER 只能由人类走成员面转移,不能通过内部接口直接塞入。 let user = models::users::users::Entity::find_by_id(user_id) .filter(models::users::users::Column::DeletedAt.is_null()) .one(&txn) .await? .ok_or(SpaceError::UserNotFound)?; if !user.bot { return Err(SpaceError::Forbidden); } let (row, created) = member::upsert_join( &txn, organization_id, user_id, SpaceMemberRole::Member, SpaceJoinMethod::Direct, None, None, ) .await?; let mut roles = Vec::new(); for key in role_keys { if let Some(role) = space_roles::Entity::find() .filter(space_roles::Column::OrganizationId.eq(organization_id)) .filter(space_roles::Column::Key.eq(key)) .one(&txn) .await? { if role.key == SYSTEM_ROLE_OWNER { return Err(SpaceError::Forbidden); } roles.push(role); } } for id in role_ids { let Ok(id) = Uuid::parse_str(id.trim()) else { continue; }; if let Some(role) = space_roles::Entity::find_by_id(id) .filter(space_roles::Column::OrganizationId.eq(organization_id)) .one(&txn) .await? { if role.key == SYSTEM_ROLE_OWNER { return Err(SpaceError::Forbidden); } roles.push(role); } } for role in roles { let existing = models::space::space_member_roles::Entity::find() .filter(models::space::space_member_roles::Column::MemberId.eq(row.id)) .filter(models::space::space_member_roles::Column::RoleId.eq(role.id)) .one(&txn) .await?; if existing.is_none() { models::space::space_member_roles::ActiveModel { member_id: Set(row.id), role_id: Set(role.id), ..Default::default() } .insert(&txn) .await?; } } service::bump_version(&txn, organization_id).await?; txn.commit().await?; let summary = self.member_summary(&row).await?; Ok((summary, created)) } #[tracing::instrument(skip_all, fields(organization_id = %organization_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))] pub async fn touch_member_activity( &self, organization_id: Uuid, user_id: i64, request_id: Option<&str>, ) { let _ = space_members::Entity::update_many() .col_expr(space_members::Column::LastActiveAt, Expr::value(Utc::now())) .filter(space_members::Column::OrganizationId.eq(organization_id)) .filter(space_members::Column::UserId.eq(user_id)) .exec(self.db_conn().write()) .await; } }