Files
notify/backend_rust/src/api/reminder_tasks.rs
Michael Dong a98e12f286 first commit
2026-02-05 11:24:40 +08:00

452 lines
17 KiB
Rust

use actix_web::{HttpResponse, Responder, Scope, delete, get, post, put, web};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::app_data::AppData;
use crate::entity::sea_orm_active_enums::{RecurrenceType, TargetType};
use crate::entity::{recurrence_rule, reminder_offset, reminder_task, reminder_task_recipient};
use crate::error::ApiError;
use crate::middleware::auth::AuthUser;
use crate::timer::WorkerCommand;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecurrenceRuleInput {
pub r#type: String,
pub interval: Option<i32>,
pub by_weekday: Option<i32>,
pub by_monthday: Option<i32>,
pub timezone: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OffsetInput {
pub offset_minutes: i32,
pub channel_inapp: Option<bool>,
pub channel_bark: Option<bool>,
/// Custom title for Bark push notification
pub bark_title: Option<String>,
/// Custom subtitle for Bark push notification
pub bark_subtitle: Option<String>,
/// Markdown content for Bark push (overrides body if set)
pub bark_body_markdown: Option<String>,
/// Alert level: active, timeSensitive, passive, critical
pub bark_level: Option<String>,
/// Custom icon URL for Bark push
pub bark_icon: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReminderTaskInput {
pub title: String,
pub description: Option<String>,
pub due_at: chrono::DateTime<chrono::Utc>,
pub recipient_ids: Vec<Uuid>,
pub recurrence_rule: Option<RecurrenceRuleInput>,
pub offsets: Option<Vec<OffsetInput>>,
}
#[derive(Debug, Serialize)]
pub struct OkResponse {
pub ok: bool,
}
#[get("")]
async fn list_tasks(
app_data: web::Data<AppData>,
auth: AuthUser,
) -> Result<impl Responder, ApiError> {
let items = reminder_task::Entity::find()
.filter(reminder_task::Column::CreatorId.eq(auth.user_id))
.order_by_desc(reminder_task::Column::DueAt)
.find_also_related(recurrence_rule::Entity)
.all(&app_data.db)
.await?;
let task_ids: Vec<Uuid> = items.iter().map(|(t, _)| t.id).collect();
let recipients = reminder_task_recipient::Entity::find()
.filter(reminder_task_recipient::Column::TaskId.is_in(task_ids.clone()))
.all(&app_data.db)
.await?;
let offsets = reminder_offset::Entity::find()
.filter(reminder_offset::Column::TargetType.eq(TargetType::ReminderTask))
.filter(reminder_offset::Column::TargetId.is_in(task_ids))
.all(&app_data.db)
.await?;
let result: Vec<serde_json::Value> = items
.into_iter()
.map(|(t, rule)| {
let task_recipients: Vec<_> = recipients
.iter()
.filter(|r| r.task_id == t.id)
.cloned()
.collect();
let task_offsets: Vec<_> = offsets
.iter()
.filter(|o| o.target_id == t.id)
.cloned()
.collect();
serde_json::json!({
"id": t.id,
"creatorId": t.creator_id,
"title": t.title,
"description": t.description,
"dueAt": t.due_at,
"recurrenceRuleId": t.recurrence_rule_id,
"createdAt": t.created_at,
"updatedAt": t.updated_at,
"recurrenceRule": rule,
"recipients": task_recipients,
"offsets": task_offsets,
})
})
.collect();
Ok(HttpResponse::Ok().json(result))
}
#[post("")]
async fn create_task(
app_data: web::Data<AppData>,
auth: AuthUser,
body: web::Json<ReminderTaskInput>,
) -> Result<impl Responder, ApiError> {
if body.title.is_empty() {
return Err(ApiError::BadRequest("Invalid payload".to_string()));
}
let now = chrono::Utc::now().fixed_offset();
let body = body.into_inner();
let user_id = auth.user_id;
let result = app_data
.db
.transaction::<_, reminder_task::Model, ApiError>(|txn| {
Box::pin(async move {
// Create recurrence rule if provided
let rule_id = if let Some(rule_input) = body.recurrence_rule {
let rule = recurrence_rule::ActiveModel {
id: Set(Uuid::new_v4()),
r#type: Set(parse_recurrence_type(&rule_input.r#type)?),
interval: Set(rule_input.interval.unwrap_or(1)),
by_weekday: Set(rule_input.by_weekday),
by_monthday: Set(rule_input.by_monthday),
timezone: Set(rule_input.timezone.unwrap_or("Asia/Shanghai".to_string())),
created_at: Set(now),
updated_at: Set(now),
};
let created = rule.insert(txn).await?;
Some(created.id)
} else {
None
};
// Create task
let new_task = reminder_task::ActiveModel {
id: Set(Uuid::new_v4()),
creator_id: Set(user_id),
title: Set(body.title),
description: Set(body.description),
due_at: Set(body.due_at.fixed_offset()),
recurrence_rule_id: Set(rule_id),
created_at: Set(now),
updated_at: Set(now),
};
let created_task = new_task.insert(txn).await?;
// Create recipients
for recipient_id in body.recipient_ids {
let new_recipient = reminder_task_recipient::ActiveModel {
task_id: Set(created_task.id),
user_id: Set(recipient_id),
};
new_recipient.insert(txn).await?;
}
// Create offsets
if let Some(offsets) = body.offsets {
for offset in offsets {
let new_offset = reminder_offset::ActiveModel {
id: Set(Uuid::new_v4()),
target_type: Set(TargetType::ReminderTask),
target_id: Set(created_task.id),
offset_minutes: Set(offset.offset_minutes),
channel_inapp: Set(offset.channel_inapp.unwrap_or(true)),
channel_bark: Set(offset.channel_bark.unwrap_or(false)),
created_at: Set(now),
bark_title: Set(offset.bark_title),
bark_subtitle: Set(offset.bark_subtitle),
bark_body_markdown: Set(offset.bark_body_markdown),
bark_level: Set(offset.bark_level),
bark_icon: Set(offset.bark_icon),
};
new_offset.insert(txn).await?;
}
}
Ok(created_task)
})
})
.await
.map_err(|e| match e {
sea_orm::TransactionError::Connection(e) => ApiError::Internal(e.to_string()),
sea_orm::TransactionError::Transaction(e) => e,
})?;
// 触发通知生成
let _ = app_data
.send_worker_command(WorkerCommand::GenerateNotifications {
target_type: TargetType::ReminderTask,
target_id: result.id,
})
.await;
Ok(HttpResponse::Ok().json(result))
}
#[get("/{id}")]
async fn get_task(
app_data: web::Data<AppData>,
auth: AuthUser,
path: web::Path<Uuid>,
) -> Result<impl Responder, ApiError> {
let id = path.into_inner();
let (t, rule) = reminder_task::Entity::find_by_id(id)
.filter(reminder_task::Column::CreatorId.eq(auth.user_id))
.find_also_related(recurrence_rule::Entity)
.one(&app_data.db)
.await?
.ok_or_else(|| ApiError::NotFound("Not found".to_string()))?;
let recipients = reminder_task_recipient::Entity::find()
.filter(reminder_task_recipient::Column::TaskId.eq(t.id))
.all(&app_data.db)
.await?;
let offsets = reminder_offset::Entity::find()
.filter(reminder_offset::Column::TargetType.eq(TargetType::ReminderTask))
.filter(reminder_offset::Column::TargetId.eq(t.id))
.all(&app_data.db)
.await?;
let result = serde_json::json!({
"id": t.id,
"creatorId": t.creator_id,
"title": t.title,
"description": t.description,
"dueAt": t.due_at,
"recurrenceRuleId": t.recurrence_rule_id,
"createdAt": t.created_at,
"updatedAt": t.updated_at,
"recurrenceRule": rule,
"recipients": recipients,
"offsets": offsets,
});
Ok(HttpResponse::Ok().json(result))
}
#[put("/{id}")]
async fn update_task(
app_data: web::Data<AppData>,
auth: AuthUser,
path: web::Path<Uuid>,
body: web::Json<ReminderTaskInput>,
) -> Result<impl Responder, ApiError> {
let id = path.into_inner();
if body.title.is_empty() {
return Err(ApiError::BadRequest("Invalid payload".to_string()));
}
let now = chrono::Utc::now().fixed_offset();
let body = body.into_inner();
let user_id = auth.user_id;
let result = app_data
.db
.transaction::<_, reminder_task::Model, ApiError>(|txn| {
Box::pin(async move {
let existing = reminder_task::Entity::find_by_id(id)
.filter(reminder_task::Column::CreatorId.eq(user_id))
.one(txn)
.await?
.ok_or_else(|| ApiError::NotFound("Not found".to_string()))?;
// Handle recurrence rule
let mut rule_id = existing.recurrence_rule_id;
if let Some(rule_input) = body.recurrence_rule {
if let Some(existing_rule_id) = rule_id {
// Update existing rule
let mut rule: recurrence_rule::ActiveModel =
recurrence_rule::Entity::find_by_id(existing_rule_id)
.one(txn)
.await?
.ok_or_else(|| ApiError::Internal("Rule not found".to_string()))?
.into();
rule.r#type = Set(parse_recurrence_type(&rule_input.r#type)?);
rule.interval = Set(rule_input.interval.unwrap_or(1));
rule.by_weekday = Set(rule_input.by_weekday);
rule.by_monthday = Set(rule_input.by_monthday);
rule.timezone =
Set(rule_input.timezone.unwrap_or("Asia/Shanghai".to_string()));
rule.updated_at = Set(now);
rule.update(txn).await?;
} else {
// Create new rule
let rule = recurrence_rule::ActiveModel {
id: Set(Uuid::new_v4()),
r#type: Set(parse_recurrence_type(&rule_input.r#type)?),
interval: Set(rule_input.interval.unwrap_or(1)),
by_weekday: Set(rule_input.by_weekday),
by_monthday: Set(rule_input.by_monthday),
timezone: Set(rule_input
.timezone
.unwrap_or("Asia/Shanghai".to_string())),
created_at: Set(now),
updated_at: Set(now),
};
let created = rule.insert(txn).await?;
rule_id = Some(created.id);
}
} else if let Some(existing_rule_id) = rule_id {
// Delete existing rule
recurrence_rule::Entity::delete_by_id(existing_rule_id)
.exec(txn)
.await?;
rule_id = None;
}
// Delete existing recipients and create new ones
reminder_task_recipient::Entity::delete_many()
.filter(reminder_task_recipient::Column::TaskId.eq(id))
.exec(txn)
.await?;
for recipient_id in body.recipient_ids {
let new_recipient = reminder_task_recipient::ActiveModel {
task_id: Set(id),
user_id: Set(recipient_id),
};
new_recipient.insert(txn).await?;
}
// Delete existing offsets and create new ones
reminder_offset::Entity::delete_many()
.filter(reminder_offset::Column::TargetType.eq(TargetType::ReminderTask))
.filter(reminder_offset::Column::TargetId.eq(id))
.exec(txn)
.await?;
if let Some(offsets) = body.offsets {
for offset in offsets {
let new_offset = reminder_offset::ActiveModel {
id: Set(Uuid::new_v4()),
target_type: Set(TargetType::ReminderTask),
target_id: Set(id),
offset_minutes: Set(offset.offset_minutes),
channel_inapp: Set(offset.channel_inapp.unwrap_or(true)),
channel_bark: Set(offset.channel_bark.unwrap_or(false)),
created_at: Set(now),
bark_title: Set(offset.bark_title),
bark_subtitle: Set(offset.bark_subtitle),
bark_body_markdown: Set(offset.bark_body_markdown),
bark_level: Set(offset.bark_level),
bark_icon: Set(offset.bark_icon),
};
new_offset.insert(txn).await?;
}
}
// Update task
let mut active: reminder_task::ActiveModel = existing.into();
active.title = Set(body.title);
active.description = Set(body.description);
active.due_at = Set(body.due_at.fixed_offset());
active.recurrence_rule_id = Set(rule_id);
active.updated_at = Set(now);
let updated = active.update(txn).await?;
Ok(updated)
})
})
.await
.map_err(|e| match e {
sea_orm::TransactionError::Connection(e) => ApiError::Internal(e.to_string()),
sea_orm::TransactionError::Transaction(e) => e,
})?;
// 触发通知重新生成
let _ = app_data
.send_worker_command(WorkerCommand::GenerateNotifications {
target_type: TargetType::ReminderTask,
target_id: result.id,
})
.await;
Ok(HttpResponse::Ok().json(result))
}
#[delete("/{id}")]
async fn delete_task(
app_data: web::Data<AppData>,
auth: AuthUser,
path: web::Path<Uuid>,
) -> Result<impl Responder, ApiError> {
let id = path.into_inner();
let result = reminder_task::Entity::delete_many()
.filter(reminder_task::Column::Id.eq(id))
.filter(reminder_task::Column::CreatorId.eq(auth.user_id))
.exec(&app_data.db)
.await?;
if result.rows_affected == 0 {
return Err(ApiError::NotFound("Not found".to_string()));
}
// Delete offsets
reminder_offset::Entity::delete_many()
.filter(reminder_offset::Column::TargetType.eq(TargetType::ReminderTask))
.filter(reminder_offset::Column::TargetId.eq(id))
.exec(&app_data.db)
.await?;
// Delete recipients
reminder_task_recipient::Entity::delete_many()
.filter(reminder_task_recipient::Column::TaskId.eq(id))
.exec(&app_data.db)
.await?;
Ok(HttpResponse::Ok().json(OkResponse { ok: true }))
}
fn parse_recurrence_type(s: &str) -> Result<RecurrenceType, ApiError> {
match s {
"hourly" => Ok(RecurrenceType::Hourly),
"daily" => Ok(RecurrenceType::Daily),
"weekly" => Ok(RecurrenceType::Weekly),
"monthly" => Ok(RecurrenceType::Monthly),
"yearly" => Ok(RecurrenceType::Yearly),
_ => Err(ApiError::BadRequest("Invalid recurrence type".to_string())),
}
}
pub fn routes() -> Scope {
web::scope("/api/reminder-tasks")
.service(list_tasks)
.service(create_task)
.service(get_task)
.service(update_task)
.service(delete_task)
}