如何将使用Diesel的多个功能合并为一个抽象? [英] How do I combine multiple functions using Diesel into one through abstraction?

查看:188
本文介绍了如何将使用Diesel的多个功能合并为一个抽象?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有以下两个函数:

$ $ p $ pub fn get_most_recent_eth_entry(conn:& SqliteConnection) - >结果< i32,错误> {
let res = types :: ethereum :: table
.order(types :: ethereum :: time.desc())
.limit(1)
.load: :其中::类型ÐRecord GT;(&安培; * conn);在
match res {
Ok(x)=> {
if x.len()> 0 {
Ok(x.get(0).unwrap()。time)
} else {
Ok(0)
}
}
Err(err)=> Err(format_err!(Error here!{:?},err)),
}
}

pub fn get_most_recent_btc_entry(conn:& SqliteConnection) - >结果< i32,错误> {
let res = types :: bitcoin :: table
.order(types :: bitcoin :: time.desc())
.limit(1)
.load: :其中::类型&BTCRecord GT;(&安培; * conn);在
match res {
Ok(x)=> {
if x.len()> 0 {
Ok(x.get(0).unwrap()。time)
} else {
Ok(0)
}
}
Err(err)=> Err(format_err!(Error here!{:?},err))


$ c
$ b

我想将两者合并为一个函数。我尝试了几种不同的方式,但是


  1. 我对Rust很新颖

  2. Diesel有奇怪的类型(或至少是这种感觉)

有什么方法可以合并这两个函数字段 types :: ethereum ETHRecord 合并为一个统一函数 get_most_recent_entry
$ pre
$ b pre $ > #[derive(Insertable,Queryable,Debug)]
#[table_name =bitcoin]
pub struct BTCRecord {
pub time:i32,
pub market_cap:f32,
pub price_btc:f32,
pub price_usd:f32,
pub vol_usd:f32,
}



 类型:: ethereum :: time是`database :: types :: __ diesel_infer_schema :: infer_bitcoin :: bitcoin :: columns :: time` 



 `types :: ethereum :: table`的类型是
`database :: types :: __ diesel_infer_schema: :infer_bitcoin :: bitcoin :: table`


解决方案

让我们从 MCVE 开始。这是专业程序员在试图理解问题时使用的工具。它去除了多余的细节,但为任何人提供了足够的细节,以便能够拾取并重现这种情况。比较您没有提供的代码。每个缺失的部分是回答者必须猜测的东西,以及您的时间和生成时间。

  [依赖关系] 
diesel = {version =1.0.0-beta,features = [sqlite]}





 #[macro_use] 
extern crate diesel;

使用diesel :: prelude :: *;
使用diesel :: SqliteConnection;

mod类型{
表! {
比特币(时间){
时间 - > Int4,
}
}

表! {
ethereum(time){
time - > Int4,
}
}

#[derive(Insertable,Queryable,Debug)]
#[table_name =bitcoin]
pub struct BtcRecord {b $ b pub时间:i32,
}

#[派生(可插入,可查询,调试)]
#[table_name =ethereum]
pub struct EthRecord {
pub time:i32,
}
}

pub fn get_most_recent_eth_entry(conn:& SqliteConnection) - >结果< i32,String> {
let res = types :: ethereum :: table
.order(types :: ethereum :: time.desc())
.limit(1)
.load: :其中::类型&EthRecord GT;(&安培; * conn);在
match res {
Ok(x)=> {
if x.len()> 0 {
Ok(x.get(0).unwrap()。time)
} else {
Ok(0)
}
}
Err(err)=> Err(格式!(Error here!{:?},err)),
}
}

pub fn get_most_recent_btc_entry(conn:& SqliteConnection) - >结果< i32,String> {
let res = types :: bitcoin :: table
.order(types :: bitcoin :: time.desc())
.limit(1)
.load: :其中::类型&BtcRecord GT;(&安培; * conn);在
match res {
Ok(x)=> {
if x.len()> 0 {
Ok(x.get(0).unwrap()。time)
} else {
Ok(0)
}
}
Err(err)=> Err(format!(Error here!{:?},err))


$ c
$ b

接下来,在两段代码之间执行差异以确定差异。你说过:


只有字段 types :: ethereum ETHRecord


然而,它们在四个地方有所不同。仅仅因为某些东西具有相同的前缀并不意味着你可以传递前缀。模块不是运行时存在于Rust中的概念:

pre $ get_most_recent_eth_entry结果< i32,String> {
// ^^^^^^^^^^^^^^^^^^^^^^^^^
让res = types :: ethereum :: table
// ^^^^^^^^
.order(types :: ethereum :: time.desc())
// ^^^^^^^^
.limit( 1)
.load ::< types :: EthRecord>(& * conn);
// ^^^^^^^^^

让我们复制并粘贴其中一个函数并将所有唯一值替换为傻瓜:

pre $ pub fn get_most_recent_entry<'a,Tbl,Expr,Record>(
conn:& SqliteConnection,
表:Tbl,
时间:Expr,
) - >结果< i32,String> {
let res = table
.order(time.desc())
.limit(1)
.load ::< Record>(& * conn);
// ...

接下来的部分并不漂亮。基本上,编译器会告诉你一个接一个没有被满足的特性。您只需将每个错误复制回代码以设置所有约束:

  pub fn get_most_recent_entry<'a,Tbl ,Expr,Record>(
conn:& SqliteConnection,
table:Tbl,
time:Expr,
) - >结果< i32,String>
其中
Expr:diesel :: ExpressionMethods,
Tbl:OrderDsl< Expr>> ;,
< Tbl as OrderDsl< Expr>>>: :输出:LimitDsl,
<< Tbl as OrderDsl< Expr>>> :: Output as LimitDsl> :: Output:RunQueryDsl< SqliteConnection> + Query,
Sqlite:HasSqlType<<<<< Tbl作为OrderDsl< Expr>>> :: Output as LimitDsl> :: Output as Query> :: SqlType> ;,
<<< Tbl as OrderDsl< Expr>>>> :: Output as LimitDsl> :: Output:QueryFragment< Sqlite>,
<< Tbl as OrderDsl< Expr>>> ; :: Output as LimitDsl>> :: Output:QueryId,
Record:Queryable<<<< Tbl as OrderDsl< Expr>>> :: Output as LimitDsl> :: Output as Query> ; :: SqlType,Sqlite> ;,

这会导致新的错误:

错误[E0609]:类型'& Record`没有字段`时间'
- > src / main.rs:64:38
|
64 |好的(x.get(0).unwrap()。time)
| ^^^^

你不能假设泛型的任何字段,我们需要一个特征:

  pub trait时间{
fn time(& self) - > 123-132;

您:


  • 实现两个具体类型的特征

  • 添加此特征绑定到 Record



















  • 所有在一起:

     #[macro_use] 
    extern crate diesel;

    使用diesel :: prelude :: *;
    使用diesel :: SqliteConnection;

    mod类型{
    表! {
    比特币(时间){
    时间 - > Int4,
    }
    }

    表! {
    ethereum(time){
    time - > Int4,
    }
    }

    #[derive(Insertable,Queryable,Debug)]
    #[table_name =bitcoin]
    pub struct BtcRecord {b $ b pub时间:i32,
    }

    #[派生(可插入,可查询,调试)]
    #[table_name =ethereum]
    pub struct EthRecord {
    pub time:i32,
    }
    }

    pub trait时间{
    fn time(& self) - > 123-132;
    }

    impl类型的时间:: EthRecord {
    fn time(& self) - > i32 {
    self.time
    }
    }

    impl类型的时间:: BtcRecord {
    fn time(& self) - > i32 {
    self.time
    }
    }

    使用diesel :: sqlite :: Sqlite;
    使用diesel :: types :: HasSqlType;
    使用diesel :: query_dsl :: methods :: {LimitDsl,OrderDsl};
    使用diesel :: expression :: operators :: Desc;
    使用diesel :: query_builder :: {Query,QueryFragment,QueryId};
    使用diesel :: Queryable;

    pub fn get_most_recent_entry<'a,Tbl,Expr,Record>(
    conn:& SqliteConnection,
    table:Tbl,
    time:Expr,
    ) - >结果< i32,String>
    其中
    Expr:diesel :: ExpressionMethods,
    Tbl:OrderDsl< Expr>> ;,
    < Tbl as OrderDsl< Expr>>>: :输出:LimitDsl,
    << Tbl as OrderDsl< Expr>>> :: Output as LimitDsl> :: Output:RunQueryDsl< SqliteConnection> + Query,
    Sqlite:HasSqlType<<<<< Tbl作为OrderDsl< Expr>>> :: Output as LimitDsl> :: Output as Query> :: SqlType> ;,
    <<< Tbl as OrderDsl< Expr>>>> :: Output as LimitDsl> :: Output:QueryFragment< Sqlite>,
    << Tbl as OrderDsl< Expr>>> ; :: Output as LimitDsl>> :: Output:QueryId,
    Record:Queryable<<<< Tbl as OrderDsl< Expr>>> :: Output as LimitDsl> :: Output as Query> ; :: SqlType,Sqlite> + Time,
    {
    let res = table.order(time.desc())。limit(1).load ::< Record>(& * conn);
    match res {
    Ok(x)=> {
    if x.len()> 0 {
    Ok(x.get(0).unwrap()。time())
    } else {
    Ok(0)
    }
    }
    Err(err)=> Err(格式!(Error here!{:?},err)),
    }
    }

    pub fn get_most_recent_eth_entry(conn:& SqliteConnection) - >结果< i32,String> {
    get_most_recent_entry ::< _,_,types :: EthRecord>(
    conn,
    types :: ethereum :: table,
    types :: ethereum :: time,

    }

    pub fn get_most_recent_btc_entry(conn:& SqliteConnection) - >结果< i32,String> {
    get_most_recent_entry ::< _,_,types :: BtcRecord>(
    conn,
    types :: bitcoin :: table,
    types :: bitcoin :: time,

    }






    接下来的步骤需要深入探索Diesel。 helper_types 模块包含允许我们缩短界限的类型别名:

      pub fn get_most_recent_entry<'a,Tbl,Expr,Record>(
    conn:& SqliteConnection,
    表:Tbl,
    时间:Expr,
    ) - >结果< i32,String>
    其中
    Expr:diesel :: ExpressionMethods,
    Tbl:OrderDsl > ;,
    Order< Tbl,Desc< Expr>> ;: LimitDsl,
    Limit< Order< Tbl,Desc< Expr>>> ;: RunQueryDsl< SqliteConnection>
    +查询
    + QueryFragment< Sqlite>
    + QueryId,
    Sqlite:HasSqlType<<< Limit< Order< Tbl,Desc< Expr>>>作为Query> :: SqlType> ;,
    Record:Queryable<<< Order< Tbl,Desc< Expr>>>作为查询> :: SqlType,Sqlite> + Time,

    还有一个特性,它包含了所有的 Query * -related subtraits: loadquery子 。使用它,我们可以将它缩减为:

      pub fn get_most_recent_entry<'a,Tbl,Expr,Record>(
    conn:& SqliteConnection,
    table:Tbl,
    time:Expr,
    ) - >结果< i32,String>
    其中
    Expr:diesel :: ExpressionMethods,
    Tbl:OrderDsl > ;,
    Order< Tbl,Desc< Expr>> ;: LimitDsl,
    Limit< Order< Tbl,Desc< Expr>>>:LoadQuery< SqliteConnection,Record> ;,
    记录:时间,






    然后,您可以使用Diesel的第一函数和结果 s combinators缩短整个函数:

      use diesel :: expression :: operators: :说明; 
    使用diesel :: helper_types :: {Limit,Order};
    使用diesel :: query_dsl :: methods :: {LimitDsl,OrderDsl};
    使用diesel :: query_dsl :: LoadQuery;

    pub fn get_most_recent_entry<'a,Tbl,Expr,Record>(
    conn:& SqliteConnection,
    table:Tbl,
    time:Expr,
    ) - >结果< i32,String>
    其中
    Expr:diesel :: ExpressionMethods,
    Tbl:OrderDsl > ;,
    Order< Tbl,Desc< Expr>> ;: LoadQuery< SqliteConnection,记录和GT; + LimitDsl,
    Limit< Order< Tbl,Desc< Expr>>>:LoadQuery< SqliteConnection,Record> ;,
    记录:时间,
    {

    .order(time.desc())
    .first(conn)
    .optional()
    .map(| x | x.map_or(0,| x | x.time )))
    .map_err(| e | format!(Error here!{:?},e))
    }


    I have the following two functions:

    pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, Error> {
        let res = types::ethereum::table
            .order(types::ethereum::time.desc())
            .limit(1)
            .load::<types::ETHRecord>(&*conn);
        match res {
            Ok(x) => {
                if x.len() > 0 {
                    Ok(x.get(0).unwrap().time)
                } else {
                    Ok(0)
                }
            }
            Err(err) => Err(format_err!("Error here! {:?}", err)),
        }
    }
    
    pub fn get_most_recent_btc_entry(conn: &SqliteConnection) -> Result<i32, Error> {
        let res = types::bitcoin::table
            .order(types::bitcoin::time.desc())
            .limit(1)
            .load::<types::BTCRecord>(&*conn);
        match res {
            Ok(x) => {
                if x.len() > 0 {
                    Ok(x.get(0).unwrap().time)
                } else {
                    Ok(0)
                }
            }
            Err(err) => Err(format_err!("Error here! {:?}", err)),
        }
    }
    

    I want to combine both into one function. I have tried out a few different ways, but

    1. I am quite new to Rust
    2. Diesel has weird types (or at least that's what it feels like)

    What are some ways to merge these two functions (which differ in only the fields types::ethereum and ETHRecord into one unified function get_most_recent_entry?

    These are my database struct definitions (the SQL schemas are equivalently defined):

    #[derive(Insertable, Queryable, Debug)]
    #[table_name="bitcoin"]
    pub struct BTCRecord {
        pub time: i32,
        pub market_cap: f32,
        pub price_btc: f32,
        pub price_usd: f32,
        pub vol_usd: f32,
    }
    

    and the type of

    `types::ethereum::time` is `database::types::__diesel_infer_schema::infer_bitcoin::bitcoin::columns::time`
    

    and the type of

    `types::ethereum::table` is
    `database::types::__diesel_infer_schema::infer_bitcoin::bitcoin::table`
    

    解决方案

    First, let's start with a MCVE. This is a tool that professional programmers use when trying to understand a problem. It removes extraneous detail but provides enough detail for anyone to be able to pick it up and reproduce the situation. Compare how much code is present here that you didn't provide. Each missing piece is something that an answerer has to guess on as well as your time and their time generating.

    [dependencies]
    diesel = { version = "1.0.0-beta", features = ["sqlite"] }
    

    #[macro_use]
    extern crate diesel;
    
    use diesel::prelude::*;
    use diesel::SqliteConnection;
    
    mod types {
        table! {
            bitcoin (time) {
                time -> Int4,
            }
        }
    
        table! {
            ethereum (time) {
                time -> Int4,
            }
        }
    
        #[derive(Insertable, Queryable, Debug)]
        #[table_name="bitcoin"]
        pub struct BtcRecord {
            pub time: i32,
        }
    
        #[derive(Insertable, Queryable, Debug)]
        #[table_name="ethereum"]
        pub struct EthRecord {
            pub time: i32,
        }
    }
    
    pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, String> {
        let res = types::ethereum::table
            .order(types::ethereum::time.desc())
            .limit(1)
            .load::<types::EthRecord>(&*conn);
        match res {
            Ok(x) => {
                if x.len() > 0 {
                    Ok(x.get(0).unwrap().time)
                } else {
                    Ok(0)
                }
            }
            Err(err) => Err(format!("Error here! {:?}", err)),
        }
    }
    
    pub fn get_most_recent_btc_entry(conn: &SqliteConnection) -> Result<i32, String> {
        let res = types::bitcoin::table
            .order(types::bitcoin::time.desc())
            .limit(1)
            .load::<types::BtcRecord>(&*conn);
        match res {
            Ok(x) => {
                if x.len() > 0 {
                    Ok(x.get(0).unwrap().time)
                } else {
                    Ok(0)
                }
            }
            Err(err) => Err(format!("Error here! {:?}", err)),
        }
    }
    

    Next, perform a diff between the two pieces of code to identify the differences. You stated:

    which differ in only the fields types::ethereum and ETHRecord

    However, they differ in four locations. Just because something has the same prefix doesn't mean you can pass that prefix around. Modules aren't concepts that exist at runtime in Rust:

    pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, String> {
        // ^^^^^^^^^^^^^^^^^^^^^^^^^
        let res = types::ethereum::table
        //               ^^^^^^^^ 
            .order(types::ethereum::time.desc())
        //                ^^^^^^^^ 
            .limit(1)
            .load::<types::EthRecord>(&*conn);
        //                 ^^^^^^^^^
    

    Let's copy and paste one of the functions and replace all the unique values with dummies:

    pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
        conn: &SqliteConnection,
        table: Tbl,
        time: Expr,
    ) -> Result<i32, String> {
        let res = table
            .order(time.desc())
            .limit(1)
            .load::<Record>(&*conn);
        // ...
    

    This next part isn't pretty. Basically, the compiler will tell you every trait bound that isn't met, one-by-one. You "just" copy each error back to the code to set up all the constraints:

    pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
        conn: &SqliteConnection,
        table: Tbl,
        time: Expr,
    ) -> Result<i32, String>
    where
        Expr: diesel::ExpressionMethods,
        Tbl: OrderDsl<Desc<Expr>>,
        <Tbl as OrderDsl<Desc<Expr>>>::Output: LimitDsl,
        <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: RunQueryDsl<SqliteConnection> + Query,
        Sqlite: HasSqlType<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType>,
        <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryFragment<Sqlite>,
        <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryId,
        Record: Queryable<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType, Sqlite>,
    

    This leads to the new error:

    error[E0609]: no field `time` on type `&Record`
      --> src/main.rs:64:38
       |
    64 |                 Ok(x.get(0).unwrap().time)
       |                                      ^^^^
    

    You cannot assume any fields on a generic type, we need a trait:

    pub trait Time {
        fn time(&self) -> i32;
    }
    

    You:

    • implement the trait for both concrete types
    • add this trait bound to Record
    • call .time() in the method

    All together:

    #[macro_use]
    extern crate diesel;
    
    use diesel::prelude::*;
    use diesel::SqliteConnection;
    
    mod types {
        table! {
            bitcoin (time) {
                time -> Int4,
            }
        }
    
        table! {
            ethereum (time) {
                time -> Int4,
            }
        }
    
        #[derive(Insertable, Queryable, Debug)]
        #[table_name = "bitcoin"]
        pub struct BtcRecord {
            pub time: i32,
        }
    
        #[derive(Insertable, Queryable, Debug)]
        #[table_name = "ethereum"]
        pub struct EthRecord {
            pub time: i32,
        }
    }
    
    pub trait Time {
        fn time(&self) -> i32;
    }
    
    impl Time for types::EthRecord {
        fn time(&self) -> i32 {
            self.time
        }
    }
    
    impl Time for types::BtcRecord {
        fn time(&self) -> i32 {
            self.time
        }
    }
    
    use diesel::sqlite::Sqlite;
    use diesel::types::HasSqlType;
    use diesel::query_dsl::methods::{LimitDsl, OrderDsl};
    use diesel::expression::operators::Desc;
    use diesel::query_builder::{Query, QueryFragment, QueryId};
    use diesel::Queryable;
    
    pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
        conn: &SqliteConnection,
        table: Tbl,
        time: Expr,
    ) -> Result<i32, String>
    where
        Expr: diesel::ExpressionMethods,
        Tbl: OrderDsl<Desc<Expr>>,
        <Tbl as OrderDsl<Desc<Expr>>>::Output: LimitDsl,
        <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: RunQueryDsl<SqliteConnection> + Query,
        Sqlite: HasSqlType<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType>,
        <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryFragment<Sqlite>,
        <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryId,
        Record: Queryable<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType, Sqlite> + Time,
    {
        let res = table.order(time.desc()).limit(1).load::<Record>(&*conn);
        match res {
            Ok(x) => {
                if x.len() > 0 {
                    Ok(x.get(0).unwrap().time())
                } else {
                    Ok(0)
                }
            }
            Err(err) => Err(format!("Error here! {:?}", err)),
        }
    }
    
    pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, String> {
        get_most_recent_entry::<_, _, types::EthRecord>(
            conn,
            types::ethereum::table,
            types::ethereum::time,
        )
    }
    
    pub fn get_most_recent_btc_entry(conn: &SqliteConnection) -> Result<i32, String> {
        get_most_recent_entry::<_, _, types::BtcRecord>(
            conn,
            types::bitcoin::table,
            types::bitcoin::time,
        )
    }
    


    The next steps require a deeper dive into Diesel. The helper_types module contains type aliases that allow us to shorten the bounds:

    pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
        conn: &SqliteConnection,
        table: Tbl,
        time: Expr,
    ) -> Result<i32, String>
    where
        Expr: diesel::ExpressionMethods,
        Tbl: OrderDsl<Desc<Expr>>,
        Order<Tbl, Desc<Expr>>: LimitDsl,
        Limit<Order<Tbl, Desc<Expr>>>: RunQueryDsl<SqliteConnection>
            + Query
            + QueryFragment<Sqlite>
            + QueryId,
        Sqlite: HasSqlType<<Limit<Order<Tbl, Desc<Expr>>> as Query>::SqlType>,
        Record: Queryable<<Limit<Order<Tbl, Desc<Expr>>> as Query>::SqlType, Sqlite> + Time,
    

    There's also a trait that wraps up all the Query*-related subtraits: LoadQuery. Using that, we can reduce it down to:

    pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
        conn: &SqliteConnection,
        table: Tbl,
        time: Expr,
    ) -> Result<i32, String>
    where
        Expr: diesel::ExpressionMethods,
        Tbl: OrderDsl<Desc<Expr>>,
        Order<Tbl, Desc<Expr>>: LimitDsl,
        Limit<Order<Tbl, Desc<Expr>>>: LoadQuery<SqliteConnection, Record>,
        Record: Time,
    


    You can then make use of Diesel's first function and Results combinators to shorten the entire function:

    use diesel::expression::operators::Desc;
    use diesel::helper_types::{Limit, Order};
    use diesel::query_dsl::methods::{LimitDsl, OrderDsl};
    use diesel::query_dsl::LoadQuery;
    
    pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
        conn: &SqliteConnection,
        table: Tbl,
        time: Expr,
    ) -> Result<i32, String>
    where
        Expr: diesel::ExpressionMethods,
        Tbl: OrderDsl<Desc<Expr>>,
        Order<Tbl, Desc<Expr>>: LoadQuery<SqliteConnection, Record> + LimitDsl,
        Limit<Order<Tbl, Desc<Expr>>>: LoadQuery<SqliteConnection, Record>,
        Record: Time,
    {
        table
            .order(time.desc())
            .first(conn)
            .optional()
            .map(|x| x.map_or(0, |x| x.time()))
            .map_err(|e| format!("Error here! {:?}", e))
    }
    

    这篇关于如何将使用Diesel的多个功能合并为一个抽象?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆