简介:基于SpringBoot框架与Java语言开发的一款个性化服装搭配推荐小程序毕业论文,主要面向电子商务、软件工程等专业的毕业设计选题,也适用于对个性化推荐系统在微信小程序端落地实践感兴趣的开发者。论文围绕传统电商推荐系统在服装搭配场景下难以满足用户个性化需求的问题,提出基于用户偏好、商品信息、风格匹配及反馈优化的推荐方案,并通过协同过滤算法生成搭配建议;系统功能覆盖用户微信端与管理端,包含用户管理、商品分类、风格标签、搭配反馈及订单联动等模块,完整呈现了从需求分析、系统设计、代码实现到功能测试的毕业设计流程,并对未来结合AR虚拟试穿技术增强用户体验进行了展望。资源包为单个docx文档,大小2.25MB,文档含中英文摘要、目录、正文及参考文献等完整毕业论文结构,可直接作为论文撰写参照或项目开发蓝本。已有71人学习,适合需要系统梳理个性化推荐落地思路并快速搭建同类项目的毕业设计学生与开发者学习参考。
1. 个性化服装搭配推荐小程序:一份能直接跑通的毕设论文工程
Spring Boot做后端、微信小程序做端侧的个性化服装搭配推荐系统,是这几年毕业设计里存在感很高的一类题目。这份论文工程最值得复现的地方,是把“个性化推荐”这种听着很虚的功能,落成了能跑的后端接口和能点的小程序页面:数据库SQL、推荐接口、wx.login登录、首页加载更多,整条链路是通的,不是只有文档没有代码的半成品。它适合两类人:一类是正在做同题或相近题目毕设、需要一套前后端工程作参照的;另一类是手里有现成小程序项目、想快速给用户加一个“猜你喜欢”入口的。先把这套工程的骨架拆明白,再往自己的项目里搬,比从零拼接口省事得多。
2. Spring Boot后端:四张数据表与推荐接口的落地写法
后端部分的核心不在Controller写了多少行,而在数据模型怎么设计。推荐类项目最忌讳一上来就堆算法,用户行为表都没建好,算出来的权重全是空中楼阁。这套工程用了最经典的Spring Boot分层结构,配合MyBatis-Plus操作数据库,数据量在毕设这个级别完全够用,而且代码量比纯MyBatis少一大截。
2.1 分层结构:为什么毕设建议用这套包骨架
工程按controller、service、mapper、entity四层组织,外加config和common两个辅助包。我刚接手时第一反应是“怎么这么规整”,看完代码才发现这种规整是故意的:推荐逻辑放service,接口参数校验放controller,SQL操作全在mapper层,后面要改推荐策略,只动service一个文件就行。
com.example.fashion ├── controller # 接收前端请求,做参数校验 │ ├── AuthController.java │ ├── OutfitController.java │ └── UploadController.java ├── service # 业务逻辑,包括推荐策略 │ ├── AuthService.java │ ├── OutfitService.java │ └── RecommendService.java ├── mapper # MyBatis-Plus 数据访问层 │ ├── UserMapper.java │ ├── ClothingMapper.java │ ├── OutfitMapper.java │ └── UserBehaviorMapper.java ├── entity # 数据库表对应的实体类 │ ├── User.java │ ├── Clothing.java │ ├── Outfit.java │ └── UserBehavior.java ├── config # 分页插件、跨域等配置 │ └── MybatisPlusConfig.java └── common # 统一返回结果、异常处理 ├── Result.java └── GlobalExceptionHandler.java各层职责很清晰:Controller只做参数接收和结果包装,Service写业务判断,Mapper层用MyBatis-Plus的BaseMapper接口,单表查询基本不用写SQL。common包里那个Result类建议保留,前后端联调时统一返回结构能省掉大量“为什么这里多了一个data字段”的扯皮。
MybatisPlusConfig里要注册分页插件,否则后面推荐接口的分页参数不生效。常见做法是直接用PaginationInnerInterceptor,配置数据库类型为MySQL,这个不写的话,page参数传了也白传。
2.2 核心数据表:把“搭配推荐”拆成四张表
搭配推荐的核心是“用户-服装-行为”三者之间的关系。这套工程用了四张表来承载:用户表、服装单品表、搭配主表、用户行为表。搭配和服装之间还有一张关联表,因为一套搭配里有多件衣服,一件衣服也可能出现在多套搭配里,多对多关系必须用中间表拆开。
-- 用户表 CREATE TABLE `tb_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid', `nickname` varchar(50) DEFAULT '' COMMENT '昵称', `avatar` varchar(255) DEFAULT '' COMMENT '头像地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 服装单品表 CREATE TABLE `tb_clothing` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '服装名称', `category` varchar(20) NOT NULL COMMENT '分类: top/bottom/shoes/accessory', `style_tag` varchar(100) DEFAULT '' COMMENT '风格标签, 逗号分隔: 休闲,通勤,运动', `color` varchar(30) DEFAULT '' COMMENT '主色调: 深蓝/米白/军绿', `image_url` varchar(255) NOT NULL COMMENT '图片地址', `like_count` int(11) DEFAULT 0 COMMENT '点赞数, 用于冷启动兜底', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 搭配推荐表 CREATE TABLE `tb_outfit` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '搭配标题', `style_tag` varchar(100) DEFAULT '' COMMENT '搭配风格: 通勤/约会/运动', `cover_url` varchar(255) NOT NULL COMMENT '搭配封面图', `like_count` int(11) DEFAULT 0 COMMENT '点赞数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 搭配与单品的关联表 CREATE TABLE `tb_outfit_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `outfit_id` bigint(20) NOT NULL, `clothing_id` bigint(20) NOT NULL, `sort_order` int(11) DEFAULT 0 COMMENT '展示顺序', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户行为表 CREATE TABLE `tb_user_behavior` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `target_type` varchar(20) NOT NULL COMMENT '行为对象: outfit/clothing', `target_id` bigint(20) NOT NULL, `behavior_type` varchar(20) NOT NULL COMMENT '行为类型: view/collect/purchase', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;表设计里有两个细节值得注意。一是所有表都用utf8mb4而不是utf8,因为微信昵称里经常出现emoji表情,utf8存不了四个字节的字符,真踩过这个坑的人都知道有多痛。二是openid加了唯一索引,同一个微信号不会注册出两条用户记录,省得每次登录都要查重。
用户行为表是推荐算法的输入源。behavior_type区分了view(浏览)、collect(收藏)、purchase(购买)三类行为,这三类行为在推荐时权重不一样,购买权重最高,收藏次之,浏览最低。可以在Service里定义一个行为权重映射表,比如view=1、collect=3、purchase=5,聚合用户风格偏好时按权重累加。
2.3 推荐接口:在Spring Boot里写出“根据用户偏好推搭配”
推荐接口的入口很朴素,一个GET请求带上userId和分页参数。核心逻辑在RecommendService里,先查用户行为表聚合出偏好,再按偏好风格召回搭配。新用户没有行为数据时走的是另一条路,这个放到第5章单独讲。
@RestController @RequestMapping("/api/outfit") public class OutfitController { @Autowired private RecommendService recommendService; /** * 推荐搭配列表 * @param userId 登录用户ID, 新用户传0 * @param page 页码, 从1开始 * @param size 每页条数, 默认10 */ @GetMapping("/recommend") public Result recommend(@RequestParam Long userId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { List<OutfitVO> list = recommendService.recommendWithColdStart(userId, page, size); return Result.ok(list); } }Controller层不写业务判断,只做参数透传。@RequestParam里的defaultValue很关键,小程序端第一次加载首页时可能没传page,默认值1保证接口不报错。
@Service public class RecommendService { @Autowired private UserBehaviorMapper userBehaviorMapper; @Autowired private OutfitMapper outfitMapper; public List<OutfitVO> recommendWithColdStart(Long userId, int page, int size) { // 1. 取用户的行为风格偏好 List<String> styleTags = userBehaviorMapper.selectUserPreferredStyle(userId); // 2. 没有行为数据时走热门兜底 if (styleTags == null || styleTags.isEmpty()) { return outfitMapper.selectHotOutfits(page, size); } // 3. 有偏好则按风格标签召回 return outfitMapper.selectByStyleTags(styleTags, page, size); } }selectUserPreferredStyle在Mapper里是一段SQL:从行为表关联搭配表,统计用户点赞和收藏的搭配里出现频率最高的风格标签。比如一个用户收藏过的搭配里“通勤”出现5次、“休闲”出现3次,那这个用户就是典型的通勤风格偏好。这一步用SQL聚合一次搞定,没必要在Java里循环统计,数据库做聚合比应用层做快一个数量级。
2.4 配置项:application.yml中容易被忽略的四个参数
工程能跑起来,一半功劳在配置文件。我见过太多项目代码没问题、配置文件翻车的case,下面这四个参数是这套工程里必须确认的。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/fashion_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0第一个是时区参数serverTimezone=Asia/Shanghai,不写的话MySQL 8.x驱动会报时区错误,或者在插入时间字段时差8小时,小程序端显示的时间永远对不上。第二个是上传大小限制,Spring Boot默认max-file-size只有1MB,服装搭配的图片随便一张都是2MB起步,不调大的话上传必然失败。第三个是map-underscore-to-camel-case: true,数据库字段是create_time,实体类是createTime,这个配置开启后自动映射,省掉一堆@TableField注解。第四个是逻辑删除配置,搭配表里的数据不建议物理删除,用户收藏过的搭配如果被物理删了,行为表里就留下了悬空引用。
3. 小程序前端:从登录态到“加载更多”的完整链路
后端接口写得再完整,小程序端对接不上也是白搭。这套工程的小程序端核心有三块:网络请求封装、微信登录、首页推荐流的加载更多。把这三块跑通,剩下的页面基本就是套模板。
3.1 网络层封装:统一处理token和错误码
小程序里不能像Web端那样用axios,所有请求都走wx.request。如果每个页面都直接调wx.request,token拼接、错误处理、loading状态会散落得到处都是。这套工程在utils目录下做了统一封装,所有请求都走这一个方法。
// utils/request.js const BASE_URL = 'http://192.168.1.100:8080/api'; function request({ url, method = 'GET', data = {} }) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 200 && res.data.code === 200) { resolve(res.data.data); } else if (res.statusCode === 401) { // token过期, 清掉重新走登录流程 wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail: (err) => { wx.showToast({ title: '网络错误,请检查后端是否启动', icon: 'none' }); reject(err); } }); }); } module.exports = { request };BASE_URL这里有个必须注意的细节:不能在真机预览时写localhost。localhost在开发者工具里指向你的电脑,但在手机上指向手机本身,后端服务根本不在手机上。常见做法是开发时用电脑的局域网IP,比如192.168.x.x,真机预览时保证手机和电脑连同一个WiFi。请求封装里的401处理也值得留一下,token过期后不弹一堆乱七八糟的报错,而是静默清token并跳转登录页。
3.2 wx.login到自定义登录:code换token的完整链路
微信小程序的登录不是直接用账号密码,而是走wx.login拿一个临时code,后端用这个code去微信服务器换openid。openid是用户在微信体系里的唯一标识,但openid不能直接当token用,因为任何人都能拿openid伪造身份,正确做法是后端自己生成一个token返回给前端。
// pages/login/login.js login() { wx.login({ success: async (res) => { const code = res.code; const resp = await request({ url: '/auth/login', method: 'POST', data: { code } }); wx.setStorageSync('token', resp.token); wx.setStorageSync('userId', resp.userId); wx.switchTab({ url: '/pages/index/index' }); } }); }// AuthController.java 核心逻辑 @PostMapping("/login") public Result login(@RequestBody LoginReq req) { // 1. 用code换openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + req.getCode() + "&grant_type=authorization_code"; String result = restTemplate.getForObject(url, String.class); JSONObject json = JSONObject.parseObject(result); String openid = json.getString("openid"); // 2. openid查库, 不存在就注册新用户 User user = userMapper.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); userMapper.insert(user); } // 3. 生成自己的token返回给前端 String token = UUID.randomUUID().toString().replace("-", ""); return Result.ok(new LoginResp(token, user.getId())); }微信服务器返回的json里有一个session_key字段,注意这个字段不要返回给前端,它是用来解密用户手机号等敏感信息的,泄露出去有安全风险。后端自己生成token后,后续请求带上token,后端就可以在拦截器里解析出用户身份,不需要每次都调微信接口。
3.3 首页推荐流:列表渲染与触底加载更多
首页推荐流用的是scroll-view组件的bindscrolltolower事件,滚动到底部自动加载下一页。这里有个常见的翻车点:直接把所有数据一次性渲染出来,数据量大了页面卡成PPT。正确的做法是分页加载,每页10条,滚动到底部再拉下一页。
<!-- pages/index/index.wxml --> <scroll-view class="list" scroll-y bindscrolltolower="loadMore"> <view class="card" wx:for="{{outfits}}" wx:key="id"> <image src="{{item.coverUrl}}" mode="aspectFill"></image> <view class="card-info"> <text class="title">{{item.title}}</text> <text class="tag">{{item.styleTag}}</text> </view> </view> <view wx:if="{{loading}}" class="loading">加载中...</view> <view wx:if="{{!hasMore && outfits.length > 0}}" class="no-more">没有更多了</view> </scroll-view>// pages/index/index.js data: { outfits: [], page: 1, size: 10, hasMore: true, loading: false }, loadMore() { if (!this.data.hasMore || this.data.loading) return; this.setData({ loading: true }); request({ url: '/outfit/recommend', data: { userId: wx.getStorageSync('userId'), page: this.data.page, size: this.data.size } }).then((list) => { this.setData({ outfits: this.data.outfits.concat(list), page: this.data.page + 1, hasMore: list.length === this.data.size, loading: false }); }); }加载更多的判断逻辑是hasMore: list.length === this.data.size,意思是这次返回的数量等于请求的数量,那很可能还有下一页。如果返回不足10条,说明数据库里的数据已经拉完了,hasMore置为false,底部显示“没有更多了”,同时阻止后续的加载请求。loading标志位是防止用户快速滚动时连续触发多次请求的,这个一定要加,不然会出现数据重复。
3.4 图片加载不出来:域名白名单与抓包排查
小程序里图片加载不出来,一半以上的原因不是代码问题,是域名配置问题。开发模式下,在开发者工具右上角“详情-本地设置”里勾选“不校验合法域名”,图片和请求都能正常发出去。但真机预览和正式上线时,微信强制要求所有请求域名和图片域名都必须在小程序后台配置白名单,而且request合法域名和downloadFile合法域名是分开配置的,图片用image组件加载走的是downloadFile那条,漏配一个就加载不出来。
排查这类问题时我一般会开抓包工具看请求到底发出去没有。用Charles抓一下小程序的HTTPS请求,如果看到图片请求返回了403或域名错误,基本就是白名单漏配;如果请求压根没发出,那是业务逻辑的问题。小程序端的网络请求在开发者工具的Network面板里也能看到,但真机上出了问题就只能靠抓包,Charles抓小程序的流程是:手机设置代理指向电脑的Charles监听端口,装好Charles证书后就能看到小程序发出的所有HTTPS请求,包括图片、接口、上传文件。
4. 踩坑与排查:五个最容易翻车的位置
这套工程我从导入到跑通,前后折腾了一天半。一半的时间花在代码逻辑上,另一半全在填环境配置的坑。下面五个问题是我实际遇到过、而且反复出现的,按概率排序,先排查这些能省下大量时间。
4.1 小程序请求直接失败:本地后端连不上
现象:wx.request走fail回调,提示信息是request:fail,后端控制台一条日志都没有。
原因:两种情况最常见。一是开发者工具没勾选“不校验合法域名”,小程序默认拒绝向非HTTPS域名发请求;二是BASE_URL写成了localhost,真机预览时localhost指向的是手机自己。
解决:开发工具里勾选“不校验合法域名”,BASE_URL改成电脑的局域网IP,手机和电脑连同一个WiFi。如果用的是云开发或线上服务器,确认域名是HTTPS且在后台配了白名单。后端启动后先自己在浏览器里访问一下接口,确认服务真的起来了再做前端排查。
4.2 登录接口报错:code2session返回40029
现象:后端调用微信接口返回{"errcode":40029,"errmsg":"invalid code"}。
原因:微信的code是一次性的,用一次就失效。前端每调一次wx.login就会刷新code,如果旧code还在请求队列里没发出去,或者前端代码里一次性用了两次,第二个请求拿到的code就废了。另外AppID和AppSecret不匹配也会报这个错,把测试号的AppID配到了正式的secret上。
解决:前端确保每次登录流程只调一次wx.login,拿到code立刻传给后端;后端在日志里打AppID和secret的脱敏值,核对是不是同一套小程序的。这个错是我排查最久的,最后发现是复制配置文件时把secret多复制了一位。
4.3 Spring Boot版本太高,JDK版本对不上
现象:用IDEA启动项目直接报UnsupportedClassVersionError,或者Maven编译时报错无法解析Spring Boot依赖。
原因:Spring Boot 3.x要求JDK 17以上,而很多毕设模板和电脑上的默认JDK还是8,版本对不上直接启动不了。反过来说,如果用了JDK 17却引用了Spring Boot 2.x的某些旧依赖,也会因为兼容性问题报错。
解决:先跑java -version确认当前JDK版本,再看pom.xml里Spring Boot的parent版本。Spring Boot 3.x就配JDK 17,Spring Boot 2.7.x就配JDK 8,不要混。IDEA里可以在Project Structure里给这个Module单独指定JDK版本,不用全局改。
4.4 上传服装图片老是失败
现象:上传进度条走到一半就断,后端日志报MaxUploadSizeExceededException或413 Request Entity Too Large。
原因:Spring Boot内置的Tomcat默认单文件上传上限是1MB,服装图片随便一张手机拍的都是2MB以上,必超。这个默认值经常被人忽略,因为本地小图测试没问题,一旦上真机拍的照片就翻车。
解决:在application.yml里把max-file-size改成10MB,max-request-size改成20MB,改完必须重启应用。如果用的是Nginx反向代理,还要检查Nginx的client_max_body_size是不是也限制了。
4.5 真机canvas生成的搭配图模糊
现象:开发者工具里canvas生成的分享图很清晰,一到iPhone上就模糊,文字边缘发虚。
原因:canvas在真机上按物理像素渲染,而开发者工具里按CSS像素渲染。设计稿宽度375px的图,在iPhone的2倍屏上只有187.5个物理像素,不糊才怪。
解决:绘制前用wx.getSystemInfoSync()拿设备pixelRatio,把canvas的宽高乘以pixelRatio,再通过scale放大绘制。代码上就是在canvas的初始化阶段多写三行:const dpr = wx.getSystemInfoSync().pixelRatio; canvas.width = width * dpr; ctx.scale(dpr, dpr);。这个坑属于不做真机测试绝对发现不了的,血泪经验。
5. 进阶:先别急着上算法,把冷启动兜住
推荐接口能返回数据了,但还有一个隐藏问题:新用户第一次打开小程序,行为表是空的,没有浏览记录、没有收藏、没有购买,按用户偏好去查搭配,查出来的列表是空的,首页直接白屏。这是推荐系统里的冷启动问题,也是这套工程最值得改的一个点。最
本文还有配套的精品资源,点击获取