Skip to main content

tdm_server_rust/service/
editor_service.rs

1//! 编辑器服务 (Editor Service)
2//!
3//! 提供编辑器工作区、页面、标记单元的读写与整话工作流确认。
4//!
5//! - 页面只读图片 OSS 地址(由图源导入服务生成)
6//! - 标记单元按 `page_id` 整页替换保存,分片键 `episode_id` 由页面解析得到
7//! - 页面确认只更新编辑器页面状态;整话确认回写 `mangaepisodedetail` 并失效相关缓存
8
9use crate::app::AppState;
10use crate::cache::on_episode_mutated;
11use crate::entity::editor::{
12    EditorPageInfo, EditorUnitInfo, EditorWorkspace, SaveEditorUnitsRequest,
13};
14use crate::error::{ApiResult, AppError};
15use crate::repository::editor_page_repo::EditorPageRepository;
16use crate::repository::editor_unit_repo::EditorUnitRepository;
17use crate::repository::episode_repo::EpisodeRepository;
18use crate::repository::manga_repo::MangaRepository;
19
20/// 编辑器服务
21pub struct EditorService;
22
23impl EditorService {
24    /// 获取编辑器工作区(话数信息 + 页面列表)
25    #[tracing::instrument(skip_all, level = "debug")]
26    pub async fn get_workspace(state: &AppState, episode_id: i32) -> ApiResult<EditorWorkspace> {
27        let episode_repo = EpisodeRepository::new(state.db.clone());
28        let manga_id = episode_repo
29            .get_manga_id_by_episode_id(episode_id)
30            .await?
31            .ok_or_else(|| AppError::business(format!("漫画单话 {episode_id} 不存在喵")))?;
32        let manga_name = MangaRepository::new(state.db.clone())
33            .get_manga_by_id(manga_id)
34            .await?
35            .and_then(|manga| manga.manga_tran_name);
36        let has_provider_source = episode_repo
37            .get_provider_oss_id(episode_id)
38            .await?
39            .is_some();
40        let pages = EditorPageRepository::new(state.db.clone())
41            .list_by_episode(episode_id)
42            .await?
43            .into_iter()
44            .map(EditorPageInfo::from)
45            .collect();
46        Ok(EditorWorkspace {
47            episode_id,
48            manga_id,
49            manga_name,
50            has_provider_source,
51            pages,
52        })
53    }
54
55    /// 查询某话数全部页面
56    #[tracing::instrument(skip_all, level = "debug")]
57    pub async fn list_pages(state: &AppState, episode_id: i32) -> ApiResult<Vec<EditorPageInfo>> {
58        let pages = EditorPageRepository::new(state.db.clone())
59            .list_by_episode(episode_id)
60            .await?
61            .into_iter()
62            .map(EditorPageInfo::from)
63            .collect();
64        Ok(pages)
65    }
66
67    /// 查询某页标记单元(分片键 episode_id 由页面解析得到)
68    #[tracing::instrument(skip_all, level = "debug")]
69    pub async fn list_units(state: &AppState, page_id: i64) -> ApiResult<Vec<EditorUnitInfo>> {
70        let page_repo = EditorPageRepository::new(state.db.clone());
71        let episode_id = page_repo
72            .episode_of_page(page_id)
73            .await?
74            .ok_or_else(|| AppError::business(format!("页面 {page_id} 不存在喵")))?;
75        let units = EditorUnitRepository::new(state.db.clone())
76            .list_by_page(episode_id, page_id)
77            .await?
78            .into_iter()
79            .map(EditorUnitInfo::from)
80            .collect();
81        Ok(units)
82    }
83
84    /// 整页替换式保存标记单元
85    #[tracing::instrument(skip_all, level = "debug")]
86    pub async fn save_units(
87        state: &AppState,
88        req: SaveEditorUnitsRequest,
89        editor_member_id: i32,
90    ) -> ApiResult<usize> {
91        let page_repo = EditorPageRepository::new(state.db.clone());
92        let episode_id = page_repo
93            .episode_of_page(req.page_id)
94            .await?
95            .ok_or_else(|| AppError::business(format!("页面 {} 不存在喵", req.page_id)))?;
96        let saved = EditorUnitRepository::new(state.db.clone())
97            .replace_page_units(episode_id, req.page_id, editor_member_id, &req.units)
98            .await?;
99        Ok(saved)
100    }
101
102    /// 设置/取消页面翻译完成状态
103    #[tracing::instrument(skip_all, level = "debug")]
104    pub async fn set_page_translation_completion(
105        state: &AppState,
106        page_id: i64,
107        completed: bool,
108    ) -> ApiResult<()> {
109        EditorPageRepository::new(state.db.clone())
110            .set_translation_completed(page_id, completed)
111            .await
112    }
113
114    /// 整话工作流确认:回写 `mangaepisodedetail` 交稿时间并失效相关缓存
115    ///
116    /// `role` 仅支持 `translator` / `proofreader`。
117    #[tracing::instrument(skip_all, level = "debug")]
118    pub async fn complete_episode_workflow(
119        state: &AppState,
120        episode_id: i32,
121        role: &str,
122        member_id: i32,
123    ) -> ApiResult<()> {
124        let episode_repo = EpisodeRepository::new(state.db.clone());
125        let manga_id = episode_repo
126            .get_manga_id_by_episode_id(episode_id)
127            .await?
128            .ok_or_else(|| AppError::business(format!("漫画单话 {episode_id} 不存在喵")))?;
129        episode_repo
130            .touch_detail_submit_time(episode_id, role)
131            .await?;
132        // 失效现有话数缓存、任务看板、统计缓存(沿用统一失效入口)
133        on_episode_mutated(state, manga_id, &[member_id]).await;
134        Ok(())
135    }
136}