毕设所有选题:
https://blog.csdn.net/2303_76227485/article/details/131104075

基于Springboot+Vue3+Ai对话的图书馆图书借阅系统(源代码+数据库+开题+PPT)

项目编号:262

一、系统介绍

本项目前后端分离,分为用户、管理员2种角色。

1、用户:

  • 图书协同过滤推荐、热门图书、热门分类、图书搜索、图书借阅、续借、还书、图书预约、评论、查看通知、AI助手、个人信息

2、管理员:

  • 图书管理:图书信息录入,图书信息查询,图书信息修改,图书信息删除,图书分类管理
  • 用户管理:启用禁用
  • 借阅管理:借阅记录查询,图书归还,借阅审核,逾期管理
  • 预约管理:预约记录查询,预约取消,预约通知
  • 评论管理:评论查看,评论删除
  • 统计分析:借阅统计,热门图书排行,用户借阅排行,图书库存统计,逾期统计

3、亮点:

  • 实现ai对话优化用户体验
  • 使用协同过滤算法为用户推荐图书
  • 后台首页大屏使用echarts图表统计,更直观的看出系统的运行数据
  • 使用定时任务查询逾期的借阅并通知用户

二、所用技术

后端技术栈:

  • Springboot3
  • mybatisPlus
  • Jwt
  • Mysql
  • Maven

前端技术栈:

  • Vue3
  • Vue-router
  • axios
  • elementPlus
  • echarts

三、环境介绍

基础环境 :IDEA/eclipse, JDK1.8, Mysql5.7及以上, Maven3.6, node16, navicat, 通义千问apikey

所有项目以及源代码本人均调试运行无问题 可支持远程调试运行

四、页面截图

1、用户:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2、管理员:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

五、浏览地址

前台地址:http://localhost:3000

  • 用户账号密码:zhangsan/123456

  • 管理员账户密码:admin/123456

六、部署教程

  1. 使用Navicat或者其它工具,在mysql中创建对应名称的数据库,并执行项目的sql文件

  2. 使用IDEA/Eclipse导入backend项目,若为maven项目请选择maven,等待依赖下载完成

  3. 修改application.yml里面的数据库配置和通义千问的apikey配置,src/main/java/com/library/LibraryManagementApplication.java启动后端项目

  4. vscode或idea打开frontend项目

  5. 在编译器中打开terminal,执行npm install 依赖下载完成后执行 npm run serve,执行成功后会显示访问地址

七、协同过滤推荐部分代码

       @Override
public IPage<BookVO> recommendBooksByCF(Long userId, Integer page, Integer size) {
        // 1:获取目标用户的借阅记录(已借阅的图书ID)
        Set<Long> userBorrowedBookIds = getBorrowedBookIds(userId);
        if (CollectionUtils.isEmpty(userBorrowedBookIds)) {
        // 无借阅历史,返回热门图书(兜底策略)
        return getHotBooks(page, size);
        }

        // 2:构建用户-图书借阅矩阵(用户ID -> 图书ID -> 借阅次数)
        Map<Long, Map<Long, Integer>> userBookMatrix = buildUserBookMatrix();

        // 3:计算目标用户与其他用户的相似度
        Map<Long, Double> userSimilarityMap = calculateUserSimilarity(userId, userBookMatrix);

        // 4:筛选相似度最高的N个用户
        List<Long> similarUserIds = userSimilarityMap.entrySet().stream()
        .sorted((e1, e2) -> Double.compare(e2.getValue(), e1.getValue()))
        .limit(SIMILAR_USER_COUNT)
        .map(Map.Entry::getKey)
        .collect(Collectors.toList());

        // 5:收集相似用户借阅过、目标用户未借阅的图书,计算推荐分数
        Map<Long, Double> bookRecommendScore = calculateBookRecommendScore(
        similarUserIds, userSimilarityMap, userBookMatrix, userBorrowedBookIds);

        // 6:按推荐分数排序,过滤无效图书,分页返回
        return getRecommendBookPage(bookRecommendScore, page, size);
        }

/**
 * 获取用户已借阅的图书ID
 */
private Set<Long> getBorrowedBookIds(Long userId) {
        return borrowRecordMapper.selectList(
        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BorrowRecord>()
        .eq(BorrowRecord::getUserId, userId)
        .in(BorrowRecord::getStatus,
        Constants.BORROW_STATUS_BORROWED,
        Constants.BORROW_STATUS_RETURNED)
        ).stream()
        .map(BorrowRecord::getBookId)
        .collect(Collectors.toSet());
        }

/**
 * 构建用户-图书借阅矩阵
 */
private Map<Long, Map<Long, Integer>> buildUserBookMatrix() {
        // 查询所有有效借阅记录(排除待审核/拒绝的)
        List<BorrowRecord> allBorrowRecords = borrowRecordMapper.selectList(
        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BorrowRecord>()
        .in(BorrowRecord::getStatus,
        Constants.BORROW_STATUS_BORROWED,
        Constants.BORROW_STATUS_RETURNED)
        );

        // 统计每个用户对每本书的借阅次数
        Map<Long, Map<Long, Integer>> userBookMatrix = new HashMap<>();
        for (BorrowRecord record : allBorrowRecords) {
        Long userId = record.getUserId();
        Long bookId = record.getBookId();

        userBookMatrix.putIfAbsent(userId, new HashMap<>());
        Map<Long, Integer> bookCountMap = userBookMatrix.get(userId);
        bookCountMap.put(bookId, bookCountMap.getOrDefault(bookId, 0) + 1);
        }

        // 过滤借阅次数过少的记录(减少噪声)
        userBookMatrix.values().forEach(bookCountMap ->
        bookCountMap.entrySet().removeIf(entry -> entry.getValue() < MIN_BORROW_COUNT)
        );
        // 过滤无有效借阅的用户
        userBookMatrix.entrySet().removeIf(entry -> entry.getValue().isEmpty());

        return userBookMatrix;
        }

/**
 * 计算目标用户与其他用户的余弦相似度
 */
private Map<Long, Double> calculateUserSimilarity(Long targetUserId, Map<Long, Map<Long, Integer>> userBookMatrix) {
        Map<Long, Integer> targetUserRatings = userBookMatrix.get(targetUserId);
        if (CollectionUtils.isEmpty(targetUserRatings)) {
        return new HashMap<>();
        }

        Map<Long, Double> similarityMap = new HashMap<>();
        for (Map.Entry<Long, Map<Long, Integer>> entry : userBookMatrix.entrySet()) {
        Long otherUserId = entry.getKey();
        if (otherUserId.equals(targetUserId)) {
        continue; // 跳过自己
        }

        Map<Long, Integer> otherUserRatings = entry.getValue();
        // 计算余弦相似度
        double similarity = calculateCosineSimilarity(targetUserRatings, otherUserRatings);
        if (similarity > 0) { // 只保留正相似的用户
        similarityMap.put(otherUserId, similarity);
        }
        }

        return similarityMap;
        }

/**
 * 计算两个用户的余弦相似度
 * 公式:相似度 = (A·B) / (|A| * |B|),其中A·B是点积,|A|是A的模长
 */
private double calculateCosineSimilarity(Map<Long, Integer> userARatings, Map<Long, Integer> userBRatings) {
        // 找出共同借阅的图书
        Set<Long> commonBookIds = new HashSet<>(userARatings.keySet());
        commonBookIds.retainAll(userBRatings.keySet());

        if (commonBookIds.isEmpty()) {
        return 0.0; // 无共同图书,相似度为0
        }

        // 计算点积
        double dotProduct = 0.0;
        for (Long bookId : commonBookIds) {
        dotProduct += userARatings.get(bookId) * userBRatings.get(bookId);
        }

        // 计算模长
        double normA = Math.sqrt(userARatings.values().stream()
        .mapToDouble(r -> r * r)
        .sum());
        double normB = Math.sqrt(userBRatings.values().stream()
        .mapToDouble(r -> r * r)
        .sum());

        if (normA == 0 || normB == 0) {
        return 0.0;
        }

        return dotProduct / (normA * normB);
        }

/**
 * 计算图书推荐分数(相似用户相似度 * 借阅次数)
 */
private Map<Long, Double> calculateBookRecommendScore(
        List<Long> similarUserIds,
        Map<Long, Double> userSimilarityMap,
        Map<Long, Map<Long, Integer>> userBookMatrix,
        Set<Long> userBorrowedBookIds) {

        Map<Long, Double> bookScoreMap = new HashMap<>();
        for (Long similarUserId : similarUserIds) {
        double similarity = userSimilarityMap.get(similarUserId);
        Map<Long, Integer> similarUserBooks = userBookMatrix.get(similarUserId);

        for (Map.Entry<Long, Integer> entry : similarUserBooks.entrySet()) {
        Long bookId = entry.getKey();
        int borrowCount = entry.getValue();

        // 跳过目标用户已借阅的图书
        if (userBorrowedBookIds.contains(bookId)) {
        continue;
        }

        // 推荐分数 = 相似度 * 借阅次数(加权)
        double score = similarity * borrowCount;
        bookScoreMap.put(bookId, bookScoreMap.getOrDefault(bookId, 0.0) + score);
        }
        }

        return bookScoreMap;
        }

/**
 * 过滤无效图书,分页返回推荐列表
 */
private IPage<BookVO> getRecommendBookPage(Map<Long, Double> bookScoreMap, Integer page, Integer size) {
        if (CollectionUtils.isEmpty(bookScoreMap)) {
        // 无推荐结果,返回热门图书
        return getHotBooks(page, size);
        }

        // 按推荐分数降序排序
        List<Long> recommendBookIds = bookScoreMap.entrySet().stream()
        .sorted((e1, e2) -> Double.compare(e2.getValue(), e1.getValue()))
        .map(Map.Entry::getKey)
        .collect(Collectors.toList());

        // 过滤下架/库存不足的图书
        List<Long> validBookIds = recommendBookIds.stream()
        .filter(bookId -> {
        Book book = bookMapper.selectById(bookId);
        return book != null
        && book.getStatus() == Constants.BOOK_STATUS_ONLINE
        && book.getAvailableQuantity() > 0;
        })
        .collect(Collectors.toList());

        // 分页处理
        Page<BookVO> resultPage = new Page<>(page, size);
        int start = (page - 1) * size;
        int end = Math.min(start + size, validBookIds.size());

        if (start >= validBookIds.size()) {
        resultPage.setRecords(new ArrayList<>());
        resultPage.setTotal(0);
        return resultPage;
        }

        // 查询分页后的图书VO
        List<Long> pageBookIds = validBookIds.subList(start, end);
        List<BookVO> bookVOs = pageBookIds.stream()
        .map(this::getBookById)
        .collect(Collectors.toList());

        resultPage.setRecords(bookVOs);
        resultPage.setTotal(validBookIds.size());
        return resultPage;
        }

/**
 * 兜底策略:返回热门图书(按借阅次数排序)
 */
private IPage<BookVO> getHotBooks(Integer page, Integer size) {
        Page<Book> bookPage = new Page<>(page, size);
        // 查询上架且有库存的热门图书(按借阅次数降序)
        List<Book> hotBooks = bookMapper.selectList(
        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Book>()
        .eq(Book::getStatus, Constants.BOOK_STATUS_ONLINE)
        .gt(Book::getAvailableQuantity, 0)
        .orderByDesc(Book::getBorrowCount)
        );

        // 分页处理
        Page<BookVO> resultPage = new Page<>(page, size);
        int start = (page - 1) * size;
        int end = Math.min(start + size, hotBooks.size());

        if (start >= hotBooks.size()) {
        resultPage.setRecords(new ArrayList<>());
        resultPage.setTotal(0);
        return resultPage;
        }

        List<BookVO> bookVOs = hotBooks.subList(start, end).stream()
        .map(book -> this.getBookById(book.getId()))
        .collect(Collectors.toList());

        resultPage.setRecords(bookVOs);
        resultPage.setTotal(hotBooks.size());
        return resultPage;
        }
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐