大一第一个独自完成的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

alt

alt

alt

alt

alt

alt

alt

alt

帖子来源:https://movie.douban.com/review/6180877/

#项目##spring boot##spring boot项目##新人求带#
全部评论
回想起大一卷Java的我也是这样,现在已经转前端了
9 回复 分享
发布于 03-18 02:26 四川
未来的佬,太强了
5 回复 分享
发布于 03-22 17:26 四川
今天下午写这个文档了,复盘一下
4 回复 分享
发布于 03-12 17:27 广东
mark一下
3 回复 分享
发布于 03-12 19:16 江苏
大一的话很厉害了
3 回复 分享
发布于 03-12 18:23 北京
别听楼下让你双修学ai的,本人本科期间最大的失误就是因为自己的兴趣爱好搞了整整一年半的ai,如果你的目标不是读研而是就业,而你的本科院校不是特别优秀的话,直接allin开发岗,大二就想办法走日常实习等你大三结束的时候两三段实习经历,那个时候基本上就尘埃落定了此时如果你愿意的话可以玩玩ai,了解一下大模型的原理即可,然后花点小钱玩玩API得了。明确自己的定位千万别走偏。ai的确是大浪潮,但不是对所有人来说的,不要被营销号蒙蔽双眼,什么事都要用自己的眼睛去确定,用自己的嘴去问,用自己的心去鉴别信息的真伪别被时代浪潮裹挟。。。当然你想往上考研的话当我没说
2 回复 分享
发布于 昨天 11:03 广西
想问下大佬前端问的是哪个ai是怎么问的呀,我也大一,苍穹外卖敲完了,也想做个自己设计的项目,但是前端页面有点无从下手呀
2 回复 分享
发布于 03-30 09:28 黑龙江
做管理系统的话就用若依框架吧,很多大厂也是用的若依
1 回复 分享
发布于 04-08 13:30 重庆
不错,比我大一强多了,我大一还在寝室里面上网课呢,哈哈哈哈
1 回复 分享
发布于 04-07 15:10 重庆
1 回复 分享
发布于 03-14 15:03 湖北
ego-driven, passion.....无敌了,你的前端比我这个春招小丑焊的好一百倍;图书馆管理系统我大三上才做了个若依的垃圾玩意,无敌了,感觉可以Java搞定后直接大模型什么的双修多修,猛猛实习,毕业狠狠恰大包
点赞 回复 分享
发布于 04-18 23:15 福建
大一我还在过家家 tql
点赞 回复 分享
发布于 04-07 01:09 广东
mark一下
点赞 回复 分享
发布于 04-05 09:47 湖北
很强
点赞 回复 分享
发布于 03-30 22:57 北京
点赞 回复 分享
发布于 03-26 08:51 浙江
哇,太强啦
点赞 回复 分享
发布于 03-21 12:49 江苏
希望你是带🧱的
点赞 回复 分享
发布于 03-14 21:30 北京
mark一下
点赞 回复 分享
发布于 03-12 18:05 广东

相关推荐

Asprose:云兰到家
投递牛客等公司6个岗位 > 投了多少份简历才上岸
点赞 评论 收藏
分享
评论
40
29
分享

创作者周榜

更多
牛客网
牛客企业服务