Skip to main content

tdm_server_rust/web/
manga_controller.rs

1//! 漫画管理接口 (Manga Controller)
2//!
3//! 漫画 CRUD、收藏、术语表、常驻管理、RSS 导出。
4//! 对应 Java MangaController。
5
6use 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/// 漫画分页查询参数
40#[derive(Debug, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct MangaPageQuery {
43    /// 页码
44    #[serde(default, deserialize_with = "de_page")]
45    pub page: i32,
46    /// 每页条数
47    #[serde(default, deserialize_with = "de_page_size")]
48    pub page_size: i32,
49    /// 译名
50    #[serde(default, deserialize_with = "de_opt_string")]
51    pub manga_tran_name: Option<String>,
52    /// 原名
53    #[serde(default, deserialize_with = "de_opt_string")]
54    pub manga_ori_name: Option<String>,
55    /// 分类
56    #[serde(default, deserialize_with = "de_opt_i16")]
57    pub category: Option<i16>,
58    /// 连载状态
59    #[serde(default, deserialize_with = "de_opt_i16")]
60    pub manga_status: Option<i16>,
61    /// 作者 ID
62    #[serde(default, deserialize_with = "de_opt_i16")]
63    pub author_id: Option<i16>,
64    /// 作者名
65    #[serde(default, deserialize_with = "de_opt_string")]
66    pub author_name: Option<String>,
67    /// 杂志名
68    #[serde(default, deserialize_with = "de_opt_string")]
69    pub magazine_name: Option<String>,
70}
71
72/// 收藏列表查询参数
73#[derive(Debug, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct CollectListQuery {
76    /// 页码
77    #[serde(default, deserialize_with = "de_page")]
78    pub page: i32,
79    /// 每页条数
80    #[serde(default, deserialize_with = "de_page_size")]
81    pub page_size: i32,
82    /// 组员或漫画 ID
83    #[serde(default, deserialize_with = "de_opt_i32")]
84    pub id: Option<i32>,
85}
86
87/// 收藏详情查询参数
88#[derive(Debug, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct CollectDetailQuery {
91    /// 漫画 ID
92    #[serde(deserialize_with = "de_i32")]
93    pub manga_id: i32,
94    /// 组员 ID
95    #[serde(deserialize_with = "de_i32")]
96    pub member_id: i32,
97}
98
99/// 术语分页参数
100#[derive(Debug, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct GlossaryPageQuery {
103    /// 页码
104    #[serde(default, deserialize_with = "de_page")]
105    pub page: i32,
106    /// 每页条数
107    #[serde(default, deserialize_with = "de_page_size")]
108    pub page_size: i32,
109    /// 术语类型
110    #[serde(default, deserialize_with = "de_opt_i16")]
111    pub r#type: Option<i16>,
112    /// 漫画 ID
113    #[serde(deserialize_with = "de_i16")]
114    pub manga_id: i16,
115}
116
117/// 上传文件表单参数
118#[derive(Debug, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct UploadFormQuery {
121    /// 话数 ID
122    #[serde(deserialize_with = "de_i32")]
123    pub id: i32,
124    /// 岗位名
125    pub my_name: String,
126    /// 漫画 ID
127    #[serde(deserialize_with = "de_i32")]
128    pub manga_id: i32,
129}
130
131/// 常驻组员分页参数
132#[derive(Debug, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct StationedMembersQuery {
135    /// 页码
136    #[serde(default, deserialize_with = "de_page")]
137    pub page: i32,
138    /// 每页条数
139    #[serde(default, deserialize_with = "de_station_page_size")]
140    pub page_size: i32,
141    /// 漫画 ID
142    #[serde(deserialize_with = "de_i32")]
143    pub id: i32,
144}
145
146/// 漫画路由(挂载于 `/api/mangas`)
147pub 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/// 分页查询漫画
181#[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/// 全部译名
203#[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/// 全部原名
212#[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/// 删除漫画
221#[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/// 新增漫画(multipart)
231#[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/// 按 ID 查询漫画
244#[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/// 更新漫画(multipart)
254#[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/// 查询收藏详情
267#[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/// 收藏列表
282#[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/// 收藏组员列表
293#[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/// 删除收藏
304#[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/// 新增收藏
322#[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/// 分页查询术语
332#[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/// 删除术语
343#[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/// 新增术语
353#[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/// 查询术语
366#[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/// 更新术语
376#[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/// 漫画 RSS
389#[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/// 话数 RSS
396#[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/// RSS 输出
405#[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/// 上传话数文件
415#[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/// 下载话数文件
443#[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/// 常驻组员列表
465#[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/// 删除常驻
475#[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/// 申请常驻
485#[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/// 管理员添加常驻
495#[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/// 更新常驻状态
505#[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            // #region agent log
562            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            // #endregion
569            metadata = serde_json::from_slice(&raw)
570                .map_err(|e| AppError::business(format!("metadata 解析失败: {e}")))?;
571        }
572    }
573    Ok(metadata)
574}