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    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/// 漫画分页查询参数
43#[derive(Debug, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct MangaPageQuery {
46    /// 页码
47    #[serde(default, deserialize_with = "de_page")]
48    pub page: i32,
49    /// 每页条数
50    #[serde(default, deserialize_with = "de_page_size")]
51    pub page_size: i32,
52    /// 译名
53    #[serde(default, deserialize_with = "de_opt_string")]
54    pub manga_tran_name: Option<String>,
55    /// 原名
56    #[serde(default, deserialize_with = "de_opt_string")]
57    pub manga_ori_name: Option<String>,
58    /// 分类
59    #[serde(default, deserialize_with = "de_opt_i16")]
60    pub category: Option<i16>,
61    /// 连载状态
62    #[serde(default, deserialize_with = "de_opt_i16")]
63    pub manga_status: Option<i16>,
64    /// 作者 ID
65    #[serde(default, deserialize_with = "de_opt_i16")]
66    pub author_id: Option<i16>,
67    /// 作者名
68    #[serde(default, deserialize_with = "de_opt_string")]
69    pub author_name: Option<String>,
70    /// 杂志名
71    #[serde(default, deserialize_with = "de_opt_string")]
72    pub magazine_name: Option<String>,
73    /// 排序字段:updateTime / setupTime(兼容 createTime、createdAt、addTime)
74    #[serde(default, deserialize_with = "de_opt_string")]
75    pub sort_prop: Option<String>,
76    /// 排序方向:descending / ascending
77    #[serde(default, deserialize_with = "de_opt_string")]
78    pub sort_order: Option<String>,
79}
80
81/// 收藏列表查询参数
82#[derive(Debug, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct CollectListQuery {
85    /// 页码
86    #[serde(default, deserialize_with = "de_page")]
87    pub page: i32,
88    /// 每页条数
89    #[serde(default, deserialize_with = "de_page_size")]
90    pub page_size: i32,
91    /// 组员或漫画 ID
92    #[serde(default, deserialize_with = "de_opt_i32")]
93    pub id: Option<i32>,
94}
95
96/// 收藏详情查询参数
97#[derive(Debug, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct CollectDetailQuery {
100    /// 漫画 ID
101    #[serde(deserialize_with = "de_i32")]
102    pub manga_id: i32,
103    /// 组员 ID
104    #[serde(deserialize_with = "de_i32")]
105    pub member_id: i32,
106}
107
108/// 术语分页参数
109#[derive(Debug, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct GlossaryPageQuery {
112    /// 页码
113    #[serde(default, deserialize_with = "de_page")]
114    pub page: i32,
115    /// 每页条数
116    #[serde(default, deserialize_with = "de_page_size")]
117    pub page_size: i32,
118    /// 术语类型
119    #[serde(default, deserialize_with = "de_opt_i16")]
120    pub r#type: Option<i16>,
121    /// 漫画 ID
122    #[serde(deserialize_with = "de_i16")]
123    pub manga_id: i16,
124}
125
126/// 上传文件表单参数
127#[derive(Debug, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct UploadFormQuery {
130    /// 话数 ID
131    #[serde(deserialize_with = "de_i32")]
132    pub id: i32,
133    /// 岗位名
134    pub my_name: String,
135    /// 漫画 ID
136    #[serde(deserialize_with = "de_i32")]
137    pub manga_id: i32,
138}
139
140/// 常驻组员分页参数
141#[derive(Debug, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct StationedMembersQuery {
144    /// 页码
145    #[serde(default, deserialize_with = "de_page")]
146    pub page: i32,
147    /// 每页条数
148    #[serde(default, deserialize_with = "de_station_page_size")]
149    pub page_size: i32,
150    /// 漫画 ID
151    #[serde(deserialize_with = "de_i32")]
152    pub id: i32,
153}
154
155/// 漫画路由(挂载于 `/api/mangas`)
156pub 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/// 分页查询漫画
190#[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/// 全部译名
214#[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/// 全部原名
223#[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/// 删除漫画
232#[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/// 新增漫画(multipart)
242#[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/// 按 ID 查询漫画
255#[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/// 更新漫画(multipart)
265#[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/// 查询收藏详情
278#[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/// 收藏列表(缓存命中时直出预序列化 JSON)
293#[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/// 收藏组员列表
304#[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/// 删除收藏
315#[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/// 新增收藏
333#[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/// 分页查询术语
343#[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/// 删除术语
354#[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/// 新增术语
364#[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/// 查询术语
377#[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/// 更新术语
387#[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/// 漫画 RSS
400#[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/// 话数 RSS
409#[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/// RSS 输出
418#[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/// 上传话数文件
428#[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/// 下载话数文件
462#[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/// 常驻组员列表
487#[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/// 删除常驻
497#[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/// 申请常驻
507#[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/// 管理员添加常驻
517#[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/// 更新常驻状态
527#[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            // #region agent log
584            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            // #endregion
591            metadata = serde_json::from_slice(&raw)
592                .map_err(|e| AppError::business(format!("metadata 解析失败: {e}")))?;
593        }
594    }
595    Ok(metadata)
596}