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