Skip to main content

tdm_server_rust/service/
magazine_service.rs

1//! 杂志业务服务 (Magazine Service)
2//!
3//! 杂志信息的增删改查及关联漫画查询。
4
5use crate::{
6    app::AppState,
7    cache::{
8        get_list_page_json_cached, invalidate_magazine_manga_caches, magazine_manga_cache_key,
9        on_author_or_magazine_renamed, set_list_page_json_cached,
10    },
11    common::PageBean,
12    entity::{magazine::Magazine, manga::MangaListVo},
13    error::{ApiResult, AppError},
14    repository::{
15        magazine_repo::{MagazineRepository, MagazineRow},
16        manga_repo::MangaRepository,
17    },
18    utils::page::{paginate, slice_rows},
19};
20use std::sync::Arc;
21
22/// 杂志服务
23pub struct MagazineService;
24
25impl MagazineService {
26    /// 分页查询杂志
27    #[tracing::instrument(skip_all, level = "debug")]
28    pub async fn get_magazines(
29        state: &AppState,
30        page: i32,
31        page_size: i32,
32        magazine_id: Option<i16>,
33        magazine_name: Option<String>,
34    ) -> ApiResult<PageBean<Magazine>> {
35        let repo = MagazineRepository::new(state.db.clone());
36        let all = repo
37            .get_magazines(magazine_id, magazine_name.as_deref())
38            .await?;
39        Ok(paginate(
40            all.into_iter().map(row_to_magazine).collect(),
41            page,
42            page_size,
43        ))
44    }
45
46    /// 全量杂志列表
47    #[tracing::instrument(skip_all, level = "debug")]
48    pub async fn get_manga_magazine_list(state: &AppState) -> ApiResult<Vec<Magazine>> {
49        let repo = MagazineRepository::new(state.db.clone());
50        let rows = repo.get_manga_magazine_list().await?;
51        Ok(rows.into_iter().map(row_to_magazine).collect())
52    }
53
54    /// 查询杂志关联漫画
55    #[tracing::instrument(skip_all, level = "debug")]
56    pub async fn get_magazine_manga(
57        state: &AppState,
58        page: i32,
59        page_size: i32,
60        id: i32,
61    ) -> ApiResult<PageBean<MangaListVo>> {
62        let (total, rows) = MangaRepository::new(state.db.clone())
63            .list_by_magazine_page(id, page, page_size)
64            .await?;
65        Ok(slice_rows(rows, total))
66    }
67
68    /// 杂志漫画分页预序列化 JSON(短 TTL 缓存)
69    #[tracing::instrument(skip_all, level = "debug")]
70    pub async fn get_magazine_manga_json(
71        state: &AppState,
72        page: i32,
73        page_size: i32,
74        id: i32,
75    ) -> ApiResult<Arc<Vec<u8>>> {
76        let key = magazine_manga_cache_key(state, id, page, page_size);
77        if let Some(json) = get_list_page_json_cached(state, &key).await {
78            return Ok(json);
79        }
80        let page_data = Self::get_magazine_manga(state, page, page_size, id).await?;
81        set_list_page_json_cached(state, key.clone(), page_data).await;
82        get_list_page_json_cached(state, &key)
83            .await
84            .ok_or_else(|| AppError::business("杂志漫画缓存写入失败"))
85    }
86
87    /// 删除杂志
88    #[tracing::instrument(skip_all, level = "debug")]
89    pub async fn delete_magazine(state: &AppState, id: i32) -> ApiResult<()> {
90        let repo = MagazineRepository::new(state.db.clone());
91        if repo.get_related_magazine(id).await? {
92            return Err(AppError::business("该杂志已经绑定漫画了喵!"));
93        }
94        repo.delete_magazine_by_id(id).await?;
95        invalidate_magazine_manga_caches(state).await;
96        Ok(())
97    }
98
99    /// 新增杂志
100    #[tracing::instrument(skip_all, level = "debug")]
101    pub async fn add_magazine(state: &AppState, magazine: Magazine) -> ApiResult<()> {
102        let repo = MagazineRepository::new(state.db.clone());
103        if let Some(name) = &magazine.magazine_name {
104            if repo.test_magazine_name(name).await?.is_some() {
105                return Err(AppError::unique("杂志名"));
106            }
107        }
108        let row = magazine_to_row(&magazine);
109        repo.insert_magazine(&row).await?;
110        Ok(())
111    }
112
113    /// 按 ID 查询杂志
114    #[tracing::instrument(skip_all, level = "debug")]
115    pub async fn get_magazine_by_id(state: &AppState, id: i32) -> ApiResult<Magazine> {
116        let repo = MagazineRepository::new(state.db.clone());
117        repo.get_magazine_by_id(id)
118            .await?
119            .map(row_to_magazine)
120            .ok_or_else(|| AppError::business("杂志不存在喵"))
121    }
122
123    /// 更新杂志
124    #[tracing::instrument(skip_all, level = "debug")]
125    pub async fn update_magazine(state: &AppState, magazine: Magazine) -> ApiResult<()> {
126        let repo = MagazineRepository::new(state.db.clone());
127        let id = magazine
128            .id
129            .ok_or_else(|| AppError::business("缺少杂志 ID"))?;
130        let mut row = repo
131            .get_magazine_by_id(id)
132            .await?
133            .ok_or_else(|| AppError::business("杂志不存在喵"))?;
134        apply_magazine_fields(&mut row, &magazine);
135        if let Some(name) = &row.magazine_name {
136            if let Some(existing) = repo.test_magazine_name(name).await? {
137                if existing.id != Some(id) {
138                    return Err(AppError::unique("杂志名"));
139                }
140            }
141        }
142        repo.update_magazine(&row).await?;
143        on_author_or_magazine_renamed(state).await;
144        Ok(())
145    }
146}
147
148/// 请求字段合并到杂志行
149fn apply_magazine_fields(row: &mut MagazineRow, magazine: &Magazine) {
150    if let Some(v) = &magazine.magazine_name {
151        row.magazine_name = Some(v.clone());
152    }
153    if let Some(v) = magazine.r#type {
154        row.type_ = Some(v);
155    }
156    if let Some(v) = &magazine.update_time {
157        row.update_time = Some(v.clone());
158    }
159    if let Some(v) = magazine.price {
160        row.price = Some(v);
161    }
162}
163
164/// 杂志实体转行
165fn magazine_to_row(magazine: &Magazine) -> MagazineRow {
166    MagazineRow {
167        id: magazine.id,
168        magazine_name: magazine.magazine_name.clone(),
169        type_: magazine.r#type,
170        update_time: magazine.update_time.clone(),
171        price: magazine.price,
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::{apply_magazine_fields, magazine_to_row};
178    use crate::entity::magazine::Magazine;
179    use crate::repository::magazine_repo::MagazineRow;
180
181    #[test]
182    fn magazine_to_row_maps_all_fields() {
183        let m = Magazine {
184            id: None,
185            magazine_name: Some("季刊".into()),
186            r#type: Some(4),
187            update_time: Some("每月1日".into()),
188            price: Some(680),
189        };
190        let row = magazine_to_row(&m);
191        assert_eq!(row.magazine_name.as_deref(), Some("季刊"));
192        assert_eq!(row.type_, Some(4));
193        assert_eq!(row.update_time.as_deref(), Some("每月1日"));
194        assert_eq!(row.price, Some(680));
195    }
196
197    #[test]
198    fn apply_magazine_fields_merges_partial_update() {
199        let mut row = MagazineRow {
200            id: Some(1),
201            magazine_name: Some("旧名".into()),
202            type_: Some(1),
203            update_time: Some("旧时间".into()),
204            price: Some(100),
205        };
206        let patch = Magazine {
207            id: Some(1),
208            magazine_name: None,
209            r#type: Some(5),
210            update_time: Some("新时间".into()),
211            price: None,
212        };
213        apply_magazine_fields(&mut row, &patch);
214        assert_eq!(row.magazine_name.as_deref(), Some("旧名"));
215        assert_eq!(row.type_, Some(5));
216        assert_eq!(row.update_time.as_deref(), Some("新时间"));
217        assert_eq!(row.price, Some(100));
218    }
219}
220
221/// 杂志行转实体
222fn row_to_magazine(row: MagazineRow) -> Magazine {
223    Magazine {
224        id: row.id,
225        magazine_name: row.magazine_name,
226        r#type: row.type_,
227        update_time: row.update_time,
228        price: row.price,
229    }
230}