Skip to main content

fishpi_sdk/model/
article.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use serde_json::Value;
3
4use crate::impl_str_enum;
5use crate::model::user::Metal;
6use crate::model::{bool_from_int, bool_from_zero, deserialize_sys_metal};
7use crate::utils::error::Error;
8
9fn normalize_float_numbers(value: &mut Value) {
10    match value {
11        Value::Array(arr) => {
12            for item in arr {
13                normalize_float_numbers(item);
14            }
15        }
16        Value::Object(map) => {
17            for v in map.values_mut() {
18                normalize_float_numbers(v);
19            }
20        }
21        Value::Number(num) => {
22            if num.as_u64().is_none()
23                && num.as_i64().is_none()
24                && let Some(f) = num.as_f64()
25                && f.is_finite()
26            {
27                let n = if f <= 0.0 { 0 } else { f.trunc() as u64 };
28                *value = Value::Number(serde_json::Number::from(n));
29            }
30        }
31        _ => {}
32    }
33}
34
35fn parse_with_float_fallback<T>(data: &Value, type_name: &str) -> Result<T, Error>
36where
37    T: for<'de> Deserialize<'de>,
38{
39    match serde_json::from_value::<T>(data.clone()) {
40        Ok(v) => Ok(v),
41        Err(first_err) => {
42            let mut normalized = data.clone();
43            normalize_float_numbers(&mut normalized);
44            serde_json::from_value::<T>(normalized).map_err(|second_err| {
45                Error::Parse(format!(
46                    "Failed to parse {}: {} (fallback after float-normalize also failed: {})",
47                    type_name, first_err, second_err
48                ))
49            })
50        }
51    }
52}
53
54/// 发帖信息
55#[derive(Clone, Debug, Serialize, Deserialize)]
56#[allow(non_snake_case)]
57pub struct ArticlePost {
58    /// 帖子标题
59    #[serde(rename = "articleTitle")]
60    pub title: String,
61    /// 帖子内容
62    #[serde(rename = "articleContent")]
63    pub content: String,
64    /// 帖子标签
65    #[serde(rename = "articleTags")]
66    pub tags: String,
67    /// 是否允许评论
68    #[serde(rename = "articleCommentable")]
69    pub commentable: bool,
70    /// 是否通知帖子关注者
71    #[serde(rename = "articleNotifyFollowers")]
72    pub notifyFollowers: bool,
73    /// 帖子类型,ArticleType
74    #[serde(rename = "articleType")]
75    pub type_: ArticleType,
76    /// 是否在列表展示
77    #[serde(rename = "articleShowInList")]
78    pub showInList: u32,
79    /// 打赏内容
80    #[serde(rename = "articleRewardContent")]
81    pub rewardContent: Option<String>,
82    /// 打赏积分
83    #[serde(rename = "articleRewardPoint")]
84    pub rewardPoint: Option<String>,
85    /// 是否匿名
86    #[serde(rename = "articleAnonymous")]
87    pub anonymous: Option<bool>,
88    /// 提问悬赏积分
89    #[serde(rename = "articleQnAOfferPoint")]
90    pub offerPoint: Option<u32>,
91}
92
93impl ArticlePost {
94    pub fn from_value(data: &Value) -> Result<Self, Error> {
95        serde_json::from_value(data.clone())
96            .map_err(|e| Error::Parse(format!("Failed to parse ArticlePost: {}", e)))
97    }
98
99    pub fn to_json(&self) -> Result<Value, Error> {
100        serde_json::to_value(self)
101            .map_err(|e| Error::Parse(format!("Failed to serialize ArticlePost: {}", e)))
102    }
103}
104
105/// 文章标签
106#[derive(Clone, Debug, Deserialize)]
107#[allow(non_snake_case)]
108pub struct ArticleTag {
109    /// 标签 id
110    pub oId: String,
111    /// 标签名
112    #[serde(rename = "tagTitle")]
113    pub title: String,
114    /// 标签描述
115    #[serde(rename = "tagDescription")]
116    pub description: String,
117    /// icon 地址
118    #[serde(rename = "tagIconPath")]
119    pub iconPath: String,
120    /// 标签地址
121    #[serde(rename = "tagURI")]
122    pub uri: String,
123    /// 标签自定义 CSS
124    #[serde(rename = "tagCSS")]
125    pub diyCSS: String,
126    /// 反对数
127    #[serde(rename = "tagBadCnt")]
128    pub badCnt: u64,
129    /// 标签回帖计数
130    #[serde(rename = "tagCommentCount")]
131    pub commentCnt: u32,
132    /// 关注数
133    #[serde(rename = "tagFollowerCount")]
134    pub followerCnt: u32,
135    /// 点赞数
136    #[serde(rename = "tagGoodCnt")]
137    pub goodCnt: u64,
138    /// 引用计数
139    #[serde(rename = "tagReferenceCount")]
140    pub referenceCnt: u32,
141    /// 标签相关链接计数
142    #[serde(rename = "tagLinkCount")]
143    pub linkCnt: u32,
144    /// 标签 SEO 描述
145    #[serde(rename = "tagSeoDesc")]
146    pub seoDesc: String,
147    /// 标签关键字
148    #[serde(rename = "tagSeoKeywords")]
149    pub seoKeywords: String,
150    /// 标签 SEO 标题
151    #[serde(rename = "tagSeoTitle")]
152    pub seoTitle: String,
153    /// 标签广告内容
154    #[serde(rename = "tagAd")]
155    pub tagAd: String,
156    /// 是否展示广告
157    #[serde(rename = "tagShowSideAd")]
158    pub showSideAd: u32,
159    /// 标签状态
160    #[serde(rename = "tagStatus")]
161    pub status: u32,
162    /// 标签随机数
163    #[serde(rename = "tagRandomDouble")]
164    pub randomDouble: f64,
165}
166
167impl ArticleTag {
168    pub fn from_value(data: &Value) -> Result<Self, Error> {
169        serde_json::from_value(data.clone())
170            .map_err(|e| Error::Parse(format!("Failed to parse ArticleTag: {}", e)))
171    }
172}
173
174/// 投票状态,点赞与否
175#[derive(Clone, Debug)]
176#[derive(Default)]
177pub enum VoteStatus {
178    /// 未投票
179    #[default]
180    Normal,
181    /// 点赞
182    Up,
183    /// 点踩
184    Down,
185}
186
187impl VoteStatus {
188    pub fn from_index(index: usize) -> Self {
189        match index {
190            1 => VoteStatus::Up,
191            2 => VoteStatus::Down,
192            _ => VoteStatus::Normal,
193        }
194    }
195}
196
197
198/// 文章状态
199#[derive(Clone, Debug)]
200pub enum ArticleStatus {
201    /// 正常
202    Normal,
203
204    /// 封禁
205    Ban,
206
207    /// 锁定
208    Lock,
209}
210
211impl ArticleStatus {
212    pub fn from_index(index: usize) -> Self {
213        match index {
214            0 => ArticleStatus::Normal,
215            1 => ArticleStatus::Ban,
216            _ => ArticleStatus::Lock, // 默认值
217        }
218    }
219}
220
221impl Default for ArticleStatus {
222    fn default() -> Self {
223        Self::Normal
224    }
225}
226
227pub fn deserialize_score<'de, D>(deserializer: D) -> Result<String, D::Error>
228where
229    D: Deserializer<'de>,
230{
231    let value: u64 = Deserialize::deserialize(deserializer)?;
232    Ok(value.to_string())
233}
234
235pub fn deserialize_vote<'de, D>(deserializer: D) -> Result<VoteStatus, D::Error>
236where
237    D: Deserializer<'de>,
238{
239    let value: i64 = Deserialize::deserialize(deserializer)?;
240    Ok(VoteStatus::from_index((value + 1) as usize))
241}
242
243pub fn deserialize_status<'de, D>(deserializer: D) -> Result<ArticleStatus, D::Error>
244where
245    D: Deserializer<'de>,
246{
247    let value: u64 = Deserialize::deserialize(deserializer)?;
248    Ok(ArticleStatus::from_index(value as usize))
249}
250
251#[derive(Clone, Debug, Default, Deserialize)]
252#[serde(default)]
253#[allow(non_snake_case)]
254pub struct ArticleAuthor {
255    /// 用户是否在线
256    pub isOnline: bool,
257    /// 用户在线时长
258    pub onlineMinute: u64,
259    /// 是否公开积分列表
260    #[serde(deserialize_with = "bool_from_zero")]
261    pub pointStatus: bool,
262    /// 是否公开关注者列表
263    #[serde(deserialize_with = "bool_from_zero")]
264    pub followerStatus: bool,
265    /// 用户完成新手指引步数
266    pub guideStep: u64,
267    /// 是否公开在线状态
268    #[serde(deserialize_with = "bool_from_zero")]
269    pub onlineStatus: bool,
270    /// 当前连续签到起始日
271    pub currentCheckinStreakStart: u64,
272    /// 是否聊天室图片自动模糊
273    #[serde(deserialize_with = "bool_from_int")] // == 1
274    pub isAutoBlur: bool,
275    /// 用户标签
276    pub tags: String,
277    /// 是否公开回帖列表
278    #[serde(deserialize_with = "bool_from_zero")]
279    pub commentStatus: bool,
280    /// 用户时区
281    pub timezone: String,
282    /// 用户个人主页
283    pub homePage: String,
284    /// 是否启用站外链接跳转页面
285    #[serde(deserialize_with = "bool_from_int")] // == 1
286    pub isEnableForwardPage: bool,
287    /// 是否公开 UA 信息
288    #[serde(deserialize_with = "bool_from_zero")]
289    pub userUAStatus: bool,
290    /// 自定义首页跳转地址
291    pub userIndexRedirectURL: String,
292    /// 最近发帖时间
293    pub latestArticleTime: u64,
294    /// 标签计数
295    pub tagCount: u64,
296    /// 昵称
297    pub nickname: String,
298    /// 回帖浏览模式
299    pub listViewMode: u64,
300    /// 最长连续签到
301    pub longestCheckinStreak: u64,
302    /// 用户头像类型
303    pub avatarType: String,
304    /// 用户确认邮件发送时间
305    pub subMailSendTime: u64,
306    /// 用户最后更新时间
307    pub updateTime: u64,
308    /// userSubMailStatus
309    #[serde(deserialize_with = "bool_from_zero")]
310    pub subMailStatus: bool,
311    /// 是否加入积分排行
312    #[serde(deserialize_with = "bool_from_zero")]
313    pub isJoinPointRank: bool,
314    /// 用户最后登录时间
315    pub latestLoginTime: u64,
316    /// 应用角色
317    pub userAppRole: u64,
318    /// 头像查看模式
319    pub userAvatarViewMode: u64,
320    /// 用户状态
321    pub userStatus: u64,
322    /// 用户上次最长连续签到日期
323    pub longestCheckinStreakEnd: u64,
324    /// 是否公开关注帖子列表
325    #[serde(deserialize_with = "bool_from_zero")]
326    pub watchingArticleStatus: bool,
327    /// 上次回帖时间
328    pub latestCmtTime: u64,
329    /// 用户省份
330    pub province: String,
331    /// 用户当前连续签到计数
332    pub currentCheckinStreak: u64,
333    /// 用户编号
334    pub userNo: u64,
335    /// 用户头像
336    pub avatarURL: String,
337    /// 是否公开关注标签列表
338    #[serde(deserialize_with = "bool_from_zero")]
339    pub followingTagStatus: bool,
340    /// 用户语言
341    pub userLanguage: String,
342    /// 是否加入消费排行
343    #[serde(deserialize_with = "bool_from_zero")]
344    pub isJoinUsedPointRank: bool,
345    /// 上次签到日期
346    pub currentCheckinStreakEnd: u64,
347    /// 是否公开收藏帖子列表
348    #[serde(deserialize_with = "bool_from_zero")]
349    pub followingArticleStatus: bool,
350    /// 是否启用键盘快捷键
351    #[serde(deserialize_with = "bool_from_zero")]
352    pub keyboardShortcutsStatus: bool,
353    /// 是否回帖后自动关注帖子
354    #[serde(deserialize_with = "bool_from_zero")]
355    pub replyWatchArticleStatus: bool,
356    /// 回帖浏览模式
357    pub commentViewMode: u64,
358    /// 是否公开清风明月列表
359    #[serde(deserialize_with = "bool_from_zero")]
360    pub breezemoonStatus: bool,
361    /// 用户上次签到时间
362    pub userCheckinTime: u64,
363    /// 用户消费积分
364    pub usedPoint: u64,
365    /// 是否公开发帖列表
366    #[serde(deserialize_with = "bool_from_zero")]
367    pub articleStatus: bool,
368    /// 用户积分
369    pub userPoint: u64,
370    /// 用户回帖数
371    pub commentCount: u64,
372    /// 用户个性签名
373    pub userIntro: String,
374    /// 移动端主题
375    pub userMobileSkin: String,
376    /// 分页每页条目
377    pub listPageSize: u64,
378    /// 文章 Id
379    pub oId: String,
380    /// 用户名
381    pub userName: String,
382    /// 是否公开 IP 地理信息
383    #[serde(deserialize_with = "bool_from_zero")]
384    pub geoStatus: bool,
385    /// 最长连续签到起始日
386    pub longestCheckinStreakStart: u64,
387    /// 用户主题
388    pub userSkin: String,
389    /// 是否启用 Web 通知
390    #[serde(deserialize_with = "bool_from_zero")]
391    pub notifyStatus: bool,
392    /// 公开关注用户列表
393    #[serde(deserialize_with = "bool_from_zero")]
394    pub followingUserStatus: bool,
395    /// 文章数
396    pub articleCount: u64,
397    /// 用户角色
398    pub userRole: String,
399    /// 徽章
400    #[serde(deserialize_with = "deserialize_sys_metal")]
401    pub sysMetal: Vec<Metal>,
402}
403
404impl ArticleAuthor {
405    pub fn from_value(data: &Value) -> Result<Self, Error> {
406        parse_with_float_fallback(data, "ArticleAuthor")
407    }
408}
409
410/// 评论作者
411pub type CommentAuthor = ArticleAuthor;
412
413#[derive(Clone, Debug, Default, Deserialize)]
414#[serde(default)]
415#[allow(non_snake_case)]
416pub struct ArticleComment {
417    /// 是否优评
418    #[serde(rename = "commentNice")]
419    pub isNice: bool,
420    /// 评论创建时间字符串
421    #[serde(rename = "commentCreateTimeStr")]
422    pub createTimeStr: String,
423    /// 评论作者 id
424    #[serde(rename = "commentAuthorId")]
425    pub authorId: String,
426    /// 评论分数
427    #[serde(deserialize_with = "deserialize_score")]
428    pub score: String,
429    /// 评论创建时间
430    #[serde(rename = "commentCreateTime")]
431    pub createTime: String,
432    /// 评论作者头像
433    #[serde(rename = "commentAuthorURL")]
434    pub authorURL: String,
435    /// 评论状态
436    #[serde(deserialize_with = "deserialize_vote")]
437    pub vote: VoteStatus,
438    /// 评论引用数
439    #[serde(rename = "commentRevisionCount")]
440    pub revisionCount: u64,
441    /// 评论经过时间
442    #[serde(rename = "timeAgo")]
443    pub timeAgo: String,
444    /// 回复评论 id
445    #[serde(rename = "commentOriginalCommentId")]
446    pub replyId: String,
447    /// 徽章
448    #[serde(deserialize_with = "deserialize_sys_metal")]
449    pub sysMetal: Vec<Metal>,
450    /// 点赞数
451    #[serde(rename = "commentGoodCnt")]
452    pub goodCnt: u64,
453    /// 评论是否可见
454    #[serde(deserialize_with = "bool_from_zero")]
455    pub visible: bool,
456    /// 文章 id
457    #[serde(rename = "commentOnArticleId")]
458    pub articleId: String,
459    /// 评论感谢数
460    #[serde(rename = "rewardedCnt")]
461    pub rewardedCnt: u64,
462    /// 评论地址
463    #[serde(rename = "commentSharpURL")]
464    pub sharpURL: String,
465    /// 是否匿名
466    #[serde(deserialize_with = "bool_from_int")]
467    pub isAnonymous: bool,
468    /// 评论回复数
469    #[serde(rename = "commentReplyCnt")]
470    pub replyCnt: u64,
471    /// 评论 id
472    #[serde(rename = "oId")]
473    pub oId: String,
474    /// 评论内容
475    #[serde(rename = "commentContent")]
476    pub content: String,
477    /// 评论状态
478    #[serde(deserialize_with = "deserialize_status")]
479    pub status: ArticleStatus,
480    /// 评论作者
481    pub commenter: CommentAuthor,
482    /// 评论作者用户名
483    #[serde(rename = "commentAuthorName")]
484    pub author: String,
485    /// 评论感谢数
486    #[serde(rename = "commentThankCnt")]
487    pub thankCnt: u64,
488    /// 评论点踩数
489    #[serde(rename = "commentBadCnt")]
490    pub badCnt: u64,
491    /// 是否已感谢
492    #[serde(rename = "rewarded")]
493    pub rewarded: bool,
494    /// 评论作者头像
495    #[serde(rename = "commentAuthorThumbnailURL")]
496    pub thumbnailURL: String,
497    /// 评论音频地址
498    #[serde(rename = "commentAudioURL")]
499    pub audioURL: String,
500    /// 评论是否采纳,1 表示采纳
501    #[serde(rename = "commentQnAOffered")]
502    pub offered: u64,
503}
504
505impl ArticleComment {
506    pub fn from_value(data: &Value) -> Result<Self, Error> {
507        parse_with_float_fallback(data, "ArticleComment")
508    }
509}
510
511/// 分页信息
512#[derive(Clone, Debug, Default, Deserialize)]
513#[allow(non_snake_case)]
514pub struct Pagination {
515    /// 总分页数
516    #[serde(rename = "paginationPageCount")]
517    pub count: u32,
518    /// 建议分页页码
519    #[serde(rename = "paginationPageNums")]
520    pub pageNums: Vec<u32>,
521}
522
523impl Pagination {
524    pub fn from_value(data: &Value) -> Result<Self, Error> {
525        parse_with_float_fallback(data, "Pagination")
526    }
527}
528
529/// 帖子类型
530#[derive(Clone, Debug, Serialize, Deserialize)]
531#[repr(u8)]
532#[derive(Default)]
533pub enum ArticleType {
534    Normal = 0,
535    Private = 1,
536    Broadcast = 2,
537    Thought = 3,
538    #[default]
539    Unknown = 4,
540    Question = 5,
541}
542
543impl ArticleType {
544    pub fn from_index(index: usize) -> Self {
545        match index {
546            0 => ArticleType::Normal,
547            1 => ArticleType::Private,
548            2 => ArticleType::Broadcast,
549            3 => ArticleType::Thought,
550            5 => ArticleType::Question,
551            _ => ArticleType::Unknown,
552        }
553    }
554}
555
556
557fn default_article_type() -> ArticleType {
558    ArticleType::Unknown
559}
560
561pub fn deserialize_type<'de, D>(deserializer: D) -> Result<ArticleType, D::Error>
562where
563    D: Deserializer<'de>,
564{
565    let value: u64 = Deserialize::deserialize(deserializer)?;
566    Ok(ArticleType::from_index(value as usize))
567}
568
569pub fn deserialize_reddit_score<'de, D>(deserializer: D) -> Result<String, D::Error>
570where
571    D: Deserializer<'de>,
572{
573    let value: u64 = Deserialize::deserialize(deserializer)?;
574    Ok(value.to_string())
575}
576
577pub fn deserialize_tag_objs<'de, D>(deserializer: D) -> Result<Vec<ArticleTag>, D::Error>
578where
579    D: Deserializer<'de>,
580{
581    let arr: Vec<Value> = Deserialize::deserialize(deserializer)?;
582    arr.into_iter()
583        .map(|v| ArticleTag::from_value(&v))
584        .collect::<Result<Vec<_>, _>>()
585        .map_err(serde::de::Error::custom)
586}
587
588pub fn deserialize_author<'de, D>(deserializer: D) -> Result<ArticleAuthor, D::Error>
589where
590    D: Deserializer<'de>,
591{
592    let value: Value = Deserialize::deserialize(deserializer)?;
593    ArticleAuthor::from_value(&value).map_err(serde::de::Error::custom)
594}
595
596pub fn deserialize_pagination<'de, D>(deserializer: D) -> Result<Option<Pagination>, D::Error>
597where
598    D: Deserializer<'de>,
599{
600    let value: Option<Value> = Deserialize::deserialize(deserializer)?;
601    match value {
602        Some(v) => Pagination::from_value(&v)
603            .map(Some)
604            .map_err(serde::de::Error::custom),
605        None => Ok(None),
606    }
607}
608
609pub fn deserialize_comments<'de, D>(deserializer: D) -> Result<Vec<ArticleComment>, D::Error>
610where
611    D: Deserializer<'de>,
612{
613    let arr: Vec<Value> = Deserialize::deserialize(deserializer)?;
614    arr.into_iter()
615        .map(|v| ArticleComment::from_value(&v))
616        .collect::<Result<Vec<_>, _>>()
617        .map_err(serde::de::Error::custom)
618}
619
620/// 文章详情
621#[derive(Clone, Debug, Default, Deserialize)]
622#[serde(default)]
623#[allow(non_snake_case)]
624pub struct ArticleDetail {
625    /// 是否在列表展示
626    #[serde(rename = "articleShowInList", deserialize_with = "bool_from_int")]
627    pub showInList: bool,
628    /// 文章创建时间
629    #[serde(rename = "articleCreateTime")]
630    pub createTime: String,
631    /// 发布者Id
632    #[serde(rename = "articleAuthorId")]
633    pub authorId: String,
634    /// 反对数
635    #[serde(rename = "articleBadCnt")]
636    pub badCnt: u32,
637    /// 文章最后评论时间
638    #[serde(rename = "articleLatestCmtTime")]
639    pub latestCmtTime: String,
640    /// 赞同数
641    #[serde(rename = "articleGoodCnt")]
642    pub goodCnt: u32,
643    /// 悬赏积分
644    #[serde(rename = "articleQnAOfferPoint")]
645    pub offerPoint: u64,
646    /// 文章缩略图
647    #[serde(rename = "articleThumbnailURL")]
648    pub thumbnailURL: String,
649    /// 置顶序号
650    #[serde(rename = "articleStickRemains")]
651    pub stickRemains: u64,
652    /// 发布时间简写
653    #[serde(rename = "timeAgo")]
654    pub timeAgo: String,
655    /// 文章更新时间
656    #[serde(rename = "articleUpdateTimeStr")]
657    pub updateTimeStr: String,
658    /// 作者用户名
659    #[serde(rename = "articleAuthorName")]
660    pub authorName: String,
661    /// 文章类型
662    #[serde(
663        rename = "articleType",
664        default = "default_article_type",
665        deserialize_with = "deserialize_type"
666    )]
667    pub type_: ArticleType,
668    /// 是否悬赏
669    #[serde(rename = "offered")]
670    pub offered: bool,
671    /// 文章创建时间字符串
672    #[serde(rename = "articleCreateTimeStr")]
673    pub createTimeStr: String,
674    /// 文章浏览数
675    #[serde(rename = "articleViewCount")]
676    pub viewCnt: u64,
677    /// 作者头像缩略图
678    #[serde(rename = "articleAuthorThumbnailURL20")]
679    pub thumbnailURL20: String,
680    /// 关注数
681    #[serde(rename = "articleWatchCnt")]
682    pub watchCnt: u64,
683    /// 文章预览内容
684    #[serde(rename = "articlePreviewContent")]
685    pub previewContent: String,
686    /// 文章标题
687    #[serde(rename = "articleTitleEmoj")]
688    pub titleEmoj: String,
689    /// 文章标题(Unicode 的 Emoji)
690    #[serde(rename = "articleTitleEmojUnicode")]
691    pub titleEmojUnicode: String,
692    /// 文章标题
693    #[serde(rename = "articleTitle")]
694    pub title: String,
695    /// 作者头像缩略图
696    #[serde(rename = "articleAuthorThumbnailURL48")]
697    pub thumbnailURL48: String,
698    /// 文章评论数
699    #[serde(rename = "articleCommentCount")]
700    pub commentCnt: u64,
701    /// 收藏数
702    #[serde(rename = "articleCollectCnt")]
703    pub collectCnt: u64,
704    /// 文章最后评论者
705    #[serde(rename = "articleLatestCmterName")]
706    pub latestCmterName: String,
707    /// 文章标签
708    #[serde(rename = "articleTags")]
709    pub tags: String,
710    /// 文章 id
711    #[serde(rename = "oId")]
712    pub oId: String,
713    /// 最后评论时间简写
714    #[serde(rename = "cmtTimeAgo")]
715    pub cmtTimeAgo: String,
716    /// 是否置顶
717    #[serde(rename = "articleStick")]
718    pub stick: u64,
719    /// 文章标签信息
720    #[serde(
721        rename = "articleTagObjs",
722        default,
723        deserialize_with = "deserialize_tag_objs"
724    )]
725    pub tagObjs: Vec<ArticleTag>,
726    /// 文章最后评论时间
727    #[serde(rename = "articleLatestCmtTimeStr")]
728    pub latestCmtTimeStr: String,
729    /// 是否匿名
730    #[serde(rename = "articleAnonymous", deserialize_with = "bool_from_int")]
731    pub anonymous: bool,
732    /// 文章感谢数
733    #[serde(rename = "articleThankCnt")]
734    pub thankCnt: u64,
735    /// 文章更新时间
736    #[serde(rename = "articleUpdateTime")]
737    pub updateTime: String,
738    /// 文章状态
739    #[serde(rename = "articleStatus", deserialize_with = "deserialize_status")]
740    pub status: ArticleStatus,
741    /// 文章点击数
742    #[serde(rename = "articleHeat")]
743    pub heat: u64,
744    /// 文章是否优选
745    #[serde(rename = "articlePerfect", deserialize_with = "bool_from_int")]
746    pub perfect: bool,
747    /// 作者头像缩略图
748    #[serde(rename = "articleAuthorThumbnailURL210")]
749    pub thumbnailURL210: String,
750    /// 文章固定链接
751    #[serde(rename = "articlePermalink")]
752    pub permalink: String,
753    /// 作者用户信息
754    #[serde(
755        rename = "articleAuthor",
756        default,
757        deserialize_with = "deserialize_author"
758    )]
759    pub author: ArticleAuthor,
760    /// 文章感谢数
761    #[serde(rename = "thankedCnt")]
762    pub thankedCnt: u64,
763    /// 文章匿名浏览量
764    #[serde(rename = "articleAnonymousView")]
765    pub anonymousView: u64,
766    /// 文章浏览量简写
767    #[serde(rename = "articleViewCntDisplayFormat")]
768    pub viewCntFormat: String,
769    /// 文章是否启用评论
770    #[serde(rename = "articleCommentable")]
771    pub commentable: bool,
772    /// 是否已打赏
773    #[serde(rename = "rewarded")]
774    pub rewarded: bool,
775    /// 打赏人数
776    #[serde(rename = "rewardedCnt")]
777    pub rewardedCnt: u64,
778    /// 文章打赏积分
779    #[serde(rename = "articleRewardPoint")]
780    pub rewardPoint: u64,
781    /// 是否已收藏
782    #[serde(rename = "isFollowing")]
783    pub isFollowing: bool,
784    /// 是否已关注
785    #[serde(rename = "isWatching")]
786    pub isWatching: bool,
787    /// 是否是我的文章
788    #[serde(rename = "isMyArticle")]
789    pub isMyArticle: bool,
790    /// 是否已感谢
791    #[serde(rename = "thanked")]
792    pub thanked: bool,
793    /// 编辑器类型
794    #[serde(rename = "articleEditorType")]
795    pub editorType: u64,
796    /// 文章音频地址
797    #[serde(rename = "articleAudioURL")]
798    pub audioURL: String,
799    /// 文章目录 HTML
800    #[serde(rename = "articleToC")]
801    pub table: String,
802    /// 文章内容 HTML
803    #[serde(rename = "articleContent")]
804    pub content: String,
805    /// 文章内容 Markdown
806    #[serde(rename = "articleOriginalContent")]
807    pub source: String,
808    /// 文章缩略图
809    #[serde(rename = "articleImg1URL")]
810    pub img1URL: String,
811    /// 文章点赞状态
812    #[serde(rename = "articleVote", deserialize_with = "deserialize_vote")]
813    pub vote: VoteStatus,
814    /// 文章随机数
815    #[serde(rename = "articleRandomDouble")]
816    pub randomDouble: f64,
817    /// 作者签名
818    #[serde(rename = "articleAuthorIntro")]
819    pub authorIntro: String,
820    /// 发布城市
821    #[serde(rename = "articleCity")]
822    pub city: String,
823    /// 发布者 IP
824    #[serde(rename = "articleIP")]
825    pub IP: String,
826    /// 作者首页地址
827    #[serde(rename = "articleAuthorURL")]
828    pub authorURL: String,
829    /// 推送 Email 推送顺序
830    #[serde(rename = "articlePushOrder")]
831    pub pushOrder: u64,
832    /// 打赏内容
833    #[serde(rename = "articleRewardContent")]
834    pub rewardContent: String,
835    /// reddit分数
836    #[serde(deserialize_with = "deserialize_reddit_score")]
837    pub redditScore: String,
838    /// 评论分页信息
839    #[serde(default, deserialize_with = "deserialize_pagination")]
840    pub pagination: Option<Pagination>,
841    /// 评论是否可见
842    #[serde(rename = "discussionViewable")]
843    pub commentViewable: bool,
844    /// 文章修改次数
845    #[serde(rename = "articleRevisionCount")]
846    pub revisionCount: u64,
847    /// 文章的评论
848    #[serde(
849        rename = "articleComments",
850        default,
851        deserialize_with = "deserialize_comments"
852    )]
853    pub comments: Vec<ArticleComment>,
854    /// 文章最佳评论
855    #[serde(
856        rename = "articleNiceComments",
857        default,
858        deserialize_with = "deserialize_comments"
859    )]
860    pub niceComments: Vec<ArticleComment>,
861}
862
863impl ArticleDetail {
864    pub fn from_value(data: &Value) -> Result<Self, Error> {
865        parse_with_float_fallback(data, "ArticleDetail")
866    }
867}
868
869pub fn deserialize_articles<'de, D>(deserializer: D) -> Result<Vec<ArticleDetail>, D::Error>
870where
871    D: Deserializer<'de>,
872{
873    let arr: Vec<Value> = Deserialize::deserialize(deserializer)?;
874    arr.into_iter()
875        .map(|v| ArticleDetail::from_value(&v))
876        .collect::<Result<Vec<_>, _>>()
877        .map_err(serde::de::Error::custom)
878}
879
880/// 文章列表
881#[derive(Clone, Debug, Deserialize)]
882#[allow(non_snake_case)]
883pub struct ArticleList {
884    /// 文章列表
885    #[serde(rename = "articles", deserialize_with = "deserialize_articles")]
886    pub list: Vec<ArticleDetail>,
887    /// 分页信息
888    pub pagination: Pagination,
889    /// 标签信息,仅查询标签下文章列表有效
890    pub tag: Option<ArticleTag>,
891}
892
893impl ArticleList {
894    pub fn from_value(data: &Value) -> Result<Self, Error> {
895        parse_with_float_fallback(data, "ArticleList")
896    }
897}
898
899/// 帖子列表查询类型
900#[derive(Clone, Debug)]
901pub enum ArticleListType {
902    /// 最近
903    Recent,
904    /// 热门
905    Hot,
906    /// 点赞
907    Good,
908    /// 最近回复
909    Reply,
910    /// 优选,需包含标签
911    Perfect,
912}
913
914impl_str_enum! {
915    ArticleListType {
916        Recent => "recent",
917        Hot => "hot",
918        Good => "good",
919        Reply => "reply",
920        Perfect => "perfect",
921    }
922}
923
924impl ArticleListType {
925    pub fn to_code(&self) -> &'static str {
926        match self {
927            ArticleListType::Recent => "",
928            ArticleListType::Hot => "/hot",
929            ArticleListType::Good => "/good",
930            ArticleListType::Reply => "/reply",
931            ArticleListType::Perfect => "/perfect",
932        }
933    }
934
935    pub fn values() -> Vec<Self> {
936        vec![
937            ArticleListType::Recent,
938            ArticleListType::Hot,
939            ArticleListType::Good,
940            ArticleListType::Reply,
941            ArticleListType::Perfect,
942        ]
943    }
944}
945
946/// 评论发布
947#[derive(Clone, Debug, Serialize, Deserialize)]
948#[allow(non_snake_case)]
949pub struct CommentPost {
950    /// 文章 Id
951    pub articleId: String,
952    /// 是否匿名评论
953    #[serde(rename = "commentAnonymous")]
954    pub isAnonymous: bool,
955    /// 评论是否楼主可见
956    #[serde(rename = "commentVisible")]
957    pub isVisible: bool,
958    /// 评论内容
959    #[serde(rename = "commentContent")]
960    pub content: String,
961    /// 回复评论 Id
962    #[serde(rename = "commentOriginalCommentId")]
963    pub replyId: String,
964}
965
966impl CommentPost {
967    pub fn from_value(data: &Value) -> Result<Self, Error> {
968        serde_json::from_value(data.clone())
969            .map_err(|e| Error::Parse(format!("Failed to parse CommentPost: {}", e)))
970    }
971
972    pub fn to_value(&self) -> Result<Value, Error> {
973        serde_json::to_value(self)
974            .map_err(|e| Error::Parse(format!("Failed to serialize CommentPost: {}", e)))
975    }
976}