Android JetPack Compose+Room----实现搜索记录功能

文章目录

  • 需求概述
  • 功能展示
  • 实现搜索功能使用的技术
    • 1.Android Jetpack room
    • 2.Android JetPack Compose
  • 代码实现
    • 编写搜索界面
    • 接入Room实现搜索功能的管理
      • 引入依赖
      • 定义包结构
      • 定义操作表的Dao类
      • 定义数据库的基础配置
      • 定义数据库的Dao管理类
      • 使用
      • 数据库升级
  • 源码地址

需求概述

搜索功能是很多APP都会重点维护的功能,因为搜索功能可以很好的帮助用户找到自己需要的内容,电商APP上,用户可以使用搜索功能快速找到自己想要的商品,社交App上,用户可以使用搜索功能快速找到对应的好友和内容以及使用浏览器快速搜索自己想要知道的问题答案等…。所以搜索功能的使用频率是很高的,所以搜索功能的用户体验也就相当重要,如果搜索功能只是提供搜索的话,有点美中不足,但是如果能加上搜索记录的管理就很好了。目前基本上所有的带有搜索的APP都会带有搜索记录的功能。试想下如果没有搜索记录的功能,用户搜索完自己想要的东西后,如果再次进入到搜索页还想搜索之前的内容就需要再次输入对应的关键字,关键字短还好,假如关键字很长,或者输入的是链接,那用户输入起来就太麻烦了。所以搜索记录功能的重要性不言而喻。本文就是要实现一个搜索记录的管理功能,包括显示搜索记录,删除单条搜索记录,删除全部搜索记录的功能同时实现一个搜索页面。

功能展示

在这里插入图片描述

如上面的gif图展示的一样,搜索界面有一个输入框,在输入框中输入我们的关键词点击搜索,这是就会产生一个搜索记录,这些搜索记录会以sqlite数据库的方式保存起来,当再次打开搜索界面后就会显示之前的搜索记录。用户可以点击搜索记录开始搜索,也可以删除不想要的搜索记录,或者是清除所有的搜索记录。搜索页面中有些细节需要关注下,刚进入搜索页面的时候会默认拉起键盘,输入框中的清除搜索内容按钮是当有输入内容的时候才展示,否则不展示,搜索小图标的颜色也是有输入内容的时候才会显示得更加清晰。这些功能会在代码实现的部分讲解

实现搜索功能使用的技术

1.Android Jetpack room

因为搜索记录需要持久化存储到手机里面,我们可以选择文件,shared perference,Sqlite数据库,这里选择Sqlite数据库无疑是最合适的,但是见过很多的小伙伴却在数据库和文件以及SP中选择了文件和SP,原因肯定是和Sqlite数据库的使用比较繁琐,甚至还涉及到数据库升级的问题。面试的时候很多小伙伴肯定都会被问到数据库升级的问题。因为数据库的升级如果处理不好,就会导致APP闪退,所以很多小伙伴选择了更为稳妥的方式。但是Android jetpack 的ROOM出现后,这一切都变得简单了,Room库在 SQLite 上提供了一个抽象层,充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。Room 提供针对 SQL 查询的编译时验证并提供方便注解,可最大限度减少重复和容易出错的样板代码并且还简化了数据库迁移升级。可以说非常的好用。搜索记录选择它持久化也非常方便,因为搜索记录会涉及到排序,删除,限制搜索记录的条数,逻辑删除等功能,使用sqlite数据库无疑是最佳选择。

2.Android JetPack Compose

Compose是Android 推出的新一代UI框架,是一种声明式的UI框架,本文涉及的搜索功能的界面全部都由Compose开发,Compose基于Kotlin的DSL语言 做界面的UI描述,UI表达能力丝毫不逊色于XML。使用Compose,我们再也不用写XML布局和findViewByID了。建议读者去了解下Compose UI。

代码实现

编写搜索界面

搜索界面主要就是包括一个输入框,返回按钮,和展示搜索记录的部分,先定义搜索界面的Composable函数,并且定义好对应的事件回调,这样做的好处是可以让我们的程序符合单向数据流的结构,我们让数据单向流向UI,而UI的更新通过事件的方式来通知数据源头更新数据,而数据源头更新数据后,由于Compose的State是一种基于观察者模式的状态,所以当State状态更新的时候,UI会自动重组更新。所以我们需要定义一个SearchHistoryState,如下所示:

@Stable
data class SearchHistoryState(var history_id: Long = -1L,var history_title: String = "",var history_content:  String = "",var createTime: Long = System.currentTimeMillis(),var isDelete:  String = "0"
)

搜索界面以回调的方式向调用者提供目前搜索界面中执行的操作,这样做可以使我们的搜索界面的复用性更高,也让搜索界面的职责更加单一,不用承担数据的更新操作。

@Composable
fun SearchPage(searchHistoryList: MutableList<SearchHistoryState>,onBackClick: () -> Unit,onSearchTrigger: (String) -> Unit,onClearAllClick: () -> Unit,onDeleteClick: (SearchHistoryState) -> Unit,onHistoryItemClick: (SearchHistoryState) -> Unit
) {....
}

搜索界面也很简单页面分解如图所示:
在这里插入图片描述
一个纵向布局,绿色框中是一个横向布局,包括搜索框和一个返回按钮,在红色框里面就是一个纵向布局,包括显示近期浏览,全部删除按钮的头部分和展示索记录的列表部分,界面的代码就不贴了,太多了,文章结尾会给源码:

接入Room实现搜索功能的管理

引入依赖

// zoom sqlite jetpack组件val roomVersion = "2.6.1"implementation("androidx.room:room-runtime:$roomVersion")implementation("androidx.room:room-ktx:$roomVersion")implementation("androidx.room:room-paging:$roomVersion")ksp("androidx.room:room-compiler:$roomVersion")

注意这里的KSP需要引入对应的插件

plugins {id("com.android.application")id("org.jetbrains.kotlin.android")id("com.google.devtools.ksp").version("1.9.20-1.0.14") // 需要与Android的构建插件的版本相对应
}

定义包结构

如下图所示,我们先定义几个包,方便后面我们编写对应的代码,dao用来放操作数据库的方法,entitiy用于存放我们的定义的实体类,repository是我们管理dao的操作的类,调用者可以通过它获取各种dao操作接口去操作对应的表数据。
在这里插入图片描述
我们首先应该定义的是entity,搜索记录的entity如下所示:

@Keep //防止混淆的时候将我们的实体类混淆,导致无法找到
@Entity(tableName = "t_search_history") // 定义sqlite数据库中表的名字,后面操作搜索记录时就操作这个表
data class SearchHistory(@PrimaryKey(autoGenerate = true)@ColumnInfo(name = "id")val id: Long = 0L,@ColumnInfo(name = "title")val title: String,@ColumnInfo(name = "webUrl")val webUrl: String,@ColumnInfo(name = "create_time")val createTime: Long = 0L,// 是否已经删除,0表示未删除,1表示已删除@ColumnInfo(name = "isDelete")var isDelete: String = "0",
)

注意:我们在定义entity的类时,会映射成数据库中的表,这里就会涉及的到插入数据的记录时的id自动生成的问题,我们定义ID的时候需要将其默认值定义为0,而不是其他的

 @PrimaryKey(autoGenerate = true)@ColumnInfo(name = "id")val id: Long = 0L,

如果定义成-1或者是其他的会导致无法插入记录,因为ID没有自增

定义操作表的Dao类

对数据进行查询,删除,更新等操作我们定义一个Dao类来实现,代码如下所示:

@Dao
interface SearchHistoryDao {
// 限制搜索记录为10条,这就是使用sqlite数据库的优势之一。可以随意变换显示的条数,并且可以通过时间排序。@Query("select * from t_search_history where isDelete = 0 order by create_time desc limit 10")fun getAllHistory():MutableList<SearchHistory>@Insert(onConflict = OnConflictStrategy.REPLACE)suspend fun insertHistory(history: SearchHistory):Long@Query("delete from t_search_history")suspend fun clearAll()@Transaction@Query("select * from t_search_history where id=:id")suspend fun findSearchHistoryById(id:Long):SearchHistory?@Updatesuspend fun update(searchHistory: SearchHistory)
}

定义数据库的基础配置

定义完entity和Dao类后,我们就可以开始定义数据库的对应配置了,之所以将这步放到entity和dao之后是因为这步需要用到entity和dao,代码如下所示:

@Database(version = 1,//数据库的版本,这里在数据库迁移升级的时候需要改变entities = [SearchHistory::class,] // 和表相互映射的实体类
)
abstract class AppSqliteDataBase:RoomDatabase(){abstract fun searchHistoryDao():SearchHistoryDao // 定义获取Dao操作类的抽象方法。
}// 数据库的初始化配置类,使用Room数据库时我们需要先初始化它。在我们的Application中调用AppDB.init(Context)
// 就可以初始化数据库了
class AppDB{companion object{fun init(context: Context):AppSqliteDataBase{val databaseBuilder = Room.databaseBuilder(context = context,klass = AppSqliteDataBase::class.java,// 数据库配置类name = "SearchDB" // 数据库的名字).apply {fallbackToDestructiveMigration()}return databaseBuilder.build()}}
}

定义数据库的Dao管理类

我们的项目中可能会有很多的数据库表,每个表都会有一个Dao操作接口类,所以我们需要一个类去管理这些接口,这就是我们的Repository类。如下所示:

class SearchHistoryRepository(private val db: AppSqliteDataBase) {/*** 获取搜索列表*/fun getSearchHistoryList(): MutableList<SearchHistory> {return db.searchHistoryDao().getAllHistory()}/*** 新增搜索历史记录*/suspend fun insertHistory(searchHistory: SearchHistory) {val oldHistory = db.searchHistoryDao().findSearchHistoryById(searchHistory.id)if (oldHistory != null) {db.searchHistoryDao().update(searchHistory)} else {db.searchHistoryDao().insertHistory(searchHistory)}}/*** 通过ID删除历史记录*/suspend fun deleteById(id: Long) {val searchHistory = db.searchHistoryDao().findSearchHistoryById(id)if (searchHistory != null) {// 将删除的标志更新成1,表示已经删除searchHistory.isDelete = "1"db.searchHistoryDao().update(searchHistory)}}/*** 更新历史记录*/suspend fun updateHistory(searchHistory: SearchHistory) {db.searchHistoryDao().update(searchHistory)}/*** 清除历史记录*/suspend fun clearAllHistory() {db.searchHistoryDao().clearAll()}
}

使用

定义好了对应的接口后,我们就可以在ViewModel中使用了。

class SearchHistoryViewModel(db: AppSqliteDataBase) : ViewModel() {private val TAG = "SearchHistoryViewModel"var searchHistoryRepo: SearchHistoryRepository = SearchHistoryRepository(db = db)var searchHistoryStateList = mutableStateListOf<SearchHistoryState>() // 使用// compose的StateAPI,当数据更新时,界面会自动重组更新fun loadHistoryList() {Log.d(TAG, "loadHistoryList")viewModelScope.launch(Dispatchers.IO) {searchHistoryRepo.getSearchHistoryList().forEach { searchHistory: SearchHistory ->Log.d(TAG,"loadHistoryList: $searchHistory")val searchHistoryState = SearchHistoryState(history_id = searchHistory.id,history_title = searchHistory.title,history_content = searchHistory.webUrl,createTime = searchHistory.createTime,isDelete = searchHistory.isDelete)searchHistoryStateList.add(searchHistoryState)}}}fun deleteHistory(searchHistoryState: SearchHistoryState) {Log.d(TAG, "deleteHistory: $searchHistoryState")viewModelScope.launch(Dispatchers.IO) {searchHistoryStateList.remove(searchHistoryState)searchHistoryStateList.sortBy { it.createTime }searchHistoryRepo.deleteById(searchHistoryState.history_id)}}fun addHistory(searchHistoryState: SearchHistoryState) {Log.d(TAG, "deleteHistory: $searchHistoryState")if(searchHistoryStateList.size == 10){searchHistoryStateList.removeLast()}viewModelScope.launch(Dispatchers.IO) {searchHistoryStateList.add(searchHistoryState)searchHistoryStateList.sortBy { it.createTime }val searchHistory = SearchHistory(title = searchHistoryState.history_title,webUrl = searchHistoryState.history_content,createTime = searchHistoryState.createTime)searchHistoryRepo.insertHistory(searchHistory)}}fun clearAllHistory() {Log.d(TAG, "clearAllHistory")searchHistoryStateList.clear()viewModelScope.launch {searchHistoryRepo.clearAllHistory()}}fun updateHistory(searchHistoryState: SearchHistoryState){viewModelScope.launch {val searchHistory = SearchHistory(title = searchHistoryState.history_title,webUrl = searchHistoryState.history_content,createTime = searchHistoryState.createTime)searchHistoryRepo.updateHistory(searchHistory)}}
}

在Activity中,初始化ViewModel根据搜索页面中触发的事件去做对应的搜索记录操作。

  SearchPage(searchHistoryViewModel.searchHistoryStateList,onBackClick = { finish() },onSearchTrigger = { url ->if (url.isNotEmpty()) {val searchHistoryState = SearchHistoryState(history_title = url,history_content = url,createTime = System.currentTimeMillis())searchHistoryViewModel.addHistory(searchHistoryState)}},onClearAllClick = {searchHistoryViewModel.clearAllHistory()},onDeleteClick = { searchHistoryState ->Log.d(TAG, "onDeleteClick=>searchHistoryState: $searchHistoryState")searchHistoryViewModel.deleteHistory(searchHistoryState)},onHistoryItemClick = { searchHistoryState ->Log.d(TAG, "onHistoryItemClick=>searchHistoryState: $searchHistoryState")val content = searchHistoryState.history_contentval searchHistory = SearchHistoryState(history_title = content,history_content = content,createTime = System.currentTimeMillis())searchHistoryViewModel.updateHistory(searchHistory)})

数据库升级

数据库升级就是我们发布了app的第一个版本,这个版本上只有搜索记录的数据库表t_searchhistory,然后我们打算发布app的第二个版本,在第二个版本上我们新增了数据库的表t_test,或者是修改了t_searchhistory的字段,这时如果用户更新我们的app第二个版本时,由于数据库中没有我们新增的第二张表,这就会导致出现下面的异常导致APP直接闪退。
在这里插入图片描述
所以需要我们做数据库的升级迁移,当用户安装我们第二个app版本时,我们将更新的表更新到用户的本地数据库中,我们在项目中新建一个TestEntity演示数据库的迁移升级,定义的过程和我们的搜索记录的定义过程一样,不同的点在于。我们需要新建一个Migration类去管理我们的升级版本,如下所示:

val MIGRATION_1_2 = object : Migration(1,2){override fun migrate(db: SupportSQLiteDatabase) {db.execSQL("CREATE TABLE IF NOT EXISTS `t_test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)")}
}

比如我们新增了一张表,就像上面的写法一样。有读者可能会决定Sqlite语句的写法有难度,还容易错,这里有个很好的办法,因为Room是使用注解去生成代码的,所以我们定义好我们的功能后,构建下项目,然后去到生成的代码中复制对应的Sqlite代码就可以了。比如本例中生成的代码如下:
在这里插入图片描述然后就是配置AppSqliteDataBase,配置对应的升级策略和版本号,如下所示:

@Database(version = 2,//数据库的版本升级到2entities = [SearchHistory::class,TestEntity::class] // 和表相互映射的实体类
)
abstract class AppSqliteDataBase:RoomDatabase(){abstract fun searchHistoryDao():SearchHistoryDaoabstract fun testEntityDao():TestDao
}class AppDB{companion object{fun init(context: Context):AppSqliteDataBase{val databaseBuilder = Room.databaseBuilder(context = context,klass = AppSqliteDataBase::class.java,name = "SearchDB").apply {fallbackToDestructiveMigration()addMigrations( // 数据库升级迁移MIGRATION_1_2 // 将我们的新版APP的新增的数据库操作配置到这里就可以了)}return databaseBuilder.build()}}
}

为了验证我们的数据库是否升级成功,我们在SearchHistoryViewModel的loadHistoryList中加入如下的测试代码:

   fun loadHistoryList() {Log.d(TAG, "loadHistoryList")viewModelScope.launch(Dispatchers.IO) {searchHistoryRepo.getSearchHistoryList().forEach { searchHistory: SearchHistory ->Log.d(TAG,"loadHistoryList: $searchHistory")val searchHistoryState = SearchHistoryState(history_id = searchHistory.id,history_title = searchHistory.title,history_content = searchHistory.webUrl,createTime = searchHistory.createTime,isDelete = searchHistory.isDelete)searchHistoryStateList.add(searchHistoryState)}searchHistoryRepo.insertTest(TestEntity(name = "walt"))searchHistoryRepo.insertTest(TestEntity(name = "zhong"))searchHistoryRepo.insertTest(TestEntity(name = "007"))searchHistoryRepo.getTestList().forEach {Log.d(TAG,"result: $it")}}}

运行结果如下表示我们数据库升级成功了。完整的例子请参考源码。
在这里插入图片描述

源码地址

为了方便读者熟悉Room的使用,在此贴上源码,建议读者下载源码自己动手实现一遍,后面遇到相关的需求时就可以快速搞定了。这个仓库我以后涉及到jetpack的使用时都会更新,欢迎读者克隆更新,相互参考学习。有问题欢迎评论区交流。
搜索记录功能的源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/822330.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

台灯目前口碑最好的护眼灯有哪些?分享多款高口碑护眼灯

根据数据表明中国近视患者人数已达到7亿&#xff0c;关于视力问题&#xff0c;而儿童及中小学生居多&#xff0c;关于视力问题&#xff0c;很多人都归咎于幼时学习时的用眼时间过长&#xff0c;但这只是其中的一部分&#xff0c;不合适的光源环境影响也是非常大的&#xff0c;尤…

006Node.js cnpm的安装

百度搜索 cnpm,进入npmmirror 镜像站https://npmmirror.com/ cmd窗口输入 npm install -g cnpm --registryhttps://registry.npmmirror.com

Fluke ADPT连接器(隔离版)----发布2

代替手工记录、记录后在整理的麻烦&#xff0c;轻点鼠标&#xff08;单次采集、自动时间间隔采集自由选择&#xff09;即可完成&#xff0c;测试数据导出图片、导出数据到EXCEL文件随意选择&#xff1b; 所需设备&#xff1a; 1、Fluke ADPT连接器&#xff1b;内附链接 ● …

基于小程序实现的餐饮外卖系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

闲不住,手写一个数据库文档生成工具

shigen坚持更新文章的博客写手&#xff0c;擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长&#xff0c;分享认知&#xff0c;留住感动。 个人IP&#xff1a;shigen 逛博客的时候&#xff0c;发现了一个很有意思的文章&#xff1a;数据库表结构导…

xxl-job使用自动注册节点,ip不对,如何解决????

很明显这时我们本机的ip和我们xxl-job自动注册的ip是不一致的&#xff0c;此时该如何处理呢&#xff1f;&#xff1f;&#xff1f;&#xff1f; 方法一&#xff1a;在配置文件中&#xff0c;将我们的ip固定写好。 ### xxl-job executor server-info xxl.job.executor.ip写你的…

解决EasyPoi导入Excel获取不到第一列的问题

文章目录 1. 复现错误2. 分析错误2.1 导入的代码2.2 DictExcel实体类2.2 表头和标题3. 解决问题1. 复现错误 使用EasyPoi导入数据时,Excel表格如下图: 但在导入时,出现如下错误: name为英文名称,在第一列,Excel表格有值,但导入的代码中为null,就很奇怪? 2. 分析错误 …

线上频繁fullgc问题-SpringActuator的坑

整体复盘 一个不算普通的周五中午&#xff0c;同事收到了大量了cpu异常的报警。根据报警表现和通过arthas查看&#xff0c;很明显的问题就是内存不足&#xff0c;疯狂无效gc。而且结合arthas和gc日志查看&#xff0c;老年代打满了&#xff0c;gc不了一点。既然问题是内存问题&…

翱途O2OA新手上路-服务器下载及私有云部署

本篇主要简要描述从官网下载服务器&#xff0c;进行部署&#xff0c;启动的过程&#xff0c;并且描述在部署过程中常见的问题与报错以及云服务器安全策略配置和O2OA服务器端口修改的方式。 O2OA部署的服务器要求不高&#xff0c;一般使用4C8G以上的服务器均可正常运行。 一、检…

Spark_SparkSql写入Oracle_Undefined function.....将长字符串写入Oracle中方法..

在使用Spark编写代码将读库处理然后写入Oracle中遇到了诸多小bug,很磨人&#xff0c;好在解决了。shit!! 实测1&#xff1a;TO_CLOB(a3) 代码样例 --这是一个sparksql写入hive的一个小逻辑&#xff0c;我脱敏了噻 SELECT a1, a2, TO_CLOB(a3) AS clob_data, TO_DATE(a4) AS …

【C语言】每日一题,快速提升(4)!

&#x1f525;博客主页&#x1f525;&#xff1a;【 坊钰_CSDN博客 】 欢迎各位点赞&#x1f44d;评论✍收藏⭐ 题目&#xff1a;实现计算机程序 解答&#xff1a; 该程序运用函数指针数组&#xff0c;具体请看代码 代码&#xff1a; #include <stdio.h> int add(int a…

C语言 | Leetcode C语言题解之第25题K个一组翻转链表

题目&#xff1a; 题解&#xff1a; /* 定义保存两个地址的结构体* 用来保存反转后结果的头节点和尾节点*/ typedef struct {struct ListNode* head; struct ListNode* tail; } TwoAddress; // 反转中间链表 TwoAddress* reverse(struct ListNode* head){struct ListNode* pr…

媒体邀约的好处?怎么邀请媒体?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 媒体邀约的好处主要体现在提高品牌知名度、扩大受众群体以及与媒体建立良好的合作关系。 媒体邀约是一种有效的公关策略&#xff0c;通过吸引媒体关注来促进信息的传播。它可以帮助组织…

MySQL 核心模块揭秘 | 14 期 | 回滚整个事务

回滚整个事务要怎么清除 binlog 日志&#xff0c;InnoDB 又会进行哪些操作&#xff1f; 作者&#xff1a;操盛春&#xff0c;爱可生技术专家&#xff0c;公众号『一树一溪』作者&#xff0c;专注于研究 MySQL 和 OceanBase 源码。 爱可生开源社区出品&#xff0c;原创内容未经授…

第七届机器学习与机器智能国际会议(MLMI 2024)即将召开!

第七届机器学习与机器智能国际会议&#xff08;MLMI 2024&#xff09;将于2024年8月2-4日在日本大阪举行。AI之巅&#xff0c;智慧之源&#xff0c;MLMI 2024将汇聚全球机器学习与机器智能领域的专家学者&#xff0c;分享最新的研究成果和实践经验&#xff0c;共同探讨如何将最…

安防视频监控/视频集中存储EasyCVR平台级联时,下级平台未发流是什么原因?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…

常见面试算法题-九宫格按键输入法

■ 题目描述 九宫格按键输入&#xff0c;判断输出&#xff0c;有英文和数字两个模式&#xff0c;默认是数字模式&#xff0c;数字模式直接输出数字&#xff0c;英文模式连续按同一个按键会依次出现这个按键上的字母&#xff0c;如果输入”/”或者其他字符&#xff0c;则循环中…

基于Echarts的超市销售可视化分析系统(数据+程序+论文

本论文旨在研究Python技术和ECharts可视化技术在超市销售数据分析系统中的应用。本系统通过对超市销售数据进行分析和可视化展示&#xff0c;帮助决策层更好地了解销售情况和趋势&#xff0c;进而做出更有针对性的决策。本系统主要包括数据处理、数据可视化和系统测试三个模块。…

SpringBoot相关知识点总结

1 SpringBoot的目的 简化开发&#xff0c;开箱即用。 2 Spring Boot Starter Spring Boot Starter 是 Spring Boot 中的一个重要概念&#xff0c;它是一种提供依赖项的方式&#xff0c;可以帮助开发人员快速集成各种第三方库和框架。Spring Boot Starter 的目的是简化 Sprin…

椭圆曲线密码学(ECC)基本介绍和总结

背景 ECC英文全称"Elliptic Curve Cryptography"&#xff0c;其背后的密码学原理或者说安全性&#xff0c;是基于椭圆曲线离散对数问题&#xff08;Elliptic Curve Discrete Logarithm Problem&#xff0c;ECDLP&#xff09;。ECC密码学被普遍认为是RSA密码系统的接…