Skip to main content

bpi_rs/user/
info.rs

1//! B站用户信息相关接口
2//!
3//! [查看 API 文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
4use crate::models::{ LevelInfo, Nameplate, Official, OfficialVerify, Pendant, Vip, VipLabel };
5use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
6use serde::{ Deserialize, Serialize };
7
8/// 用户空间详细信息响应结构体
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct UserSpaceInfo {
11    /// 用户mid
12    pub mid: u64,
13    /// 昵称
14    pub name: String,
15    /// 性别 男/女/保密
16    pub sex: String,
17    /// 头像链接
18    pub face: String,
19    /// 是否为NFT头像 0:不是NFT头像 1:是NFT头像
20    pub face_nft: u8,
21    /// NFT头像类型
22    pub face_nft_type: Option<u8>,
23    /// 签名
24    pub sign: String,
25    /// 用户权限等级
26    pub rank: u32,
27    /// 当前等级 0-6级
28    pub level: u8,
29    /// 注册时间 此接口返回恒为0
30    pub jointime: u64,
31    /// 节操值 此接口返回恒为0
32    pub moral: u64,
33    /// 封禁状态 0:正常 1:被封
34    pub silence: u8,
35    /// 硬币数 需要登录(Cookie) 只能查看自己的 默认为0
36    pub coins: f64,
37    /// 是否具有粉丝勋章
38    pub fans_badge: bool,
39    /// 粉丝勋章信息
40    pub fans_medal: Option<FansMedal>,
41    /// 认证信息
42    pub official: Official,
43    /// 会员信息
44    pub vip: Vip,
45    /// 头像框信息
46    pub pendant: Pendant,
47    /// 勋章信息
48    pub nameplate: Nameplate,
49    /// 用户荣誉信息
50    pub user_honour_info: UserHonourInfo,
51    /// 是否关注此用户 需要登录(Cookie) 未登录恒为false
52    pub is_followed: bool,
53    /// 主页头图链接
54    pub top_photo: String,
55    /// 主题信息
56    pub theme: serde_json::Value,
57    /// 系统通知
58    pub sys_notice: SysNotice,
59    /// 直播间信息
60    pub live_room: LiveRoom,
61    /// 生日 MM-DD 如设置隐私为空
62    pub birthday: String,
63    /// 学校
64    pub school: School,
65    /// 专业资质信息
66    pub profession: Option<Profession>,
67    /// 个人标签
68    pub tags: Option<Vec<String>>,
69    /// 系列信息
70    pub series: Series,
71    /// 是否为硬核会员 0:否 1:是
72    pub is_senior_member: u8,
73    /// MCN信息
74    pub mcn_info: Option<serde_json::Value>,
75    /// Gaia资源类型
76    pub gaia_res_type: Option<u8>,
77    /// Gaia数据
78    pub gaia_data: Option<serde_json::Value>,
79    /// 是否存在风险
80    pub is_risk: bool,
81    /// 充电信息
82    pub elec: Elec,
83    /// 是否显示老粉计划
84    pub contract: Contract,
85    /// 证书显示
86    pub certificate_show: Option<bool>,
87    /// 昵称渲染信息
88    pub name_render: Option<serde_json::Value>,
89}
90
91/// 粉丝勋章信息
92#[derive(Debug, Clone, Deserialize, Serialize)]
93pub struct FansMedal {
94    /// 是否展示
95    pub show: bool,
96    /// 是否佩戴了粉丝勋章
97    pub wear: bool,
98    /// 粉丝勋章详细信息
99    pub medal: Option<Medal>,
100}
101
102/// 粉丝勋章详细信息
103#[derive(Debug, Clone, Deserialize, Serialize)]
104pub struct Medal {
105    /// 粉丝勋章等级
106    pub level: u8,
107    /// 粉丝勋章等级
108    pub guard_level: u8,
109    /// 粉丝勋章名称
110    pub medal_name: String,
111}
112
113/// 用户荣誉信息
114#[derive(Debug, Clone, Deserialize, Serialize)]
115pub struct UserHonourInfo {
116    /// 用户mid
117    pub mid: u64,
118    /// 颜色
119    pub colour: Option<String>,
120    /// 标签
121    pub tags: Option<Vec<serde_json::Value>>,
122}
123
124/// 系统通知
125#[derive(Debug, Clone, Deserialize, Serialize)]
126pub struct SysNotice {
127    /// 通知id
128    pub id: Option<u32>,
129    /// 显示文案
130    pub content: Option<String>,
131    /// 跳转地址
132    pub url: Option<String>,
133    /// 提示类型
134    pub notice_type: Option<u8>,
135    /// 前缀图标
136    pub icon: Option<String>,
137    /// 文字颜色
138    pub text_color: Option<String>,
139    /// 背景颜色
140    pub bg_color: Option<String>,
141}
142
143/// 直播间信息
144#[derive(Debug, Clone, Deserialize, Serialize)]
145pub struct LiveRoom {
146    /// 直播间状态 0:无房间 1:有房间
147    #[serde(rename = "roomStatus")]
148    pub room_status: u8,
149    /// 直播状态 0:未开播 1:直播中
150    #[serde(rename = "liveStatus")]
151    pub live_status: u8,
152    /// 直播间网页url
153    pub url: String,
154    /// 直播间标题
155    pub title: String,
156    /// 直播间封面url
157    pub cover: String,
158    /// 观看显示信息
159    pub watched_show: WatchedShow,
160    /// 直播间id
161    pub roomid: u64,
162    /// 轮播状态 0:未轮播 1:轮播
163    #[serde(rename = "roundStatus")]
164    pub round_status: u8,
165    /// 广播类型
166    pub broadcast_type: u8,
167}
168
169/// 观看显示信息
170#[derive(Debug, Clone, Deserialize, Serialize)]
171pub struct WatchedShow {
172    /// 开关
173    pub switch: bool,
174    /// 观看人数
175    pub num: u64,
176    /// 小文本
177    pub text_small: String,
178    /// 大文本
179    pub text_large: String,
180    /// 图标
181    pub icon: String,
182    /// 图标位置
183    pub icon_location: String,
184    /// 网页图标
185    pub icon_web: String,
186}
187
188/// 学校信息
189#[derive(Debug, Clone, Deserialize, Serialize)]
190pub struct School {
191    /// 就读大学名称 没有则为空
192    pub name: String,
193}
194
195/// 专业资质信息
196#[derive(Debug, Clone, Deserialize, Serialize)]
197pub struct Profession {
198    /// 资质名称
199    pub name: String,
200    /// 职位
201    pub department: String,
202    /// 所属机构
203    pub title: String,
204    /// 是否显示 0:不显示 1:显示
205    pub is_show: u8,
206}
207
208/// 系列信息
209#[derive(Debug, Clone, Deserialize, Serialize)]
210pub struct Series {
211    /// 用户升级状态
212    pub user_upgrade_status: u8,
213    /// 是否显示升级窗口
214    pub show_upgrade_window: bool,
215}
216
217/// 充电信息
218#[derive(Debug, Clone, Deserialize, Serialize)]
219pub struct Elec {
220    /// 显示的充电信息
221    pub show_info: ShowInfo,
222}
223
224/// 显示的充电信息
225#[derive(Debug, Clone, Deserialize, Serialize)]
226pub struct ShowInfo {
227    /// 是否显示充电按钮
228    pub show: bool,
229    /// 充电功能开启状态 -1:未开通充电功能 1:已开通自定义充电 2:已开通包月、自定义充电 3:已开通包月高档、自定义充电
230    pub state: i8,
231    /// 充电按钮显示文字
232    pub title: String,
233    /// 充电图标
234    pub icon: String,
235    /// 跳转url
236    pub jump_url: String,
237}
238
239/// 老粉计划信息
240#[derive(Debug, Clone, Deserialize, Serialize)]
241pub struct Contract {
242    /// 是否显示
243    pub is_display: bool,
244    /// 是否在显示老粉计划 true:显示 false:不显示
245    pub is_follow_display: bool,
246}
247
248/// 用户名片信息响应结构体
249#[derive(Debug, Clone, Deserialize, Serialize)]
250pub struct UserCardInfo {
251    /// 卡片信息
252    pub card: Card,
253    /// 是否关注此用户 true:已关注 false:未关注 需要登录(Cookie) 未登录为false
254    pub following: bool,
255    /// 用户稿件数
256    pub archive_count: u32,
257    /// 作用尚不明确
258    pub article_count: u32,
259    /// 粉丝数
260    pub follower: u32,
261    /// 点赞数
262    pub like_num: u32,
263}
264
265/// 用户卡片详细信息
266#[derive(Debug, Clone, Deserialize, Serialize)]
267pub struct Card {
268    /// 用户mid
269    pub mid: String,
270    /// 用户昵称
271    pub name: String,
272    /// 用户性别 男/女/保密
273    pub sex: String,
274    /// 用户头像链接
275    pub face: String,
276    /// 显示排名 作用尚不明确
277    #[serde(rename = "DisplayRank")]
278    pub display_rank: String,
279    /// 注册时间 作用尚不明确
280    pub regtime: u64,
281    /// 用户状态 0:正常 -2:被封禁
282    pub spacesta: i32,
283    /// 生日 作用尚不明确
284    pub birthday: String,
285    /// 地点 作用尚不明确
286    pub place: String,
287    /// 描述 作用尚不明确
288    pub description: String,
289    /// 文章数 作用尚不明确
290    pub article: u32,
291    /// 关注列表 作用尚不明确
292    pub attentions: Vec<serde_json::Value>,
293    /// 粉丝数
294    pub fans: u32,
295    /// 好友数
296    pub friend: u32,
297    /// 关注数
298    pub attention: u32,
299    /// 签名
300    pub sign: String,
301    /// 等级信息
302    pub level_info: LevelInfo,
303    /// 挂件信息
304    pub pendant: Pendant,
305    /// 勋章信息
306    pub nameplate: Nameplate,
307    /// 认证信息
308    #[serde(rename = "Official")]
309    pub official: Official,
310    /// 认证信息2
311    pub official_verify: OfficialVerify,
312    /// 大会员状态
313    pub vip: Vip,
314    /// 主页头图
315    pub space: Option<Space>,
316}
317
318/// 主页头图信息
319#[derive(Debug, Clone, Deserialize, Serialize)]
320pub struct Space {
321    /// 主页头图url 小图
322    pub s_img: String,
323    /// 主页头图url 正常
324    pub l_img: String,
325}
326
327/// 用户卡片(精简版)
328#[derive(Debug, Clone, Deserialize, Serialize)]
329pub struct UserCard {
330    pub mid: u64,
331    pub name: String,
332    pub face: String,
333    pub sign: String,
334    pub rank: i32,
335    pub level: i32,
336    pub silence: i32,
337}
338
339/// 用户详细信息(完整版)
340#[derive(Debug, Clone, Deserialize, Serialize)]
341pub struct UserInfo {
342    pub mid: u64,
343    pub name: String,
344    pub sign: String,
345    pub rank: i32,
346    pub level: i32,
347    pub silence: i32,
348
349    pub sex: Option<String>,
350    pub face: String,
351    pub vip: Option<UserVip>,
352    pub official: Option<UserOfficial>,
353    pub is_fake_account: Option<u32>,
354    pub expert_info: Option<serde_json::Value>,
355}
356
357/// 大会员信息
358#[derive(Debug, Clone, Deserialize, Serialize)]
359pub struct UserVip {
360    pub r#type: i32,
361    pub status: i32,
362    pub due_date: i64,
363    pub vip_pay_type: i32,
364    pub theme_type: i32,
365    pub label: Option<VipLabel>,
366}
367
368/// 认证信息
369#[derive(Debug, Clone, Deserialize, Serialize)]
370pub struct UserOfficial {
371    #[serde(default)]
372    pub role: i32,
373    #[serde(default)]
374    pub title: String,
375    #[serde(default)]
376    pub desc: String,
377    #[serde(default)]
378    pub r#type: i32,
379}
380
381impl BpiClient {
382    /// 获取用户空间详细信息
383    /// 需要 Wbi 签名认证
384    ///
385    /// # 文档
386    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
387    ///
388    /// - `mid`: 用户 UID
389    pub async fn user_space_info(&self, mid: u64) -> Result<BpiResponse<UserSpaceInfo>, BpiError> {
390        // 构建查询参数
391        let params = vec![("mid", mid.to_string())];
392
393        let params = self.get_wbi_sign2(params).await?;
394
395        self
396            .get("https://api.bilibili.com/x/space/wbi/acc/info")
397            .query(&params)
398            .send_bpi("获取用户空间详细信息").await
399    }
400
401    /// 获取用户名片信息
402    ///
403    /// # 文档
404    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
405    ///
406    /// # 参数
407    ///
408    /// | 名称 | 类型 | 说明 |
409    /// | ---- | ---- | ---- |
410    /// | `mid` | u64 | 用户 UID |
411    /// | `photo` | `Option<bool>` | 是否包含主页头图 |
412    pub async fn user_card_info(
413        &self,
414        mid: u64,
415        photo: Option<bool>
416    ) -> Result<BpiResponse<UserCardInfo>, BpiError> {
417        let mut params = vec![("mid", mid.to_string())];
418
419        // 如果指定了photo参数,则添加到请求参数中
420        if let Some(photo_value) = photo {
421            params.push(("photo", photo_value.to_string()));
422        }
423
424        self
425            .get("https://api.bilibili.com/x/web-interface/card")
426            .query(&params)
427            .send_bpi("获取用户名片信息").await
428    }
429
430    /// 获取用户名片信息(包含主页头图)
431    ///
432    /// # 文档
433    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
434    ///
435    /// - `mid`: 用户 UID
436    pub async fn user_card_info_with_photo(
437        &self,
438        mid: u64
439    ) -> Result<BpiResponse<UserCardInfo>, BpiError> {
440        self.user_card_info(mid, Some(true)).await
441    }
442
443    /// 获取用户名片信息(不包含主页头图)
444    ///
445    /// # 文档
446    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
447    ///
448    /// - `mid`: 用户 UID
449    pub async fn user_card_info_without_photo(
450        &self,
451        mid: u64
452    ) -> Result<BpiResponse<UserCardInfo>, BpiError> {
453        self.user_card_info(mid, Some(false)).await
454    }
455
456    /// 批量获取用户卡片(精简信息)
457
458    pub async fn user_cards(&self, mids: &[u64]) -> Result<BpiResponse<Vec<UserCard>>, BpiError> {
459        let mids_str = mids
460            .iter()
461            .map(|m| m.to_string())
462            .collect::<Vec<_>>()
463            .join(",");
464
465        self
466            .get("https://api.vc.bilibili.com/account/v1/user/cards")
467            .query(&[("uids", mids_str)])
468            .send_bpi("批量获取用户卡片").await
469    }
470
471    /// 批量获取用户详细信息(带大会员/认证信息)
472
473    pub async fn user_infos(&self, mids: &[u64]) -> Result<BpiResponse<Vec<UserInfo>>, BpiError> {
474        let mids_str = mids
475            .iter()
476            .map(|m| m.to_string())
477            .collect::<Vec<_>>()
478            .join(",");
479
480        self
481            .get("https://api.vc.bilibili.com/x/im/user_infos")
482            .query(&[("uids", mids_str)])
483            .send_bpi("批量获取用户详细信息").await
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    #[tokio::test]
492    async fn test_get_user_space_info() {
493        tracing::info!("开始测试获取用户空间详细信息");
494
495        let bpi = BpiClient::new();
496        let mid = 2; // 测试用户ID
497
498        tracing::info!("测试用户ID: {}", mid);
499
500        let resp = bpi.user_space_info(mid).await;
501
502        match &resp {
503            Ok(response) => {
504                let data = response.clone().data.unwrap();
505                tracing::info!("请求成功,返回码: {}", response.code);
506                tracing::info!("用户昵称: {}", data.name);
507                tracing::info!("用户等级: {}", data.level);
508                tracing::info!("是否为会员: {}", data.vip.vip_type > 0);
509                tracing::info!(
510                    "粉丝数量: {}",
511                    data.fans_medal.as_ref().map_or(0, |_| 1)
512                );
513            }
514            Err(e) => {
515                tracing::error!("请求失败: {:?}", e);
516            }
517        }
518
519        assert!(resp.is_ok());
520        tracing::info!("测试完成");
521    }
522
523    #[tokio::test]
524    async fn test_get_user_space_info_nonexistent() {
525        tracing::info!("开始测试获取不存在用户的空间详细信息");
526
527        let bpi = BpiClient::new();
528        let mid = 0; // 不存在的用户ID
529
530        tracing::info!("测试用户ID: {}", mid);
531
532        let resp = bpi.user_space_info(mid).await;
533
534        match &resp {
535            Ok(response) => {
536                tracing::info!("请求返回码: {}", response.code);
537                tracing::info!("错误信息: {}", response.message);
538                // 应该返回 -404 表示用户不存在
539                assert_eq!(response.code, -404);
540            }
541            Err(e) => {
542                tracing::error!("请求失败: {:?}", e);
543            }
544        }
545
546        tracing::info!("测试完成");
547    }
548
549    #[tokio::test]
550    async fn test_get_user_card_info() {
551        tracing::info!("开始测试获取用户名片信息");
552
553        let bpi = BpiClient::new();
554        let mid = 2; // 测试用户ID
555
556        tracing::info!("测试用户ID: {}", mid);
557
558        let resp = bpi.user_card_info(mid, None).await;
559
560        match &resp {
561            Ok(response) => {
562                let data = response.clone().data.unwrap();
563
564                tracing::info!("请求成功,返回码: {}", response.code);
565                tracing::info!("用户昵称: {}", data.card.name);
566                tracing::info!("用户性别: {}", data.card.sex);
567                tracing::info!("用户等级: {}", data.card.level_info.current_level);
568                tracing::info!("是否关注: {}", data.following);
569                tracing::info!("稿件数: {}", data.archive_count);
570                tracing::info!("粉丝数: {}", data.follower);
571                tracing::info!("点赞数: {}", data.like_num);
572                tracing::info!("用户签名: {}", data.card.sign);
573
574                // 认证信息
575                let official = &data.card.official;
576                if official.r#type >= 0 {
577                    tracing::info!("认证类型: {}", official.r#type);
578                    tracing::info!("认证信息: {}", official.title);
579                } else {
580                    tracing::info!("用户未认证");
581                }
582
583                // VIP信息
584                let vip = &data.card.vip;
585                if vip.vip_status > 0 {
586                    tracing::info!("大会员状态: 已开通");
587                    tracing::info!("大会员类型: {}", vip.vip_type);
588                } else {
589                    tracing::info!("大会员状态: 未开通");
590                }
591            }
592            Err(e) => {
593                tracing::error!("请求失败: {:?}", e);
594            }
595        }
596
597        assert!(resp.is_ok());
598        tracing::info!("测试完成");
599    }
600
601    #[tokio::test]
602    async fn test_get_user_card_info_with_photo() {
603        tracing::info!("开始测试获取用户名片信息(包含主页头图)");
604
605        let bpi = BpiClient::new();
606        let mid = 2; // 测试用户ID
607
608        tracing::info!("测试用户ID: {}", mid);
609
610        let resp = bpi.user_card_info_with_photo(mid).await;
611
612        match &resp {
613            Ok(response) => {
614                let data = response.clone().data.unwrap();
615
616                tracing::info!("请求成功,返回码: {}", response.code);
617                tracing::info!("用户昵称: {}", data.card.name);
618
619                // 检查主页头图信息
620                if let Some(space) = &data.card.space {
621                    tracing::info!("主页头图(小): {}", space.s_img);
622                    tracing::info!("主页头图(正常): {}", space.l_img);
623                } else {
624                    tracing::info!("用户没有设置主页头图");
625                }
626
627                // 挂件信息
628                let pendant = &data.card.pendant;
629                if pendant.pid > 0 {
630                    tracing::info!("挂件名称: {}", pendant.name);
631                    tracing::info!("挂件图片: {}", pendant.image);
632                } else {
633                    tracing::info!("用户没有佩戴挂件");
634                }
635
636                // 勋章信息
637                let nameplate = &data.card.nameplate;
638                if nameplate.nid > 0 {
639                    tracing::info!("勋章名称: {}", nameplate.name);
640                    tracing::info!("勋章等级: {}", nameplate.level);
641                } else {
642                    tracing::info!("用户没有佩戴勋章");
643                }
644            }
645            Err(e) => {
646                tracing::error!("请求失败: {:?}", e);
647            }
648        }
649
650        assert!(resp.is_ok());
651        tracing::info!("测试完成");
652    }
653
654    #[tokio::test]
655    async fn test_get_user_card_info_without_photo() {
656        tracing::info!("开始测试获取用户名片信息(不包含主页头图)");
657
658        let bpi = BpiClient::new();
659        let mid = 123456; // 测试用户ID
660
661        tracing::info!("测试用户ID: {}", mid);
662
663        let resp = bpi.user_card_info_without_photo(mid).await;
664
665        match &resp {
666            Ok(response) => {
667                let data = response.clone().data.unwrap();
668
669                tracing::info!("请求成功,返回码: {}", response.code);
670                tracing::info!("用户昵称: {}", data.card.name);
671                tracing::info!("粉丝数: {}", data.card.fans);
672                tracing::info!("关注数: {}", data.card.attention);
673
674                // 应该没有主页头图信息
675                if data.card.space.is_none() {
676                    tracing::info!("正确:没有返回主页头图信息");
677                } else {
678                    tracing::warn!("注意:返回了主页头图信息");
679                }
680            }
681            Err(e) => {
682                tracing::error!("请求失败: {:?}", e);
683            }
684        }
685
686        assert!(resp.is_ok());
687        tracing::info!("测试完成");
688    }
689
690    #[tokio::test]
691    async fn test_get_user_card_info_invalid_user() {
692        tracing::info!("开始测试获取不存在用户的名片信息");
693
694        let bpi = BpiClient::new();
695        let mid = 0; // 不存在的用户ID
696
697        tracing::info!("测试用户ID: {}", mid);
698
699        let resp = bpi.user_card_info(mid, None).await;
700
701        match &resp {
702            Ok(response) => {
703                tracing::info!("请求返回码: {}", response.code);
704                tracing::info!("错误信息: {}", response.message);
705                // 应该返回错误码
706                if response.code != 0 {
707                    tracing::info!("正确:返回了错误码 {}", response.code);
708                }
709            }
710            Err(e) => {
711                tracing::error!("请求失败: {:?}", e);
712            }
713        }
714
715        tracing::info!("测试完成");
716    }
717
718    #[tokio::test]
719    async fn test_get_user_card_info_banned_user() {
720        tracing::info!("开始测试获取被封禁用户的名片信息");
721
722        let bpi = BpiClient::new();
723        let mid = 999999999; // 假设的被封禁用户ID
724
725        tracing::info!("测试用户ID: {}", mid);
726
727        let resp = bpi.user_card_info(mid, None).await;
728
729        match &resp {
730            Ok(response) => {
731                tracing::info!("请求成功,返回码: {}", response.code);
732
733                if response.code == 0 {
734                    let data = response.clone().data.unwrap();
735
736                    let spacesta = data.card.spacesta;
737                    if spacesta == -2 {
738                        tracing::info!("用户状态: 被封禁");
739                    } else if spacesta == 0 {
740                        tracing::info!("用户状态: 正常");
741                    } else {
742                        tracing::info!("用户状态: 未知 ({})", spacesta);
743                    }
744                }
745            }
746            Err(e) => {
747                tracing::error!("请求失败: {:?}", e);
748            }
749        }
750
751        tracing::info!("测试完成");
752    }
753
754    #[tokio::test]
755    async fn test_user_cards_and_infos() {
756        let bpi = BpiClient::new();
757
758        // 测试精简版
759        let cards = bpi.user_cards(&[2, 3]).await.unwrap();
760        tracing::info!("用户卡片: {:?}", cards.data);
761
762        // 测试完整版
763        let infos = bpi.user_infos(&[2, 3]).await.unwrap();
764        tracing::info!("用户详细信息: {:?}", infos.data);
765    }
766}