Android SqlBrite使用介绍和官方demo详解

一、什么是SqlBrite

对 Android 系统的SQLiteOpenHelperContentResolver 的轻量级封装,配合Rxjava使用。

github地址: https://github.com/square/sqlbrite

ps: 2017年3月15号为止,还不支持Rxjava2,有点遗憾。

二、导包和初始化

在module的builde.gradle依赖加入以下,如果你没导入Rxjava,那么他会自动导入:

compile 'com.squareup.sqlbrite:sqlbrite:1.1.1'

创建一个 SqlBrite 对象,该对象是该库的入口:

SqlBrite sqlBrite = new SqlBrite.Builder().build();

通过SQLiteOpenHelper实例和调度创建BriteDatabase,由于数据库操作是个耗时操作,不建议在 UI 线程中执行的,所以这里可以添加调度器进行线程的控制Scheduler ,一般指定 Schedulers.io() 。

//根据SQLiteOpenHelper和Scheduler来创建BriteDatabase,指定调度器为io操作
BriteDatabase db = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io());

如果是使用 ContentProvider 的话,需要使用 ContentResolver 来创建一个 BriteContentResolver 对象:

BriteContentResolver resolver = sqlBrite.wrapContentProvider(contentResolver, Schedulers.io());
Observable<Query> query = resolver.createQuery(/*...*/);

三、 数据库操作

增删改查基本用法。

查询操作

BriteDatabase.createQuery方法类似于SQLiteDatabase.rawQuery,但是它需要一个附加的表参数来监听更改。
Subscribe 返回的Observable ,它将立即通知Query运行。

Observable<Query> users = db.createQuery("users", "SELECT * FROM users");
users.subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    Cursor cursor = query.run();
    // TODO parse data...
  }
});

与传统的rawQuery不同,对指定表的更新将触发其他通知,只要您保持订阅Observable,
意思就是,当您插入,更新或删除数据时,任何subscribed的查询会立即更新新的数据。

增加插入操作

插入方法调用的就是SqlDataBase的插入方法,这个没什么好说的了。

//根据todo_list表的id,插入数据到todo_item表,
db.insert(TodoItem.TABLE, null, new TodoItem.Builder()
        .listId(groceryListId)
        .description("Beer")
        .build());

修改更新操作

有5个参数

  • 表名
  • ContentValues
  • 冲突解决方案,一般为0就行
  • where条件字句
  • where 可变参数

例如官方栗子里面的:

//更新数据库
    db.update(TodoItem.TABLE, new TodoItem.Builder().complete(newValue).build(),
            TodoItem.ID + " = ?", String.valueOf(event.id()));
}

删除操作

从表中删除指定的行,有三个参数:

  • 表名
  • where条件语句
  • where可变参数
    db.delete(TodoItem.TABLE,"");

删除表就执行sql语句了要。

四、其他使用注意

在下面的例子中,重用了BriteDatabase对象“db”作为插入。所有插入,更新或删除操作都必须通过此对象才能正确通知subscribers。

Unsubscribe取消订阅退回对应的subscriber,可以停止获取更新。

final AtomicInteger queries = new AtomicInteger();
Subscription s = users.subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    queries.getAndIncrement();
  }
});
System.out.println("Queries: " + queries.get()); // Prints 1

db.insert("users", createUser("jw", "Jake Wharton"));
db.insert("users", createUser("mattp", "Matt Precious"));
s.unsubscribe();

db.insert("users", createUser("strong", "Alec Strong"));

System.out.println("Queries: " + queries.get()); // Prints 3

使用transactions可防止数据的大量更改导致subscribers频繁调用。

final AtomicInteger queries = new AtomicInteger();
users.subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    queries.getAndIncrement();
  }
});
System.out.println("Queries: " + queries.get()); // Prints 1

Transaction transaction = db.newTransaction();
try {
  db.insert("users", createUser("jw", "Jake Wharton"));
  db.insert("users", createUser("mattp", "Matt Precious"));
  db.insert("users", createUser("strong", "Alec Strong"));
  transaction.markSuccessful();
} finally {
  transaction.end();
}

System.out.println("Queries: " + queries.get()); // Prints 2

ps: 可以用try-with-resources语法使用transaction。可以简化代码,例如:

try (BriteDatabase.Transaction transaction = db.newTransaction()){
            db.insert("users", createUser("jw", "Jake Wharton"));
            db.insert("users", createUser("mattp", "Matt Precious"));
            db.insert("users", createUser("strong", "Alec Strong"));
            transaction.markSuccessful();
        } 

由于查询只是常规的RxJava Observable对象,运算符也可以用于控制向订阅者发送通知的频率。

users.debounce(500, MILLISECONDS).subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    // TODO...
  }
});

四、官方demo讲解

官方的demo是一个记事本备忘录的功能的一个app,名为todo。 谷歌的栗子也是这个todo。

导入了官方的demo,可以发现,里面有利用的Dagger2、AutoValue、ButterKnife等等这些包。对这些知识还不会用的朋友请先看:

Dagger2:
Android快速依赖注入框架Dagger2使用1

Android快速依赖注入框架Dagger2使用2

AutoValue:
Android AutoValue使用和扩展库

ButterKnife: ButterKnife8.5.1 使用方法教程总结

我们应该怎么分析一个app呢? 我一般的习惯是

  • 程序跑起来
  • 看导包
  • 看manifest
  • 看application
  • 看入口activity

然后就一个页面一个页面地看下去

目录

这里写图片描述
db目录存放数据库相关的类和bean

ui目录放view: activity和fragment和适配器

外层是Dagger2: Component和Module和Application

整体流程

利用Dagger2在ListsFragmnet、ItemsFragment、NewItemFragment、NewListFragment进行注入,
AppModule提供单例的Application,TodoModule里面包括了DbModule。

@Singleton
@Component(modules = TodoModule.class)
public interface TodoComponent {

  void inject(ListsFragment fragment);

  void inject(ItemsFragment fragment);

  void inject(NewItemFragment fragment);

  void inject(NewListFragment fragment);
}

在DbModule中进行了DbOpenHelper、SqlBrite、BriteDatabase的初始化,提供注入。

@Module
public final class DbModule {

    //初始化提供SQLiteOpenHelper
    @Provides
    @Singleton
    SQLiteOpenHelper provideOpenHelper(Application application) {
        return new DbOpenHelper(application);
    }

    //初始化提供SqlBrite
    @Provides
    @Singleton
    SqlBrite provideSqlBrite() {
        return SqlBrite.create(new SqlBrite.Logger() {
            @Override
            public void log(String message) {
                Timber.tag("Database").v(message);
            }
        });
    }

    //初始化提供BriteDatabase
    @Provides
    @Singleton
    BriteDatabase provideDatabase(SqlBrite sqlBrite, SQLiteOpenHelper helper) {
        BriteDatabase db = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io());
        db.setLoggingEnabled(true);
        return db;
    }

}

DbOpenHelper进行了表的创建和插入对应的数据:

final class DbOpenHelper extends SQLiteOpenHelper {
    private static final int VERSION = 1;

    //创建todo_list表的语句
    private static final String CREATE_LIST = ""
            + "CREATE TABLE " + TodoList.TABLE + "("
            + TodoList.ID + " INTEGER NOT NULL PRIMARY KEY,"
            + TodoList.NAME + " TEXT NOT NULL,"
            + TodoList.ARCHIVED + " INTEGER NOT NULL DEFAULT 0"
            + ")";

    //创建todo_item表的语句
    private static final String CREATE_ITEM = ""
            + "CREATE TABLE " + TodoItem.TABLE + "("
            + TodoItem.ID + " INTEGER NOT NULL PRIMARY KEY,"
            + TodoItem.LIST_ID + " INTEGER NOT NULL REFERENCES " + TodoList.TABLE + "(" + TodoList.ID + "),"
            + TodoItem.DESCRIPTION + " TEXT NOT NULL,"
            + TodoItem.COMPLETE + " INTEGER NOT NULL DEFAULT 0"
            + ")";


    //创建 单列索引,加速查询
    private static final String CREATE_ITEM_LIST_ID_INDEX =
            "CREATE INDEX item_list_id ON " + TodoItem.TABLE + " (" + TodoItem.LIST_ID + ")";

    //构造器,创建数据库
    public DbOpenHelper(Context context) {
        super(context, "todo.db", null /* factory */, VERSION);
    }


    //创建数据库的时候回调
    @Override
    public void onCreate(SQLiteDatabase db) {
        //创建对应的表和索引
        db.execSQL(CREATE_LIST);
        db.execSQL(CREATE_ITEM);
        db.execSQL(CREATE_ITEM_LIST_ID_INDEX);

        //插入数据到todo_list表,返回id
        long groceryListId = db.insert(TodoList.TABLE, null, new TodoList.Builder()
                .name("Grocery List")
                .build());

        //根据todo_list表的id,插入数据到todo_item表,
        db.insert(TodoItem.TABLE, null, new TodoItem.Builder()
                .listId(groceryListId)
                .description("Beer")
                .build());
        db.insert(TodoItem.TABLE, null, new TodoItem.Builder()
                .listId(groceryListId)
                .description("Point Break on DVD")
                .build());
        db.insert(TodoItem.TABLE, null, new TodoItem.Builder()
                .listId(groceryListId)
                .description("Bad Boys 2 on DVD")
                .build());

        //下面的三列和上面的套路一样,就不贴代码了

    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

这时候我就就知道应用创建了两个表: todo_list和todo_item,根据todo_list的id进行关联。就相当于todo_list表存放的是标题,todo_item存放的是所有的数据。

这里写图片描述

TodoList和TodoItem是bean类,使用了AutoValue进行bean类代码生成。
里面存放了表名、字段对应的常量,还有对应的数据获取。还有使用Builder用来生成ContentValues。
(这里贴出TodoList的代码,TodoItem的套路一样)

@AutoValue
public abstract class TodoList implements Parcelable {


    public static final String TABLE = "todo_list";

    public static final String ID = "_id";
    public static final String NAME = "name";
    public static final String ARCHIVED = "archived";

    public abstract long id();

    public abstract String name();

    public abstract boolean archived();


    //利用RXjava进行数据的获取,返回bean列表
    public static Func1<Cursor, List<TodoList>> MAP = new Func1<Cursor, List<TodoList>>() {
        @Override
        public List<TodoList> call(final Cursor cursor) {
            try {
                List<TodoList> values = new ArrayList<>(cursor.getCount());

                while (cursor.moveToNext()) {
                    long id = Db.getLong(cursor, ID);
                    String name = Db.getString(cursor, NAME);
                    boolean archived = Db.getBoolean(cursor, ARCHIVED);
                    values.add(new AutoValue_TodoList(id, name, archived));
                }
                return values;
            } finally {
                cursor.close();
            }
        }
    };


    //构建器,用来生成ContentValues
    public static final class Builder {
        private final ContentValues values = new ContentValues();

        public Builder id(long id) {
            values.put(ID, id);
            return this;
        }

        public Builder name(String name) {
            values.put(NAME, name);
            return this;
        }

        public Builder archived(boolean archived) {
            values.put(ARCHIVED, archived);
            return this;
        }

        public ContentValues build() {
            return values; // TODO defensive copy?
        }
    }
}

接下来看回到Application初始化TodoComponent。供给Fragment进行注入。

public final class TodoApp extends Application {

    private TodoComponent mainComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        if (BuildConfig.DEBUG) {
            Timber.plant(new Timber.DebugTree());
        }

        mainComponent = DaggerTodoComponent.builder().todoModule(new TodoModule(this)).build();
    }

    public static TodoComponent getComponent(Context context) {
        return ((TodoApp) context.getApplicationContext()).mainComponent;
    }
}

整个app就一个activity,其他都是Fragment。MainActivity利用了系统自带的布局进行初始化显示ListsFragment,
并且回调ListsFragment操作的接口。

public final class MainActivity extends FragmentActivity
        implements ListsFragment.Listener, ItemsFragment.Listener {

    // 设置内容为Listsfragment
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(android.R.id.content, ListsFragment.newInstance())
                    .commit();
        }
    }

    //ListsFragment的item点击回调
    @Override
    public void onListClicked(long id) {
        getSupportFragmentManager().beginTransaction()
                .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left,
                        R.anim.slide_out_right)
                .replace(android.R.id.content, ItemsFragment.newInstance(id))
                .addToBackStack(null)
                .commit();
    }

    //菜单的newList点击回调
    @Override
    public void onNewListClicked() {
        NewListFragment.newInstance().show(getSupportFragmentManager(), "new-list");
    }

    /**
     * 菜单的newItem点击回调
     * @param listId 对应todo_list表的列的id
     */
    @Override
    public void onNewItemClicked(long listId) {
        NewItemFragment.newInstance(listId).show(getSupportFragmentManager(), "new-item");
    }

}

ListsFragment为单例,是todo_list表的数据,在初始化时候回调onAttach进行Dagger2的注入、初始化ListsAdapter适配器、开启右上角的菜单。
布局主要是一个ListView用来显示TodoList表的数据。界面实现完毕进行设置标题、查询数据库。

public final class ListsFragment extends Fragment {

    //回调到MainActivity的接口
    interface Listener {
        //列表点击
        void onListClicked(long id);
        //新建列表点击
        void onNewListClicked();
    }

    //单例实现
    static ListsFragment newInstance() {
        return new ListsFragment();
    }

    @Inject
    BriteDatabase db;   //注入获取BriteDatabase

    //使用ButterKnife获取View
    @BindView(android.R.id.list)
    ListView listView;
    @BindView(android.R.id.empty)
    View emptyView;

    //回调接口
    private Listener listener;
    //适配器
    private ListsAdapter adapter;
    //Rxjava的订阅者,在离开界面进行接触订阅
    private Subscription subscription;

    //初始化时候进行Dagger2的注入
    @Override
    public void onAttach(Activity activity) {
        if (!(activity instanceof Listener)) {
            throw new IllegalStateException("Activity must implement fragment Listener.");
        }

        super.onAttach(activity);

        //Dagger2的注入
        TodoApp.getComponent(activity).inject(this);

        //设置菜单
        setHasOptionsMenu(true);

        listener = (Listener) activity;
        //初始化适配器
        adapter = new ListsAdapter(activity);
    }

    //添加菜单为 NEW LIST
    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);

        //点击菜单回调
        MenuItem item = menu.add(R.string.new_list)
                .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        listener.onNewListClicked();
                        return true;
                    }
                });
        MenuItemCompat.setShowAsAction(item, SHOW_AS_ACTION_IF_ROOM | SHOW_AS_ACTION_WITH_TEXT);
    }

    //创建Fragment的布局
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.lists, container, false);
    }

    //view创建完毕,开始绑定Butterknife、适配器
    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        ButterKnife.bind(this, view);
        listView.setEmptyView(emptyView);
        listView.setAdapter(adapter);
    }

    @OnItemClick(android.R.id.list)
    void listClicked(long listId) {
        listener.onListClicked(listId);
    }

    //界面显示完毕设置标题、查询数据库
    @Override
    public void onResume() {
        super.onResume();

        getActivity().setTitle("To-Do");

        subscription = db.createQuery(ListsItem.TABLES, ListsItem.QUERY)
                .mapToList(ListsItem.MAPPER)    //映射到ListItem的MAPPER
                .observeOn(AndroidSchedulers.mainThread())//设置订阅者在主线程进行
                .subscribe(adapter);
    }

    @Override
    public void onPause() {
        super.onPause();
        //界面隐藏解除订阅
        subscription.unsubscribe();
    }
}

ListsAdapter作为数据的适配显示,实现了Rxjava的Action1

final class ListsAdapter extends BaseAdapter implements Action1<List<ListsItem>> {
    private final LayoutInflater inflater;

    private List<ListsItem> items = Collections.emptyList();

    public ListsAdapter(Context context) {
        this.inflater = LayoutInflater.from(context);
    }

    //在ListsFragment里面Rxjava查询数据库完毕,在这里更新适配器
    @Override
    public void call(List<ListsItem> items) {
        this.items = items;
        notifyDataSetChanged();
    }

    ....(下面的就省略了)
}

ListsItem是ListsFragment的数据list的bean,里面定义了一个查询语句,这个语句可能比较复杂,意思就是 根据todo_list的_id分组
,选择_id,name,todo_listd _id对应的todo_item的数量(Sql语句不懂的先去补习一下吧)。 还有一个MAPPER映射,Rxjava的处理把Cursor转换为ListsItem。

SELECT list._id, list.name, COUNT(item._id) as item_count 
FROM todo_list AS list 
LEFT OUTER JOIN todo_item AS item ON list._id = item.todo_list_id 
GROUP BY list._id

ItemsFragment是todo_item表的数据,存放的是所有的item。
通过在ListsFragment中点击item的时候传递todo_list表的_id来,然后ItemFragmnet根据list的_id查询得到item显示,和查询数量和todo_list的nane作为标题。
基本套路和ListsFragmnet差不多。

ItemsFragmeng用到了Rxbinding,对listView的item点击进行监听,每点击一次就更新todo_item表对应的数据,就是备忘录的任务完成了的意思。

public final class ItemsFragment extends Fragment {
    private static final String KEY_LIST_ID = "list_id";

    //根据todo_list_id查询todo_item表的所有的数据
    private static final String LIST_QUERY = "SELECT * FROM "
            + TodoItem.TABLE
            + " WHERE "
            + TodoItem.LIST_ID
            + " = ? ORDER BY "
            + TodoItem.COMPLETE
            + " ASC";

    //根据todo_list_id查询todo_item表所有的数据的总数
    private static final String COUNT_QUERY = "SELECT COUNT(*) FROM "
            + TodoItem.TABLE
            + " WHERE "
            + TodoItem.COMPLETE
            + " = "
            + Db.BOOLEAN_FALSE
            + " AND "
            + TodoItem.LIST_ID
            + " = ?";

    //根据_id查询todo_list表的数据的name
    private static final String TITLE_QUERY =
            "SELECT " + TodoList.NAME + " FROM " + TodoList.TABLE + " WHERE " + TodoList.ID + " = ?";

    public interface Listener {
        void onNewItemClicked(long listId);
    }

    public static ItemsFragment newInstance(long listId) {
        Bundle arguments = new Bundle();
        arguments.putLong(KEY_LIST_ID, listId);

        ItemsFragment fragment = new ItemsFragment();
        fragment.setArguments(arguments);
        return fragment;
    }

    @Inject
    BriteDatabase db;

    @BindView(android.R.id.list)
    ListView listView;
    @BindView(android.R.id.empty)
    View emptyView;

    private Listener listener;
    private ItemsAdapter adapter;
    private CompositeSubscription subscriptions;   //可以组合多个subscriptions,便于解除订阅

    private long getListId() {
        return getArguments().getLong(KEY_LIST_ID);
    }

    @Override
    public void onAttach(Activity activity) {
        if (!(activity instanceof Listener)) {
            throw new IllegalStateException("Activity must implement fragment Listener.");
        }

        super.onAttach(activity);
        TodoApp.getComponent(activity).inject(this);
        setHasOptionsMenu(true);

        listener = (Listener) activity;
        adapter = new ItemsAdapter(activity);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);

        MenuItem item = menu.add(R.string.new_item)
                .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        listener.onNewItemClicked(getListId());
                        return true;
                    }
                });
        MenuItemCompat.setShowAsAction(item, SHOW_AS_ACTION_IF_ROOM | SHOW_AS_ACTION_WITH_TEXT);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.items, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        ButterKnife.bind(this, view);
        listView.setEmptyView(emptyView);
        listView.setAdapter(adapter);

        //监听Listview的点击,更新todo_item表的状态
        RxAdapterView.itemClickEvents(listView) //
                .observeOn(Schedulers.io())
                .subscribe(new Action1<AdapterViewItemClickEvent>() {
                    @Override
                    public void call(AdapterViewItemClickEvent event) {
                        //取相反值
                        boolean newValue = !adapter.getItem(event.position()).complete();
                        //更新数据库
                        db.update(TodoItem.TABLE, new TodoItem.Builder().complete(newValue).build(),
                                TodoItem.ID + " = ?", String.valueOf(event.id()));
                    }
                });
    }


    @Override
    public void onResume() {
        super.onResume();
        //得到todo_list表对应的_id
        String listId = String.valueOf(getListId());

        //创建订阅者
        subscriptions = new CompositeSubscription();

        //查询todo_item表的对应todo_list_id的总数
        Observable<Integer> itemCount = db.createQuery(TodoItem.TABLE, COUNT_QUERY, listId) //
                .map(new Func1<Query, Integer>() {
                    @Override
                    public Integer call(Query query) {
                        Cursor cursor = query.run();
                        try {
                            if (!cursor.moveToNext()) {
                                throw new AssertionError("No rows");
                            }
                            return cursor.getInt(0);
                        } finally {
                            cursor.close();
                        }
                    }
                });

        //根据_id查询todo_list表的数据的name
        Observable<String> listName =
                db.createQuery(TodoList.TABLE, TITLE_QUERY, listId).map(new Func1<Query, String>() {
                    @Override
                    public String call(Query query) {
                        Cursor cursor = query.run();
                        try {
                            if (!cursor.moveToNext()) {
                                throw new AssertionError("No rows");
                            }
                            return cursor.getString(0);
                        } finally {
                            cursor.close();
                        }
                    }
                });

        //取得对应list的名字和todo_item表对应的数量数量作为标题。
        subscriptions.add(
                Observable.combineLatest(listName, itemCount, new Func2<String, Integer, String>() {
                    @Override
                    public String call(String listName, Integer itemCount) {
                        return listName + " (" + itemCount + ")";
                    }
                })
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(new Action1<String>() {
                            @Override
                            public void call(String title) {
                                getActivity().setTitle(title);
                            }
                        }));

        //根据todo_list_id查询todo_item表的所有的数据,更新到适配器
        subscriptions.add(db.createQuery(TodoItem.TABLE, LIST_QUERY, listId)
                .mapToList(TodoItem.MAPPER)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(adapter));
    }

    @Override
    public void onPause() {
        super.onPause();
        //解除订阅多个subscriptions
        subscriptions.unsubscribe();
    }
}

好了,SqlBrite官方的demo基本功能就这样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KeepStudya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值