☰
SSM图书推荐系统毕设源码:基于物品协同过滤ItemCF的完整实现
2026/10/27 23:09:57 网站建设 项目流程

简介:一份基于协同过滤算法的图书推荐系统完整毕业设计源码,面向Java/SSM技术栈的毕业生和课程设计者,解决图书个性化推荐与后台管理一体化实现问题。压缩包共829个文件,大小23.39MB,包含120个Java源码、167个JavaScript交互脚本、55个CSS样式、42个HTML页面、47个Vue组件,以及SQL数据库脚本、Maven配置和Tomcat部署相关文件,覆盖前后端完整工程结构。系统基于Spring+SpringMVC+MyBatis框架,JDK1.8环境,实现了首页推荐、用户管理、书籍分类、热门图书、收藏、订单等模块,并以协同过滤算法为核心生成个性化书目推荐。目前已有57人学习下载,适合需要参考完整SSM项目源码、数据库表设计及推荐算法落地思路的Java学习者。包内附带LW文档和部署脚本,便于直接运行与二次开发。

1. 基于协同过滤的图书推荐系统:一份能直接部署的SSM毕业设计源码

协同过滤是图书推荐系统毕业设计里最核心的算法环节,但网上大量“图书推荐系统源码”实际只是图书管理加一个假推荐接口,点进去全是写死的列表。这份源码包不一样的地方在于,它把 SSM 框架的完整后台和基于物品的协同过滤推荐引擎真的串了起来:用户登录后能看到“为你推荐”,管理员能维护图书和借阅数据,用户的借阅行为会被转成评分,推荐模块基于这些评分计算 Top-N 列表。适合三类人:正在做 Java 毕设选题的学生、需要课程设计完整项目的开发者、以及想复用一套推荐算法代码快速搭 demo 的从业者。下面我会从算法选型开始,把数据表、核心代码和部署踩坑完整过一遍,最后给出验证推荐效果的方式。

2. 推荐算法选型:为什么图书场景优先走 ItemCF 而不是 UserCF

这一章先解决“算法为什么这么选”的问题。很多同学拿到源码第一反应是直接跑,跑通了却讲不出原理,答辩被问两句就卡住。所以我先把两种主流协同过滤路线的差异说清楚,再落到这份源码里的评分矩阵设计和 SSM 分层结构。

2.1 UserCF 与 ItemCF 的取舍

UserCF(基于用户的协同过滤)的核心思路是:先找到和我兴趣相似的一群用户,再把这些用户喜欢而我没看过的书推荐给我。ItemCF(基于物品的协同过滤)则反过来:先找到和我历史上喜欢过的书相似的其它书,直接推荐这些相似书。两者的区别本质上是相似度计算的对象不同——一个是用户对用户,一个是物品对物品。

图书推荐场景里,ItemCF 是更稳妥的默认选择,原因有三条。第一,用户的读书兴趣相对稳定,借书、购书行为是长期偏好,不像新闻资讯那样兴趣漂移快,所以“物品之间的相似关系”比“用户之间的实时相似关系”更可靠。第二,图书是典型的长尾物品,冷门书很多,UserCF 在用户数量不够多时很难算出稳定的邻居集合,而 ItemCF 只需要两个物品有共同评分用户就能算相似度,对数据量的要求更低。第三,ItemCF 的解释性更好,“因为您借过《算法导论》,所以推荐《编程珠玑》”这种推荐理由在答辩和演示时一眼就能看懂。

我做过的项目里也见过把 UserCF 写进论文做对比的版本,但默认入口和实验数据都是 ItemCF。你如果想在论文里同时讨论两种算法,可以保留一个算法切换的开关,但主流程不要两头都跑,否则答辩时容易把自己绕进去。下面这个对比表可以帮你快速定位:

维度UserCFItemCF
相似度对象用户与用户物品与物品
适合场景新闻、短视频、热点内容图书、电商、长尾商品
数据量要求用户行为越丰富越好物品有共同评分即可
推荐结果解释较难解释容易解释
计算实时性用户相似度需频繁更新物品相似度可离线计算

2.2 评分矩阵:协同过滤的输入长什么样

协同过滤的一切计算都围绕 user-item-rating 三元组展开。把这三类数据放到一个表格里,行是用户,列是图书,单元格是评分,就是评分矩阵。ItemCF 要做的就是从这张矩阵里算出“列与列”之间的相似度。

图书系统里最常见的坑是没有显式评分。绝大多数用户不会主动给书打分,所以这份源码里的评分数据主要来自三个渠道。第一个是借阅行为映射:借过并归还一次记 3 分,借过两次记 4 分,借过三次及以上记 5 分;收藏图书额外加 1 分,总分不超过 5 分。第二个是图书详情页的手动评分控件,登录用户可以主动给读过的书打分,这部分数据最干净。第三个是浏览行为兜底,用户只要点开过图书详情页就记 1 分,目的是让评分矩阵没那么稀疏。实际效果里,第 1 种和第 3 种是填充矩阵的主力,第 2 种数据量少但质量高。

注意,矩阵稀疏是协同过滤项目里最头疼的问题,它比算法本身更影响最终效果。一个几百用户、几千图书的系统,如果评分数据只有几百条,相似度算出来基本都是 0,推荐列表自然就空了。我后面避坑章节会专门讲这个,这里先记住一个结论:数据准备阶段投入的时间,至少要和算法实现投入的时间一样多。

2.3 SSM 架构下推荐模块放在哪一层

这个项目的分层是经典的 SSM 五层结构:Controller 接收请求,Service 处理业务,Mapper 访问数据库,底层是 MySQL,另外单开一个 recommend 算法包放协同过滤引擎。推荐模块没有写在 Mapper 的复杂关联查询里,而是把评分数据一次性加载进内存,在 Java 侧完成相似度计算和推荐分数聚合,这样算法逻辑可以脱离数据库跑,方便调试也方便替换算法。

完整的调用链是这样的:用户进入“为你推荐”页面,请求带 userId;RecommendController 从 session 里取登录用户 ID;RecommendService 调用 RatingMapper 查出全部评分记录和该用户的已评图书 ID;算法引擎基于全量评分构建物品倒排表、计算相似度、生成候选集合;最后过滤掉用户已经看过的书,按预测分排序返回前 N 本。下面是 Controller 层的代码示意。

@Controller @RequestMapping("/recommend") public class RecommendController { @Autowired private RecommendService recommendService; @GetMapping("/list") public String recommendList(HttpSession session, Model model) { // 从登录态取用户ID,未登录直接跳登录页 Integer userId = (Integer) session.getAttribute("userId"); if (userId == null) { return "redirect:/login"; } // 默认推荐8本,具体数量由Service层控制 List<RecommendItem> books = recommendService.getTopN(userId, 8); model.addAttribute("recommendBooks", books); return "recommend_list"; } }

逻辑说明:这个控制器只做两件事,取登录用户 ID 和调用 Service 拿推荐列表,算法细节全部封装在 Service 和 recommend 包里。推荐数量 8 是页面展示的默认值,两行四列刚好铺满。如果你想调试时不依赖登录态,可以额外加一个/recommend/test?userId=1的测试入口,这在开发期很有用。参数说明:topN这个参数在 Controller 和 Service 之间传递,真正的调参入口在 Service 层,页面和测试接口共用同一套逻辑。

3. 数据库设计与评分矩阵落地:从借阅行为到可计算的数字

推荐算法能不能跑起来,一半取决于数据表设计是否合理。这一章先把四张核心表的 SQL 给出来,再讲借阅行为怎么转成评分,最后落到 Mapper 层的查询方法。

3.1 核心表结构:用户、图书、评分、借阅记录

标准的 SSM 毕设项目至少需要四张表:用户表、图书表、评分表和借阅记录表。用户表存登录信息,图书表存书目元数据,评分表是协同过滤的直接输入,借阅记录表是生成评分的原始数据源。建表 SQL 如下。

CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(64) NOT NULL, `nickname` varchar(50) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `book` ( `id` int NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL, `author` varchar(100) DEFAULT NULL, `category` varchar(50) DEFAULT NULL, `publisher` varchar(100) DEFAULT NULL, `stock` int DEFAULT '0', `cover_url` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表'; CREATE TABLE `rating` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `book_id` int NOT NULL, `score` tinyint NOT NULL COMMENT '1-5分', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_book` (`user_id`,`book_id`), KEY `idx_book_id` (`book_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评分表'; CREATE TABLE `borrow_record` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `book_id` int NOT NULL, `borrow_time` datetime DEFAULT CURRENT_TIMESTAMP, `return_time` datetime DEFAULT NULL, `status` tinyint DEFAULT '0' COMMENT '0借出 1已还', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_book_id` (`book_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';

字段说明如下表所示,表名与用途一一对应:

表名关键字段作用
userid / username / password登录与用户身份标识
bookid / title / author / category / stock图书基础信息,推荐结果展示时要用
ratinguser_id / book_id / score协同过滤的输入矩阵
borrow_recorduser_id / book_id / borrow_time / status行为数据来源,用于生成评分

特别提醒,rating 表必须加(user_id, book_id)联合唯一索引。原因是同一用户对同一本书只能保留一条评分记录,重复借阅时更新分数而不是插入新行,这样评分矩阵的行和列才不会出现重复。如果你不加这个唯一索引,后面跑行为转评分的 SQL 时,数据会越插越乱。

3.2 行为数据转评分:借阅次数怎么映射成 1-5 分

借阅记录表里存的是原始行为,要变成协同过滤能用的评分,需要一条聚合 SQL。常见做法是把借阅次数映射成分数:借 1 次 3 分,借 2 次 4 分,借 3 次及以上 5 分。

INSERT INTO rating (user_id, book_id, score) SELECT user_id, book_id, CASE WHEN COUNT(*) >= 3 THEN 5 WHEN COUNT(*) = 2 THEN 4 ELSE 3 END FROM borrow_record WHERE status = 1 GROUP BY user_id, book_id ON DUPLICATE KEY UPDATE score = VALUES(score);

逻辑说明:这段 SQL 把“借过并归还”的次数聚合成评分。只统计status = 1已归还的记录,因为未归还的书可能还没看完,不适合作为兴趣信号。GROUP BY user_id, book_id保证每个用户对每本书只算一条。ON DUPLICATE KEY UPDATE配合前面说的联合唯一索引,让脚本可以重复执行而不会产生脏数据。参数说明:分数映射的阈值可以按你的数据分布调整,如果系统里借 5 次以上的用户很多,可以把 5 分的门槛提到 4 次,避免评分全堆在最高分上。

3.3 Mapper 层:两个查询方法串起评分矩阵

Mapper 层不需要复杂 SQL,两个查询就够用。第一个查询拉全量评分记录,用于算法引擎构建物品倒排表;第二个查询拉当前用户的已评图书 ID,用于推荐时过滤。

public interface RatingMapper { // 查全部评分,ItemCF在内存中构建物品-用户倒排表 @Select("SELECT user_id AS userId, book_id AS bookId, score FROM rating") List<RatingPO> selectAll(); // 查用户评过分的图书ID集合,推荐时排除已读 @Select("SELECT book_id AS bookId FROM rating WHERE user_id = #{userId}") List<Integer> selectBookIdsByUserId(Integer userId); }

逻辑说明:第一个查询是全表扫描,几百用户、几千条评分时性能没有问题;如果数据量到了十万级,建议加分页或只取近三个月的评分。第二个查询的结果是一个 ID 列表,Service 层可以直接放到Set里做过滤。参数说明:RatingPO是简单 POJO,字段和 rating 表一一对应;这里用了注解式 SQL,如果你想改成 XML 写法,把@Select换成对应 mapper XML 里的<select>即可,SQL 内容完全一样。

这里解释一下为什么用两条独立查询而不是一条 JOIN。ItemCF 需要把全量评分一次性读进内存,JOIN 反而会把行数放大,而且用户已读列表是纯粹用于过滤的,和矩阵构建没有关系,拆开写逻辑更清晰,也方便单独测试。

4. 核心实现:余弦相似度、最近邻与 Top-N 推荐列表的 Java 代码

这一章是整份源码里最值得读的部分。推荐引擎单独放在 recommend 包下,不依赖 Spring 容器,是一个纯 Java 的计算类,这样你可以脱离项目单独跑测试。下面按“构建矩阵 → 计算相似度 → 生成推荐列表 → Service 组装”的顺序拆。

4.1 构建倒排表与计算余弦相似度

ItemCF 的第一步是把评分数据转成“物品到用户”的倒排表结构。第二步是计算两个物品的余弦相似度,公式是物品A与物品B的共同评分向量的点积 / 两个向量的模长乘积,结果越接近 1 表示越相似。

public class ItemBasedCF { // 倒排表:key=图书ID,value=用户ID->评分 private Map<Integer, Map<Integer, Double>> itemUsers; // 用全量评分数据构建倒排表 public void buildMatrix(List<RatingPO> ratings) { itemUsers = new HashMap<>(); for (RatingPO r : ratings) { itemUsers.computeIfAbsent(r.getBookId(), k -> new HashMap<>()) .put(r.getUserId(), r.getScore().doubleValue()); } } // 余弦相似度 public double similarity(int bookA, int bookB) { Map<Integer, Double> usersA = itemUsers.getOrDefault(bookA, Collections.emptyMap()); Map<Integer, Double> usersB = itemUsers.getOrDefault(bookB, Collections.emptyMap()); double dot = 0.0, normA = 0.0, normB = 0.0; for (Map.Entry<Integer, Double> entry : usersA.entrySet()) { normA += entry.getValue() * entry.getValue(); Double scoreB = usersB.get(entry.getKey()); if (scoreB != null) { dot += entry.getValue() * scoreB; } } for (Double value : usersB.values()) { normB += value * value; } if (normA == 0.0 || normB == 0.0) { return 0.0; } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } }

逻辑说明:buildMatrix是算法引擎的入口,接收 Mapper 查出来的List<RatingPO>,逐条放进itemUsers这个 Map 里,外层 key 是图书 ID,内层 key 是用户 ID,value 是评分。similarity方法遍历usersA,通过usersB.get(entry.getKey())找到共同评分用户,只有两个用户都对同一本书评过分,dot才会累加。参数说明:评分转成double是为了后续加权求和时浮点精度不丢;如果算出来的相似度为负数,说明两个物品负相关,直接返回 0 处理即可。

4.2 最近邻筛选与评分加权预测

有了相似度函数,推荐逻辑就顺理成章了。用户对候选书的预测分 = 用户对已读书的评分 × 已读书与候选书的相似度,把所有已读书的贡献累加,排序后取前 N 本。

public List<RecommendItem> recommend(int userId, int topN) { // 取当前用户评过分的图书及分数 Map<Integer, Double> userRatings = getUserRatings(userId); if (userRatings.isEmpty()) { return Collections.emptyList(); } Map<Integer, Double> scores = new HashMap<>(); for (Map.Entry<Integer, Double> rated : userRatings.entrySet()) { int ratedBook = rated.getKey(); for (Map.Entry<Integer, Map<Integer, Double>> entry : itemUsers.entrySet()) { int candidateBook = entry.getKey(); // 过滤:用户已经评过分的书不再推荐 if (userRatings.containsKey(candidateBook)) { continue; } double sim = similarity(ratedBook, candidateBook); // 过滤:相似度小于等于0的放弃 if (sim <= 0.0) { continue; } double weight = sim * rated.getValue(); scores.merge(candidateBook, weight, Double::sum); } } // 按预测分降序,取TopN return scores.entrySet().stream() .sorted((a, b) -> Double.compare(b.getValue(), a.getValue())) .limit(topN) .map(e -> new RecommendItem(e.getKey(), e.getValue())) .collect(Collectors.toList()); }

逻辑说明:外层循环遍历用户已读的每本书,内层循环遍历全量图书,对每个 candidate 计算与已读书的相似度。sim * rated.getValue()就是加权贡献值,用scores.merge累加,最终预测分等于所有相似书的加权和。过滤条件写了两次:一次过滤已读书,一次过滤相似度非正的书,这两个条件缺一不可。参数说明:topN决定了最终返回的条数,一般取 8 到 12;如果相似度阈值想更严格,可以在sim <= 0.0处改成sim <= 0.05。

4.3 Service 层组装:空矩阵兜底与推荐结果封装

RecommendService 是整个推荐模块的门面,Controller 只跟它打交道。它的职责是查数据、构建矩阵、调用算法、兜底降级。

@Service public class RecommendService { @Autowired private RatingMapper ratingMapper; @Autowired private BorrowRecordMapper borrowRecordMapper; public List<RecommendItem> getTopN(Integer userId, Integer topN) { // 1. 查全量评分和已读书单 List<RatingPO> allRatings = ratingMapper.selectAll(); if (allRatings.isEmpty()) { // 2. 系统没有任何评分时,走热门兜底 return hotFallback(topN); } // 3. 构建倒排表并计算推荐 ItemBasedCF cf = new ItemBasedCF(); cf.buildMatrix(allRatings); return cf.recommend(userId, topN); } private List<RecommendItem> hotFallback(int topN) { // 按借阅次数倒序取热门图书 return borrowRecordMapper.selectHotBooks(topN); } }

逻辑说明:Service 层先判断全库是否为空评分状态,如果是,直接调hotFallback返回热门图书,避免算法引擎拿到空矩阵报错。矩阵有数据时才构建 ItemBasedCF 并调用recommend。这个兜底策略在项目初期几乎一定会触发,因为刚部署时没有用户行为数据。参数说明:hotFallback里的热门定义是借阅次数,可以在 borrow_record 表上GROUP BY book_id ORDER BY COUNT(*) DESC实现,返回结果统一封装成RecommendItem,前端不用区分个性化推荐和热门兜底。

5. 避坑记录:SSM 环境、中文乱码与冷启动的五条实战教训

这一章的内容是我拆过多个 SSM 毕设项目后攒下的血泪经验。源码能跑通是一回事,部署到你本地电脑上能不能跑通是另一回事,下面五条是我见过频率最高的坑,按“现象 → 原因 → 解决”的顺序写。

5.1 Tomcat 启动报错:javax.servlet 包冲突

现象:项目导入 IDEA 后,Tomcat 启动直接抛java.lang.LinkageError,或者提示ClassNotFoundException: javax.servlet.ServletContext,页面全部 404。

原因:spring-webmvc 的传递依赖里带了 servlet-api,IDE 误把它打包进了 WEB-INF/lib,和 Tomcat 自带的 servlet 实现冲突。有些同学还手动往项目里加过本地的 servlet-api.jar,这是最典型的翻车点。

解决:打开 pom.xml,在依赖声明里把 servlet-api 的scope改成provided,表示这个 jar 由 Tomcat 提供,不需要打包。如果依赖树里还有别的 jar 传递引入,用mvn dependency:tree查出来再排除。

5.2 中文乱码:建库、连接串、JSP 三处必须对齐

现象:图书标题、作者名在页面上显示成???,控制台日志也是乱码。

原因:MySQL 表字符集不是 utf8mb4,JDBC 连接串里没有指定characterEncoding=UTF-8,JSP 页面编码不一致。三个地方只要有其中一个不对,中文就会烂。

解决:建库时统一DEFAULT CHARSET=utf8mb4;JDBC url 尾部加上?useUnicode=true&characterEncoding=UTF-8;JSP 顶部pageEncoding="UTF-8"。改完重启 Tomcat,一般就正常了。注意,如果数据已经在乱码库里插进去了,光改连接串救不回来,需要清掉重建。

5.3 新用户推荐列表为空

现象:刚注册一个账号,登录后打开“为你推荐”,页面是空的。

原因:新用户没有任何评分记录,getUserRatings(userId)返回空 Map,recommend方法第一步就返回空列表。这是 ItemCF 的冷启动问题,不是代码 bug。

解决:Service 层加兜底逻辑,就是 4.3 里的hotFallback。新用户先看热门榜,等用户借了几本书产生评分后,再切换到个性化推荐。判断条件很简单:该用户评分记录数为 0 就走热门。

5.4 相似度全是 0,推荐列表顺序和热榜一样

现象:算法跑了,但推荐结果排序很奇怪,基本等于随机,细看相似度全是 0。

原因:评分矩阵太稀疏,两本书几乎没有共同评分的用户,余弦点积永远为 0。这在数据量小的毕设项目里太常见了,几十个用户、几百条评分,算出来的相似度矩阵基本是空的。

解决:两个方向。数据侧,把“浏览过详情页”也映射成 1 分写进评分表,增加矩阵填充率;算法侧,把纯余弦改成“共同用户数加权的余弦相似度”,比如sim = 余弦值 * Math.log(1 + 共同用户数),或者要求两个物品至少有 2 个共同评分用户才参与计算。改完重跑,推荐列表会明显有区分度。

5.5 JDK 版本:用 JDK17 跑老 SSM 项目编译报错

现象:代码编译通过,但 Tomcat 启动时频繁报模块访问错误,或者 Spring 的 CGLIB 动态代理直接抛异常。

原因:SSM 项目大多是 Spring 4.x/5.x 配 MyBatis 3.4/3.5,这些版本是按 JDK8 写的。JDK17 强封装模块限制了对内部 API 的反射访问,老框架直接踩雷。

解决:装 JDK8,并且把 IDE 的 Project Structure、Maven 编译器 target、Tomcat JRE 三处全部指向 JDK8。环境变量 PATH 里也确认java -version显示的确实是 1.8。这些老项目认准 JDK8 加 Tomcat 8.5/9 是最省事的组合,不要在这上面追求新版本。

你会发现这五条有一个共同点:全是环境配置和数据问题,没有一条是算法逻辑问题。协同过滤算法本身不难,难的是让它在你的电脑上跑起来、让评分数据喂得进去。

6. 验证与调参:三步把推荐从“能跑”变成“能答辩”

最后说一个很多人忽略的动作——验证。跑通代码只是起点,推荐效果到底怎么样,需要用数据说话,这也是答辩和实际项目里最能体现专业度的地方。

6.1 离线命中率验证

我一般会把评分数据按时间切分,前 80% 的用户行为作为训练集,后 20% 作为测试集。用训练集跑推荐,然后检查测试集里用户实际借阅的书,有多少出现在推荐列表里,这个比例就是命中率。

-- 命中率简算:测试期借阅且被推荐的数量 / 测试期总借阅数量 SELECT COUNT(DISTINCT b.book_id) AS hit_count FROM borrow_record b WHERE b.borrow_time >= '2024-06-01' AND EXISTS ( SELECT 1 FROM recommend_result r WHERE r.user_id = b.user_id AND r.book_id = b.book_id );

逻辑说明:这个 SQL 只是简算思路,关键是理解指标含义。命中率 5% 以上,说明推荐不是瞎猜;10% 以上就很有说服力了,可以写进论文。参数说明:切分时间点要根据你的数据量调整,测试集至少要覆盖用户真实借阅行为,时间窗口太短命中率会失真。

6.2 三个关键参数怎么调

参数位置建议范围调整方向
topNController / Service8 - 12越大覆盖率越高,精准度下降
K 最近邻数recommend 方法5 - 15太小噪声大,太大引入弱相关
相似度阈值similarity 调用处0.05 - 0.1稀疏矩阵时反而要调低

数据稀疏时,K 值和阈值的联动效应很强。K 调小、阈值调高,推荐列表会更聚焦,但容易空;反过来,列表会变长但相关性下降。我调参的习惯是:第一次跑用 K=10、阈值 0.05,看命中率再决定下一步方向,而不是凭感觉乱试。

6.3 给推荐结果加解释

答辩时最容易加分的技巧,是把推荐理由展示在页面上。实现起来不复杂,在RecommendItem里加一个 sourceBook 字段,记录推荐来源是哪本书。展示时写成“因为您借过《算法导论》,所以推荐《编程珠玑》”,比单纯列出推荐列表有说服力得多。

从那以后我每次调完参数都会强制自己走一遍离线命中率计算,确认改动是正向的还是负向的,不再凭感觉上线。这个习惯让我在毕设答辩和实际项目里都少挨了不少骂。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询