feat(service): expand service layer with new domain operations

- Add IM service modules: audit, channel roles, custom emojis, forum
  tags, integrations, invitations, repo links, slash commands, stages,
  voice, webhooks
- Add PR service modules: review requests, templates
- Add repo service modules: contributors, release assets, git extras
  (archive, branch rename, commit extras, diff/merge, tag, tree)
- Add user service: social (follow/block)
- Add internal auth service
- Update existing service modules with expanded functionality
- Remove deleted IM modules: articles, delivery trace, drafts,
  follows, messages, polls, presence, reactions, threads
This commit is contained in:
zhenyi
2026-06-10 18:49:32 +08:00
parent cec6dce955
commit 420dedbc1e
100 changed files with 3797 additions and 3839 deletions
+226
View File
@@ -0,0 +1,226 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::AppError;
use crate::models::common::{DeviceType, PresenceStatus};
use crate::models::users::{UserBlock, UserFollow, UserPresence};
use crate::service::UserService;
use crate::session::Session;
use super::util::ensure_affected;
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UpdatePresenceParams {
pub status: PresenceStatus,
pub custom_status_text: Option<String>,
pub custom_status_emoji: Option<String>,
pub device_type: Option<DeviceType>,
pub ip_address: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
struct UserPresenceRow {
id: Uuid,
user_id: Uuid,
status: PresenceStatus,
custom_status_text: Option<String>,
custom_status_emoji: Option<String>,
device_type: Option<DeviceType>,
ip_address: Option<String>,
last_active_at: DateTime<Utc>,
last_seen_at: Option<DateTime<Utc>>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl From<UserPresenceRow> for UserPresence {
fn from(row: UserPresenceRow) -> Self {
Self {
id: row.id,
user_id: row.user_id,
status: row.status,
custom_status_text: row.custom_status_text,
custom_status_emoji: row.custom_status_emoji,
device_type: row.device_type,
ip_address: row.ip_address,
last_active_at: row.last_active_at,
last_seen_at: row.last_seen_at,
created_at: row.created_at,
updated_at: row.updated_at,
}
}
}
impl UserService {
pub async fn user_presence_get(&self, session: &Session) -> Result<UserPresence, AppError> {
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
let row = sqlx::query_as::<_, UserPresenceRow>(
"SELECT id, user_id, status, custom_status_text, custom_status_emoji, device_type, ip_address, \
last_active_at, last_seen_at, created_at, updated_at \
FROM user_presence WHERE user_id = $1",
)
.bind(user_uid)
.fetch_optional(self.ctx.db.reader())
.await
.map_err(AppError::Database)?;
row.map(Into::into)
.ok_or_else(|| AppError::NotFound("presence not found".into()))
}
pub async fn user_presence_update(
&self,
session: &Session,
params: UpdatePresenceParams,
) -> Result<UserPresence, AppError> {
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
let id = Uuid::now_v7();
let now = chrono::Utc::now();
let row = sqlx::query_as::<_, UserPresenceRow>(
"INSERT INTO user_presence (id, user_id, status, custom_status_text, custom_status_emoji, \
device_type, ip_address, last_active_at, last_seen_at, created_at, updated_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NULL, $9, $9) \
ON CONFLICT (user_id) DO UPDATE SET \
status = $3, custom_status_text = $4, custom_status_emoji = $5, \
device_type = $6, ip_address = $7, last_active_at = $8, updated_at = $9 \
RETURNING id, user_id, status, custom_status_text, custom_status_emoji, device_type, ip_address, \
last_active_at, last_seen_at, created_at, updated_at",
)
.bind(id)
.bind(user_uid)
.bind(&params.status)
.bind(&params.custom_status_text)
.bind(&params.custom_status_emoji)
.bind(&params.device_type)
.bind(&params.ip_address)
.bind(now)
.bind(now)
.fetch_one(self.ctx.db.writer())
.await
.map_err(AppError::Database)?;
Ok(row.into())
}
pub async fn user_blocks_list(
&self,
session: &Session,
limit: i64,
offset: i64,
) -> Result<Vec<UserBlock>, AppError> {
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
let limit = limit.clamp(1, 100);
let offset = offset.max(0);
sqlx::query_as::<_, UserBlock>(
"SELECT blocker_id, blocked_id, reason, created_at \
FROM user_block WHERE blocker_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
)
.bind(user_uid)
.bind(limit)
.bind(offset)
.fetch_all(self.ctx.db.reader())
.await
.map_err(AppError::Database)
}
pub async fn user_block_create(
&self,
session: &Session,
target_user_id: Uuid,
reason: Option<String>,
) -> Result<UserBlock, AppError> {
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
if user_uid == target_user_id {
return Err(AppError::BadRequest("cannot block yourself".into()));
}
let now = chrono::Utc::now();
sqlx::query_as::<_, UserBlock>(
"INSERT INTO user_block (blocker_id, blocked_id, reason, created_at) \
VALUES ($1, $2, $3, $4) \
RETURNING blocker_id, blocked_id, reason, created_at",
)
.bind(user_uid)
.bind(target_user_id)
.bind(&reason)
.bind(now)
.fetch_one(self.ctx.db.writer())
.await
.map_err(AppError::Database)
}
pub async fn user_block_delete(
&self,
session: &Session,
target_user_id: Uuid,
) -> Result<(), AppError> {
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
let result =
sqlx::query("DELETE FROM user_block WHERE blocker_id = $1 AND blocked_id = $2")
.bind(user_uid)
.bind(target_user_id)
.execute(self.ctx.db.writer())
.await
.map_err(AppError::Database)?;
ensure_affected(result.rows_affected(), "block not found")
}
pub async fn user_follows_list(
&self,
session: &Session,
limit: i64,
offset: i64,
) -> Result<Vec<UserFollow>, AppError> {
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
let limit = limit.clamp(1, 100);
let offset = offset.max(0);
sqlx::query_as::<_, UserFollow>(
"SELECT follower_id, following_id, created_at \
FROM user_follow WHERE follower_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
)
.bind(user_uid)
.bind(limit)
.bind(offset)
.fetch_all(self.ctx.db.reader())
.await
.map_err(AppError::Database)
}
pub async fn user_follow_create(
&self,
session: &Session,
target_user_id: Uuid,
) -> Result<UserFollow, AppError> {
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
if user_uid == target_user_id {
return Err(AppError::BadRequest("cannot follow yourself".into()));
}
let now = chrono::Utc::now();
sqlx::query_as::<_, UserFollow>(
"INSERT INTO user_follow (follower_id, following_id, created_at) \
VALUES ($1, $2, $3) \
RETURNING follower_id, following_id, created_at",
)
.bind(user_uid)
.bind(target_user_id)
.bind(now)
.fetch_one(self.ctx.db.writer())
.await
.map_err(AppError::Database)
}
pub async fn user_follow_delete(
&self,
session: &Session,
target_user_id: Uuid,
) -> Result<(), AppError> {
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
let result =
sqlx::query("DELETE FROM user_follow WHERE follower_id = $1 AND following_id = $2")
.bind(user_uid)
.bind(target_user_id)
.execute(self.ctx.db.writer())
.await
.map_err(AppError::Database)?;
ensure_affected(result.rows_affected(), "follow not found")
}
}