Skip to main content

tdm_server_rust/repository/
task_tracking_repo.rs

1//! 稿件监控数据访问层 (Task Tracking Repository)
2//!
3//! 封装任务看板的多表联查逻辑,聚合组员待做稿件数据。
4
5use crate::entity::{
6    episode::{EpisodeTask, MemberTaskCount, TaskTrackingResponse},
7    manga::Mangatb,
8};
9use chrono::{DateTime, Utc};
10use sqlx::{MySql, Pool, Row};
11use std::collections::HashMap;
12
13/// 待发布话数联表行
14#[derive(Debug, Clone)]
15pub struct PendingEpisodeRow {
16    /// 话数 ID
17    pub episode_id: i32,
18    /// 漫画 ID
19    pub manga_id: i32,
20    /// 话数标签
21    pub manga_episode: Option<String>,
22    /// 话数名
23    pub manga_episode_name: Option<String>,
24    /// 图源 ID
25    pub provider_id: Option<i32>,
26    /// 翻译 ID
27    pub translator_id: Option<i32>,
28    /// 校对 ID
29    pub proofreader_id: Option<i32>,
30    /// 嵌字 ID
31    pub letterer_id: Option<i32>,
32    /// 时轴 ID
33    pub timer_id: Option<i32>,
34    /// 审稿 ID
35    pub reviewer_id: Option<i32>,
36    /// 设立时间
37    pub setup_time: Option<DateTime<Utc>>,
38    /// 更新时间
39    pub update_time: Option<DateTime<Utc>>,
40    /// 翻译文件
41    pub translator_file: Option<String>,
42    /// 校对文件
43    pub proofreader_file: Option<String>,
44    /// 时轴文件
45    pub timer_file: Option<String>,
46    /// 发布链接
47    pub publish_link: Option<String>,
48    /// 图源 OSS ID
49    pub provider_file_oss_id: Option<i32>,
50    /// 翻译稿 OSS ID
51    pub translator_file_oss_id: Option<i32>,
52    /// 校对稿 OSS ID
53    pub proofreader_file_oss_id: Option<i32>,
54    /// 嵌字稿 OSS ID
55    pub letterer_file_oss_id: Option<i32>,
56    /// 时轴 OSS ID
57    pub timer_file_oss_id: Option<i32>,
58    /// 审稿交稿时间
59    pub reviewer_update_time: Option<DateTime<Utc>>,
60    /// 漫画译名
61    pub manga_tran_name: Option<String>,
62    /// 漫画原名
63    pub manga_ori_name: Option<String>,
64    /// 漫画分类
65    pub category: Option<i16>,
66    /// 漫画状态
67    pub manga_status: Option<i16>,
68    /// 封面
69    pub image: Option<String>,
70    /// 开坑时间
71    pub manga_setup_time: Option<DateTime<Utc>>,
72    /// 漫画更新时间
73    pub manga_update_time: Option<DateTime<Utc>>,
74    /// 原著链接
75    pub link: Option<String>,
76    /// 简介
77    pub introduction: Option<String>,
78}
79
80/// 未发布话数详情行(含流程时间)
81#[derive(Debug, Clone)]
82pub struct UnpublishedEpisodeRow {
83    /// 话数 ID
84    pub episode_id: i32,
85    /// 漫画 ID
86    pub manga_id: i32,
87    /// 话数标签
88    pub manga_episode: Option<String>,
89    /// 话数名
90    pub manga_episode_name: Option<String>,
91    /// 译名
92    pub manga_tran_name: Option<String>,
93    /// 漫画分类
94    pub category: Option<i16>,
95    /// 翻译者 ID
96    pub translator_id: Option<i32>,
97    /// 校对者 ID
98    pub proofreader_id: Option<i32>,
99    /// 嵌字者 ID
100    pub letterer_id: Option<i32>,
101    /// 时轴 ID
102    pub timer_id: Option<i32>,
103    /// 审稿者 ID
104    pub reviewer_id: Option<i32>,
105    /// 翻译接稿时间
106    pub translator_setup_time: Option<DateTime<Utc>>,
107    /// 翻译交稿时间
108    pub translator_update_time: Option<DateTime<Utc>>,
109    /// 校对接稿时间
110    pub proofreader_setup_time: Option<DateTime<Utc>>,
111    /// 校对交稿时间
112    pub proofreader_update_time: Option<DateTime<Utc>>,
113    /// 嵌字接稿时间
114    pub letterer_setup_time: Option<DateTime<Utc>>,
115    /// 嵌字交稿时间
116    pub letterer_update_time: Option<DateTime<Utc>>,
117    /// 时轴接稿时间
118    pub timer_setup_time: Option<DateTime<Utc>>,
119    /// 时轴交稿时间
120    pub timer_update_time: Option<DateTime<Utc>>,
121    /// 审稿接稿时间
122    pub reviewer_setup_time: Option<DateTime<Utc>>,
123    /// 审稿交稿时间
124    pub reviewer_update_time: Option<DateTime<Utc>>,
125    /// 话数更新时间
126    pub episode_update_time: Option<DateTime<Utc>>,
127}
128
129/// 组员任务计数行
130#[derive(Debug, Clone)]
131pub struct MemberTaskCountRow {
132    /// 组员 ID
133    pub user_id: i32,
134    /// 用户名
135    pub user_name: Option<String>,
136    /// 职阶
137    pub intern: Option<i32>,
138    /// 邮箱
139    pub email: Option<String>,
140    /// 最后交稿时间
141    pub last_update_time: Option<String>,
142    /// 翻译任务数
143    pub translator_count: i64,
144    /// 校对任务数
145    pub proofreader_count: i64,
146    /// 嵌字任务数
147    pub letterer_count: i64,
148    /// 时轴任务数
149    pub timer_count: i64,
150    /// 审稿任务数
151    pub reviewer_count: i64,
152    /// 常驻数
153    pub stationed_count: i64,
154}
155
156/// 稿件监控仓储
157pub struct TaskTrackingRepository {
158    /// 连接池
159    pool: Pool<MySql>,
160}
161
162impl TaskTrackingRepository {
163    /// 构造仓储
164    pub fn new(pool: Pool<MySql>) -> Self {
165        Self { pool }
166    }
167
168    /// 查询组员任务计数列表
169    #[tracing::instrument(skip_all, level = "debug")]
170    pub async fn list_member_task_counts(&self) -> crate::error::ApiResult<Vec<MemberTaskCountRow>> {
171        let rows = sqlx::query(
172            "SELECT member.Id AS userId, member.username AS userName, \
173             member.lastSubmitTime AS lastUpdateTime, member.intern AS intern, member.email AS email, \
174             COALESCE(sc.stationedCount, 0) AS stationedCount, \
175             COALESCE(tc.translatorCount, 0) AS translatorCount, \
176             COALESCE(pc.proofreaderCount, 0) AS proofreaderCount, \
177             COALESCE(lc.lettererCount, 0) AS lettererCount, \
178             COALESCE(tmc.timerCount, 0) AS timerCount, \
179             COALESCE(rc.reviewerCount, 0) AS reviewerCount \
180             FROM membertb member \
181             LEFT JOIN ( \
182               SELECT ms.memberId, COUNT(*) AS stationedCount \
183               FROM mangastationed ms WHERE ms.status = 1 GROUP BY ms.memberId \
184             ) sc ON sc.memberId = member.Id \
185             LEFT JOIN ( \
186               SELECT me.translatorId AS memberId, COUNT(*) AS translatorCount \
187               FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
188               WHERE med.translatorUpdateTime IS NULL AND me.publishLink IS NULL \
189               GROUP BY me.translatorId \
190             ) tc ON tc.memberId = member.Id \
191             LEFT JOIN ( \
192               SELECT me.proofreaderId AS memberId, COUNT(*) AS proofreaderCount \
193               FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
194               WHERE med.translatorUpdateTime IS NOT NULL AND med.proofreaderUpdateTime IS NULL \
195               AND me.publishLink IS NULL GROUP BY me.proofreaderId \
196             ) pc ON pc.memberId = member.Id \
197             LEFT JOIN ( \
198               SELECT me.lettererId AS memberId, COUNT(*) AS lettererCount \
199               FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
200               WHERE med.proofreaderUpdateTime IS NOT NULL AND med.lettererUpdateTime IS NULL \
201               AND me.publishLink IS NULL GROUP BY me.lettererId \
202             ) lc ON lc.memberId = member.Id \
203             LEFT JOIN ( \
204               SELECT me.timerId AS memberId, COUNT(*) AS timerCount \
205               FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
206               WHERE med.proofreaderUpdateTime IS NOT NULL AND med.timerUpdateTime IS NULL \
207               AND me.publishLink IS NULL GROUP BY me.timerId \
208             ) tmc ON tmc.memberId = member.Id \
209             LEFT JOIN ( \
210               SELECT me.reviewerId AS memberId, COUNT(*) AS reviewerCount \
211               FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
212               WHERE med.lettererUpdateTime IS NOT NULL AND med.reviewerUpdateTime IS NULL \
213               AND me.publishLink IS NULL GROUP BY me.reviewerId \
214             ) rc ON rc.memberId = member.Id \
215             WHERE (COALESCE(tc.translatorCount, 0) + COALESCE(lc.lettererCount, 0) \
216             + COALESCE(tmc.timerCount, 0) + COALESCE(rc.reviewerCount, 0)) > 0 \
217             ORDER BY member.Id DESC",
218        )
219        .fetch_all(&self.pool)
220        .await?;
221        Ok(rows.into_iter().map(|r| row_to_member_task_count(&r)).collect())
222    }
223
224    /// 转为 MemberTaskCount 列表
225    pub fn to_member_task_counts(rows: &[MemberTaskCountRow]) -> Vec<MemberTaskCount> {
226        rows.iter()
227            .map(|r| MemberTaskCount {
228                user_name: r.user_name.clone(),
229                user_id: Some(r.user_id.to_string()),
230                intern: r.intern,
231                email: r.email.clone(),
232                last_update_time: r.last_update_time.clone(),
233                stationed_count: r.stationed_count as i32,
234                translator_count: r.translator_count as i32,
235                proofreader_count: r.proofreader_count as i32,
236                letterer_count: r.letterer_count as i32,
237                timer_count: r.timer_count as i32,
238                reviewer_count: r.reviewer_count as i32,
239            })
240            .collect()
241    }
242
243    /// 查询全部未发布话数及详情
244    #[tracing::instrument(skip_all, level = "debug")]
245    pub async fn list_unpublished_episodes(&self) -> crate::error::ApiResult<Vec<UnpublishedEpisodeRow>> {
246        let rows = sqlx::query(
247            "SELECT me.Id AS episodeId, me.mangaId, me.mangaEpisode, me.mangaEpisodeName, me.translatorId, me.proofreaderId, \
248             me.lettererId, me.timerId, me.reviewerId, me.updateTime AS episodeUpdateTime, \
249             m.mangaTranName, m.category, \
250             med.translatorSetupTime, med.translatorUpdateTime, \
251             med.proofreaderSetupTime, med.proofreaderUpdateTime, \
252             med.lettererSetupTime, med.lettererUpdateTime, \
253             med.timerSetupTime, med.timerUpdateTime, \
254             med.reviewerSetupTime, med.reviewerUpdateTime \
255             FROM mangaepisodetb me \
256             LEFT JOIN mangaepisodedetail med ON med.episodeId = me.Id \
257             LEFT JOIN mangatb m ON m.Id = me.mangaId \
258             WHERE (me.publishLink IS NULL OR me.publishLink = '')",
259        )
260        .fetch_all(&self.pool)
261        .await?;
262        Ok(rows.into_iter().map(|r| row_to_unpublished(&r)).collect())
263    }
264
265    /// 按工作流规则构建 TaskTrackingResponse(对齐 Java buildWorkflowChain)
266    pub fn build_task_response(episodes: &[UnpublishedEpisodeRow]) -> TaskTrackingResponse {
267        let mut resp = TaskTrackingResponse::empty();
268        for ep in episodes {
269            if ep.reviewer_update_time.is_some() {
270                push_task(
271                    &mut resp,
272                    ep,
273                    "PUBLISHER",
274                    None,
275                    ep.reviewer_setup_time,
276                    ep.reviewer_update_time,
277                    |r, t| r.publish_task_list.push(t),
278                );
279                continue;
280            }
281            if ep.letterer_update_time.is_some() || ep.timer_update_time.is_some() {
282                let prev = ep
283                    .letterer_update_time
284                    .or(ep.timer_update_time);
285                push_task(
286                    &mut resp,
287                    ep,
288                    "REVIEWER",
289                    ep.reviewer_id,
290                    ep.reviewer_setup_time,
291                    prev,
292                    |r, t| r.reviewer_task_list.push(t),
293                );
294                continue;
295            }
296            if ep.proofreader_update_time.is_some() {
297                let is_animation = ep.category == Some(4);
298                if is_animation {
299                    push_task(
300                        &mut resp,
301                        ep,
302                        "TIMER",
303                        ep.timer_id,
304                        ep.timer_setup_time,
305                        ep.proofreader_update_time,
306                        |r, t| r.timer_task_list.push(t),
307                    );
308                } else {
309                    push_task(
310                        &mut resp,
311                        ep,
312                        "LETTERER",
313                        ep.letterer_id,
314                        ep.letterer_setup_time,
315                        ep.proofreader_update_time,
316                        |r, t| r.letterer_task_list.push(t),
317                    );
318                }
319                continue;
320            }
321            if ep.translator_update_time.is_some() {
322                push_task(
323                    &mut resp,
324                    ep,
325                    "PROOFREADER",
326                    ep.proofreader_id,
327                    ep.proofreader_setup_time,
328                    ep.translator_update_time,
329                    |r, t| r.proofreader_task_list.push(t),
330                );
331                continue;
332            }
333            push_task(
334                &mut resp,
335                ep,
336                "TRANSLATOR",
337                ep.translator_id,
338                ep.translator_setup_time,
339                ep.episode_update_time,
340                |r, t| r.translator_task_list.push(t),
341            );
342        }
343        resp
344    }
345
346    /// 统计嵌字完成且未发布的待处理话数
347    #[tracing::instrument(skip_all, level = "debug")]
348    pub async fn count_pending_publish_episodes(
349        &self,
350        manga_tran_name: Option<&str>,
351    ) -> crate::error::ApiResult<i64> {
352        let (from_where, _) = pending_publish_from_where(manga_tran_name.is_some());
353        let sql = format!("SELECT COUNT(*) AS cnt {from_where}");
354        let mut q = sqlx::query(&sql);
355        if let Some(name) = manga_tran_name {
356            q = q.bind(name);
357        }
358        Ok(q.fetch_one(&self.pool).await?.get("cnt"))
359    }
360
361    /// 查询嵌字完成且未发布的待处理话数(SQL 分页)
362    #[tracing::instrument(skip_all, level = "debug")]
363    pub async fn list_pending_publish_episodes(
364        &self,
365        manga_tran_name: Option<&str>,
366        page: i32,
367        page_size: i32,
368    ) -> crate::error::ApiResult<Vec<PendingEpisodeRow>> {
369        let (from_where, select_cols) = pending_publish_from_where(manga_tran_name.is_some());
370        let page = page.max(1);
371        let page_size = page_size.max(1);
372        let offset = (page - 1) * page_size;
373        let sql = format!(
374            "SELECT {select_cols} {from_where} \
375             ORDER BY m.mangaTranName ASC, me.mangaId ASC, \
376             CAST(SUBSTRING_INDEX(me.mangaEpisode, '+', 1) AS DECIMAL(10,2)) ASC \
377             LIMIT ? OFFSET ?"
378        );
379        let mut q = sqlx::query(&sql);
380        if let Some(name) = manga_tran_name {
381            q = q.bind(name);
382        }
383        q = q.bind(page_size).bind(offset);
384        let rows = q.fetch_all(&self.pool).await?;
385        Ok(rows.into_iter().map(|r| row_to_pending_episode(&r)).collect())
386    }
387
388    /// 单次扫描:各漫画最新已发布话数标签 + 下一个待发布话数 ID
389    #[tracing::instrument(skip_all, level = "debug")]
390    pub async fn map_publish_episode_context(
391        &self,
392        manga_ids: &[i32],
393    ) -> crate::error::ApiResult<(HashMap<i32, String>, HashMap<i32, i32>)> {
394        if manga_ids.is_empty() {
395            return Ok((HashMap::new(), HashMap::new()));
396        }
397        let placeholders = manga_ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
398        let sql = format!(
399            "SELECT me.mangaId, me.Id AS episodeId, me.mangaEpisode, me.publishLink \
400             FROM mangaepisodetb me \
401             WHERE me.mangaId IN ({placeholders})"
402        );
403        let mut q = sqlx::query(&sql);
404        for id in manga_ids {
405            q = q.bind(id);
406        }
407        let rows = q.fetch_all(&self.pool).await?;
408
409        let mut max_published_num: HashMap<i32, f64> = HashMap::new();
410        let mut latest_label: HashMap<i32, String> = HashMap::new();
411        let mut unpublished: Vec<(i32, i32, f64)> = Vec::new();
412
413        for r in &rows {
414            let manga_id: i32 = r.get("mangaId");
415            let episode_id: i32 = r.get("episodeId");
416            let episode_label: String = r.try_get("mangaEpisode").unwrap_or_default();
417            let publish_link: Option<String> = r.try_get("publishLink").ok();
418            let ep_num = episode_sort_key(&episode_label);
419            let published = publish_link.as_deref().is_some_and(|s| !s.is_empty());
420
421            if published {
422                let replace = max_published_num
423                    .get(&manga_id)
424                    .is_none_or(|&n| ep_num > n);
425                if replace {
426                    max_published_num.insert(manga_id, ep_num);
427                    latest_label.insert(manga_id, episode_label);
428                }
429            } else {
430                unpublished.push((manga_id, episode_id, ep_num));
431            }
432        }
433
434        let mut next_min: HashMap<i32, (f64, i32)> = HashMap::new();
435        for (manga_id, episode_id, ep_num) in unpublished {
436            let threshold = max_published_num.get(&manga_id).copied().unwrap_or(-1.0);
437            if ep_num <= threshold {
438                continue;
439            }
440            match next_min.get(&manga_id) {
441                None => {
442                    next_min.insert(manga_id, (ep_num, episode_id));
443                }
444                Some((min_num, _)) if ep_num < *min_num => {
445                    next_min.insert(manga_id, (ep_num, episode_id));
446                }
447                _ => {}
448            }
449        }
450        let next_publish = next_min
451            .into_iter()
452            .map(|(manga_id, (_, episode_id))| (manga_id, episode_id))
453            .collect();
454
455        Ok((latest_label, next_publish))
456    }
457
458    /// PendingEpisodeRow 转 Mangatb
459    pub fn row_mangatb(row: &PendingEpisodeRow) -> Mangatb {
460        Mangatb {
461            id: Some(row.manga_id),
462            manga_tran_name: row.manga_tran_name.clone(),
463            manga_ori_name: row.manga_ori_name.clone(),
464            image: row.image.clone(),
465            category: row.category.map(|v| v as i32),
466            manga_status: row.manga_status.map(|v| v as i32),
467            setup_time: row.manga_setup_time,
468            update_time: row.manga_update_time,
469            link: row.link.clone(),
470            introduction: row.introduction.clone(),
471        }
472    }
473}
474
475/// 待发布主查询 SELECT 列与 FROM/WHERE 片段
476fn pending_publish_from_where(name_filter: bool) -> (String, String) {
477    let select_cols = "me.Id AS episodeId, me.mangaId, me.mangaEpisode, me.mangaEpisodeName, \
478             me.providerId, me.translatorId, me.proofreaderId, me.lettererId, me.timerId, me.reviewerId, \
479             me.setupTime, me.updateTime, me.translatorFile, me.proofreaderFile, me.timerFile, me.publishLink, \
480             me.provider_file_oss_id AS providerFileOssId, me.translator_file_oss_id AS translatorFileOssId, \
481             me.proofreader_file_oss_id AS proofreaderFileOssId, me.letterer_file_oss_id AS lettererFileOssId, \
482             me.timer_file_oss_id AS timerFileOssId, \
483             med.reviewerUpdateTime, \
484             m.Id AS mangaDbId, m.mangaTranName, m.mangaOriName, m.category, m.mangaStatus, m.image, \
485             m.setupTime AS mangaSetupTime, m.updateTime AS mangaUpdateTime, m.link, m.introduction"
486        .to_string();
487    let mut from_where = String::from(
488        "FROM mangaepisodetb me \
489         LEFT JOIN mangaepisodedetail med ON med.episodeId = me.Id \
490         INNER JOIN mangatb m ON m.Id = me.mangaId \
491         WHERE me.letterer_file_oss_id IS NOT NULL \
492         AND (me.publishLink IS NULL OR me.publishLink = '')",
493    );
494    if name_filter {
495        from_where.push_str(" AND m.mangaTranName LIKE CONCAT('%', ?, '%')");
496    }
497    (from_where, select_cols)
498}
499
500/// 话数标签排序键(对齐 SQL SUBSTRING_INDEX + DECIMAL)
501fn episode_sort_key(episode: &str) -> f64 {
502    episode
503        .split('+')
504        .next()
505        .unwrap_or(episode)
506        .parse()
507        .unwrap_or(0.0)
508}
509
510/// 行映射为组员任务计数
511fn row_to_member_task_count(row: &sqlx::mysql::MySqlRow) -> MemberTaskCountRow {
512    MemberTaskCountRow {
513        user_id: row.get("userId"),
514        user_name: row.try_get("userName").ok(),
515        intern: row.try_get("intern").ok(),
516        email: row.try_get("email").ok(),
517        last_update_time: row
518            .try_get::<DateTime<Utc>, _>("lastUpdateTime")
519            .ok()
520            .map(|t| t.to_rfc3339()),
521        translator_count: row.try_get("translatorCount").unwrap_or(0),
522        proofreader_count: row.try_get("proofreaderCount").unwrap_or(0),
523        letterer_count: row.try_get("lettererCount").unwrap_or(0),
524        timer_count: row.try_get("timerCount").unwrap_or(0),
525        reviewer_count: row.try_get("reviewerCount").unwrap_or(0),
526        stationed_count: row.try_get("stationedCount").unwrap_or(0),
527    }
528}
529
530/// 行映射为未发布话数
531fn row_to_unpublished(row: &sqlx::mysql::MySqlRow) -> UnpublishedEpisodeRow {
532    UnpublishedEpisodeRow {
533        episode_id: row.get("episodeId"),
534        manga_id: row.get("mangaId"),
535        manga_episode: row.try_get("mangaEpisode").ok(),
536        manga_episode_name: row.try_get("mangaEpisodeName").ok(),
537        manga_tran_name: row.try_get("mangaTranName").ok(),
538        category: row.try_get("category").ok(),
539        translator_id: row.try_get("translatorId").ok(),
540        proofreader_id: row.try_get("proofreaderId").ok(),
541        letterer_id: row.try_get("lettererId").ok(),
542        timer_id: row.try_get("timerId").ok(),
543        reviewer_id: row.try_get("reviewerId").ok(),
544        translator_setup_time: row.try_get("translatorSetupTime").ok(),
545        translator_update_time: row.try_get("translatorUpdateTime").ok(),
546        proofreader_setup_time: row.try_get("proofreaderSetupTime").ok(),
547        proofreader_update_time: row.try_get("proofreaderUpdateTime").ok(),
548        letterer_setup_time: row.try_get("lettererSetupTime").ok(),
549        letterer_update_time: row.try_get("lettererUpdateTime").ok(),
550        timer_setup_time: row.try_get("timerSetupTime").ok(),
551        timer_update_time: row.try_get("timerUpdateTime").ok(),
552        reviewer_setup_time: row.try_get("reviewerSetupTime").ok(),
553        reviewer_update_time: row.try_get("reviewerUpdateTime").ok(),
554        episode_update_time: row.try_get("episodeUpdateTime").ok(),
555    }
556}
557
558/// 行映射为待发布话数
559fn row_to_pending_episode(row: &sqlx::mysql::MySqlRow) -> PendingEpisodeRow {
560    PendingEpisodeRow {
561        episode_id: row.get("episodeId"),
562        manga_id: row.get("mangaId"),
563        manga_episode: row.try_get("mangaEpisode").ok(),
564        manga_episode_name: row.try_get("mangaEpisodeName").ok(),
565        provider_id: row.try_get("providerId").ok(),
566        translator_id: row.try_get("translatorId").ok(),
567        proofreader_id: row.try_get("proofreaderId").ok(),
568        letterer_id: row.try_get("lettererId").ok(),
569        timer_id: row.try_get("timerId").ok(),
570        reviewer_id: row.try_get("reviewerId").ok(),
571        setup_time: row.try_get("setupTime").ok(),
572        update_time: row.try_get("updateTime").ok(),
573        translator_file: row.try_get("translatorFile").ok(),
574        proofreader_file: row.try_get("proofreaderFile").ok(),
575        timer_file: row.try_get("timerFile").ok(),
576        publish_link: row.try_get("publishLink").ok(),
577        provider_file_oss_id: row.try_get("providerFileOssId").ok(),
578        translator_file_oss_id: row.try_get("translatorFileOssId").ok(),
579        proofreader_file_oss_id: row.try_get("proofreaderFileOssId").ok(),
580        letterer_file_oss_id: row.try_get("lettererFileOssId").ok(),
581        timer_file_oss_id: row.try_get("timerFileOssId").ok(),
582        reviewer_update_time: row.try_get("reviewerUpdateTime").ok(),
583        manga_tran_name: row.try_get("mangaTranName").ok(),
584        manga_ori_name: row.try_get("mangaOriName").ok(),
585        category: row.try_get("category").ok(),
586        manga_status: row.try_get("mangaStatus").ok(),
587        image: row.try_get("image").ok(),
588        manga_setup_time: row.try_get("mangaSetupTime").ok(),
589        manga_update_time: row.try_get("mangaUpdateTime").ok(),
590        link: row.try_get("link").ok(),
591        introduction: row.try_get("introduction").ok(),
592    }
593}
594
595fn make_episode_task(
596    ep: &UnpublishedEpisodeRow,
597    post: &str,
598    member_id: Option<i32>,
599    setup_time: Option<DateTime<Utc>>,
600    previous_update_time: Option<DateTime<Utc>>,
601) -> EpisodeTask {
602    EpisodeTask {
603        manga_id: Some(ep.manga_id),
604        episode_id: Some(ep.episode_id),
605        manga_episode: ep.manga_episode.clone(),
606        episode_name: ep.manga_episode_name.clone(),
607        member_id,
608        setup_time,
609        previous_update_time,
610        post: Some(post.to_string()),
611    }
612}
613
614fn push_task<F>(
615    resp: &mut TaskTrackingResponse,
616    ep: &UnpublishedEpisodeRow,
617    post: &str,
618    member_id: Option<i32>,
619    setup_time: Option<DateTime<Utc>>,
620    previous_update_time: Option<DateTime<Utc>>,
621    push_list: F,
622) where
623    F: FnOnce(&mut TaskTrackingResponse, EpisodeTask),
624{
625    let task = make_episode_task(ep, post, member_id, setup_time, previous_update_time);
626    resp.all_task_list.push(task.clone());
627    push_list(resp, task);
628}