大一第一个独自完成的springboot项目
关于图书馆系统
前言
这是第一个我独自开发的Springboot+vue的一个练习项目。当然vue前端主要借助ai工具进行开发。
写这个项目的主要目的就是巩固一下,前一个多月前刚刚学的springboot,虽然看着视频教程写了一个关于外卖的项目,但是我感觉还是不够的,所有就有一个自己开发一个练习项目的想法,并且可以提高自己的项目经验,从结果来看这两点都已实现,并且我感觉是超预期的,做这个项目也可以发现自己的不足,然后针对性学习,下面将介绍这个项目的具体功能以及开发中遇到的难点和解决办法。
开发前期
在到学校的第一天我就有了这个项目的初步想法,比如我在想前端怎么实现感觉不同的身份跳转不同的页面呢?当然这个问题ai帮我解决,解决办法就是使用vue的路由,登录时后端发送用户的基本数据,其中就包括用户的身份,然后根据用户的身份跳转就好了。当然还要配置路由守卫,防止不同身份的用户访问其他的页面。代码:
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes: [
...publicRoutes,
...userRoutes,
...adminRoutes,
// 默认重定向到登录页 (或根据登录状态)
{
path: "/",
redirect: () => {
const isAuthenticated = !!localStorage.getItem('user');
const role = localStorage.getItem('role');
if (isAuthenticated) {
return role === 'admin' ? '/manager' : '/user/books';
}
return '/login';
}
},
// 404处理(最后定义)
{
path: "/:pathMatch(.*)*",
redirect: "/login"
}
]
});
// 路由守卫
router.beforeEach((to, from, next) => {
const role = localStorage.getItem('role');
const isAuthenticated = !!localStorage.getItem('user');
// 未认证用户访问非登录页或者非注册页 → 登录页
if (!isAuthenticated && to.path !== '/login' && to.path !== '/register') {
return next('/login');
}
// 权限控制:管理员可以访问管理页面,普通用户只能访问用户页面
if (isAuthenticated) {
if (role === 'admin') {
// 管理员试图访问用户页面,重定向到管理页面
if (to.path.startsWith('/user')) {
return next('/manager');
}
} else {
// 普通用户试图访问管理页面,重定向到用户页面
if (to.path.startsWith('/manager')) {
return next('/user/books');
}
}
}
// 其他情况,允许导航
next();
});
上面是开发前端时的主要问题。
回到了springboot,既然要开发图书馆系统,就一定要有图书,存储图书的数据就要用数据库,数据库这里选择的是目前最流行的MySQL数据库,当前花了一个上午的时间设计了项目最基本的表,后面开发的时候还是会发现有些地方设计的有问题,刚开始只设计了8张表,到现在基本完成项目表一共17张了。后面会发现可以写很多扩展的功能:比如:出版社,帖子,评论点赞关系,标签等。
数据库初步设计完了,现在就要来开发了,后端使用的是分模块开发,这样复用性是比较强的。比如这个项目的公共模块我就是用的之前的外卖项目,虽然很多都没有用,但是公共的JWT令牌,OSS存储就不用再去写了。
pojo模块存储实体类还有枚举,server存储的就是服务模块了,服务模块又有mapper,service,controller,还有一些其他的,比如日志,拦截器,AOP,注解,定时任务。最大的收获就是写项目的时候应该一边开发一边写日志,我现在都有点忘记具体遇到哪些问题了,这就非常的尴尬😅。
开发前期主要就是关于用户和图书的开发,先是普通的用户登录,注册,还有修改密码,修改个人信息,管理端需要获取用户的基本信息,还有一些地方需要获取用户列表,只需要用户名和用户的id,当然这不是前期开发的,还有就是管理端封禁用户,修改用户的借阅上限基本的功能,值得一提的是用户密码加密用的不是MD5,用的是BCrypt 算法,比MD5更安全。
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
!encoder.matches(password, user.getPassword());
//需要用自带的方法比较,无法将密码加密之后跟原密码比较
//加密
String pwd = encoder.encode(password);
然后就是图书了,作为一个图书馆系统,核心当然是图书了,图书的外键:分类,地址。主要功能:分页获取图书,修改图书,添加图书,借书(生成借阅记录),还书等,图书下面还有评论功能,当然这是后期加的。
借阅功能,借书时生成借阅记录:还书时间默认是30天后,分页获取,增删改查,续借等功能,这里前期开发时有个bug,就是用户如果是违规未还书,也是可以还书的,还书需要修改图书的状态,但是没有写,后面当然把这个bug修改过来了。
像地址,出版社都是分页查询然后一些增删改查的接口,并没有什么特色可以讲的。
值得仔细讲的就是分类管理了,分类的表结构是这样的:
clazz记录书本类型
id | int | 类型id | |
name | varchar(20) | 类型名称 | |
parent_id | int | 父分类id |
主要就是父分类id上面,首先是获取没有父分类的分类,这个还是很简单的,只需要判断parent_id是不是null就行,问题在于,获取所有分类,首先一个分类有没有父分类id呢?这个需要判断,如果有,那么父分类还有没有父分类?这个可能有点复杂一点,但是想一想还是可以明白的,就是要一个向下找,这里用的是递归来查找:代码实现:
public String getFullPath(Integer classId) {
//初始化一个集合,用来存储分类
List<String> pathSegments = new ArrayList<>();
//传递需要查找的分类id,已经集合容器存储分类
findParent(classId, pathSegments);
Collections.reverse(pathSegments); // 反转顺序(从顶级到子类)
//每个分类用”/“分隔
return String.join("/", pathSegments);
}
private void findParent(Integer classId, List<String> pathSegments) {
// 查询当前分类
BookClasses current = bookClassMapper.selectById(classId);
//终止条件,当前分类为null
if (current == null) return;
// 将分类名添加到集合中
pathSegments.add(current.getName());
// 如果分类的父id不等于null,递归
if (current.getParentId() != null) {
findParent(current.getParentId(), pathSegments); // 递归查找父分类
}
}
像出版社,分类,地址经常查询并且没有复杂的条件,所以存储在redis缓存中。不包括分页查询。
到了这里前期的开发基本完成,核心功能也完成了,但是还有很多可以扩展的功能:比如消息列表,通知,用户反馈,近期活动,罚款,管理端处理用户反馈,处理之后添加到消息列表通知用户,以及系统日志,管理端主页。
总结在前期主要的问题是前端路由,还有第一次用BCrypt 算法,不知道怎么验证,还有获取分类的问题。
开发中期
中期主要在核心功能的基础之上进行扩展,前期开发的一些功能实在没有什么意义,毕竟只是模拟,所以在这个阶段就脱离了图书馆系统这个名字进行开发,尽量开发一些有实际作用,这个阶段将会开发评论功能,以及评论点赞功能,还有帖子功能,帖子评论功能,帖子点赞功能,添加阿里云内容审核,保证内容的安全,评论添加人工审核,避免ai审核出现误差,开发管理端的主页,添加日志功能,用户主页。
在前期的开发完成了符合这个项目名字的功能。这个阶段的开发是扩展一些使用的功能,实现是完成用户的主页:展示用户的基本信息,然后可以修改自己的基本信息,中间展示用户的借书历史数量,当前借书数量,总共借书,最多借书数量。下面展示借阅记录,这些后端接口其实在前期就已经开发完成了。主要就是前端的事,当然这个项目主要是练习springboot,前端我主要用ai写的,然后修改。所以这里不讨论vue。
核心功能之一:评论,评论功能在设计数据库时就打算开发这个功能,但是前期的时候我没有写,因为第一次写这个有点不熟悉,所以先把熟悉的接口写完,评论功能的主要问题在于,审核,以及点赞,先说审核,我使用了阿里云的内容安全api,花了点时间去学习了一下,然后加到项目的公共模块,直接调用就是了,没有什么难点。还有就是点赞,我本来想的是,一个用户点赞产生一条数据,然后其他用户点赞累加,然后发现有很大的问题。首先是无法区分用户到底点没点赞,还有用户取消点赞也有问题,然后最终点赞关系表格设计成了这样:
review_id | int | 评论id | 外键 |
user_id | int | 用户id | 外键 |
一个用户点赞一条评论就会产生一条数据,然后取消点赞或者被删除都会同步删除。这样设计很好的解决之前的问题,点赞时评论的点赞数量加1,取消则减一,获取评论根据图书id和当前登录用户id获取评论的列表,这里需要当前登录用户id是为了判断用户有没有点赞评论,还有就是判断哪些评论是当前用户发送的,如果是当前用户发的,就可以删除,前端动展示一个删除键。这里没有用分页,用分页也太丑了吧!发布评论时需要通过阿里云的审核,如果没有通过就存储在数据库,将这条评论逻辑删除,交给管理员审核。其他就没什么好说的了。部分代码实现:
//发布评论
@Override
@Transactional
public Boolean sendReview(ReviewDTO reviewDTO) {
// 发送评论
// 如果是在帖子下发送评论需要指向额外的操作
String content = reviewDTO.getContent();
Review review = BeanUtil.copyProperties(reviewDTO, Review.class);
String image = userService.getById(reviewDTO.getUserId()).getImage();
review.setImage(image);
TextResult textResult = TextModerationPlusDemo.DetectionText(content);
assert textResult != null;//断言不为空
String s = textResult.getLevel();
review.setDescription(textResult.getText());//设置评论的描述
if(s!=null&&!s.equals("none")){
// 说明评论有问题!
review.setIsAudit(StatusConstant.DISABLE);
// 保存但是不展示,让管理员审核
save(review);
return false;
}
Integer postId = reviewDTO.getPostId();
boolean save = save(review);
if(postId!=null){
// 帖子评论加1
addReviewCount(reviewDTO.getPostId());
// 说明是给帖子评论,添加,不需要给图书评论添加消息列表
// 这里不仅需要帖子的id,还需要获取评论的id,可以根据帖子的id和用户id获取评论的id
Integer reviewId = review.getId();
Integer userId = postService.getById(postId).getUserId();
saveMessage(userId,review.getUserId(),postId,reviewId);
}
return save;
}
//获取评论
@Override
public List<ReviewVO> getReviewByBookId(GetReviewDTO dto) {
Integer postId = dto.getPostId();
Integer bookId = dto.getBookId();
Integer userId = dto.getUserId();
List<Review> list = lambdaQuery()
.eq(bookId!=null, Review::getBookId, bookId)
.eq(postId != null, Review::getPostId, postId)
.list();
if(list!=null&& !list.isEmpty()){
// 获取所有的用户id
Map<Integer, String> map = ServiceUtils.buildEntityMap(
list,
Review::getUserId,
userService::listByIds,
Users::getId,
Users::getUsername
);
log.info("getReviewByBookId: "+list);
List<ReviewVO> reviewVO = BeanUtil.copyToList(list, ReviewVO.class);
reviewVO.forEach(review->{
Integer id = review.getId();
String name = map.get(review.getUserId());
review.setUserName(name);
review.setIsLike(isLike(userId,id));
});
return reviewVO;
}
return List.of();
}
然后就是消息列表,刚开始我只考虑到了用户反馈,管理员处理完成之后,提醒用户,首先获取未读消息数量,然后用户点击查看消息时,根据当前的用户获取对应的消息,刚开始写的还是比较简单的,用户查看消息时,顺便将状态修改成为已读。剩下就是一些基本的增删改查了。
我认为最有价值的就是帖子了,接下来就是关于帖子的开发。之前已经开发了评论接口,现在只需要在评论表添加一个字段就行了,也就是post_id,评论就不用再去开发了,帖子的大概结构:
id | int | 主键 | |
content | varchar(1000) | 帖子内容 | |
user_id | id | 外键 | |
image | varchar(100) | 图片 | |
create_time | datetime | 创建时间 | |
update_time | datetime | 更新时间 |
还有一张标签表格,另外有一张帖子标签关系表格,可以获取帖子对应的标签,帖子点赞独立弄了一个:
post_id | INT UNSIGNED | 帖子id | |
user_id | int | 用户id |
跟评论点赞表几乎是一样的,其实是可以在评论点赞表的加个字段的,不过这里还是独立设计了一个。发送帖子,首先是阿里云审核,帖子我没有弄人工审核,如果阿里云审核没有通过直接会发送失败,发帖子的字段:
private Integer id;
private Integer userId;
private String title;
private String content;
private String image;
// 标签id
private List<Integer> tagsId;
@Override
public boolean sendPost(PostDTO postDTO) {
// 发帖子需要向帖子标签关系表添加,前端发送的标签id一定是存在的
// 首先将帖子插入表中,然后再获取帖子的id,再向帖子标签关系表中插入数据
String content = postDTO.getContent();
String title = postDTO.getTitle();
if(Violation(content,title)){
return false;
}
String image = postDTO.getImage();
Integer userId = postDTO.getUserId();
Posts post = Posts.builder()
.content(content)
.title(title)
.userId(userId)
.image(image)
.build();
boolean save = save(post);
if(save){
// 判断标题,标题是唯一的
Integer id = lambdaQuery()
.eq(Posts::getUserId, userId)
.eq(Posts::getTitle,title)
.one()
.getId();
if(id!=null){
List<Integer> tagsId = postDTO.getTagsId();
tagsId.forEach(t->{
postTagsService.save(
PostTags.builder()
.postId(id)
.tagId(t)
.build()
);
});
// 帖子发送成功
return true;
}
}
return false;
}
我觉得开发帖子接口的时候,最难的是更新帖子,主要问题就是标签的更新,如果是之前我一般是直接先将更新帖子的标签关系数据删除,然后插入新的数据,这样子也很简单,但是现在有了ai我就问ai有没有更好的办法呢?它就给了以下的代码:
private void updateTag(Integer id,List<Integer> newTags){
// List<Integer> newTags = postDTO.getTagsId();
if(newTags==null||newTags.isEmpty()||newTags.contains(null)){
log.debug("不需要更新标签");
return;//不需要更新标签
}
log.debug("更新标签, id:{},新加标签id:{}",id,newTags);
//获取旧标签
List<Integer> oldTags = postTagsService.lambdaQuery()
// .select(PostTags::getTagId)
.eq(PostTags::getPostId, id)
.list()
.stream()
.map(PostTags::getTagId)
.toList();
// 找到旧标签中需要删除的标签,也就是不包含在新标签中的标签
List<Integer> needDelete = oldTags.stream()
.filter(oldTag -> !newTags.contains(oldTag))
.toList();
// 真的需要新增的标签,也就是旧标签中没有的新标签
List<Integer> needAdd = newTags.stream()
.filter(newTag -> !oldTags.contains(newTag))
.toList();
// 批量删除旧标签
if(!needDelete.isEmpty()){
postTagsService.lambdaUpdate()
.eq(PostTags::getPostId,id)
.in(PostTags::getTagId,needDelete)
.remove();
}
// 批量插入新加的标签
if(!needAdd.isEmpty()){
List<PostTags> list = needAdd.stream()
.map(tagId -> PostTags.builder()
.postId(id)
.tagId(tagId)
.build())
.toList();
postTagsService.saveBatch(list);
}
}
我感觉ai给的代码好像性能更差一点,不过我还是用了,剩下的就是根据id获取,还有根据用户获取之类的增删改重复的代码了。
然后还有管理端的主页,获取用户的基本数据,获取一些统计数据展示。
后期开发
后期主要修改了一些bug,还有优化了消息页面,添加了评论,点赞消息提醒,消息表设计:
id | int | 主键 | |
type | tinyint(1) | 1.点赞类型 2.评论类型 | |
sender_id | int | 用户外键 | 发送消息的人id |
receiver_id | int | 用户外键 | 接收人id |
is_read | tinyint(1) | 0:未读 1:已读 | |
time | datetime | 时间 | 时间戳 |
post_id | int | 帖子id | |
review_id | int | 评论id |
这里的已读消息,在之前写的接口,用户反馈那调用接口获取消息的未读数量,然后加起来,就不用在前端修改了。保存消息:
private void saveMessage(Integer receiverId, Integer senderId, Integer postId, Integer reviewId){
if(Objects.equals(receiverId, senderId)){
return;
}
Message message = Message.builder()
.receiverId(receiverId)
.senderId(senderId)
.postId(postId)
.reviewId(reviewId)
.type(MessageType.REVIEW).build();
messageService.save(message);
}
private void saveMessage(PostLikeDTO postDTO, Posts post) {
// 点赞 根据帖子获取发布者的id
Integer receiverId = post.getUserId();
// 点赞的用户id
Integer senderId= postDTO.getUserId();
// 同一个人不需要消息~
if(Objects.equals(receiverId, senderId)){
return;
}
Message message = Message.builder()
.receiverId(receiverId)
.senderId(senderId)
.postId(postDTO.getPostId())
.type(MessageType.LIKE).build();
messageService.save(message);
}
关于消息主要就是这些了,还有根据用户id获取消息列表其实跟之前写的差不多。
然后就是解决一些bug,比如前端消息出现幻影,还有帖子点赞也有bug,就是取消点赞的时候没有删除,后端优化了一下一些臃肿的代码,测试,修复一些bug. 如果你看到了这里,非常感谢,如果有什么建议,欢迎在评论区讨论~😀。
下一步的学习打算
首先是学习一下Java的stream流,然后学习一下redis,然后写一个小demo巩固一下,之后打算学习Linux,学习前端的一些知识
github仓库:后端:https://github.com/huuhi/library.git
前端:https://github.com/huuhi/library_html.git
帖子来源:https://movie.douban.com/review/6180877/
#项目##spring boot##spring boot项目##新人求带#