
说来有点不好意思我当年选题的时候第一反应也是“图书管理系统”或者“电商购物系统”结果被老师一句“太普通了”直接打回。后来换成了这个“计算机毕设 java 服装搭配推荐系统”采用了 SSM 框架做底层围绕搭配推荐场景构建了一个完整的服务平台。整个过程做下来我最大的感受是这个题目的度把握得非常好——它有推荐算法作为亮点又不至于用到深度学习那种难度同时 SSM 框架带来的代码量和业务完整度也足够撑起一篇像样的毕业设计论文。这篇文章我不打算写成说明书而是把我从选题、建模、写代码到答辩准备的完整过程和踩过的坑都摊开来说清楚尤其会把推荐算法怎么在 SSM 环境里落地这件事讲透。1. 项目到底在做什么需求拆解与选题价值先把这个项目的核心逻辑讲明白。它不是一个普通的服装商城而是一个“围绕搭配行为做推荐”的闭环系统。业务上分两条主线一条是用户的日常使用路径——注册登录、浏览服装、查看搭配方案、创建搭配、收藏评论另一条是管理端的维护路径——管理员对服装商品、搭配方案、用户状态进行管理。推荐模块不是孤立的它会根据用户收藏、评分、浏览这些行为数据去生成个性化的服装推荐和搭配方案推荐。1.1 核心需求拆解我把需求拆成了四个模块来理解这也是后面设计数据库和代码分层的基础用户模块注册、登录、个人信息维护区分普通用户和管理员两种角色。服装管理模块服装商品的分类、上下架、库存、图片上传与维护。搭配模块搭配方案的创建与展示。用户创建时可以选多件衣服组成一套方案写标题和描述提交后由管理员审核。推荐模块根据用户行为数据从服装和搭配方案两个维度做个性化推荐。这个拆法能保证答辩时思路清晰。评委问你“系统有哪些功能”你按模块讲每个模块都有明确的实体和数据支撑评委再往下问“你这个推荐是怎么实现的”你就可以从推荐模块单独展开把算法逻辑讲透。1.2 为什么说这个选题适合毕设我自己的体会是毕业设计最怕的就是选题两极化。太简单的没有技术含量太复杂的做不完。服装搭配推荐系统的优势在于它的“中间态”——推荐算法部分有独立思考的空间但不需要庞大的数据集和计算资源业务管理部分又足够完整能够体现你对 SSM 集成、数据库设计、权限控制的掌握程度。另外很重要的一点是演示效果好。系统跑起来之后用户可以收藏衣服、创建搭配方案、给方案打分推荐结果会因为这些操作实时变化。这种“可感知”的交互效果在答辩现场非常加分比单纯展示 CRUD 页面有说服力得多。提示选题时一定要考虑数据来源。服装图片我建议用公开数据集或者电商网站爬取的商品图注意数量不用太多每个分类准备 20-30 张就足够跑推荐效果了。2. 技术选型SSM 框架为什么依然能打2.1 为什么不用 Spring Boot这个问题我几乎每场答辩模拟都会被问。我的回答是SSM 是手动集成框架你能真正理解 Spring 的 IoC 容器、AOP 事务、MyBatis 的映射原理而 Spring Boot 把这些东西自动化了很多同学做完项目都不清楚请求是怎么一步步走到数据库的。用 SSM 反而逼着你把每一条配置都写明白这在答辩讲“框架原理”的时候是巨大的优势。当然如果学校要求必须用 Spring Boot其实业务代码基本不用改把 SSM 换成 Spring Boot MyBatis 是很容易的核心的算法代码和数据库设计完全兼容。2.2 项目结构与依赖清单我推荐的分包结构是这样清晰且不容易乱com.example.collocation ├── controller // Controller 层 ├── service // Service 接口 │ └── impl // Service 实现 ├── mapper // MyBatis Mapper 接口 ├── entity // 实体类 ├── common // 通用返回结果、常量、工具类 └── interceptor // 登录拦截器、管理员拦截器Maven 依赖方面版本一定要用稳定的不要追新。我项目里最终确定的是这套组合dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.9/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.11/version /dependency这里要特别提醒spring-core、spring-context、spring-webmvc 这些组件的版本必须保持一致否则容易出现 NoSuchMethodError 之类的运行时异常排查起来非常费时间。2.3 Spring 和 SpringMVC 配置各管什么SSM 的配置最容易把人绕晕。我建议按职责来记applicationContext.xml 管非 Web 层的配置组件扫描排除 Controller、Druid 数据源、SqlSessionFactoryBean、MapperScannerConfigurer、事务管理器。springmvc.xml 管 Web 层注解驱动、视图解析器、静态资源映射、CommonsMultipartResolver 文件上传解析器、拦截器注册。web.xml 里只需要配置 DispatcherServlet 和 ContextLoaderListener再加一个 CharacterEncodingFilter 解决编码问题。数据库连接池我用的是 DruidURL 必须带上编码参数jdbc.urljdbc:mysql://localhost:3306/collocation?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai连接池的 initialSize 设 5maxActive 设 20这个量级对毕设场景完全够用。3. 数据库设计把“搭配”拆成一张张表数据库设计是整个项目的地基。很多同学一上来就建表结果做到推荐模块发现缺字段回头改表改代码苦不堪言。我花了两天时间把表结构反复推演了一遍最终确定了七张核心表。3.1 核心表结构说明用户表 user字段类型说明idint主键自增usernamevarchar(50)用户名唯一passwordvarchar(64)密码MD5 加密存储nicknamevarchar(50)昵称gendertinyint0 未知 1 男 2 女height / weightdecimal身高体重用于后期扩展体型匹配body_typevarchar(20)体型描述avatarvarchar(255)头像路径roletinyint0 普通用户 1 管理员create_timedatetime注册时间服装表 clothing字段类型说明idint主键namevarchar(100)服装名称category_idint关联分类colorvarchar(30)颜色stylevarchar(50)风格休闲/商务/运动/甜美等seasonvarchar(20)季节春/夏/秋/冬/四季occasionvarchar(50)场合通勤/约会/聚会等pricedecimal(10,2)价格imagevarchar(255)图片路径stockint库存statustinyint0 下架 1 上架descriptiontext描述搭配方案表 collocation字段类型说明idint主键user_idint创建者titlevarchar(100)搭配标题descriptiontext描述occasionvarchar(50)场合stylevarchar(50)风格seasonvarchar(20)季节cover_imagevarchar(255)封面图statustinyint0 待审核 1 通过 2 驳回view_countint浏览量like_countint点赞数create_timedatetime创建时间搭配项表 collocation_item 用来连接搭配方案和具体服装字段类型说明idint主键collocation_idint搭配方案 IDclothing_idint服装 IDpositionvarchar(20)上装/下装/外套/鞋子/配饰收藏表 favorite 是一个通用表既能收藏服装也能收藏搭配方案字段类型说明idint主键user_idint用户favorite_typevarchar(20)clothing 或 collocationfavorite_idint对应 IDcreate_timedatetime收藏时间评论评分表 comment字段类型说明idint主键user_idint评论者target_typevarchar(20)clothing 或 collocationtarget_idint目标 IDcontentvarchar(255)评论内容scoretinyint评分 1-5create_timedatetime评论时间3.2 建表时的小心思有几个细节是我做了之后才觉得真香的第一收藏表设计成通用表而不是分成服装收藏表和搭配收藏表两张表。这样推荐模块在统计用户行为的时候一次查询就能拿到全部收藏数据不用做表关联。第二collocation_item 单独成表是为了支持“一个搭配包含多件衣服”这种一对多关系这也是“搭配”和“商品”本质区别的地方。第三所有表都加了 create_time 字段方便推荐时按时间维度给行为加权这个下面讲算法时会提到。4. 推荐算法落地从朴素算法到可用效果4.1 基于标签的内容推荐内容推荐的思路很直接给每件服装打上标签集合包括风格、季节、场合、颜色、分类名再统计用户历史收藏过的衣服的标签权重算相似度推荐最像的。具体实现是分三步的第一步定义服装标签集合。我在 ClothingService 里写了一个方法把一件衣服的标签拼成一个归一化的字符串public String buildTag(Clothing c) { StringBuilder sb new StringBuilder(); sb.append(c.getStyle()).append(,); sb.append(c.getSeason()).append(,); sb.append(c.getOccasion()).append(,); sb.append(c.getColor()).append(,); if (c.getCategory() ! null) { sb.append(c.getCategory().getName()); } return sb.toString(); }第二步统计用户偏好向量。从 favorite 表里查出用户收藏过的所有服装累加每个标签的出现次数得到类似 “休闲:3, 夏季:2, 通勤:2, 蓝色:1” 这样的权重表。这里可以稍微优化一下收藏时间在最近 7 天内的权重乘以 1.5时间久的乘以 0.8让推荐更贴近当前喜好。第三步计算相似度并排序。对每件用户没收藏过的服装用余弦相似度计算与偏好向量的匹配程度public double cosineScore(MapString, Integer userTags, MapString, Integer clothTags) { double dot 0; for (String tag : clothTags.keySet()) { if (userTags.containsKey(tag)) { dot userTags.get(tag) * clothTags.get(tag); } } double userNorm 0, clothNorm 0; for (Integer v : userTags.values()) { userNorm v * v; } for (Integer v : clothTags.values()) { clothNorm v * v; } return dot / (Math.sqrt(userNorm) * Math.sqrt(clothNorm)); }最后按分数降序取前 10 件返回。这个算法虽然朴素但效果很真实用户收藏了一件“休闲、夏季、蓝色”的 T 恤系统推荐的就会是同样风格和季节的衣服。答辩时你可以现场演示——收藏一条运动裤刷新推荐列表跑出来全是运动风格的商品。4.2 基于用户的协同过滤协同过滤负责推荐“和你品位相似的人喜欢的东西”。它强调的是用户之间的行为相似性而不是内容本身的属性。实现的逻辑分三步用户行为映射把用户收藏过的搭配方案集合作为该用户的“兴趣集合”。计算用户相似度使用 Jaccard 相似度统计两个用户收藏集合的交集大小除以并集大小。生成推荐找到最相似的 Top5 用户把他们收藏过而当前用户没收藏过的搭配方案按收藏人数排序取前 6 个。Jaccard 相似度代码很简短public double jaccard(SetInteger a, SetInteger b) { if (a.isEmpty() b.isEmpty()) return 0; SetInteger union new HashSet(a); union.addAll(b); SetInteger inter new HashSet(a); inter.retainAll(b); return (double) inter.size() / union.size(); }协同过滤在毕设场景下不需要完整跑一遍全量计算。一种取巧但合理的做法是用户登录后立刻查一次当前用户与其他所有用户的相似度存到内存缓存里只有在库里的用户数超过 100 时才考虑用离线任务预计算。真正答辩时有个三五十个测试账号就足够演示相似推荐了。4.3 冷启动与混合推荐策略冷启动问题是推荐系统绕不开的话题。新注册用户没有任何收藏和评分行为协同过滤直接失效。我的处理方式是注册时引导用户选择喜欢的风格标签比如“休闲、运动、甜美”最多选三个。新用户没有行为数据时直接用注册选的风格匹配内容推荐。如果连风格都没选就回退到全局热度推荐按浏览量、收藏数、评分综合排序SELECT * FROM collocation WHERE status 1 ORDER BY view_count * 0.4 like_count * 0.6 DESC LIMIT 10最后给用户展示的推荐结果不是单一算法跑出来的而是混合策略优先用基于用户的协同过滤推荐搭配方案如果有结果的话再用基于标签的内容推荐补足服装推荐两个列表都不满 10 条时用热度推荐填空。这样既保证了推荐的个性化程度又不会出现首页空荡荡的情况。5. 核心功能实操与避坑记录5.1 登录拦截与权限校验权限控制我用的是 SpringMVC 拦截器没有引入 Shiro 或 Spring Security因为毕设场景下自己写拦截器更能体现思路。设计上分两层LoginInterceptor拦截 /user/** 和 /admin/** 路径检查 session 里是否存在登录用户。AdminInterceptor拦截 /admin/** 路径额外检查角色是否为管理员。配置在 springmvc.xmlmvc:interceptors mvc:interceptor mvc:mapping path/user/**/ mvc:exclude-mapping path/user/login/ mvc:exclude-mapping path/user/register/ bean classcom.example.collocation.interceptor.LoginInterceptor/ /mvc:interceptor mvc:interceptor mvc:mapping path/admin/**/ bean classcom.example.collocation.interceptor.AdminInterceptor/ /mvc:interceptor /mvc:interceptors踩坑经验静态资源路径一定不要拦截。我一开始没把 /css/, /js/, /images/, /upload/排除结果登录页样式全没了排查了很久才发现是拦截器把静态资源的请求也给拦了。注册和登录接口也要放行不然永远进不了系统。5.2 图片上传的路径问题服装图片上传是毕设里最容易出问题的地方。我总结的三个关键点第一保存的位置不是项目目录里的某个文件夹而是本机磁盘的物理路径比如 D:/collocation/upload/。项目重启之后上传的文件不会丢失这是和保存到项目内部目录最大的区别。第二数据库里存的是相对访问路径比如 /upload/20250520/123.jpg而不是物理路径。这样换机器部署时只要保证上传目录存在图片就能正常访问。第三SpringMVC 的静态资源映射要做两段配置一段映射静态资源到指定物理目录mvc:resources mapping/upload/** locationfile:D:/collocation/upload//我建议用参数配置而不是硬编码。在配置文件中写file.upload.pathD:/collocation/upload/然后用 Value 注解注入到 controller 里这样部署到服务器时只需要改配置不用改代码。5.3 验证码的处理细节验证码我用的是 Kaptcha。集成方式很简单引入依赖后在 springmvc.xml 里配置一个 Producer 的 beanbean idcaptchaProducer classcom.google.code.kaptcha.impl.DefaultKaptcha property nameconfig bean classcom.google.code.kaptcha.util.Config constructor-arg props prop keykaptcha.textproducer.char.length4/prop prop keykaptcha.textproducer.font.size40/prop /props /constructor-arg /bean /property /bean然后在 controller 里生成图片并写入 sessionString text captchaProducer.createText(); request.getSession().setAttribute(captcha, text); BufferedImage image captchaProducer.createImage(text); ImageIO.write(image, jpg, response.getOutputStream());用户登录时先用一个参数接收用户输入再与 session 中的验证码进行忽略大小写的比较。这里要特别注意比较完成之后不管正确与否session 里的验证码都要作废否则下次还用同一个验证码等于没设防。这是一个能在答辩时讲出亮点的安全细节。5.4 搭配发布与审核流程搭配发布是“全流程”一词最有说服力的功能。用户的操作流程是进入搭配创建页面从服装列表里选衣服给每件衣服指定“上装”“下装”“外套”等角色再填标题和描述选择适用场合、风格、季节最后上传封面图。提交后方案状态变成“待审核”只有管理员审核通过后方案才在首页公开展示。前台展示时查询条件里必须带上 status 1SELECT * FROM collocation WHERE status 1 AND (occasion #{occasion} OR #{occasion} IS NULL) ORDER BY create_time DESC审核拒绝时管理员要填驳回原因用户端可以从“我的搭配”里看到被驳回的消息。这个细节让系统的完整度上了一个档次也避免了“提交后石沉大海”的体验问题。6. 常见问题排查实录6.1 中文乱码乱码是 SSM 项目里出现频率最高的问题通常有三个层次数据库连接 URL 没加 characterEncodingutf8或者 MySQL 表本身的字符集不是 utf8mb4。JSP 页面头部没设置 pageEncoding 和 contentType。web.xml 里没有配置 CharacterEncodingFilter。处理方案是我在项目一开始就固定下来的数据库连接 URL 统一带 utf8建库语句写 DEFAULT CHARSETutf8mb4web.xml 配置 CharacterEncodingFilter 并设置 forceEncodingtrue。做完这三步乱码问题基本不会出现。6.2 图片能存上但显示不出来这个问题我从一开始就遇上了。本地开发时图片显示正常部署到 Tomcat 后 404。原因就是物理路径变了。排查思路很简单先确认数据库里存的路径再到浏览器里直接访问完整 URL看请求打到哪个目录。这个方法能快速定位是否路径错误。如果是线上部署更稳妥的做法是把上传目录放到项目同级目录下比如 /opt/collocation/upload/不要在 /root 或用户主目录里避免权限问题。6.3 MyBatis 字段映射不生效我在做收藏表查询时发现查询结果里 user_id 一直是 null。找了半天才发现是数据库字段前缀下划线的问题。MyBatis 默认不会把数据库列名 user_id 自动映射到实体属性 userId。解决办法有两个在 resultMap 里显式配置 column 和 property 的对应关系或者在 MyBatis 全局配置里开启驼峰映射configuration settings setting namemapUnderscoreToCamelCase valuetrue/ /settings /configuration开启驼峰映射之后user_id 就会自动映射到 userId非常省事。但要注意如果实体属性本身也带下划线反而会映射不上。所有实体类属性全部使用驼峰命名是最稳妥的约定。6.4 Tomcat 启动内存不足刚开始启动项目时Tomcat 经常报 OutOfMemoryError。这通常是因为本地开发时同时开了 IDEA、MySQL、Tomcat内存不够用。解决方法是设置 Tomcat 启动参数-Xms512m -Xmx1024m -XX:MaxMetaspaceSize512m在 IDEA 的 VM options 里直接填上即可。如果项目里引入了多个开源组件JVM 参数调大后基本能解决问题。6.5 Maven 依赖冲突SSM 项目里最容易出现的是 spring-web 和 spring-webmvc 版本不一致运行时抛出 NoSuchMethodError 或 ClassNotFoundException。我的做法是打开 Dependency Hierarchy 视图看看有没有同一个依赖出现多个版本有的话在 pom 里显式声明统一版本号。宁可多写一行 version也不要默认依赖传递。7. 最后分享一点实操经验项目做完之后如果让我重新做一遍我会在三个方面做得不一样第一提前准备测试数据。我中途才意识到推荐算法的效果完全取决于数据量。两三个测试账号和几十条商品数据跑出来的推荐结果看不出任何规律。后来我写了脚本生成 50 个模拟用户每个用户随机收藏 10 到 20 件商品再手动创建 30 个搭配方案推荐效果立刻不一样了。建议你在开发初期就把这些数据准备好测试和调试效率翻倍。第二接口返回统一格式。早期我的 controller 有的返回 ModelAndView有的直接返回 String前端对接时很痛苦。后来我引入了一个统一的 AjaxResult 类包含 code、message、data 三个字段。所有 JSON 交互都走这个结构前端只用处理一种格式哪怕给小程序或者 App 端做接口复用也完全兼容。第三把所有配置从代码里抽出来。连接数据库的用户名密码、图片上传路径、文件大小限制这些不要硬编码在代码里。放在 properties 或者 yml 文件里统一管理。你以后部署到云服务器的时候会感谢自己当初这个决定。如果你正准备做类似的系统我的建议是先把数据库设计好再写代码。我刚做的时候急着写页面结果后面为了加“风格标签”字段改表结构改到头疼。先建表、先把字段定义清楚再动代码看起来慢实际是最快的路径。希望这篇记录能让你少走点弯路。