Spring Boot整合SSM与Vue构建美容院商城预约管理系统实战

发布时间:2026/9/11 5:44:20
Spring Boot整合SSM与Vue构建美容院商城预约管理系统实战 年前刚把一个美容院美妆化妆品商城管理系统从头到尾撸完后端用 Spring Boot 整合 SSMSpring Spring MVC MyBatis前端用 Vue 做前后端分离从数据库建模到权限控制再到订单流程全部自己搞定。整个项目做完之后最大的感受是这类“管理系统”看起来到处都是但真要把业务逻辑理清楚、把坑踩平、把代码写得能给别人看还是有不少门道。这套系统表面上是个电商商城实际上揉进了两种完全不同的业务场景一边是标准的美妆商品在线购买流程另一边是美容院线下服务项目的预约与管理。这也让它比普通的商品管理系统多了不少可聊的点。这篇文章我就把整个项目的设计思路、核心模块实现、实操过程和踩坑记录完整分享出来。如果你正在做类似的毕设、接私活或者想用这套技术栈练手应该能省下不少时间。1. 项目整体设计与业务模块拆解1.1 项目定位这个商城系统到底给谁用在动手写代码之前我先把项目的使用场景捋了一遍。市面上常见的商城系统大多是纯线上零售比如卖衣服鞋子数码产品这类系统的核心是商品、购物车、订单、支付、物流。但美容院场景有个特殊性它不仅有实体美妆产品的销售还有线下服务项目比如面部护理、身体按摩、美甲美睫这类需要提前预约、分配美容师和工位的服务。所以这套系统实际包含了三个端前台商城端给普通用户用浏览商品、加购物车、下单、查看订单、预约美容服务、查看美妆资讯。后台管理端给管理员和运营用管理商品分类、商品上下架、库存、订单处理、用户管理、轮播图配置、优惠券设置。美容院运营端给门店美容师或店长用查看预约记录、管理服务项目、维护排班和技师信息。我选择把它定位成“商品销售 服务预约”双核心模式而不是单纯做个商品展示加购物车的demo。原因很简单纯商品商城在网上随便一搜就有无数个但能把服务预约和商品销售结合好的项目不多这个差异点无论放在毕设答辩还是接私活的项目演示里都更容易讲出东西。1.2 功能模块拆解与角色权限划分整个系统的功能模块我拆成了六大块每一块再往下细化成具体功能点模块前端功能后台功能用户中心注册登录、个人信息编辑、收货地址管理用户列表、用户状态管理禁用/启用商品中心分类浏览、商品搜索、商品详情、多规格选择分类管理、商品管理、SKU管理、库存管理购物车与订单购物车增删改查、结算、下单、订单列表、订单详情订单列表、发货、订单备注、退款处理服务预约服务项目展示、选择门店/技师/时间、提交预约预约管理、排班设置、服务项目维护营销模块优惠券领取、下单抵扣优惠券发放、核销统计系统管理首页轮播图、公告展示轮播图配置、公告管理、管理员账号管理角色权限这块我没有简单用一张用户表加一个role字段糊弄过去而是用了 RBAC基于角色的访问控制模型。系统里规划了四类角色超级管理员、运营人员、美容师、普通用户。后端在做接口权限的时候用的方式是自定义注解 拦截器给需要鉴权的接口标注需要的角色拦截器统一校验当前用户角色。提示很多新手做权限控制喜欢在每个Controller方法里手写if判断写三五个接口还没什么感觉接口一多就全是重复代码。用注解加拦截器的方案后面扩展新接口只需要加一行注解维护成本低很多。2. 技术选型思路为什么是 Spring Boot SSM Vue2.1 先搞清 SSM 和 Spring Boot 的关系标题里同时出现“springboot”和“ssm”很多刚入门的人会困惑这俩不是两套东西吗其实不是。SSM 指的是 Spring、Spring MVC、MyBatis 这三个框架的整合使用而 Spring Boot 可以理解成对 Spring 生态体系的一次大幅简化封装。用 Spring Boot 搭出来的项目底层仍然是 Spring Spring MVC MyBatis 这套架构在跑。我打个比方SSM 就像给你一堆食材和菜谱需要自己开火、自己调火候、自己掌握下锅顺序Spring Boot 就像给了你一台智能料理机食材放进去、选好模式它自己把火候和顺序都处理好了但做出来的还是那道菜。所以“springboot ssm”这个组合在真实项目里通常指的是使用 Spring Boot 作为基础工程框架整合 Spring MVC 处理前端请求使用 MyBatis 操作数据库。这个组合的优势非常明显MyBatis 对 SQL 的控制力很强商城系统里各种多表联查、动态条件查询、分页统计用 MyBatis 写起来非常顺手。Spring Boot 自动配置解决了传统 SSM 项目里大量 XML 配置的问题开发效率提升明显。这套技术栈在国内中小型项目里的普及率极高遇到问题搜索答案非常容易。2.2 前后端分离架构下的核心机制这套系统我采用了完全的前后端分离架构前端 Vue 项目通过 HTTP 接口与后端交互。分离架构带来一个需要重点设计的环节接口规范。我定的统一返回结构是这样的{ code: 200, message: 操作成功, data: { } }code 用 200 表示成功404 表示资源不存在500 表示服务器异常401 表示未登录或 token 失效403 表示没有权限。前端 axios 响应拦截器会统一处理这些 code遇到 401 自动跳转登录页遇到 500 弹出错误提示。接口风格用的是 RESTful 设计比如GET /api/products 获取商品列表POST /api/products 新增商品PUT /api/products/{id} 修改商品DELETE /api/products/{id} 删除商品统一返回结构这件事看似简单但必须从一开始就定好。我见过不少项目有的接口返回 {code, msg, data}有的接口直接返回一个数组有的返回布尔值前端每次接数据都要看一遍后端代码联调效率极低。认证方案选择了 JWTJSON Web Token Token 拦截器。用户登录成功后后端生成一个 token 返回给前端前端存在本地存储里之后每次请求都在请求头里带上 Authorization 字段。后端拦截器从请求头取 token解析出用户 ID 和角色信息放到请求上下文里后面的接口直接用就行。3. 数据库设计与核心业务实现3.1 核心表结构设计数据库设计是整个项目最重要的一步我花的时间比写代码还多。核心表包括用户表、分类表、商品表、SKU表、购物车表、订单表、订单明细表、优惠券表、预约表。这里我把最关键的商品相关表结构和大家拆解一下。用户表CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录用户名, password varchar(100) NOT NULL COMMENT 密码BCrypt加密, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 角色0超管 1用户 2运营 3美容师, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;分类表CREATE TABLE category ( id bigint(20) NOT NULL AUTO_INCREMENT, parent_id bigint(20) NOT NULL DEFAULT 0 COMMENT 父分类ID0表示顶级, name varchar(50) NOT NULL COMMENT 分类名称, sort int(11) NOT NULL DEFAULT 0 COMMENT 排序值越小越靠前, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1启用 0禁用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品分类表;商品表CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(200) NOT NULL COMMENT 商品名称, category_id bigint(20) NOT NULL COMMENT 分类ID, main_image varchar(255) DEFAULT NULL COMMENT 主图, images text COMMENT 商品轮播图JSON数组, detail longtext COMMENT 商品详情, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1上架 0下架, sales int(11) NOT NULL DEFAULT 0 COMMENT 销量, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;SKU表CREATE TABLE sku ( id bigint(20) NOT NULL AUTO_INCREMENT, product_id bigint(20) NOT NULL COMMENT 商品ID, specs varchar(255) NOT NULL COMMENT 规格描述如 颜色:红色;容量:100ml, price decimal(10,2) NOT NULL COMMENT 售价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1启用 0禁用, PRIMARY KEY (id), KEY idx_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTSKU表;还有订单表、订单明细表、购物车表、优惠券表、预约表等结构上遵循的原则是一样的字段必加注释、主键用 bigint 自增、时间字段默认当前时间、需要查询的字段尽量建索引。3.2 多级分类与 SKU 库存商品分类我设计了 parent_id 自关联字段这样就能支持无限层级分类。比如“美妆护肤”是顶级分类下面可以有“面部护理”“身体护理”“面部护理”下面还可以有“洁面”“面膜”等子分类。前端在展示分类时通过递归方式把树状结构渲染出来。后端提供一个接口返回全部分类前端一次性拿到后自己构建树比每次展开都请求接口体验好很多。SKU 这个概念对没接触过的读者需要解释一下。美妆商品有个特点同一款产品往往有不同容量、不同色号比如一瓶精华有 30ml 和 50ml 两种规格价格和库存都不一样。如果每个规格都单独建一条商品记录后台管理会非常痛苦前台展示也很奇怪全部堆在一起像重复商品。SKU 的方案就是商品信息只维护一份规格相关的内容放到 sku 表里每个规格对应一条 sku 记录有自己的价格和库存。前端商品详情页会展示规格选择器用户选了某个规格后页面动态切换到对应 sku 的价格和库存。用户加入购物车时提交的是 sku_id 加数量这样购物车里的每个条目都精确对应一个具体规格的商品。3.3 订单流程与库存扣减订单模块是整个系统业务逻辑最复杂的地方。我设计的状态流转是待付款 - 待发货 - 已发货 - 已完成另外还有已取消和退款中两个分支状态。下单接口的核心逻辑按顺序执行以下步骤校验用户传入的购物车条目是否为空、每个商品是否上架、sku 是否存在。判断 sku 库存是否充足不足的条目直接返回错误提示。计算订单总金额验算前端传过来的金额与后端重新计算的结果是否一致。这一点必须做否则用户修改请求参数就可以用异常低价下单。生成订单号和订单记录批量插入订单明细。更新 sku 库存。删除对应的购物车记录。如果使用了优惠券同步核销优惠券并记录使用情况。这整个流程必须放在同一个事务里任何一步失败都要全部回滚。我在实现时直接用 Transactional 注解并指定回滚类型Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { // 业务逻辑 }库存扣减这里有个经典的并发问题两个用户同时买了同一个 sku此时库存只剩 1 件如果不做任何控制两个请求都可能先查询到库存充足然后都执行扣减最终库存变成负数出现超卖。解决办法是在扣减库存时使用带条件的 update 语句UPDATE sku SET stock stock - #{quantity} WHERE id #{skuId} AND stock #{quantity}这条语句执行后返回受影响行数如果为 0说明库存不足或商品已被其他请求扣完此时抛出库存不足异常事务回滚。这种方式效率高也不用引入复杂的分布式锁。4. 实操过程从零搭建前后端项目4.1 后端工程初始化与关键配置后端工程我直接用 IDEA 创建 Spring Boot 项目。这里有个常见问题必须先说直接用 IDEA 默认的 https://start.spring.io 创建项目经常超时尤其是网络环境不稳定的情况下等半天还是失败。解决方法是把创建项目的地址换成阿里云镜像https://start.aliyun.com在 IDEA 的 New Project 窗口把 Spring Initializr 的 URL 改成这个地址创建速度和成功率会明显提升。但要注意阿里云镜像上支持的 Spring Boot 版本可能比官方更新的慢一点我建议选择 2.7.x 系列这既兼容性稳定也符合绝大多数项目需求。Spring Boot 3.x 虽然已经出来了但它基于 JDK 17并且部分第三方 starter 的兼容性还需要时间沉淀对于以学习和稳定交付为目标的项目没必要追新版本。项目依赖方面我添加了 spring-boot-starter-web、spring-boot-starter-validation、mybatis-spring-boot-starter、mysql-connector-java、lombok、jjwt 等。这里有个细节MyBatis 的 starter 在较新版本里是mybatis-spring-boot-starter需要注意选择与 Spring Boot 版本匹配的版本号我用的是 2.3.1。application.yml 里的关键配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/beauty_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl关于 map-underscore-to-camel-case 这个配置一定要设置为 true。数据库字段都是下划线风格比如 created_time而 Java 实体类属性是驼峰风格比如 createdTime开启这个配置后 MyBatis 就能自动完成映射省去在 XML 里一个一个写 resultMap 的麻烦。还有个小细节可以提升开发体验项目开发阶段开启 SQL 日志输出logging: level: com.example.mapper: debug这样 MyBatis 执行的每一条 SQL 和参数都会在控制台输出排查问题会方便很多。但上线前记得关掉。4.2 前端 Vue 工程搭建与路由设计前端我选的是 Vue 3 Vite。创建项目的命令npm create vuelatest这个命令会生成一个基于 Vite 的 Vue 3 项目。执行过程中可以选择是否安装 Vue Router、Pinia、ESLint 等根据自己的需要勾选即可。前端项目依赖安装是个容易卡住的环节。如果 npm install 速度极慢或者直接卡死改成国内镜像源npm config set registry https://registry.npmmirror.com改完之后再执行安装速度会有质的提升。另外 npm 偶尔会出现缓存问题安装到一半报各种奇怪的错试试清理缓存npm cache clean --force项目目录结构我按功能做了划分src/ api/ // 接口请求封装按模块拆分 assets/ // 静态资源 components/ // 公共组件 router/ // 路由配置 store/ // Pinia 状态管理 utils/ // 工具函数axios 封装在这里 views/ // 页面视图路由这块有两个模式需要知道createWebHistory 和 createWebHashHistory。history 模式的 URL 更美观没有 # 号但刷新页面时如果后端没配置路径重写会出现 404。hash 模式 URL 里有 #不用后端配合但美观度差一些。本地开发时由于 Vite dev server 默认支持 history fallback所以直接用 history 模式没问题。部署到线上时如果用了 Nginx需要加一段配置把未知路径都重写到 index.htmllocation / { try_files $uri $uri/ /index.html; }我把这个配置写在部署小节方便线上部署时查阅。4.3 登录注册与 token 权限控制的完整实现登录逻辑是整个前后端联动最典型的模块我完整梳理一遍。后端部分用户注册时密码不存明文用 BCrypt 加密。Spring Security 里自带了 BCryptPasswordEncoder但为了不引入整套 Spring Security我直接使用了 spring-security-crypto 这个单独依赖只用到它的加密能力。注册接口核心代码PostMapping(/register) public ResultVoid register(RequestBody Valid UserRegisterDTO dto) { // 检查用户名是否已存在 if (userMapper.findByUsername(dto.getUsername()) ! null) { return Result.error(用户名已存在); } // 密码加密 String encodedPassword BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt()); User user new User(); user.setUsername(dto.getUsername()); user.setPassword(encodedPassword); user.setNickname(dto.getNickname()); userMapper.insert(user); return Result.success(); }登录时用 BCrypt.checkpw 校验密码。登录成功生成 JWT tokenpublic String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }token 有效期我设了 24 小时对商城这种使用频率来说比较合适太短了用户要频繁重新登录太长了安全风险增加。前端部分axios 封装里最关键的是请求拦截器和响应拦截器。请求拦截器负责从 Pinia store 里取 token 并放到请求头service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }, error Promise.reject(error) )响应拦截器统一处理业务状态码service.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } if (res.code 401) { // token 过期跳转登录页 router.push(/login) return Promise.reject(new Error(登录已过期)) } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )前端路由守卫用来控制页面访问权限。未登录用户只能访问登录页、注册页、商品浏览和详情页进入购物车、订单中心、用户中心这些需要登录的页面时会跳转到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这套 token 方案实现起来快也能满足大多数中小型系统的需求。它的局限性是 token 一旦在有效期内泄露别人可以冒用你的身份这就是为什么我强调生产环境必须使用 HTTPS。如果想更严谨可以做成双 token 机制accessToken refreshToken短 token 过期后通过刷新接口自动续期体验和安全都能兼顾但复杂度会上去一些后面有空我再单独讲。5. 常见问题与排查技巧实录5.1 环境与工程初始化问题这部分问题我在开发过程中全踩过一遍直接整理成速查表问题现象原因解决方案IDEA 创建 Spring Boot 项目超时默认 start.spring.io 访问不稳定改用阿里云镜像 https://start.aliyun.comMaven/ npm 依赖下载极慢使用了国外仓库换成阿里云 Maven 镜像或 npmmirrorSpring Boot 版本太高导致依赖冲突starter 之间版本不兼容使用 Spring Boot 2.7.x 稳定版本Maven 统一依赖版本管理MySQL 连接报时区错误jar 包或 JDBC 连接串未指定时区连接串加 serverTimezoneAsia/ShanghaiMyBatis 映射 XML 找不到mapper-locations 路径写错确认 classpath:mapper/*.xml 与实际路径一致这里重点提醒一下 Spring Boot 版本问题。Spring Boot 3.x 刚发布那阵子我尝试把一个旧项目升级上去结果因为个别第三方 starter 没有适配折腾了一整天最后还是放弃了。所以做项目应该追求稳定交付而不是新版本尝鲜。等一个版本出来一年以上生态成熟了再考虑升级。5.2 前后端联调与数据交互问题前后端分离之后的联调阶段问题的出现频率比想象中高很多而且很多问题看着像前端问题仔细排查却发现是后端配置没弄对。第一个高频问题是跨域。前端在 localhost:5173后端在 localhost:8080直接请求肯定被浏览器拦截。我后端的处理方式是通过配置类统一配置跨域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 换成实际的域名而不是用通配符这样更安全。第二个问题是请求参数解析失败。前端用 JSON 格式提交后端用 RequestBody 接收如果后端没配置 JSON 序列化或者前端 content-type 不对就会报 415 或 400。排查时我习惯先在浏览器开发者工具的网络面板里看下请求头确认 content-type 是 application/json然后看后端日志里具体是哪个字段解析失败。时间字段容易踩坑比如前端传的 2025-01-26 14:30:00 要能被正确解析需要在后端统一配置日期格式Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder - builder .serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))) .deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); } }第三个问题是前端 Vue Router 用了 history 模式部署到服务器后刷新页面就 404。这本质上是后端的静态资源服务器没有把前端路由的 URL 重写到 index.html。Nginx 加 try_files 就能解决前面配置也提到过。本地开发不会出现这个问题所以很多人是部署上线时才发现。5.3 安全加固与线上部署经验系统开发完成后我专门做了一轮安全加固有几个点很值得分享。第一个是关于 Spring Boot Actuator 的安全配置。Actuator 是 Spring Boot 提供的监控组件里面包含了一些敏感接口其中 /actuator/heapdump 这个端点可以直接把 JVM 堆内存 dump 下来而堆内存里可能包含数据库连接信息、用户会话、配置项等敏感内容。这在生产环境是非常危险的漏洞。我的处理方式是在配置文件中禁用这些敏感端点management: endpoints: web: exposure: include: health,info只暴露 health 和 info 两个端点用于健康检查其他全部关闭。如果你线上确实需要用到一些监控信息建议配合 Spring Security 做访问限制只允许内网或特定 IP 访问。第二个是数据库密码等敏感配置的加密。application.yml 里直接写数据库明文密码如果项目代码不小心上传到公共仓库或者服务器被入侵拿到配置密码就直接暴露了。我用的是 jasypt-spring-boot-starter配置里写成密文jasypt: encryptor: password: ${JASYPT_PASSWORD} spring: datasource: password: ENC(加密后的密文)解密密钥通过环境变量传入不写死在代码里。这样即使配置泄露没有密钥也无法反推出真实密码。第三个是文件上传的安全限制。商城系统需要上传商品图片、用户头像如果不对上传做限制攻击者可以上传恶意文件。我在后端做了三个层面的控制文件类型白名单校验、文件大小限制单文件最大 5MB、上传目录禁止执行脚本。文件路径使用 UUID 重命名避免路径穿越攻击。部署这块我最终的方案是后端打包成 jar直接运行java -jar xxx.jar用 systemd 或宝塔面板做进程守护。前端npm run build 生成静态文件部署到 Nginx 的 html 目录。数据库使用 MySQL 8.0线上开启 binlog定期自动备份。用宝塔面板部署的话整个过程可以在网页上点几下完成对新手环境很友好。但我自己实际维护的时候更倾向用命令行加 systemd 的方式启动日志、进程管理、故障恢复都更透明。还有一个自动建表的小技巧值得单独说一下。有段时间项目在测试环境和生产环境之间频繁切换手工维护 SQL 脚本会经常忘执行。后来我用 spring.sql.init 结合 schema.sql 做自动建表但这个方式对已有表不会重复建也不会动数据。如果团队用的是 MyBatis Plus还可以考虑 MyBatis Plus 代码生成器先建好表再生成代码路径更顺畅。注意生产环境我不建议让框架自动改表结构改表是有风险的最好通过专门的发布流程执行 SQL 脚本。从数据库建模、角色权限设计到前端路由守卫、SKU 库存扣减再到最后的 token 认证、安全加固整个项目做下来我最大的体会是这类系统真正的难点从来不是某个功能写不出来而是模块之间的耦合关系理不清。比如下单逻辑涉及用户、购物车、库存、订单、优惠券五个模块如果前面表的关联设计不清晰后面每一个接口都会写得很痛苦。如果让我再做一遍我会在项目启动初期把更多的精力放在接口文档上哪怕是手写一个简单的接口清单把所有接口的请求参数和返回结构提前定义好前后端并行开发时能省掉大量来回确认的时间。这一点在团队协作和接私活的场景下价值比多写几个功能还大。