tdm_server_rust/entity/rss.rs
1//! RSS 生成用数据结构 (RSS Feed Entities)
2//!
3//! 定义 RSS 2.0 订阅源生成所需的全部数据结构。
4//!
5//! ## RSS 体系概览
6//!
7//! 提灯喵网站通过 RSS 2.0 对外提供以下订阅源:
8//!
9//! | RSS 文件 | 内容 | 来源查询 |
10//! |----------|------|----------|
11//! | `rss.xml` | 最新开坑漫画 | `get_manga_rss` |
12//! | `rssUpdated.xml` | 最近更新漫画 | `get_manga_updated_rss` |
13//! | `rssEpisode.xml` | 最新话数发布 | `get_episode_rss` |
14//! | `rssEpisode_{post}.xml` | 各岗位工作提醒 | `get_work_reminder_rss` |
15//!
16//! ## 数据流
17//!
18//! ```text
19//! 数据库查询 → RssXxxRow → RssService 格式化 → RssItem → RSS XML 文件
20//! ```
21//!
22//! `RssXxxRow` 是数据库查询的中间行结构,
23//! [`RssItem`] 是统一的 RSS 条目格式,
24//! 最终由 `RssService` 序列化为 XML 写入 `folder.base` 目录。
25
26use chrono::{DateTime, Utc};
27use serde::Serialize;
28
29/// RSS 2.0 统一条目
30///
31/// 所有 RSS 订阅源的条目均转换为此格式后再序列化为 XML。
32/// 包含 RSS 2.0 规范要求的全部可选字段。
33///
34/// ## 字段说明
35///
36/// | 字段 | RSS 元素 | 说明 |
37/// |------|----------|------|
38/// | `title` | `<title>` | 条目标题(漫画译名 或 话数标签) |
39/// | `link` | `<link>` | 条目链接(站内 URL) |
40/// | `author` | `<author>` | 作者信息(翻译组员名 或 原作作者名) |
41/// | `pub_date` | `<pubDate>` | RFC 2822 格式发布时间 |
42/// | `category` | `<category>` | 分类描述(长短篇 + 连载状态) |
43/// | `description` | `<description>` | HTML 正文(CDATA 包裹) |
44/// | `enclosure_url` | `<enclosure url="...">` | 封面图片 URL |
45/// | `enclosure_type` | `<enclosure type="...">` | 封面 MIME 类型(通常 `image/jpeg`) |
46/// | `guid` | `<guid>` | 唯一标识(漫画 ID 或话数 ID) |
47#[derive(Debug, Clone)]
48pub struct RssItem {
49 /// RSS 条目标题
50 pub title: String,
51 /// RSS 条目链接(完整 URL)
52 pub link: String,
53 /// 作者信息(组员名或原作作者名)
54 pub author: String,
55 /// 发布时间(中文格式,如"2024年1月15日20点30分")
56 pub pub_date: String,
57 /// 分类描述(长短篇 + 连载状态)
58 pub category: String,
59 /// HTML 正文描述(CDATA 包裹)
60 pub description: String,
61 /// 封面图片 URL
62 pub enclosure_url: String,
63 /// 封面图片 MIME 类型
64 pub enclosure_type: String,
65 /// 全局唯一标识符
66 pub guid: String,
67}
68
69/// 漫画 RSS 查询行
70///
71/// 来自 `get_manga_rss` 和 `get_manga_updated_rss` 查询的中间结果。
72/// 包含漫画基本信息 + 作者信息,用于生成 `rss.xml` 和 `rssUpdated.xml`。
73///
74/// ## 查询来源
75///
76/// - [`MangaRepository::get_manga_rss`](crate::repository::manga_repo::MangaRepository) — 最新开坑漫画(按 `setupTime` 降序取 2 条)
77/// - [`MangaRepository::get_manga_updated_rss`](crate::repository::manga_repo::MangaRepository) — 最近更新漫画(按 `updateTime` 降序取 3 条)
78///
79/// ## 对应 Java 查询
80///
81/// Java `MangaMapper.getMangaRss`。
82#[derive(Debug, Clone, Serialize, Default)]
83#[serde(rename_all = "camelCase")]
84pub struct RssMangaRow {
85 /// 漫画 ID,序列化为 `"Id"`
86 #[serde(rename = "Id")]
87 pub id: i32,
88 /// 漫画译名
89 pub manga_tran_name: Option<String>,
90 /// 漫画原名
91 pub manga_ori_name: Option<String>,
92 /// 漫画分类
93 pub category: Option<i16>,
94 /// 连载状态
95 pub manga_status: Option<i16>,
96 /// 封面图片路径
97 pub image: Option<String>,
98 /// 开坑时间
99 pub setup_time: Option<DateTime<Utc>>,
100 /// 最近更新时间
101 pub update_time: Option<DateTime<Utc>>,
102 /// 原作作者 ID
103 pub author_id: Option<i32>,
104 /// 原作作者名
105 pub author_name: Option<String>,
106 /// 作画作者 ID
107 pub author_id2: Option<i32>,
108 /// 作画作者名
109 pub author_name2: Option<String>,
110}
111
112/// 话数 RSS 查询行
113///
114/// 来自 `get_episode_rss` 查询的中间结果。
115/// 包含话数信息 + 关联漫画信息 + 图源/翻译组员信息。
116/// 用于生成 `rssEpisode.xml`。
117///
118/// ## 查询来源
119///
120/// [`MangaRepository::get_episode_rss`](crate::repository::manga_repo::MangaRepository) — 最新话数发布(按 ID 降序取 5 条)。
121///
122/// ## 对应 Java 查询
123///
124/// Java `MangaMapper.getEpisodeRss`。
125#[derive(Debug, Clone, Serialize, Default)]
126#[serde(rename_all = "camelCase")]
127pub struct EpisodeRssRow {
128 /// 话数 ID,序列化为 `"Id"`
129 #[serde(rename = "Id")]
130 pub id: i32,
131 /// 漫画 ID
132 pub manga_id: i32,
133 /// 话数标签(如"第1话")
134 pub manga_episode: Option<String>,
135 /// 话数标题
136 pub manga_episode_name: Option<String>,
137 /// 话数设立时间
138 pub setup_time: Option<DateTime<Utc>>,
139 /// 漫画译名
140 pub manga_name: Option<String>,
141 /// 漫画原名
142 pub manga_ori_name: Option<String>,
143 /// 漫画分类
144 pub category: Option<i16>,
145 /// 漫画连载状态
146 pub manga_status: Option<i16>,
147 /// 漫画开坑时间
148 pub manga_setup_time: Option<DateTime<Utc>>,
149 /// 封面图片路径
150 pub image: Option<String>,
151 /// 图源组员名
152 pub provider_name: Option<String>,
153 /// 翻译组员名
154 pub translator_name: Option<String>,
155 /// 翻译职阶
156 pub intern: Option<i16>,
157 /// 翻译邮箱
158 pub email: Option<String>,
159 /// 发布链接
160 pub publish_link: Option<String>,
161}
162
163/// 交稿提醒 RSS 查询行
164///
165/// 来自 `get_work_reminder_rss` 查询的中间结果。
166/// 记录某岗位组员交稿后,下一工序组员的提醒信息。
167///
168/// ## 字段含义
169///
170/// `my_name` 为当前交稿岗位(如 "translator" 完成翻译,提醒校对接稿)。
171/// `username_now`/`intern_now`/`email_now` 为交稿组员信息。
172/// `username`/`intern`/`email` 为下一工序组员信息。
173///
174/// ## 查询来源
175///
176/// [`MangaRepository::get_work_reminder_rss`](crate::repository::manga_repo::MangaRepository) — 按岗位名查询最新 5 条交稿提醒。
177///
178/// ## 对应 Java 查询
179///
180/// Java `MangaMapper.getWorkReminderRss`。
181#[derive(Debug, Clone, Default)]
182pub struct WorkReminderRssRow {
183 /// 话数 ID
184 pub episode_id: i32,
185 /// 漫画 ID
186 pub manga_id: i32,
187 /// 话数标签
188 pub manga_episode: Option<String>,
189 /// 话数标题
190 pub manga_episode_name: Option<String>,
191 /// 话数设立时间
192 pub setup_time: Option<DateTime<Utc>>,
193 /// 漫画译名
194 pub manga_name: Option<String>,
195 /// 漫画原名
196 pub manga_ori_name: Option<String>,
197 /// 漫画分类
198 pub category: Option<i16>,
199 /// 漫画连载状态
200 pub manga_status: Option<i16>,
201 /// 漫画开坑时间
202 pub manga_setup_time: Option<DateTime<Utc>>,
203 /// 封面图片路径
204 pub image: Option<String>,
205 /// 当前交稿岗位英文名(translator / proofreader / letterer / timer)
206 pub my_name: String,
207 /// 交稿组员名
208 pub username_now: Option<String>,
209 /// 交稿组员职阶
210 pub intern_now: Option<i16>,
211 /// 交稿组员邮箱
212 pub email_now: Option<String>,
213 /// 下一工序组员名
214 pub username: Option<String>,
215 /// 下一工序组员职阶
216 pub intern: Option<i16>,
217 /// 下一工序组员邮箱
218 pub email: Option<String>,
219}
220
221/// 三月未交稿组员 RSS 查询行
222///
223/// 查询超过三个月未交稿的组员信息,用于生成提醒 RSS。
224///
225/// ## 查询来源
226///
227/// [`MemberRepository::get_member_reminder_rss`](crate::repository::member_repo::MemberRepository) — 筛选 `lastSubmitTime <= NOW() - 3 MONTH`。
228#[derive(Debug, Clone, Default)]
229pub struct MemberReminderRssRow {
230 /// 组员 ID
231 pub id: i32,
232 /// 组员用户名
233 pub username: Option<String>,
234 /// 组员职阶
235 pub intern: i16,
236 /// 组员邮箱
237 pub email: Option<String>,
238 /// 最近一次交稿时间
239 pub last_submit_time: Option<DateTime<Utc>>,
240}
241
242/// 岗位 RSS 输出配置
243///
244/// 定义每个岗位对应的 RSS XML 文件名和查询参数。
245/// 用于 `RSS_POST_CONFIGS` 常量数组,驱动各岗位 RSS 文件的批量生成。
246///
247/// ## 字段说明
248///
249/// | 字段 | 说明 |
250/// |------|------|
251/// | `post_name` | 岗位英文名,对应 `workreminder.myName` 列值 |
252/// | `file_name` | 输出的 RSS XML 文件名 |
253pub struct RssPostConfig {
254 /// 岗位英文名(translator / proofreader / letterer / timer / reviewer)
255 pub post_name: &'static str,
256 /// 输出的 RSS XML 文件名
257 pub file_name: &'static str,
258}
259
260/// 全部岗位 RSS 输出配置
261///
262/// 包含翻译、校对、嵌字、审稿、时轴五个岗位的 RSS 文件配置。
263/// 用于 `RssService` 中批量生成各岗位的工作提醒 RSS XML。
264///
265/// ## 使用方式
266///
267/// ```rust,ignore
268/// for config in RSS_POST_CONFIGS {
269/// let rows = repo.get_work_reminder_rss(config.post_name).await?;
270/// write_rss_file(config.file_name, rows).await?;
271/// }
272/// ```
273pub const RSS_POST_CONFIGS: &[RssPostConfig] = &[
274 RssPostConfig {
275 post_name: "translator",
276 file_name: "rssEpisode_translator.xml",
277 },
278 RssPostConfig {
279 post_name: "proofreader",
280 file_name: "rssEpisode_proofreader.xml",
281 },
282 RssPostConfig {
283 post_name: "letterer",
284 file_name: "rssEpisode_letterer.xml",
285 },
286 RssPostConfig {
287 post_name: "reviewer",
288 file_name: "rssEpisode_reviewer.xml",
289 },
290 RssPostConfig {
291 post_name: "timer",
292 file_name: "rssEpisode_timer.xml",
293 },
294];