Skip to main content

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