跟婚纱预定系统这种业务项目打交道多了,你会发现一个很有意思的现象:单纯写CRUD的教程遍地都是,但把一个项目从零到一完整落地的过程,尤其是那些藏在代码背后的设计决策和部署坑点,反而没人愿意讲透。我自己做过几个前后端分离的业务系统之后,深感这种“看起来简单、做起来全是细节”的项目,才是最考验功力的。

这次借着一个民族婚纱预定系统的完整开发过程,我把整套技术方案、代码结构和部署思路都梳理一遍。这个项目用的是SpringBoot+Vue+MyBatis+MySQL这套经典组合,该踩的坑我都踩过了,该填的坑我也都填好了,你拿到源码之后能直接跑、能照着改造,也能从里面学到不少前后端分离项目的实战套路。不管你是准备做毕业设计,还是刚进公司要接手类似的管理系统,这篇内容都值得你花点时间看完。

1. 项目定位与技术选型思路

1.1 这套系统到底解决什么问题

民族婚纱预定系统,本质上是一个面向特定人群(少数民族婚礼场景、传统服饰租赁/定制商家)的预约管理系统。它要管的不是“婚纱好看不好看”这种审美问题,而是实实在在的业务流:用户浏览服饰、查看档期、提交预定、商家确认订单、后台管理服饰库存和订单状态。

所以核心需求拆下来就是三块。第一,前台展示与预定,用户能按民族风格、适用场景、价格区间筛选服饰,查看详情和可预约时间;第二,订单全流程管理,包括提交预定、支付占位(或者线下支付占位)、商家审核、订单状态流转;第三,后台管理,服饰信息维护、分类管理、轮播图配置、订单处理、用户管理。这三块合在一起,就是一个典型的“C端展示+B端管理”双端系统。

这种系统的难点不在于某个功能单点有多难写,而在于前后端如何分工、状态如何流转、数据如何组织。

1.2 为什么选SpringBoot+Vue+MyBatis这套组合

很多人问我,现在新项目为什么不上Spring Cloud微服务?为什么不用MyBatis-Plus或者JPA?我的回答很简单:看业务复杂度。

这个项目的数据量级、并发规模和团队协作方式,完全不需要微服务那一套。SpringBoot单应用就能搞定所有后端接口,部署运维成本低,本地跑起来也快。Vue做前端SPA,前后端通过JSON交互,天然契合这种管理类系统的开发节奏。至于MyBatis,它的优势是SQL可控,复杂查询可以手动优化,特别适合这种业务表结构相对固定、查询逻辑五花八门的系统。MySQL则是久经考验的关系型数据库,存储订单、用户、服饰这类结构化数据非常合适。

说实话,这个组合现在看起来“传统”,但它最大的价值是:技术栈足够通用,网上资料多,招人也好招。你在牛客上搜SpringBoot面试题,搜出来的高频考点就是这套东西。把这个项目吃透了,面试聊项目经验的时候非常有料。

2. 核心功能模块与设计思路拆解

2.1 用户端的三个关键页面

用户端最核心的页面是服饰列表页、服饰详情页和预定下单页。

列表页要解决的核心问题是“筛选和分页”。民族服饰有很多维度——民族(藏族、苗族、蒙古族等)、场景(婚礼、写真、仪式)、价格区间。如果只用MySQL的 LIKE 查询,一旦数据量上来,接口响应会很慢。所以我在设计时采用了动态SQL拼接,根据前端传来的筛选条件组合查询语句,同时用PageHelper做物理分页。

这里有个细节值得说:服饰列表的封面图,不要用数据库的 BLOB 字段直接存图片二进制,而是存图片URL路径。图片文件走独立的上传接口,落盘到服务器本地,数据库只记路径。这样做的好处是前端 <img> 标签直接绑定路径就能显示,不需要额外写二进制流转换接口,而且后续迁移到OSS也方便。

详情页要处理的则是“图片展示+档期查询”。一个民族婚纱套系往往有五六张展示图,数据表设计成主表(服饰基本信息)+子表(图片列表)的方式,查询时用一条SQL把主表和子表数据联合查出来,组装成带图片列表的VO对象返回给前端。

下单页的核心是“防重复预定”。用户选好服饰、填了婚期、提交预定,后端必须校验这条档期是否已被别人占用。我的做法是在数据库层面加唯一约束,用 occasion_date (档期日期)+ dress_id (服饰ID)作为联合唯一索引。这样就算两个用户同时提交,数据库也会拒绝其中一条,比单靠代码判断靠谱得多。

2.2 管理后台的功能划分

管理后台我按角色权限拆成两块:管理员和运营人员。

管理员负责用户管理、角色分配、系统配置。运营人员负责服饰管理、订单审核、轮播图维护。权限控制这块,SpringBoot集成Spring Security或者Sa-Token都可以。我这个项目用的是基于Token的认证方案——前端登录后拿到Token,之后每个请求都在Header里带上Token,后端通过拦截器统一校验身份和权限。

后台界面我用的是Vue+Element UI。Element UI的表格组件、表单组件、对话框组件都是现成的,写后台管理页面效率非常高。比如服饰管理页面,一个 el-table 绑定列表数据, el-dialog 里放表单,提交后刷新列表,这一套组合拳能覆盖80%的后台页面场景。

2.3 为什么强调“前端分离”这个关键词

“前后端分离”这个关键词现在被说烂了,但很多人其实没搞明白它到底分离了什么。

分离的不仅仅是代码仓库和开发人员,更重要的是 交互协议 。前端不再通过服务端模板(比如JSP、Thymeleaf)拿数据,而是通过HTTP接口拿JSON。这意味着前端可以独立部署(Nginx或Node服务器),后端可以独立部署(Tomcat或Docker容器),两边只要约定好接口文档,就可以并行开发。

这个模式有个必须处理的痛点就是跨域。前端跑在8080端口,后端跑在9090端口,浏览器默认会拦截跨域的Ajax请求。解决办法有两个:后端配置 @CrossOrigin 注解或CORS全局配置;或者通过Nginx反向代理,把 /api 路径转发到后端服务。生产环境我推荐用Nginx方案,因为前后端同源之后,连Cookie、Token传递都不用额外处理。

3. 数据库设计与后端核心实现

3.1 四张核心表的结构设计

整个系统的数据库我用了一个很克制的设计,核心业务表就四张:用户表、服饰表、订单表、图片表。

用户表( sys_user )字段包括 id 、 username 、 password 、 nickname 、 avatar 、 role 、 create_time 。密码用BCrypt加密存储,绝对不允许明文入库。角色字段用字符串标识,比如 ADMIN 、 OPERATOR 、 CUSTOMER ,简单直白。

服饰表( dress )字段包括 id 、 name 、 ethnic_type 、 scene_type 、 price 、 description 、 status 、 cover_image 。 ethnic_type 存民族分类,比如“藏族”“苗族”, scene_type 存适用场景,比如“婚礼”“写真”。这两个字段在列表页的筛选中起着关键作用。

订单表( order_info )字段包括 id 、 order_no 、 user_id 、 dress_id 、 occasion_date 、 contact_name 、 contact_phone 、 status 、 remark 、 create_time 。 order_no 用时间戳+随机数生成,保证唯一,方便后续对账。 status 用整数表示,0待审核、1已确认、2已取消、3已完成。

图片表( dress_image )字段包括 id 、 dress_id 、 image_url 、 sort_order 。这个表主要解决一件服饰多张展示图的问题, sort_order 用来控制前端展示顺序。

这四张表的关联关系很清晰:用户1对多订单,服饰1对多订单,服饰1对多图片。不需要复杂的中间表,也不需要过度设计,够用且好理解。

3.2 后端接口设计规范

后端接口我统一采用 /api 前缀。比如 /api/user/login 、 /api/dress/list 、 /api/order/create 、 /api/order/list 、 /api/admin/dress/add 。统一前缀的好处是前端封装的Axios实例可以统一配置 baseURL ,Nginx反向代理也只需要转发一个路径。

返回格式也做了统一封装。写了一个 Result 类,结构是 {code: 200, message: "success", data: {...}} 。成功时 code 为200,业务异常时 code 为500或自定义错误码,前端Axios拦截器统一判断,不是200就弹错误提示。

Token认证这块我用的是JWT。用户登录成功后,后端生成一个有效期为24小时的JWT返回给前端。前端把它存在 localStorage 里,每次请求在Axios请求拦截器中添加:

service.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers['Authorization'] = token
  }
  return config
})

后端收到请求后,拦截器先解析Token,如果Token无效或过期,直接返回401状态码。前端Axios响应拦截器看到401,自动跳转到登录页。这套流程就是热词里说的“vue前后端分离请求token处理”,实际跑起来非常丝滑。

3.3 MyBatis动态SQL的几个关键写法

MyBatis是这个项目的数据访问层核心,动态SQL用得好不好,直接决定代码优雅不优雅。

服饰列表查询是我重点展示的部分。因为前端传来的是不确定的筛选条件——可能只传了民族,可能只传了场景,可能什么都没传。如果为每种情况写一个SQL方法,代码会膨胀到没法维护。动态SQL的 <where> 标签完美解决了这个问题:

<select id="selectDressList" resultType="com.example.entity.Dress">
  SELECT * FROM dress
  <where>
    <if test="ethnicType != null and ethnicType != ''">
      AND ethnic_type = #{ethnicType}
    </if>
    <if test="sceneType != null and sceneType != ''">
      AND scene_type = #{sceneType}
    </if>
    <if test="keyword != null and keyword != ''">
      AND name LIKE CONCAT('%', #{keyword}, '%')
    </if>
    AND status = 1
  </where>
  ORDER BY create_time DESC
</select>

<where> 标签会自动处理掉第一个 AND ,不会出现 WHERE AND 这种SQL语法错误。这也是MyBatis的一个高频考点,面试时经常被问到。

订单创建接口就必须加事务控制。创建订单时要做两件事:插入订单记录、更新服饰状态。如果只插入订单而更新状态失败,数据就不一致了。所以我在Service层加了 @Transactional 注解,任何一步异常都能回滚。这里插一句, @Transactional 有几个容易踩的坑:默认只回滚 RuntimeException ,如果是自定义异常需要指定 rollbackFor ;同类内部调用会让代理失效,事务不生效。这些都值得单独写一篇细说,但在这个项目里,我直接用最简单的方式——把事务方法放在Controller调用的Service实现类里。

我在这个项目的数据库脚本中,还专门为订单表加了一个状态索引:

CREATE INDEX idx_order_status ON order_info(status);

订单状态流转查询是高频操作,加了这个索引之后,后台订单列表页的响应速度会明显提升。

4. 前端实现与项目部署全流程

4.1 Vue工程结构与路由配置

前端工程我基于Vue CLI(Webpack)搭建,目前Vue 2.6依然是我在这个项目中偏稳妥的选择——组件生态丰富,Element UI适配度高,相关的坑早就被填平了。工程结构如下:

src/
  api/           // 封装所有接口请求
  assets/        // 静态资源
  components/    // 公共组件
  router/        // 路由配置
  store/         // Vuex状态管理
  views/         // 页面组件
    admin/       // 后台管理页面
    user/        // 用户端页面
  App.vue        // 根组件
  main.js        // 入口文件

路由配置里需要区分用户端和管理后台。用户端页面放在 / 路径下,比如 / 、 /dress/:id 、 /order/create 。管理后台统一挂在 /admin 路径下,并且配置路由守卫,未登录或非管理员角色访问时跳转到登录页。

router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')
  if (to.path.startsWith('/admin') && !token) {
    next('/login')
  } else {
    next()
  }
})

这里有个细节想提醒:用户端和管理后台最好用不同的Layout组件。用户端是导航栏+内容的布局,管理后台是侧边栏+顶部栏+内容区的布局。不要混用一套,后期调整起来非常痛苦。

4.2 Axios封装与接口调用

Axios封装是前端工程的基础设施。我的做法是建一个 request.js 文件,导出配置好的Axios实例,所有API请求都基于这个实例发出。

import axios from 'axios'

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_URL || '/api',
  timeout: 10000
})

service.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 200) {
      this.$message.error(res.message)
      return Promise.reject(new Error(res.message))
    }
    return res.data
  },
  error => {
    if (error.response && error.response.status === 401) {
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

这样封装之后,业务代码里调用接口就非常清爽。比如服饰详情页:

getDressDetail(id).then(data => {
  this.dress = data
  this.imageList = data.imageList
})

不用在每个页面重复处理错误提示和登录失效问题,一套拦截器全部搞定。

关于Vue项目中播放m3u8视频的需求,我在这个项目的服饰展示模块里也有涉及。因为部分高端婚纱套系会提供试穿视频或动态展示视频,视频源格式是m3u8。处理方案是前端引入 hls.js 库,在Vue组件里动态创建 <video> 标签并加载m3u8流。这个方案在PC端兼容性很好,移动端的话可以优先用原生 <video> 标签配合 x5-playsinline 属性处理。

4.3 完整部署流程

部署是我要详细展开的部分。很多人项目写完了,部署却卡住,我能理解那种感觉。所以我整理了一套相对完整的流程,从环境准备到正式上线。

第一步,环境准备。

需要准备一台Linux服务器(我这边测试用的是CentOS 7.9)。安装JDK 1.8、MySQL 5.7、Nginx,这些直接用包管理器安装即可。Node.js环境用于前端打包,版本要注意,Vue CLI项目用Node 14或16都比较稳。

第二步,初始化数据库。

把项目自带的 dress_booking.sql 脚本上传到服务器,执行:

mysql -u root -p < dress_booking.sql

脚本里建好了库和表,还有一个默认管理员账号 admin/admin123 。用Navicat或命令行检查一下表是否创建成功。

第三步,修改后端配置并打包。

找到 application.yml ,修改数据库连接信息:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dress_booking?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 你的数据库密码

然后执行Maven打包命令:

mvn clean package -DskipTests

打包完成后, target 目录下会生成一个 dressbooking-0.0.1-SNAPSHOT.jar 。

第四步,启动后端服务。

直接使用 java -jar 命令启动,搭配 nohup :

nohup java -jar dressbooking-0.0.1-SNAPSHOT.jar > app.log 2>&1 &

启动之后看日志确认没有报错。测试接口是否可以访问:

curl http://localhost:9090/api/dress/list

能返回JSON数据就说明后端没问题。

第五步,构建前端并配置Nginx。

在前端工程目录执行:

npm install
npm run build

构建完成后, dist 目录就是前端静态文件。把它上传到服务器的 /usr/share/nginx/html/dress 目录,然后修改Nginx配置:

server {
    listen       80;
    server_name  your_domain.com;

    location / {
        root   /usr/share/nginx/html/dress;
        index  index.html;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:9090/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

注意 try_files $uri $uri/ /index.html 这行,它的作用是处理Vue Router的history模式。如果不加这行,刷新某个子页面路由时,Nginx会返回404。 /api 反向代理配置则是把前端的API请求转发到后端Java服务,解决跨域问题。

第六步,重启Nginx并验证。

nginx -t
nginx -s reload

浏览器访问服务器IP,能看到系统首页,登录、查询、下单流程都跑通,部署就算完成了。

5. 常见问题与排查技巧实录

5.1 数据库自动断开导致的“服务不可用”

这个问题我在这个项目里遇到过。MySQL的 wait_timeout 默认是8小时,如果长时间没有请求,MySQL会自动断开空闲连接。数据库连接池里的连接还在被使用,但MySQL已经断开了,就会出现连接异常。

排查思路其实很直接:启动项目后一切正常,但过了一晚上再访问,接口报错。看后端日志,关键报错是 Communications link failure 或者 Connection is not available, request timed out 。

解决办法有两个方向。一是给连接池配置合适的参数,比如HikariCP(SpringBoot默认连接池)可以设置:

spring:
  datasource:
    hikari:
      max-lifetime: 1800000
      idle-timeout: 600000

二是调整MySQL的 wait_timeout ,把它改大为28800秒(8小时)以上。我两个都改了,目前运行稳定。

5.2 “端口被占用”引发的部署失败

第一次部署时,后端启动失败,报 Port 9090 was already in use 。排查方法是:

lsof -i :9090
kill -9 PID

然后重新启动SpringBoot服务。这个操作很基础,但新手经常被卡在这里,值得单独提一下。如果是多次部署,建议用脚本统一管理启动、停止、重启操作,避免每次都手动找PID。

5.3 Vue打包后布局异常

“vue 打包后布局异常”也是热词里出现频率很高的一个问题。开发环境跑得好好的, npm run build 之后,CSS错乱、图片不显示。这个大概率是静态资源路径问题。需要在 vue.config.js 里设置 publicPath 为相对路径:

module.exports = {
  publicPath: './'
}

或者干脆用绝对路径 /dress/ ,配合Nginx的root配置来使用。这个坑很容易踩,尤其是部署到子目录时更明显。

5.4 MyBatis字符串比较的“隐形坑”

热词里有个“mybatis 单个数字字符比较”,这个坑我在项目中实际遇到过。在XML里写条件判断时,比如判断订单状态是否为某个值:

<if test="status == 1">
  AND status = 1
</if>

此时 status 是前端传过来的整数 1 ,没问题。但如果是字符串类型的参数,比如 '1' , status == '1' 这种写法在某些MyBatis版本中会触发数字开头的字符串比较问题。解决方法是统一用 status == '1'.toString() ,或者干脆在入参时就转为Integer类型,避免使用单引号包裹数字比较。

5.5 SpringBoot版本选择引发的连锁问题

热词里有“springboot版本太高”这个表述,说明很多人在版本选择上吃过亏。SpringBoot 3.x对JDK版本有要求,必须JDK 17以上,而且很多第三方组件的兼容性还没完全跟上。如果你本地是JDK 8,就不要强行用SpringBoot 3.x,老老实实用SpringBoot 2.7.x。这个项目我选的就是2.7.6版本,稳定且兼容面广。

这里给大家一张简单的版本对照表:

JDK版本 推荐SpringBoot版本 说明
JDK 8 2.5.x ~ 2.7.x 主流稳定版,资料多
JDK 11 2.7.x 兼容性好
JDK 17 3.0.x+ 新特性多,但需确认生态兼容

5.6 前端请求BaseURL如何按环境切换

开发时前端走 http://localhost:9090 ,部署时走Nginx的 /api ,实现方式是 .env.development 和 .env.production 两个环境变量文件:

// .env.development
VUE_APP_BASE_URL = 'http://localhost:9090'

// .env.production
VUE_APP_BASE_URL = '/api'

打包时Vue CLI会自动读取对应的环境变量。这样代码里只需要写 axios.create({ baseURL: process.env.VUE_APP_BASE_URL }) ,不同环境下自动切换。

6. 项目后续扩展方向与个人经验总结

6.1 这个项目还能怎么玩

如果想把项目做得更完整,或者说想在面试时有更多亮点可说,我建议在下面几个方向选一个做延伸。

支付模块是当前项目的一个明显短板。目前订单只是预约占位,没有真正接入支付。可以接微信支付H5+Native支付,在用户提交订单后生成支付二维码,支付完成后通过回调更新订单状态。这个功能一加上,整个系统的商业闭环就完整了。集成微信支付官方SDK,文档写得很详细,照着流程走一遍基本能通。

消息通知也可以做一下。用户提交预定后,给管理员发送通知;管理员审核后,给用户发送通知。如果不想引第三方推送服务,最简单的做法是在系统内部做个站内信表,用户在消息中心查看通知,不用搞复杂了,但能体现你对业务流程的思考。

还有权限细化。现在只有Admin和User两种角色,可以扩展成RBAC模型,引入角色表、菜单表、权限表,用Spring Security的注解控制接口权限。

6.2 写代码之外我说句实在话

这个项目让我最满意的不是代码本身,而是整套方案是可复制、可迁移的。技术选型不花哨,但每一块都能讲清楚为什么这么选;业务流程不复杂,但每个环节的前后端交互都考虑到了;部署方式很简单,但生产环境下遇到的问题也都覆盖到了。

我在实际开发中体会很深的一点是:很多人学SpringBoot,天天背面试题,什么自动配置原理、Bean生命周期,倒背如流,但真让他从零搭一个能跑的前后端分离项目,他会卡在跨域上,卡在Token校验上,卡在Nginx配置上。这个项目就是用来填平“理论”和“实践”之间的鸿沟的。

最后再分享一个小技巧:拿到任何一套前后端分离项目源码,不要急着跑起来,先看数据库脚本,把表结构理清楚;再看后端接口文档或Controller层,搞清楚有哪些接口、每个接口做什么;然后看前端路由和API封装,理解页面和接口的对应关系;最后再启动项目,把核心流程走一遍。按这个顺序走下来,一套陌生的系统你一天之内就能上手。做项目,慢就是快,理顺了再动手,比盲目改代码高效得多。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐