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