Skip to main content

bpi_rs/electric/
bcoin.rs

1use crate::BilibiliRequest;
2use crate::BpiError;
3use crate::BpiResult;
4use crate::electric::ElectricClient;
5use serde::{Deserialize, Serialize};
6
7const BCOIN_QUICK_PAY_ENDPOINT: &str =
8    "https://api.bilibili.com/x/ugcpay/web/v2/trade/elec/pay/quick";
9
10#[derive(Debug, Serialize, Clone, Deserialize)]
11pub struct BcoinQuickPayData {
12    /// 本用户 mid
13    pub mid: i64,
14    /// 目标用户 mid
15    pub up_mid: i64,
16    /// 留言 token(用于添加充电留言)
17    pub order_no: String,
18    /// 充电贝壳数(字符串)
19    pub bp_num: String,
20    /// 获得经验数
21    pub exp: u32,
22    /// 返回结果
23    /// - `4`:成功
24    /// - `-2`:低于 20 电池下限
25    /// - `-4`:B 币不足
26    pub status: i32,
27    /// 错误信息(默认为空)
28    pub msg: String,
29}
30
31/// Parameters for B-coin quick electric charging.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct BcoinQuickPayParams {
34    bp_num: i32,
35    is_bp_remains_prior: bool,
36    up_mid: i64,
37    otype: String,
38    oid: i64,
39}
40
41impl BcoinQuickPayParams {
42    pub fn new(
43        bp_num: i32,
44        is_bp_remains_prior: bool,
45        up_mid: i64,
46        otype: impl Into<String>,
47        oid: i64,
48    ) -> BpiResult<Self> {
49        if !(2..=9999).contains(&bp_num) {
50            return Err(BpiError::invalid_parameter(
51                "bp_num",
52                "value must be between 2 and 9999",
53            ));
54        }
55        if up_mid <= 0 {
56            return Err(BpiError::invalid_parameter("up_mid", "id must be positive"));
57        }
58        if oid <= 0 {
59            return Err(BpiError::invalid_parameter("oid", "id must be positive"));
60        }
61
62        let otype = otype.into();
63        if !matches!(otype.as_str(), "up" | "archive") {
64            return Err(BpiError::invalid_parameter(
65                "otype",
66                "value must be 'up' or 'archive'",
67            ));
68        }
69
70        Ok(Self {
71            bp_num,
72            is_bp_remains_prior,
73            up_mid,
74            otype,
75            oid,
76        })
77    }
78
79    fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
80        vec![
81            ("bp_num", self.bp_num.to_string()),
82            ("is_bp_remains_prior", self.is_bp_remains_prior.to_string()),
83            ("up_mid", self.up_mid.to_string()),
84            ("otype", self.otype.clone()),
85            ("oid", self.oid.to_string()),
86            ("csrf", csrf.to_string()),
87        ]
88    }
89}
90
91impl<'a> ElectricClient<'a> {
92    /// Performs B-coin quick electric charging and returns the canonical payload result.
93    pub async fn bcoin_quick_pay(
94        &self,
95        params: BcoinQuickPayParams,
96    ) -> BpiResult<BcoinQuickPayData> {
97        let csrf = self.client.csrf()?;
98
99        self.client
100            .post(BCOIN_QUICK_PAY_ENDPOINT)
101            .form(&params.form_pairs(&csrf))
102            .send_bpi_payload("electric.bcoin.quick_pay")
103            .await
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn bcoin_quick_pay_params_rejects_invalid_charge_amount() {
113        let err = BcoinQuickPayParams::new(1, true, 42, "up", 42).unwrap_err();
114
115        assert!(matches!(
116            err,
117            BpiError::InvalidParameter {
118                field: "bp_num",
119                ..
120            }
121        ));
122    }
123}