Room-数据持久化存储(进阶)

Room持久化库:从零到一的全面解析与实战 本文全面解析Android Room持久化库,从基础配置到企业级应用实战,涵盖实体类设计、DAO操作、数据库迁移及SQLCipher加密技术。通过代码示例详解Room与Kotlin协程、Hilt依赖注入的集成,帮助开发者高效构建安全、可维护的数据库系统,适用于零基础到进阶开发需求。 阅读详情


前言

上一篇文章我们讲述room的基本用法.
本篇将讲述: 复杂数据处理, 配合Flow实现全局UI刷新, DB版本升级方式.


Room的其他功能

以下功能了解即可

  • 定义对象间的关系: 有一对多, 多对多什么的. 但博主暂时没有业务需要在 App 中构建过于庞大且复杂的数据库. 而且构建这种关系的对象时, 必须要额外创建一个复合对象来容纳他们.
  • 定义视图: 一般多表关联的复杂查询才需要视图
  • 预填充数据库: 有 应用资源预填充, 文件系统预填充, 数据库的迁移 等

需要了解的, 请看查看官方文档


提示:以下是本篇文章正文内容,下面案例可供参考

一、复杂数据处理?

有的时候我们会对象套对象, 对象套集合, 例如:

@Entity
class RoomTwoEntity {
    @PrimaryKey
    var id: String = ""
    @ColumnInfo
    var nickname: String? = null

	@ColumnInfo
    var imgs: MutableList<Image>? = null    //集合实体

    @ColumnInfo
    var room: RoomEntity? = null	//其他实体
}

class Image(val res: Int)

上面 @ColumnInfo 直接写是不行的. room也不知道它算什么类型.
我们为这些实体单独创建表, 再管理关联关系的话, 相当麻烦
@TypeConverter 会让这件事非常简单.

1. @TypeConverter

类型转换器, 它的本质就是:

插入库表时, 将实体类型转换为 基本类型. 查询时再讲基本类型转换为实体类型; 例如: 对象转换为JSON, Date转换为Long等

第一步: 编写转换器

class MyConverters {
    @TypeConverter
    //Image集合 转换为 Json字符串
    fun imgsToStr(imgList: MutableList<Image>? = null): String? {
        imgList ?: return null
        return Gson().toJson(imgList)
    }

    @TypeConverter
    //Json字符串 转换为 Image集合
    fun strToImgs(imgStr: String?): MutableList<Image>? {
        imgStr ?: return null
        return imgStr.toBeanList()
    }

    @TypeConverter
    //实体 转换为 Json字符串
    fun roomToStr(room: RoomEntity?): String? {
        room ?: return null
        return Gson().toJson(room)
    }

    @TypeConverter
    //Json字符串 转换为 实体
    fun strToRoom(str: String?): RoomEntity? {
        str ?: return null
        return Gson().fromJson(str, RoomEntity::class.java)
    }
}

转换器会根据匹配到的类型, 自动转换

第二步: 使用转换器

@Entity
@TypeConverters(MyConverters::class)
class RoomTwoEntity {

如图所示, 直接在 Entity类上 加上 @TypeConverters 注释即可

OK, 这样就完事了. 可以写测试代码了

注解还可以加在 DataBase上

试想一下, 好多类都有 Image 集合对象, 我每个 Entity 都加注释, 岂不是很麻烦

@Database(entities = [RoomEntity::class, RoomTwoEntity::class], version = 10)
@TypeConverters(MyConverters::class)
abstract class RoomTestDatabase : RoomDatabase() {

这样就可以省去 每个Entity都加注解的烦恼.

对了, 还有个 toBeanList, 它是一个扩展函数, 将json字符串转换为实体集合

inline fun <reified T> String.toBeanList(): MutableList<T> = 
ApiManager.INSTANCE.mGson.fromJson(this, ParameterizedTypeImpl(T::class.java))

//ParameterizedType 
class ParameterizedTypeImpl(private val clz: Class<*>) : ParameterizedType {
    override fun getRawType(): Type = List::class.java
    override fun getOwnerType(): Type? = null
    override fun getActualTypeArguments(): Array<Type> = arrayOf(clz)
}

再给个 Date 转 Long, 其实都一样啦:

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time?.toLong()
    }
}

最后:
db别忘了升级

二、配合Flow全局刷新 UI

比如当前登录用户-我自己. 我修改了我的头像,昵称啥的. 而显示这些资料的地方有好几处, 比如 “我”, “个人主页”, “资料设置” 等. 它们都要跟着改掉.

此时我们可以将user信息存入数据库中, 然后用Room+Flow

@Query("select * from UserInfo where id = 0")
fun getSelf(): Flow<UserInfo?>

在需要显示的页面中, 订阅它:

lifecycleScope.launch {
    RoomTestDatabase
        .getInstance(applicationContext)
        .roomTwoDao()
        .getSelf()
        .asLiveData()
        .observe(this@SelfActivity){
            mDataBind.user = it
    }
}

此时, 当我们修改了个人资料. 只需要把修改内容同步到数据库即可.

lifecycleScope.launch(Dispatchers.IO) {
    val user = UserInfo().apply {
        imgRes = R.drawable.bg
        name = mDataBind.etName.text.toString()
    }
    RoomTestDatabase.getInstance(applicationContext).roomTwoDao().setSelf(user)
}

然后, 不管之前有几个页面, 在它们重新显示时,都会更新为最新数据.

那么, 这是怎么实现的呢?

只要表中的任何数据发生变化,返回的 Flow 对象就会再次触发查询并重新发出整个结果集。

注意: 这个触发机制是以表为单位. 也就说, 只要当前表 经历了增删改操作, 这个表关联的 所有Flow全部失效,重新查询. 不管我当前数据是不是被修改的那个

distinctUntilChanged:

官方说:
通过将 distinctUntilChanged() 运算符应用于返回的 Flow 对象,可以确保仅在实际查询结果发生更改时通知界面:

但这个效果, 博主没有具体测试. 还没弄明白. 具体写法如下:
然后直接使用 getSelfDistinctUntilChanged() 即可.

@Query("select * from UserInfo where id = 0")
    fun getSelf(): Flow<UserInfo?>
    fun getSelfDistinctUntilChanged() =
        getSelf().distinctUntilChanged()

三、数据库升级

数据库升级需要管理版本号. 它们对这个数字非常敏感, 所以一定不要忘记这个版本号.

@Database(entities = [RoomEntity::class, RoomTwoEntity::class, UserInfo::class], version = 3)

1.清空重建

.fallbackToDestructiveMigration()

简单粗暴, 直接清除原来的表及数据, 按照注解重新建表.
适合开发阶段, 业务及实体经常变

instance = Room.databaseBuilder(
	    context.applicationContext,
	    RoomTestDatabase::class.java,
	    "Test.db" //数据库名称
	)
	    .fallbackToDestructiveMigration() //数据稳定前, 重建.
	    .build()

2.按版本号升级

.addMigrations()

companion object {
        private var instance: RoomTestDatabase? = null
        fun getInstance(context: Context): RoomTestDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(
                    context.applicationContext,
                    RoomTestDatabase::class.java,
                    "Test.db" //数据库名称
                )
                    .fallbackToDestructiveMigration() //数据稳定前, 重建.
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3) //版本升级
                    .build()
            }
            return instance!!
        }
    }

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE `RoomTwoEntity` (`id` TEXT NOT NULL, `nickname` TEXT, `sex` INTEGER NOT NULL, PRIMARY KEY(`id`))")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
    }
}

val MIGRATION_1_3 = object : Migration(1, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
    	database.execSQL("CREATE TABLE `RoomTwoEntity` (`id` TEXT NOT NULL, `nickname` TEXT, `sex` INTEGER NOT NULL, PRIMARY KEY(`id`))")
        database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
    }
}

Migration()

  • 这个对象应该很好理解吧. 两个参数分别表示前后版本号. 当然它是可以垮版本升级的, 比如 我 MIGRATION_1_3 的写法.
  • database.execSQL: 版本要升级 数据库要做调整. 它就是用来执行这些变化.

垮版本迁移:

例如: 版本2时,我们创建了某表, 版本3时,我们又不用它了. 此时 1 -> 3 我们按顺序升级的话, 需要执行建表, 再执行删表. 是不是白折腾了.
此时我们给出 1 -> 3 的空 Migration() 即可. room会自动判断并执行更快捷的 Migration()

3.建表注意事项:

下面 name 可以是 null, name1 NOT NULL; 此时建表语句的 NOT NULL一定要写上.

var name: String? = null
var name1: String = ""

下面 name 有默认值, 升级建表或加字段时 也要带上.

@ColumnInfo(defaultValue = "123")
var name: String? = null 

4.升级总结

  • 无论增表, 删表, 增改字段等. 都必须要做版本升级 (升号码, 并给Migration)
  • fallbackToDestructiveMigration() 与 addMigrations() 并不冲突, 在room没有找到相匹配的 Migration 时, 会抛出异常. 如果加上 fallbackToDestructiveMigration() 则会将库表清空重建. 也就是说room会优先匹配 Migration. 带上它,会防止部分异常,但也会存在丢失旧数据的风险
  • 单表升级时, 比如增加字段, 只需要执行 database.execSQL("ALTER TABLE ...")
  • 新建表时, 需要执行建表语句, 主键, NOT NULL, DEFAULT等都不要写错. 当然,写错的话 需要仔细比对报错信息, 找出自己升级的语句中的错误, 改掉即可
  • 删表时, 去掉实体@Entity时, 可以给空的升级 Migration(), 也可以执行 DROP TABLE

空 Migration() 如下:

val MIGRATION_1_2 = object : Migration(6, 7) {
    override fun migrate(database: SupportSQLiteDatabase) {
//        database.execSQL("DROP TABLE RoomTwoEntity")
    }
}

总结

实际项目中可以合理的规划 DataBase. 例如不常修改的,配置什么的单独一个DB.
常改的,业务相关的 也可以按业务模块 分BD.

上一篇: Room-数据持久化存储(入门)
下一篇: Kotlin Coroutine(协程): 一、样例

Android Room 之存储 Objects 中的 List 前言 Room 是官方推出的一个在 SQLite 上提供抽象层的持久存储库,提供了强大而可靠的 SQL 对象映射能力,并且支持 LiveData 和 RxJava。 在项目中添加以下依赖 def room_version = &quot;2.1.0-alpha04&quot; def lifecycle_version = &quot;2.0.0&quot; def rxjava_version = '2.1.7... 阅读详情

相关推荐

Android 数据持久化之 Room 数据库存储

Room 是 Google 推出的 Android 持久层框架,建立在 SQLite 之上,提供了一个抽象层,简化了数据库操作。它通过注解和编译时检查来确保数据操作的正确性。Entity(实体):定义封装实际数据的实体类,每个实体类对应数据库中的一个表,每个字段对应表中的一个列。DAO(数据访问对象):定义了用于操作数据库的方法,Room 会自动生成这些方法的实现。逻辑层无需和底层数据库打交道,直接和 Dao 层交互。为应用提供在数据库中查询、更新、插入和删除数据的方法。Database(数据库)

casual_clover的博客 933

Room数据库--TypeConverter

TypeConverter是什么 TypeConverter是Room数据库的类型转换器,可以对收到的数据进行类型转换 例如: 1、服务器返回Long类型的时间戳,我们可以通过TypeConverter将时间戳转换为Date类型 2、如果服务器返回了对象数组,默认Room是无法处理数组的,此时我们可以通过TypeConverter将Json中的数组转换为对象 使用TypeConverter存储数组和日期 首先定义一个Student类和Book类 一个学生是可以有多本书的,所以我们的Student构造中有一个

ymeddmn的博客 1828

Android数据存储:SQLite与Room持久化

Volatile= null.fallbackToDestructiveMigration() // 仅用于开发阶段.build()instance。

Ya-Jun 的博客 462

安卓Room存储复杂类型(类型转化)

简要介绍 通常我们存储的是基础类型,如Int,String等,但是如果要存在复杂的类型时,那该如何呢? 解决方案(以Date类型为例) 定义类型转换器 class Converters { @TypeConverter fun fromTimestamp(value: Long?): Date? { return if (value == null) null ...

蓝不蓝编程 3332

Android Studio开发本地记账App:Room持久化+Material UI实战

记账App本质是典型的本地优先(Local-First)移动业务系统,其核心在于离线可用、数据可靠与UI稳定。它依托Android原生能力实现本地持久化,通过Room数据库保障结构化存储与事务安全,结合ViewModel+LiveData实现响应式状态管理,并利用Material Design组件构建跨明暗模式的高一致性界面。技术价值体现在零网络依赖、毫秒级启动、隐私可控及强健的异常处理机制。典型应用场景包括个人财务管理、小微企业内控工具、开发者工程能力验证项目等。本文以可交付安卓记账本为载体,详解从And

weixin_26850069的博客 114

超炫Android军旗游戏源码解析与实战项目

接下来聊聊复用性问题。假设你现在要在80个格子里实现“翻开棋子”的动画效果,你是打算复制粘贴80次代码吗?当然不行。聪明的做法是:封装一个可复用的FlipPieceViewinit {// 实现Y轴旋转动画start()对应的XML布局也很简单:<ImageView<ImageView</merge>注意到用了<merge>标签没?这是为了防止多一层无意义的嵌套,进一步减少View层级。现在你在任何地方都可以这样使用它:改一次样式,全局生效。

weixin_36474966的博客 1063

Android 中以对象方式操作数据库:复杂对象转化实践

Room是Google官方推荐的数据库ORM库,它在SQLite之上提供了一个抽象层,允许更流畅的数据库访问,同时保留SQLite的全部功能。kotlin对于Room不支持的类型(如Date、List、自定义类等),我们可以使用TypeConverter进行转换。kotlin) {通过Room库,我们可以以面向对象的方式优雅地操作Android数据库。对于复杂对象:使用@Embedded处理嵌套对象使用处理自定义类型使用JSON序列化处理复杂集合使用@Relation处理对象间关系。

专注于移动开发的干货分享。提供高效解决方案、避坑指南与前沿技术解析。为开发者创造价值。 967

Android图书管理系统实战:Room持久化+RecyclerView响应式UI

Android应用开发中,数据持久化与UI响应性是保障用户体验的核心基础。Room作为官方推荐的SQLite抽象层,通过编译期校验、类型安全DAO和结构化迁移,显著提升数据库操作的安全性与可维护性;而RecyclerView配合DataBinding,借助DiffUtil局部刷新与声明式绑定,有效解决传统列表卡顿、代码冗余与逻辑耦合问题。这些技术组合不仅支撑中小型管理类App(如图书系统)的稳定运行,更广泛适用于需要离线存储、高频列表交互及长期迭代的业务场景。本文以课程级图书管理系统为载体,详解Room建模

weixin_29169505的博客 161

room使用TypeConverter转换复杂类型或者转换数组的时候,数据库crash

/** * user中的photos字段转换 */ open class UserPhotoConverters { @TypeConverter fun stringToObject(value: String): List<String> { val listType = object : TypeToken<List<String...

tao_zi7890的专栏 3696

Android Studio旅游记录与分享APP源码解析:架构、定位、防坑

移动应用开发中,基于Android Studio的旅游类项目常涉及地图SDK、定位服务、SQLite持久化等复杂技术点。从Android APP源码的工程视角展开,先梳理旅游记录与分享应用的整体架构与数据模型,再深入路线轨迹采集、地图绘制、图片压缩与动态广场等核心功能的实现原理,并针对编译失败、模拟器定位不准、分区存储崩溃、地图白屏等高频问题提供工程化解决方案。无论是毕业设计快速落地,还是个人旅游工具开发,掌握这些通用技术原理都能显著提升调试效率。通过解析一份完整的旅游记录与分享APP源码,让开发者理解从定

weixin_26749245的博客 339

从零开始:手把手教你检查并更新英伟达GPU驱动和CUDA版本(Windows/Linux双平台)

本文详细指导如何在Windows和Linux系统上检查并更新英伟达GPU驱动和CUDA版本,解决版本不匹配导致的常见问题。涵盖驱动更新、CUDA Toolkit安装、版本冲突排解及性能优化技巧,帮助开发者高效管理GPU计算环境。

weixin_29237635的博客 701

Android数据存储与持久化技术:从SharedPreferences到Room

掌握数据持久化是Android开发中至关重要的一环。从简单的到强大的Room数据库,选择合适的技术将直接影响应用的性能和可维护性。对于新项目,强烈推荐使用Room作为数据库解决方案。

m0_65382359的博客 989

【学习体会】Lighttools8.4.0:设置虚拟相机+仿真结果3D显示

1.设置虚拟相机并渲染视图中的图像 添加球型光源、接收器、cube物体,设置物体表面的光学属性为简单散射

LeonJin的博客 6285

Android数据持久化:SharedPreferences、SQLite与Room实战指南

数据持久化是移动应用开发的核心技术之一,它确保了应用数据在设备上的可靠存储。在Android平台上,系统提供了多种数据存储方案,从简单的键值对存储到复杂的关系型数据库。SharedPreferences适用于轻量级配置数据,采用XML文件格式存储键值对;SQLite作为嵌入式关系数据库,支持复杂的SQL查询和事务处理;而Room作为SQLite的现代化封装,通过编译时验证和ORM映射大幅提升了开发效率。这些技术在电商、社交、工具类等各类应用中都有广泛应用,开发者需要根据数据规模、查询复杂度等因素选择合适方案

weixin_29163797的博客 128

agv调度系统3.0技术介绍

agv调度系统,穿梭车调度系统,调度系统技术介绍,教你做调度

持续分享调度系统开发过程 4869

深入浅出SSD:固态存储核心技术、原理与实战(文末赠书)

作为经典畅销书的升级版,本书在第1版的基础上新增了大量新知识(如闪存、UFS、测试、文件系统等),并改写或删减了许多过时或不适用的内容,总修改量超过60%。本书由SSD技术社区SSDFans的核心团队成员联合业界、高校SSD方面的专家共同完成,相对于第1版,内容更全面、更新,也更有深度。本书从产品、技术和应用3个维度展开,全面、详细讲解了SSD的发展历史、产品形态、工作原理、整体架构,以及各个功能模块的原理和使用,从SSD的前端接口协议到FTL算法,再到后端的闪存和纠错,以及测试,本书都有涉及。

阿玥的小博客 3125

第 8 篇|本地存储技术选型 —— SP 与文件读写

文章摘要 本文系统介绍了Android本地存储技术,重点讲解了轻量级存储方案SharedPreferences的使用方法和三大常见坑点(commit/apply区别、多进程不可靠、潜在ANR风险)。同时详细解析了Android存储目录体系,包括内部存储与外部存储的区别,以及分区存储的适配方案。文章还提供了Kotlin文件读写的简洁方式,并介绍了SP的高性能替代方案MMKV的基本使用。最后通过一个笔记草稿箱案例,将各类存储技术串联应用,帮助开发者掌握Android数据持久化的核心技能。

Jm0218Xx的博客 388

gradle报错:A problem occurred configuring root project ‘My Application‘.

即使翻译了,我开始也看不懂,到处找网上教程,但都是改环境变量。马虎的我即使看了很多遍,改了很多遍也没反应过来,今天第一次搞gradle,突然就报错了,打印出。好了我就继续看项目了。

m0_59878789的博客 4091

01-04-16 网络与存储系列总结

本文总结了Android网络与存储开发系列(01-04-01至01-04-16)的核心内容,涵盖网络基础、架构设计、数据存储和进阶实践四大模块。系列详细讲解了Retrofit和OkHttp的使用与优化,包括RESTful API调用、拦截器链、连接池等核心概念;网络架构设计中的分层架构、Repository模式和安全策略;数据存储方案如DataStore、文件存储和加密存储;以及图片加载、大文件传输等进阶主题。文章还提供了完整的Kotlin代码示例,展示Retrofit+OkHttp+协程的最佳实践架构,包

时代我西的博客 391

操作系统实验四 进程运行轨迹的跟踪与统计(哈工大李治军)

实验4 进程运行轨迹的跟踪与统计 实验目的 掌握 Linux 下的多进程编程技术; 通过对进程运行轨迹的跟踪来形象化进程的概念; 在进程运行轨迹跟踪的基础上进行相应的数据统计,从而能对进程调度算法进行实际的量化评价,更进一步加深对调度和调度算法的理解,获得能在实际操作系统上对调度算法进行实验数据对比的直接经验。 实验内容 进程从创建(Linux 下调用 fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上就表现为进程状态的多次切换,如进程创建以后会成为就绪态;当该进程被调度以

leoabcd12的博客 4653

PCL学习笔记(四)——点云曲面重建

文章目录一、凸包算法二、 单纯的点云数据固然很好,但仅仅是点的话仍是不够的,因为这不利于我们后续对实体对象的渲染与各种应用,如计算实体的表面积、体积等等,所以这就需要我们对实体进行点云的曲面重建。 一、凸包算法 在计算机图形学中,凸包是指在任意维空间中包含某一有限点集P的最小凸集,它由凸包顶点构成,在二维上其表现形式为一个凸多边形,而在三维上则表现为一个凸多面体。在二维层面上来讲,最常用的凸包算法有Graham扫描法和Jarvis卷包裹法,但前者并不适用于三维层面,后者倒是可以扩展到三维。除此之外,在

dayuhaitang1的博客 6912

【Android】使用Room数据库解决本地持久化

Android数据库的使用

2301_79344902的博客 1844

手把手入门YOLO到改进优化教程 | 【P2基础篇】数据标注+环境搭建+模型训练

本节以 YOLO自定义目标检测训练为主线,带你从零开始完成 数据标注 → 环境搭建 → 模型训练 → 结果的流程。使用labelImg制作符合YOLO格式的自定义数据集。在Windows基于 Python + Conda 创建独立虚拟环境,安装 PyTorch + CUDA + ultralytics,配置 PyCharm 运行环境。本节不仅使用自定义数据集进行演示,同时也用公开数据集训练 YOLO,查看训练日志与可视化结果(PR、P/R/F1 曲线等)等。

嵌入式阿齐Archie 2万+

Material CalendarView 与 Room 数据库集成终极指南:实现高效持久化存储

Material CalendarView 是一款遵循 Material Design 规范的 Android 日历视图组件,它提供了美观的界面和丰富的交互功能。本指南将带你完成 Material CalendarView 与 Room 数据库的完整集成方案,实现日历数据的持久化存储,让你的应用能够高效管理用户日程和事件数据。🚀 ## 为什么需要 Material CalendarView 与

gitblog_00494的博客 1134

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Android数据存储及Room数据库的使用和原理分析

介绍Room的基本使用

m0_53185556的博客 2126

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

上一篇: Room-数据持久化存储(入门)
下一篇: Kotlin Coroutine(协程): 一、样例
孟老板007
博客等级 码龄5年 19粉丝 27原创
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值