Skip to main content

bpi_rs/models/
vip.rs

1use serde::de::{self, Deserializer, MapAccess, Visitor};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5/// 会员信息
6#[derive(Default, Debug, Clone, Serialize)]
7pub struct Vip {
8    /// 会员类型 0:无 1:月大会员 2:年度及以上大会员
9    /// 别名:vipType | vip_type | type
10    pub vip_type: u8,
11    /// 会员状态 0:无 1:有
12    /// 别名:vipStatus | vip_status | status
13    pub vip_status: u8,
14    /// 会员过期时间 毫秒时间戳
15    /// 别名:vipDueDate | due_date | vip_due_date
16    pub vip_due_date: u64,
17    /// 会员标签
18    pub label: VipLabel,
19    /// 会员昵称颜色 颜色码,一般为#FB7299
20    pub nickname_color: String,
21
22    /// 支付类型 0:未开启自动续费 1:已开启自动续费
23    pub vip_pay_type: Option<u8>,
24
25    /// 大角色类型 1:月度大会员 3:年度大会员 7:十年大会员 15:百年大会员
26    pub role: Option<u8>,
27
28    /// 是否为tv会员
29    pub is_tv_vip: Option<bool>,
30    /// 电视大会员状态 0:未开通
31    pub tv_vip_status: Option<u8>,
32    /// 电视大会员支付类型
33    pub tv_vip_pay_type: Option<u8>,
34    /// 电视大会员过期时间 秒级时间戳
35    pub tv_due_date: Option<u64>,
36
37    /// 用户mid
38    pub mid: Option<u64>,
39    /// 昵称
40    pub name: Option<String>,
41}
42
43/// 会员标签结构体
44#[derive(Default, Debug, Clone, Serialize, Deserialize)]
45pub struct VipLabel {
46    /// 会员类型文案(大会员/年度大会员/十年大会员/百年大会员/最强绿鲤鱼)
47    #[serde(default)]
48    pub text: String,
49    /// 会员标签(vip/annual_vip/ten_annual_vip/hundred_annual_vip/fools_day_hundred_annual_vip)
50    #[serde(default)]
51    pub label_theme: String,
52    /// 会员标签文本颜色
53    #[serde(default)]
54    pub text_color: String,
55    /// 样式
56    #[serde(default)]
57    pub bg_style: u32,
58    /// 会员标签背景颜色(颜色码,一般为#FB7299)
59    #[serde(default)]
60    pub bg_color: String,
61}
62
63impl<'de> Deserialize<'de> for Vip {
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: Deserializer<'de>,
67    {
68        struct VipVisitor;
69
70        impl<'de> Visitor<'de> for VipVisitor {
71            type Value = Vip;
72
73            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
74                formatter.write_str("a Vip object")
75            }
76
77            fn visit_map<M>(self, mut map: M) -> Result<Vip, M::Error>
78            where
79                M: MapAccess<'de>,
80            {
81                let mut vip_type = None;
82                let mut vip_status = None;
83                let mut vip_due_date = None;
84                let mut label = None;
85                let mut nickname_color = None;
86                let mut vip_pay_type = None;
87                let mut role = None;
88                let mut is_tv_vip = None;
89                let mut tv_vip_status = None;
90                let mut tv_vip_pay_type = None;
91                let mut tv_due_date = None;
92                let mut mid = None;
93                let mut name = None;
94
95                while let Some(key) = map.next_key::<String>()? {
96                    match key.as_str() {
97                        "vipType" | "vip_type" | "type" => {
98                            if vip_type.is_some() {
99                                return Err(de::Error::duplicate_field("vip_type"));
100                            }
101                            vip_type = Some(next_u8_value(&mut map, "vip_type")?);
102                        }
103                        "vip_status" | "vipStatus" | "status" => {
104                            if vip_status.is_none() {
105                                vip_status = Some(next_u8_value(&mut map, "vip_status")?);
106                            } else {
107                                let _: serde_json::Value = map.next_value()?;
108                            }
109                        }
110                        "vipDueDate" | "due_date" | "vip_due_date" => {
111                            if vip_due_date.is_none() {
112                                vip_due_date = Some(next_u64_value(&mut map, "vip_due_date")?);
113                            } else {
114                                let _: serde_json::Value = map.next_value()?;
115                            }
116                        }
117                        "label" => {
118                            if label.is_some() {
119                                return Err(de::Error::duplicate_field("label"));
120                            }
121                            label = Some(map.next_value()?);
122                        }
123                        "nickname_color" => {
124                            if nickname_color.is_some() {
125                                return Err(de::Error::duplicate_field("nickname_color"));
126                            }
127                            nickname_color = Some(map.next_value()?);
128                        }
129                        "vip_pay_type" => {
130                            if vip_pay_type.is_some() {
131                                return Err(de::Error::duplicate_field("vip_pay_type"));
132                            }
133                            vip_pay_type = next_optional_u8_value(&mut map, "vip_pay_type")?;
134                        }
135                        "role" => {
136                            if role.is_some() {
137                                return Err(de::Error::duplicate_field("role"));
138                            }
139                            role = next_optional_u8_value(&mut map, "role")?;
140                        }
141                        "is_tv_vip" => {
142                            if is_tv_vip.is_some() {
143                                return Err(de::Error::duplicate_field("is_tv_vip"));
144                            }
145                            is_tv_vip = Some(map.next_value()?);
146                        }
147                        "tv_vip_status" => {
148                            if tv_vip_status.is_some() {
149                                return Err(de::Error::duplicate_field("tv_vip_status"));
150                            }
151                            tv_vip_status = next_optional_u8_value(&mut map, "tv_vip_status")?;
152                        }
153                        "tv_vip_pay_type" => {
154                            if tv_vip_pay_type.is_some() {
155                                return Err(de::Error::duplicate_field("tv_vip_pay_type"));
156                            }
157                            tv_vip_pay_type = next_optional_u8_value(&mut map, "tv_vip_pay_type")?;
158                        }
159                        "tv_due_date" => {
160                            if tv_due_date.is_some() {
161                                return Err(de::Error::duplicate_field("tv_due_date"));
162                            }
163                            tv_due_date = next_optional_u64_value(&mut map, "tv_due_date")?;
164                        }
165                        "mid" => {
166                            if mid.is_some() {
167                                return Err(de::Error::duplicate_field("mid"));
168                            }
169                            mid = next_optional_u64_value(&mut map, "mid")?;
170                        }
171                        "name" => {
172                            if name.is_some() {
173                                return Err(de::Error::duplicate_field("name"));
174                            }
175                            name = Some(map.next_value()?);
176                        }
177                        _ => {
178                            // 忽略未知字段
179                            let _: serde_json::Value = map.next_value()?;
180                        }
181                    }
182                }
183
184                let vip_type = vip_type.ok_or_else(|| de::Error::missing_field("vip_type"))?;
185                let vip_status =
186                    vip_status.ok_or_else(|| de::Error::missing_field("vip_status"))?;
187                let vip_due_date = vip_due_date.unwrap_or_default();
188                let label = label.unwrap_or_default();
189                let nickname_color = nickname_color.unwrap_or_default();
190
191                Ok(Vip {
192                    vip_type,
193                    vip_status,
194                    vip_due_date,
195                    label,
196                    nickname_color,
197                    vip_pay_type,
198                    role,
199                    is_tv_vip,
200                    tv_vip_status,
201                    tv_vip_pay_type,
202                    tv_due_date,
203                    mid,
204                    name,
205                })
206            }
207        }
208
209        deserializer.deserialize_map(VipVisitor)
210    }
211}
212
213fn next_u8_value<'de, M>(map: &mut M, field: &'static str) -> Result<u8, M::Error>
214where
215    M: MapAccess<'de>,
216{
217    parse_u8_value(map.next_value()?, field)
218}
219
220fn next_u64_value<'de, M>(map: &mut M, field: &'static str) -> Result<u64, M::Error>
221where
222    M: MapAccess<'de>,
223{
224    parse_u64_value(map.next_value()?, field)
225}
226
227fn next_optional_u8_value<'de, M>(map: &mut M, field: &'static str) -> Result<Option<u8>, M::Error>
228where
229    M: MapAccess<'de>,
230{
231    match map.next_value()? {
232        serde_json::Value::Null => Ok(None),
233        value => parse_u8_value(value, field).map(Some),
234    }
235}
236
237fn next_optional_u64_value<'de, M>(
238    map: &mut M,
239    field: &'static str,
240) -> Result<Option<u64>, M::Error>
241where
242    M: MapAccess<'de>,
243{
244    match map.next_value()? {
245        serde_json::Value::Null => Ok(None),
246        value => parse_u64_value(value, field).map(Some),
247    }
248}
249
250fn parse_u8_value<E>(value: serde_json::Value, field: &'static str) -> Result<u8, E>
251where
252    E: de::Error,
253{
254    let raw = parse_u64_value::<E>(value, field)?;
255    u8::try_from(raw).map_err(|_| E::custom(format!("{field} must fit in u8")))
256}
257
258fn parse_u64_value<E>(value: serde_json::Value, field: &'static str) -> Result<u64, E>
259where
260    E: de::Error,
261{
262    match value {
263        serde_json::Value::Number(number) => number
264            .as_u64()
265            .ok_or_else(|| E::custom(format!("{field} must be a non-negative integer"))),
266        serde_json::Value::String(text) => text
267            .parse::<u64>()
268            .map_err(|_| E::custom(format!("{field} must be a numeric string"))),
269        _ => Err(E::custom(format!("{field} must be a string or number"))),
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn vip_deserializes_numeric_fields_from_strings() {
279        let vip: Vip = serde_json::from_str(
280            r##"{
281                "type": "2",
282                "status": "1",
283                "due_date": "1813334400000",
284                "label": {
285                    "text": "年度大会员",
286                    "label_theme": "annual_vip",
287                    "text_color": "#FFFFFF",
288                    "bg_style": 1,
289                    "bg_color": "#FB7299"
290                },
291                "nickname_color": "#FB7299",
292                "role": "3",
293                "tv_due_date": "0",
294                "tv_vip_pay_type": "0",
295                "tv_vip_status": "0",
296                "vip_pay_type": "0",
297                "mid": "4279370"
298            }"##,
299        )
300        .expect("vip should parse string numeric fields");
301
302        assert_eq!(vip.vip_type, 2);
303        assert_eq!(vip.vip_status, 1);
304        assert_eq!(vip.vip_due_date, 1813334400000);
305        assert_eq!(vip.role, Some(3));
306        assert_eq!(vip.mid, Some(4279370));
307    }
308
309    #[test]
310    fn vip_label_defaults_missing_display_fields() {
311        let label: VipLabel = serde_json::from_str(
312            r##"{
313                "bg_style": 1,
314                "text_color": ""
315            }"##,
316        )
317        .expect("vip label should tolerate omitted display fields");
318
319        assert_eq!(label.text, "");
320        assert_eq!(label.label_theme, "");
321        assert_eq!(label.text_color, "");
322        assert_eq!(label.bg_style, 1);
323        assert_eq!(label.bg_color, "");
324    }
325
326    #[test]
327    fn vip_deserializes_compact_relation_payload() {
328        let vip: Vip = serde_json::from_str(
329            r#"{
330                "vipType": 0,
331                "vipStatus": 0
332            }"#,
333        )
334        .expect("relation-list vip payload should parse");
335
336        assert_eq!(vip.vip_type, 0);
337        assert_eq!(vip.vip_status, 0);
338        assert_eq!(vip.vip_due_date, 0);
339        assert_eq!(vip.nickname_color, "");
340        assert_eq!(vip.label.text, "");
341    }
342}