1use crate::{
7 entity::episode::{
8 EpisodeDetailVo, EpisodeEditDto, EpisodeListVo, EpisodeSimpleListVo, MemberStatistics,
9 NewestEpisodeVo, PublishLinkRequest, Statistics, UploadPageVo,
10 },
11 repository::episode_mapper::{
12 row_to_episode_detail, row_to_episode_list, row_to_episode_simple, row_to_newest,
13 row_to_upload_page, EPISODE_WORKFLOW_FROM, EPISODE_WORKFLOW_SELECT,
14 },
15};
16use chrono::{DateTime, NaiveDateTime, Utc};
17use sqlx::{MySql, Pool, Row};
18use tracing::Instrument;
19
20fn read_mysql_sum(row: &sqlx::mysql::MySqlRow, col: &str) -> i64 {
22 row.try_get::<i64, _>(col)
23 .or_else(|_| row.try_get::<i32, _>(col).map(i64::from))
24 .unwrap_or(0)
25}
26
27#[derive(Debug, Clone)]
29pub struct PostCompletionStats {
30 pub translator_count: i64,
32 pub proofreader_count: i64,
34 pub letterer_count: i64,
36 pub reviewer_count: i64,
38}
39
40pub struct EpisodeRepository {
42 pool: Pool<MySql>,
44}
45
46impl EpisodeRepository {
47 pub fn new(pool: Pool<MySql>) -> Self {
49 Self { pool }
50 }
51
52 #[tracing::instrument(skip_all, level = "debug")]
58 pub async fn list_by_manga_id(&self, manga_id: i32) -> crate::error::ApiResult<Vec<EpisodeSimpleListVo>> {
59 let sql = format!(
60 "SELECT {EPISODE_WORKFLOW_SELECT} {EPISODE_WORKFLOW_FROM} \
61 WHERE me.mangaId = ? ORDER BY CAST(me.mangaEpisode AS DECIMAL(10, 2)) DESC"
62 );
63 let rows = sqlx::query(&sql).bind(manga_id).fetch_all(&self.pool).await?;
64 Ok(rows.iter().map(row_to_episode_simple).collect())
65 }
66
67 #[tracing::instrument(skip_all, level = "debug")]
69 pub async fn list_full_by_manga_id(&self, manga_id: i32) -> crate::error::ApiResult<Vec<EpisodeListVo>> {
70 let sql = format!(
71 "SELECT {EPISODE_WORKFLOW_SELECT} {EPISODE_WORKFLOW_FROM} \
72 WHERE me.mangaId = ? ORDER BY CAST(me.mangaEpisode AS DECIMAL(10, 2)) DESC"
73 );
74 let rows = sqlx::query(&sql).bind(manga_id).fetch_all(&self.pool).await?;
75 Ok(rows.iter().map(row_to_episode_list).collect())
76 }
77
78 #[tracing::instrument(skip_all, level = "debug")]
85 pub async fn get_by_id(&self, id: i32) -> crate::error::ApiResult<Option<EpisodeDetailVo>> {
86 let sql = format!(
87 "SELECT {EPISODE_WORKFLOW_SELECT} {EPISODE_WORKFLOW_FROM} WHERE me.Id = ?"
88 );
89 let row = sqlx::query(&sql).bind(id).fetch_optional(&self.pool).await?;
90 Ok(row.as_ref().map(row_to_episode_detail))
91 }
92
93 #[tracing::instrument(skip_all, level = "debug")]
95 pub async fn get_legacy_file_path(
96 &self,
97 id: i32,
98 post_name: &str,
99 ) -> crate::error::ApiResult<Option<String>> {
100 let row = sqlx::query(
101 "SELECT translatorFile, proofreaderFile, timerFile FROM mangaepisodetb WHERE Id = ?",
102 )
103 .bind(id)
104 .fetch_optional(&self.pool)
105 .await?;
106 let Some(row) = row else {
107 return Ok(None);
108 };
109 let col = match post_name.to_lowercase().as_str() {
110 "translator" => "translatorFile",
111 "proofreader" => "proofreaderFile",
112 "timer" => "timerFile",
113 _ => {
114 return Err(crate::error::AppError::business(format!(
115 "当前岗位文件不存在喵:{post_name}"
116 )));
117 }
118 };
119 Ok(row.try_get(col).ok())
120 }
121
122 #[tracing::instrument(skip_all, level = "debug")]
124 pub async fn set_legacy_file_path(
125 &self,
126 id: i32,
127 post_name: &str,
128 path: &str,
129 ) -> crate::error::ApiResult<()> {
130 let sql = match post_name.to_lowercase().as_str() {
131 "translator" => "UPDATE mangaepisodetb SET translatorFile = ?, updateTime = NOW() WHERE Id = ?",
132 "proofreader" => {
133 "UPDATE mangaepisodetb SET proofreaderFile = ?, updateTime = NOW() WHERE Id = ?"
134 }
135 "timer" => "UPDATE mangaepisodetb SET timerFile = ?, updateTime = NOW() WHERE Id = ?",
136 _ => {
137 return Err(crate::error::AppError::business(format!(
138 "当前岗位文件不存在喵:{post_name}"
139 )));
140 }
141 };
142 sqlx::query(sql).bind(path).bind(id).execute(&self.pool).await?;
143 Ok(())
144 }
145
146 #[tracing::instrument(skip_all, level = "debug")]
148 pub async fn get_newest_by_manga_id(&self, manga_id: i32) -> crate::error::ApiResult<Option<NewestEpisodeVo>> {
149 let row = sqlx::query(
150 "SELECT me.Id FROM mangaepisodetb me WHERE me.mangaId = ? ORDER BY me.Id DESC LIMIT 1",
151 )
152 .bind(manga_id)
153 .fetch_optional(&self.pool)
154 .await?;
155 let Some(ep_row) = row else {
156 return Ok(None);
157 };
158 let ep_id: i32 = ep_row.get("Id");
159 let sql = format!(
160 "SELECT {EPISODE_WORKFLOW_SELECT} {EPISODE_WORKFLOW_FROM} WHERE me.Id = ?"
161 );
162 let row = sqlx::query(&sql).bind(ep_id).fetch_optional(&self.pool).await?;
163 Ok(row.as_ref().map(row_to_newest))
164 }
165
166 #[tracing::instrument(skip_all, level = "debug")]
168 pub async fn page_uploaded_submit(
169 &self,
170 page: i32,
171 page_size: i32,
172 manga_tran_name: Option<&str>,
173 username: Option<&str>,
174 ) -> crate::error::ApiResult<(i64, Vec<UploadPageVo>)> {
175 let from_sql = " FROM mangaepisodetb me \
176 LEFT JOIN membertb m ON m.Id = me.translatorId \
177 LEFT JOIN membertb m2 ON m2.Id = me.proofreaderId \
178 LEFT JOIN mangatb m3 ON m3.Id = me.mangaId \
179 WHERE (me.translatorFile IS NOT NULL OR me.proofreaderFile IS NOT NULL OR me.timerFile IS NOT NULL \
180 OR me.translator_file_oss_id IS NOT NULL OR me.proofreader_file_oss_id IS NOT NULL OR me.timer_file_oss_id IS NOT NULL)";
181 let mut where_extra = String::new();
182 if manga_tran_name.is_some() {
183 where_extra.push_str(" AND m3.mangaTranName = ?");
184 }
185 if username.is_some() {
186 where_extra.push_str(" AND (m2.username = ? OR m.username = ?)");
187 }
188
189 let count_sql = format!("SELECT COUNT(*) AS cnt{from_sql}{where_extra}");
190 let mut count_q = sqlx::query(&count_sql);
191 count_q = bind_uploaded_submit_filters(count_q, manga_tran_name, username);
192 let total: i64 = count_q.fetch_one(&self.pool).await?.get("cnt");
193
194 let page = page.max(1);
195 let page_size = page_size.max(1);
196 let offset = (page - 1) * page_size;
197 let data_sql = format!(
198 "SELECT me.*, m.username AS translatorName, m.email, m2.username AS proofreaderName, \
199 m2.email AS emailNow, letterer.username AS lettererName, timer.username AS timerName, \
200 reviewer.username AS reviewerName, m3.mangaTranName AS mangaName, \
201 providerFile.filename AS providerFilename, translatorFile.filename AS translatorFilename, \
202 proofreaderFile.filename AS proofreaderFilename, lettererFile.filename AS lettererFilename, \
203 timerFile.filename AS timerFilename \
204 FROM mangaepisodetb me \
205 LEFT JOIN membertb m ON m.Id = me.translatorId \
206 LEFT JOIN membertb m2 ON m2.Id = me.proofreaderId \
207 LEFT JOIN membertb letterer ON letterer.Id = me.lettererId \
208 LEFT JOIN membertb reviewer ON reviewer.Id = me.reviewerId \
209 LEFT JOIN membertb timer ON timer.Id = me.timerId \
210 LEFT JOIN mangatb m3 ON m3.Id = me.mangaId \
211 LEFT JOIN oss providerFile ON providerFile.id = me.provider_file_oss_id \
212 LEFT JOIN oss translatorFile ON translatorFile.id = me.translator_file_oss_id \
213 LEFT JOIN oss proofreaderFile ON proofreaderFile.id = me.proofreader_file_oss_id \
214 LEFT JOIN oss lettererFile ON lettererFile.id = me.letterer_file_oss_id \
215 LEFT JOIN oss timerFile ON timerFile.id = me.timer_file_oss_id \
216 WHERE (me.translatorFile IS NOT NULL OR me.proofreaderFile IS NOT NULL OR me.timerFile IS NOT NULL \
217 OR me.translator_file_oss_id IS NOT NULL OR me.proofreader_file_oss_id IS NOT NULL OR me.timer_file_oss_id IS NOT NULL){where_extra} \
218 ORDER BY me.updateTime DESC LIMIT ? OFFSET ?"
219 );
220 let mut data_q = sqlx::query(&data_sql);
221 data_q = bind_uploaded_submit_filters(data_q, manga_tran_name, username);
222 data_q = data_q.bind(page_size).bind(offset);
223 let rows = async {
224 data_q.fetch_all(&self.pool).await
225 }
226 .instrument(tracing::debug_span!("list_uploaded_submit_sql"))
227 .await?;
228 let result = {
229 let _span = tracing::debug_span!("list_uploaded_submit_map").entered();
230 rows.iter().map(row_to_upload_page).collect()
231 };
232 Ok((total, result))
233 }
234
235 #[tracing::instrument(skip_all, level = "debug")]
237 pub async fn count_episode_by_number(
238 &self,
239 manga_id: i32,
240 manga_episode: &str,
241 ) -> crate::error::ApiResult<i64> {
242 let row = sqlx::query(
243 "SELECT COUNT(*) AS cnt FROM mangaepisodetb WHERE mangaId = ? AND mangaEpisode = ?",
244 )
245 .bind(manga_id)
246 .bind(manga_episode)
247 .fetch_one(&self.pool)
248 .await?;
249 Ok(row.get("cnt"))
250 }
251
252 #[tracing::instrument(skip_all, level = "debug")]
254 pub async fn touch_manga_update_time(&self, manga_id: i32) -> crate::error::ApiResult<()> {
255 sqlx::query("UPDATE mangatb SET updateTime = NOW() WHERE Id = ?")
256 .bind(manga_id)
257 .execute(&self.pool)
258 .await?;
259 Ok(())
260 }
261
262 #[tracing::instrument(skip_all, level = "debug")]
264 pub async fn insert(&self, dto: &EpisodeEditDto) -> crate::error::ApiResult<i32> {
265 let manga_id = dto.manga_id.ok_or_else(|| {
266 crate::error::AppError::business("缺少漫画 ID")
267 })?;
268 let r = sqlx::query(
269 "INSERT INTO mangaepisodetb(mangaId, mangaEpisode, mangaEpisodeName, \
270 providerId, translatorId, proofreaderId, lettererId, timerId, reviewerId, \
271 setupTime, updateTime, publishLink) \
272 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), ?)",
273 )
274 .bind(manga_id)
275 .bind(&dto.manga_episode)
276 .bind(&dto.manga_episode_name)
277 .bind(dto.provider_id)
278 .bind(dto.translator_id)
279 .bind(dto.proofreader_id)
280 .bind(dto.letterer_id)
281 .bind(dto.timer_id)
282 .bind(dto.reviewer_id)
283 .bind(&dto.publish_link)
284 .execute(&self.pool)
285 .await?;
286 let id = r.last_insert_id() as i32;
287 crate::utils::agent_debug::log(
289 "E1",
290 "episode_repo.rs:insert",
291 "episode inserted",
292 serde_json::json!({
293 "episodeId": id,
294 "mangaId": manga_id,
295 "providerId": dto.provider_id,
296 "translatorId": dto.translator_id
297 }),
298 );
299 Ok(id)
301 }
302
303 #[tracing::instrument(skip_all, level = "debug")]
305 pub async fn insert_detail(&self, episode_id: i32, dto: &EpisodeEditDto) -> crate::error::ApiResult<()> {
306 sqlx::query(
307 "INSERT INTO mangaepisodedetail(episodeId, translatorSetupTime, proofreaderSetupTime, \
308 lettererSetupTime, timerSetupTime, reviewerSetupTime) \
309 VALUES (?, \
310 IF(? IS NULL, NULL, NOW()), \
311 IF(? IS NULL, NULL, NOW()), \
312 IF(? IS NULL, NULL, NOW()), \
313 IF(? IS NULL, NULL, NOW()), \
314 IF(? IS NULL, NULL, NOW()))",
315 )
316 .bind(episode_id)
317 .bind(dto.translator_id)
318 .bind(dto.proofreader_id)
319 .bind(dto.letterer_id)
320 .bind(dto.timer_id)
321 .bind(dto.reviewer_id)
322 .execute(&self.pool)
323 .await?;
324 Ok(())
325 }
326
327 #[tracing::instrument(skip_all, level = "debug")]
329 pub async fn update_publish_link_only(
330 &self,
331 id: i32,
332 publish_link: Option<String>,
333 ) -> crate::error::ApiResult<()> {
334 sqlx::query(
335 "UPDATE mangaepisodetb SET publishLink = ?, updateTime = NOW() WHERE Id = ?",
336 )
337 .bind(publish_link)
338 .bind(id)
339 .execute(&self.pool)
340 .await?;
341 Ok(())
342 }
343
344 #[tracing::instrument(skip_all, level = "debug")]
346 pub async fn update(&self, dto: &EpisodeEditDto) -> crate::error::ApiResult<()> {
347 let id = dto.id.ok_or_else(|| crate::error::AppError::business("缺少话数 ID"))?;
348 let manga_episode_name = dto.manga_episode_name.as_deref().map(|s| {
349 if s.is_empty() || s == "null" {
350 None
351 } else {
352 Some(s)
353 }
354 }).flatten();
355 sqlx::query(
356 "UPDATE mangaepisodetb SET \
357 mangaEpisode = COALESCE(?, mangaEpisode), \
358 mangaEpisodeName = ?, \
359 providerId = ?, translatorId = ?, proofreaderId = ?, \
360 lettererId = ?, timerId = ?, reviewerId = ?, \
361 publishLink = ?, updateTime = NOW() \
362 WHERE Id = ?",
363 )
364 .bind(&dto.manga_episode)
365 .bind(manga_episode_name)
366 .bind(dto.provider_id)
367 .bind(dto.translator_id)
368 .bind(dto.proofreader_id)
369 .bind(dto.letterer_id)
370 .bind(dto.timer_id)
371 .bind(dto.reviewer_id)
372 .bind(&dto.publish_link)
373 .bind(id)
374 .execute(&self.pool)
375 .await?;
376 self.sync_assignment_setup_times(id, dto).await?;
377 if let Some(manga_id) = dto.manga_id {
378 self.touch_manga_update_time(manga_id).await?;
379 }
380 Ok(())
381 }
382
383 async fn sync_assignment_setup_times(
385 &self,
386 episode_id: i32,
387 dto: &EpisodeEditDto,
388 ) -> crate::error::ApiResult<()> {
389 for (setup_col, member_id) in assignment_setup_columns(dto) {
390 let sql = if member_id.is_some() {
391 format!(
392 "UPDATE mangaepisodedetail SET {setup_col} = COALESCE({setup_col}, NOW()) WHERE episodeId = ?"
393 )
394 } else {
395 format!("UPDATE mangaepisodedetail SET {setup_col} = NULL WHERE episodeId = ?")
396 };
397 sqlx::query(&sql)
398 .bind(episode_id)
399 .execute(&self.pool)
400 .await?;
401 }
402 Ok(())
403 }
404
405 #[tracing::instrument(skip_all, level = "debug")]
407 pub async fn delete_by_id(&self, id: i32) -> crate::error::ApiResult<()> {
408 sqlx::query("DELETE FROM mangaepisodedetail WHERE episodeId = ?")
409 .bind(id)
410 .execute(&self.pool)
411 .await?;
412 sqlx::query("DELETE FROM mangaepisodetb WHERE Id = ?")
413 .bind(id)
414 .execute(&self.pool)
415 .await?;
416 Ok(())
417 }
418
419 #[tracing::instrument(skip_all, level = "debug")]
421 pub async fn update_publish_links_batch(
422 &self,
423 requests: &[PublishLinkRequest],
424 ) -> crate::error::ApiResult<()> {
425 if requests.is_empty() {
426 return Ok(());
427 }
428 let mut case_sql = String::from("publishLink = CASE ");
429 let mut ids = Vec::new();
430 for req in requests {
431 case_sql.push_str("WHEN Id = ? THEN ? ");
432 ids.push(req.id);
433 }
434 case_sql.push_str("ELSE publishLink END, updateTime = NOW()");
435 let placeholders = ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
436 let sql = format!(
437 "UPDATE mangaepisodetb SET {case_sql} WHERE Id IN ({placeholders})"
438 );
439 let mut q = sqlx::query(&sql);
440 for req in requests {
441 q = q.bind(req.id).bind(&req.publish_link);
442 }
443 for id in &ids {
444 q = q.bind(id);
445 }
446 q.execute(&self.pool).await?;
447 Ok(())
448 }
449
450 #[tracing::instrument(skip_all, level = "debug")]
452 pub async fn count_publish_link(&self, publish_link: &str, exclude_id: Option<i32>) -> crate::error::ApiResult<i64> {
453 let row = if let Some(id) = exclude_id {
454 sqlx::query("SELECT COUNT(*) AS cnt FROM mangaepisodetb WHERE publishLink = ? AND Id != ?")
455 .bind(publish_link)
456 .bind(id)
457 .fetch_one(&self.pool)
458 .await?
459 } else {
460 sqlx::query("SELECT COUNT(*) AS cnt FROM mangaepisodetb WHERE publishLink = ?")
461 .bind(publish_link)
462 .fetch_one(&self.pool)
463 .await?
464 };
465 Ok(row.get::<i64, _>("cnt"))
466 }
467
468 #[tracing::instrument(skip_all, level = "debug")]
470 pub async fn get_statistic_count(
471 &self,
472 start: NaiveDateTime,
473 end: NaiveDateTime,
474 ) -> crate::error::ApiResult<PostCompletionStats> {
475 let row = sqlx::query(
476 "SELECT \
477 CAST(COALESCE(SUM(IF(med.translatorUpdateTime >= ? AND med.translatorUpdateTime <= ?, 1, 0)), 0) AS SIGNED) AS translatorCount, \
478 CAST(COALESCE(SUM(IF(med.proofreaderUpdateTime >= ? AND med.proofreaderUpdateTime <= ?, 1, 0)), 0) AS SIGNED) AS proofreaderCount, \
479 CAST(COALESCE(SUM(IF(med.lettererUpdateTime >= ? AND med.lettererUpdateTime <= ?, 1, 0)), 0) AS SIGNED) AS lettererCount, \
480 CAST(COALESCE(SUM(IF(med.reviewerUpdateTime >= ? AND med.reviewerUpdateTime <= ?, 1, 0)), 0) AS SIGNED) AS reviewerCount \
481 FROM mangaepisodedetail med",
482 )
483 .bind(start)
484 .bind(end)
485 .bind(start)
486 .bind(end)
487 .bind(start)
488 .bind(end)
489 .bind(start)
490 .bind(end)
491 .fetch_one(&self.pool)
492 .await?;
493 Ok(PostCompletionStats {
494 translator_count: read_mysql_sum(&row, "translatorCount"),
495 proofreader_count: read_mysql_sum(&row, "proofreaderCount"),
496 letterer_count: read_mysql_sum(&row, "lettererCount"),
497 reviewer_count: read_mysql_sum(&row, "reviewerCount"),
498 })
499 }
500
501 pub fn stats_from_post(stats: &PostCompletionStats) -> Statistics {
503 Statistics {
504 translator_count: stats.translator_count as i32,
505 proofreader_count: stats.proofreader_count as i32,
506 letterer_count: stats.letterer_count as i32,
507 reviewer_count: stats.reviewer_count as i32,
508 }
509 }
510
511 #[tracing::instrument(skip_all, level = "debug")]
513 pub async fn get_member_statistics(
514 &self,
515 start: DateTime<Utc>,
516 end: DateTime<Utc>,
517 member_id: Option<i32>,
518 ) -> crate::error::ApiResult<Vec<MemberStatistics>> {
519 let mut sql = String::from(
520 "SELECT m.Id AS userId, m.username AS userName, \
521 COALESCE(t.translatorCount, 0) AS translatorCount, \
522 COALESCE(p.proofreaderCount, 0) AS proofreaderCount, \
523 COALESCE(l.lettererCount, 0) AS lettererCount, \
524 COALESCE(ti.timerCount, 0) AS timerCount, \
525 COALESCE(r.reviewerCount, 0) AS reviewerCount \
526 FROM membertb m \
527 LEFT JOIN ( \
528 SELECT me.translatorId AS memberId, COUNT(DISTINCT med.Id) AS translatorCount \
529 FROM mangaepisodetb me JOIN mangaepisodedetail med ON med.episodeId = me.Id \
530 WHERE med.translatorUpdateTime >= ? AND med.translatorUpdateTime < ? \
531 GROUP BY me.translatorId \
532 ) t ON t.memberId = m.Id \
533 LEFT JOIN ( \
534 SELECT me.proofreaderId AS memberId, COUNT(DISTINCT med.Id) AS proofreaderCount \
535 FROM mangaepisodetb me JOIN mangaepisodedetail med ON med.episodeId = me.Id \
536 WHERE med.proofreaderUpdateTime >= ? AND med.proofreaderUpdateTime < ? \
537 GROUP BY me.proofreaderId \
538 ) p ON p.memberId = m.Id \
539 LEFT JOIN ( \
540 SELECT me.lettererId AS memberId, COUNT(DISTINCT med.Id) AS lettererCount \
541 FROM mangaepisodetb me JOIN mangaepisodedetail med ON med.episodeId = me.Id \
542 WHERE med.lettererUpdateTime >= ? AND med.lettererUpdateTime < ? \
543 GROUP BY me.lettererId \
544 ) l ON l.memberId = m.Id \
545 LEFT JOIN ( \
546 SELECT me.timerId AS memberId, COUNT(DISTINCT med.Id) AS timerCount \
547 FROM mangaepisodetb me JOIN mangaepisodedetail med ON med.episodeId = me.Id \
548 WHERE med.timerUpdateTime >= ? AND med.timerUpdateTime < ? \
549 GROUP BY me.timerId \
550 ) ti ON ti.memberId = m.Id \
551 LEFT JOIN ( \
552 SELECT me.reviewerId AS memberId, COUNT(DISTINCT med.Id) AS reviewerCount \
553 FROM mangaepisodetb me JOIN mangaepisodedetail med ON med.episodeId = me.Id \
554 WHERE med.reviewerUpdateTime >= ? AND med.reviewerUpdateTime < ? \
555 GROUP BY me.reviewerId \
556 ) r ON r.memberId = m.Id \
557 WHERE (COALESCE(t.translatorCount,0)+COALESCE(p.proofreaderCount,0)+COALESCE(l.lettererCount,0)+COALESCE(ti.timerCount,0)+COALESCE(r.reviewerCount,0)) > 0",
558 );
559 if member_id.is_some() {
560 sql.push_str(" AND m.Id = ?");
561 }
562 let mut q = sqlx::query(&sql);
563 for _ in 0..5 {
564 q = q.bind(start).bind(end);
565 }
566 if let Some(id) = member_id {
567 q = q.bind(id);
568 }
569 let rows = q.fetch_all(&self.pool).await?;
570 Ok(rows
571 .into_iter()
572 .map(|r| MemberStatistics {
573 user_name: r.try_get("userName").ok(),
574 user_id: r.try_get::<i32, _>("userId").ok().map(|v| v.to_string()),
575 translator_count: r.try_get("translatorCount").unwrap_or(0),
576 proofreader_count: r.try_get("proofreaderCount").unwrap_or(0),
577 letterer_count: r.try_get("lettererCount").unwrap_or(0),
578 timer_count: r.try_get("timerCount").unwrap_or(0),
579 reviewer_count: r.try_get("reviewerCount").unwrap_or(0),
580 })
581 .collect())
582 }
583
584 #[tracing::instrument(skip_all, level = "debug")]
586 pub async fn rollback_episode(&self, episode_id: i32, workflow_type: &str) -> crate::error::ApiResult<Option<i32>> {
587 let post = workflow_type.to_lowercase();
588 let (episode_sql, detail_sql, oss_col) = match post.as_str() {
589 "provider" => (
590 "UPDATE mangaepisodetb SET provider_file_oss_id = NULL, updateTime = NOW() WHERE Id = ?",
591 None,
592 Some("provider_file_oss_id"),
593 ),
594 "translator" => (
595 "UPDATE mangaepisodetb SET translatorFile = NULL, translator_file_oss_id = NULL, updateTime = NOW() WHERE Id = ?",
596 Some("UPDATE mangaepisodedetail SET translatorUpdateTime = NULL WHERE episodeId = ?"),
597 Some("translator_file_oss_id"),
598 ),
599 "proofreader" => (
600 "UPDATE mangaepisodetb SET proofreaderFile = NULL, proofreader_file_oss_id = NULL, updateTime = NOW() WHERE Id = ?",
601 Some("UPDATE mangaepisodedetail SET proofreaderUpdateTime = NULL WHERE episodeId = ?"),
602 Some("proofreader_file_oss_id"),
603 ),
604 "letterer" => (
605 "UPDATE mangaepisodetb SET letterer_file_oss_id = NULL, updateTime = NOW() WHERE Id = ?",
606 Some("UPDATE mangaepisodedetail SET lettererUpdateTime = NULL WHERE episodeId = ?"),
607 Some("letterer_file_oss_id"),
608 ),
609 "timer" => (
610 "UPDATE mangaepisodetb SET timerFile = NULL, timer_file_oss_id = NULL, updateTime = NOW() WHERE Id = ?",
611 Some("UPDATE mangaepisodedetail SET timerUpdateTime = NULL WHERE episodeId = ?"),
612 Some("timer_file_oss_id"),
613 ),
614 "reviewer" => (
615 "",
616 Some("UPDATE mangaepisodedetail SET reviewerUpdateTime = NULL WHERE episodeId = ?"),
617 None,
618 ),
619 _ => {
620 return Err(crate::error::AppError::business("不支持的回退流程类型喵!"));
621 }
622 };
623
624 let oss_id = if let Some(col) = oss_col {
625 let row = sqlx::query(&format!("SELECT {col} AS ossId FROM mangaepisodetb WHERE Id = ?"))
626 .bind(episode_id)
627 .fetch_optional(&self.pool)
628 .await?;
629 row.and_then(|r| r.try_get("ossId").ok())
630 } else {
631 None
632 };
633
634 if !episode_sql.is_empty() {
635 sqlx::query(episode_sql)
636 .bind(episode_id)
637 .execute(&self.pool)
638 .await?;
639 }
640 if let Some(sql) = detail_sql {
641 sqlx::query(sql)
642 .bind(episode_id)
643 .execute(&self.pool)
644 .await?;
645 }
646 Ok(oss_id)
647 }
648
649 #[tracing::instrument(skip_all, level = "debug")]
651 pub async fn list_unpublished_with_detail(&self) -> crate::error::ApiResult<Vec<EpisodeListVo>> {
652 let rows = sqlx::query(
653 "SELECT me.Id, me.mangaId, me.mangaEpisode \
654 FROM mangaepisodetb me \
655 WHERE me.publishLink IS NULL OR me.publishLink = '' \
656 ORDER BY me.Id DESC",
657 )
658 .fetch_all(&self.pool)
659 .await?;
660 Ok(rows
661 .into_iter()
662 .map(|r| EpisodeListVo {
663 id: r.get("Id"),
664 manga_id: r.get("mangaId"),
665 manga_episode: r.try_get("mangaEpisode").ok(),
666 ..Default::default()
667 })
668 .collect())
669 }
670}
671
672fn bind_uploaded_submit_filters<'q>(
674 mut q: sqlx::query::Query<'q, sqlx::MySql, sqlx::mysql::MySqlArguments>,
675 manga_tran_name: Option<&'q str>,
676 username: Option<&'q str>,
677) -> sqlx::query::Query<'q, sqlx::MySql, sqlx::mysql::MySqlArguments> {
678 if let Some(v) = manga_tran_name {
679 q = q.bind(v);
680 }
681 if let Some(v) = username {
682 q = q.bind(v).bind(v);
683 }
684 q
685}
686
687fn assignment_setup_columns(dto: &EpisodeEditDto) -> [(&'static str, Option<i32>); 5] {
689 [
690 ("translatorSetupTime", dto.translator_id),
691 ("proofreaderSetupTime", dto.proofreader_id),
692 ("lettererSetupTime", dto.letterer_id),
693 ("timerSetupTime", dto.timer_id),
694 ("reviewerSetupTime", dto.reviewer_id),
695 ]
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 fn dto_with_assignments() -> EpisodeEditDto {
704 EpisodeEditDto {
705 id: Some(1),
706 manga_id: Some(2),
707 manga_episode: Some("11.1".to_string()),
708 manga_episode_end: None,
709 manga_episode_name: Some("测试话".to_string()),
710 provider_id: Some(97),
711 translator_id: Some(10),
712 proofreader_id: Some(20),
713 letterer_id: Some(30),
714 timer_id: None,
715 reviewer_id: Some(40),
716 publish_link: None,
717 }
718 }
719
720 #[test]
721 fn assignment_setup_columns_maps_member_ids_to_setup_columns() {
722 let columns = assignment_setup_columns(&dto_with_assignments());
723
724 assert_eq!(
725 columns,
726 [
727 ("translatorSetupTime", Some(10)),
728 ("proofreaderSetupTime", Some(20)),
729 ("lettererSetupTime", Some(30)),
730 ("timerSetupTime", None),
731 ("reviewerSetupTime", Some(40)),
732 ]
733 );
734 }
735
736 #[test]
737 fn assignment_setup_columns_keeps_empty_assignment_as_none() {
738 let mut dto = dto_with_assignments();
739 dto.proofreader_id = None;
740 dto.letterer_id = None;
741
742 let columns = assignment_setup_columns(&dto);
743
744 assert_eq!(columns[1], ("proofreaderSetupTime", None));
745 assert_eq!(columns[2], ("lettererSetupTime", None));
746 }
747}