bpi_rs/vip/
center.rs

1use crate::models::{Account, Vip};
2use crate::{BilibiliRequest, BpiClient, BpiError, BpiResponse};
3use serde::Deserialize;
4
5/// 大会员中心信息响应结构体
6#[derive(Debug, Clone, Deserialize)]
7pub struct VipCenterData {
8    /// 用户信息
9    pub user: User,
10    /// 钱包信息
11    pub wallet: WalletInfo,
12
13    pub in_review: bool,
14}
15
16/// 用户信息结构体
17#[derive(Debug, Clone, Deserialize)]
18pub struct User {
19    /// 账号基本信息
20    pub account: Account,
21    /// 账号会员信息
22    pub vip: Vip,
23    /// 电视会员信息
24    pub tv: TvVipInfo,
25    /// 空字段
26    pub background_image_small: String,
27    /// 空字段
28    pub background_image_big: String,
29    /// 用户昵称
30    pub panel_title: String,
31
32    /// 大会员提示文案(有效期/到期)
33    pub vip_overdue_explain: String,
34    /// 电视大会员提示文案(有效期/到期)
35    pub tv_overdue_explain: String,
36    /// 空字段
37    pub account_exception_text: String,
38    /// 是否自动续费
39    pub is_auto_renew: bool,
40    /// 是否自动续费电视大会员
41    pub is_tv_auto_renew: bool,
42    /// 大会员到期剩余时间(单位为秒)
43    pub surplus_seconds: u64,
44    /// 持续开通大会员时间(单位为秒)
45    pub vip_keep_time: u64,
46}
47
48/// 电视会员信息结构体
49#[derive(Debug, Clone, Deserialize)]
50pub struct TvVipInfo {
51    /// 电视大会员类型(0:无,1:月大会员,2:年度及以上大会员)
52    #[serde(rename = "type")]
53    pub tv_type: u32,
54    /// 电视大支付类型(0:未支付,1:已支付)
55    pub vip_pay_type: u32,
56    /// 电视大会员状态(0:无,1:有)
57    pub status: u32,
58    /// 电视大会员过期时间(毫秒时间戳)
59    pub due_date: u64,
60}
61
62/// 钱包信息结构体
63#[derive(Debug, Clone, Deserialize)]
64pub struct WalletInfo {
65    /// 当前B币券
66    pub coupon: u64,
67    /// 积分
68    pub point: u64,
69    /// 是否已领取特权
70    pub privilege_received: bool,
71}
72
73impl BpiClient {
74    /// 获取大会员中心信息
75    ///
76    /// 文档: https://socialsisteryi.github.io/bilibili-API-collect/docs/vip/center.html#获取大会员中心信息
77    ///
78    pub async fn vip_center_info(&self) -> Result<BpiResponse<VipCenterData>, BpiError> {
79        self.get("https://api.bilibili.com/x/vip/web/vip_center/combine")
80            .query(&[("build", 0)])
81            .send_bpi("获取大会员中心信息")
82            .await
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[tokio::test]
91    async fn test_vip_center_info_comprehensive() {
92        tracing::info!("开始测试大会员中心信息的综合功能");
93
94        let bpi = BpiClient::new();
95        let resp = bpi.vip_center_info().await;
96
97        match resp {
98            Ok(response) => {
99                tracing::info!("开始详细分析大会员中心信息数据结构");
100
101                let data = &response.data.unwrap();
102
103                // 1. 用户账户信息详细检查
104                tracing::info!("=== 用户账户信息 ===");
105                let account = &data.user.account;
106                tracing::info!("用户ID: {}", account.mid);
107                tracing::info!("用户昵称: {}", account.name);
108                tracing::info!("用户性别: {}", account.sex);
109                tracing::info!("用户等级: {}", account.rank);
110                tracing::info!("用户签名: {}", account.sign);
111                tracing::info!(
112                    "是否注销: {}",
113                    if account.is_deleted == 0 {
114                        "正常"
115                    } else {
116                        "已注销"
117                    }
118                );
119                tracing::info!(
120                    "是否转正: {}",
121                    if account.is_senior_member == 1 {
122                        "正式会员"
123                    } else {
124                        "未转正"
125                    }
126                );
127
128                // 2. 会员信息详细检查
129                tracing::info!("=== 会员信息 ===");
130                let vip = &data.user.vip;
131                let vip_type_text = match vip.vip_type {
132                    0 => "无会员",
133                    1 => "月大会员",
134                    2 => "年度及以上大会员",
135                    _ => "未知类型",
136                };
137                tracing::info!("会员类型: {} ({})", vip.vip_type, vip_type_text);
138                tracing::info!(
139                    "会员状态: {}",
140                    if vip.vip_status == 1 {
141                        "有效"
142                    } else {
143                        "无效"
144                    }
145                );
146
147                if vip.vip_due_date > 0 {
148                    let due_date = chrono::DateTime::from_timestamp_millis(vip.vip_due_date as i64);
149                    if let Some(date) = due_date {
150                        tracing::info!("会员到期时间: {}", date.format("%Y-%m-%d %H:%M:%S"));
151                    }
152                }
153
154                tracing::info!("会员标签主题: {}", vip.label.label_theme);
155                tracing::info!("会员标签文案: {}", vip.label.text);
156                tracing::info!("昵称颜色: {}", vip.nickname_color);
157
158                // 3. 电视会员信息
159                tracing::info!("=== 电视会员信息 ===");
160                let tv = &data.user.tv;
161                let tv_type_text = match tv.tv_type {
162                    0 => "无电视会员",
163                    1 => "月电视会员",
164                    2 => "年度及以上电视会员",
165                    _ => "未知类型",
166                };
167                tracing::info!("电视会员类型: {} ({})", tv.tv_type, tv_type_text);
168                tracing::info!(
169                    "电视会员状态: {}",
170                    if tv.status == 1 { "有效" } else { "无效" }
171                );
172
173                // 4. 头像框信息
174                tracing::info!("=== 头像框信息 ===");
175                // let pendant = &data.user.as_ref().avatar_pendant.unwrap();
176                // tracing::info!("头像框URL: {}", pendant.image);
177                // tracing::info!("动态头像框URL: {}", pendant.image_enhance);
178
179                // 5. 续费和通知信息
180                tracing::info!("=== 续费和通知信息 ===");
181                tracing::info!("自动续费状态: {}", data.user.is_auto_renew);
182                tracing::info!("电视会员自动续费: {}", data.user.is_tv_auto_renew);
183                tracing::info!("大会员提示: {}", data.user.vip_overdue_explain);
184                tracing::info!("电视会员提示: {}", data.user.tv_overdue_explain);
185
186                // 6. 钱包详细信息
187                tracing::info!("=== 钱包信息 ===");
188                let wallet = &data.wallet;
189                tracing::info!("B币券: {}", wallet.coupon);
190                tracing::info!("积分: {}", wallet.point);
191                tracing::info!("特权已领取: {}", wallet.privilege_received);
192
193                // 验证数据合理性
194                assert!(data.user.account.mid > 0, "用户mid应该大于0");
195                assert!(!data.user.account.name.is_empty(), "用户昵称不应为空");
196
197                tracing::info!("大会员中心信息综合测试通过");
198            }
199            Err(e) => {
200                if let BpiError::Api { code: -101, .. } = e {
201                    tracing::info!("账号未登录,无法获取详细信息,测试通过");
202                } else {
203                    tracing::error!("综合测试失败: {:?}", e);
204                    assert!(false, "综合测试失败");
205                }
206            }
207        }
208    }
209
210    #[tokio::test]
211    async fn test_time_calculation() {
212        tracing::info!("开始测试时间计算功能");
213
214        let bpi = BpiClient::new();
215        let resp = bpi.vip_center_info().await;
216
217        match resp {
218            Ok(response) => {
219                let user = &response.data.unwrap().user;
220
221                // 计算剩余时间
222                if user.surplus_seconds > 0 {
223                    let total_seconds = user.surplus_seconds;
224                    let days = total_seconds / (24 * 3600);
225                    let hours = (total_seconds % (24 * 3600)) / 3600;
226                    let minutes = (total_seconds % 3600) / 60;
227                    let seconds = total_seconds % 60;
228
229                    tracing::info!(
230                        "大会员剩余时间详细: {}天{}小时{}分钟{}秒",
231                        days,
232                        hours,
233                        minutes,
234                        seconds
235                    );
236
237                    if days > 0 {
238                        tracing::info!("剩余时间充足(超过1天)");
239                    } else if hours > 0 {
240                        tracing::info!("剩余时间不足1天但超过1小时");
241                    } else {
242                        tracing::info!("剩余时间不足1小时,即将到期");
243                    }
244                } else {
245                    tracing::info!("没有大会员或已过期");
246                }
247
248                // 计算持续开通时间
249                if user.vip_keep_time > 0 {
250                    let keep_days = user.vip_keep_time / (24 * 3600);
251                    let keep_years = keep_days / 365;
252                    let keep_months = (keep_days % 365) / 30;
253                    let remaining_days = keep_days % 30;
254
255                    tracing::info!(
256                        "持续开通大会员时间: {}年{}个月{}天",
257                        keep_years,
258                        keep_months,
259                        remaining_days
260                    );
261
262                    if keep_years > 0 {
263                        tracing::info!("长期忠实用户(超过1年)");
264                    } else if keep_days > 30 {
265                        tracing::info!("短期用户(超过1个月)");
266                    } else {
267                        tracing::info!("新用户(少于1个月)");
268                    }
269                } else {
270                    tracing::info!("没有持续开通记录");
271                }
272
273                // 分析会员到期时间
274                let vip = &user.vip;
275                if vip.vip_due_date > 0 {
276                    if let Some(due_date) =
277                        chrono::DateTime::from_timestamp_millis(vip.vip_due_date as i64)
278                    {
279                        let now = chrono::Utc::now();
280                        let duration = due_date.signed_duration_since(now);
281
282                        if duration.num_days() > 0 {
283                            tracing::info!("会员还有{}天到期", duration.num_days());
284                        } else if duration.num_hours() > 0 {
285                            tracing::info!("会员还有{}小时到期", duration.num_hours());
286                        } else if duration.num_minutes() > 0 {
287                            tracing::info!("会员还有{}分钟到期", duration.num_minutes());
288                        } else if duration.num_seconds() > 0 {
289                            tracing::info!("会员还有{}秒到期", duration.num_seconds());
290                        } else {
291                            tracing::info!("会员已过期");
292                        }
293
294                        tracing::info!(
295                            "会员到期时间: {}",
296                            due_date.format("%Y-%m-%d %H:%M:%S UTC")
297                        );
298                    }
299                }
300
301                tracing::info!("时间计算测试通过");
302                assert!(true);
303            }
304            Err(e) => {
305                if let BpiError::Api { code: -101, .. } = e {
306                    tracing::info!("账号未登录,无法进行时间计算测试");
307                    assert!(true);
308                } else {
309                    tracing::error!("时间计算测试失败: {:?}", e);
310                    assert!(false);
311                }
312            }
313        }
314    }
315}