1use crate::utils::query_deserialize::{
7 de_i16, de_i32, de_opt_i16, de_opt_i32, de_opt_string, de_page, de_page_size,
8 de_station_page_size,
9};
10use crate::{
11 common::AppJson,
12 app::AppState,
13 common::{page_bean::PageBean, result::ResultBody},
14 entity::{
15 manga::{
16 AddStationRequest, CollectedMembersVo, GlossaryRequest, GlossaryVo, MangaCollect,
17 MangaDetailVo, MangaListVo, MangaSimpleVo, MangaUpdateRequest,
18 },
19 member::Member,
20 rss::{EpisodeRssRow, RssMangaRow},
21 },
22 error::{ApiResult, AppError},
23 middleware::AuthMember,
24 service::{
25 episode_service::EpisodeService,
26 manga_service::{MangaService, StationMemberBody},
27 },
28};
29use axum::{
30 body::Bytes,
31 extract::{Extension, Multipart, Path, Query, State},
32 http::{header, HeaderValue, StatusCode},
33 response::{IntoResponse, Response},
34 routing::{delete, get, post},
35 Router,
36};
37use serde::Deserialize;
38
39#[derive(Debug, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct MangaPageQuery {
43 #[serde(default, deserialize_with = "de_page")]
45 pub page: i32,
46 #[serde(default, deserialize_with = "de_page_size")]
48 pub page_size: i32,
49 #[serde(default, deserialize_with = "de_opt_string")]
51 pub manga_tran_name: Option<String>,
52 #[serde(default, deserialize_with = "de_opt_string")]
54 pub manga_ori_name: Option<String>,
55 #[serde(default, deserialize_with = "de_opt_i16")]
57 pub category: Option<i16>,
58 #[serde(default, deserialize_with = "de_opt_i16")]
60 pub manga_status: Option<i16>,
61 #[serde(default, deserialize_with = "de_opt_i16")]
63 pub author_id: Option<i16>,
64 #[serde(default, deserialize_with = "de_opt_string")]
66 pub author_name: Option<String>,
67 #[serde(default, deserialize_with = "de_opt_string")]
69 pub magazine_name: Option<String>,
70}
71
72#[derive(Debug, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct CollectListQuery {
76 #[serde(default, deserialize_with = "de_page")]
78 pub page: i32,
79 #[serde(default, deserialize_with = "de_page_size")]
81 pub page_size: i32,
82 #[serde(default, deserialize_with = "de_opt_i32")]
84 pub id: Option<i32>,
85}
86
87#[derive(Debug, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct CollectDetailQuery {
91 #[serde(deserialize_with = "de_i32")]
93 pub manga_id: i32,
94 #[serde(deserialize_with = "de_i32")]
96 pub member_id: i32,
97}
98
99#[derive(Debug, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct GlossaryPageQuery {
103 #[serde(default, deserialize_with = "de_page")]
105 pub page: i32,
106 #[serde(default, deserialize_with = "de_page_size")]
108 pub page_size: i32,
109 #[serde(default, deserialize_with = "de_opt_i16")]
111 pub r#type: Option<i16>,
112 #[serde(deserialize_with = "de_i16")]
114 pub manga_id: i16,
115}
116
117#[derive(Debug, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct UploadFormQuery {
121 #[serde(deserialize_with = "de_i32")]
123 pub id: i32,
124 pub my_name: String,
126 #[serde(deserialize_with = "de_i32")]
128 pub manga_id: i32,
129}
130
131#[derive(Debug, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct StationedMembersQuery {
135 #[serde(default, deserialize_with = "de_page")]
137 pub page: i32,
138 #[serde(default, deserialize_with = "de_station_page_size")]
140 pub page_size: i32,
141 #[serde(deserialize_with = "de_i32")]
143 pub id: i32,
144}
145
146pub fn routes() -> Router<AppState> {
148 Router::new()
149 .route("/", get(page_manga).post(add_manga).put(update_manga))
150 .route("/mangaTranName", get(get_manga_tran_name))
151 .route("/mangaOriName", get(get_manga_ori_name))
152 .route("/:id", get(get_manga_by_id).delete(delete_manga))
153 .route(
154 "/collect",
155 get(get_collect_detail)
156 .delete(del_collect)
157 .post(add_collect),
158 )
159 .route("/collectList", get(get_collect_list))
160 .route("/collectedMembers", get(get_collected_members))
161 .route(
162 "/glossary",
163 get(page_glossary).post(add_glossary).put(update_glossary),
164 )
165 .route(
166 "/glossary/:id",
167 get(get_glossary_by_id).delete(delete_glossary),
168 )
169 .route("/mangaRss", get(get_manga_rss))
170 .route("/episodeRss", get(get_episode_rss))
171 .route("/rssOutput", post(rss_output))
172 .route("/uploadService", post(upload))
173 .route("/downloadService", post(download_file))
174 .route("/stationedMembers", get(get_stationed_members))
175 .route("/station/:stationId", delete(del_station))
176 .route("/station", post(add_station).put(update_station))
177 .route("/station/admin", post(add_station_by_admin))
178}
179
180#[tracing::instrument(skip_all, level = "info")]
182pub async fn page_manga(
183 State(state): State<AppState>,
184 Query(q): Query<MangaPageQuery>,
185) -> ApiResult<ResultBody<PageBean<MangaListVo>>> {
186 let data = MangaService::page(
187 &state,
188 q.page,
189 q.page_size,
190 q.manga_tran_name,
191 q.manga_ori_name,
192 q.category,
193 q.manga_status,
194 q.author_id,
195 q.author_name,
196 q.magazine_name,
197 )
198 .await?;
199 Ok(ResultBody::success_data(data))
200}
201
202#[tracing::instrument(skip_all, level = "info")]
204pub async fn get_manga_tran_name(
205 State(state): State<AppState>,
206) -> ApiResult<ResultBody<Vec<MangaSimpleVo>>> {
207 let data = MangaService::get_manga_tran_name(&state).await?;
208 Ok(ResultBody::success_data(data))
209}
210
211#[tracing::instrument(skip_all, level = "info")]
213pub async fn get_manga_ori_name(
214 State(state): State<AppState>,
215) -> ApiResult<ResultBody<Vec<MangaSimpleVo>>> {
216 let data = MangaService::get_manga_ori_name(&state).await?;
217 Ok(ResultBody::success_data(data))
218}
219
220#[tracing::instrument(skip_all, level = "info")]
222pub async fn delete_manga(
223 State(state): State<AppState>,
224 Path(id): Path<i32>,
225) -> ApiResult<ResultBody<()>> {
226 MangaService::delete_manga(&state, id).await?;
227 Ok(ResultBody::success())
228}
229
230#[tracing::instrument(skip_all, level = "info")]
232pub async fn add_manga(
233 State(state): State<AppState>,
234 Extension(AuthMember(member)): Extension<AuthMember>,
235 mut multipart: Multipart,
236) -> ApiResult<ResultBody<()>> {
237 let member_id = member.map(|m| m.id).unwrap_or(0);
238 let (image, metadata) = parse_manga_multipart(&mut multipart).await?;
239 MangaService::add_new_manga(&state, metadata, image, member_id).await?;
240 Ok(ResultBody::success())
241}
242
243#[tracing::instrument(skip_all, level = "info")]
245pub async fn get_manga_by_id(
246 State(state): State<AppState>,
247 Path(id): Path<i32>,
248) -> ApiResult<ResultBody<MangaDetailVo>> {
249 let data = MangaService::get_manga_by_id(&state, id).await?;
250 Ok(ResultBody::success_data(data))
251}
252
253#[tracing::instrument(skip_all, level = "info")]
255pub async fn update_manga(
256 State(state): State<AppState>,
257 Extension(AuthMember(member)): Extension<AuthMember>,
258 mut multipart: Multipart,
259) -> ApiResult<ResultBody<()>> {
260 let member_id = member.map(|m| m.id).unwrap_or(0);
261 let (image, metadata) = parse_manga_multipart(&mut multipart).await?;
262 MangaService::update_manga(&state, metadata, image, member_id).await?;
263 Ok(ResultBody::success())
264}
265
266#[tracing::instrument(skip_all, level = "info")]
268pub async fn get_collect_detail(
269 State(state): State<AppState>,
270 Query(q): Query<CollectDetailQuery>,
271) -> ApiResult<ResultBody<MangaCollect>> {
272 let collect = MangaCollect {
273 id: None,
274 manga_id: q.manga_id,
275 member_id: q.member_id,
276 };
277 let data = MangaService::get_collect_detail(&state, collect).await?;
278 Ok(ResultBody::success_data(data))
279}
280
281#[tracing::instrument(skip_all, level = "info")]
283pub async fn get_collect_list(
284 State(state): State<AppState>,
285 Query(q): Query<CollectListQuery>,
286) -> ApiResult<ResultBody<PageBean<MangaListVo>>> {
287 let id = q.id.ok_or_else(|| AppError::business("无效的 ID 喵"))?;
288 let data = MangaService::get_collect_list(&state, q.page, q.page_size, id).await?;
289 Ok(ResultBody::success_data(data))
290}
291
292#[tracing::instrument(skip_all, level = "info")]
294pub async fn get_collected_members(
295 State(state): State<AppState>,
296 Query(q): Query<CollectListQuery>,
297) -> ApiResult<ResultBody<PageBean<CollectedMembersVo>>> {
298 let id = q.id.ok_or_else(|| AppError::business("无效的 ID 喵"))?;
299 let data = MangaService::get_collected_members(&state, q.page, q.page_size, id).await?;
300 Ok(ResultBody::success_data(data))
301}
302
303#[tracing::instrument(skip_all, level = "info")]
305pub async fn del_collect(
306 State(state): State<AppState>,
307 Query(q): Query<CollectDetailQuery>,
308) -> ApiResult<ResultBody<()>> {
309 MangaService::del_collect(
310 &state,
311 MangaCollect {
312 id: None,
313 manga_id: q.manga_id,
314 member_id: q.member_id,
315 },
316 )
317 .await?;
318 Ok(ResultBody::success())
319}
320
321#[tracing::instrument(skip_all, level = "info")]
323pub async fn add_collect(
324 State(state): State<AppState>,
325 AppJson(body): AppJson<MangaCollect>,
326) -> ApiResult<ResultBody<()>> {
327 MangaService::add_collect(&state, body).await?;
328 Ok(ResultBody::success())
329}
330
331#[tracing::instrument(skip_all, level = "info")]
333pub async fn page_glossary(
334 State(state): State<AppState>,
335 Query(q): Query<GlossaryPageQuery>,
336) -> ApiResult<ResultBody<PageBean<GlossaryVo>>> {
337 let data =
338 MangaService::page_glossary(&state, q.page, q.page_size, q.r#type, q.manga_id).await?;
339 Ok(ResultBody::success_data(data))
340}
341
342#[tracing::instrument(skip_all, level = "info")]
344pub async fn delete_glossary(
345 State(state): State<AppState>,
346 Path(id): Path<i32>,
347) -> ApiResult<ResultBody<()>> {
348 MangaService::delete_glossary(&state, id).await?;
349 Ok(ResultBody::success())
350}
351
352#[tracing::instrument(skip_all, level = "info")]
354pub async fn add_glossary(
355 State(state): State<AppState>,
356 Extension(AuthMember(member)): Extension<AuthMember>,
357 mut multipart: Multipart,
358) -> ApiResult<ResultBody<()>> {
359 let member_id = member.map(|m| m.id).unwrap_or(0);
360 let metadata = parse_glossary_multipart(&mut multipart).await?;
361 MangaService::add_glossary(&state, metadata, member_id).await?;
362 Ok(ResultBody::success())
363}
364
365#[tracing::instrument(skip_all, level = "info")]
367pub async fn get_glossary_by_id(
368 State(state): State<AppState>,
369 Path(id): Path<i32>,
370) -> ApiResult<ResultBody<GlossaryVo>> {
371 let data = MangaService::get_glossary_by_id(&state, id).await?;
372 Ok(ResultBody::success_data(data))
373}
374
375#[tracing::instrument(skip_all, level = "info")]
377pub async fn update_glossary(
378 State(state): State<AppState>,
379 Extension(AuthMember(member)): Extension<AuthMember>,
380 mut multipart: Multipart,
381) -> ApiResult<ResultBody<()>> {
382 let member_id = member.map(|m| m.id).unwrap_or(0);
383 let metadata = parse_glossary_multipart(&mut multipart).await?;
384 MangaService::update_glossary(&state, metadata, member_id).await?;
385 Ok(ResultBody::success())
386}
387
388#[tracing::instrument(skip_all, level = "info")]
390pub async fn get_manga_rss(State(state): State<AppState>) -> ApiResult<ResultBody<Vec<RssMangaRow>>> {
391 let data = MangaService::get_manga_rss(&state).await?;
392 Ok(ResultBody::success_data(data))
393}
394
395#[tracing::instrument(skip_all, level = "info")]
397pub async fn get_episode_rss(
398 State(state): State<AppState>,
399) -> ApiResult<ResultBody<Vec<EpisodeRssRow>>> {
400 let data = MangaService::get_episode_rss(&state).await?;
401 Ok(ResultBody::success_data(data))
402}
403
404#[tracing::instrument(skip_all, level = "info")]
406pub async fn rss_output(
407 State(state): State<AppState>,
408 AppJson(xml): AppJson<String>,
409) -> ApiResult<ResultBody<()>> {
410 MangaService::rss_output(&state, xml).await?;
411 Ok(ResultBody::success())
412}
413
414#[tracing::instrument(skip_all, level = "info")]
416pub async fn upload(
417 State(state): State<AppState>,
418 Query(q): Query<UploadFormQuery>,
419 mut multipart: Multipart,
420) -> ApiResult<ResultBody<()>> {
421 let mut data = Bytes::new();
422 let mut filename = String::from("upload.dat");
423 while let Some(field) = multipart
424 .next_field()
425 .await
426 .map_err(|e| AppError::Internal(e.to_string()))?
427 {
428 if field.name() == Some("dataRaw") {
429 if let Some(name) = field.file_name() {
430 filename = name.to_string();
431 }
432 data = field
433 .bytes()
434 .await
435 .map_err(|e| AppError::Internal(e.to_string()))?;
436 }
437 }
438 EpisodeService::upload_manga_file(&state, data, q.id, &q.my_name, q.manga_id, &filename).await?;
439 Ok(ResultBody::success())
440}
441
442#[tracing::instrument(skip_all, level = "info")]
444pub async fn download_file(
445 State(state): State<AppState>,
446 AppJson(body): AppJson<crate::entity::manga::EpisodeDownloadRequest>,
447) -> ApiResult<Response> {
448 let (filename, bytes) = EpisodeService::download_episode_file(&state, body).await?;
449 let encoded = urlencoding::encode(&filename);
450 let mut headers = axum::http::HeaderMap::new();
451 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"));
452 headers.insert(
453 header::CONTENT_DISPOSITION,
454 HeaderValue::from_str(&format!("attachment; filename={encoded}"))
455 .map_err(|e| AppError::Internal(format!("生成下载文件名响应头失败: {e}")))?,
456 );
457 headers.insert(
458 header::ACCESS_CONTROL_EXPOSE_HEADERS,
459 HeaderValue::from_static("Content-Disposition,Content-Type,Content-Length"),
460 );
461 Ok((StatusCode::OK, headers, bytes).into_response())
462}
463
464#[tracing::instrument(skip_all, level = "info")]
466pub async fn get_stationed_members(
467 State(state): State<AppState>,
468 Query(q): Query<StationedMembersQuery>,
469) -> ApiResult<ResultBody<PageBean<Member>>> {
470 let data = MangaService::get_stationed_members(&state, q.page, q.page_size, q.id).await?;
471 Ok(ResultBody::success_data(data))
472}
473
474#[tracing::instrument(skip_all, level = "info")]
476pub async fn del_station(
477 State(state): State<AppState>,
478 Path(station_id): Path<i32>,
479) -> ApiResult<ResultBody<()>> {
480 MangaService::del_station(&state, station_id).await?;
481 Ok(ResultBody::success())
482}
483
484#[tracing::instrument(skip_all, level = "info")]
486pub async fn add_station(
487 State(state): State<AppState>,
488 AppJson(body): AppJson<StationMemberBody>,
489) -> ApiResult<ResultBody<()>> {
490 MangaService::add_station(&state, body).await?;
491 Ok(ResultBody::success())
492}
493
494#[tracing::instrument(skip_all, level = "info")]
496pub async fn add_station_by_admin(
497 State(state): State<AppState>,
498 AppJson(body): AppJson<AddStationRequest>,
499) -> ApiResult<ResultBody<()>> {
500 MangaService::add_station_by_admin(&state, body).await?;
501 Ok(ResultBody::success())
502}
503
504#[tracing::instrument(skip_all, level = "info")]
506pub async fn update_station(
507 State(state): State<AppState>,
508 AppJson(body): AppJson<StationMemberBody>,
509) -> ApiResult<ResultBody<()>> {
510 MangaService::update_station(&state, body).await?;
511 Ok(ResultBody::success())
512}
513
514#[tracing::instrument(skip_all, level = "info")]
515async fn parse_manga_multipart(
516 multipart: &mut Multipart,
517) -> ApiResult<(Option<Bytes>, MangaUpdateRequest)> {
518 let mut image = None;
519 let mut metadata = MangaUpdateRequest::default();
520 while let Some(field) = multipart
521 .next_field()
522 .await
523 .map_err(|e| AppError::Internal(e.to_string()))?
524 {
525 match field.name() {
526 Some("imageRaw") => {
527 image = Some(
528 field
529 .bytes()
530 .await
531 .map_err(|e| AppError::Internal(e.to_string()))?,
532 );
533 }
534 Some("metadata") => {
535 let raw = field
536 .bytes()
537 .await
538 .map_err(|e| AppError::Internal(e.to_string()))?;
539 metadata = serde_json::from_slice(&raw)
540 .map_err(|e| AppError::business(format!("metadata 解析失败: {e}")))?;
541 }
542 _ => {}
543 }
544 }
545 Ok((image, metadata))
546}
547
548#[tracing::instrument(skip_all, level = "info")]
549async fn parse_glossary_multipart(multipart: &mut Multipart) -> ApiResult<GlossaryRequest> {
550 let mut metadata = GlossaryRequest::default();
551 while let Some(field) = multipart
552 .next_field()
553 .await
554 .map_err(|e| AppError::Internal(e.to_string()))?
555 {
556 if field.name() == Some("metadata") {
557 let raw = field
558 .bytes()
559 .await
560 .map_err(|e| AppError::Internal(e.to_string()))?;
561 crate::utils::agent_debug::log(
563 "C",
564 "manga_controller.rs:parse_glossary_multipart",
565 "glossary metadata raw",
566 serde_json::json!({ "raw": String::from_utf8_lossy(&raw) }),
567 );
568 metadata = serde_json::from_slice(&raw)
570 .map_err(|e| AppError::business(format!("metadata 解析失败: {e}")))?;
571 }
572 }
573 Ok(metadata)
574}