fix(diary): 添加事务 — create_class/join_class/parent 删除原子化
- create_class: 班级创建 + 老师成员插入包裹在 db.transaction() 中 - join_class: 成员插入 + member_count 更新包裹在事务中 - delete_child_data: PIPL 删除权 — 逐条软删除包裹在事务中(避免部分删除) - DiaryError: 添加 From<TransactionError<DiaryError>> 支持事务闭包 审计 ID: B-07, B-11, 8a-C03
This commit is contained in:
@@ -106,6 +106,18 @@ impl From<sea_orm::DbErr> for DiaryError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 支持 db.transaction() 闭包的错误转换
|
||||||
|
impl From<sea_orm::TransactionError<DiaryError>> for DiaryError {
|
||||||
|
fn from(err: sea_orm::TransactionError<DiaryError>) -> Self {
|
||||||
|
match err {
|
||||||
|
sea_orm::TransactionError::Connection(db_err) => {
|
||||||
|
DiaryError::Internal(db_err.to_string())
|
||||||
|
}
|
||||||
|
sea_orm::TransactionError::Transaction(inner) => inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use chrono::{Months, Utc};
|
use chrono::{Months, Utc};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter,
|
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter,
|
||||||
Set,
|
Set, TransactionTrait,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -37,42 +37,49 @@ impl ClassService {
|
|||||||
// 过期时间:6 个月后
|
// 过期时间:6 个月后
|
||||||
let expires_at = now.checked_add_months(Months::new(6));
|
let expires_at = now.checked_add_months(Months::new(6));
|
||||||
|
|
||||||
// 创建班级记录
|
// 事务:创建班级 + 老师自动加入(原子操作,保证一致性)
|
||||||
let class_model = school_class::ActiveModel {
|
let inserted_class = db
|
||||||
id: Set(id),
|
.transaction::<_, school_class::Model, DiaryError>(|txn| {
|
||||||
tenant_id: Set(tenant_id),
|
Box::pin(async move {
|
||||||
name: Set(name),
|
let class_model = school_class::ActiveModel {
|
||||||
school_name: Set(school_name),
|
id: Set(id),
|
||||||
teacher_id: Set(teacher_id),
|
tenant_id: Set(tenant_id),
|
||||||
class_code: Set(class_code.clone()),
|
name: Set(name),
|
||||||
member_count: Set(1), // 老师自动计入
|
school_name: Set(school_name),
|
||||||
is_active: Set(true),
|
teacher_id: Set(teacher_id),
|
||||||
expires_at: Set(expires_at),
|
class_code: Set(class_code),
|
||||||
created_at: Set(now),
|
member_count: Set(1), // 老师自动计入
|
||||||
updated_at: Set(now),
|
is_active: Set(true),
|
||||||
created_by: Set(teacher_id),
|
expires_at: Set(expires_at),
|
||||||
updated_by: Set(teacher_id),
|
created_at: Set(now),
|
||||||
deleted_at: Set(None),
|
updated_at: Set(now),
|
||||||
version: Set(1),
|
created_by: Set(teacher_id),
|
||||||
};
|
updated_by: Set(teacher_id),
|
||||||
let inserted_class = class_model.insert(db).await?;
|
deleted_at: Set(None),
|
||||||
|
version: Set(1),
|
||||||
|
};
|
||||||
|
let inserted = class_model.insert(txn).await?;
|
||||||
|
|
||||||
// 自动将老师加入成员表
|
let member_model = class_member::ActiveModel {
|
||||||
let member_model = class_member::ActiveModel {
|
class_id: Set(id),
|
||||||
class_id: Set(id),
|
user_id: Set(teacher_id),
|
||||||
user_id: Set(teacher_id),
|
tenant_id: Set(tenant_id),
|
||||||
tenant_id: Set(tenant_id),
|
role: Set("teacher".to_string()),
|
||||||
role: Set("teacher".to_string()),
|
nickname: Set(None),
|
||||||
nickname: Set(None),
|
joined_at: Set(now),
|
||||||
joined_at: Set(now),
|
created_at: Set(now),
|
||||||
created_at: Set(now),
|
updated_at: Set(now),
|
||||||
updated_at: Set(now),
|
created_by: Set(teacher_id),
|
||||||
created_by: Set(teacher_id),
|
updated_by: Set(teacher_id),
|
||||||
updated_by: Set(teacher_id),
|
deleted_at: Set(None),
|
||||||
deleted_at: Set(None),
|
version: Set(1),
|
||||||
version: Set(1),
|
};
|
||||||
};
|
member_model.insert(txn).await?;
|
||||||
member_model.insert(db).await?;
|
|
||||||
|
Ok(inserted)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
// 发布 ClassCreated 事件
|
// 发布 ClassCreated 事件
|
||||||
event_bus
|
event_bus
|
||||||
@@ -175,31 +182,38 @@ impl ClassService {
|
|||||||
return Err(DiaryError::BadRequest("已是班级成员".to_string()));
|
return Err(DiaryError::BadRequest("已是班级成员".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 创建成员记录
|
// 5. 事务:创建成员记录 + 更新 member_count(原子操作)
|
||||||
let member_model = class_member::ActiveModel {
|
|
||||||
class_id: Set(class_id),
|
|
||||||
user_id: Set(user_id),
|
|
||||||
tenant_id: Set(tenant_id),
|
|
||||||
role: Set("student".to_string()),
|
|
||||||
nickname: Set(nickname),
|
|
||||||
joined_at: Set(now),
|
|
||||||
created_at: Set(now),
|
|
||||||
updated_at: Set(now),
|
|
||||||
created_by: Set(user_id),
|
|
||||||
updated_by: Set(user_id),
|
|
||||||
deleted_at: Set(None),
|
|
||||||
version: Set(1),
|
|
||||||
};
|
|
||||||
member_model.insert(db).await?;
|
|
||||||
|
|
||||||
// 6. 更新 member_count
|
|
||||||
let current_count = class_model.member_count;
|
let current_count = class_model.member_count;
|
||||||
let current_version = class_model.version;
|
let current_version = class_model.version;
|
||||||
let mut active_class: school_class::ActiveModel = class_model.into();
|
let updated_class = db
|
||||||
active_class.member_count = Set(current_count + 1);
|
.transaction::<_, school_class::Model, DiaryError>(|txn| {
|
||||||
active_class.updated_at = Set(now);
|
Box::pin(async move {
|
||||||
active_class.version = Set(current_version + 1);
|
let member_model = class_member::ActiveModel {
|
||||||
let updated_class = active_class.update(db).await?;
|
class_id: Set(class_id),
|
||||||
|
user_id: Set(user_id),
|
||||||
|
tenant_id: Set(tenant_id),
|
||||||
|
role: Set("student".to_string()),
|
||||||
|
nickname: Set(nickname),
|
||||||
|
joined_at: Set(now),
|
||||||
|
created_at: Set(now),
|
||||||
|
updated_at: Set(now),
|
||||||
|
created_by: Set(user_id),
|
||||||
|
updated_by: Set(user_id),
|
||||||
|
deleted_at: Set(None),
|
||||||
|
version: Set(1),
|
||||||
|
};
|
||||||
|
member_model.insert(txn).await?;
|
||||||
|
|
||||||
|
let mut active_class: school_class::ActiveModel = class_model.into();
|
||||||
|
active_class.member_count = Set(current_count + 1);
|
||||||
|
active_class.updated_at = Set(now);
|
||||||
|
active_class.version = Set(current_version + 1);
|
||||||
|
let updated = active_class.update(txn).await?;
|
||||||
|
|
||||||
|
Ok(updated)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
// 7. 成功加入 → 清除错误计数
|
// 7. 成功加入 → 清除错误计数
|
||||||
if let Some(redis_client) = redis {
|
if let Some(redis_client) = redis {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait,
|
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait,
|
||||||
QueryFilter, QueryOrder, Set,
|
QueryFilter, QueryOrder, Set, TransactionTrait,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -176,15 +176,22 @@ impl ParentService {
|
|||||||
let count = journals.len();
|
let count = journals.len();
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
for journal in journals {
|
// 事务:软删除所有日记(PIPL 删除权 — 原子操作,避免部分删除)
|
||||||
let current_version = journal.version;
|
db.transaction::<_, (), DiaryError>(|txn| {
|
||||||
let mut active: journal_entry::ActiveModel = journal.into();
|
Box::pin(async move {
|
||||||
active.deleted_at = Set(Some(now));
|
for journal in journals {
|
||||||
active.updated_at = Set(now);
|
let current_version = journal.version;
|
||||||
active.updated_by = Set(parent_id);
|
let mut active: journal_entry::ActiveModel = journal.into();
|
||||||
active.version = Set(current_version + 1);
|
active.deleted_at = Set(Some(now));
|
||||||
active.update(db).await?;
|
active.updated_at = Set(now);
|
||||||
}
|
active.updated_by = Set(parent_id);
|
||||||
|
active.version = Set(current_version + 1);
|
||||||
|
active.update(txn).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
event_bus
|
event_bus
|
||||||
.publish(
|
.publish(
|
||||||
|
|||||||
Reference in New Issue
Block a user