Skip to main content

tdm_server_rust/repository/
editor_page_repo.rs

1//! 编辑器页面数据访问层 (Editor Page Repository)
2//!
3//! 封装 `editor_page` 表的查询与写入(SeaORM)。所有查询强制以 `episode_id` 过滤,
4//! 与分片键对齐,禁止全表扫描。
5
6use crate::db::DbConn;
7use crate::error::ApiResult;
8use crate::sea_entity::editor_page;
9use sea_orm::{
10    ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait,
11    PaginatorTrait, QueryFilter, QueryOrder, Set,
12};
13use sqlx::postgres::PgPool;
14
15/// 新建页面的入参(服务层组装后传入)
16#[derive(Debug, Clone)]
17pub struct NewEditorPage {
18    /// 页序号
19    pub page_index: i32,
20    /// 图源 OSS 记录 ID
21    pub source_oss_id: Option<i32>,
22    /// 原始文件名
23    pub source_file_name: Option<String>,
24    /// 图片 OSS 对象键
25    pub image_object_key: String,
26    /// 图片 OSS 桶名
27    pub image_bucket: Option<String>,
28    /// 图片访问 URL
29    pub image_url: String,
30    /// 图片宽度
31    pub image_width: Option<i32>,
32    /// 图片高度
33    pub image_height: Option<i32>,
34}
35
36/// 编辑器页面仓储
37pub struct EditorPageRepository {
38    /// SeaORM 连接
39    db: DbConn,
40}
41
42impl EditorPageRepository {
43    /// 从 `PgPool` 构造
44    pub fn new(pool: PgPool) -> Self {
45        Self {
46            db: crate::db::from_sqlx_pool(pool),
47        }
48    }
49
50    /// 从 `DatabaseConnection` 构造
51    pub fn from_db(db: DatabaseConnection) -> Self {
52        Self { db }
53    }
54
55    /// 查询某话数全部页面,按页序升序
56    #[tracing::instrument(skip_all, level = "debug")]
57    pub async fn list_by_episode(&self, episode_id: i32) -> ApiResult<Vec<editor_page::Model>> {
58        Self::list_by_episode_with(&self.db, episode_id).await
59    }
60
61    /// 使用指定连接查询某话数全部页面,供事务内复用。
62    pub async fn list_by_episode_with<C: ConnectionTrait>(
63        db: &C,
64        episode_id: i32,
65    ) -> ApiResult<Vec<editor_page::Model>> {
66        let rows = editor_page::Entity::find()
67            .filter(editor_page::Column::EpisodeId.eq(episode_id))
68            .order_by_asc(editor_page::Column::PageIndex)
69            .all(db)
70            .await?;
71        Ok(rows)
72    }
73
74    /// 统计某话数页面数
75    #[tracing::instrument(skip_all, level = "debug")]
76    pub async fn count_by_episode(&self, episode_id: i32) -> ApiResult<u64> {
77        let n = editor_page::Entity::find()
78            .filter(editor_page::Column::EpisodeId.eq(episode_id))
79            .count(&self.db)
80            .await?;
81        Ok(n)
82    }
83
84    /// 删除某话数全部页面,返回被删页面的 ID 列表(供级联删除标记)
85    #[tracing::instrument(skip_all, level = "debug")]
86    pub async fn delete_by_episode(&self, episode_id: i32) -> ApiResult<Vec<i64>> {
87        Self::delete_by_episode_with(&self.db, episode_id).await
88    }
89
90    /// 使用指定连接删除某话数全部页面,供事务内复用。
91    pub async fn delete_by_episode_with<C: ConnectionTrait>(
92        db: &C,
93        episode_id: i32,
94    ) -> ApiResult<Vec<i64>> {
95        let ids: Vec<i64> = editor_page::Entity::find()
96            .filter(editor_page::Column::EpisodeId.eq(episode_id))
97            .all(db)
98            .await?
99            .into_iter()
100            .map(|m| m.id)
101            .collect();
102        editor_page::Entity::delete_many()
103            .filter(editor_page::Column::EpisodeId.eq(episode_id))
104            .exec(db)
105            .await?;
106        Ok(ids)
107    }
108
109    /// 批量插入页面记录
110    #[tracing::instrument(skip_all, level = "debug")]
111    pub async fn insert_pages(
112        &self,
113        episode_id: i32,
114        pages: Vec<NewEditorPage>,
115    ) -> ApiResult<Vec<editor_page::Model>> {
116        Self::insert_pages_with(&self.db, episode_id, pages).await
117    }
118
119    /// 使用指定连接批量插入页面,供事务内原子重建复用。
120    pub async fn insert_pages_with<C: ConnectionTrait>(
121        db: &C,
122        episode_id: i32,
123        pages: Vec<NewEditorPage>,
124    ) -> ApiResult<Vec<editor_page::Model>> {
125        if pages.is_empty() {
126            return Ok(Vec::new());
127        }
128        let models: Vec<editor_page::ActiveModel> = pages
129            .into_iter()
130            .map(|p| editor_page::ActiveModel {
131                episode_id: Set(episode_id),
132                page_index: Set(p.page_index),
133                source_oss_id: Set(p.source_oss_id),
134                source_file_name: Set(p.source_file_name),
135                image_object_key: Set(p.image_object_key),
136                image_bucket: Set(p.image_bucket),
137                image_url: Set(p.image_url),
138                image_width: Set(p.image_width),
139                image_height: Set(p.image_height),
140                uploaded: Set(true),
141                ..Default::default()
142            })
143            .collect();
144        editor_page::Entity::insert_many(models).exec(db).await?;
145        Self::list_by_episode_with(db, episode_id).await
146    }
147
148    /// 校验页面归属话数(返回页面所属 episode_id)
149    #[tracing::instrument(skip_all, level = "debug")]
150    pub async fn episode_of_page(&self, page_id: i64) -> ApiResult<Option<i32>> {
151        let row = editor_page::Entity::find_by_id(page_id)
152            .one(&self.db)
153            .await?;
154        Ok(row.map(|m| m.episode_id))
155    }
156
157    /// 设置页面翻译完成状态
158    #[tracing::instrument(skip_all, level = "debug")]
159    pub async fn set_translation_completed(&self, page_id: i64, completed: bool) -> ApiResult<()> {
160        let Some(model) = editor_page::Entity::find_by_id(page_id)
161            .one(&self.db)
162            .await?
163        else {
164            return Err(crate::error::AppError::business(format!(
165                "页面 {page_id} 不存在喵"
166            )));
167        };
168        let mut am: editor_page::ActiveModel = model.into();
169        am.translation_completed = Set(completed);
170        am.updated_at = Set(chrono::Utc::now().naive_utc());
171        am.update(&self.db).await?;
172        Ok(())
173    }
174}