use models::repo::repo_star; use models::repo::repos; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set, TransactionTrait, Unchanged, }; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use utoipa::ToSchema; use uuid::Uuid; use crate::context::facade::GitContext; use crate::service::GitService; use crate::share::errors::GitError; #[derive(Debug, Clone, Serialize, ToSchema)] pub struct RepoStarDto { pub user_id: i64, pub repo_id: Uuid, pub created_at: OffsetDateTime, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct RepoStarResp { pub stars: i32, } #[derive(Debug, Clone, Serialize, ToSchema)] pub struct StargazerDto { pub user_id: i64, pub username: String, pub display_name: String, pub avatar_url: String, pub starred_at: OffsetDateTime, } impl GitService { pub async fn star_repo( &self, ctx: &GitContext, user_id: i64, ) -> Result { let db = self.db_write(); let repo_id = ctx.model.id; let now = OffsetDateTime::now_utc(); let existing = repo_star::Entity::find() .filter(repo_star::Column::UserId.eq(user_id)) .filter(repo_star::Column::RepoId.eq(repo_id)) .one(db) .await?; if existing.is_none() { let row = repo_star::ActiveModel { id: Set(Uuid::now_v7()), user_id: Set(user_id), repo_id: Set(repo_id), created_at: Set(now), updated_at: Set(now), }; row.insert(db).await?; } let after = repo_star::Entity::find() .filter(repo_star::Column::RepoId.eq(repo_id)) .all(db) .await? .len() as i32; let am = repos::ActiveModel { id: Unchanged(repo_id), nums_star: Set(after), updated_at: Set(now), ..Default::default() }; am.update(db).await?; Ok(RepoStarResp { stars: after }) } pub async fn unstar_repo( &self, ctx: &GitContext, user_id: i64, ) -> Result { let db = self.db_write(); let repo_id = ctx.model.id; let now = OffsetDateTime::now_utc(); TransactionTrait::transaction(db, |txn| { Box::pin(async move { repo_star::Entity::delete_many() .filter(repo_star::Column::UserId.eq(user_id)) .filter(repo_star::Column::RepoId.eq(repo_id)) .exec(txn) .await?; Ok::<(), sea_orm::DbErr>(()) }) }) .await?; let after = repo_star::Entity::find() .filter(repo_star::Column::RepoId.eq(repo_id)) .all(db) .await? .len() as i32; let am = repos::ActiveModel { id: Unchanged(repo_id), nums_star: Set(after), updated_at: Set(now), ..Default::default() }; am.update(db).await?; Ok(RepoStarResp { stars: after }) } pub async fn is_starred(&self, ctx: &GitContext, user_id: i64) -> Result { let row = repo_star::Entity::find() .filter(repo_star::Column::UserId.eq(user_id)) .filter(repo_star::Column::RepoId.eq(ctx.model.id)) .one(self.db_read()) .await?; Ok(row.is_some()) } pub async fn list_stargazers( &self, ctx: &GitContext, limit: u64, offset: u64, ) -> Result, GitError> { let rows = repo_star::Entity::find() .filter(repo_star::Column::RepoId.eq(ctx.model.id)) .order_by_desc(repo_star::Column::CreatedAt) .limit(limit) .offset(offset) .all(self.db_read()) .await?; if rows.is_empty() { return Ok(Vec::new()); } let user_ids: Vec = rows.iter().map(|r| r.user_id).collect(); let users = models::users::user::Entity::find() .filter(models::users::user::Column::Uid.is_in(user_ids.iter().copied())) .all(self.db_read()) .await?; let by_id: std::collections::HashMap = users.into_iter().map(|u| (u.uid, u)).collect(); Ok(rows .into_iter() .filter_map(|row| { by_id.get(&row.user_id).map(|u| StargazerDto { user_id: u.uid, username: u.username.clone(), display_name: u.display_name.clone(), avatar_url: export::resolve_user_avatar_url(u.uid, &u.avatar_url), starred_at: row.created_at, }) }) .collect()) } }