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