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:
iven
2026-06-03 01:03:57 +08:00
parent 1750f17f41
commit e0052ea99b
3 changed files with 102 additions and 69 deletions

View File

@@ -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)]
mod tests {
use super::*;

View File

@@ -3,7 +3,7 @@
use chrono::{Months, Utc};
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter,
Set,
Set, TransactionTrait,
};
use uuid::Uuid;
@@ -37,42 +37,49 @@ impl ClassService {
// 过期时间6 个月后
let expires_at = now.checked_add_months(Months::new(6));
// 创建班级记录
let class_model = school_class::ActiveModel {
id: Set(id),
tenant_id: Set(tenant_id),
name: Set(name),
school_name: Set(school_name),
teacher_id: Set(teacher_id),
class_code: Set(class_code.clone()),
member_count: Set(1), // 老师自动计入
is_active: Set(true),
expires_at: Set(expires_at),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(teacher_id),
updated_by: Set(teacher_id),
deleted_at: Set(None),
version: Set(1),
};
let inserted_class = class_model.insert(db).await?;
// 事务:创建班级 + 老师自动加入(原子操作,保证一致性)
let inserted_class = db
.transaction::<_, school_class::Model, DiaryError>(|txn| {
Box::pin(async move {
let class_model = school_class::ActiveModel {
id: Set(id),
tenant_id: Set(tenant_id),
name: Set(name),
school_name: Set(school_name),
teacher_id: Set(teacher_id),
class_code: Set(class_code),
member_count: Set(1), // 老师自动计入
is_active: Set(true),
expires_at: Set(expires_at),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(teacher_id),
updated_by: Set(teacher_id),
deleted_at: Set(None),
version: Set(1),
};
let inserted = class_model.insert(txn).await?;
// 自动将老师加入成员表
let member_model = class_member::ActiveModel {
class_id: Set(id),
user_id: Set(teacher_id),
tenant_id: Set(tenant_id),
role: Set("teacher".to_string()),
nickname: Set(None),
joined_at: Set(now),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(teacher_id),
updated_by: Set(teacher_id),
deleted_at: Set(None),
version: Set(1),
};
member_model.insert(db).await?;
let member_model = class_member::ActiveModel {
class_id: Set(id),
user_id: Set(teacher_id),
tenant_id: Set(tenant_id),
role: Set("teacher".to_string()),
nickname: Set(None),
joined_at: Set(now),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(teacher_id),
updated_by: Set(teacher_id),
deleted_at: Set(None),
version: Set(1),
};
member_model.insert(txn).await?;
Ok(inserted)
})
})
.await?;
// 发布 ClassCreated 事件
event_bus
@@ -175,31 +182,38 @@ impl ClassService {
return Err(DiaryError::BadRequest("已是班级成员".to_string()));
}
// 5. 创建成员记录
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
// 5. 事务:创建成员记录 + 更新 member_count原子操作
let current_count = class_model.member_count;
let current_version = class_model.version;
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_class = active_class.update(db).await?;
let updated_class = db
.transaction::<_, school_class::Model, DiaryError>(|txn| {
Box::pin(async move {
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(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. 成功加入 → 清除错误计数
if let Some(redis_client) = redis {

View File

@@ -3,7 +3,7 @@
use chrono::Utc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait,
QueryFilter, QueryOrder, Set,
QueryFilter, QueryOrder, Set, TransactionTrait,
};
use uuid::Uuid;
@@ -176,15 +176,22 @@ impl ParentService {
let count = journals.len();
let now = Utc::now();
for journal in journals {
let current_version = journal.version;
let mut active: journal_entry::ActiveModel = journal.into();
active.deleted_at = Set(Some(now));
active.updated_at = Set(now);
active.updated_by = Set(parent_id);
active.version = Set(current_version + 1);
active.update(db).await?;
}
// 事务软删除所有日记PIPL 删除权 — 原子操作,避免部分删除)
db.transaction::<_, (), DiaryError>(|txn| {
Box::pin(async move {
for journal in journals {
let current_version = journal.version;
let mut active: journal_entry::ActiveModel = journal.into();
active.deleted_at = Set(Some(now));
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
.publish(