1use crate::utils::query_deserialize::de_i32;
17use crate::{
18 app::AppState,
19 common::result::ResultBody,
20 common::AppJson,
21 entity::editor::{
22 CompleteEpisodeWorkflowRequest, EditorPageInfo, EditorUnitInfo,
23 EditorUploadCredentialsRequest, EditorUploadCredentialsResponse, EditorWorkspace,
24 ImportProviderResult, RegisterEditorPagesRequest, SaveEditorUnitsRequest,
25 },
26 entity::oss::OssCredential,
27 error::{ApiResult, AppError},
28 middleware::AuthMember,
29 service::{
30 editor_import_service::EditorImportService, editor_service::EditorService,
31 editor_source_archive_service::EditorSourceArchiveService,
32 },
33};
34use axum::{
35 body::Body,
36 extract::{Extension, Path, Query, State},
37 http::{header, HeaderValue, StatusCode},
38 response::{IntoResponse, Response},
39 routing::{get, post},
40 Router,
41};
42use serde::Deserialize;
43
44#[derive(Debug, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct PagesQuery {
48 #[serde(deserialize_with = "de_i32")]
50 pub episode_id: i32,
51}
52
53#[derive(Debug, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct UnitsQuery {
57 pub page_id: i64,
59}
60
61#[derive(Debug, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct ImportProviderQuery {
65 #[serde(default)]
67 pub force: bool,
68}
69
70pub fn routes() -> Router<AppState> {
72 Router::new()
73 .route(
74 "/episodes/:episodeId/pages/import-provider",
75 post(import_provider),
76 )
77 .route(
78 "/episodes/:episodeId/pages/upload-credentials",
79 post(prepare_upload_credentials),
80 )
81 .route("/episodes/:episodeId/pages/register", post(register_pages))
82 .route(
83 "/episodes/:episodeId/source-archive",
84 get(get_source_archive),
85 )
86 .route(
87 "/episodes/:episodeId/source-archive/file",
88 get(download_source_archive_file),
89 )
90 .route("/episodes/:episodeId/workspace", get(get_workspace))
91 .route("/pages", get(list_pages))
92 .route("/units", get(list_units).put(save_units))
93 .route(
94 "/pages/:pageId/translation-completion",
95 post(set_translation_completion).delete(clear_translation_completion),
96 )
97 .route(
98 "/episodes/:episodeId/workflow/complete",
99 post(complete_workflow),
100 )
101}
102
103#[tracing::instrument(skip_all, level = "info")]
105pub async fn import_provider(
106 State(state): State<AppState>,
107 Extension(AuthMember(member)): Extension<AuthMember>,
108 Path(episode_id): Path<i32>,
109 Query(q): Query<ImportProviderQuery>,
110) -> ApiResult<ResultBody<ImportProviderResult>> {
111 require_member(&member)?;
112 let result = EditorImportService::import_provider_pages(&state, episode_id, q.force).await?;
113 Ok(ResultBody::success_data(result))
114}
115
116#[tracing::instrument(skip_all, level = "info")]
118pub async fn prepare_upload_credentials(
119 State(state): State<AppState>,
120 Extension(AuthMember(member)): Extension<AuthMember>,
121 Path(episode_id): Path<i32>,
122 AppJson(body): AppJson<EditorUploadCredentialsRequest>,
123) -> ApiResult<ResultBody<EditorUploadCredentialsResponse>> {
124 require_member(&member)?;
125 let data =
126 EditorImportService::prepare_upload_credentials(&state, episode_id, body.files).await?;
127 Ok(ResultBody::success_data(data))
128}
129
130#[tracing::instrument(skip_all, level = "info")]
132pub async fn register_pages(
133 State(state): State<AppState>,
134 Extension(AuthMember(member)): Extension<AuthMember>,
135 Path(episode_id): Path<i32>,
136 AppJson(body): AppJson<RegisterEditorPagesRequest>,
137) -> ApiResult<ResultBody<ImportProviderResult>> {
138 require_member(&member)?;
139 let result = EditorImportService::register_pages(&state, episode_id, body).await?;
140 Ok(ResultBody::success_data(result))
141}
142
143#[tracing::instrument(skip_all, level = "info")]
145pub async fn get_source_archive(
146 State(state): State<AppState>,
147 Path(episode_id): Path<i32>,
148) -> ApiResult<ResultBody<OssCredential>> {
149 let data = EditorSourceArchiveService::get_or_build_archive(&state, episode_id).await?;
150 Ok(ResultBody::success_data(data))
151}
152
153#[tracing::instrument(skip_all, level = "info")]
155pub async fn download_source_archive_file(
156 State(state): State<AppState>,
157 Path(episode_id): Path<i32>,
158) -> ApiResult<Response> {
159 let data = EditorSourceArchiveService::get_or_build_archive(&state, episode_id).await?;
160 let mut headers = axum::http::HeaderMap::new();
161 headers.insert(
162 header::LOCATION,
163 HeaderValue::from_str(&data.presigned_url)
164 .map_err(|e| AppError::Internal(format!("生成 COS 跳转地址失败: {e}")))?,
165 );
166 Ok((StatusCode::TEMPORARY_REDIRECT, headers, Body::empty()).into_response())
167}
168
169#[tracing::instrument(skip_all, level = "info")]
171pub async fn get_workspace(
172 State(state): State<AppState>,
173 Path(episode_id): Path<i32>,
174) -> ApiResult<ResultBody<EditorWorkspace>> {
175 let data = EditorService::get_workspace(&state, episode_id).await?;
176 Ok(ResultBody::success_data(data))
177}
178
179#[tracing::instrument(skip_all, level = "info")]
181pub async fn list_pages(
182 State(state): State<AppState>,
183 Query(q): Query<PagesQuery>,
184) -> ApiResult<ResultBody<Vec<EditorPageInfo>>> {
185 let data = EditorService::list_pages(&state, q.episode_id).await?;
186 Ok(ResultBody::success_data(data))
187}
188
189#[tracing::instrument(skip_all, level = "info")]
191pub async fn list_units(
192 State(state): State<AppState>,
193 Query(q): Query<UnitsQuery>,
194) -> ApiResult<ResultBody<Vec<EditorUnitInfo>>> {
195 let data = EditorService::list_units(&state, q.page_id).await?;
196 Ok(ResultBody::success_data(data))
197}
198
199#[tracing::instrument(skip_all, level = "info")]
201pub async fn save_units(
202 State(state): State<AppState>,
203 Extension(AuthMember(member)): Extension<AuthMember>,
204 AppJson(body): AppJson<SaveEditorUnitsRequest>,
205) -> ApiResult<ResultBody<usize>> {
206 let member_id = require_member(&member)?;
207 let saved = EditorService::save_units(&state, body, member_id).await?;
208 Ok(ResultBody::success_data(saved))
209}
210
211#[tracing::instrument(skip_all, level = "info")]
213pub async fn set_translation_completion(
214 State(state): State<AppState>,
215 Extension(AuthMember(member)): Extension<AuthMember>,
216 Path(page_id): Path<i64>,
217) -> ApiResult<ResultBody<()>> {
218 require_member(&member)?;
219 EditorService::set_page_translation_completion(&state, page_id, true).await?;
220 Ok(ResultBody::success())
221}
222
223#[tracing::instrument(skip_all, level = "info")]
225pub async fn clear_translation_completion(
226 State(state): State<AppState>,
227 Extension(AuthMember(member)): Extension<AuthMember>,
228 Path(page_id): Path<i64>,
229) -> ApiResult<ResultBody<()>> {
230 require_member(&member)?;
231 EditorService::set_page_translation_completion(&state, page_id, false).await?;
232 Ok(ResultBody::success())
233}
234
235#[tracing::instrument(skip_all, level = "info")]
237pub async fn complete_workflow(
238 State(state): State<AppState>,
239 Extension(AuthMember(member)): Extension<AuthMember>,
240 Path(episode_id): Path<i32>,
241 AppJson(body): AppJson<CompleteEpisodeWorkflowRequest>,
242) -> ApiResult<ResultBody<()>> {
243 let member_id = require_member(&member)?;
244 EditorService::complete_episode_workflow(&state, episode_id, &body.role, member_id).await?;
245 Ok(ResultBody::success())
246}
247
248fn require_member(member: &Option<crate::entity::member::Member>) -> ApiResult<i32> {
250 member
251 .as_ref()
252 .map(|m| m.id)
253 .ok_or_else(|| AppError::login_expired("请登录后再操作编辑器喵……"))
254}