Spring Boot装修网站这个题,在计算机毕业设计里算是常青树了。我当时拿到的题目全称是“基于Spring Boot的家居装修服务平台开发与实现”,后来查资料才知道还有“装修管理系统”这种偏后台的版本。做完整套系统再回头复盘,我最大的感受是:这个题目性价比确实高,它同时踩中了技术主流、业务贴近生活、工作量可控三个点。对Java方向的同学来说,不管你是想顺利毕业还是想在简历里添一笔,这个项目都能给足你发挥空间。
这篇文章我就以“已经做完这个毕设的人”的身份,从选题分析、技术选型、数据库设计、核心模块实现到踩坑排查,把整个开发过程完整复盘一遍。内容会尽量还原我当时的决策过程,也会把那些网上教程不爱写、但实际一定会遇到的问题讲清楚。无论你本科还是专科,只要准备选或者已经选了Spring Boot装修类题目,这篇文章应该能帮你少走不少弯路。
1. 先把题目吃透:这到底是个什么系统
1.1 毕设选题的第一道关:技术栈要“正”
“Spring Boot装修网站”这类题目在各大毕设选题库中出镜率极高,原因不难理解。对学校来说,Spring Boot是目前Java后端最主流的基础框架,用它检验学生的工程能力既不过时也不超前;对你来说,Spring Boot把Spring那一堆繁琐的XML配置全部自动化,学习曲线比SSM(Spring+SpringMVC+MyBatis)平缓太多,适合在毕设周期内快速落地。
我的建议是,在开题阶段就把方向定成“Spring Boot + MyBatis-Plus + Vue前后端分离”。理由有两条:第一,这类组合是当前Java后端岗位最常见的技能组合,写进简历有说服力;第二,毕设讲究“在规定时间内做出来”,MyBatis-Plus这种增强工具能帮你省掉至少三分之一的基础CRUD代码,你才有时间把精力放到业务设计和界面呈现上。
1.2 功能边界怎么切:别一上来就全都要
注意标题里其实藏着三个关键词——装修网站、家居装修服务平台、装修管理系统。这三个词指向的定位略有差别:
| 定位 | 偏向 | 典型用户角色 | 核心功能 |
|---|---|---|---|
| 装修网站 | C端展示 | 游客、业主 | 展示案例、装修公司信息、在线留言 |
| 家居装修服务平台 | 交易撮合 | 业主、装修公司、平台运营 | 预约装修、方案报价、订单流程管理 |
| 装修管理系统 | B端管理 | 平台管理员、装修公司员工 | 项目管理、施工进度、材料/人员管理 |
如果你照着“全部都要”去做,大概率会陷入功能臃肿、每个模块都浅尝辄止的困境。我当时跟导师确认后,选的是“平台服务+后台管理”双端结构:前台面向业主展示装修公司、案例、设计师,并提供预约装修功能;后台面向管理员管理内容、处理预约订单、发布公告。这套结构既覆盖了“网站”的展示属性,也覆盖了“平台”的交互属性,工作量对一个毕设来说刚刚好。
建议功能清单大概是这个范围:
- 前台用户端:注册登录、装修公司列表/详情、装修案例筛选与详情、设计师展示、在线预约、个人中心(我的预约/资料修改)
- 后台管理端:管理员登录、仪表盘统计、用户管理、公司管理、案例管理、预约单处理(状态流转)、公告管理、系统设置
不要为了炫技加什么社区论坛、在线支付、实时聊天,除非你时间真的很充裕。毕设答辩老师看的是“业务闭环完整、技术掌握扎实”,不是功能数量。
2. 技术选型与开发环境准备
2.1 为什么选Spring Boot而不是Servlet或者SSM
我刚开题时也纠结过:要不要用传统Servlet写,显得“底层功底扎实”?后来被自己否掉了。Servlet手写一套Web应用,光处理请求转发、参数封装、数据库连接管理就够写几百行重复代码,这类代码对毕设几乎没有正面帮助。SSM虽然有它存在过的意义,但配置文件一大堆,集成期间随便一个版本冲突就能耗掉你几天。
Spring Boot最大的价值是“约定优于配置”。你创建项目的瞬间,内嵌Tomcat、自动装配、Starter依赖体系全部就位,写一个Controller就能跑起来。对毕设这种追求“快速稳定出活”的场景,这是最理性的选择。
另外再提一下Spring Boot版本。我当时用的是Spring Boot 2.7.x,配合JDK 8。为什么不直接上Spring Boot 3.x?因为Spring Boot 3.0强制JDK 17,而且很多第三方库还需要适配期。毕设阶段求稳,2.7.x生态最成熟,网上资料也最多,遇到问题随手就能搜到答案。如果你是非要用JDK 17和Spring Boot 3.5做新技术展示的,那另说,但我不建议把版本兼容性问题带进毕设的紧张周期里。
2.2 开发环境与版本匹配
我的环境清单如下,供参考:
- JDK 1.8(Spring Boot 2.7.x)
- Maven 3.6.3以上
- MySQL 5.7 / 8.0(我用的是8.0,注意驱动配置差异)
- Redis不是必须,毕设不用缓存也说得过去
- 前端:Vue 2 + Element UI(简捷,适合展示型后台)
- 开发工具:IDEA 2023.1(自带数据库插件非常方便)
核心的pom依赖,我用的是spring-boot-starter-web、mybatis-plus-boot-starter、lombok、mysql-connector-java,再加一个spring-boot-starter-validation做参数校验。如果要加JWT鉴权,还需要引入jjwt相关依赖。注意mybatis-plus和spring boot的版本兼容问题,不要直接抄最新版,去查一下官方版本对应关系,不然启动时会报一堆奇怪的错误。
application.yml里几个容易踩坑的配置:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/decoration?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 servlet: multipart: max-file-size: 10MB max-request-size: 30MB jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里driver-class-name如果MySQL是8.0以上,要用com.mysql.cj.jdbc.Driver;MySQL 5.7用com.mysql.jdbc.Driver会频繁报警告。serverTimezone必须显式声明,否则会直接抛“The server time zone value”的异常。
3. 数据库设计:把业务落到表上
3.1 核心表怎么拆
装修平台的数据模型并不复杂,但表设计做得好不好,直接决定你写Mapper时的舒适度。我当时设计了下面这几张核心表:
- user:前台用户(业主),字段包含用户名、密码、手机号、昵称、头像、注册时间
- company:装修公司,包含公司名、简介、资质、地址、联系电话、状态(是否上架展示)
- designer:设计师,属于某家公司,包含姓名、头衔、擅长风格、个人简介、头像
- case_info:装修案例,包含标题、封面图、所属公司、风格、面积、预算、几室几厅、详情轮播图、发布时间
- appointment:预约单,核心业务表,包含业主id、公司id、期望装修风格、预算区间、房屋面积、联系手机、备注、状态字段
- admin:后台管理员
- notice:公告
为什么把设计师独立成一张表而不是写进公司表的一个字段?因为设计师本身有独立的展示页面、独立的筛选条件,后续还可能做“设计师私单”,拆出来方便扩展,也符合数据库第三范式的思想。案例表同理,一个公司有很多案例,如果不建子表,后期查询要拼接字符串,痛苦得很。
3.2 状态字段比多张表更省事
预约单的状态是我的重点设计。很多同学一看到“流程”就想建一张“状态流转表”或者搞个工作流引擎,毕设真的没必要。你只需要一个int类型的status字段,用常量去定义:
public class AppointmentStatus { public static final int PENDING = 0; // 待确认 public static final int CONFIRMED = 1; // 已沟通 public static final int SIGNED = 2; // 已签约 public static final int CANCELED = 3; // 已取消 public static final int FINISHED = 4; // 已完成 }加上一个状态变更时间字段,就可以精确记录每个节点。如果想给评审老师展示一点“设计感”,可以在代码里对状态流转作校验:比如已取消的预约不能再变更为已签约,已完成的不能回退到已沟通。这种逻辑写起来简单,却能让答辩时的业务讲解显得完整。
预约单建表SQL参考(关键部分):
CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '业主用户ID', company_id BIGINT NOT NULL COMMENT '装修公司ID', house_area DECIMAL(10,2) COMMENT '房屋面积(㎡)', budget_min DECIMAL(10,2) COMMENT '预算下限(万)', budget_max DECIMAL(10,2) COMMENT '预算上限(万)', style VARCHAR(50) COMMENT '期望风格', contact_phone VARCHAR(20) NOT NULL, remark VARCHAR(500), status TINYINT DEFAULT 0 COMMENT '状态', create_time DATETIME, update_time DATETIME, INDEX idx_user (user_id), INDEX idx_company (company_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;4. 从零搭建项目:核心代码实现过程
4.1 项目结构与包划分
我建议按功能模块分包,而不是按技术层次分包。这句话是很多同学容易忽略的点。按controller/service/mapper这种技术层分包,前期写起来顺手,但一旦系统变大,找文件就非常费劲。按业务模块分包,比如user、company、case、appointment、admin、common,每个包内再放controller、service、mapper,后期排查逻辑和管理代码都轻松得多。
我的包结构大致是:
com.example.decoration ├── common // 统一返回、异常处理、工具类 ├── config // 跨域、WebMvc配置、MyBatisPlus分页配置 ├── controller // 或者拆成 admin/ 和 front/ 两个子包 ├── entity // 数据库实体 ├── mapper // MyBatis-Plus Mapper接口 ├── service // 业务层接口与实现 ├── dto // 入参和出参对象 └── DecorationApplication.java4.2 登录与JWT鉴权:为什么不用Session
前台用户登录、后台管理员登录是系统的门户,一定要写得规范。我当时用的是JWT无状态鉴权,前端登录成功后把token存在localStorage,每次请求在请求头带上Authorization: Bearer token,后端通过拦截器校验token有效性。
选JWT而不是Session,理由是:前后端分离项目天然适合无状态认证,你将来去公司实习大概率也是这么写的;而且答辩时可以讲清楚“无状态”三个字,比一句“用了Session”更有技术含量。
JWT工具类核心部分:
public class JwtUtils { private static final String SECRET = "your-secret-key-for-decoration-platform"; private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; // 7天 public static String generateToken(Long userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }注意,SECRET是自行定义的字符串,别把线上生产环境的高强度密钥逻辑照搬过来,毕设系统的密钥只要能解释清楚用途就行。令牌有效期设7天,用户前端觉得“不用老登录”,管理员端也不需要频繁重新登录。
拦截器的实现要点是:拦截所有带/decoration/api/**前缀的接口,但在预检请求(OPTIONS)和登录接口上放行。拦截到无效token时,直接返回401状态码和统一提示信息。
4.3 装修案例的分页展示与多条件筛选
装修案例的列表页是前台的“门面”,需要支撑按风格、户型、预算、面积、公司名称等多个条件组合查询。用MyBatis-Plus的分页插件加LambdaQueryWrapper写起来非常清爽:
@Override public IPage<CaseInfo> pageCaseList(CaseQueryDTO dto) { Page<CaseInfo> page = new Page<>(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapper<CaseInfo> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(dto.getTitle()), CaseInfo::getTitle, dto.getTitle()) .eq(StringUtils.hasText(dto.getStyle()), CaseInfo::getStyle, dto.getStyle()) .eq(dto.getCompanyId() != null, CaseInfo::getCompanyId, dto.getCompanyId()) .between(dto.getMinBudget() != null && dto.getMaxBudget() != null, CaseInfo::getBudget, dto.getMinBudget(), dto.getMaxBudget()) .orderByDesc(CaseInfo::getCreateTime); return caseInfoMapper.selectPage(page, wrapper); }这段代码里的关键是链式条件判断:第一个参数是boolean,只有为true时后面的查询条件才会拼接到SQL上,从而完美解决“用户不选就不筛选”的场景。很多自学同学容易在这里用一堆if else拼字符串,MyBatis-Plus这个API就是为了消除这种重复劳动。
4.4 预约单的状态流转与防重复提交
预约模块是业务闭环的核心。前台用户提交预约时,我需要做三层校验:
- 参数校验:手机号格式、预算区间大小、必填字段是否为空
- 逻辑校验:同一用户是否已经预约过这家公司且状态还是待沟通,是则提示“请勿重复提交”
- 业务校验:如果该公司已被平台下架,前端虽然不展示,但后端接口也要拦一道
防重复提交很多人只会想到前端按钮置灰,但后端必须做兜底。我当时写得很简单:
long count = appointmentService.count(new LambdaQueryWrapper<Appointment>() .eq(Appointment::getUserId, userId) .eq(Appointment::getCompanyId, companyId) .in(Appointment::getStatus, PENDING, CONFIRMED)); if (count > 0) { throw new ServiceException("你已提交过预约,请勿重复操作"); }状态变更接口要带一个“当前状态”参数,比如把“已沟通”改成“已签约”时,执行的是:
UPDATE appointment SET status = 2 WHERE id = ? AND status = 1如果更新影响行数为0,说明数据状态发生了并发变更或当前状态不对,直接返回“状态已变更,请刷新后重试”。这种做法叫乐观锁思路,不需要引入真正的锁,但能有效防止多人操作同一数据时的逻辑错乱。
4.5 文件上传与静态资源映射
装修系统里图片是关键:案例封面、轮播图、设计师头像、公司资质图片。毕设阶段不需要引入OSS对象存储或MinIO,直接用本地存储就够了。上传接口做的事很简单:把MultipartFile保存到服务器指定目录,返回可访问的URL。
@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.fail("请选择要上传的文件"); } String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString().replace("-", "") + suffix; String today = LocalDate.now().toString(); String savePath = "D:/upload/decoration/" + today; File saveDir = new File(savePath); if (!saveDir.exists()) { saveDir.mkdirs(); } file.transferTo(new File(saveDir, fileName)); String url = "/files/" + today + "/" + fileName; return Result.success(url); }同时还需要一个WebMvc配置把/files/**映射到真实磁盘目录:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceResolver(new PathResourceResolver()) .addResourceLocations("file:D:/upload/decoration/"); }这里有个隐藏的坑:file:后面路径的分隔符,Windows下写正斜杠也完全没问题,千万别画蛇添足写反斜杠。另外定期清理这些上传目录,不然毕设做到后面磁盘会堆一大堆测试图片,别问我怎么知道的。
5. 联调与前端对接:让功能真的跑起来
5.1 统一返回体设计与接口测试工具
前后端分离项目里,接口数据格式不一致会导致联调时无尽的口水战。我强烈建议后端所有Controller都返回统一的Result结构,把code、message、data三者封装好:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> fail(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }前端只需要统一判断code是不是200,不用每次都解析不同的字段名。接口测试工具我当时用的是Apifox,比Postman更符合国内团队习惯,最重要的一点是:Apifox可以把接口文档同步给队友,连Mock数据都顺带产生了,联调效率提升明显。
5.2 跨域问题:前后端联调的第一杀手
如果你用Vue启动在8081端口,后端跑在8080端口,浏览器访问前端页面时发起的API请求必然触发跨域。解决办法是在后端写一个CorsConfig:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns和allowedOrigins的区别:新版Spring Boot推荐用Pattern,因为allowCredentials(true)时allowedOrigins不能写"*"。这个问题当时困扰了我一晚上,每次都报“Cannot use wildcard in allowedOrigins”,后来换成Patterns就解决了。
6. 常见问题排查实录:毕设最实用的一节
6.1 MySQL时区报错
启动项目时如果控制台报“The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized”,这不是什么大问题,就是连接串缺了serverTimezone参数。按照我之前给的yml配置加上Asia/Shanghai即可。如果还不行,就去MySQL命令行执行:
SET GLOBAL time_zone = '+8:00';6.2 Maven依赖冲突或下载失败
Maven里最常见的是mybatis-plus和mybatis相互冲突、pagehelper分页插件和mybatis-plus冲突。解决方案很简单:统一用mybatis-plus-boot-starter,不要自己再引入mybatis或分页插件。如果依赖下载失败,优先检查IDEA里Maven配置的镜像源是不是阿里云,私服、公司源什么的在毕设阶段统统去掉,省心。
6.3 端口被占用
启动时报“Port 8080 was already in use”,去任务管理器找占用进程,或者在IDEA终端执行:
netstat -ano | findstr :8080 taskkill /PID 进程号 /F如果是前端Vue的8081端口被占,思路一样。毕设做到后期,经常开着微信开发者工具、前端脚手架的调试服务、后端服务三四个进程,端口冲突是常态。
6.4 MyBatis-Plus分页插件失效
我遇到过查询结果是一整页数据,分页没生效。原因很典型:分页拦截器没被注入Spring容器。在配置类里加上:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }分页功能依赖这个拦截器,没有它,selectPage就会退化成查全表再内存分页,数据量一大就慢。记得加完检查依赖里有没有mybatis-plus-extension包,有些自定义精简pom会漏掉它。
6.5 数据库字段名与Java关键字冲突
我建表时设置了一个status字段,本身没问题,但如果表里有像describe、order、level这种字段,SQL查询时可能会报语法错误。经验是:字段名尽量用下划线命名且避开数据库保留字;MyBatis-Plus默认开启了驼峰映射,Java实体里的status对应表里的status,保持简洁。如果非要使用保留字作为字段名,在查询时用反引号包裹,但尽量别这么干。
6.6 前端拿不到后端传的Long型ID
这个坑特别隐蔽。Java的Long类型数字超过JavaScript安全整数范围时,前端解析会丢失精度,导致数据详情页跳转报错或404。解决方式是在后端序列化时把Long转成String:
@JsonSerialize(using = ToStringSerializer.class) private Long id;或者直接在配置里注册一个Jackson定制Bean,给所有Long类型统一加ToStringSerializer。这个细节做不好,答辩演示时一旦案例ID过大,前端跳转直接白屏,那场面非常尴尬。
最后一点建议:把项目做“厚”
做完这个系统时我的体会是:毕设项目的完成度,并不在于你用了多少新技术,而在于你把每个模块的边界想得多清楚、每种异常情况处理得多到位。我当时只用了Spring Boot全家桶加一点JWT和文件上传,但在答辩时把“预约状态机的设计”、“JWT无状态认证为什么适合前后端分离”、“文件存储后续如何平滑迁移到OSS”这三个点讲透了,老师问的问题基本都在可控范围内。
如果你想让这个项目后续还能包装得更完整,可以从三个方向扩展:一是给预约单加一个简单的提醒功能,预约创建后通过邮箱或短信模板发送通知;二是把案例的“热门推荐”用Spring Cache或Redis缓存起来,呼应性能优化的考察点;三是做一个公司维度的数据仪表盘,展示预约量趋势、签单率、风格热度排名,用ECharts画折线图和柱状图。这几个方向都属于“看着不难但很加分”的扩展,时间充裕的话值得一试。
最后再叮嘱一句:毕设排期一定要给“联调测试”留出两周。我认识太多同学代码写完了,结果卡在跨域、端口、前端部署这些环境问题上,最后赶工熬夜改bug。这个项目本身不难,但流程中的每个环节都需要你亲手走一遍,走通了,这套系统就是你真的能讲清楚的作品。