1use 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
22pub struct MagazineService;
24
25impl MagazineService {
26 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
148fn 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
164fn 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
221fn 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}