use models::issue::issue_user; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set, }; use serde::{Deserialize, Serialize}; use space::services::dto::UserDto; use time::OffsetDateTime; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; use crate::errors::IssueError; use crate::services::service::IssueService; use crate::share::lookup; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct IssueUserDto { pub id: Uuid, pub user_id: i64, pub user: Option, pub issue_id: Uuid, pub is_read: bool, pub created_at: OffsetDateTime, } pub type IssueUserPage = export::pagination::Page; #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct UpdateIssueUserCmd { #[serde(default)] pub is_read: Option, } #[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)] pub struct ListIssueUserQuery { pub limit: Option, pub offset: Option, } #[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)] pub struct IssueUserPath { pub space_name: String, pub board_id: Uuid, pub issue_id: String, pub user_id: i64, } impl IssueService { pub async fn list_issue_users( &self, caller: i64, space_name: &str, board_id: Uuid, issue_id: Uuid, q: ListIssueUserQuery, ) -> Result { lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?; let (limit, offset) = self.paginate(q.limit, q.offset); let select = issue_user::Entity::find() .filter(issue_user::Column::IssueId.eq(issue_id)) .order_by_desc(issue_user::Column::CreatedAt); let total = select.clone().count(self.db().read()).await? as i64; let rows = select .limit(limit) .offset(offset) .all(self.db().read()) .await?; let uids: Vec = rows.iter().map(|u| u.uid).collect(); let user_map = self.load_user_briefs(&uids).await?; let items = rows .into_iter() .map(|u| { let user = user_map.get(&u.uid).cloned(); IssueUserDto { id: u.id, user_id: u.uid, user, issue_id: u.issue_id, is_read: u.is_read, created_at: u.created_at, } }) .collect(); Ok(IssueUserPage { items, total, limit, offset, }) } #[tracing::instrument(name = "issues.update_issue_user", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id, uid = %uid))] pub async fn update_issue_user( &self, caller: i64, space_name: &str, board_id: Uuid, issue_id: Uuid, uid: i64, cmd: UpdateIssueUserCmd, ) -> Result { lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?; if caller != uid { return Err(IssueError::Forbidden); } let existing = issue_user::Entity::find() .filter(issue_user::Column::Uid.eq(uid)) .filter(issue_user::Column::IssueId.eq(issue_id)) .one(self.db().read()) .await?; let now = OffsetDateTime::now_utc(); let updated = if let Some(row) = existing { let mut am: issue_user::ActiveModel = row.into(); if let Some(r) = cmd.is_read { am.is_read = Set(r); } am.update(self.db().write()).await? } else { let row = issue_user::ActiveModel { id: Set(Uuid::now_v7()), uid: Set(uid), issue_id: Set(issue_id), is_read: Set(cmd.is_read.unwrap_or(false)), created_at: Set(now), }; row.insert(self.db().write()).await? }; let user_map = self.load_user_briefs(&[uid]).await?; let user = user_map.get(&uid).cloned(); Ok(IssueUserDto { id: updated.id, user_id: updated.uid, user, issue_id: updated.issue_id, is_read: updated.is_read, created_at: updated.created_at, }) } }