☰
基于Spring Boot的一周穿搭App实战:从选题到部署全流程
2026/10/6 3:20:38 网站建设 项目流程

又是一年课题季。每年这个时候都有学生朋友拿着类似"基于Spring Boot的XX系统的设计与实现"的课题来找我讨论,今年让我印象比较深的是这个"一周穿搭App"。它表面上只是个普通的CRUD项目,但仔细拆解之后会发现,从服装库管理到每日穿搭推荐,再到天气联动和一周视图,里面藏着不少值得深挖的设计点。这篇文章我就以这个课题为主线,把我从选题分析、技术选型、数据库建模,到最终联调部署的完整过程都写出来,包括那些不写进论文里、但实际动手必踩的坑,希望能给你一条可以直接参考复现的路线。

1. 这个"一周穿搭"课题,到底在做一个什么系统

1.1 从课题名称拆解核心痛点

"一周穿搭App"这个名字听起来很生活化,但它的业务内核其实很清晰:解决"每天早上不知道穿什么"这个高频痛点。用户把衣柜里的衣服录入系统,给每件衣服打上标签(类型、颜色、适用温度区间、风格),然后系统根据当天天气、用户历史穿搭记录和评分,给出今天的推荐搭配,同时支持用户按星期规划一周的穿搭,提前排布。

如果你在写开题报告或者需求分析,建议把业务痛点用一句话写明白:从"站在衣柜前纠结"变成"打开App直接抄答案"。这个一句话需求会成为你后面所有功能设计的锚点,遇到拿不准要不要做的功能,就回头对照这句话。

1.2 功能边界划定:做"有用"而非"大而全"

很多同学一上来就想着做社交、做电商、做AI试衣,我必须拦一下。课题设计的核心评价标准是有完整业务闭环,不是功能数量。我建议划定这样的功能边界:

模块必做功能选做/加分功能
用户管理注册、登录、JWT鉴权、个人信息维护头像上传、密码找回
服装库衣服增删改查、按分类/季节筛选、图片上传按颜色/标签多条件组合筛选
搭配管理每日穿搭记录(日期唯一)、评分、备注按星期批量预排一周穿搭
穿搭推荐基于天气温度和服装标签的匹配推荐基于历史评分的热门搭配排序
天气联动按城市获取当日天气和温度区间未来3天天气趋势展示
数据统计每周穿搭次数统计月度风格偏好柱状图

这个表格你要是能写进需求文档里,导师一眼就能看出你想清楚了。一定牢记:先把闭环跑通,再谈花活。没有评分统计的穿搭记录系统、没有天气联动的推荐逻辑,都只是换皮CRUD,答辩时很容易被问倒。

2. 技术选型逻辑:为什么锁死Spring Boot 2.7.x而不是3.x

2.1 版本选择是这个课题的第一个分水岭

关于Spring Boot版本,我看到有些同学用的是3.x,然后用起来发现各种不顺手。这里我给出一个非常实用的建议:做这类课题项目,直接锁定Spring Boot 2.7.x,不要上3.x。原因有三点。

第一是环境兼容性。Spring Boot 3.x最低要求JDK 17,而很多实验室电脑和答辩环境还停留在JDK 8。你千辛万苦在JDK 17上写完代码,部署到老师的机器上报"UnsupportedClassVersionError",这画面我见过太多次了。Spring Boot 2.7.x配合JDK 8非常稳定,也完全够用。

第二是生态兼容性。这是最关键的痛点。Spring Boot 3.x从javax.*迁移到了jakarta.*命名空间,意味着大量老牌第三方库不兼容。你查资料时搜到的大部分博客、CSDN帖子还是基于javax写的,照着抄代码直接编译报错。比如整合MyBatis-Plus、PageHelper这些常用组件,2.7.x生态下的稳定版本一搜一堆,3.x下还要去专门找适配版本,纯属给自己加戏。

第三是课题验收的稳妥性。课题项目追求的是逻辑清晰、能跑能演示,不是追求最新版本。导师问起来"为什么用2.7.18",你可以理直气壮地说"为了保证框架生态的稳定兼容性,降低项目风险",这本身就是加分项。

2.2 前后端架构:App端到底用什么方案

有同学纠结"App"怎么实现。这里要明确:课题里说的App,不一定非要用原生Android或Swift去写。实际上最稳妥的方案是H5移动端 + WebView壳,或者干脆做一个移动端样式的前端网页,在浏览器里用模拟器模式演示,也完全符合"App设计与实现"的课题要求。

我推荐两套路径,你根据自己的前端基础选:

  • 路径A(推荐给前端基础薄弱):纯Thymeleaf模板引擎,服务端渲染。好处是一个Spring Boot工程搞定所有,不用解决前后端分离的跨域问题,打包部署也简单。缺点是页面交互弱一点,但做管理后台和穿搭列表完全够用。
  • 路径B(推荐给有Vue基础):前后端分离,Vue 3 + Vite 构建移动端H5页面。你搜到的"vue打包放进springboot"就是这条路线的核心操作:Vue项目npm run build之后把dist目录拷到Spring Boot的src/main/resources/static下,这样前端页面和后端接口就部署在同一个服务里,既享受了前后端分离的开发体验,又避免了线上还要部署Nginx的麻烦。移动端适配用viewport+rem就能做出"看起来像个App"的效果。

我个人建议你走路径B。理由很简单:Vue做的页面美观度高,答辩演示效果远好于Thymeleaf的朴素页面,而且"基于Spring Boot + Vue"在课题描述里也更好看。

2.3 持久层选型:MyBatis-Plus依然是效率之王

持久层我不太建议用原生MyBatis,也不想让你去研究JPA的复杂映射。直接选MyBatis-Plus 3.5.x,原因是它的BaseMapper自带增删改查和分页方法,你写穿搭记录、服装库这种单表CRUD,几乎不用手写SQL,能把精力放在业务逻辑上。搭配MybatisPlusInterceptor配置分页插件,接口层配合Page对象,列表分页五分钟搞定。

你可能会问为什么不选Spring Data JPA。JPA在关联查询和动态条件拼接上反而更绕——比如按"温度区间+风格标签+颜色"组合筛选衣服,JPA需要写Specification,MyBatis-Plus用LambdaQueryWrapper几行代码就解决了。实际开发中,MyBatis-Plus的中文文档和示例代码丰富度也远高于JPA,出了问题好查。

3. 数据库建模的核心:日期唯一约束和标签辐射

3.1 核心表设计:不要一上来就设计七八张表

你打开Navicat之前先想清楚一件事:这个系统的核心关系是什么?无非是"用户拥有衣服,用户每天记录穿搭,衣服有标签"。围绕这个核心,我最终落地了五张表:

-- 用户表 CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL COMMENT 'BCrypt加密存储', `nickname` varchar(50) DEFAULT NULL, `city` varchar(50) DEFAULT NULL COMMENT '默认城市,用于天气查询', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 服装库表 CREATE TABLE `clothing_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `name` varchar(100) NOT NULL COMMENT '如:藏蓝色呢子大衣', `category` varchar(20) NOT NULL COMMENT '上装/下装/外套/鞋履', `color` varchar(20) DEFAULT NULL, `style_tag` varchar(100) DEFAULT NULL COMMENT '标签,如:通勤,休闲,运动', `min_temp` int(11) DEFAULT NULL COMMENT '适用最低温度', `max_temp` int(11) DEFAULT NULL COMMENT '适用最高温度', `image_url` varchar(200) DEFAULT NULL, `status` tinyint(4) DEFAULT '0' COMMENT '0闲置 1常用 2清洗中', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_category` (`user_id`,`category`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 每日穿搭记录表(核心表) CREATE TABLE `outfit_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `record_date` date NOT NULL COMMENT '哪一天穿了什么', `weather_info` varchar(50) DEFAULT NULL COMMENT '当时天气,冗余存储', `temperature` int(11) DEFAULT NULL COMMENT '当时温度,冗余存储', `rating` tinyint(4) DEFAULT NULL COMMENT '评分1-5,可为空', `comment` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_date` (`user_id`,`record_date`) COMMENT '一人一天只能有一条记录' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 穿搭明细表(记录一套穿搭里包含哪些衣服) CREATE TABLE `outfit_detail` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `outfit_id` bigint(20) NOT NULL, `clothing_item_id` bigint(20) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这套设计的精妙之处在于outfit_record和outfit_detail分离。为什么单独拆一张明细表?因为一套穿搭包含上装、下装、外套等多件衣服,如果都塞在outfit_record里,就要设计固定字段(top_id、bottom_id、coat_id),以后想加帽子、围巾怎么办?改表结构?明细表让你加任何单品都无需改表,这就是标准的"一对多"建模。

3.2 按周查看的查询路径:索引和日期边界

"一周穿搭"最核心的交互是日历视图:周一到周日,每天显示一套穿搭缩略。对应SQL其实非常简单,难在设计时就想到索引:

SELECT * FROM outfit_record WHERE user_id = #{userId} AND record_date BETWEEN #{startDate} AND #{endDate} ORDER BY record_date ASC;

配合uk_user_date这个唯一索引,这一条查询在数据量只有几百条的情况下性能完全够用。我特别提醒:record_date不要用字符串,要用date类型,这样MySQL才能走索引高效范围查询。好多同学喜欢图省事存'2026-03-09'字符串,一旦需要按月份统计就要写一堆DATE_FORMAT转换函数,性能直接退化。

3.3 不少初学者会忽略的点:状态字段和冗余字段

上面表里的status字段和weather_info字段,看着不起眼,实际暗含设计考量。status字段解决的是"这件衣服已经送去干洗了,不要推荐"的场景,没有这个字段,你的推荐算法会把所有衣服都拉出来,体验很差。weather_info和temperature在outfit_record里冗余存储,是为了让历史穿搭记录自解释——你回看"3月9日穿了什么"时,直接能看到当天天气,不用再反查天气服务。虽然违背了一点"规范化"原则,但换来了查询效率和快照语义,实际项目中这种冗余是被鼓励的。

4. 后端实现的重头戏:穿搭推荐与天气联动

4.1 天气接入的轻量方案和容错设计

一周穿搭App比较关键的一个接口是天气查询。不要自己解析什么气象数据,直接选一个天气API服务商(和风天气、高德天气都行)。我以高德天气为例,它只需要一个API Key,通过城市编码查询实时天气:

// 服务类核心方法 public WeatherInfo getWeather(String city) { // 1. 用城市名调用高德地理编码接口,拿到adcode城市编码 // 2. 用adcode调用天气接口,拿到实况数据 String url = "https://restapi.amap.com/v3/weather/weatherInfo?" + "key=" + weatherApiKey + "&city=" + URLEncoder.encode(adCode, "UTF-8") + "&extensions=base"; // 解析JSON,读取"temp"温度和"weather"天气现象 }

真正上线时你会遇到三个问题,我在本地测试也全部踩过:

第一是城市名模糊匹配。用户填"上海"没问题,填"上海市"地理编码也能识别,但填"shanghai"就挂了。所以入库时最好统一转成标准的城市名,或者直接用省+市两级联动下拉框,从源头避免用户乱填。

第二是接口限流。免费版天气API每分钟调用次数有限,你如果每次刷新首页都调一次天气,很快就触发限流。我的方案是加一层本地缓存:把天气查询结果放到一个HashMap里,key是城市,value是带时间戳的天气数据,5分钟内复用。配置类里加@ConfigurationProperties读取缓存时长,别写死。

第三是接口挂了怎么办。这是最重要的容错设计。天气服务偶尔超时或返回error,你的App不能跟着白屏。我的兜底逻辑是:try-catch捕获异常后,直接返回昨天或者前天的天气快照(从outfit_record里取最近的weather_info),如果没有历史数据就返回一个默认温度区间,让推荐逻辑照常运转。演示时如果现场网络抽风,你照样能展示完整的推荐流程,这个细节答辩时非常加分。

4.2 推荐算法:从一个朴素但完整的评分模型说起

很多课题里都有"智能推荐"这种字眼,但我被问过最多的问题是"算法太简单怎么办"。这里我负责任地告诉你:对于一周穿搭这种体量的系统,你不需要上机器学习,一个基于规则的评分排序模型就足够出彩。关键在于:把推荐逻辑拆成清晰的步骤,每一步都有据可依。

我的推荐流程分三步:

第一步,按温度过滤。拿到当天温度后,从衣服表里查出min_temp <= 当前温度 <= max_temp的衣服,这是硬性筛选。没有合适合适温度区间的衣服直接淘汰,避免出现6月推荐羽绒服的灾难场景。

第二步,按标签打分。给衣服的style_tag设置权重:比如当天气温低于10度,标签含"保暖"的+3分,含"羊毛"的+2分;气温高于26度,"透气"+"3分,"棉麻"+2分。这些权重我放在一张配置表里,而不是写死在Java代码中,方便答辩时展示"规则可配置"。

第三步,随机扰动去重。前两步的结果如果每次都一样,用户连续看三天推荐会发现永远是同一套衣服,体验很糟糕。我引入一个Random因子,综合分相同时做一次随机排序,并在推荐结果里排除近3天已经穿过的衣服(排除逻辑用一条SQL:查询近3天outfit_detail里的clothing_item_id,然后NOT IN)。

下面贴一段核心推荐代码的骨架:

public List<ClothingItem> recommendOutfit(Long userId, Integer temperature, String weather) { // 1. 温度区间硬过滤 List<ClothingItem> candidates = clothingItemMapper.selectList( Wrappers.<ClothingItem>lambdaQuery() .eq(ClothingItem::getUserId, userId) .ne(ClothingItem::getStatus, 2) // 排除清洗中 .le(ClothingItem::getMinTemp, temperature) .ge(ClothingItem::getMaxTemp, temperature)); // 2. 排除近3天已穿过 List<Long> recentIds = outfitDetailMapper.findWornItemIds(userId, 3); candidates.removeIf(item -> recentIds.contains(item.getId())); // 3. 标签评分 + 随机扰动 candidates.forEach(item -> { int score = tagScoreCalculator.calculate(item, temperature, weather); item.setScore(score + randomHelper.nextInt(3)); }); // 4. 按分数降序,上装/下装/外套分层返回 return candidates.stream() .sorted(Comparator.comparingInt(ClothingItem::getScore).reversed()) .collect(Collectors.toList()); }

你答辩的时候,重点不是讲代码本身,而是讲清楚三个设计意图:为什么先硬过滤再打分(减少无效计算)、为什么排除近3天(增加推荐多样性)、为什么加随机因子(提升用户体验)。这比堆砌任何复杂公式都有说服力。

4.3 穿搭日历的批量预排功能

一周穿搭里的"周视图"如果只支持每天单独记录,效率太低。我实现了批量预排:用户可以在周日晚上,一键为下一周的7天生成穿搭草稿。生成逻辑也不复杂——把推荐接口按7天的温度预报各调一遍,每天生成一套组合,存进outfit_record里,状态标记为"草稿"(rating为空,comment为空就是草稿),用户每天可以编辑调整。

这一步实现时有个细节:uk_user_date唯一索引会让你批量插入时因为某一天已存在记录而整体失败。解决方案是使用INSERT ... ON DUPLICATE KEY UPDATE或者saveBatch时捕获DuplicateKeyException逐条跳过。我用的是逐条插入前先查重,虽然多一次查询,但逻辑最清晰,代码也好解释。

5. 联调与部署里那些不写进论文的坑

5.1 跨域这只拦路虎,以及CORS的隐藏冲突

如果你走Vue前后端分离路线,第一个遇到的就是跨域。开发环境下Vue跑在5173端口,后端跑在8080端口,前端fetch请求后端必然跨域。解决方案是在Spring Boot里写一个全局CORS配置类:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(Registry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

这里有一个网上搜不到细节的坑:如果你使用.allowedOrigins("*"),同时开启了.allowCredentials(true),Spring Boot 2.7会直接启动报错,安全校验会拒绝这种组合。网上的旧教程让用这两个搭在一起,就会踩这个坑。正确写法就是上面代码里的.allowedOriginPatterns("*"),它专门解决"通配符来源+携带凭证"的兼容问题。这个坑在我第一次整合时卡了我整整一个下午,现在写出来你就绕过去了。

5.2 图片上传:本地目录和Nginx静态映射的取舍

服装图片上传也是课题里躲不开的功能。最简单的方案是上传后存在本地磁盘的一个目录(比如D:/upload/),然后通过配置类映射为静态资源访问:

@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${upload.path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把本地磁盘目录映射到 /images/** 路径 registry.addResourceHandler("/images/**") .addResourceLocations("file:" + uploadPath + "/"); } }

配置好了之后,前端访问http://localhost:8080/images/xxx.jpg就能看到图片。但有两个坑必须提醒:第一,Tomcat默认单次请求最大2MB,你需要给SpringApplication加一下配置:

spring.servlet.multipart.max-file-size=5MB spring.servlet.multipart.max-request-size=10MB

否则前端一传超过2MB的图片,报错报得莫名其妙。第二,图片文件不能放在打包后的Jar包内部目录,否则部署到服务器重启后图片全部丢失。用本地绝对路径是安全稳妥的做法。

如果项目要交到服务器上演示,可以再加一个操作:上传成功后,把D:/upload/xxx.jpg的图片复制到Nginx的静态目录下,由Nginx直接返回。两个方案选一个就行,答辩时能讲清楚你选型的理由就够。

5.3 答辩演示前的最后自检清单

项目写完了,验证环节经常被忽略,导致答辩现场翻车的案例每年都有。我建议你按这个清单逐项确认:

  • 手机和电脑连的是同一个Wi-Fi,访问的是http://局域网IP:8080而不是localhost。别笑,每年都有学生只配了localhost,最后PPT投放电脑访问不到。
  • 用Postman把所有接口过一遍,确认增删改查的返回JSON结构统一(建议统一为{code, message, data}格式),前端解析才不会出错。
  • 登录过期逻辑测一遍:JWT过期后前端要跳回登录页,不要出现白屏或者无限loading。
  • 演示时尽量用测试账号提前录好10件以上的衣服和3天的穿搭记录,演示是有故事的:从录入衣服,到查看今日推荐,到编辑穿搭打分,一气呵成。
  • 准备一个"异常演示"预案,比如把天气服务关掉,展示App如何兜底降级。这么做不是自找麻烦,而是主动展示你的容错设计,绝对是答辩的高光时刻。

如果你打算把Vue打包放进Spring Boot里,再补充一点:npm run build后生成的dist目录,内容直接粘贴到src/main/resources/static里,但注意让后端接口路径统一加/api前缀,这样才能避免静态资源路径和接口路径冲突。打包时如果遇到history路由刷新404,说明Vue路由用了history模式,改成hash模式,或者给Spring Boot配一个跳过静态资源的转发即可。

最后再分享一个我的个人习惯:给outfit_record表的comment字段留好扩展位,很多二期的想法——比如"周五晚上有约会想看正式一点的搭配"、通过记录用户调研反馈和偏好标签来"猜你想穿"——都是从这个字段里生长出来的。一个课题项目的深度,往往不体现在用了多少技术栈,而在这些细节设计里。你把这个闭环跑通,吃透每一个设计决策背后的理由,答辩的时候一定是全场最从容的那个。

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

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

立即咨询