1. 项目概述
最近在开发一个图书个性化推荐系统,采用了当前主流的技术栈:SpringBoot+Vue3+MyBatis+MySQL。这个系统最大的特点是实现了前后端分离架构,并且整合了两种推荐算法(协同过滤和基于内容的推荐)来提供更精准的图书推荐服务。
作为一个完整的项目,它包含了用户管理、图书管理、推荐算法和用户反馈等核心模块。我在开发过程中积累了不少经验,特别是在推荐算法实现和前后端交互方面,今天就来详细分享一下这个项目的实现细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 后端技术栈
选择SpringBoot作为后端框架有几个重要考虑:
- 自动配置特性大幅减少了XML配置
- 内嵌Tomcat服务器简化了部署流程
- 与MyBatis的整合非常成熟
- 丰富的starter依赖可以快速引入各种功能模块
java复制@SpringBootApplication
@MapperScan(basePackages = {"com.dao"})
public class SpringbootSchemaApplication extends SpringBootServletInitializer{
public static void main(String[] args) {
SpringApplication.run(SpringbootSchemaApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder applicationBuilder) {
return applicationBuilder.sources(SpringbootSchemaApplication.class);
}
}
这段启动类代码展示了几个关键点:
- @MapperScan注解自动扫描MyBatis的Mapper接口
- 继承SpringBootServletInitializer支持传统WAR包部署
- 简洁的main方法启动内嵌服务器
2.2 前端技术栈
Vue3作为前端框架的优势:
- Composition API提供了更好的逻辑复用
- 更小的打包体积和更快的渲染速度
- 更好的TypeScript支持
- 与ElementUI组件库完美配合
前端工程结构通常这样组织:
code复制src/
├── api/ # 接口定义
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── store/ # 状态管理
├── utils/ # 工具函数
└── views/ # 页面组件
2.3 数据库设计
MySQL数据库设计了三个核心表:
2.3.1 用户表(user)
sql复制CREATE TABLE `user` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password_hash` varchar(100) NOT NULL,
`email` varchar(100) DEFAULT NULL,
`interest_tags` text,
`registration_time` datetime DEFAULT CURRENT_TIMESTAMP,
`last_login_time` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `username_UNIQUE` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.3.2 图书表(book)
sql复制CREATE TABLE `book` (
`book_id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`author` varchar(50) NOT NULL,
`publisher` varchar(50) DEFAULT NULL,
`category` varchar(50) DEFAULT NULL,
`publish_date` date DEFAULT NULL,
`average_rating` float DEFAULT '0',
`shelf_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.3.3 推荐记录表(recommendation)
sql复制CREATE TABLE `recommendation` (
`record_id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`book_id` bigint NOT NULL,
`recommendation_score` float DEFAULT NULL,
`feedback` varchar(20) DEFAULT NULL,
`recommendation_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`record_id`),
KEY `fk_user_idx` (`user_id`),
KEY `fk_book_idx` (`book_id`),
CONSTRAINT `fk_book` FOREIGN KEY (`book_id`) REFERENCES `book` (`book_id`),
CONSTRAINT `fk_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意:密码存储一定要使用哈希算法(如BCrypt),切勿明文存储
3. 核心功能实现
3.1 用户认证模块
采用JWT(JSON Web Token)实现无状态认证:
java复制@RestController
@RequestMapping("/yonghu")
public class YonghuController {
@Autowired
private TokenService tokenService;
@IgnoreAuth
@RequestMapping(value = "/login")
public R login(String username, String password, String captcha, HttpServletRequest request) {
YonghuEntity u = yonghuService.selectOne(
new EntityWrapper<YonghuEntity>().eq("yonghuzhanghao", username));
if(u==null || !u.getMima().equals(password)) {
return R.error("账号或密码不正确");
}
String token = tokenService.generateToken(u.getId(), username,"yonghu", "用户");
return R.ok().put("token", token);
}
}
安全注意事项:
- 密码比较应该使用安全的哈希比对
- Token应该设置合理的过期时间
- 敏感接口需要@PreAuthorize注解进行权限控制
- 应该实现防暴力破解机制
3.2 推荐算法实现
3.2.1 协同过滤算法
基于用户的协同过滤实现步骤:
- 计算用户相似度矩阵
- 找出目标用户的最近邻
- 根据邻居的评分预测目标用户的评分
java复制public List<Book> userBasedCF(Long userId, int topN) {
// 1. 获取所有用户评分数据
Map<Long, Map<Long, Float>> userRatings = getRatingMatrix();
// 2. 计算用户相似度
Map<Long, Double> userSimilarities = new HashMap<>();
Map<Long, Float> targetUserRatings = userRatings.get(userId);
for (Map.Entry<Long, Map<Long, Float>> entry : userRatings.entrySet()) {
if (entry.getKey().equals(userId)) continue;
double similarity = cosineSimilarity(
targetUserRatings,
entry.getValue());
userSimilarities.put(entry.getKey(), similarity);
}
// 3. 获取相似度最高的K个用户
List<Long> nearestNeighbors = userSimilarities.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(TOP_K_NEIGHBORS)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
// 4. 预测评分并返回推荐结果
return predictRatings(userId, nearestNeighbors, userRatings)
.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(topN)
.map(e -> bookService.getById(e.getKey()))
.collect(Collectors.toList());
}
3.2.2 基于内容的推荐
实现步骤:
- 提取图书特征向量(分类、作者、标签等)
- 计算图书内容相似度
- 根据用户历史偏好推荐相似图书
java复制public List<Book> contentBasedRecommend(Long userId, int topN) {
// 1. 获取用户历史偏好
List<Book> userHistory = getHistoryBooks(userId);
// 2. 构建用户偏好向量
Map<String, Double> userProfile = buildUserProfile(userHistory);
// 3. 计算所有图书与用户偏好的相似度
List<Book> allBooks = bookService.list();
Map<Long, Double> bookScores = new HashMap<>();
for (Book book : allBooks) {
if (userHistory.contains(book)) continue;
Map<String, Double> bookFeatures = extractFeatures(book);
double score = cosineSimilarity(userProfile, bookFeatures);
bookScores.put(book.getBookId(), score);
}
// 4. 返回评分最高的图书
return bookScores.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(topN)
.map(e -> bookService.getById(e.getKey()))
.collect(Collectors.toList());
}
3.3 混合推荐策略
实际应用中采用加权混合策略:
- 协同过滤结果权重:0.6
- 基于内容结果权重:0.4
- 去重后合并结果
java复制public List<Book> hybridRecommend(Long userId, int topN) {
List<Book> cfBooks = userBasedCF(userId, topN * 2);
List<Book> cbBooks = contentBasedRecommend(userId, topN * 2);
// 合并并去重
Map<Long, Book> bookMap = new LinkedHashMap<>();
cfBooks.forEach(b -> bookMap.put(b.getBookId(), b));
cbBooks.forEach(b -> bookMap.putIfAbsent(b.getBookId(), b));
// 混合排序
return bookMap.values().stream()
.sorted(Comparator.comparingDouble(b ->
getCFScore(b) * 0.6 + getCBScore(b) * 0.4))
.limit(topN)
.collect(Collectors.toList());
}
4. 前后端交互实现
4.1 API设计规范
采用RESTful风格设计API:
| 功能 | 方法 | 路径 | 描述 |
|---|---|---|---|
| 用户登录 | POST | /api/auth/login | 获取Token |
| 获取推荐图书 | GET | /api/recommend/ | 获取推荐列表 |
| 提交反馈 | POST | /api/feedback | 提交用户反馈 |
4.2 Axios封装示例
前端对Axios的封装:
javascript复制// src/api/request.js
import axios from 'axios'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
// 请求拦截器
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
return Promise.reject(new Error(res.message || 'Error'))
}
return res
},
error => {
return Promise.reject(error)
}
)
export default service
4.3 推荐列表组件
Vue3推荐列表组件示例:
vue复制<template>
<div class="recommend-container">
<h3>为您推荐的图书</h3>
<el-row :gutter="20">
<el-col
v-for="book in bookList"
:key="book.bookId"
:span="6">
<book-card
:book="book"
@feedback="handleFeedback"/>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getRecommendations } from '@/api/recommend'
import BookCard from './BookCard.vue'
const bookList = ref([])
const fetchData = async () => {
try {
const res = await getRecommendations()
bookList.value = res.data
} catch (error) {
console.error(error)
}
}
const handleFeedback = (bookId, rating) => {
// 提交用户反馈
}
onMounted(() => {
fetchData()
})
</script>
5. 性能优化实践
5.1 推荐结果缓存
使用Redis缓存推荐结果:
java复制@Cacheable(value = "recommendations", key = "#userId")
public List<Book> getRecommendations(Long userId) {
// 复杂的推荐计算逻辑
return hybridRecommend(userId, 20);
}
缓存策略:
- 默认过期时间:1小时
- 用户行为触发缓存更新
- 热门图书单独缓存
5.2 数据库优化
- 为常用查询字段添加索引:
sql复制ALTER TABLE `recommendation` ADD INDEX `idx_user_time` (`user_id`, `recommendation_time`);
- 大文本字段单独存储:
sql复制ALTER TABLE `book` ADD COLUMN `description_text` TEXT;
- 定期执行分析表:
sql复制ANALYZE TABLE `user`, `book`, `recommendation`;
5.3 前端性能优化
- 组件懒加载:
javascript复制const BookDetail = () => import('./views/BookDetail.vue')
- 推荐列表虚拟滚动:
vue复制<template>
<RecycleScroller
class="scroller"
:items="bookList"
:item-size="320"
key-field="bookId">
<template v-slot="{ item }">
<book-card :book="item"/>
</template>
</RecycleScroller>
</template>
- API响应数据压缩:
nginx复制# nginx配置
gzip on;
gzip_types application/json;
6. 部署与运维
6.1 后端部署
使用Docker部署SpringBoot应用:
dockerfile复制FROM openjdk:11-jre
WORKDIR /app
COPY target/book-recommend-1.0.0.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
启动命令:
bash复制docker build -t book-recommend .
docker run -d -p 8080:8080 --name recommend book-recommend
6.2 前端部署
Nginx配置示例:
nginx复制server {
listen 80;
server_name recommend.example.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
}
}
6.3 监控方案
- SpringBoot Actuator健康检查:
yaml复制management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
- Prometheus监控指标:
java复制@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("application", "book-recommend");
}
- ELK日志收集:
yaml复制logging:
file:
path: /var/log/recommend
logback:
rollingpolicy:
max-file-size: 10MB
max-history: 7
7. 常见问题与解决方案
7.1 冷启动问题
新用户或新图书缺乏历史数据时的解决方案:
- 基于内容的推荐作为兜底
- 热门排行榜作为初始推荐
- 收集显式反馈(评分/标签)
- 利用社交关系数据
7.2 数据稀疏性问题
用户-图书评分矩阵稀疏的应对措施:
- 矩阵填充技术
- 降维处理(SVD)
- 引入更多上下文信息
- 使用深度学习模型
7.3 实时性需求
实现实时推荐的几种方式:
- 用户行为事件流处理(Kafka)
- 增量更新用户画像
- 在线学习算法
- 微服务架构解耦
7.4 系统扩展性
应对用户增长的技术方案:
- 推荐服务水平扩展
- 分布式缓存集群
- 读写分离数据库
- 推荐结果预计算
8. 项目扩展方向
8.1 推荐算法增强
- 引入深度学习模型(Wide&Deep, DIN)
- 增加上下文感知(时间/地点)
- 多目标优化(点击率+阅读时长)
- 强化学习动态调整
8.2 系统功能扩展
- 社交化推荐(好友书单)
- 阅读小组推荐
- 跨平台同步
- 电子书试读集成
8.3 技术架构升级
- 微服务化拆分
- 引入图数据库
- 实时推荐引擎
- 边缘计算部署
在实际开发过程中,最大的挑战是如何平衡推荐准确性和系统性能。特别是在用户量快速增长后,原先的单机算法可能需要重构为分布式实现。我的经验是,在项目初期就要考虑好扩展性设计,比如使用策略模式封装推荐算法,方便后续替换具体实现。
