Skip to main content

tdm_server_rust/service/
task_tracking_service.rs

1//! 稿件监控服务 (Task Tracking Service)
2//!
3//! 组员任务看板的业务逻辑:
4//! - 组员任务数量统计(按岗位分类)
5//! - 全部待做稿件查询(带缓存)
6//! - 待发布/待审稿漫画分页
7
8use crate::{
9    app::AppState,
10    cache::{
11        get_episode_tasks_cached, get_member_task_counts_cached, set_episode_tasks_cached,
12        set_member_task_counts_cached,
13    },
14    common::PageBean,
15    entity::episode::{MangaEpisodeTb, MemberTaskCount, PendingEpisode, PendingMangaTask, TaskTrackingResponse},
16    error::ApiResult,
17    repository::task_tracking_repo::{PendingEpisodeRow, TaskTrackingRepository},
18    utils::{agent_debug, page::slice_rows},
19};
20use std::collections::HashMap;
21
22/// 稿件监控服务
23pub struct TaskTrackingService;
24
25impl TaskTrackingService {
26    /// 查询所有组员的任务计数(带缓存)。
27    ///
28    /// 优先返回缓存数据,缓存未命中时查库并写入缓存。
29    ///
30    /// # 返回值
31    ///
32    /// 返回所有组员的各岗位任务计数列表。
33    #[tracing::instrument(skip_all, level = "debug")]
34    pub async fn get_member_task_count_list(state: &AppState) -> ApiResult<Vec<MemberTaskCount>> {
35        if let Some(cached) = get_member_task_counts_cached(state).await {
36            return Ok(cached);
37        }
38        let repo = TaskTrackingRepository::new(state.db.clone());
39        let rows = repo.list_member_task_counts().await?;
40        let data = TaskTrackingRepository::to_member_task_counts(&rows);
41        set_member_task_counts_cached(state, data.clone()).await;
42        Ok(data)
43    }
44
45    /// 查询全部待做稿件(带缓存)。
46    ///
47    /// 返回按岗位分组的待处理话数,用于任务看板主视图。
48    ///
49    /// # 返回值
50    ///
51    /// 返回 [`TaskTrackingResponse`],包含 translator/proofreader/letterer/timer/reviewer/publish 六个岗位的任务列表。
52    #[tracing::instrument(skip_all, level = "debug")]
53    pub async fn get_all_task(state: &AppState) -> ApiResult<TaskTrackingResponse> {
54        if let Some(cached) = get_episode_tasks_cached(state).await {
55            return Ok(cached);
56        }
57        let repo = TaskTrackingRepository::new(state.db.clone());
58        let episodes = repo.list_unpublished_episodes().await?;
59        let data = TaskTrackingRepository::build_task_response(&episodes);
60        set_episode_tasks_cached(state, data.clone()).await;
61        Ok(data)
62    }
63
64    /// 待发布/待审稿漫画分页(对齐 Java TaskTrackingServiceImpl.getPendingMangaTasks)
65    #[tracing::instrument(skip_all, level = "debug")]
66    pub async fn get_pending_manga_tasks(
67        state: &AppState,
68        page: i32,
69        page_size: i32,
70        manga_tran_name: Option<String>,
71    ) -> ApiResult<PageBean<PendingMangaTask>> {
72        let repo = TaskTrackingRepository::new(state.db.clone());
73        let name_ref = manga_tran_name.as_deref();
74        let (total, episodes) = tokio::try_join!(
75            repo.count_pending_publish_episodes(name_ref),
76            repo.list_pending_publish_episodes(name_ref, page, page_size),
77        )?;
78
79        // #region agent log
80        agent_debug::log(
81            "A",
82            "task_tracking_service.rs:get_pending_manga_tasks",
83            "pending episodes fetched",
84            serde_json::json!({
85                "total": total,
86                "pageCount": episodes.len(),
87                "sampleMangaIds": episodes.iter().take(3).map(|e| e.manga_id).collect::<Vec<_>>()
88            }),
89        );
90        // #endregion
91
92        let manga_ids: Vec<i32> = episodes
93            .iter()
94            .map(|e| e.manga_id)
95            .collect::<std::collections::HashSet<_>>()
96            .into_iter()
97            .collect();
98
99        let (latest, next_publish) = repo.map_publish_episode_context(&manga_ids).await?;
100
101        let mut grouped: HashMap<i32, Vec<&PendingEpisodeRow>> = HashMap::new();
102        let mut manga_order: Vec<i32> = Vec::new();
103        for ep in &episodes {
104            if !grouped.contains_key(&ep.manga_id) {
105                manga_order.push(ep.manga_id);
106            }
107            grouped.entry(ep.manga_id).or_default().push(ep);
108        }
109
110        let mut tasks: Vec<PendingMangaTask> = Vec::new();
111        for manga_id in manga_order {
112            let Some(rows) = grouped.get(&manga_id) else {
113                continue;
114            };
115            let first = rows[0];
116            let mut pending_list: Vec<PendingEpisode> = rows
117                .iter()
118                .map(|row| {
119                    let status = if row.reviewer_update_time.is_some() {
120                        "publisher".to_string()
121                    } else {
122                        "reviewer".to_string()
123                    };
124                    let next = next_publish
125                        .get(&row.manga_id)
126                        .map(|id| *id == row.episode_id)
127                        .unwrap_or(false);
128                    PendingEpisode {
129                        mangaepisodetb: row_to_episode_tb(row),
130                        status,
131                        next_publish: next,
132                    }
133                })
134                .collect();
135            pending_list.sort_by(|a, b| {
136                a.mangaepisodetb
137                    .manga_episode
138                    .cmp(&b.mangaepisodetb.manga_episode)
139            });
140
141            tasks.push(PendingMangaTask {
142                mangatb: TaskTrackingRepository::row_mangatb(first),
143                newest_manga_episode: latest.get(&manga_id).cloned(),
144                pending_episode_list: pending_list,
145            });
146        }
147
148        tasks.sort_by(|a, b| {
149            a.mangatb
150                .manga_tran_name
151                .cmp(&b.mangatb.manga_tran_name)
152        });
153
154        // #region agent log
155        agent_debug::log(
156            "A",
157            "task_tracking_service.rs:get_pending_manga_tasks",
158            "pending manga tasks built",
159            serde_json::json!({
160                "taskCount": tasks.len(),
161                "firstTaskEpisodes": tasks.first().map(|t| t.pending_episode_list.len())
162            }),
163        );
164        // #endregion
165
166        Ok(slice_rows(tasks, total))
167    }
168}
169
170/// PendingEpisodeRow 转 API 话数对象
171fn row_to_episode_tb(row: &PendingEpisodeRow) -> MangaEpisodeTb {
172    MangaEpisodeTb {
173        id: row.episode_id,
174        manga_id: row.manga_id,
175        manga_episode: row.manga_episode.clone(),
176        manga_episode_name: row.manga_episode_name.clone(),
177        provider_id: row.provider_id,
178        translator_id: row.translator_id,
179        proofreader_id: row.proofreader_id,
180        letterer_id: row.letterer_id,
181        timer_id: row.timer_id,
182        reviewer_id: row.reviewer_id,
183        setup_time: row.setup_time,
184        update_time: row.update_time,
185        translator_file: row.translator_file.clone(),
186        proofreader_file: row.proofreader_file.clone(),
187        timer_file: row.timer_file.clone(),
188        publish_link: row.publish_link.clone(),
189        provider_file_oss_id: row.provider_file_oss_id,
190        translator_file_oss_id: row.translator_file_oss_id,
191        proofreader_file_oss_id: row.proofreader_file_oss_id,
192        letterer_file_oss_id: row.letterer_file_oss_id,
193        timer_file_oss_id: row.timer_file_oss_id,
194    }
195}