first commit
This commit is contained in:
451
backend_rust/src/api/reminder_tasks.rs
Normal file
451
backend_rust/src/api/reminder_tasks.rs
Normal file
@@ -0,0 +1,451 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user