1use crate::db::DbConn;
6use crate::entity::manga::{
7 AddStationRequest, GlossaryRequest, GlossaryVo, Manga, MangaCardVo, MangaCollect,
8 MangaDetailVo, MangaSimpleVo,
9};
10use crate::entity::member::Member;
11use crate::error::AppError;
12use crate::repository::manga_list_assemble::{self, MangaListSort};
13use crate::sea_entity::{
14 glossary, mangaauthor, mangacollect, mangaepisodedetail, mangaepisodetb, mangamagazine,
15 mangastationed, mangatb, membertb,
16};
17use chrono::{DateTime, Utc};
18use sea_orm::{
19 ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait,
20 PaginatorTrait, QueryFilter, QueryOrder, Set,
21};
22use sqlx::postgres::PgPool;
23
24pub struct MangaRepository {
26 db: DbConn,
28}
29
30impl MangaRepository {
31 pub fn new(pool: PgPool) -> Self {
33 Self {
34 db: crate::db::from_sqlx_pool(pool),
35 }
36 }
37
38 pub fn from_db(db: DatabaseConnection) -> Self {
40 Self { db }
41 }
42
43 #[tracing::instrument(skip_all, level = "debug")]
45 pub async fn list_ori_names(&self) -> crate::error::ApiResult<Vec<MangaSimpleVo>> {
46 let rows = mangatb::Entity::find()
47 .order_by_asc(mangatb::Column::Id)
48 .all(&self.db)
49 .await?;
50 Ok(rows
51 .into_iter()
52 .map(|m| MangaSimpleVo {
53 id: m.id,
54 manga_ori_name: Some(m.manga_ori_name),
55 ..Default::default()
56 })
57 .collect())
58 }
59
60 #[tracing::instrument(skip_all, level = "debug")]
62 pub async fn list(
63 &self,
64 manga_tran_name: Option<&str>,
65 manga_ori_name: Option<&str>,
66 category: Option<i16>,
67 manga_status: Option<i16>,
68 author_name: Option<&str>,
69 magazine_name: Option<&str>,
70 ) -> crate::error::ApiResult<Vec<MangaCardVo>> {
71 self.list_page(
72 manga_tran_name,
73 manga_ori_name,
74 category,
75 manga_status,
76 author_name,
77 magazine_name,
78 i32::MAX,
79 0,
80 )
81 .await
82 }
83
84 #[tracing::instrument(skip_all, level = "debug")]
86 pub async fn count_list(
87 &self,
88 manga_tran_name: Option<&str>,
89 manga_ori_name: Option<&str>,
90 category: Option<i16>,
91 manga_status: Option<i16>,
92 author_name: Option<&str>,
93 magazine_name: Option<&str>,
94 ) -> crate::error::ApiResult<i64> {
95 manga_list_assemble::count_list(
96 &self.db,
97 manga_tran_name,
98 manga_ori_name,
99 category,
100 manga_status,
101 author_name,
102 magazine_name,
103 )
104 .await
105 }
106
107 #[tracing::instrument(skip_all, level = "debug")]
109 pub async fn list_page(
110 &self,
111 manga_tran_name: Option<&str>,
112 manga_ori_name: Option<&str>,
113 category: Option<i16>,
114 manga_status: Option<i16>,
115 author_name: Option<&str>,
116 magazine_name: Option<&str>,
117 limit: i32,
118 offset: i32,
119 ) -> crate::error::ApiResult<Vec<MangaCardVo>> {
120 manga_list_assemble::list_page(
121 &self.db,
122 manga_tran_name,
123 manga_ori_name,
124 category,
125 manga_status,
126 author_name,
127 magazine_name,
128 limit,
129 offset,
130 )
131 .await
132 }
133
134 #[tracing::instrument(skip_all, level = "debug")]
136 pub async fn list_page_sorted(
137 &self,
138 manga_tran_name: Option<&str>,
139 manga_ori_name: Option<&str>,
140 category: Option<i16>,
141 manga_status: Option<i16>,
142 author_name: Option<&str>,
143 magazine_name: Option<&str>,
144 sort: MangaListSort,
145 limit: i32,
146 offset: i32,
147 ) -> crate::error::ApiResult<Vec<MangaCardVo>> {
148 manga_list_assemble::list_page_sorted(
149 &self.db,
150 manga_tran_name,
151 manga_ori_name,
152 category,
153 manga_status,
154 author_name,
155 magazine_name,
156 sort,
157 limit,
158 offset,
159 )
160 .await
161 }
162
163 #[tracing::instrument(skip_all, level = "debug")]
165 pub async fn list_by_author_page(
166 &self,
167 author_id: i32,
168 page: i32,
169 page_size: i32,
170 ) -> crate::error::ApiResult<(i64, Vec<MangaCardVo>)> {
171 manga_list_assemble::list_by_author_page(&self.db, author_id, page, page_size).await
172 }
173
174 #[tracing::instrument(skip_all, level = "debug")]
176 pub async fn list_by_author(
177 &self,
178 author_id: i32,
179 ) -> crate::error::ApiResult<Vec<MangaCardVo>> {
180 manga_list_assemble::list_by_author(&self.db, author_id).await
181 }
182
183 #[tracing::instrument(skip_all, level = "debug")]
185 pub async fn list_by_magazine_page(
186 &self,
187 magazine_id: i32,
188 page: i32,
189 page_size: i32,
190 ) -> crate::error::ApiResult<(i64, Vec<MangaCardVo>)> {
191 manga_list_assemble::list_by_magazine_page(&self.db, magazine_id, page, page_size).await
192 }
193
194 #[tracing::instrument(skip_all, level = "debug")]
196 pub async fn list_by_magazine(
197 &self,
198 magazine_id: i32,
199 ) -> crate::error::ApiResult<Vec<MangaCardVo>> {
200 manga_list_assemble::list_by_magazine(&self.db, magazine_id).await
201 }
202
203 #[tracing::instrument(skip_all, level = "debug")]
205 pub async fn delete_by_id(&self, id: i32) -> crate::error::ApiResult<()> {
206 mangatb::Entity::delete_by_id(id).exec(&self.db).await?;
207 Ok(())
208 }
209
210 #[tracing::instrument(skip_all, level = "debug")]
212 pub async fn delete_manga_author(&self, id: i32) -> crate::error::ApiResult<()> {
213 mangaauthor::Entity::delete_many()
214 .filter(mangaauthor::Column::MangaId.eq(id))
215 .exec(&self.db)
216 .await?;
217 Ok(())
218 }
219
220 #[tracing::instrument(skip_all, level = "debug")]
222 pub async fn delete_manga_author2(&self, id: i32) -> crate::error::ApiResult<()> {
223 mangaauthor::Entity::delete_many()
224 .filter(mangaauthor::Column::MangaId.eq(id))
225 .filter(mangaauthor::Column::Type.eq(2))
226 .exec(&self.db)
227 .await?;
228 Ok(())
229 }
230
231 #[tracing::instrument(skip_all, level = "debug")]
233 pub async fn delete_manga_episode(&self, id: i32) -> crate::error::ApiResult<()> {
234 mangaepisodetb::Entity::delete_many()
235 .filter(mangaepisodetb::Column::MangaId.eq(id))
236 .exec(&self.db)
237 .await?;
238 Ok(())
239 }
240
241 #[tracing::instrument(skip_all, level = "debug")]
243 pub async fn delete_manga_magazine(&self, id: i32) -> crate::error::ApiResult<()> {
244 mangamagazine::Entity::delete_many()
245 .filter(mangamagazine::Column::MangaId.eq(id))
246 .exec(&self.db)
247 .await?;
248 Ok(())
249 }
250
251 #[tracing::instrument(skip_all, level = "debug")]
253 pub async fn insert(&self, manga: &Manga) -> crate::error::ApiResult<i32> {
254 let inserted = mangatb::ActiveModel {
255 manga_tran_name: Set(manga.manga_tran_name.clone().unwrap_or_default()),
256 manga_ori_name: Set(manga.manga_ori_name.clone().unwrap_or_default()),
257 category: Set(manga.category.unwrap_or(0) as i32),
258 manga_status: Set(manga.manga_status.unwrap_or(0) as i32),
259 image: Set(manga.img_url.clone().unwrap_or_default()),
260 link: Set(manga.link.clone()),
261 introduction: Set(manga.introduction.clone()),
262 setup_time: Set(chrono::Utc::now().naive_utc()),
263 update_time: Set(chrono::Utc::now().naive_utc()),
264 ..Default::default()
265 }
266 .insert(&self.db)
267 .await?;
268 Ok(inserted.id)
269 }
270
271 #[tracing::instrument(skip_all, level = "debug")]
273 pub async fn insert_manga_author(
274 &self,
275 manga_id: i32,
276 author_id: i32,
277 ) -> crate::error::ApiResult<()> {
278 mangaauthor::ActiveModel {
279 manga_id: Set(manga_id),
280 author_id: Set(author_id),
281 r#type: Set(1),
282 ..Default::default()
283 }
284 .insert(&self.db)
285 .await?;
286 Ok(())
287 }
288
289 #[tracing::instrument(skip_all, level = "debug")]
291 pub async fn insert_manga_author2(
292 &self,
293 manga_id: i32,
294 author_id: i32,
295 ) -> crate::error::ApiResult<()> {
296 mangaauthor::ActiveModel {
297 manga_id: Set(manga_id),
298 author_id: Set(author_id),
299 r#type: Set(2),
300 ..Default::default()
301 }
302 .insert(&self.db)
303 .await?;
304 Ok(())
305 }
306
307 #[tracing::instrument(skip_all, level = "debug")]
309 pub async fn insert_manga_magazine(
310 &self,
311 manga_id: i32,
312 magazine_id: i32,
313 ) -> crate::error::ApiResult<()> {
314 mangamagazine::ActiveModel {
315 manga_id: Set(Some(manga_id)),
316 magazine_id: Set(Some(magazine_id)),
317 ..Default::default()
318 }
319 .insert(&self.db)
320 .await?;
321 Ok(())
322 }
323
324 #[tracing::instrument(skip_all, level = "debug")]
326 pub async fn get_manga_detail_by_id(
327 &self,
328 id: i32,
329 ) -> crate::error::ApiResult<Option<MangaDetailVo>> {
330 manga_list_assemble::get_manga_detail_by_id(&self.db, id).await
331 }
332
333 #[tracing::instrument(skip_all, level = "debug")]
335 pub async fn get_manga_response_by_id(
336 &self,
337 id: i32,
338 ) -> crate::error::ApiResult<Option<crate::entity::manga::MangaResponse>> {
339 manga_list_assemble::get_manga_response_by_id(&self.db, id).await
340 }
341
342 #[tracing::instrument(skip_all, level = "debug")]
344 pub async fn get_manga_responses_by_ids(
345 &self,
346 ids: &[i32],
347 ) -> crate::error::ApiResult<std::collections::HashMap<i32, crate::entity::manga::MangaResponse>>
348 {
349 manga_list_assemble::get_manga_responses_by_ids(&self.db, ids).await
350 }
351
352 #[tracing::instrument(skip_all, level = "debug")]
354 pub async fn get_manga_card_by_id(
355 &self,
356 id: i32,
357 ) -> crate::error::ApiResult<Option<MangaCardVo>> {
358 manga_list_assemble::get_manga_card_by_id(&self.db, id).await
359 }
360
361 #[tracing::instrument(skip_all, level = "debug")]
363 pub async fn get_manga_by_id(&self, id: i32) -> crate::error::ApiResult<Option<Manga>> {
364 let row = mangatb::Entity::find_by_id(id).one(&self.db).await?;
365 Ok(row.map(model_to_manga))
366 }
367
368 #[tracing::instrument(skip_all, level = "debug")]
370 pub async fn update_manga(&self, manga: &Manga) -> crate::error::ApiResult<()> {
371 let id = manga
372 .id
373 .ok_or_else(|| AppError::business("漫画 ID 不能为空"))?;
374 let existing = mangatb::Entity::find_by_id(id)
375 .one(&self.db)
376 .await?
377 .ok_or_else(|| AppError::business("漫画不存在"))?;
378 let mut am: mangatb::ActiveModel = existing.into();
379 if let Some(ref v) = manga.manga_tran_name {
380 am.manga_tran_name = Set(v.clone());
381 }
382 if let Some(ref v) = manga.manga_ori_name {
383 am.manga_ori_name = Set(v.clone());
384 }
385 if let Some(v) = manga.category {
386 am.category = Set(v as i32);
387 }
388 if let Some(v) = manga.manga_status {
389 am.manga_status = Set(v as i32);
390 }
391 if let Some(ref v) = manga.img_url {
392 am.image = Set(v.clone());
393 }
394 if manga.link.is_some() {
395 am.link = Set(manga.link.clone());
396 }
397 if manga.introduction.is_some() {
398 am.introduction = Set(manga.introduction.clone());
399 }
400 am.update_time = Set(chrono::Utc::now().naive_utc());
401 am.update(&self.db).await?;
402 Ok(())
403 }
404
405 #[tracing::instrument(skip_all, level = "debug")]
407 pub async fn update_manga_author(
408 &self,
409 manga_id: i32,
410 author_id: i32,
411 ) -> crate::error::ApiResult<()> {
412 mangaauthor::Entity::update_many()
413 .col_expr(mangaauthor::Column::AuthorId, author_id.into())
414 .filter(mangaauthor::Column::MangaId.eq(manga_id))
415 .filter(mangaauthor::Column::Type.eq(1))
416 .exec(&self.db)
417 .await?;
418 Ok(())
419 }
420
421 #[tracing::instrument(skip_all, level = "debug")]
423 pub async fn update_manga_author2(
424 &self,
425 manga_id: i32,
426 author_id: i32,
427 ) -> crate::error::ApiResult<()> {
428 mangaauthor::Entity::update_many()
429 .col_expr(mangaauthor::Column::AuthorId, author_id.into())
430 .filter(mangaauthor::Column::MangaId.eq(manga_id))
431 .filter(mangaauthor::Column::Type.eq(2))
432 .exec(&self.db)
433 .await?;
434 Ok(())
435 }
436
437 #[tracing::instrument(skip_all, level = "debug")]
439 pub async fn update_manga_magazine(
440 &self,
441 manga_id: i32,
442 magazine_id: i32,
443 ) -> crate::error::ApiResult<()> {
444 mangamagazine::Entity::update_many()
445 .col_expr(mangamagazine::Column::MagazineId, magazine_id.into())
446 .filter(mangamagazine::Column::MangaId.eq(manga_id))
447 .exec(&self.db)
448 .await?;
449 Ok(())
450 }
451
452 #[tracing::instrument(skip_all, level = "debug")]
454 pub async fn test_author2(&self, manga_id: i32) -> crate::error::ApiResult<bool> {
455 let n = mangaauthor::Entity::find()
456 .filter(mangaauthor::Column::MangaId.eq(manga_id))
457 .filter(mangaauthor::Column::Type.eq(2))
458 .count(&self.db)
459 .await?;
460 Ok(n > 0)
461 }
462
463 #[tracing::instrument(skip_all, level = "debug")]
465 pub async fn test_ori_name(&self, manga_ori_name: &str) -> crate::error::ApiResult<bool> {
466 self.exists_ori_name_for_other(manga_ori_name, None).await
467 }
468
469 #[tracing::instrument(skip_all, level = "debug")]
471 pub async fn exists_tran_name_for_other(
472 &self,
473 manga_tran_name: &str,
474 exclude_id: Option<i32>,
475 ) -> crate::error::ApiResult<bool> {
476 let mut q =
477 mangatb::Entity::find().filter(mangatb::Column::MangaTranName.eq(manga_tran_name));
478 if let Some(id) = exclude_id {
479 q = q.filter(mangatb::Column::Id.ne(id));
480 }
481 Ok(q.one(&self.db).await?.is_some())
482 }
483
484 #[tracing::instrument(skip_all, level = "debug")]
486 pub async fn exists_ori_name_for_other(
487 &self,
488 manga_ori_name: &str,
489 exclude_id: Option<i32>,
490 ) -> crate::error::ApiResult<bool> {
491 let mut q =
492 mangatb::Entity::find().filter(mangatb::Column::MangaOriName.eq(manga_ori_name));
493 if let Some(id) = exclude_id {
494 q = q.filter(mangatb::Column::Id.ne(id));
495 }
496 Ok(q.one(&self.db).await?.is_some())
497 }
498
499 #[tracing::instrument(skip_all, level = "debug")]
501 pub async fn test_manga_magazine(&self, manga_id: i32) -> crate::error::ApiResult<bool> {
502 let n = mangamagazine::Entity::find()
503 .filter(mangamagazine::Column::MangaId.eq(manga_id))
504 .count(&self.db)
505 .await?;
506 Ok(n > 0)
507 }
508
509 #[tracing::instrument(skip_all, level = "debug")]
511 pub async fn test_manga_author1(&self, manga_id: i32) -> crate::error::ApiResult<bool> {
512 let n = mangaauthor::Entity::find()
513 .filter(mangaauthor::Column::MangaId.eq(manga_id))
514 .filter(mangaauthor::Column::Type.eq(1))
515 .count(&self.db)
516 .await?;
517 Ok(n > 0)
518 }
519
520 #[tracing::instrument(skip_all, level = "debug")]
522 pub async fn get_manga_tran_names(&self) -> crate::error::ApiResult<Vec<MangaSimpleVo>> {
523 let rows = mangatb::Entity::find()
524 .order_by_asc(mangatb::Column::Id)
525 .all(&self.db)
526 .await?;
527 Ok(rows
528 .into_iter()
529 .map(|m| MangaSimpleVo {
530 id: m.id,
531 manga_tran_name: Some(m.manga_tran_name),
532 ..Default::default()
533 })
534 .collect())
535 }
536
537 #[tracing::instrument(skip_all, level = "debug")]
539 pub async fn get_max_manga_id(&self) -> crate::error::ApiResult<Option<i32>> {
540 let row = mangatb::Entity::find()
541 .order_by_desc(mangatb::Column::Id)
542 .one(&self.db)
543 .await?;
544 Ok(row.map(|r| r.id))
545 }
546
547 #[tracing::instrument(skip_all, level = "debug")]
549 pub async fn get_collect_detail(
550 &self,
551 manga_id: i32,
552 member_id: i32,
553 ) -> crate::error::ApiResult<Option<MangaCollect>> {
554 let row = mangacollect::Entity::find()
555 .filter(mangacollect::Column::MangaId.eq(manga_id))
556 .filter(mangacollect::Column::MemberId.eq(member_id))
557 .one(&self.db)
558 .await?;
559 Ok(row.map(|r| MangaCollect {
560 id: Some(r.id),
561 manga_id: r.manga_id,
562 member_id: r.member_id,
563 }))
564 }
565
566 #[tracing::instrument(skip_all, level = "debug")]
568 pub async fn get_collect_list(
569 &self,
570 member_id: i32,
571 ) -> crate::error::ApiResult<Vec<MangaCardVo>> {
572 manga_list_assemble::get_collect_list(&self.db, member_id).await
573 }
574
575 #[tracing::instrument(skip_all, level = "debug")]
577 pub async fn get_collect_list_page(
578 &self,
579 member_id: i32,
580 page: i32,
581 page_size: i32,
582 ) -> crate::error::ApiResult<(i64, Vec<MangaCardVo>)> {
583 manga_list_assemble::get_collect_list_page(&self.db, member_id, page, page_size).await
584 }
585
586 #[tracing::instrument(skip_all, level = "debug")]
588 pub async fn get_collected_members(
589 &self,
590 manga_id: i32,
591 ) -> crate::error::ApiResult<Vec<crate::entity::manga::CollectedMembersVo>> {
592 manga_list_assemble::get_collected_members(&self.db, manga_id).await
593 }
594
595 #[tracing::instrument(skip_all, level = "debug")]
597 pub async fn del_collect(&self, manga_id: i32, member_id: i32) -> crate::error::ApiResult<()> {
598 mangacollect::Entity::delete_many()
599 .filter(mangacollect::Column::MangaId.eq(manga_id))
600 .filter(mangacollect::Column::MemberId.eq(member_id))
601 .exec(&self.db)
602 .await?;
603 Ok(())
604 }
605
606 #[tracing::instrument(skip_all, level = "debug")]
608 pub async fn add_collect(&self, manga_id: i32, member_id: i32) -> crate::error::ApiResult<()> {
609 mangacollect::ActiveModel {
610 member_id: Set(member_id),
611 manga_id: Set(manga_id),
612 ..Default::default()
613 }
614 .insert(&self.db)
615 .await?;
616 Ok(())
617 }
618
619 #[tracing::instrument(skip_all, level = "debug")]
621 pub async fn list_glossary(
622 &self,
623 manga_id: i32,
624 type_: Option<i16>,
625 ) -> crate::error::ApiResult<Vec<GlossaryVo>> {
626 let mut q = glossary::Entity::find().filter(glossary::Column::MangaId.eq(manga_id));
627 if let Some(t) = type_ {
628 q = q.filter(glossary::Column::Type.eq(t as i32));
629 }
630 let rows = q.all(&self.db).await?;
631 Ok(assemble_glossary_list(&self.db, rows).await?)
632 }
633
634 #[tracing::instrument(skip_all, level = "debug")]
636 pub async fn delete_glossary_by_id(&self, id: i32) -> crate::error::ApiResult<()> {
637 glossary::Entity::delete_by_id(id).exec(&self.db).await?;
638 Ok(())
639 }
640
641 #[tracing::instrument(skip_all, level = "debug")]
643 pub async fn insert_glossary(
644 &self,
645 req: &GlossaryRequest,
646 update_by: i32,
647 ) -> crate::error::ApiResult<i32> {
648 let inserted = glossary::ActiveModel {
649 r#type: Set(req.r#type.map(|v| v as i32)),
650 image: Set(req.image.clone()),
651 title: Set(req.title.clone()),
652 content: Set(req.content.clone()),
653 manga_id: Set(req.manga_id.map(|v| v as i32)),
654 first: Set(req.first.clone()),
655 update_time: Set(Some(chrono::Utc::now().naive_utc())),
656 update_by: Set(Some(update_by)),
657 ..Default::default()
658 }
659 .insert(&self.db)
660 .await?;
661 Ok(inserted.id)
662 }
663
664 #[tracing::instrument(skip_all, level = "debug")]
666 pub async fn get_glossary_by_id(&self, id: i32) -> crate::error::ApiResult<Option<GlossaryVo>> {
667 let row = glossary::Entity::find_by_id(id).one(&self.db).await?;
668 let Some(g) = row else {
669 return Ok(None);
670 };
671 let name = load_glossary_member_name(&self.db, g.update_by).await?;
672 Ok(Some(manga_list_assemble::glossary_to_vo(g, name)))
673 }
674
675 #[tracing::instrument(skip_all, level = "debug")]
677 pub async fn update_glossary(
678 &self,
679 req: &GlossaryRequest,
680 update_by: i32,
681 ) -> crate::error::ApiResult<()> {
682 let id = req
683 .id
684 .ok_or_else(|| AppError::business("术语 ID 不能为空"))?;
685 let existing = glossary::Entity::find_by_id(id)
686 .one(&self.db)
687 .await?
688 .ok_or_else(|| AppError::business("术语不存在"))?;
689 let mut am: glossary::ActiveModel = existing.into();
690 if let Some(t) = req.r#type {
691 am.r#type = Set(Some(t as i32));
692 }
693 if req.title.is_some() {
694 am.title = Set(req.title.clone());
695 }
696 if req.content.is_some() {
697 am.content = Set(req.content.clone());
698 }
699 if req.image.is_some() {
700 am.image = Set(req.image.clone());
701 }
702 if req.first.is_some() {
703 am.first = Set(req.first.clone());
704 }
705 am.update_time = Set(Some(chrono::Utc::now().naive_utc()));
706 am.update_by = Set(Some(update_by));
707 am.update(&self.db).await?;
708 Ok(())
709 }
710
711 #[tracing::instrument(skip_all, level = "debug")]
713 pub async fn get_manga_rss(
714 &self,
715 ) -> crate::error::ApiResult<Vec<crate::entity::rss::RssMangaRow>> {
716 manga_list_assemble::get_manga_rss(&self.db).await
717 }
718
719 #[tracing::instrument(skip_all, level = "debug")]
721 pub async fn get_manga_updated_rss(
722 &self,
723 ) -> crate::error::ApiResult<Vec<crate::entity::rss::RssMangaRow>> {
724 manga_list_assemble::get_manga_updated_rss(&self.db).await
725 }
726
727 #[tracing::instrument(skip_all, level = "debug")]
729 pub async fn get_episode_rss(
730 &self,
731 ) -> crate::error::ApiResult<Vec<crate::entity::rss::EpisodeRssRow>> {
732 manga_list_assemble::get_episode_rss(&self.db).await
733 }
734
735 #[tracing::instrument(skip_all, level = "debug")]
737 pub async fn get_work_reminder_rss(
738 &self,
739 post_name: &str,
740 ) -> crate::error::ApiResult<Vec<crate::entity::rss::WorkReminderRssRow>> {
741 manga_list_assemble::get_work_reminder_rss(&self.db, post_name).await
742 }
743
744 #[tracing::instrument(skip_all, level = "debug")]
746 pub async fn get_stationed_members(
747 &self,
748 manga_id: i32,
749 ) -> crate::error::ApiResult<Vec<Member>> {
750 if manga_id == 0 {
751 return self.get_pending_stationed_members().await;
752 }
753 let stations = mangastationed::Entity::find()
754 .filter(mangastationed::Column::MangaId.eq(manga_id))
755 .all(&self.db)
756 .await?;
757 assemble_station_members(&self.db, stations, false).await
758 }
759
760 #[tracing::instrument(skip_all, level = "debug")]
762 pub async fn get_pending_stationed_members(&self) -> crate::error::ApiResult<Vec<Member>> {
763 let stations = mangastationed::Entity::find()
764 .filter(
765 sea_orm::Condition::any()
766 .add(mangastationed::Column::Status.eq(0))
767 .add(mangastationed::Column::Status.eq(2)),
768 )
769 .all(&self.db)
770 .await?;
771 assemble_station_members(&self.db, stations, true).await
772 }
773
774 #[tracing::instrument(skip_all, level = "debug")]
776 pub async fn del_station(&self, station_id: i32) -> crate::error::ApiResult<()> {
777 Self::del_station_with(&self.db, station_id).await
778 }
779
780 #[tracing::instrument(skip_all, level = "debug")]
782 pub async fn get_station_by_id(
783 &self,
784 station_id: i32,
785 ) -> crate::error::ApiResult<Option<StationRecord>> {
786 Self::get_station_by_id_with(&self.db, station_id).await
787 }
788
789 #[tracing::instrument(skip_all, level = "debug")]
791 pub async fn fill_empty_episodes(
792 &self,
793 req: &AddStationRequest,
794 ) -> crate::error::ApiResult<()> {
795 Self::fill_empty_episodes_with(&self.db, req).await
796 }
797
798 #[tracing::instrument(skip_all, level = "debug")]
800 pub async fn fill_episode_detail(
801 &self,
802 req: &AddStationRequest,
803 ) -> crate::error::ApiResult<()> {
804 Self::fill_episode_detail_with(&self.db, req).await
805 }
806
807 #[tracing::instrument(skip_all, level = "debug")]
809 pub async fn clear_member_unsubmitted_episodes(
810 &self,
811 manga_id: i32,
812 member_id: i32,
813 post: i32,
814 ) -> crate::error::ApiResult<()> {
815 Self::clear_member_unsubmitted_episodes_with(&self.db, manga_id, member_id, post).await
816 }
817
818 #[tracing::instrument(skip_all, level = "debug")]
820 pub async fn add_station(
821 &self,
822 manga_id: i32,
823 member_id: i32,
824 post: i32,
825 ) -> crate::error::ApiResult<()> {
826 mangastationed::ActiveModel {
827 manga_id: Set(manga_id),
828 member_id: Set(member_id),
829 post: Set(post),
830 status: Set(Some(0)),
831 ..Default::default()
832 }
833 .insert(&self.db)
834 .await?;
835 Ok(())
836 }
837
838 #[tracing::instrument(skip_all, level = "debug")]
840 pub async fn add_station_by_admin(
841 &self,
842 req: &AddStationRequest,
843 ) -> crate::error::ApiResult<()> {
844 Self::add_station_by_admin_with(&self.db, req).await
845 }
846
847 pub(crate) async fn get_station_by_id_with<C: ConnectionTrait>(
849 conn: &C,
850 station_id: i32,
851 ) -> crate::error::ApiResult<Option<StationRecord>> {
852 let row = mangastationed::Entity::find_by_id(station_id)
853 .one(conn)
854 .await?;
855 Ok(row.map(|r| StationRecord {
856 manga_id: r.manga_id,
857 member_id: r.member_id,
858 post: r.post,
859 }))
860 }
861
862 pub(crate) async fn add_station_by_admin_with<C: ConnectionTrait>(
864 conn: &C,
865 req: &AddStationRequest,
866 ) -> crate::error::ApiResult<()> {
867 mangastationed::ActiveModel {
868 manga_id: Set(req.manga_id),
869 member_id: Set(req.member_id),
870 post: Set(req.post),
871 status: Set(Some(1)),
872 ..Default::default()
873 }
874 .insert(conn)
875 .await?;
876 Ok(())
877 }
878
879 pub(crate) async fn fill_empty_episodes_with<C: ConnectionTrait>(
881 conn: &C,
882 req: &AddStationRequest,
883 ) -> crate::error::ApiResult<()> {
884 let ep_ids =
885 episode_ids_for_fill(conn, req.manga_id, req.post, req.member_id, true).await?;
886 if ep_ids.is_empty() {
887 return Ok(());
888 }
889 apply_member_assignment(conn, req.post, &ep_ids, Some(req.member_id)).await
890 }
891
892 pub(crate) async fn fill_episode_detail_with<C: ConnectionTrait>(
894 conn: &C,
895 req: &AddStationRequest,
896 ) -> crate::error::ApiResult<()> {
897 let Some(setup_col) = post_setup_column(req.post) else {
898 return Ok(());
899 };
900 let ep_ids =
901 episode_ids_assigned_with_null_setup(conn, req.manga_id, req.post, req.member_id)
902 .await?;
903 if ep_ids.is_empty() {
904 return Ok(());
905 }
906 let details = mangaepisodedetail::Entity::find()
907 .filter(mangaepisodedetail::Column::EpisodeId.is_in(ep_ids))
908 .all(conn)
909 .await?;
910 let now = chrono::Utc::now().naive_utc();
911 for d in details {
912 if detail_setup_is_null(&d, setup_col) {
913 let mut am: mangaepisodedetail::ActiveModel = d.into();
914 set_detail_setup(&mut am, setup_col, now);
915 am.update(conn).await?;
916 }
917 }
918 Ok(())
919 }
920
921 pub(crate) async fn clear_member_unsubmitted_episodes_with<C: ConnectionTrait>(
923 conn: &C,
924 manga_id: i32,
925 member_id: i32,
926 post: i32,
927 ) -> crate::error::ApiResult<()> {
928 let ep_ids = episode_ids_for_clear(conn, manga_id, post, member_id).await?;
929 if ep_ids.is_empty() {
930 return Ok(());
931 }
932 apply_member_assignment(conn, post, &ep_ids, None).await
933 }
934
935 pub(crate) async fn del_station_with<C: ConnectionTrait>(
937 conn: &C,
938 station_id: i32,
939 ) -> crate::error::ApiResult<()> {
940 mangastationed::Entity::delete_by_id(station_id)
941 .exec(conn)
942 .await?;
943 Ok(())
944 }
945
946 #[tracing::instrument(skip_all, level = "debug")]
948 pub async fn update_station_status(
949 &self,
950 station_id: i32,
951 status: i16,
952 ) -> crate::error::ApiResult<()> {
953 mangastationed::Entity::update_many()
954 .col_expr(mangastationed::Column::Status, (status as i32).into())
955 .filter(mangastationed::Column::Id.eq(station_id))
956 .exec(&self.db)
957 .await?;
958 Ok(())
959 }
960}
961
962pub struct StationRecord {
964 pub manga_id: i32,
966 pub member_id: i32,
968 pub post: i32,
970}
971
972fn model_to_manga(m: mangatb::Model) -> Manga {
974 Manga {
975 id: Some(m.id),
976 manga_tran_name: Some(m.manga_tran_name),
977 manga_ori_name: Some(m.manga_ori_name),
978 category: Some(m.category as i16),
979 manga_status: Some(m.manga_status as i16),
980 img_url: Some(m.image),
981 link: m.link,
982 introduction: m.introduction,
983 update_time: dt_legacy(m.update_time),
984 }
985}
986
987async fn assemble_glossary_list(
989 db: &DatabaseConnection,
990 rows: Vec<glossary::Model>,
991) -> crate::error::ApiResult<Vec<GlossaryVo>> {
992 let member_ids: Vec<i32> = rows.iter().filter_map(|g| g.update_by).collect();
993 let names: std::collections::HashMap<i32, String> = if member_ids.is_empty() {
994 std::collections::HashMap::new()
995 } else {
996 membertb::Entity::find()
997 .filter(membertb::Column::Id.is_in(member_ids))
998 .all(db)
999 .await?
1000 .into_iter()
1001 .map(|m| (m.id, m.username))
1002 .collect()
1003 };
1004 Ok(rows
1005 .into_iter()
1006 .map(|g| {
1007 let name = g.update_by.and_then(|id| names.get(&id).cloned());
1008 manga_list_assemble::glossary_to_vo(g, name)
1009 })
1010 .collect())
1011}
1012
1013async fn load_glossary_member_name(
1015 db: &DatabaseConnection,
1016 update_by: Option<i32>,
1017) -> crate::error::ApiResult<Option<String>> {
1018 let Some(id) = update_by else {
1019 return Ok(None);
1020 };
1021 let m = membertb::Entity::find_by_id(id).one(db).await?;
1022 Ok(m.map(|r| r.username))
1023}
1024
1025async fn assemble_station_members(
1027 db: &DatabaseConnection,
1028 stations: Vec<mangastationed::Model>,
1029 with_manga_name: bool,
1030) -> crate::error::ApiResult<Vec<Member>> {
1031 if stations.is_empty() {
1032 return Ok(vec![]);
1033 }
1034 let member_ids: Vec<i32> = stations.iter().map(|s| s.member_id).collect();
1035 let members: std::collections::HashMap<i32, membertb::Model> = membertb::Entity::find()
1036 .filter(membertb::Column::Id.is_in(member_ids))
1037 .all(db)
1038 .await?
1039 .into_iter()
1040 .map(|m| (m.id, m))
1041 .collect();
1042 let manga_names: std::collections::HashMap<i32, String> = if with_manga_name {
1043 let manga_ids: Vec<i32> = stations.iter().map(|s| s.manga_id).collect();
1044 mangatb::Entity::find()
1045 .filter(mangatb::Column::Id.is_in(manga_ids))
1046 .all(db)
1047 .await?
1048 .into_iter()
1049 .map(|m| (m.id, m.manga_tran_name))
1050 .collect()
1051 } else {
1052 std::collections::HashMap::new()
1053 };
1054 Ok(stations
1055 .into_iter()
1056 .filter_map(|s| {
1057 let m = members.get(&s.member_id)?;
1058 Some(Member {
1059 id: m.id,
1060 username: Some(m.username.clone()),
1061 password: None,
1062 intern: m.intern as i16,
1063 email: m.email.clone(),
1064 avatar_url: m.avatar_url.clone(),
1065 registration_time: dt_legacy(m.registration_time),
1066 last_submit_time: dt_legacy(m.last_submit_time),
1067 posts: vec![],
1068 post_ids: vec![],
1069 station_count: None,
1070 station_id: Some(s.id),
1071 post: Some(s.post),
1072 status: s.status.map(|v| v as i16),
1073 manga_id: Some(s.manga_id),
1074 manga_name: manga_names.get(&s.manga_id).cloned(),
1075 })
1076 })
1077 .collect())
1078}
1079
1080async fn episode_ids_for_fill<C: ConnectionTrait>(
1082 conn: &C,
1083 manga_id: i32,
1084 post: i32,
1085 member_id: i32,
1086 member_must_be_null: bool,
1087) -> crate::error::ApiResult<Vec<i32>> {
1088 let episodes = mangaepisodetb::Entity::find()
1089 .filter(mangaepisodetb::Column::MangaId.eq(manga_id))
1090 .all(conn)
1091 .await?;
1092 let ep_ids: Vec<i32> = episodes
1093 .iter()
1094 .filter(|ep| {
1095 if member_must_be_null {
1096 episode_member_id(ep, post).is_none()
1097 } else {
1098 episode_member_id(ep, post) == Some(member_id)
1099 }
1100 })
1101 .map(|ep| ep.id)
1102 .collect();
1103 if ep_ids.is_empty() {
1104 return Ok(vec![]);
1105 }
1106 let Some(update_col) = post_update_column(post) else {
1107 return Ok(ep_ids);
1108 };
1109 let details: std::collections::HashMap<i32, mangaepisodedetail::Model> =
1110 mangaepisodedetail::Entity::find()
1111 .filter(mangaepisodedetail::Column::EpisodeId.is_in(ep_ids.clone()))
1112 .all(conn)
1113 .await?
1114 .into_iter()
1115 .map(|d| (d.episode_id, d))
1116 .collect();
1117 Ok(ep_ids
1118 .into_iter()
1119 .filter(|id| {
1120 details
1121 .get(id)
1122 .map(|d| detail_update_is_null(d, update_col))
1123 .unwrap_or(true)
1124 })
1125 .collect())
1126}
1127
1128async fn episode_ids_assigned_with_null_setup<C: ConnectionTrait>(
1130 conn: &C,
1131 manga_id: i32,
1132 post: i32,
1133 member_id: i32,
1134) -> crate::error::ApiResult<Vec<i32>> {
1135 let Some(setup_col) = post_setup_column(post) else {
1136 return Ok(vec![]);
1137 };
1138 let episodes = mangaepisodetb::Entity::find()
1139 .filter(mangaepisodetb::Column::MangaId.eq(manga_id))
1140 .all(conn)
1141 .await?;
1142 let ep_ids: Vec<i32> = episodes
1143 .iter()
1144 .filter(|ep| episode_member_id(ep, post) == Some(member_id))
1145 .map(|ep| ep.id)
1146 .collect();
1147 if ep_ids.is_empty() {
1148 return Ok(vec![]);
1149 }
1150 let details: std::collections::HashMap<i32, mangaepisodedetail::Model> =
1151 mangaepisodedetail::Entity::find()
1152 .filter(mangaepisodedetail::Column::EpisodeId.is_in(ep_ids.clone()))
1153 .all(conn)
1154 .await?
1155 .into_iter()
1156 .map(|d| (d.episode_id, d))
1157 .collect();
1158 Ok(ep_ids
1159 .into_iter()
1160 .filter(|id| {
1161 details
1162 .get(id)
1163 .map(|d| detail_setup_is_null(d, setup_col))
1164 .unwrap_or(true)
1165 })
1166 .collect())
1167}
1168
1169async fn episode_ids_for_clear<C: ConnectionTrait>(
1171 conn: &C,
1172 manga_id: i32,
1173 post: i32,
1174 member_id: i32,
1175) -> crate::error::ApiResult<Vec<i32>> {
1176 episode_ids_for_fill(conn, manga_id, post, member_id, false).await
1177}
1178
1179async fn apply_member_assignment<C: ConnectionTrait>(
1181 conn: &C,
1182 post: i32,
1183 ep_ids: &[i32],
1184 member_id: Option<i32>,
1185) -> crate::error::ApiResult<()> {
1186 let value = member_id.into();
1187 match post {
1188 0 => {
1189 mangaepisodetb::Entity::update_many()
1190 .col_expr(mangaepisodetb::Column::ProviderId, value)
1191 .filter(mangaepisodetb::Column::Id.is_in(ep_ids.to_vec()))
1192 .exec(conn)
1193 .await?;
1194 }
1195 1 => {
1196 mangaepisodetb::Entity::update_many()
1197 .col_expr(mangaepisodetb::Column::TranslatorId, value)
1198 .filter(mangaepisodetb::Column::Id.is_in(ep_ids.to_vec()))
1199 .exec(conn)
1200 .await?;
1201 }
1202 2 => {
1203 mangaepisodetb::Entity::update_many()
1204 .col_expr(mangaepisodetb::Column::ProofreaderId, value)
1205 .filter(mangaepisodetb::Column::Id.is_in(ep_ids.to_vec()))
1206 .exec(conn)
1207 .await?;
1208 }
1209 3 => {
1210 mangaepisodetb::Entity::update_many()
1211 .col_expr(mangaepisodetb::Column::LettererId, value)
1212 .filter(mangaepisodetb::Column::Id.is_in(ep_ids.to_vec()))
1213 .exec(conn)
1214 .await?;
1215 }
1216 4 => {
1217 mangaepisodetb::Entity::update_many()
1218 .col_expr(mangaepisodetb::Column::ReviewerId, value)
1219 .filter(mangaepisodetb::Column::Id.is_in(ep_ids.to_vec()))
1220 .exec(conn)
1221 .await?;
1222 }
1223 6 => {
1224 mangaepisodetb::Entity::update_many()
1225 .col_expr(mangaepisodetb::Column::TimerId, value)
1226 .filter(mangaepisodetb::Column::Id.is_in(ep_ids.to_vec()))
1227 .exec(conn)
1228 .await?;
1229 }
1230 _ => return Err(AppError::business("不支持的常驻岗位喵")),
1231 }
1232 Ok(())
1233}
1234
1235fn episode_member_id(ep: &mangaepisodetb::Model, post: i32) -> Option<i32> {
1237 match post {
1238 0 => ep.provider_id,
1239 1 => ep.translator_id,
1240 2 => ep.proofreader_id,
1241 3 => ep.letterer_id,
1242 4 => ep.reviewer_id,
1243 6 => ep.timer_id,
1244 _ => None,
1245 }
1246}
1247
1248fn post_update_column(post: i32) -> Option<&'static str> {
1250 match post {
1251 1 => Some("translatorUpdateTime"),
1252 2 => Some("proofreaderUpdateTime"),
1253 3 => Some("lettererUpdateTime"),
1254 4 => Some("reviewerUpdateTime"),
1255 6 => Some("timerUpdateTime"),
1256 _ => None,
1257 }
1258}
1259
1260fn post_setup_column(post: i32) -> Option<&'static str> {
1262 match post {
1263 1 => Some("translatorSetupTime"),
1264 2 => Some("proofreaderSetupTime"),
1265 3 => Some("lettererSetupTime"),
1266 4 => Some("reviewerSetupTime"),
1267 6 => Some("timerSetupTime"),
1268 _ => None,
1269 }
1270}
1271
1272fn detail_update_is_null(d: &mangaepisodedetail::Model, col: &str) -> bool {
1274 match col {
1275 "translatorUpdateTime" => d.translator_update_time.is_none(),
1276 "proofreaderUpdateTime" => d.proofreader_update_time.is_none(),
1277 "lettererUpdateTime" => d.letterer_update_time.is_none(),
1278 "reviewerUpdateTime" => d.reviewer_update_time.is_none(),
1279 "timerUpdateTime" => d.timer_update_time.is_none(),
1280 _ => true,
1281 }
1282}
1283
1284fn detail_setup_is_null(d: &mangaepisodedetail::Model, col: &str) -> bool {
1286 match col {
1287 "translatorSetupTime" => d.translator_setup_time.is_none(),
1288 "proofreaderSetupTime" => d.proofreader_setup_time.is_none(),
1289 "lettererSetupTime" => d.letterer_setup_time.is_none(),
1290 "reviewerSetupTime" => d.reviewer_setup_time.is_none(),
1291 "timerSetupTime" => d.timer_setup_time.is_none(),
1292 _ => true,
1293 }
1294}
1295
1296fn set_detail_setup(
1298 am: &mut mangaepisodedetail::ActiveModel,
1299 col: &str,
1300 now: chrono::NaiveDateTime,
1301) {
1302 match col {
1303 "translatorSetupTime" => am.translator_setup_time = Set(Some(now)),
1304 "proofreaderSetupTime" => am.proofreader_setup_time = Set(Some(now)),
1305 "lettererSetupTime" => am.letterer_setup_time = Set(Some(now)),
1306 "reviewerSetupTime" => am.reviewer_setup_time = Set(Some(now)),
1307 "timerSetupTime" => am.timer_setup_time = Set(Some(now)),
1308 _ => {}
1309 }
1310}
1311
1312fn dt_legacy(n: chrono::NaiveDateTime) -> Option<DateTime<Utc>> {
1314 Some(crate::utils::legacy_time::naive_db_to_api_utc(n))
1315}