
简介基于Java SSM框架与Python TF-IDF算法实现的用户画像购物网站商品推荐系统是一套面向计算机相关专业毕业设计、课程设计及项目实践的高分完整项目。资源涵盖系统源码、毕业论文、数据库脚本与使用文档代码已在mac及Windows10/11环境下多次测试运行通过并获导师认可、答辩评审95分既可整体移植也可在原逻辑基础上扩展二次开发。压缩包内共1173个文件以HTML/CSS/JS构建前端页面JSP/Java实现后端业务Python脚本用于TF-IDF推荐计算SQL文件提供建库建表及样例数据另含图片、GIF演示、视频和相关配置文件整体仅26.75MB目录清晰便于按需检索。推荐模块围绕用户画像与TF-IDF算法展开论文详细叙述了系统设计、协同过滤与画像构建思路使用文档则帮助快速完成环境配置和项目部署。已有230人学习下载适合正在做推荐系统课题或入门SSM开发的读者参考学习。1. 从 SSM 与 Python 混编排开始一次用户画像推荐系统的完整拆解购物网站的商品推荐很多人第一反应是用纯 Java 写协同过滤或者干脆上 Elasticsearch 做召回。但真正落地时会发现一个尴尬TF-IDF 这类文本向量化算法在 Java 里写起来要处理分词器、稀疏矩阵、相似度计算代码量不小调试也不直观而用 Python 写算法只要几十行却又要解决与 Java 业务系统的集成问题。这个基于 javaSSMPythonTF-IDF 算法的用户画像购物网站商品推荐系统给出的方案是让 SSM 负责业务闭环Python 独立承担画像计算两者通过标准 JSON 接口通信。这样既保住了电商系统的稳定性和事务能力又把算法迭代的成本压到最低。本文会按照架构设计、SSM 业务实现、TF-IDF 画像构建、双语言联调、部署验证这条线把整套代码的运行逻辑和改造要点讲清楚。2. SSM 骨架与购物网站核心业务的落地方式2.1 Spring、SpringMVC、MyBatis 在项目中的实际分工SSM 不是三个框架的简单叠加而是各管一段Spring 负责对象生命周期和事务边界SpringMVC 负责 HTTP 层的路由与参数绑定MyBatis 负责 SQL 与 Java 对象的映射。在这个项目中商品列表、用户注册登录、购物车、订单这些模块全部跑在 SSM 之上算法推荐的结果只是作为一个普通数据源注入到页面里。实际代码中Spring 的注解扫描配置是项目运行的基础通常会在spring-mvc.xml和applicationContext.xml中做如下切分!-- applicationContext.xml不扫描 Controller只管理 Service、DAO 和事务 -- context:component-scan base-packagecom.shop context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan !-- spring-mvc.xml只扫描 Controller避免重复实例化 -- context:component-scan base-packagecom.shop.controller use-default-filtersfalse context:include-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan这里的关键是use-default-filtersfalse。如果两个配置文件都扫描同一个包Service 层会被实例化两次事务注解可能失效。很多人在做 SSM 整合时遇到Transactional不生效多半就是这个原因。2.2 商品浏览与用户行为采集的表结构设计用户画像不是凭空生成的它依赖用户在网站上的每一次操作。这套系统在设计上把行为数据单独建表而不是散落在订单表里这是比较正确的做法。核心行为表结构如下CREATE TABLE user_behavior ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT 用户ID, product_id INT NOT NULL COMMENT 商品ID, behavior_type TINYINT NOT NULL COMMENT 1浏览 2收藏 3加购 4购买, behavior_score DOUBLE DEFAULT 0 COMMENT 行为权重分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;每条行为记录都有独立的权重分behavior_score这是为了后续 Python 侧计算用户画像时直接读取数值而不用在算法脚本里再做一次行为映射。浏览、收藏、加购、购买四种行为的权重可以设为 0.2、0.5、0.8、1.5这个比例可以根据业务调。行为数据采集的入口在 SpringMVC 的 Controller 层用户点击商品详情页时通过 AOP 切面或拦截器异步写入这张表。异步写入一般用一个线程池搞定避免行为采集阻塞主流程。2.3 用户画像标签的 Java 侧缓存设计Python 算完用户画像后结果会写回 MySQL 的user_profile表。Java 侧如果每次都查数据库性能会有明显瓶颈尤其是首页推荐位需要同时渲染多个用户的数据时。常见做法是用 Redis 做一层缓存key 设计为user:profile:{userId}value 直接存 JSON 字符串过期时间设为画像的重算周期。代码实现如下Service public class ProfileCacheService { Autowired private StringRedisTemplate redisTemplate; private static final String PROFILE_KEY_PREFIX user:profile:; public String getUserProfile(Integer userId) { String key PROFILE_KEY_PREFIX userId; String profile redisTemplate.opsForValue().get(key); if (profile null) { // 缓存未命中从数据库加载并回填 profile loadProfileFromDb(userId); redisTemplate.opsForValue().set(key, profile, 6, TimeUnit.HOURS); } return profile; } }缓存过期时间的设置要跟 Python 端的重算任务配合。如果画像每 6 小时重算一次Redis 的 TTL 也设为 6 小时能保证数据不会太旧也不会在算完之前被击穿。缓存穿透的防护可以在 Java 侧加一个空值缓存避免用户没有画像时每次都打到数据库。3. TF-IDF 算法与用户画像构建的工程化实现3.1 TF-IDF 原理回顾与在推荐场景中的意义TF-IDF 的核心思想是一个词在某个用户的行为序列里出现频率高但在所有用户中很少出现那这个词就具有很好的区分度。公式上 TF 是词频IDF 是逆文档频率两者相乘得到词的权重。在用户画像场景里「文档」不是文章而是一个用户所有行为涉及的商品文本信息拼接结果。比如用户浏览过的商品标题、商品描述、品类名称把它们拼成一段文本再按 TF-IDF 计算关键词权重。权重高的词就是这个用户的兴趣标签。这套推荐系统的设计思路是把每个用户的历史行为文本当作一个独立文档构建用户-关键词的 TF-IDF 矩阵。与传统的基于物品的协同过滤相比TF-IDF 方法的优势在于新商品没有评分数据时只要商品描述中有与用户标签匹配的关键词也能被推荐出去这属于内容推荐的一种正好补足了协同过滤冷启动的短板。3.2 Python 侧的文本预处理与 TF-IDF 计算代码Python 端的主要工作是读取 MySQL 中的用户行为数据与商品表关联得到文本内容再做分词和 TF-IDF 计算。核心代码用jieba分词加sklearn的TfidfVectorizer实现import pymysql import jieba import jieba.analyse import json from collections import defaultdict def load_user_texts(): conn pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databaseshop_recommend, charsetutf8mb4 ) cursor conn.cursor() sql SELECT ub.user_id, GROUP_CONCAT(CONCAT(p.product_name, , p.category_name)) FROM user_behavior ub JOIN product p ON ub.product_id p.id GROUP BY ub.user_id cursor.execute(sql) rows cursor.fetchall() cursor.close() conn.close() return {user_id: text for user_id, text in rows} def build_tfidf_profile(user_id, text, top_k10): tags jieba.analyse.extract_tags( text, topKtop_k, withWeightTrue ) return { user_id: user_id, tags: [{tag: tag, weight: round(float(w), 4)} for tag, w in tags] }jieba.analyse.extract_tags底层封装的就是 TF-IDF 算法它先对文本做全切分通过词频和 IDF 值计算权重返回 topK 个关键词。用withWeightTrue可以拿到具体权重值这个值直接作为画像标签的置信度。注意GROUP_CONCAT默认有长度限制默认 1024 字节如果用户行为很多文本会被截断此时需要在 MySQL 中设置group_concat_max_len。3.3 用户画像权重归一化与标签结构单纯的 TF-IDF 权重不适合直接存储因为不同用户的文本长度差异大权重的绝对值没有跨用户可比性。实际落地时要做一次归一化处理把同一用户的所有标签权重压缩到 0 到 1 之间。归一化公式用最大最小值缩放Python 侧可以这样实现def normalize_tags(tags): if not tags: return [] max_weight max(t[weight] for t in tags) min_weight min(t[weight] for t in tags) if max_weight min_weight: for t in tags: t[weight] 1.0 return tags for t in tags: t[weight] round((t[weight] - min_weight) / (max_weight - min_weight), 4) return tags归一化之后的画像 JSON 结构是这样的{ user_id: 1001, profile_time: 2025-05-20 10:30:00, tags: [ {tag: 机械键盘, weight: 1.0}, {tag: 电竞, weight: 0.87}, {tag: 有线, weight: 0.64} ] }这个 JSON 会通过 Python 的pymysql写入user_profile表的profile_json字段同时把 top 标签拆出来放到独立的tag_name、tag_weight字段中方便 Java 侧做 SQL 查询和排序。3.4 关于 TF-IDF 与用户画像结合时的边界问题TF-IDF 本身存在一个已知局限它只考虑词频和逆文档频率不关心词与词之间的语义关系。比如用户看了「华为手机」和「小米手机」TF-IDF 会把「华为」「小米」标为高权重标签但无法理解这两个词其实属于同一品类。这个项目没有引入 Word2Vec 或 BERT 做语义向量所以如果想进一步做品类泛化可以在 Python 端维护一个同义词映射表把同义词合并后重新累加权重。这种做法性价比很高不需要引入重模型但对推荐结果的多样性有明显帮助。4. Java 与 Python 协同工作的接口设计与数据库实现4.1 两种跨语言集成方式的选型对比Java 调用 Python 有两条主流路径一是用ProcessBuilder直接启动 Python 子进程二是 Python 侧用 Flask 封装 HTTP 服务。这个毕业设计项目采用的是独立 Python 脚本加重启触发的方式即由 Java 定时任务调用 Python 脚本完成画像重算。下面是对比集成方式优点缺点适用场景ProcessBuilder 调用脚本部署简单无需启动额外服务每次启动有解释器开销定时批量任务Flask 常驻服务响应快适合实时推荐多一个进程要维护在线实时推荐Spring 集成 Jython全 Java 栈Jython 对 Python 3 支持差不推荐对于用户画像这种小时级更新的数据用ProcessBuilder加定时任务足够了。Java 侧用 Spring 的Scheduled注解和 Quartz 都可以核心调用代码如下Scheduled(cron 0 0 */6 * * ?) public void executeProfileTask() { try { ProcessBuilder pb new ProcessBuilder( python3, /opt/recommend/build_profile.py, --date, LocalDate.now().toString() ); pb.redirectErrorStream(true); Process process pb.start(); // 读取子进程输出避免阻塞 try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line reader.readLine()) ! null) { log.info(python output: {}, line); } } int exitCode process.waitFor(); if (exitCode ! 0) { log.error(profile build failed, exit code: {}, exitCode); } } catch (Exception e) { log.error(profile task error, e); } }这里有两个常见坑一是ProcessBuilder的环境变量可能不包含 Python 的安装路径建议在命令中用绝对路径或先加载/etc/profile二是子进程的 stdout 和 stderr 输出如果不及时读取缓冲区满了之后子进程会阻塞挂死所以redirectErrorStream(true)和实时读取缺一不可。4.2 推荐商品查询的 SQL 设计与候选集生成画像标签写入数据库之后Java 侧要做的工作是根据标签查找候选商品。最简单高效的查询方式是把标签词拆成多个LIKE条件用OR连接再按匹配命中的数量降序排列SELECT p.id, p.product_name, p.category_name, (CASE WHEN p.product_name LIKE %机械键盘% THEN 1 ELSE 0 END CASE WHEN p.category_name LIKE %机械键盘% THEN 2 ELSE 0 END CASE WHEN p.product_desc LIKE %机械键盘% THEN 0.5 ELSE 0 END) AS match_score FROM product p WHERE p.product_name LIKE %机械键盘% OR p.category_name LIKE %机械键盘% OR p.product_desc LIKE %机械键盘% ORDER BY match_score DESC, p.sales_count DESC LIMIT 20;这条 SQL 的思路是商品标题命中的权重最高品类命中次之描述命中最低。这样排序能保证推荐结果和用户标签的相关性。在多标签场景下动态拼接 SQL 时用foreach标签遍历 tagList每个标签生成一组OR条件。注意 LIKE 查询无法走索引商品表数据量大时需要引入 Elasticsearch 做检索层但作为毕业设计和中小型项目这张表的数据量在万条以下时上面的 SQL 性能完全够用。4.3 冷启动用户与推荐降级策略新注册用户没有任何行为数据TF-IDF 算不出画像这时候推荐接口直接返回空列表显然不合适。项目中的处理方式是为冷启动用户准备一个「默认推荐池」按商品销量和浏览量排序从热门商品中取 10 条填充推荐位。代码判断逻辑如下public ListProductVO recommendForUser(Integer userId) { String profile profileCacheService.getUserProfile(userId); if (profile null || profile.isEmpty()) { // 冷启动返回热门商品列表 return productMapper.selectHotProducts(10); } // 正常推荐逻辑解析 profile 中的 tags生成查询条件 ListString tags parseTagsFromProfile(profile); return productMapper.selectByTags(tags, 10); }另外一个容易忽略的场景是用户画像存在但所有标签词在商品表中一个都匹配不到。这种情况发生在用户关注的是小众商品且商品库更新不及时的时候。做法是加一层兜底如果查询结果少于 3 条就用销量最高的热门商品补齐到 10 条。这个规则写在 Service 层而不是 SQL 层因为 SQL 层做不了这种动态数量判断。5. 部署验证与 TF-IDF 推荐效果的可观测性检查部署这套系统时最容易出问题的不是业务代码而是 Java 与 Python 运行环境的衔接。Python 端依赖pymysql、jieba如果直接跑在系统 Python 环境里容易碰到版本冲突。稳妥的做法是在项目根目录建一个独立的虚拟环境cd /opt/recommend python3 -m venv venv source venv/bin/activate pip install pymysql jieba scikit-learnJava 侧在执行 Python 脚本时把命令从python3改成/opt/recommend/venv/bin/python3这样不会受系统默认 Python 环境的影响。数据库连接参数建议通过环境变量传入 Python 脚本而不是硬编码在.py文件里避免账号密码泄露到代码仓库。验证推荐效果时不能只看接口返回有没有数据还要关注标签覆盖率。一个简单有效的检查方式是在 MySQL 中执行下面的查询看大部分用户是否都有画像记录SELECT COUNT(*) AS total_users, SUM(CASE WHEN up.id IS NOT NULL THEN 1 ELSE 0 END) AS profiled_users FROM user u LEFT JOIN user_profile up ON u.id up.user_id;如果画像覆盖率低于 60%说明行为数据采集有问题优先排查用户浏览商品时是否成功写了user_behavior表。针对单个用户可以直接查看他的画像 JSON 是否合理比如一个只买数码产品的用户画像里不应该出现「婴儿纸尿裤」这类标签。最后再回到 TF-IDF 本身。这个项目里的相似度计算其实只用了线性加权没有用到 TF-IDF 向量之间的余弦相似度。如果你想进一步做「相似用户推荐」可以在 Python 端用sklearn的TfidfVectorizer直接生成用户向量矩阵再计算用户与用户的余弦距离这样就能把推荐链路从「标签-商品」拓展到「用户-用户」这算是在原项目基础上成本最低的一个进阶玩法。本文还有配套的精品资源点击获取