1use crate::models::{ LevelInfo, Nameplate, Official, OfficialVerify, Pendant, Vip, VipLabel };
5use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
6use serde::{ Deserialize, Serialize };
7
8#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct UserSpaceInfo {
11 pub mid: u64,
13 pub name: String,
15 pub sex: String,
17 pub face: String,
19 pub face_nft: u8,
21 pub face_nft_type: Option<u8>,
23 pub sign: String,
25 pub rank: u32,
27 pub level: u8,
29 pub jointime: u64,
31 pub moral: u64,
33 pub silence: u8,
35 pub coins: f64,
37 pub fans_badge: bool,
39 pub fans_medal: Option<FansMedal>,
41 pub official: Official,
43 pub vip: Vip,
45 pub pendant: Pendant,
47 pub nameplate: Nameplate,
49 pub user_honour_info: UserHonourInfo,
51 pub is_followed: bool,
53 pub top_photo: String,
55 pub theme: serde_json::Value,
57 pub sys_notice: SysNotice,
59 pub live_room: LiveRoom,
61 pub birthday: String,
63 pub school: School,
65 pub profession: Option<Profession>,
67 pub tags: Option<Vec<String>>,
69 pub series: Series,
71 pub is_senior_member: u8,
73 pub mcn_info: Option<serde_json::Value>,
75 pub gaia_res_type: Option<u8>,
77 pub gaia_data: Option<serde_json::Value>,
79 pub is_risk: bool,
81 pub elec: Elec,
83 pub contract: Contract,
85 pub certificate_show: Option<bool>,
87 pub name_render: Option<serde_json::Value>,
89}
90
91#[derive(Debug, Clone, Deserialize, Serialize)]
93pub struct FansMedal {
94 pub show: bool,
96 pub wear: bool,
98 pub medal: Option<Medal>,
100}
101
102#[derive(Debug, Clone, Deserialize, Serialize)]
104pub struct Medal {
105 pub level: u8,
107 pub guard_level: u8,
109 pub medal_name: String,
111}
112
113#[derive(Debug, Clone, Deserialize, Serialize)]
115pub struct UserHonourInfo {
116 pub mid: u64,
118 pub colour: Option<String>,
120 pub tags: Option<Vec<serde_json::Value>>,
122}
123
124#[derive(Debug, Clone, Deserialize, Serialize)]
126pub struct SysNotice {
127 pub id: Option<u32>,
129 pub content: Option<String>,
131 pub url: Option<String>,
133 pub notice_type: Option<u8>,
135 pub icon: Option<String>,
137 pub text_color: Option<String>,
139 pub bg_color: Option<String>,
141}
142
143#[derive(Debug, Clone, Deserialize, Serialize)]
145pub struct LiveRoom {
146 #[serde(rename = "roomStatus")]
148 pub room_status: u8,
149 #[serde(rename = "liveStatus")]
151 pub live_status: u8,
152 pub url: String,
154 pub title: String,
156 pub cover: String,
158 pub watched_show: WatchedShow,
160 pub roomid: u64,
162 #[serde(rename = "roundStatus")]
164 pub round_status: u8,
165 pub broadcast_type: u8,
167}
168
169#[derive(Debug, Clone, Deserialize, Serialize)]
171pub struct WatchedShow {
172 pub switch: bool,
174 pub num: u64,
176 pub text_small: String,
178 pub text_large: String,
180 pub icon: String,
182 pub icon_location: String,
184 pub icon_web: String,
186}
187
188#[derive(Debug, Clone, Deserialize, Serialize)]
190pub struct School {
191 pub name: String,
193}
194
195#[derive(Debug, Clone, Deserialize, Serialize)]
197pub struct Profession {
198 pub name: String,
200 pub department: String,
202 pub title: String,
204 pub is_show: u8,
206}
207
208#[derive(Debug, Clone, Deserialize, Serialize)]
210pub struct Series {
211 pub user_upgrade_status: u8,
213 pub show_upgrade_window: bool,
215}
216
217#[derive(Debug, Clone, Deserialize, Serialize)]
219pub struct Elec {
220 pub show_info: ShowInfo,
222}
223
224#[derive(Debug, Clone, Deserialize, Serialize)]
226pub struct ShowInfo {
227 pub show: bool,
229 pub state: i8,
231 pub title: String,
233 pub icon: String,
235 pub jump_url: String,
237}
238
239#[derive(Debug, Clone, Deserialize, Serialize)]
241pub struct Contract {
242 pub is_display: bool,
244 pub is_follow_display: bool,
246}
247
248#[derive(Debug, Clone, Deserialize, Serialize)]
250pub struct UserCardInfo {
251 pub card: Card,
253 pub following: bool,
255 pub archive_count: u32,
257 pub article_count: u32,
259 pub follower: u32,
261 pub like_num: u32,
263}
264
265#[derive(Debug, Clone, Deserialize, Serialize)]
267pub struct Card {
268 pub mid: String,
270 pub name: String,
272 pub sex: String,
274 pub face: String,
276 #[serde(rename = "DisplayRank")]
278 pub display_rank: String,
279 pub regtime: u64,
281 pub spacesta: i32,
283 pub birthday: String,
285 pub place: String,
287 pub description: String,
289 pub article: u32,
291 pub attentions: Vec<serde_json::Value>,
293 pub fans: u32,
295 pub friend: u32,
297 pub attention: u32,
299 pub sign: String,
301 pub level_info: LevelInfo,
303 pub pendant: Pendant,
305 pub nameplate: Nameplate,
307 #[serde(rename = "Official")]
309 pub official: Official,
310 pub official_verify: OfficialVerify,
312 pub vip: Vip,
314 pub space: Option<Space>,
316}
317
318#[derive(Debug, Clone, Deserialize, Serialize)]
320pub struct Space {
321 pub s_img: String,
323 pub l_img: String,
325}
326
327#[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#[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#[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#[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 pub async fn user_space_info(&self, mid: u64) -> Result<BpiResponse<UserSpaceInfo>, BpiError> {
390 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(¶ms)
398 .send_bpi("获取用户空间详细信息").await
399 }
400
401 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 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(¶ms)
427 .send_bpi("获取用户名片信息").await
428 }
429
430 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 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 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 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; 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; 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 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; 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 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 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; 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 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 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 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; 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 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; 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 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; 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 let cards = bpi.user_cards(&[2, 3]).await.unwrap();
760 tracing::info!("用户卡片: {:?}", cards.data);
761
762 let infos = bpi.user_infos(&[2, 3]).await.unwrap();
764 tracing::info!("用户详细信息: {:?}", infos.data);
765 }
766}