资讯动态

SpringBoot+Vue+MyBatis+MySQL蛋糕店管理系统实战开发

发布时间:2026/9/10 16:52:46 来源:尧图企业网站定制
1. 这个蛋糕店项目的前期规划技术选型怎么定先说结论如果你正打算做一个中小型的管理系统——可能是毕设、个人项目、接单交付甚至是给亲戚的蛋糕店做个线上售卖系统——2025年这个时间点上SpringBoot Vue MyBatis MySQL这套组合依然是国内最靠谱的起步方案。不是因为它新恰恰因为它成熟踩坑成本低资料多遇到问题一搜就有答案。我最早接触这类项目是几年前那时候SpringBoot才到2.xVue还是2.x的天下MyBatis还在跟通用Mapper、tk.mybatis这些库纠缠。到了2025年SpringBoot 3.x已经普及JDK 17甚至21成为基准Vue 3 Vite成了前端标配MyBatis这边虽然MyBatis-Plus用的人越来越多但原生MyBatis依然有大量的存量项目。标题里既然写明是MyBatis MySQL那这篇文章就按这套主栈来讲顺带会提一些与MyBatis-Plus的差异方便你理解为什么有人用原生注解有人用XML。1.1 这套系统到底解决什么问题网上蛋糕售卖店管理系统名字听着长业务其实很清晰一个围绕“蛋糕售卖”的线上小程序/网页系统。用户进来看蛋糕、搜分类、下订单管理员在后台维护蛋糕信息、处理订单、管理用户。核心业务实体就四个用户、蛋糕、订单、订单明细。顶多加一个购物车或者收藏功能。所以这是一个典型的双端管理系统——前台面向顾客后台面向店长/管理员。你不需要把它想成什么高并发、微服务、分布式的东西它就是一个单体应用。单体应用恰恰能让你把注意力放在业务功能本身而不是放在服务治理上。这个定位非常重要因为很多初学者一上来就想上Redis、RabbitMQ、Nacos结果项目还没开始先被中间件的配置搞崩溃了。1.2 为什么是SpringBoot Vue而不是别的组合我们来横向对比一下2025年常见的替代选择组合方案适用场景学习成本生态资料SpringBoot Vue中小型管理系统、Web应用中等极多SpringBoot React更复杂的前端交互、组件化程度高中高多Node.js (Express/Koa) Vue轻量接口、前端为主低多Python Django Vue快速原型、AI相关模块低较多对蛋糕店这种业务来说SpringBoot的强项在于事务管理、依赖注入、生态成熟尤其是订单模块涉及金额和时间Spring的声明式事务用起来非常顺手。Vue的强项在于渐进式上手从数据绑定到组件化再到状态管理路径非常清晰模板语法对后端出身的人也很友好。MyBatis这个选择有点微妙它不像JPA/Hibernate那样帮你省掉大部分SQL相反它要求你写SQL——但你获得的是对SQL的绝对掌控。做蛋糕店系统蛋糕列表的筛选条件、订单统计的报表查询、组合条件动态SQL这些都是MyBatis的强项。你不需要跟Hibernate的一级缓存、二级缓存、N1问题较劲专心把SQL写好就行。MySQL就不用多说了关系型数据事务支持部署方便文档满天飞。对于这个量级的系统不需要考虑分库分表一套InnoDB默认配置就够用。1.3 适合谁来学、怎么用这套源码如果你是非科班转行、在校学生、刚工作一两年的后端开发这套源码的经验迁移价值很高。因为蛋糕店系统的业务复杂度和真实企业项目非常接近——有用户的注册登录、有商品的分类检索、有带事务的订单流程、有后台权限控制。你把这一套吃透去看电商、购物、零售类的项目很多套路都是相通的。怎么学我的建议是第一遍跑通第二遍改需求第三遍自己重写。先把它跑起来看数据怎么流动然后在“分类管理”里加个排序字段在“订单列表”里加个按日期筛选改完你就知道哪里动了前端、哪里动了后端、哪里动了SQL。最后如果你真想做毕设或者简历项目试着不参考源码自己重写一遍写不出来的地方再回头看——这个过程比你看十遍都有效。接下来我按项目落地的顺序从前到后把每个环节拆开讲。2. 数据库设计不是堆表从业务到表结构的推导很多初学者拿到“蛋糕店管理系统”这个需求第一反应是建表用户表、蛋糕表、订单表……然后就不知道该怎么细化或者反过来一顿操作建了十几张表字段生怕不够多。这两种极端都不对。数据库设计的核心不是“建几张表”而是“把业务规则翻译成数据约束”。2.1 理清业务角色和核心流程吃透需求之前先定角色。蛋糕店系统通常有三类角色顾客、管理员、超级管理员。顾客能做的事注册登录、浏览蛋糕、按分类筛选、加入购物车、提交订单、查看自己的订单状态。管理员能做的事管理蛋糕分类、管理蛋糕信息、上下架、修改库存、处理订单状态发货、完成、取消。超级管理员一般就是管管理员账号这个在中小型系统里可以简化成一张用户表加一个角色字段。核心流程是两条线一条是顾客下单流——选蛋糕→加购→提交订单→支付真实系统有支付这里通常模拟/跳过→管理员发货→顾客确认另一条是管理员维护流——录入蛋糕→设置价格库存→上架→订单处理。2.2 表结构的构建与字段解释基于上述流程我设计五张核心表表名用途核心字段user用户表id, username, password, role, nickname, phone, avatar, create_timecategory蛋糕分类表id, name, sort, create_timecake蛋糕信息表id, category_id, name, description, price, stock, image, status, sales, create_timecart购物车表id, user_id, cake_id, quantity, create_timeorders订单表id, order_no, user_id, total_amount, status, address, receiver_name, receiver_phone, create_time, pay_time, ship_timeorder_item订单明细表id, order_id, cake_id, cake_name, cake_image, price, quantity这里有几个容易想不清楚的点我给你逐个说明为什么订单表里要冗余蛋糕名称和图片因为蛋糕的价格和名称是可能变动的。如果订单明细表只存cake_id你关联查询出来的就是“当前”的名称和价格而不是“下单时”的名称和价格。比如前天下单时蛋糕是99元今天涨价到129元顾客查看历史订单时就会显示129元这是绝对不允许的。所以订单明细里必须冗余下单时的快照——名称、价格、图片。这个设计思路在电商系统里叫“交易快照”蛋糕店虽然小但这个坑千万别踩。权限控制怎么做很多人第一反应是建一个角色表、一个权限表、再来一个用户角色关联表、角色权限关联表——RBAC那一套。但蛋糕店系统真的需要吗如果只有“顾客”和“管理员”两种角色直接在user表加一个role字段就够了0表示普通用户1表示管理员2表示超级管理员。中间表是给角色灵活变化的场景用的角色一旦固定用字段就是最简洁的方案。购物车表为什么不设计成“购物车主表购物车明细表”因为现实业务里一个顾客一般只有一条购物车记录吗不是一辆车里多条明细。严格来说cart表应该只存“谁的购物车”另建cart_item表存“哪个蛋糕几个”。但蛋糕店这种轻量场景一个用户最多也就买那么几样直接把user_id cake_id组合放在cart表里当明细用反而简单。只要加一个唯一索引(user_id, cake_id)就能保证同一用户不会重复加入同一个蛋糕。如果需要支持多选结账查询时where user_id ?即可。2.3 索引策略别让订单表变成慢查询重灾区蛋糕店系统的数据量说实话再怎么卖也很难到百万级。但是订单表是全系统查询最频繁的表建议给它建这几个索引user_id支撑“我的订单”列表status支撑管理员按状态筛选订单create_time支撑按时间查询order_no唯一索引这不仅是业务需要订单号不允许重复在按单号查询时也能直接命中蛋糕表的category_id建普通索引支撑分类筛选。price字段除非你要做价格排序和范围查询否则一般不单独建索引。索引不是越多越好每多一个索引写入时多一次维护成本。以蛋糕店的写入频率其实随便建都无所谓但从学习习惯上说按需建索引更专业。3. 后端开发SpringBoot集成MyBatis的核心实现后端是整个系统的中枢做得好不好直接决定接口好不好调、扩展性够不够。这一节我不贴整个项目代码那太多我挑核心的部分拆开讲项目结构怎么组织、配置文件怎么写、MyBatis怎么和SpringBoot融合、动态SQL在哪发挥价值、事务怎么做。3.1 项目结构与分层思想后端我用标准的四层结构com.example.cake ├── config // 配置类跨域、拦截器配置 ├── controller // 控制层接收请求返回结果 ├── service // 业务层核心业务逻辑 │ └── impl // 业务实现类 ├── mapper // MyBatis的数据访问层接口XML ├── entity // 实体类对应数据库表 ├── dto // 数据传输对象用于接收前端参数、返回前端数据 ├── vo // 视图对象给前端展示用的组合数据 ├── common // 通用类Result封装、异常处理、常量 └── utils // 工具类分层的思想很简单controller只做参数接收和结果返回不写业务逻辑service写业务逻辑事务注解打在这里mapper只做SQL操作。很多人觉得分层麻烦——一个查询而已controller里直接调mapper不行吗行但一旦逻辑复杂起来比如下订单要扣库存、生成订单号、插入订单明细三个操作你还写在controller里那controller就变成了一个谁也不想碰的“垃圾场”。我的建议是即便项目不大也老老实实分层。以后你进公司看任何Java项目基本都是这个结构提前养成习惯后面适应得快。3.2 application.yml配置细节SpringBoot的配置文件2025年推荐用YAML格式。重点看几个容易出问题的配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cake_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.cake.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这几个配置你逐个看driver-class-name必须用com.mysql.cj.jdbc.Driver这是MySQL Connector/J 8.x之后的驱动类。如果你在网上看到旧教程写com.mysql.jdbc.Driver那是在MySQL 5.x时代的东西用在新版本上会直接报ClassNotFoundException。url后面那一长串参数useSSLfalse解决本地SSL握手警告serverTimezoneAsia/Shanghai解决时区问题不设置会报Server returns invalid timezone错误allowPublicKeyRetrievaltrue解决MySQL 8.x的public key retrieval错误——这三个是本地开发最常见的启动报错源头如果你连接MySQL 8.x这三个参数一个都不能少。map-underscore-to-camel-case: true这个配置非常实用它能让数据库字段user_id自动映射到实体类的userId属性不用在XML里写resultMap和每个字段的映射。前提是数据库字段用下划线命名Java属性用驼峰命名大部分人建表都符合这个规范所以直接开就好。log-impl改成StdOutImpl开发环境就能在控制台看到SQL日志。这是排查MyBatis问题最重要的工具——先看SQL对不对再看参数绑没绑上最后看结果映射有没有问题。3.3 用户登录接口从Controller到Mapper的完整链路以用户登录为例看一个请求从前到后的路径。先定义统一的返回结果类ResultData public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }这个类用泛型方便返回任意类型数据。所有接口返回Result前端统一按code判断成功失败不用每个接口各搞一套。Controller层RestController RequestMapping(/api/user) public class UserController { Resource private UserService userService; PostMapping(/login) public ResultUserVO login(RequestBody LoginDTO loginDTO) { UserVO userVO userService.login(loginDTO); return Result.success(userVO); } PostMapping(/register) public ResultVoid register(RequestBody RegisterDTO registerDTO) { userService.register(registerDTO); return Result.success(null); } }Service层Service public class UserServiceImpl implements UserService { Resource private UserMapper userMapper; Override public UserVO login(LoginDTO loginDTO) { // 1. 参数校验略 // 2. 根据用户名查找用户 User user userMapper.selectByUsername(loginDTO.getUsername()); // 3. 校验用户是否存在和密码 if (user null || !user.getPassword().equals(DigestUtils.md5DigestAsHex(loginDTO.getPassword().getBytes()))) { throw new RuntimeException(用户名或密码错误); } // 4. 转换为VO返回 UserVO userVO new UserVO(); BeanUtils.copyProperties(user, userVO); return userVO; } }密码我用了MD5做演示实际项目中更推荐加盐哈希Spring Security的BCrypt这里为了简化就不引入额外的安全框架了——毕竟蛋糕店系统更重要的是先跑通业务。Mapper层Mapper public interface UserMapper { User selectByUsername(Param(username) String username); }对应的XML文件select idselectByUsername resultTypecom.example.cake.entity.User SELECT * FROM user WHERE username #{username} /select你可能好奇为什么不用注解Select(SELECT * FROM user WHERE username #{username})注解确实更简洁但MyBatis的XML文件能让你写动态SQL的时候更方便——where标签、if标签、foreach标签这些都是注解里写起来很痛苦的东西。我的习惯是简单查询用注解涉及动态条件的查询用XML。但一个项目里风格最好统一蛋糕店系统里面动态查询不少所以我还是推荐全部走XML一致性更好。3.4 动态SQL的价值蛋糕列表的多条件筛选蛋糕管理后台最常见的一个功能就是多条件筛选按分类查、按价格区间查、按名称模糊查、按上下架状态查。四个条件任意组合如果用注解写SQL你会疯掉的——要拼字符串还要处理“没有条件时不能多一个WHERE”这种边界问题。MyBatis的where标签专门解决这个select idselectCakeList resultTypecom.example.cake.vo.CakeVO SELECT c.*, cat.name as category_name FROM cake c LEFT JOIN category cat ON c.category_id cat.id where if testcategoryId ! null AND c.category_id #{categoryId} /if if testminPrice ! null AND c.price gt; #{minPrice} /if if testmaxPrice ! null AND c.price lt; #{maxPrice} /if if testname ! null and name ! AND c.name LIKE CONCAT(%, #{name}, %) /if if teststatus ! null AND c.status #{status} /if /where ORDER BY c.create_time DESC /select这里的where标签会自动处理两种情况一是有条件时自动在前面加WHERE二是第一个条件前面如果带着AND它会自动把AND去掉。你不需要担心用户传的参数不完整也不用担心SQL语法出错。注意大于号和小于号在XML里必须转义成gt;和lt;否则XML解析会报错这是很多人刚开始写MyBatis时最容易卡住的地方。3.5 订单事务一张订单背后的原子性下单流程要同时做三件事生成订单记录、插入订单明细、扣减蛋糕库存。这三件事要么全部成功要么全部失败——比如扣库存扣完但订单没生成那库存就凭空少了。这就是事务的应用场景。SpringBoot里加事务非常简单在Service方法上打一个Transactional注解Transactional(rollbackFor Exception.class) Override public OrderVO createOrder(CreateOrderDTO dto) { // 1. 根据用户id和购物车数据生成订单略 // 2. 插入订单记录 // 3. 批量插入订单明细 // 4. 更新蛋糕库存 return orderVO; }rollbackFor Exception.class意味着任何异常发生整个事务就回滚。很多人写事务容易犯一个错只在查询方法上打注解或者忘了处理异常。Transactional默认只在RuntimeException和Error时回滚checked exception不会触发回滚所以显式指定rollbackFor是专业习惯。库存扣减还有一个并发问题两个人同时下单库存只有1个可能两个人都扣成功了。简单解决方案是用乐观锁——在cake表加一个version字段更新时带上WHERE version #{version}更新成功才说明抢到了库存。这一步在做演示项目时不一定非要做但知道有这个问题跟面试官聊起来会加分不少。4. 前端开发Vue视图层设计与对接后端接口前端部分我用Vue 3 Vite Vue Router Pinia Element Plus这套组合。Element Plus是Element UI的Vue 3版本它最大的优势是组件丰富——表格、表单、对话框、分页、日期选择器都有现成的做管理后台不用自己写复杂样式。4.1 前端项目结构与关键目录cake-web/ ├── src/ │ ├── api/ // 存放接口调用函数 │ ├── assets/ // 静态资源 │ ├── components/ // 公共组件 │ ├── router/ // 路由配置 │ ├── store/ // Pinia状态管理 │ ├── views/ // 页面组件 │ │ ├── Home.vue // 首页蛋糕展示 │ │ ├── Category.vue // 分类页 │ │ ├── CakeDetail.vue // 蛋糕详情 │ │ ├── Cart.vue // 购物车 │ │ ├── Checkout.vue // 结算页 │ │ ├── OrderList.vue // 我的订单 │ │ ├── admin/ // 后台管理页面 │ │ │ ├── Dashboard.vue // 管理首页 │ │ │ ├── CakeManage.vue // 蛋糕管理 │ │ │ ├── CategoryManage.vue // 分类管理 │ │ │ └── OrderManage.vue // 订单管理 │ ├── App.vue │ └── main.jsviews目录下分成前端用户页面和管理后台页面两个区域这个组织方式很清晰——顾客和管理员看到的页面天然不同拆开管理。4.2 路由配置与登录鉴权路由用Vue Router的createWebHistory模式配置示例import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: home, component: () import(../views/Home.vue) }, { path: /cake/:id, name: cake-detail, component: () import(../views/CakeDetail.vue) }, { path: /cart, name: cart, component: () import(../views/Cart.vue) }, { path: /orders, name: orders, component: () import(../views/OrderList.vue) }, { path: /admin, component: () import(../views/admin/AdminLayout.vue), meta: { requiresAuth: true, role: admin }, children: [ { path: , redirect: /admin/dashboard }, { path: dashboard, component: () import(../views/admin/Dashboard.vue) }, { path: cakes, component: () import(../views/admin/CakeManage.vue) }, { path: categories, component: () import(../views/admin/CategoryManage.vue) }, { path: orders, component: () import(../views/admin/OrderManage.vue) } ] } ]对管理后台的路由加meta.requiresAuth和meta.role两个字段然后在全局前置守卫里做校验router.beforeEach((to, from, next) { const token localStorage.getItem(token) const user JSON.parse(localStorage.getItem(user) || {}) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.role user.role ! to.meta.role) { next(/) return } next() })这个守卫的逻辑是进入管理后台必须有登录token同时角色的role必须是admin。前端鉴权只是体验层面的控制真正可靠的鉴权必须靠后端接口校验——前端守卫拦不住一个直接拿curl调接口的人。所以你后端登录接口要返回token和role管理端接口也要在服务端校验admin权限。前端守卫只是一个友好的导航拦截。4.3 axios封装与接口对接前端发请求我用axios。直接在每个页面里面import axios再写请求也是能跑的但到时候你会遇到两个麻烦请求头里要带token每次都要写接口报错要统一提示每个页面都要写catch。所以我习惯在api/request.js里封装一个axios实例import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器把token放到请求头 request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.clear() router.push(/login) } else { ElMessage.error(error.message || 网络错误) } return Promise.reject(error) } ) export default request封装完之后每个接口文件就很干净了。以蛋糕接口为例// api/cake.js import request from ./request export function getCakeList(params) { return request.get(/cake/list, { params }) } export function getCakeDetail(id) { return request.get(/cake/${id}) } export function addCake(data) { return request.post(/admin/cake, data) } export function updateCake(data) { return request.put(/admin/cake/ data.id, data) } export function deleteCake(id) { return request.delete(/admin/cake/ id) }getCakeList接收的参数params可能是分类id、价格区间、名称、页码这些全都由后端动态SQL处理。前端只要你把用户选的筛选条件传过去就行。在Vue页面里调用// views/admin/CakeManage.vue 中核心逻辑 script setup import { ref, onMounted } from vue import { getCakeList, deleteCake } from /api/cake const cakeList ref([]) const loading ref(false) const total ref(0) const queryParams ref({ page: 1, pageSize: 10, name: , categoryId: null, status: null }) async function fetchCakeList() { loading.value true try { const data await getCakeList(queryParams.value) cakeList.value data.records total.value data.total } finally { loading.value false } } function handleSearch() { queryParams.value.page 1 fetchCakeList() } function handleDelete(row) { ElMessageBox.confirm(确定删除蛋糕「${row.name}」吗, 提示, { type: warning }) .then(async () { await deleteCake(row.id) ElMessage.success(删除成功) fetchCakeList() }) .catch(() {}) } onMounted(fetchCakeList) /script删除操作必须用确认框用户一不小心点错了蛋糕没了后台直接少一条数据——这种体验就是被骂出来的。Element Plus自带的ElMessageBox就能解决。4.4 购物车和订单流程的前端实现购物车的核心交互是加入购物车、修改数量、删除条目、勾选结算。计算总价用computed属性最合适const cartList ref([]) const totalPrice computed(() { return cartList.value .filter(item item.checked) .reduce((sum, item) sum item.price * item.quantity, 0) })加入了checked字段用来表示当前购物车条目是否被勾选。用户结算时只计算勾选中的项这个交互是电商标配。订单提交这步是在Checkout页面发请求给后端。前端要做的是把用户在购物车勾选的条目组织成后端需要的结构——典型的DTO是{ userId: 1, address: 上海市浦东新区, receiverName: 张三, receiverPhone: 13800000000, items: [ { cakeId: 1, quantity: 2 }, { cakeId: 3, quantity: 1 } ] }前端不用传价格之类的信息——总价必须由后端计算这就是我之前强调的快照道理。如果你在前端算了总价直接传给后端用户改一下请求体就能0元购那整个系统基本就报废了。4.5 管理后台的表格分页与日期筛选管理后台的蛋糕列表和订单列表通常用表格组件展示。Element Plus的el-table配合el-pagination是最常见的组合。分页这块要注意后端返回的数据要包含total总数前端根据总数来计算总页数。日期筛选是订单管理里的高频操作——查看某个时间段的订单。Element Plus的el-date-picker支持日期区间选择选完传给后端两个参数startDate和endDate。后端的动态SQL就可以用create_time BETWEEN #{startDate} AND #{endDate}来过滤。这里有个细节传日期的时候要控制好格式一致用yyyy-MM-dd时间范围要包含结束那一天的23:59:59否则你会发现“今天”的订单在“今天00:00:00到今天00:00:00”的区间里查不出来。处理办法是后端在接收结束日期时自动加上23:59:59或者前端把结束日期设为end 23:59:59二选一别两头都处理否则会重复添加。表格渲染后管理员的日常操作就是翻页、搜索、改状态。前端写熟了之后你会发现这类管理后台的交互都是“三板斧”——表单Query、表格展示、弹窗编辑核心逻辑高度相似。一个项目做完你基本掌握了Vue在中小型管理系统里的完整工作方式。5. 本地调试与服务器部署让系统真正跑起来写代码只是第一步把系统跑起来、部署到服务器上让别人能访问才是完整的交付。这一章我专门讲环境问题和部署过程——说真的我见过的项目里死在部署阶段的比死在写代码阶段的还多。5.1 本地运行环境准备跑这套系统你的机器上需要这几样东西JDK 17SpringBoot 3.x要求Maven 3.6Node.js 16Vite 5要求Node 18建议装Node 20 LTSMySQL 8.x一个IDEIDEA最好社区版就够用JDK版本这里要特别强调SpringBoot 3.0开始官方最低要求JDK 17。如果你装的是JDK 8SpringBoot 3.x根本起不来。如果你看到项目里有个maven依赖报错提示class file has wrong version十有八九是JDK版本不匹配。还有一个坑是本地装了多个JDK版本环境变量指向了旧版本导致项目启动报错排查半天发现是Java版本不对。用java -version先确认当前生效的版本。MySQL安装之后需要先创建数据库然后导入项目中提供的SQL脚本mysql -u root -p cake_shop.sql这个命令会直接执行SQL文件里所有建表和插入数据的语句。SQL脚本里的数据库名、用户名、密码要和你application.yml里配置的一致。5.2 前端开发代理与跨域问题开发阶段前端跑在5173端口Vite默认后端跑在8080端口两个端口不同浏览器直接请求就会遇到跨域问题。Vite里解决很简单在vite.config.js里配置代理export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样配置之后前端请求/api/user/loginVite开发服务器会把它转发到http://localhost:8080/api/user/login。浏览器看到的请求是同源的跨域问题就没了。同时要注意后端也要允许来自不同Origin的请求SpringBoot里可以用一个WebMvcConfigurer配置跨域或者直接在Controller上加CrossOrigin。更专业的做法是加一个跨域配置类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); } }有了这个配置前端用任意Origin访问后端接口后端都会在响应头里带上CORS相关的字段。开发阶段这样做最省心部署到生产环境如果前后端域名相同其实CORS已经不影响因为同源了。注意allowedOriginPatterns(*)配合allowCredentials(true)时不能写成allowedOrigins(*)否则会报错——这是Spring 5.3之后的一个变化。5.3 后端打包与运行后端打包用Mavenmvn clean package -DskipTests执行完会在target目录生成一个cake-0.0.1-SNAPSHOT.jar。在服务器上运行java -jar cake-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod--spring.profiles.activeprod指定用生产环境的配置文件这个配置文件里数据库地址要改成服务器的MySQL地址、用户名密码改成服务器的。实际上我会建议准备两个配置文件application-dev.yml和application-prod.yml开发和生产环境分别加载避免改配置时误把生产环境的东西改乱。5.4 前端构建与Nginx部署前端打包npm run build生成到dist目录这个目录里全是静态文件可以用Nginx托管。Nginx配置示例server { listen 80; server_name your-domain.com; # 前端静态资源 root /var/www/cake-web/dist; index index.html; # 解决Vue路由history模式刷新404 location / { try_files $uri $uri/ /index.html; } # 反向代理后端接口 location /api/ { proxy_pass http://127.0.0.1:8080; 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模式部署时必配的一行。没有这一行你点击页面内跳转没问题但一旦刷新某个非首页路径比如直接刷新/admin/dashboardNginx找不到这个路径对应的文件会返回404。加上try_files之后请求会回退到index.html由Vue Router自己接管路由问题就解决了。proxy_pass http://127.0.0.1:8080;要注意不要写成http://127.0.0.1:8080/带斜杠带斜杠和不带斜杠的转发规则不一样写错会导致接口路径多一层或少一层。5.5 云服务器部署的经验说到云服务器我踩过不少坑直接说重点端口放行云服务器的安全组规则里默认只放行22、80、443等端口。你后端跑在8080如果不放行8080外网访问不到。MySQL端口3306建议不要对公网开放——默认只监听本地或者干脆在安全组里不给3306放行。用MySQL客户端远程连不上时先想想是不是3306没放行再看MySQL的用户权限。数据库连接不要用localhost服务器上MySQL和Java进程往往在同一台机器数据库连接串里的地址用127.0.0.1不要用localhost。有些操作系统会把localhost解析成IPv6的::1而MySQL可能没有监听IPv6导致Connection refused。使用systemd管理Java进程直接在终端里java -jar启动等你关掉终端进程就没了。正确做法是写一个systemd service文件[Unit] DescriptionCake Shop Application Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/opt/cake ExecStart/usr/bin/java -jar /opt/cake/cake-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod Restarton-failure RestartSec10 [Install] WantedBymulti-user.target然后systemctl daemon-reload systemctl start cake systemctl enable cakeRestarton-failure保证进程挂了自动拉起enable保证开机自启。这样部署完基本就不用手动管理进程了。6. 复盘与扩展这套系统还能怎么改系统做完只是一小步关键是知道下一步往哪走。这一章我把自己做类似项目时的复盘思路分享出来你可以当作对照清单来用。6.1 我在开发中遇到的坑第一个坑是数据库字段名和Java属性映射不上。一张表里如果有create_time字段而实体类里是createTime如果你忘记开map-underscore-to-camel-case运行时你会看到createTime永远是null。排查方法就是把SQL打印出来看结果集或者查一下映射配置。这个坑最隐蔽的地方在于SQL能查出来返回结果的实体就是null不报错但数据不对非常让人抓狂。第二个坑是Vue的响应式丢失。在拿后端返回的列表时如果你直接给数组的某个索引赋值Vue 3的reactive是能监听到的但如果你用数组长度重置之类的方法或者把ref对象整个替换却忘了.value页面上的数据就不会更新。排查这类问题先确认是不是API返回了数据在then里打个console.log看下再确认是不是响应式处理有误。最好从页面数据流的角度逐步向上查而不是在模板里瞎猜。第三个坑是日期类型前后端解析不一致。后端返回的LocalDateTime默认序列化成数组格式比如[2025, 3, 15, 14, 30, 0]前端拿来直接显示就变成了一串数字或者干脆报错。解决后端配置Jackson的时间格式为yyyy-MM-dd HH:mm:ss再给LocalDateTime字段加上JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。前端侧接收后如果需要格式化也可以用dayjs之类工具再处理一次。第四个坑是跨域配置和代理冲突。本地开发时Vite已经配了proxy后端又配了CORS两个都配置意味着请求会先走代理转发到8080然后后端又向浏览器返回CORS头——有些情况下会多一层。调试时要确认请求到底打到哪一层了。如果代理生效前端就感觉不到跨域如果代理没生效比如502、404才会走CORS那一套。两个配置并存不是不能用但要调试清楚。6.2 功能上的进阶方向这套蛋糕店系统跑起来之后往上扩展的空间其实很大。我给你列几个具体的方向图片上传和云存储现在蛋糕图片如果放本地服务器重启或者多实例部署时容易丢。可以接OSS后端加一个上传接口前端用Element Plus的Upload组件对接图片上传成功返回URL再入库。支付功能模拟真实接入支付宝/微信支付需要商户资质但模拟支付流程是可以做的在提交订单后跳转到模拟收银台点“确认支付”就把订单状态从待支付改成已支付。这样就把整个支付闭环跑通了比直接跳过要完整得多。数据统计给管理员做一个销售统计面板按天筛选营业额展示TOP5热销蛋糕。订单表里已经有create_time和total_amount一条分组SQL就能搞定select idgetDailySales resultTypecom.example.cake.vo.SalesVO SELECT DATE(create_time) as date, SUM(total_amount) as amount, COUNT(*) as order_count FROM orders WHERE status ! CANCELLED GROUP BY DATE(create_time) ORDER BY date DESC LIMIT 30 /select前端用ECharts画个折线图一眼看出哪天生意好。角色权限细化如果你想把系统做得更完善可以把用户表里的role字段升级成真正的RBAC模型——角色表、菜单表、用户角色关联表、角色菜单关联表。前端根据用户拥有的菜单权限动态生成路由后台接口用拦截器校验权限码。这套是大部分企业后台的标准做法把它掌握了你面试时就有了谈资。对我来说这套蛋糕店项目最值得你学习的不是某一个具体页面或者接口而是“从一个需求到一套完整可运行系统”的落地路径。你知道数据怎么从MySQL查出来经过MyBatis、Service、Controller、axios、Vue最终显示在用户面前——这条链路想明白了之后做任何类似的CRUD管理系统都只是换了个业务外壳而已。最后再分享一个我自己的习惯项目做完后花半小时画一张架构图把模块边界、数据流向、部署结构画清楚。别小看这半小时它是你从“会写代码”到“会讲清楚系统”的分水岭。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价