bpi_rs/user/
info.rs

1//! B站用户信息相关接口
2//!
3//! 文档: 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    /// 文档: https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user
386    ///
387    /// - `mid`: 用户 UID
388    pub async fn user_space_info(&self, mid: u64) -> Result<BpiResponse<UserSpaceInfo>, BpiError> {
389        // 构建查询参数
390        let params = vec![("mid", mid.to_string())];
391
392        let params = self.get_wbi_sign2(params).await?;
393
394        self.get("https://api.bilibili.com/x/space/wbi/acc/info")
395            .query(&params)
396            .send_bpi("获取用户空间详细信息")
397            .await
398    }
399
400    /// 获取用户名片信息
401    ///
402    /// 文档: https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user
403    ///
404    /// 参数
405    ///
406    /// | 名称 | 类型 | 说明 |
407    /// | ---- | ---- | ---- |
408    /// | `mid` | u64 | 用户 UID |
409    /// | `photo` | Option<bool> | 是否包含主页头图 |
410    pub async fn user_card_info(
411        &self,
412        mid: u64,
413        photo: Option<bool>,
414    ) -> Result<BpiResponse<UserCardInfo>, BpiError> {
415        let mut params = vec![("mid", mid.to_string())];
416
417        // 如果指定了photo参数,则添加到请求参数中
418        if let Some(photo_value) = photo {
419            params.push(("photo", photo_value.to_string()));
420        }
421
422        self.get("https://api.bilibili.com/x/web-interface/card")
423            .query(&params)
424            .send_bpi("获取用户名片信息")
425            .await
426    }
427
428    /// 获取用户名片信息(包含主页头图)
429    ///
430    /// 文档: https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user
431    ///
432    /// - `mid`: 用户 UID
433    pub async fn user_card_info_with_photo(
434        &self,
435        mid: u64,
436    ) -> Result<BpiResponse<UserCardInfo>, BpiError> {
437        self.user_card_info(mid, Some(true)).await
438    }
439
440    /// 获取用户名片信息(不包含主页头图)
441    ///
442    /// 文档: https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user
443    ///
444    /// - `mid`: 用户 UID
445    pub async fn user_card_info_without_photo(
446        &self,
447        mid: u64,
448    ) -> Result<BpiResponse<UserCardInfo>, BpiError> {
449        self.user_card_info(mid, Some(false)).await
450    }
451
452    /// 批量获取用户卡片(精简信息)
453
454    pub async fn user_cards(&self, mids: &[u64]) -> Result<BpiResponse<Vec<UserCard>>, BpiError> {
455        let mids_str = mids
456            .iter()
457            .map(|m| m.to_string())
458            .collect::<Vec<_>>()
459            .join(",");
460
461        self.get("https://api.vc.bilibili.com/account/v1/user/cards")
462            .query(&[("uids", mids_str)])
463            .send_bpi("批量获取用户卡片")
464            .await
465    }
466
467    /// 批量获取用户详细信息(带大会员/认证信息)
468
469    pub async fn user_infos(&self, mids: &[u64]) -> Result<BpiResponse<Vec<UserInfo>>, BpiError> {
470        let mids_str = mids
471            .iter()
472            .map(|m| m.to_string())
473            .collect::<Vec<_>>()
474            .join(",");
475
476        self.get("https://api.vc.bilibili.com/x/im/user_infos")
477            .query(&[("uids", mids_str)])
478            .send_bpi("批量获取用户详细信息")
479            .await
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[tokio::test]
488    async fn test_get_user_space_info() {
489        tracing::info!("开始测试获取用户空间详细信息");
490
491        let bpi = BpiClient::new();
492        let mid = 2; // 测试用户ID
493
494        tracing::info!("测试用户ID: {}", mid);
495
496        let resp = bpi.user_space_info(mid).await;
497
498        match &resp {
499            Ok(response) => {
500                let data = response.clone().data.unwrap();
501                tracing::info!("请求成功,返回码: {}", response.code);
502                tracing::info!("用户昵称: {}", data.name);
503                tracing::info!("用户等级: {}", data.level);
504                tracing::info!("是否为会员: {}", data.vip.vip_type > 0);
505                tracing::info!("粉丝数量: {}", data.fans_medal.as_ref().map_or(0, |_| 1));
506            }
507            Err(e) => {
508                tracing::error!("请求失败: {:?}", e);
509            }
510        }
511
512        assert!(resp.is_ok());
513        tracing::info!("测试完成");
514    }
515
516    #[tokio::test]
517    async fn test_get_user_space_info_nonexistent() {
518        tracing::info!("开始测试获取不存在用户的空间详细信息");
519
520        let bpi = BpiClient::new();
521        let mid = 0; // 不存在的用户ID
522
523        tracing::info!("测试用户ID: {}", mid);
524
525        let resp = bpi.user_space_info(mid).await;
526
527        match &resp {
528            Ok(response) => {
529                tracing::info!("请求返回码: {}", response.code);
530                tracing::info!("错误信息: {}", response.message);
531                // 应该返回 -404 表示用户不存在
532                assert_eq!(response.code, -404);
533            }
534            Err(e) => {
535                tracing::error!("请求失败: {:?}", e);
536            }
537        }
538
539        tracing::info!("测试完成");
540    }
541
542    #[tokio::test]
543    async fn test_get_user_card_info() {
544        tracing::info!("开始测试获取用户名片信息");
545
546        let bpi = BpiClient::new();
547        let mid = 2; // 测试用户ID
548
549        tracing::info!("测试用户ID: {}", mid);
550
551        let resp = bpi.user_card_info(mid, None).await;
552
553        match &resp {
554            Ok(response) => {
555                let data = response.clone().data.unwrap();
556
557                tracing::info!("请求成功,返回码: {}", response.code);
558                tracing::info!("用户昵称: {}", data.card.name);
559                tracing::info!("用户性别: {}", data.card.sex);
560                tracing::info!("用户等级: {}", data.card.level_info.current_level);
561                tracing::info!("是否关注: {}", data.following);
562                tracing::info!("稿件数: {}", data.archive_count);
563                tracing::info!("粉丝数: {}", data.follower);
564                tracing::info!("点赞数: {}", data.like_num);
565                tracing::info!("用户签名: {}", data.card.sign);
566
567                // 认证信息
568                let official = &data.card.official;
569                if official.r#type >= 0 {
570                    tracing::info!("认证类型: {}", official.r#type);
571                    tracing::info!("认证信息: {}", official.title);
572                } else {
573                    tracing::info!("用户未认证");
574                }
575
576                // VIP信息
577                let vip = &data.card.vip;
578                if vip.vip_status > 0 {
579                    tracing::info!("大会员状态: 已开通");
580                    tracing::info!("大会员类型: {}", vip.vip_type);
581                } else {
582                    tracing::info!("大会员状态: 未开通");
583                }
584            }
585            Err(e) => {
586                tracing::error!("请求失败: {:?}", e);
587            }
588        }
589
590        assert!(resp.is_ok());
591        tracing::info!("测试完成");
592    }
593
594    #[tokio::test]
595    async fn test_get_user_card_info_with_photo() {
596        tracing::info!("开始测试获取用户名片信息(包含主页头图)");
597
598        let bpi = BpiClient::new();
599        let mid = 2; // 测试用户ID
600
601        tracing::info!("测试用户ID: {}", mid);
602
603        let resp = bpi.user_card_info_with_photo(mid).await;
604
605        match &resp {
606            Ok(response) => {
607                let data = response.clone().data.unwrap();
608
609                tracing::info!("请求成功,返回码: {}", response.code);
610                tracing::info!("用户昵称: {}", data.card.name);
611
612                // 检查主页头图信息
613                if let Some(space) = &data.card.space {
614                    tracing::info!("主页头图(小): {}", space.s_img);
615                    tracing::info!("主页头图(正常): {}", space.l_img);
616                } else {
617                    tracing::info!("用户没有设置主页头图");
618                }
619
620                // 挂件信息
621                let pendant = &data.card.pendant;
622                if pendant.pid > 0 {
623                    tracing::info!("挂件名称: {}", pendant.name);
624                    tracing::info!("挂件图片: {}", pendant.image);
625                } else {
626                    tracing::info!("用户没有佩戴挂件");
627                }
628
629                // 勋章信息
630                let nameplate = &data.card.nameplate;
631                if nameplate.nid > 0 {
632                    tracing::info!("勋章名称: {}", nameplate.name);
633                    tracing::info!("勋章等级: {}", nameplate.level);
634                } else {
635                    tracing::info!("用户没有佩戴勋章");
636                }
637            }
638            Err(e) => {
639                tracing::error!("请求失败: {:?}", e);
640            }
641        }
642
643        assert!(resp.is_ok());
644        tracing::info!("测试完成");
645    }
646
647    #[tokio::test]
648    async fn test_get_user_card_info_without_photo() {
649        tracing::info!("开始测试获取用户名片信息(不包含主页头图)");
650
651        let bpi = BpiClient::new();
652        let mid = 123456; // 测试用户ID
653
654        tracing::info!("测试用户ID: {}", mid);
655
656        let resp = bpi.user_card_info_without_photo(mid).await;
657
658        match &resp {
659            Ok(response) => {
660                let data = response.clone().data.unwrap();
661
662                tracing::info!("请求成功,返回码: {}", response.code);
663                tracing::info!("用户昵称: {}", data.card.name);
664                tracing::info!("粉丝数: {}", data.card.fans);
665                tracing::info!("关注数: {}", data.card.attention);
666
667                // 应该没有主页头图信息
668                if data.card.space.is_none() {
669                    tracing::info!("正确:没有返回主页头图信息");
670                } else {
671                    tracing::warn!("注意:返回了主页头图信息");
672                }
673            }
674            Err(e) => {
675                tracing::error!("请求失败: {:?}", e);
676            }
677        }
678
679        assert!(resp.is_ok());
680        tracing::info!("测试完成");
681    }
682
683    #[tokio::test]
684    async fn test_get_user_card_info_invalid_user() {
685        tracing::info!("开始测试获取不存在用户的名片信息");
686
687        let bpi = BpiClient::new();
688        let mid = 0; // 不存在的用户ID
689
690        tracing::info!("测试用户ID: {}", mid);
691
692        let resp = bpi.user_card_info(mid, None).await;
693
694        match &resp {
695            Ok(response) => {
696                tracing::info!("请求返回码: {}", response.code);
697                tracing::info!("错误信息: {}", response.message);
698                // 应该返回错误码
699                if response.code != 0 {
700                    tracing::info!("正确:返回了错误码 {}", response.code);
701                }
702            }
703            Err(e) => {
704                tracing::error!("请求失败: {:?}", e);
705            }
706        }
707
708        tracing::info!("测试完成");
709    }
710
711    #[tokio::test]
712    async fn test_get_user_card_info_banned_user() {
713        tracing::info!("开始测试获取被封禁用户的名片信息");
714
715        let bpi = BpiClient::new();
716        let mid = 999999999; // 假设的被封禁用户ID
717
718        tracing::info!("测试用户ID: {}", mid);
719
720        let resp = bpi.user_card_info(mid, None).await;
721
722        match &resp {
723            Ok(response) => {
724                tracing::info!("请求成功,返回码: {}", response.code);
725
726                if response.code == 0 {
727                    let data = response.clone().data.unwrap();
728
729                    let spacesta = data.card.spacesta;
730                    if spacesta == -2 {
731                        tracing::info!("用户状态: 被封禁");
732                    } else if spacesta == 0 {
733                        tracing::info!("用户状态: 正常");
734                    } else {
735                        tracing::info!("用户状态: 未知 ({})", spacesta);
736                    }
737                }
738            }
739            Err(e) => {
740                tracing::error!("请求失败: {:?}", e);
741            }
742        }
743
744        tracing::info!("测试完成");
745    }
746
747    #[tokio::test]
748    async fn test_user_cards_and_infos() {
749        let bpi = BpiClient::new();
750
751        // 测试精简版
752        let cards = bpi.user_cards(&[2, 3]).await.unwrap();
753        tracing::info!("用户卡片: {:?}", cards.data);
754
755        // 测试完整版
756        let infos = bpi.user_infos(&[2, 3]).await.unwrap();
757        tracing::info!("用户详细信息: {:?}", infos.data);
758    }
759}