feat: init

This commit is contained in:
zhenyi
2026-06-07 11:30:56 +08:00
commit 563381c1ca
361 changed files with 41327 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
use serde::{Deserialize, Serialize};
use crate::error::AppError;
use crate::models::users::UserProfile;
use crate::service::UserService;
use crate::session::Session;
use super::util::merge_optional_text;
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct UpdateUserProfileParams {
pub full_name: Option<String>,
pub company: Option<String>,
pub location: Option<String>,
pub website_url: Option<String>,
pub twitter_username: Option<String>,
pub timezone: Option<String>,
pub language: Option<String>,
pub profile_readme: Option<String>,
}
impl UserService {
pub async fn user_profile(&self, ctx: &Session) -> Result<UserProfile, AppError> {
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
self.ensure_user_profile(user_uid).await
}
pub async fn user_update_profile(
&self,
ctx: &Session,
params: UpdateUserProfileParams,
) -> Result<UserProfile, AppError> {
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
let current = self.ensure_user_profile(user_uid).await?;
let now = chrono::Utc::now();
sqlx::query_as::<_, UserProfile>(
"UPDATE user_profile SET full_name = $1, company = $2, location = $3, website_url = $4, \
twitter_username = $5, timezone = $6, language = $7, profile_readme = $8, updated_at = $9 \
WHERE user_id = $10 RETURNING user_id, full_name, company, location, website_url, \
twitter_username, timezone, language, profile_readme, created_at, updated_at",
)
.bind(merge_optional_text(params.full_name, current.full_name))
.bind(merge_optional_text(params.company, current.company))
.bind(merge_optional_text(params.location, current.location))
.bind(merge_optional_text(params.website_url, current.website_url))
.bind(merge_optional_text(params.twitter_username, current.twitter_username))
.bind(merge_optional_text(params.timezone, current.timezone))
.bind(merge_optional_text(params.language, current.language))
.bind(merge_optional_text(params.profile_readme, current.profile_readme))
.bind(now)
.bind(user_uid)
.fetch_one(self.ctx.db.writer())
.await
.map_err(AppError::Database)
}
async fn ensure_user_profile(&self, user_uid: uuid::Uuid) -> Result<UserProfile, AppError> {
if let Some(profile) = self.find_user_profile(user_uid).await? {
return Ok(profile);
}
let now = chrono::Utc::now();
sqlx::query(
"INSERT INTO user_profile (user_id, created_at, updated_at) VALUES ($1, $2, $2) ON CONFLICT (user_id) DO NOTHING",
)
.bind(user_uid)
.bind(now)
.execute(self.ctx.db.writer())
.await
.map_err(AppError::Database)?;
self.find_user_profile(user_uid)
.await?
.ok_or(AppError::UserNotFound)
}
async fn find_user_profile(
&self,
user_uid: uuid::Uuid,
) -> Result<Option<UserProfile>, AppError> {
sqlx::query_as::<_, UserProfile>(
"SELECT user_id, full_name, company, location, website_url, twitter_username, \
timezone, language, profile_readme, created_at, updated_at \
FROM user_profile WHERE user_id = $1",
)
.bind(user_uid)
.fetch_optional(self.ctx.db.reader())
.await
.map_err(AppError::Database)
}
}