Room 没那么复杂

一、Room 的基本使用方法

1.1、相关概念

没什么复杂的,就 3 个注解类

  • @Entity:一个 Entity 对应于数据库中的一张表。

  • @Dao:操作类
    @Insert:增
    @Delete:删
    @Update:改
    @Query(" … "):增/删/改/查

  • @Database:数据库相关

1.2、案例分析

假设要创建一个学生数据库,数据库中有一张学生表,用于保存学生的基本信息。

1、导包
    implementation "androidx.room:room-runtime:2.2.5"
    annotationProcessor "androidx.room:room-compiler:2.2.5"
2、@Entity:创建一张学生表
  • @Entity 标签用于将 Student 类与 Room 中的数据表对应起来。tableName 属性可以为数据表设置表名,若不设置,则表名与类名相同。
  • @PrimaryKey 标签用于指定该字段作为表的主键。autoGenerate 属性设置为 true 为自增。
  • @ColumnInfo 标签可用于设置该字段存储在数据库表中的名字,并指定字段的类型。name 属性设置字段名,若不设置,默认为该变量名。
  • @Ignore 标签用来告诉 Room 忽略该字段或方法。
@Entity(tableName = "student")
public class Student {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
    public int id;

    @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
    public String name;

    @ColumnInfo(name = "age", typeAffinity = ColumnInfo.TEXT)
    public String age;

    /**
     * Room 默认会使用这个构造器操作数据
     */
    public Student(int id, String name, String age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    /**
     * 由于 Room 只能识别和使用一个构造器,如果希望定义多个构造器可以使用 Ignore 标签,让 Room 忽略这个构造器
     * 不仅如此,@Ignore 标签还可用于字段
     * Room 不会持久化被 @Ignore 标签标记过的字段的数据
     */
    @Ignore
    public Student(String name, String age) {
        this.name = name;
        this.age = age;
    }
}

存储集合类型的数据(待更新。。。)参考博客

3、@Dao:定义一个接口,以便对 Entity 进行操作
@Dao
public interface StudentDao {
    @Insert
    void insertStudent(Student... student);

    @Delete
    void deleteStudent(Student... student);

    @Update
    void updateStudent(Student... student);

    @Query("SELECT * FROM student")
    List<Student> getStudent();

    @Query("SELECT * FROM student WHERE id=:id")
    Student getStudentById(int id);

    //在 MVVM+JetPack 架构中,返回数据可用 LiveData 包装可观察数据变化
//    @Query("SELECT * FROM student WHERE id=:id")
//    LiveData<Student> getStudentById(int id);
//
//    @Query("SELECT * FROM student")
//    LiveData<List<Student>> getStudent();
}
4、@Database:定义好 Entity 和 Dao 后,接下来是创建数据库
  • @DataBase 标签用于告诉系统这是 Room 数据库对象。entities 属性用于指定该数据库有哪些表,若需要建立多张表,则表名以逗号隔开。version 属性用于指定数据库版本号,后面数据库的升级正是依据版本号进行判断的。
  • 数据库类需要继承自 RoomDatabase,并通过 Room.databaseBuilder() 结合单例设计模式完成创建。另外,我们之前创建的 Dao 对象,在此以抽象方法的形式返回,只需一行代码即可。
@Database(entities = {Student.class}, version = 1, exportSchema = true) //exportSchema 
public abstract class MyDatabase extends RoomDatabase {

    private static final String DATABASE_NAME = "student_database";
    private static MyDatabaseINSTANCE;

    public static synchronized StudentDatabase getInstance(Context context) {
        if (INSTANCE == null) {
            INSTANCE = Room.databaseBuilder(context.getApplicationContext(), MyDatabase.class, DATABASE_NAME)
                    //.allowMainThreadQueries() //允许在 main 线程运行(慎用,不推荐,可做单元测试使用)
                    .build();
        }
        return INSTANCE;
    }

    //对外暴露
    public abstract StudentDao studentDao();
}
5、使用

数据库和表的创建工作就完成后。下面来看看如何对数据库进行增/删/改/查。这些对数据库的操作方法都是我们之前在 Dao 文件中已经定义好的。需要注意的是,不能直接在 UI 线程中执行这些操作,所有操作都需要放在工作线程中进行。

public class RoomActivity extends AppCompatActivity {

    private MyDatabase myDatabase;
    private List<Student> mStudentList;
    private RoomAdapter mRoomAdapter;

    private EditText etName;
    private EditText etAge;
    private EditText etId;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_room);
        myDatabase = MyDatabase.getInstance(this);
        initRecyclerView();
        etName = findViewById(R.id.etName);
        etAge = findViewById(R.id.etAge);
        etId = findViewById(R.id.etId);
        findViewById(R.id.addStudent).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (TextUtils.isEmpty(etName.getText().toString()) || TextUtils.isEmpty(etAge.getText().toString())) {
                    Toast.makeText(RoomActivity.this, "不能为空!", Toast.LENGTH_SHORT).show();
                } else {
                    new InsertStudentTask(etName.getText().toString(), etAge.getText().toString()).execute();
                }
            }
        });
        findViewById(R.id.queryStudentList).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new QueryStudentListTask().execute();
            }
        });
        findViewById(R.id.queryStudentById).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (TextUtils.isEmpty(etId.getText().toString())) {
                    Toast.makeText(RoomActivity.this, "不能为空!", Toast.LENGTH_SHORT).show();
                } else {
                    int id = Integer.parseInt(etId.getText().toString());
                    new QueryStudentByIdTask(id).execute();
                }
            }
        });
    }

    private void initRecyclerView() {
        RecyclerView recyclerView = findViewById(R.id.recyclerView);
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(RecyclerView.VERTICAL);
        recyclerView.setLayoutManager(manager);
        mStudentList = new ArrayList<>();
        mRoomAdapter = new RoomAdapter(this, mStudentList);
        recyclerView.setAdapter(mRoomAdapter);
        mRoomAdapter.setOnClickListener(new RoomAdapter.OnClickListener() {
            @Override
            public void onDeleteClick(Student student) {
                new DeleteStudentTask(student).execute();
            }

            @Override
            public void onUpdateClick(int id, String name, String age) {
                new UpdateStudentTask(id, name, age).execute();
            }
        });
    }

    /**
     * 增
     */
    private class InsertStudentTask extends AsyncTask<Void, Void, Void> {
        private String name;
        private String age;

        public InsertStudentTask(String name, String age) {
            this.name = name;
            this.age = age;
        }

        @Override
        protected Void doInBackground(Void... voids) {
            // 插入数据
            myDatabase.studentDao().insertStudent(new Student(name, age));
            return null;
        }

    }

    /**
     * 删
     */
    private class DeleteStudentTask extends AsyncTask<Void, Void, Void> {
        private Student student;

        public DeleteStudentTask(Student student) {
            this.student = student;
        }

        @Override
        protected Void doInBackground(Void... voids) {
            // 删除数据
            myDatabase.studentDao().deleteStudent(student);
            return null;
        }

    }

    /**
     * 改
     */
    private class UpdateStudentTask extends AsyncTask<Void, Void, Void> {
        private int id;
        private String name;
        private String age;

        public UpdateStudentTask(int id, String name, String age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        @Override
        protected Void doInBackground(Void... voids) {
            // 更新数据
            myDatabase.studentDao().updateStudent(new Student(id, name, age));
            return null;
        }

    }

    /**
     * 查询所有
     */
    private class QueryStudentListTask extends AsyncTask<Void, Void, Void> {

        @Override
        protected Void doInBackground(Void... voids) {
            // 查询所有学生
            List<Student> studentList = myDatabase.studentDao().getStudentList();
            mStudentList.clear();
            mStudentList.addAll(studentList);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            mRoomAdapter.notifyDataSetChanged();
        }
    }

    /**
     * 根据 Id 查询
     */
    private class QueryStudentByIdTask extends AsyncTask<Void, Void, Void> {
        private int id;

        public QueryStudentByIdTask(int id) {
            this.id = id;
        }

        @Override
        protected Void doInBackground(Void... voids) {
            // 查询某个学生
            Student student = myDatabase.studentDao().getStudentById(id);
            mStudentList.clear();
            if (student != null) {
                mStudentList.add(student);
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            mRoomAdapter.notifyDataSetChanged();
        }
    }
}
public class RoomAdapter extends RecyclerView.Adapter<RoomAdapter.RoomHolder> {
    private Context mContext;
    private List<Student> mStudentList;

    public RoomAdapter(Context mContext, List<Student> studentList) {
        this.mContext = mContext;
        this.mStudentList = studentList;
    }

    @NonNull
    @Override
    public RoomHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.item_room, parent, false);
        return new RoomHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull final RoomHolder holder, final int position) {
        if (mStudentList == null || mStudentList.size() == 0
                || mStudentList.get(position) == null) return;
        holder.tvId.setText(mStudentList.get(position).id + "");
        holder.tvName.setText(mStudentList.get(position).name);
        holder.tvAge.setText(mStudentList.get(position).age);
        holder.btnDelete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onClickListener != null) {
                    onClickListener.onDeleteClick(mStudentList.get(position));
                }
            }
        });
        holder.btnUpdate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onClickListener != null) {
                    int id = mStudentList.get(position).id;
                    String name = holder.etName.getText().toString();
                    String age = holder.etAge.getText().toString();
                    onClickListener.onUpdateClick(id, name, age);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return mStudentList.size();
    }

    class RoomHolder extends RecyclerView.ViewHolder {
        private TextView tvId, tvName, tvAge;
        private EditText etName, etAge;
        private Button btnDelete, btnUpdate;

        public RoomHolder(@NonNull View itemView) {
            super(itemView);
            tvId = itemView.findViewById(R.id.tvId);
            tvName = itemView.findViewById(R.id.tvName);
            tvAge = itemView.findViewById(R.id.tvAge);
            etName = itemView.findViewById(R.id.etName);
            etAge = itemView.findViewById(R.id.etAge);
            btnDelete = itemView.findViewById(R.id.btnDelete);
            btnUpdate = itemView.findViewById(R.id.btnUpdate);
        }
    }

    private OnClickListener onClickListener;

    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    public interface OnClickListener {
        void onDeleteClick(Student student);

        void onUpdateClick(int id, String name, String age);
    }
}
6、运行效果

如下所示。数据库的增/删/改/查已全部实现。上面展示的都是核心代码。布局文件等相关的代码需要自行完善。

在这里插入图片描述

二、Room 数据库升级

2.1、使用 Migration 升级数据库

随着业务的变化,数据库可能也需要做一些调整。例如,数据表可能需要增加一个新字段。Android 提供了一个名为 Migration 的类,来对 Room 进行升级。

public Migration(int startVersion, int endVersion)

Migration 有两个参数,startVersion 和 endVersion。startVersion 表示当前数据库版本(设备上安装的版本),endVersion 表示将要升级到的版本。若设备中的应用程序数据库版本为 1,那么以下 Migration 会将你的数据库版本从 1 升级到 2。

    private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            // 执行与升级相关的操作
        }
    };

以此类推,若数据库版本从 2 升级到 3,则需要写一个这样的 Migration。

    private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            // 执行与升级相关的操作
        }
    };

若用户设备上的应用程序数据库版本为 1,而当前要安装的应用程序数据库版本为 3,那么该怎么办呢?在这种情况下,Room 会先判断当前有没有直接从 1 到 3 的升级方案,如果有,就直接执行 1 到 3 的升级方案;如果没有,那么 Room 会按照顺序先后执行 Migration(1,2)、Migration(2,3) 以完成升级。

在 Migration 中编写升级方案后,还需要通过 addMigration() 方法,将升级方案添加到 Room。

    public static synchronized MyDatabase upgradeSQLite(Context context) {
        if (databaseInstance == null) {
            databaseInstance = Room.databaseBuilder(context.getApplicationContext(), MyDatabase.class, DATABASE_NAME)
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3)
                    .build();
        }
        return databaseInstance;
    }

在升级过程,该如何修改 Room 数据库的版本号呢?直接通过 @Database 标签中的 version 属性进行修改就可以了。

@Database(entities = {Student.class}, version = 1)

2.2、异常处理

假设我们将数据库版本升级到 4,却没有为此写相应的 Migration,则会出现一个 IllegalStateException 异常。
在这里插入图片描述
这是因为 Room 在升级过程中没有匹配到相应的 Migration。为了防止出现升级失败导致应用程序崩溃的情况,我们可以在创建数据库时加入 fallbackToDestructiveMigration() 方法。该方法能够在出现升级异常时,重新创建版本号为 4 的数据表。需要注意的是,虽然应用程序不会崩溃,但由于数据表被重新创建,所有的数据也将会丢失。

    public static synchronized MyDatabase upgradeSQLite(Context context) {
        if (databaseInstance == null) {
            databaseInstance = Room.databaseBuilder(context.getApplicationContext(), MyDatabase.class, DATABASE_NAME)
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3,MIGRATION_1_3)
                    .fallbackToDestructiveMigration()
                    .build();
        }
        return databaseInstance;
    }

2.3、Schema 文件

在没有 Room 组件之前,若想要验证数据的修改是否符合预期,我们需要找到这个数据库文件,接着使用第三方 Sqlite 查看工具,对其进行查看和验证。另外,如果我们希望查看数据库的历次升级情况,只能通过代码版本控制工具,根据源代码的历次修改情况进行推测。这无疑是耗时耗力的。为此,Room 提供了一项功能,在每次数据库的升级过程中,它都会为你导出一个 Schema 文件,这是一个 json 格式的文件,其中包含了数据库的所有基本信息。有了该文件,开发者便能清楚地知道数据库的历次变更情况,这极大地方便了开发者排查问题。Schema 文件默认是导出的,你只需指定它的导出位置即可。

例如,我们可以在 app 的 build.gradle 文件中指定 Schema 文件的导出位置。

android {
    defaultConfig {
        //指定room.schemaLocation生成的文件路径
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
            }
        }
    }
}

当数据库生成或修改成功后,便可以在项目的根目录下看见对应的 Schema 文件了。数字代表数据库的版本号,如下图所示。
在这里插入图片描述
Room 默认导出 Schema 文件,如果你不想导出这些文件,那么可以在数据库标签 @Database 中指定 exportSchema = false。但我们建议导出这些文件。如果你使用版本控制系统(如 Git)来管理代码,那么可以将这些文件一并提交到代码仓库。

@Database(entities = {Student.class}, exportSchema = false, version = 1)

2.4、销毁与重建策略

在 Sqlite 中修改表结构比较麻烦。例如,我们想将 Student 表中的 age 字段类型从 TEXT 改为 INTEGER。

面对此类需求,最好的方式是采用销毁与重建策略,该策略大致分为以下几个步骤。

  1. 创建一个张符合表结构要求的临时表 temp_Student。
  2. 将数据从旧表 Student 复制至临时表 temp_Student。
  3. 删除旧表 Student。
  4. 将临时表 temp_Student 重命名为 Student。

修改数据库的版本号为 4,并将 Student 中的 age 类型改为 int。Migration 的代码如下所示。

    private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("CREATE TABLE temp_Student (id INTEGER PRIMARY KEY NOT NULL,name TEXT,age INTEGER)");
            database.execSQL("INSERT INTO temp_Student (id,name ,age) SELECT id,name,age FROM Student");
            database.execSQL("DROP TABLE Student");
            database.execSQL("ALTER TABLE teamp_Student RENAME TO Student");
        }
    };

执行上方代码后,通过查看 Schema 升级文件,我们可以看到,age 字段的类型已经被修改为 INTEGER,如下图所示
在这里插入图片描述

三、预填充数据库

3.1、createFromAsset() API 和 createFromFile() API

从 Room 2.2 版本开始,Room 加入了两个新的 API,用于在给定已填充数据库文件的基础上创建 Room 数据库。基于这两个 API,开发者可以基于特定的预打包好的数据库文件来创建 Room 数据库。

例如,假设你的应用程序需要用一个 Room 数据库,以存储世界各地的城市信息。那么你可以在应用程序发布时,将 cities.db 文件放到 assets 目录下,在用户首次打开应用程序时,使用 createFromAsset() 方法,基于 cities.db 文件创建你的 Room 数据库。如果你担心将数据库文件打包进 assets 目录会增加应用程序的大小,还可以考虑在用户首次打开应用程序时,通过网络连接将数据库文件下载至 SD 卡,接着通过 createFromFile() 方法来创建 Room 数据库。

3.2、createFromAsset() API 的使用方法

1、 首先需要创建一个数据库文件 students.db。创建的方式有很多,这里使用的是 DB Browser for SQLite 软件。安装该软件后,单击 “新建数据库”,新建一个名为 students 的数据库。如下图所示。

在这里插入图片描述
2、接着创建数据表 student,其中的字段与 Student 模型类中的字段保持一致。如下图所示。

在这里插入图片描述

3、接着,往数据表中添加数据,模拟数据的预填充。如下图所示,我们添加了 5 条数据。

在这里插入图片描述

4、保存数据库,并将数据库文件放到项目的 assets/databases 目录下,如下图所示。

在这里插入图片描述

5、接来下是需要关注的重点。在创建数据库时,调用 createFromAsset() 方法,基于 assets/database/students.db 文件创建 Room 数据库。

@Database(entities = {Student.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {

    private static final String DATABASE_NAME = "my_db";
    private static MyDatabase databaseInstance;

    /**
     * 创建
     */
    public static synchronized MyDatabase getInstance(Context context) {
        if (databaseInstance == null) {
            databaseInstance = Room.databaseBuilder(context, MyDatabase.class, DATABASE_NAME)
                    // 从 assets/database 目录下读取 students.db
                    .createFromAsset("databases/students.db")
                    .build();
        }
        return databaseInstance;
    }

    public abstract StudentDao studentDao();
}

6、 student 类中,数据类型也需要改成和数据库相对应的。我尝试把 age 字段改成 INTEGER,age属性改成 int,包括数据库中的 age 的类型也是 INTEGER,但是会报数据库结构的错误。

@Entity(tableName = "student")
public class Student {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
    public int id;

    @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
    public String name;

    @ColumnInfo(name = "age", typeAffinity = ColumnInfo.TEXT)
    public String age;

    /**
     * Room 默认会使用这个构造器操作数据
     */
    public Student(int id, String name, String age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    /**
     * 由于 Room 只能识别和使用一个构造器,如果希望定义多个构造器可以使用 Ignore 标签,让 Room 忽略这个构造器
     * 不仅如此,@Ignore 标签还可用于字段
     * Room 不会持久化被 @Ignore 标签标记过的字段的数据
     */
    @Ignore
    public Student(String name, String age) {
        this.name = name;
        this.age = age;
    }
}

7、运行应用程序,可以看到,students.db 文件中的数据已经被写入 Room 数据库中了,如下图所示。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android RoomAndroid架构组件的一部分,它是一个持久性库,提供了对SQLite数据库的抽象层,使得在使用SQLite的强大功能的同时,能够更加方便地访问数据库Room具有以下优势: 1. 针对SQL查询的编译时验证:Room在编译时会检查SQL查询的语法和表结构,可以最大限度地减少错误和重复的代码。 2. 方便的注解:Room使用注解来定义实体类、DAO接口和数据库,可以简化数据库操作的代码编写。 3. 简化数据库迁移路径:Room提供了数据库迁移的支持,可以方便地升级数据库版本,而不需要手动编写复杂的SQL语句。 Room库由三个主要组件组成: 1. 实体(Entity):实体是数据库中的表的映射类,使用注解来定义。每个实体类都应该有一个主键,并且可以定义索引、关联表等。 2. 数据访问对象(DAO):DAO是用于定义数据库操作的接口或抽象类,使用注解来标记。通过DAO可以定义插入、更新、删除等操作,并且可以使用SQL查询语句。 3. 数据库(Database):数据库Room的核心组件,通过注解来定义。数据库类应该是抽象类,并且继承自RoomDatabase。在数据库类中可以定义实体类和DAO接口的关联关系,并且提供数据库的版本号和迁移策略。 以上是关于Android Room的基本介绍和组成部分的说明。通过使用Room,开发者可以更加方便地进行数据库操作,并且减少了编写重复和容易出错的代码的工作量。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [AndroidRoom的基本使用](https://blog.csdn.net/qq_42326415/article/details/119608921)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Android Room 使用详解](https://blog.csdn.net/Snow_Ice_Yang/article/details/128929138)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Android Room的使用详解](https://blog.csdn.net/fjnu_se/article/details/128173203)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值