Skip to main content

tdm_server_rust/repository/
manga_repo.rs

1//! 漫画数据访问层 (Manga Repository)
2//!
3//! 封装 `mangatb`、`mangacollect`、`glossary` 等表的查询(SeaORM)。
4
5use 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
24/// 漫画仓储
25pub struct MangaRepository {
26    /// SeaORM 数据库连接
27    db: DbConn,
28}
29
30impl MangaRepository {
31    /// 从 `PgPool` 构造
32    pub fn new(pool: PgPool) -> Self {
33        Self {
34            db: crate::db::from_sqlx_pool(pool),
35        }
36    }
37
38    /// 从 `DatabaseConnection` 构造
39    pub fn from_db(db: DatabaseConnection) -> Self {
40        Self { db }
41    }
42
43    /// 查询全部漫画原名(SeaORM,供 service 缓存)
44    #[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    /// 条件分页查询漫画卡片(全量,Service 层内存分页)
61    #[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    /// 统计符合条件的漫画总数
85    #[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    /// 条件分页查询漫画卡片(SQL LIMIT/OFFSET)
108    #[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    /// 条件分页查询漫画卡片(SQL LIMIT/OFFSET,支持排序)
135    #[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    /// 按作者 ID 分页查询漫画卡片
164    #[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    /// 按作者 ID 查询漫画卡片
175    #[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    /// 按杂志 ID 分页查询漫画卡片
184    #[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    /// 按杂志 ID 查询漫画卡片
195    #[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    /// 按 ID 删除漫画主表
204    #[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    /// 删除漫画作者关联
211    #[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    /// 删除漫画作者关联(作画作者 type=2)
221    #[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    /// 删除漫画话数
232    #[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    /// 删除漫画杂志关联
242    #[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    /// 新增漫画
252    #[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    /// 新增漫画作者(type=1)
272    #[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    /// 新增漫画作者(type=2)
290    #[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    /// 新增漫画杂志关联
308    #[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    /// 按 ID 查询漫画详情
325    #[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    /// 按 ID 查询漫画(对齐 Java getMangaById)
334    #[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    /// 批量按 ID 查询漫画
343    #[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    /// 按 ID 查询漫画卡片详情
353    #[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    /// 按 ID 查询漫画
362    #[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    /// 更新漫画
369    #[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    /// 更新漫画作者(type=1)
406    #[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    /// 更新漫画作者(type=2)
422    #[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    /// 更新漫画杂志关联
438    #[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    /// 检测作画作者是否存在
453    #[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    /// 检测原名是否重复
464    #[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    /// 检测译名是否与其他漫画重复
470    #[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    /// 检测原名是否与其他漫画重复
485    #[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    /// 检测漫画是否已有杂志关联
500    #[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    /// 检测漫画是否已有原作作者关联(type=1)
510    #[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    /// 查询全部译名
521    #[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    /// 获取最大漫画 ID
538    #[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    /// 查询收藏详情
548    #[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    /// 分页查询组员收藏(SQL 真分页)
567    #[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    /// 分页查询组员收藏(返回 total + 当前页卡片)
576    #[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    /// 查询收藏该漫画的组员
587    #[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    /// 删除收藏
596    #[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    /// 新增收藏
607    #[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    /// 查询术语列表
620    #[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    /// 按 ID 删除术语
635    #[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    /// 新增术语
642    #[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    /// 按 ID 查询术语
665    #[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    /// 更新术语
676    #[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    /// 漫画 RSS 列表
712    #[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    /// 最近更新漫画 RSS
720    #[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    /// 话数 RSS 列表
728    #[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    /// 交稿提醒 RSS
736    #[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    /// 查询漫画常驻组员
745    #[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    /// 查询待审批常驻组员(mangaId=0)
761    #[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    /// 删除常驻记录
775    #[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    /// 按 ID 查询常驻记录
781    #[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    /// 填充漫画内该岗位为空且未交稿的单话
790    #[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    /// 填充后写入岗位指派时间
799    #[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    /// 清空组员未交稿单话分配
808    #[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    /// 组员申请常驻
819    #[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    /// 管理员添加常驻
839    #[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    /// 事务内:按 ID 查询常驻记录
848    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    /// 事务内:插入已通过常驻记录
863    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    /// 事务内:填充空且未交稿的单话岗位
880    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    /// 事务内:为刚填充的单话写入岗位指派时间
893    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    /// 事务内:清空组员未交稿单话岗位分配
922    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    /// 事务内:删除常驻记录
936    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    /// 更新常驻状态
947    #[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
962/// 常驻记录摘要
963pub struct StationRecord {
964    /// 漫画 ID
965    pub manga_id: i32,
966    /// 组员 ID
967    pub member_id: i32,
968    /// 岗位 ID
969    pub post: i32,
970}
971
972/// Model 映射为 Manga
973fn 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
987/// 组装术语列表(含更新人名)
988async 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
1013/// 加载术语更新人名
1014async 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
1025/// 组装常驻组员列表
1026async 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
1080/// 填充用:筛选待写入岗位的单话 ID
1081async 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
1128/// 填充接稿时间用:组员已占位且接稿时间为空
1129async 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
1169/// 清空用:筛选未交稿且由该组员占位的单话 ID
1170async 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
1179/// 按岗位批量写入或清空组员 ID
1180async 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
1235/// 读取话数岗位组员 ID
1236fn 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
1248/// 岗位交稿时间列标识
1249fn 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
1260/// 岗位接稿时间列标识
1261fn 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
1272/// 判断详情交稿时间是否为空
1273fn 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
1284/// 判断详情接稿时间是否为空
1285fn 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
1296/// 写入详情接稿时间
1297fn 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
1312/// naive 时间转 API UTC
1313fn dt_legacy(n: chrono::NaiveDateTime) -> Option<DateTime<Utc>> {
1314    Some(crate::utils::legacy_time::naive_db_to_api_utc(n))
1315}