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, pub by_weekday: Option, pub by_monthday: Option, pub timezone: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OffsetInput { pub offset_minutes: i32, pub channel_inapp: Option, pub channel_bark: Option, /// Custom title for Bark push notification pub bark_title: Option, /// Custom subtitle for Bark push notification pub bark_subtitle: Option, /// Markdown content for Bark push (overrides body if set) pub bark_body_markdown: Option, /// Alert level: active, timeSensitive, passive, critical pub bark_level: Option, /// Custom icon URL for Bark push pub bark_icon: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ReminderTaskInput { pub title: String, pub description: Option, pub due_at: chrono::DateTime, pub recipient_ids: Vec, pub recurrence_rule: Option, pub offsets: Option>, } #[derive(Debug, Serialize)] pub struct OkResponse { pub ok: bool, } #[get("")] async fn list_tasks( app_data: web::Data, auth: AuthUser, ) -> Result { 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 = 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 = 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, auth: AuthUser, body: web::Json, ) -> Result { 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, auth: AuthUser, path: web::Path, ) -> Result { 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, auth: AuthUser, path: web::Path, body: web::Json, ) -> Result { 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, auth: AuthUser, path: web::Path, ) -> Result { 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 { 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) }