use models::issue::issue_user; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set, }; use serde::{Deserialize, Serialize}; use space::services::spaces::UserBriefDto; use time::OffsetDateTime; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; use crate::errors::IssueError; use crate::services::service::IssueService; use crate::share::lookup::{lookup_board, lookup_issue, lookup_space_by_name, require_member}; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct IssueUserDto { pub id: Uuid, pub uid: Uuid, pub user: Option, pub issue_id: Uuid, pub is_read: bool, pub created_at: OffsetDateTime, } #[derive(Debug, Clone, Serialize, ToSchema)] pub struct IssueUserPage { pub items: Vec, pub total: i64, } #[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: Uuid, pub uid: Uuid, } impl IssueService { pub async fn list_issue_users( &self, caller: Uuid, space_name: &str, board_id: Uuid, issue_id: Uuid, q: ListIssueUserQuery, ) -> Result { let space = lookup_space_by_name(self, space_name).await?; require_member(self, &space, caller).await?; let board = lookup_board(self, &space, board_id).await?; let _ = lookup_issue(self, &board, issue_id).await?; let max_pagination = self.config().issues.max_pagination; let default_pagination = self.config().issues.default_pagination; let limit = q.limit.unwrap_or(default_pagination).clamp(1, max_pagination); let offset = q.offset.unwrap_or(0); 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, uid: u.uid, user, issue_id: u.issue_id, is_read: u.is_read, created_at: u.created_at, } }) .collect(); Ok(IssueUserPage { items, total }) } pub async fn update_issue_user( &self, caller: Uuid, space_name: &str, board_id: Uuid, issue_id: Uuid, uid: Uuid, cmd: UpdateIssueUserCmd, ) -> Result { let space = lookup_space_by_name(self, space_name).await?; require_member(self, &space, caller).await?; let board = lookup_board(self, &space, board_id).await?; let _ = lookup_issue(self, &board, 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, uid: updated.uid, user, issue_id: updated.issue_id, is_read: updated.is_read, created_at: updated.created_at, }) } }