ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaWeb数码推荐平台:手写Servlet+JSP实现协同过滤

JavaWeb数码推荐平台:手写Servlet+JSP实现协同过滤 简介本资源是一套基于JavaWeb技术栈开发的数码产品推荐平台完整项目源码面向计算机专业本科生、毕业设计学生及Java全栈初学者解决数码商品分类展示、智能筛选与会员制下载管理等典型电商场景需求。项目采用前后端分离架构后端基于SSM框架SpringSpringMVCMyBatisMySQL存储数据前端兼容多端实现含Vue组件、HTML页面及JS交互逻辑支持手机、笔记本、台式机配件三大类目及价格区间筛选产品资料下载需会员权限。压缩包共812个文件涵盖119个Java业务逻辑文件、46个Vue组件、156个JS脚本、46个CSS样式文件及大量SVG图标与图片资源整体大小16.13MB结构清晰含build/run/install批处理脚本及数据库SQL文件便于快速部署调试。目前已有196人学习下载提供可运行的完整工程、模块化目录结构及备份文件如.vue.bak、.html.bak有助于理解真实项目分层设计与前后端协同机制。1. 为什么一个“数码产品推荐平台”要用 JavaWeb 而不是直接上 Spring Boot你手头正赶毕业设计导师甩来一句“做个基于 JavaWeb 的数码产品推荐平台”你打开 IDEA新建项目时却卡在第一步到底该选 Servlet/JSP 原生栈还是强行套 Spring Boot 框架网上搜“javaweb 数码产品推荐平台”满屏是 ZIP 包下载链接、毕设代做广告、还有写着“含源码数据库论文”的压缩包——但没人告诉你这个 ZIP 里真正决定系统能不能跑通、推荐结果靠不靠谱、答辩老师问一句“协同过滤怎么实现的”你能不能答上来全在三层结构里那 37 行核心推荐逻辑和 4 张关键表的设计上。这不是一个“用 JSP 做个登录页 MySQL 存商品”的练手项目而是要让“用户浏览手机详情页后系统能实时从 2000 数码 SKU 中筛出 5 款他大概率会加购的耳机/充电宝/智能手表”背后牵扯用户行为建模、冷启动应对、实时点击流聚合、以及 Web 层如何把推荐结果稳稳塞进div idrecomm-list里——而 JavaWeb 的价值恰恰在于它逼你亲手串起 Servlet 生命周期、JDBC 连接池配置、JSP EL 表达式渲染链路而不是靠RestController自动生成 JSON 就交差。适合正在写毕设、需要可答辩、可演示、可讲清技术细节的计算机/软件工程本科生也适合想补足 Web 底层链路、摆脱“只会 Spring Boot 注解”的初级开发。2. 从零搭起 JavaWeb 推荐平台Servlet JSP MySQL 最小可行架构2.1 为什么坚持用原生 JavaWeb 而不是 Spring Boot这不是守旧是教学场景下的理性选择。Spring Boot 自动装配掩盖了太多关键路径比如DispatcherServlet怎么拦截/recommend请求、Filter如何在请求进入前解析用户 session、DataSource连接池参数调优对并发推荐接口的影响——这些恰恰是毕设答辩时老师最爱问的“你这个系统请求进来后到底走了哪几步”而原生 JavaWeb 强制你写明web.xml中servlet-mapping显式绑定/RecommendServletRecommendServlet.java中doGet()里手动调用UserBehaviorAnalyzer.getTopNByCollaborativeFiltering(userId, 5)JSP 页面用c:forEach items${recommList} varitem渲染结果而非 Thymeleaf 的th:each。这种“每一步都看得见”的架构让代码可追溯、逻辑可打断、调试可断点——当你在RecommendServlet的第 42 行打上断点看到userId1024传进来getTopNByCollaborativeFiltering返回[AirPods Pro, Anker 737, Xiaomi Watch S1]你就真懂了推荐是怎么“算出来”的。提示本方案采用 Tomcat 9.0 JDK 8兼容性最强MySQL 5.7避免高版本权限问题所有依赖仅需servlet-api.jar、jstl.jar、mysql-connector-java-5.1.47.jar三件套杜绝 Maven 多模块嵌套导致的 classpath 冲突。2.2 四张核心数据表不靠 ORM手写 SQL 建模推荐关系推荐效果好不好一半看算法一半看数据结构是否支撑得住。本系统摒弃 Hibernate 等全自动 ORM用纯 JDBC 手写 SQL 控制每一处关联。四张表设计直指数码推荐痛点表名字段关键设计意图数码场景特例user_behavioruser_id,product_id,behavior_type(view/click/cart/buy),timestamp记录用户与数码产品的交互粒度behavior_typeview且product_id对应“iPhone 15 Pro 256GB”时权重设为 1buy权重设为 5product_infoproduct_id,category(手机/耳机/配件),brand,price,score_avg,tags(快充、降噪、OLED)结构化数码属性支撑基于内容的召回tags用逗号分隔便于FIND_IN_SET(降噪, tags)快速筛选user_profileuser_id,preference_tags(游戏手机, 摄影党, 学生党),last_active_time解决新用户冷启动preference_tags由注册时勾选生成非空字段避免 NULL 导致推荐失效recommend_cacheuser_id,recomm_list(JSON 格式),update_time,valid_hours缓存推荐结果降低实时计算压力valid_hours2用户 2 小时内再次访问直接读缓存避免重复调用协同过滤建表 SQL 示例MySQL 5.7CREATE TABLE user_behavior ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, behavior_type ENUM(view,click,cart,buy) NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_product (user_id, product_id), INDEX idx_time (timestamp) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 注意product_info 的 tags 字段不建索引因使用 FIND_IN_SET 查询建索引反而拖慢写入2.3 推荐引擎入口RecommendServlet 的 3 个关键职责RecommendServlet是整个系统的“推荐中枢”它不负责算法实现但必须精准调度、兜底、埋点。其doGet()方法承担三项硬任务用户身份校验与 session 绑定从request.getSession().getAttribute(userId)取 ID若为空则跳转登录页绝不允许未登录用户触发推荐逻辑防止爬虫刷接口缓存穿透防护先查recommend_cache表若update_time超过valid_hours或记录不存在则调用RecommendEngine.generateForUser(userId)生成新结果并写回缓存结果安全封装将Product对象 List 转为 JSON用org.json.JSONObject不引入 Jackson通过request.setAttribute(recommList, jsonStr)传给 JSP禁止直接response.getWriter().print(jsonStr)输出否则 JSP 无法渲染。关键代码段RecommendServlet.javaprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(false); Integer userId session ! null ? (Integer) session.getAttribute(userId) : null; if (userId null) { response.sendRedirect(login.jsp); return; // 重要return 阻止后续执行 } // 1. 查缓存 RecommendCache cache RecommendCacheDAO.findByUserId(userId); ListProduct recommList; if (cache ! null !isCacheExpired(cache.getUpdateTime(), cache.getValidHours())) { recommList ProductDAO.findByIds(JSON.parseArray(cache.getRecommList(), Integer.class)); } else { // 2. 生成新推荐调用算法类 recommList RecommendEngine.generateForUser(userId); // 3. 写缓存异步更佳此处同步简化 cache new RecommendCache(userId, toJSON(recommList), 2); // 2小时有效 RecommendCacheDAO.save(cache); } request.setAttribute(recommList, recommList); // 传给 JSP request.getRequestDispatcher(recommend.jsp).forward(request, response); }参数说明isCacheExpired()判断逻辑为System.currentTimeMillis() - updateTimestamp validHours * 3600 * 1000toJSON()方法手动拼接 JSON 字符串避免依赖库确保{id:101,name:AirPods Pro,price:1899}格式。3. 协同过滤推荐算法不用 Mahout手写 UserCF 实现附可运行代码3.1 为什么选 User-Based Collaborative Filtering数码产品有强品类关联性买 iPhone 的用户大概率也看 AirPods、MagSafe 充电器、Apple Pencil但买机械键盘的用户几乎不碰耳机。ItemCF 在跨品类时容易推荐偏差如给手机用户推“RTX4090 显卡”而 UserCF 基于“相似用户买了什么”做推荐天然适配数码圈层——我们只需保证user_behavior表中行为数据足够单用户 ≥ 10 条有效行为UserCF 就能跑出可用结果。且算法逻辑清晰找出与目标用户Ua行为最相似的 K 个用户K10这 K 人买过但Ua没买过的商品按相似度加权排序取 Top-N。全程不依赖外部 ML 库纯 Java 实现答辩时可逐行讲解。3.2 核心算法类 RecommendEngine.java3 个方法撑起推荐主干public class RecommendEngine { // 1. 计算用户相似度余弦相似度 public static double calculateUserSimilarity(int userA, int userB) { // 获取两用户的共现商品集合都浏览/购买过的商品ID SetInteger commonItems getCommonItems(userA, userB); if (commonItems.isEmpty()) return 0.0; // 分子共现商品数 double numerator commonItems.size(); // 分母用户A行为总数 × 用户B行为总数 开根 double denominator Math.sqrt(getUserBehaviorCount(userA) * getUserBehaviorCount(userB)); return numerator / denominator; } // 2. 获取相似用户 Top-K按相似度降序 public static ListInteger getSimilarUsers(int targetUserId, int k) { ListUserSimilarity similarities new ArrayList(); // 扫描所有其他用户生产环境需加缓存此处简化 ListInteger allUserIds UserDao.findAllUserIds(); for (int otherUserId : allUserIds) { if (otherUserId targetUserId) continue; double sim calculateUserSimilarity(targetUserId, otherUserId); if (sim 0.1) { // 过滤低相似度用户减少计算量 similarities.add(new UserSimilarity(otherUserId, sim)); } } // 按相似度排序取前K similarities.sort((a, b) - Double.compare(b.similarity, a.similarity)); return similarities.stream().limit(k).map(s - s.userId).collect(Collectors.toList()); } // 3. 生成推荐列表核心业务逻辑 public static ListProduct generateForUser(int userId) { ListInteger similarUsers getSimilarUsers(userId, 10); MapInteger, Double candidateScores new HashMap(); // 遍历每个相似用户 for (int similarUserId : similarUsers) { // 获取该相似用户买过但目标用户没买过的商品 ListInteger boughtBySimilar BehaviorDao.findBoughtProducts(similarUserId); ListInteger boughtByTarget BehaviorDao.findBoughtProducts(userId); SetInteger candidates new HashSet(boughtBySimilar); candidates.removeAll(boughtByTarget); // 差集 // 为每个候选商品累加分数相似度 × 行为权重 double similarity calculateUserSimilarity(userId, similarUserId); for (Integer productId : candidates) { double weight BehaviorDao.getBehaviorWeight(similarUserId, productId); // view1, buy5 candidateScores.merge(productId, similarity * weight, Double::sum); } } // 按分数降序取 Top-5 return candidateScores.entrySet().stream() .sorted(Map.Entry.Integer, DoublecomparingByValue().reversed()) .limit(5) .map(entry - ProductDAO.findById(entry.getKey())) .filter(Objects::nonNull) // 过滤已被下架的商品 .collect(Collectors.toList()); } }关键参数说明getBehaviorWeight()返回行为权重view→1,click→2,cart→3,buy→5体现用户决策深度calculateUserSimilarity()中sim 0.1是经验值低于此值视为无实际参考价值直接丢弃generateForUser()中candidates.removeAll(boughtByTarget)是防“推荐用户已买商品”的硬规则答辩时可强调“这是商业逻辑不是算法缺陷”。3.3 数据预热用 Python 脚本生成 5000 条模拟行为数据附脚本真实数据难获取但毕设演示需要“看起来像真数据”。用 Python 脚本生成符合数码消费规律的模拟数据直接导入 MySQL# generate_behavior_data.py import random import pymysql from datetime import datetime, timedelta # 模拟用户1000 个标签分布 user_tags [游戏手机, 摄影党, 学生党, 商务人士, 数码发烧友] users [{id: i, tag: random.choice(user_tags)} for i in range(1, 1001)] # 模拟商品2000 个按品类分组 categories { 手机: list(range(1, 301)), 耳机: list(range(301, 601)), 充电器: list(range(601, 901)), 智能手表: list(range(901, 1201)), 平板: list(range(1201, 1501)), 配件: list(range(1501, 2001)) } # 行为类型权重模拟用户决策链路 behaviors [(view, 0.6), (click, 0.25), (cart, 0.1), (buy, 0.05)] def get_product_ids_by_tag(tag): if 游戏 in tag: return categories[手机] categories[耳机] categories[充电器] if 摄影 in tag: return categories[手机] categories[智能手表] categories[配件] if 学生 in tag: return categories[手机] categories[耳机] categories[平板] return sum(categories.values(), []) # 默认全品类 # 生成 5000 条行为记录 records [] base_time datetime.now() - timedelta(days30) for _ in range(5000): user random.choice(users) product_ids get_product_ids_by_tag(user[tag]) product_id random.choice(product_ids) behavior, prob random.choices(behaviors, weights[w for _, w in behaviors])[0] timestamp base_time timedelta(secondsrandom.randint(0, 2592000)) # 30天内随机时间 records.append((user[id], product_id, behavior, timestamp)) # 插入 MySQL需提前创建好 user_behavior 表 conn pymysql.connect(hostlocalhost, userroot, password123456, dbdigital_recomm) cursor conn.cursor() cursor.executemany( INSERT INTO user_behavior (user_id, product_id, behavior_type, timestamp) VALUES (%s, %s, %s, %s), records ) conn.commit() cursor.close() conn.close() print(✅ 5000 条模拟数据插入完成)执行前确认MySQL 已启动数据库digital_recomm已创建user_behavior表结构与 2.2 节一致。脚本运行后SELECT COUNT(*) FROM user_behavior应返回 5000SELECT * FROM user_behavior LIMIT 5可验证数据合理性如user_id123与product_id456出现在同一行。4. JSP 页面渲染与交互让推荐结果“活”在网页上4.1 recommend.jsp用 JSTL EL 实现动态渲染拒绝 scriptletJSP 不是 HTML它的价值在于服务端逻辑与视图的紧耦合。本页面严格禁用% %脚本片段全部用 JSTL 标签和 EL 表达式确保逻辑清晰、易维护、可被答辩老师一眼看懂% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head title数码推荐/title link relstylesheet hrefcss/recomm.css /head body div classcontainer h2为您推荐/h2 c:choose c:when test${empty recommList || recommList.size() 0} p classno-data暂无推荐试试浏览热门商品吧/p /c:when c:otherwise div classrecomm-grid c:forEach items${recommList} varproduct varStatusstatus div classproduct-card img srcimages/${product.id}.jpg alt${product.name} onerrorthis.srcimages/placeholder.jpg h3${product.name}/h3 p classprice¥${product.price}/p p classcategory${product.category}/p div classtags c:forEach items${fn:split(product.tags, ,)} vartag varStatustagStatus span classtag${tag}/span /c:forEach /div button onclickaddToCart(${product.id}) classbtn-buy加入购物车/button /div c:if test${status.count % 3 0}div classclearfix/div/c:if /c:forEach /div /c:otherwise /c:choose /div script function addToCart(productId) { fetch(CartServlet?productId productId actionadd, { method: POST, headers: {Content-Type: application/x-www-form-urlencoded} }) .then(response response.text()) .then(data { if (data success) { alert(已加入购物车); } else { alert(加入失败 data); } }); } /script /body /html关键设计点onerrorthis.srcimages/placeholder.jpg防止图片加载失败导致布局错乱c:if test${status.count % 3 0}实现每行 3 个卡片的 CSS Grid 布局配合.recomm-grid { display: grid; grid-template-columns: repeat(3, 1fr); }fn:split(product.tags, ,)将数据库中逗号分隔的tags字段拆成数组循环渲染为span classtag降噪/span直观展示推荐理由。4.2 推荐理由浮层用 AJAX 加载“为什么推荐这个”用户看到“AirPods Pro”被推荐会问“为什么”。我们在卡片右上角加一个ⓘ图标点击后用 AJAX 加载解释!-- 在 product-card 内添加 -- div classreason-icon onclickshowReason(${product.id}, ${userId})ⓘ/div script function showReason(productId, userId) { fetch(ReasonServlet?productId productId userId userId) .then(response response.json()) .then(data { alert(推荐理由\n data.reason \n相似用户 data.similarUsers.join(, )); }); } /script对应的ReasonServlet.javaprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int productId Integer.parseInt(request.getParameter(productId)); int userId Integer.parseInt(request.getParameter(userId)); // 查找哪些相似用户买了这个商品用于解释 ListInteger similarUsers RecommendEngine.getSimilarUsers(userId, 5); ListInteger buyers new ArrayList(); for (int suId : similarUsers) { if (BehaviorDao.hasBought(suId, productId)) { buyers.add(suId); } } String reason 您关注的用户 buyers.get(0) 也购买了此商品; if (buyers.size() 1) { reason 且 buyers.subList(1, Math.min(3, buyers.size())).stream() .map(String::valueOf).collect(Collectors.joining(, )) 等用户也购买了; } response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\reason\:\ reason \,\similarUsers\: new JSONArray(buyers).toString() }); }效果点击ⓘ后弹窗显示“推荐理由您关注的用户 1024 也购买了此商品且 1089, 1156 等用户也购买了”把抽象算法翻译成用户能懂的语言——这是答辩加分项。5. 避坑指南JavaWeb 数码推荐平台 5 个血泪经验现象→原因→解决5.1 现象Tomcat 启动后访问/recommend报 404但/login.jsp正常原因web.xml中servlet-mapping的url-pattern写成了/RecommendServlet带大写 R而浏览器请求的是/recommend小写。JavaWeb 对 URL 大小写敏感且WebServlet注解未生效因用了web.xml配置。解决统一改为小写url-pattern/recommend/url-pattern并在RecommendServlet类上删掉WebServlet注解避免双配置冲突。5.2 现象推荐结果总是空recommList传到 JSP 后 size0原因RecommendEngine.generateForUser()中getSimilarUsers()返回空列表根源是user_behavior表里user_id1024的行为记录太少 5 条导致calculateUserSimilarity()计算时commonItems.isEmpty()为 true相似度恒为 0。解决在数据预热脚本中为每个用户强制生成至少 8 条行为min_behavior_per_user8或在算法中增加 fallback当相似用户为空时退化为“热门商品推荐”SELECT * FROM product_info ORDER BY sales_count DESC LIMIT 5。5.3 现象JSP 页面中文显示为??但数据库里存的是正确汉字原因Tomcat 的conf/server.xml中Connector缺少URIEncodingUTF-8导致 GET 请求参数如?productId1024中的中文字符虽本例无但扩展功能会用到被错误解码。解决编辑server.xml在Connector port8080 ... /标签内添加URIEncodingUTF-8重启 Tomcat。5.4 现象点击“加入购物车”按钮无反应控制台报403 Forbidden原因CartServlet的doPost()方法未正确处理 CSRF跨站请求伪造而现代浏览器对fetch()POST 请求默认发送Origin头Tomcat 8.5 默认拦截无Referer的跨域 POST。解决在CartServlet开头加判断String referer request.getHeader(Referer); if (referer null || !referer.contains(localhost:8080)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, 非法请求来源); return; }或更简单在web.xml中配置security-constraint放宽限制仅限开发环境。5.5 现象推荐结果每次刷新都变但用户没新行为理论上应稳定原因RecommendCache.update_time字段用的是CURRENT_TIMESTAMP但isCacheExpired()判断逻辑是System.currentTimeMillis() - updateTimestamp validHours * 3600 * 1000而updateTimestamp是数据库时间System.currentTimeMillis()是服务器时间两者可能有秒级偏差导致缓存频繁失效。解决统一用数据库时间判断在RecommendCacheDAO.findByUserId()查询时加条件AND update_time DATE_SUB(NOW(), INTERVAL 2 HOUR)彻底规避时钟不同步问题。6. 让推荐“可验证”3 种低成本效果验证法不用 A/B 测试6.1 人工构造测试用例用确定性输入验证算法输出别等上线后看数据先用“人造场景”锤炼算法。在TestRecommendEngine.java中写单元测试Test public void testUserCFWithKnownData() { // 构造测试数据用户1001 和 1002 都买了商品201、202用户1001 还买了203用户1002 还买了204 insertMockBehavior(1001, 201, buy); insertMockBehavior(1001, 202, buy); insertMockBehavior(1001, 203, buy); insertMockBehavior(1002, 201, buy); insertMockBehavior(1002, 202, buy); insertMockBehavior(1002, 204, buy); ListProduct result RecommendEngine.generateForUser(1001); // 断言204 应在推荐列表中因用户1002买了204且与1001相似203 不应在1001自己买了 assertTrue(result.stream().anyMatch(p - p.getId() 204)); assertFalse(result.stream().anyMatch(p - p.getId() 203)); }这比“跑一遍看页面有没有商品”靠谱十倍。答辩时老师问“你确定推荐准吗”你直接打开 IDE 展示这个测试用例说“我用 3 行行为数据验证了算法逻辑”可信度拉满。6.2 日志埋点分析用 System.out 打印推荐路径生产环境换 Log4j在RecommendServlet关键节点加日志不依赖监控系统用最原始方式追踪// 在 generateForUser() 开头 System.out.println([RECOMM] Start generating for user userId); // 在 getSimilarUsers() 返回前 System.out.println([RECOMM] Found similarUsers.size() similar users: similarUsers); // 在 generateForUser() 结尾 System.out.println([RECOMM] Generated recommList.size() items: recommList.stream().map(p - p.getName()).collect(Collectors.joining(, )));启动 Tomcat 后观察catalina.out日志[RECOMM] Start generating for user 1024 [RECOMM] Found 7 similar users: [1089, 1156, 1203, ...] [RECOMM] Generated 5 items: AirPods Pro, Anker 737, Xiaomi Watch S1, ...这就是你的“黑匣子飞行记录仪”。当推荐出错第一眼就看日志里Found 0 similar users还是Generated 0 items快速定位是数据问题还是算法问题。6.3 推荐多样性检查用 Excel 快速统计品类覆盖导出recommend_cache表中最近 100 条记录的recomm_list字段用 Excel 公式提取品类user_idrecomm_list (JSON)category1category2...多样性得分1024[{id:101,category:耳机},...]耳机充电器智能手表3/560%公式LEN(SUBSTITUTE(SUBSTITUTE(A2,category:,),},))-LEN(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(A2,category:,),},),,,))1如果 100 个用户的推荐列表中80% 都是“耳机”说明算法陷入局部最优只推热门品类需调整getBehaviorWeight()中buy权重或增加品类惩罚因子。这比等用户投诉“怎么老推耳机”早发现两周。最后说句实在的这个 ZIP 包里的代码从来不是为了“跑起来就行”而是为了让你在答辩现场当老师指着屏幕问“这个推荐到底是怎么算出来的”你能不翻笔记、不卡壳、手指着RecommendEngine.java第 87 行说“老师这里用相似用户的行为权重加总比如用户 1024 和 1089 相似度是 0.731089 买了 AirPods Pro所以给它加 0.73×5 分……”——那一刻你写的不是毕设是工程师的底气。希望帮到你。本文还有配套的精品资源点击获取
返回列表