不到一周我开发出了属于自己的知识共享平台
前言:
近期在CSDN平台上开设了一个个人专栏《从0搭建项目》,从而来向大家分享自己是如何一步步搭建项目的,以及记录在项目开发中遇到的一些问题是如何解决的。 并且该专栏中的搭建的项目将全部开源,供大家参考学习。凭借这个由头,也是引来了不少志同道合的同学的支持,并且在这过程中也是帮助解决了不少同学开发中遇到的问题,不仅如此,也有不少大佬对我开源的项目中存在的不足进行了指点以及修正。随后就在想,反正都是在平台上分享知识和资源,那为什么自己不能搭建一个资源分享的平台呢?想到这里,脑子一热拍一拍大腿”干就完了!“。紧接着就开始技术选型和着手数据库的设计,经过了比较漫长的一段时间也是慢慢的把项目搭建起来了。
🏡 博客首页:派 大 星
⛳️ 欢迎关注 🐳 点赞 🎒 收藏 ✏️ 留言
🎢 本文由派大星原创编撰
🚧 系列专栏:项目从0搭建
🎈 本系列项目从设计到实现源码全部开源免费学习使用,一起追向理想,欢迎各位大佬监督打卡开发!
🍌 难度分析
后端的难度主要在于使用Redis来实现点赞功能
等部分操作以及其中的一些逻辑校验
的处理,再对Redis缓存的数据定时进行持久化操作。
🍉 实现点赞功能简易流程图
对上图稍作解释:
- 参数校验 :对入参进行非空校验
- 逻辑校验 :对于用户点赞,要对操作逻辑进行校验,用户不能重复点赞同一个资源项,对于取消点赞,
用户则不能取消未进行点赞的资源项 - 存入Redis :存入Redis的数据主要是资源的点赞数,某个资源的点赞数,用户点赞的资源项
- 定时任务 :通过定时任务,从Redis读取数据并且持久化到MySQL中【本项目每两个小时执行一次】
🍍 项目回顾(知识分享平台)
🍇 最终效果演示
🌽 技术选型:
- 🍅 SpringBoot
- 🥒 MySQL
- 🍑 MyBatis-Plus
- 🍋 Redis
- 🍊 Swagger
- 🍆 阿里云Oss存储对象实现文件上传
- 🍉 Vue
- 🦞 Axios
- 🍰 Vue Router
- 🍨 Vue AntDesign
🥝 项目需求分析
用户通过该平台可以搜索自己想要查询到的资源,同时也可以把自己拥有以及保存的学习资料进行无偿的开源给使用平台的用户。用户上传的资源是需要后台管理员审核后选择某资源是否上线以及可以对自己感兴趣的资源进行点赞收藏到自己的个人中心以防后续难以查询。
🌾 搭建项目
1. 导入部分Maven依赖
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--commons-pools2 对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--ES-->
<!-- <dependency>-->
<!-- <groupId>org.elasticsearch.client</groupId>-->
<!-- <artifactId>elasticsearch-rest-high-level-client</artifactId>-->
<!-- <version>7.13.2</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!--阿里云oss依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.4.2</version>
</dependency>
<!--日期工具栏依赖-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.5</version>
</dependency>
......
2. 配置文件编写
3. 项目初期基本配置
① SwaggerAPI文档的使用与配置
对于典型的前后端项目来说,文档的编写是必不可少的,因为对于这种项目的架构在公司中一般是由两班人马联合开发:前端组和后端组。这种模式的开发有什么好处呢?简单来说可以并行开发,大大减少了项目的开发周期,前端不需要等后端的接口,而后端也不需要等待前端页面才能测试响应接口是否正确,如果没有前端页面的配合那么后端组又该如何测试接口是否正确呢,解决方案也就是现在介绍的SwaggerAPI文档,可以帮助后端人员大大减少等待时间,而且可以完美的符合我们的要求。
@Configuration
@EnableSwagger2
public class SwaggerConfig {
private Boolean enable = true;
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.pdx.controller"))
.paths(PathSelectors.any())
.build()
.enable(enable);
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("派大星在线图书室")
.description("派大星在线图书室")
.termsOfServiceUrl("")
.version("1.0")
.build();
}
}
② 配置异常处理类以及响应码
对于一个项目来说,不管是使用Thymeleaf
模板渲染还是JSP
搭建的项目,配置异常处理类都是不可忽略的存在,它可以对我们的操作出现异常的业务进行精准的捕获以及反馈出来,以至于我们可以对特定的异常做出特定的处理,而避免使用户看到一些不友好的页面或场景比如:404、403
等等。而响应码的配置更是前后端分离开发的一大特点,我们在前端请求数据时可以根据后端提示的响应码来向用户显示一些友好的提示,但是响应码的设定是需要在项目启动前就要进行统一的,不能存在一个接口前端使用某个自定义响应码进行判断,而后端使用另一响应码进行判别和处理,最终会造成一些不必要的问题。
异常处理类:
@RestControllerAdvice
@Slf4j
public class RestExceptionHandler {
@ExceptionHandler(value = Exception.class)
public DataResult exception(Exception e){
log.error("Exception====>{}",e.getLocalizedMessage(),e);
return DataResult.getResult(BaseResponseCode.SYSTEM_ERROR);
}
@ExceptionHandler(value = BusinessException.class)
public DataResult businessException(BusinessException e){
log.error("businessException ====>{}",e.getLocalizedMessage(),e);
return DataResult.getResult(e.getCode(),e.getMsg());
}
/**
* 框架异常
* @param e
* @param <T>
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
<T> DataResult<T> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
log.error("methodArgumentNotValidExceptionHandler bindingResult.allErrors():{},exception:{}", e.getBindingResult().getAllErrors(), e);
List<ObjectError> errors = e.getBindingResult().getAllErrors();
return createValidExceptionResp(errors);
}
private <T> DataResult<T> createValidExceptionResp(List<ObjectError> errors) {
String[] msgs = new String[errors.size()];
int i = 0;
for (ObjectError error : errors) {
msgs[i] = error.getDefaultMessage();
log.info("msg={}",msgs[i]);
i++;
}
return DataResult.getResult(BaseResponseCode.METHOD_IDENTITY_ERROR.getCode(), msgs[0]);
}
}
系统响应码:(推荐使用枚举类型)
public enum BaseResponseCode implements ResponseCodeInterface{
/*
* code=0: 服务器已成功处理了请求。通常,这表示服务器提供了请求的
* code=20x: 系统主动抛出的业务异常
* code=301: 系统异常
* */
SUCCESS(0,"操作成功"),
SYSTEM_ERROR(301,"系统异常,请稍后再试.."),
METHOD_IDENTITY_ERROR(201,"数据校验异常"),
ACCOUNT_LOCKED(202,"用户已被锁定"),
ACCOUNT_PASSWORD_ERROR(203,"用户密码错误"),
CAPTCHA_EXPIRED(204,"验证码已过期"),
CAPTCHA_IS_ERROR(205,"验证码错误"),
PASSWORD_IS_EMPTY(206,"密码不能为空"),
OPERATION_ERROR(207,"操作异常"),
PARAM_CAN_NOT_NULL(212,"参数不能为空"),
LIKE_BOOK_IS_EXISTS(213,"重复点赞"),
;
/*
* 响应码
* */
private int code;
/*
* 提示
* */
private String msg;
BaseResponseCode(int code,String msg){
this.code = code;
this.msg = msg;
}
@Override
public int getCode() {
return code;
}
@Override
public String getMsg() {
return msg;
}
}
③ 点赞业务逻辑实现
对于点赞功能,其实是一个很有意思的功能,基本的实现思路有两种:一种是直接对数据库进行操作,另一种则是利用点赞的业务特点将其放到Redis
中,然后再将缓存的数据同步到MySQL
数据库中即可!
简单介绍二者的利弊:
- 直接写入MySQL
直接写入MySQL是最简单的做法。只需要设计两张表A表:用于记录资源被点赞次数
,B表:记录用户赞过哪些资源项
缺点:
- 数据库读写压力大,当一个资源特别受欢迎,那么点赞的用户就会特别多,甚至是短时间内被大量点赞,直接操作数据库并不是很理想的方案。
- 使用Redis存储定时同步到数据库
首先Redis主要的特点就是快!毕竟是单线程而且存储在内存中,而且Redis还支持多种数据类型,比如:Hash、set、zset
等等。
优点:
- 性能高
- 缓解数据库读写压力
缺点:
- 开发复杂,逻辑比较绕脑子
- 不能保证数据的安全性,如果Redis某段时间挂掉了可能会丢失数据以及同步数据库不及时,可能会导致数据被淘汰。
下面来看具体实现(使用Hash):
1. 编写Controller前端控制器:
@PostMapping("/{bookId}")
@ApiOperation(value = "点赞某个书籍")
public DataResult likeBooks(@PathVariable("bookId")String bookId){
User user = (User) redisTemplate.opsForValue().get(Constant.USER_LOGIN);
likeService.likeBook(user.getId(),bookId);
return DataResult.success(bookId);
}
2. 业务层具体实现操作
@Autowired
private RedisTemplate redisTemplate;
/**
* 用户点赞书籍key
*/
private static final String USER_LIKE_BOOKS_KEY = "USER:LIKE:ARTICLE";
/**
* 书籍被点赞的key
*/
private static final String BOOKS_LIKED_USER_KEY = "ARTICLE:LIKED:USER";
/**
* 用户点赞某个书籍资源
* @param id 点赞用户
* @param bookId 书籍ID
*/
@Override
public void likeBook(String id, String bookId) {
// 入参校验 -----> 判断参数是否为空
validateParam(id,bookId);
log.info("点赞数据存入redis开始,bookId:{},userId:{}",bookId,id);
// 只有未点赞的用户才可以进行点赞 -----> 点赞逻辑
likeBooIdLogicValidate(bookId,id);
synchronized (this){
//1. 用户喜欢的书籍+1
String userLikeResult = (String) redisTemplate.opsForHash().get(USER_LIKE_BOOKS_KEY, id);
Set<String> bookIdSet = userLikeResult == null? new HashSet<>():FastJsonUtils.deserializeToSet(userLikeResult,String.class);
bookIdSet.add(bookId);
redisTemplate.opsForHash().put(USER_LIKE_BOOKS_KEY,id,FastJsonUtils.serialize(bookIdSet));
//2. 书籍点赞数+1
String bookLikeResult = (String) redisTemplate.opsForHash().get(BOOKS_LIKED_USER_KEY, bookId);
Set<String> userIdSet = bookLikeResult == null? new HashSet<>() :FastJsonUtils.deserializeToSet(bookLikeResult,String.class);
userIdSet.add(id);
redisTemplate.opsForHash().put(BOOKS_LIKED_USER_KEY,bookId,FastJsonUtils.serialize(userIdSet));
log.info("取消点赞存储redis结束,bookId:{},userId:{}",bookId,id);
}
}
在实现用户点赞操作时使用了synchronized
同步锁来对用户的操作进行上锁处理,避免同一时间内多个用户对同一资源进行点赞数据出现丢失或者错误 ❌
❗️❗️❗️注意事项
本项目采用的是前后端分离的模式进行开发,所以在启动前后端项目前先配置nginx
进行反向代理,避免出现跨域方面的错误 ❌
配置如下:
# 派大星在线阅读室
server {
listen 9000;
server_name localhost;
location ~ /pdx/{
proxy_pass http://localhost:8000;
}
}
⭐️ 项目总结
项目总体搭建下来遇到最麻烦的点在于点赞业务的实现与逻辑的校验处理,第一次真正使用Redis来实现一些比较常见而且复杂的业务逻辑,也算是不枉熬了几个大夜终于把其中的逻辑梳理清晰并且最终得以实现,通过这么一个稍微复杂的业务流程,也算是给自己的开发前期做了一个稍难需求的铺垫,思维逻辑上的认可😊
新鲜出炉的代码将会及时更新到Gitee
仓库
以上代码属于部分实现,想要了解完整版请移步派大星的Gitee仓库
❗️❗️❗️时刻保持对新鲜想法的热情❗️❗️❗️
- 点赞
- 收藏
- 关注作者
评论(0)