☰
Spring Boot装饰公司网站:从装修需求管理到业务闭环的实战设计
2026/10/10 7:39:14 网站建设 项目流程

我第一次见到有人把“装饰公司网站”这个题目当成一个带图的官网来做时,就知道这个题目里藏着一个特别容易被低估的坑。你只看字面意思,会以为要写一个展示页;可当你真正去梳理业主、设计师、运营这几类人的使用场景后会发现,它本质上是基于 Spring Boot 构建的家居装潢企业数字化运营系统——前台承载案例展示、室内外装修需求提交、在线预约,后台承载线索跟进、设计师派单、案例发布和账号权限管理。如果你正在准备计算机毕业设计,或者想拿 Java Web 做一套能讲清楚业务闭环的项目,这篇文章就是按我可复现的思路来写的。

我带的某同学,最初给我的方案就是“首页放几张效果图、案例页放一些图、后台能增删改查案例”,我听完直接让他重做。原因很简单:这样的系统,答辩时评委随便问一句“业主提交的装修需求你打算怎么处理?”就会卡住。而真正能拿到高分的毕设,至少要让数据在“访客提交 → 管理员处理 → 设计师介入 → 状态更新”这条链路里流动起来。下面我以自己实际开发的一套装饰公司网站为例,把系统拆开讲清楚:业务边界怎么划、表结构怎么设计、核心流程怎么写、权限怎么控制,以及最后演示时会踩的哪些坑。

1. 先想清楚业务闭环:这不是企业官网,是装修业务管理系统

1.1 别急着写登录注册,先把角色和使用场景列出来

很多人在动手写代码之前,第一件事就是搭 Spring Boot 工程、建用户表、写登录页。这个顺序其实反了。装饰公司网站的核心不是登录,而是“需求”这条线索怎么被采集、处理和推进。你要先把自己代入三种角色去思考。

我先说业主,也就是访客。他打开网站的目的非常直接:看到真实的装修案例,觉得这家公司靠谱,然后留下联系方式,要么预约量房,要么提交一个“我家几室几厅、想全包/半包、预算多少”的装修需求。他几乎不会注册之后再慢慢浏览,因为注册是有门槛的,会挫伤转化意愿。

再看公司内部的运营人员和设计师。运营人员每天打开后台,第一个想看到的是“今天新增了多少条需求、几条还没处理、哪些预约需要回电确认”。设计师则更关心“分给我哪几个工地、我的案例上传了没有”。最后是管理员,他不仅仅要看业务数据,还要管理账号、分配角色、审核案例和文章。

把这些角色列成一张表,系统的功能边界就非常清晰了:

角色核心痛点系统对应功能
业主/访客想找靠谱装修公司、想获取报价/方案案例展示、需求提交、在线预约、资讯阅读
运营/客服线索多而杂,容易漏单、跟进无记录需求单管理、预约管理、状态跟进、派单
设计师案例作品分散、缺少展示渠道个人介绍、案例上传、分配需求单
管理员账号权限混乱、内容审核麻烦用户管理、角色权限、案例/文章审核、数据看板

做完这一步,你根本不需要“硬凑功能”,因为每个模块都是被业务场景推着走出来的。尤其是“需求单管理”这一块,是整个系统的命脉,后面第 4 部分我会给出完整的代码链路。

1.2 我最终留下的功能清单:前台要轻,后台要重

很多人把功夫花在前台样式上,页面做得很漂亮,后台却只有两个可怜的 CRUD 界面。但实际评项目时,后台业务的完整度远比前台颜值重要。前台你只需要保证“信息结构清楚、浏览流畅”就行,真正花时间的地方是后台模块。

我最终做出来的功能清单如下:

  • 前台门户:首页 banner、公司简介、装修案例列表与详情、设计师团队、新闻资讯、在线预约表单、装修需求提交表单。
  • 后台管理:管理员登录、后台首页数据统计(今日新增需求、待处理数量、案例总数)、需求单列表(筛选、详情、跟进、指派设计师)、预约管理(确认、取消、完成)、案例管理(发布、编辑、上架/下架)、设计师管理(新增、排序、简介编辑)、文章管理、系统用户管理与角色分配。

这个清单本质上就是一个轻量级 CRM 加内容管理系统的结合体。对于毕设来说,它已经覆盖了 Spring Boot、MyBatis Plus、权限控制、文件上传、分页查询、状态机这几个核心考点,足够撑起一次完整的答辩。

1.3 哪些功能可以明确不做?系统边界要提前划好

同样重要的是“明确不做什么”。我在设计的时候主动砍掉了三块:在线支付、自动报价计算、ERP 进销存。

原因不是难,而是它们会引入大量异常分支。比如在线支付一旦涉及退款、回调,你需要处理很多极端情况,容易把主体业务淹没;自动报价则需要维护复杂的材料价格库,和装修公司实际线下报价的模式也不太相符。我在答辩时是这样解释的:系统定位为“数字化运营系统”,重点解决获客、线索管理、案例展示和内部协作问题,报价和支付属于线下业务闭环,系统预留了字段和扩展接口,避免越做越臃肿。这个说法既显得你有取舍能力,也让项目的技术范围足够聚焦。

2. 技术选型:Spring Boot 为主,但别把技术栈堆成全家桶

2.1 Spring Boot 版本怎么选?JDK 版本先定下来

这个项目是以 Java Web 为基础的,所以主角必然是 Spring Boot。但在选具体版本时,很多人犯过同一个错:电脑上明明是 JDK 8,却从网上复制了一段 Spring Boot 3.x 的配置,结果一启动就报错。

Spring Boot 2.7.x 支持 JDK 8 到 JDK 17;Spring Boot 3.x 最低要求 JDK 17。网上很多教程已经是 3.x 了,但如果你不熟悉 Java 模块化、不太会处理新版依赖变化,我建议按自己本机环境来定版本。我实际使用的是 Spring Boot 2.7 加 JDK 8,倒不是因为追求老旧,而是那台演示机的环境最稳。如果你用的是 JDK 17 或更高,那直接用 Spring Boot 3.x 也没问题,但注意 javax 包名要改成 jakarta,这在老代码迁移时特别容易踩。

本机 JDK推荐 Spring Boot 版本注意事项
JDK 82.7.x需连接池、MyBatis Plus 等依赖选兼容版
JDK 112.7.x基本同上,稳定优先
JDK 17+2.7.x 或 3.x3.x 使用 jakarta.,2.7 可继续用 javax.
JDK 213.x编译参数、Lombok 版本需更新

一句话总结:先看本机 Java 版本再选 Spring Boot 版本,整个项目依赖统一按这个主线去配,能少折腾很多。

2.2 MyBatis Plus 带来的便利,以及不能无脑用的地方

持久层我用了 MyBatis Plus。它提供的 BaseMapper 和 LambdaQueryWrapper 能让我少写大量样板 SQL,分页插件也是一行配置搞定。对毕设项目来说,开发效率是第一位的,你不需要像 SSM 时代那样手写 XML 里的每个 select 和 update。

但我也要说清楚它的边界。有些人误以为用了 MyBatis Plus 就完全不用写 SQL 了,真到了要做“今日新增需求数”这种统计,或者多表联查(比如查询案例时带出设计师姓名)时,你还是要老老实实写自定义 SQL。我的做法是:单表简单 CRUD 用 BaseMapper,涉及 JOIN 或聚合统计的写在 XML 里,这样代码既干净又不会在答辩时被问倒。

还有一个容易漏的细节:MyBatis Plus 的逻辑删除是全局配置logic-delete-field: deleted,如果你业务表里没有这个字段,千万不要全局开启,否则所有查询都会自动拼上AND deleted=0,数据都查不出来。我一般只在核心业务表上加 deleted 字段,并把删除字段的类型定为tinyint,这属于典型的“安全兜底”设计。

2.3 权限处理:这可能是答辩时被问到的第一个技术点

装饰公司网站天然分成访客区和管理后台,访客不需要登录,管理后台必须有权限限制。很多人第一反应是上 Spring Security 或者 Shiro,但我不建议毕设这么做,除非你能把它的过滤器链原理讲得非常清楚,否则自己写的代码里一旦出了奇奇怪怪的 403,排查起来会很痛苦。

我当时采用的是“登录用户 + 角色 + 自定义注解 + 拦截器”的轻量权限方案:用户登录后把用户信息放进 Session,管理后台的请求经过拦截器校验,再通过自定义@RequireRole注解限制接口只能由管理员或运营人员访问。这套方案代码量不大,但你所用的知识点都看得见摸得着,面试或答辩时也能把“基于 RBAC 的轻量权限模型”讲明白。第 5 部分我会给拦截器代码,这里先记住一个核心原则:权限控制要放在服务端,前端隐藏按钮只能算辅助。

3. 数据库设计:装修业务数据怎么落库,直接决定项目上限

3.1 核心表结构一览:我用到的十张核心表

数据库设计我是这样规划的:用户体系独立成组,业务数据围绕“需求”和“案例”两条主线展开,内容类数据单独建表。最终落地的核心表如下:

表名作用关键字段
sys_user登录账号id, username, password, real_name, phone, avatar, status
sys_role角色表id, role_code, role_name
sys_user_role用户角色关联user_id, role_id
biz_demand装修需求单id, owner_name, phone, house_area, budget_range, decoration_type, content, status, designer_id, handle_user_id, create_time
biz_reservation在线预约id, user_id, designer_id, reserve_date, remark, status
biz_case装修案例id, title, cover_image, designer_id, style, area, budget, description, content, publish_status
biz_designer设计师id, name, avatar, title, specialty, introduction, sort
biz_article新闻资讯/装修知识id, title, cover_image, summary, content, publish_status
biz_demand_log需求跟进日志id, demand_id, operator_id, from_status, to_status, remark, create_time
biz_feedback留言/反馈id, contact_name, phone, content, status

这套结构最核心的一点,是把“需求”和“案例”分开,而不是让它们混在一张表里。需求是客户尚未成交的线索,案例是已经完成的项目展示,两者生命周期完全不同。如果非要在案例表里塞需求状态,后面你就会陷入“到底该改哪张表”的混乱。

3.2 装修需求单是核心:status 字段要有“状态机”思维

在众多表里,biz_demand是最值得花心思的。它的status字段不能随便用字符串存“待处理”“已签约”这类中文词,万一前端传了个“已签单”,后端和数据库里的“已签约”对不上,查出来的数据就全是空的。我的方案是用tinyint存状态码,并通过一个 Java 枚举统一管理:

状态码状态名称含义谁可以操作
0待处理用户刚提交,还没有人跟进所有人可见,运营人员接收
1沟通中客服/运营已电话联系业主运营、管理员
2设计中已派给设计师,正在出方案设计师、运营、管理员
3已签约客户认可方案,签订装修合同运营、管理员
4已完成施工/服务已完成,可归档案例管理员
5已关闭客户流失或无效线索管理员

这样设计的好处有三个:第一,排序和筛选都很方便,where status = 2比字符串比较更可靠;第二,状态码完全由后端控制,前端拿到数字后映射到中文标签即可,不会出现“手写文字不一致”的低级错误;第三,你可以基于状态码设计一个小状态机,防止需求单被随意跳转(比如没指派设计师就直接变成“设计中”),这在第 4 部分会具体演示。

3.3 图片、富文本和软删除:三个拉开档次的设计细节

第一个细节是图片存储。装修案例和设计师头像都需要图片,最省事的方式是把图片文件传到服务器指定目录,数据库只存 URL。很多人喜欢把图片直接转 base64 长字符串存进数据库,这会让表体积迅速膨胀,性能也差。我用的字段是cover_image varchar(255),上传接口返回/upload/2025/xxx.jpg,前端用这个相对地址展示。

第二个细节是富文本内容。案例详情和文章正文会很长,用varchar很可能不够,建议直接用longtext。如果你担心编辑器传回来的 HTML 带样式隐患,可以在后端做白名单过滤,只保留 p、img、h2、ul、li 这些基础标签。

第三个细节是软删除。我不会直接物理删除需求单和案例,因为业务数据即便标记为“已关闭”或“已下架”,也需要保留历史记录。所以我在核心表里统一加deleted tinyint default 0,用 MyBatis Plus 的逻辑删除功能处理。答辩时被问“为什么不用外键、为什么逻辑删除”,你就可以把“保留历史、方便恢复、避免级联删除复杂度”这几个理由讲清楚。

4. 业务闭环实现:需求提交、状态流转、后台派单的代码链路

4.1 需求提交接口:参数校验和防重复必做

业主在前台填一张装修需求表单,提交到后端。这个接口是整个系统的入口,也是演示时最容易出效果的地方。我在代码里只做了两件“额外”的事:参数校验和防重复提交。

@PostMapping("/api/demand/submit") public Result<Void> submit(@RequestBody @Valid DemandSubmitDTO dto) { String key = "demand:repeat:" + dto.getPhone(); Boolean isFirst = redisTemplate.opsForValue() .setIfAbsent(key, "1", Duration.ofSeconds(60)); if (!Boolean.TRUE.equals(isFirst)) { return Result.error("您的需求已收到,请勿重复提交"); } Demand demand = new Demand(); demand.setOwnerName(dto.getOwnerName()); demand.setPhone(dto.getPhone()); demand.setHouseArea(dto.getHouseArea()); demand.setBudgetRange(dto.getBudgetRange()); demand.setDecorationType(dto.getDecorationType()); demand.setContent(dto.getContent()); demand.setStatus(DemandStatus.PENDING.getCode()); demand.setDeleted(0); demandService.save(demand); return Result.ok(); }

这段代码最值钱的是setIfAbsent这一行。它利用 Redis 的原子操作,保证同一个手机号在 60 秒内只能提交一次。如果没有 Redis,也可以用本地ConcurrentHashMap做时间窗口,但用 Redis 会让演示效果更完整,而且能自然引出“为什么用 Redis”的追问。DTO 上加@NotBlank、@Pattern校验手机号,由全局异常处理器统一返回友好提示,这样接口层就非常干净。

4.2 后台需求列表:筛选、分页和排序一步到位

后台运营人员打开需求列表时,需要按状态筛选,也需要搜索手机号。实现上我用 MyBatis Plus 的 LambdaQueryWrapper 很顺手:

Page<Demand> page = demandService.page( new Page<>(pageNum, pageSize), new LambdaQueryWrapper<Demand>() .eq(StringUtils.hasText(phone), Demand::getPhone, phone) .eq(status != null, Demand::getStatus, status) .orderByDesc(Demand::getCreateTime) );

这里有两个小坑。一是“手机号筛选”不要写成like然后传%,这会破坏索引,除非你真的需要模糊搜索;二是分页参数要设置合理上限,我一般限制每页最大 50 条,告诉前端不要一次拿太多。你可以在返回结果里顺便带上total、pages,前端分页组件才能正常工作。这个接口看起来简单,但它覆盖了“条件构造器 + 分页插件 + 排序规则”三个考点,答辩时完全可以展开讲。

4.3 状态机更新:权限校验和业务校验一个都不能少

需求单不能谁想改状态就改,也不能乱序跳转。我单独写了一个状态变更方法,放在 Service 层:

@Transactional(rollbackFor = Exception.class) public void changeStatus(Long demandId, Integer targetStatus, Long operatorId) { Demand demand = demandService.getById(demandId); if (demand == null) { throw new BizException("需求单不存在"); } DemandStatus target = DemandStatus.codeOf(targetStatus); if (!allowedOperator(operatorId, target)) { throw new BizException("当前角色无权执行该状态变更"); } if (target == DemandStatus.DESIGNING && demand.getDesignerId() == null) { throw new BizException("请先指派设计师,再进入设计中"); } if (target == DemandStatus.CLOSED && demand.getStatus() != DemandStatus.PENDING.getCode()) { throw new BizException("只有待处理的需求才能直接关闭"); } DemandLog log = new DemandLog(); log.setDemandId(demandId); log.setOperatorId(operatorId); log.setFromStatus(demand.getStatus()); log.setToStatus(targetStatus); log.setRemark("状态变更"); demand.setStatus(targetStatus); demandService.updateById(demand); demandLogService.save(log); }

这个方法把三件事融为一体:状态权限校验、业务前置条件校验、操作日志记录。尤其是日志表,很多人会忽略,但它恰恰是答辩时的加分项。比如你演示“把一个待处理的需求改成设计中”,系统首先会校验是否已指派设计师,然后记录“从 0 到 2”的完整操作日志。相比普通的管理后台增删改查,这个模块已经具备工作流引擎的雏形了。

5. 前后端交互与权限控制:管理后台不能“裸奔”

5.1 服务端渲染还是前后端分离?我最终选了“混合模式”

这个问题几乎每个做毕设的人都会纠结。前后端分离确实显得技术先进,但它会给毕业设计带来额外的工程复杂度:跨域、Token 管理、前端打包、部署路径,每一个都能让人折腾很久。我当时的前台页面用的是 Thymeleaf 服务端渲染,首页和案例详情直接返回页面;后台管理同样使用 Thymeleaf 模板,表格区域通过 AJAX 调接口拿 JSON 数据,再局部刷新。这样既保留了服务端渲染对 SEO 友好、首屏快的优点,又能避免为了一个小后台单独起一个 Vue 工程。

如果你希望锻炼前端技能,可以在后台管理里引入 Vue 3 加 Element Plus,做成独立的前端工程。但我个人的建议是:除非你已经很熟练,否则不要为了“用新技术”而把部署复杂度提上去。答辩评委更关心你是否真的理解数据怎么在前后端之间流动,而不在乎你的前端是不是 SPA。

5.2 登录拦截器加注解权限:挡住不该进的页面

管理后台的接口必须统一做登录校验。我实现了一个AuthInterceptor:

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } // 这里可以继续校验角色,或者配合自定义注解做更细粒度控制 return true; } }

配置类里只拦截/admin/**和部分/api/**写操作,放行静态资源、登录接口和前台公开接口。用拦截器有一个好处:你可以在答辩时清楚地说出“哪些路径允许匿名访问、哪些必须登录”,这是很多人答不上来的细节。如果还需要细化角色,我配合@RequireRole注解实现,拦截器里读取注解值再和用户角色比对,逻辑很直接。

5.3 文件上传的三个细节:重命名、校验、外链播放

后台发布案例时会上传封面图,这里我做了一个通用上传接口。实际代码比较短,但有三个细节很重要:

@PostMapping("/api/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException { String original = file.getOriginalFilename(); String ext = original == null ? "" : original.substring(original.lastIndexOf('.')).toLowerCase(); List<String> allowed = Arrays.asList(".jpg", ".jpeg", ".png", ".webp"); if (!allowed.contains(ext)) { return Result.error("图片格式不支持"); } if (file.getSize() > 5 * 1024 * 1024) { return Result.error("图片不能超过5MB"); } String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")); String filename = UUID.randomUUID().toString().replace("-", "") + ext; Path dir = Paths.get(uploadDir, datePath); Files.createDirectories(dir); file.transferTo(dir.resolve(filename).toFile()); return Result.ok("/upload/" + datePath + "/" + filename); }

第一点是文件名重命名,用 UUID,而不是用户上传时可能带中文名字的文件名,避免乱码和路径穿越;第二点是扩展名白名单和大小限制,直接挡住不合规文件;第三点是按月份分目录,防止所有文件堆在一个目录里导致查找困难。演示时哪怕只上传一张图,你也能顺带讲出这三个设计理由,比单纯说“我把图存上去了”要专业得多。

6. 从“能跑”到“能演示”:毕设通关避坑清单

6.1 环境问题:本地跑不起来,多半是版本和字符集

我见过太多项目在写代码阶段没毛病,一到演示环境就“翻车”。最常见的问题有三个。第一个是数据库连接 URL 没配characterEncoding=utf8,导致表单提交的中文变成问号;第二个是 Maven 依赖版本冲突,比如 MyBatis Plus 版本和 Spring Boot 版本不匹配,一启动就报NoSuchMethodError;第三个是 JDK 版本不对,Spring Boot 3.x 项目跑到 JDK 8 上根本无法启动。

我强烈建议在项目交付前做一次“干净环境验证”:把本地target目录清掉,重新mvn clean package,然后用java -jar跑一遍。这能暴露出那些平时靠 IDE 缓存掩盖的问题。数据库连接串我通常这样配:

jdbc:mysql://localhost:3306/fitment_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false

6.2 演示时容易被追问的问题,提前准备标准回答

答辩和演示是一个“主动展示 + 防御追问”的过程。我把评委大概率问的问题提前整理了一遍,这里直接放参考答案:

追问问题建议切入角度
为什么用 Spring Boot 而不是传统 SSM?自动配置、内嵌容器、约定优于配置,降低工程搭建成本
权限是怎么做的?Session 登录 + 拦截器 + 角色注解,讲清 RBAC 模型
为什么需求表要设计状态机?保证流程合规,避免任意跳转,后续可扩展为工作流
图片为什么存 URL 而不存数据库?数据库只存元数据,文件放磁盘/CDN,降低读写压力
系统用户量大了怎么办?先加索引、引入 Redis 缓存热点数据,再谈横向扩展

这些问题没有标准答案,但你只要能把“为什么这么设计”讲清楚,就已经赢过大多数只会背代码的选手了。尤其是“状态机”和“逻辑删除”这两个点,技术含量不高,却是区分用心程度的关键。

6.3 如果这个项目继续扩展,我会优先加什么

做完这套装饰公司网站之后,我很明确如果继续往下做,优先级最高的不是花里胡哨的功能,而是消息通知与跟进闭环。比如:业主提交需求后,系统给运营人员弹一条待办;运营把状态改成“沟通中”,自动提醒设计师接单;需求进入“设计中”后,给业主发一条进度短信。这些能力相当于把线下装修公司的“跟单”动作搬到线上,才是真正意义上的数字化运营。

其次是数据看板,用 ECharts 展示每月需求数量变化、各状态占比、设计师案例量排行。这个功能在技术上新意不大,但视觉效果最好,也最容易形成项目亮点。至于小程序端和在线支付,我认为可以作为后续方向提一嘴,不需要真的实现,因为那已经超出毕设合理周期了。

最后说句实在话:这类 Java Web 项目能不能打动人,不在于你用了多新的框架,而在于你有没有把一条业务线从头走通。我当年做完这套系统,最深的体会是把状态机想清楚之后,后面所有需求单相关功能都变得特别好写。如果你正准备动手,建议先不要急着敲代码,花一个下午把你项目里的角色、状态、权限画一遍,你会收获比写十个接口更大的进步。

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

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

立即咨询