Skip to main content

tdm_server_rust/entity/
editor.rs

1//! 在线翻译编辑器实体 (Editor Entities)
2//!
3//! 定义编辑器页面、标记单元的视图对象(VO)与请求体(DTO)。
4//!
5//! ## 兼容形态
6//!
7//! 页面返回结构对齐 Poprako:`pageId / index / sourceFileName / imageUrl /
8//! imageWidth / imageHeight / uploaded`,编辑器只消费 `imageUrl`(图片 OSS 地址)。
9
10use crate::sea_entity::{editor_page, editor_unit};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13
14/// 编辑器页面信息(响应)
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct EditorPageInfo {
18    /// 页面 ID
19    pub page_id: i64,
20    /// 所属话数 ID
21    pub episode_id: i32,
22    /// 页序号(从 0 起)
23    pub index: i32,
24    /// 压缩包内原始文件名
25    pub source_file_name: Option<String>,
26    /// 图片访问 URL(图片 OSS / CDN 直链)
27    pub image_url: String,
28    /// 图片像素宽度
29    pub image_width: Option<i32>,
30    /// 图片像素高度
31    pub image_height: Option<i32>,
32    /// 图片是否已上传
33    pub uploaded: bool,
34    /// 该页翻译是否已确认完成
35    pub translation_completed: bool,
36}
37
38impl From<editor_page::Model> for EditorPageInfo {
39    /// 由数据库行模型转换为页面 VO
40    fn from(m: editor_page::Model) -> Self {
41        Self {
42            page_id: m.id,
43            episode_id: m.episode_id,
44            index: m.page_index,
45            source_file_name: m.source_file_name,
46            image_url: m.image_url,
47            image_width: m.image_width,
48            image_height: m.image_height,
49            uploaded: m.uploaded,
50            translation_completed: m.translation_completed,
51        }
52    }
53}
54
55/// 编辑器标记单元信息(响应)
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct EditorUnitInfo {
59    /// 单元 ID
60    pub id: i64,
61    /// 所属页面 ID
62    pub page_id: i64,
63    /// 页面内序号
64    pub index: i32,
65    /// 横坐标
66    pub x_coord: f32,
67    /// 纵坐标
68    pub y_coord: f32,
69    /// 是否在气泡内
70    pub in_bubble: bool,
71    /// 是否已校对
72    pub is_proofread: bool,
73    /// 翻译文本
74    pub translated_text: Option<String>,
75    /// 翻译者组员 ID
76    pub translator_id: Option<i32>,
77    /// 翻译备注
78    pub translator_comment: Option<String>,
79    /// 校对文本
80    pub proofreader_text: Option<String>,
81    /// 校对者组员 ID
82    pub proofreader_id: Option<i32>,
83    /// 校对备注
84    pub proofreader_comment: Option<String>,
85    /// 版本号
86    pub revision: i32,
87    /// 最后编辑者组员 ID
88    pub last_edited_by: Option<i32>,
89    /// 最后编辑时间(RFC3339)
90    pub last_edited_at: Option<String>,
91}
92
93impl From<editor_unit::Model> for EditorUnitInfo {
94    /// 由数据库行模型转换为单元 VO
95    fn from(m: editor_unit::Model) -> Self {
96        Self {
97            id: m.id,
98            page_id: m.page_id,
99            index: m.unit_index,
100            x_coord: m.x_coord,
101            y_coord: m.y_coord,
102            in_bubble: m.in_bubble,
103            is_proofread: m.is_proofread,
104            translated_text: m.translated_text,
105            translator_id: m.translator_id,
106            translator_comment: m.translator_comment,
107            proofreader_text: m.proofreader_text,
108            proofreader_id: m.proofreader_id,
109            proofreader_comment: m.proofreader_comment,
110            revision: m.revision,
111            last_edited_by: m.last_edited_by,
112            last_edited_at: m
113                .last_edited_at
114                .map(|t| DateTime::<Utc>::from_naive_utc_and_offset(t, Utc).to_rfc3339()),
115        }
116    }
117}
118
119/// 保存标记单元的单条输入(PUT /units 的元素)
120#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct EditorUnitInput {
123    /// 页面内序号
124    pub index: i32,
125    /// 横坐标
126    pub x_coord: f32,
127    /// 纵坐标
128    pub y_coord: f32,
129    /// 是否在气泡内
130    #[serde(default = "default_true")]
131    pub in_bubble: bool,
132    /// 是否已校对
133    #[serde(default)]
134    pub is_proofread: bool,
135    /// 翻译文本
136    #[serde(default)]
137    pub translated_text: Option<String>,
138    /// 翻译者组员 ID
139    #[serde(default)]
140    pub translator_id: Option<i32>,
141    /// 翻译备注
142    #[serde(default)]
143    pub translator_comment: Option<String>,
144    /// 校对文本
145    #[serde(default)]
146    pub proofreader_text: Option<String>,
147    /// 校对者组员 ID
148    #[serde(default)]
149    pub proofreader_id: Option<i32>,
150    /// 校对备注
151    #[serde(default)]
152    pub proofreader_comment: Option<String>,
153}
154
155/// in_bubble 默认 true
156fn default_true() -> bool {
157    true
158}
159
160/// 批量保存标记单元请求(PUT /units,整页替换)
161#[derive(Debug, Clone, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct SaveEditorUnitsRequest {
164    /// 目标页面 ID
165    pub page_id: i64,
166    /// 该页全部单元(替换式保存)
167    pub units: Vec<EditorUnitInput>,
168}
169
170/// 图源导入请求(POST /episodes/{id}/pages/import-provider)
171#[derive(Debug, Clone, Default, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct ImportProviderPagesRequest {
174    /// 是否强制重建:true 时删除旧页面/标点/图片记录后重新导入
175    #[serde(default)]
176    pub force: bool,
177}
178
179/// 前端图源解压直传:批量上传凭证请求(POST /episodes/{id}/pages/upload-credentials)
180#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct EditorUploadCredentialsRequest {
183    /// 待上传的页面文件(页序 + 原始文件名)
184    pub files: Vec<EditorUploadFile>,
185}
186
187/// 待上传文件描述
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct EditorUploadFile {
191    /// 页序号(从 0 起)
192    pub index: i32,
193    /// 原始文件名(用于推断后缀)
194    pub filename: String,
195}
196
197/// 单图上传凭证(响应元素)
198#[derive(Debug, Clone, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct EditorUploadCredential {
201    /// 页序号(与请求对应)
202    pub index: i32,
203    /// 图片 OSS 对象键(注册页面时回传)
204    pub object_key: String,
205    /// 预签名 PUT URL(前端直传图片字节)
206    pub presigned_url: String,
207    /// 图片 OSS 桶名
208    pub bucket: String,
209    /// 上传后图片访问 URL(CDN / 直链)
210    pub image_url: String,
211}
212
213/// 图源压缩包上传凭证
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct SourceArchiveUploadCredential {
217    /// 压缩包 OSS 对象键(与页面集 hash 对齐)
218    pub object_key: String,
219    /// 预签名 PUT URL(前端直传 zip 字节)
220    pub presigned_url: String,
221    /// 上传后压缩包访问 URL(CDN / 直链)
222    pub download_url: String,
223}
224
225/// 前端图源解压直传:批量上传凭证响应
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct EditorUploadCredentialsResponse {
229    /// 单图上传凭证列表
230    pub files: Vec<EditorUploadCredential>,
231    /// 同步上传的图源 zip 凭证
232    pub source_archive: SourceArchiveUploadCredential,
233    /// 申请凭证时已有的页面 ID,用于注册时检测并发重建。
234    #[serde(default)]
235    pub expected_page_ids: Vec<i64>,
236}
237
238/// 注册编辑器页面请求(POST /episodes/{id}/pages/register)
239///
240/// 前端把图源解压/识别后的单图逐张上传到图片 OSS,再调用本接口落库为页面。
241#[derive(Debug, Clone, Default, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct RegisterEditorPagesRequest {
244    /// 是否强制重建:true 时删除旧页面/标点后重新注册
245    #[serde(default)]
246    pub force: bool,
247    /// 申请上传凭证时看到的旧页面 ID,用于防止覆盖他人刚完成的重建。
248    #[serde(default)]
249    pub expected_page_ids: Vec<i64>,
250    /// 页面列表(已上传到图片 OSS)
251    pub pages: Vec<RegisterEditorPage>,
252}
253
254/// 注册编辑器页面的单条
255#[derive(Debug, Clone, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct RegisterEditorPage {
258    /// 页序号(从 0 起,服务端会按此排序后重排)
259    pub page_index: i32,
260    /// 图片 OSS 对象键(来自上传凭证)
261    pub object_key: String,
262    /// 原始文件名
263    #[serde(default)]
264    pub source_file_name: Option<String>,
265    /// 图片像素宽度
266    #[serde(default)]
267    pub image_width: Option<i32>,
268    /// 图片像素高度
269    #[serde(default)]
270    pub image_height: Option<i32>,
271}
272
273/// 整话工作流确认请求(POST /episodes/{id}/workflow/complete)
274#[derive(Debug, Clone, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct CompleteEpisodeWorkflowRequest {
277    /// 确认角色:`translator`(翻译交稿)或 `proofreader`(校对交稿)
278    pub role: String,
279}
280
281/// 编辑器工作区(GET /episodes/{id}/workspace)
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct EditorWorkspace {
285    /// 话数 ID
286    pub episode_id: i32,
287    /// 所属漫画 ID
288    pub manga_id: i32,
289    /// 漫画译名
290    pub manga_name: Option<String>,
291    /// 是否已有图源可导入(话数存在 provider_file_oss_id)
292    pub has_provider_source: bool,
293    /// 页面列表(图片 OSS 地址)
294    pub pages: Vec<EditorPageInfo>,
295}
296
297/// 图源导入结果(POST import-provider 响应)
298#[derive(Debug, Clone, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct ImportProviderResult {
301    /// 话数 ID
302    pub episode_id: i32,
303    /// 是否本次新建了页面(false 表示命中幂等,复用已有页面)
304    pub imported: bool,
305    /// 当前页面总数
306    pub page_count: usize,
307    /// 页面列表
308    pub pages: Vec<EditorPageInfo>,
309}