1. 项目概述与背景
作为一名经历过毕业设计"洗礼"的过来人,我深知开发一个完整的餐厅点餐管理系统对计算机专业学生来说意味着什么。这个项目看似简单,实则暗藏玄机,特别是在表结构设计和业务逻辑实现上,稍有不慎就会掉进各种"坑"里。今天我就来分享一个基于SpringBoot+Vue+MySQL的实战方案,这个方案不仅通过了我的毕业答辩,还获得了导师的高度评价。
这个系统最核心的价值在于它完整覆盖了从用户点餐到商家管理的全流程,包含了现代餐饮管理系统的主要功能模块。不同于市面上简单的Demo项目,我们在开发过程中特别注重数据库设计的规范性和业务逻辑的完整性,这也是为什么我要重点强调购物车表与用户/商品表的双外键关联设计。
2. 需求分析与功能规划
2.1 核心功能定位
在项目初期,我犯了一个很多同学都会犯的错误——追求功能的大而全。最初的设计方案包含了消费数据可视化大屏、会员积分系统、智能推荐算法等"高大上"的功能,结果导师一针见血地指出:"你的核心是点餐系统,不是数据分析平台"。经过反复推敲,最终确定了以下核心功能矩阵:
-
用户端核心功能:
- 菜品浏览与搜索(支持分类筛选和关键词搜索)
- 购物车管理(增删改查、数量调整)
- 订单创建与支付(支持多种支付方式模拟)
- 订单状态跟踪(从下单到完成的完整流程)
-
管理端核心功能:
- 菜品信息管理(CRUD操作)
- 订单处理(状态变更、异常处理)
- 用户管理(权限控制、信息维护)
- 基础数据维护(分类管理、系统参数设置)
2.2 需求分析实战技巧
在实际需求分析过程中,我总结了几个特别实用的方法:
-
角色-场景-功能矩阵法:为每个用户角色(普通用户、管理员)列出其所有可能的使用场景,再为每个场景设计必要的功能。这个方法帮我避免了功能遗漏和过度设计的问题。
-
纸质原型测试:在正式编码前,我用纸笔画出所有关键界面,邀请10位同学模拟完整操作流程。这个低成本的方法帮我发现了3个重要的交互问题,节省了后期大量的修改时间。
-
约束条件清单:提前列出所有业务规则和技术限制,比如"菜品图片不超过2MB"、"订单15分钟未支付自动取消"等。这个清单成为了后续开发的重要依据。
3. 技术架构设计
3.1 技术选型决策
在技术选型上,我经历了从"追新"到"务实"的转变过程。最初为了追求技术先进性,我选择了SpringBoot 3+Vue 3的组合,结果在开发过程中遇到了各种兼容性问题。最终回归到更稳定的技术栈:
-
后端技术栈:
- SpringBoot 2.7.12(稳定版)
- MyBatis-Plus 3.5.3(简化DAO层开发)
- Hutool 5.8.16(工具类库)
- Lombok(减少样板代码)
-
前端技术栈:
- Vue 2.6.14(稳定兼容)
- ElementUI 2.15.13(UI组件库)
- Axios 0.27.2(HTTP客户端)
- Vuex 3.6.2(状态管理)
-
数据库:
- MySQL 5.7(事务支持完善)
- Redis 6.2(缓存,非必须)
技术选型心得:毕业设计不是技术试验场,稳定性和开发效率应该优先于技术新颖性。特别是临近答辩时,一个稳定的技术栈能让你少很多麻烦。
3.2 系统架构设计
系统采用经典的前后端分离架构:
code复制┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Vue前端 │ ←→ │ SpringBoot │ ←→ │ MySQL │
│ (8080端口) │ │ (8087端口) │ │ (3306端口) │
└─────────────┘ └─────────────┘ └─────────────┘
前端通过axios与后端RESTful API交互,后端使用MyBatis-Plus操作数据库。这种架构的优势在于:
- 前后端可以并行开发,提高效率
- 接口定义清晰,便于后期维护
- 前端可以独立部署,灵活性高
4. 数据库设计与实现
4.1 核心表结构设计
数据库设计是整个系统最关键的环节之一。经过多次迭代,最终确定了12张核心表,这里重点介绍几个关键表的设计:
-
用户表(yonghu):
sql复制CREATE TABLE `yonghu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `yonghu_name` varchar(50) DEFAULT NULL COMMENT '用户姓名', `yonghu_phone` varchar(11) NOT NULL COMMENT '手机号', `yonghu_password` varchar(100) NOT NULL COMMENT '密码', `yonghu_photo` varchar(255) DEFAULT NULL COMMENT '头像', `yonghu_email` varchar(50) DEFAULT NULL COMMENT '邮箱', `new_money` decimal(10,2) DEFAULT '0.00' COMMENT '余额', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `yonghu_phone_unique` (`yonghu_phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -
商品表(shangpin):
sql复制CREATE TABLE `shangpin` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `shangjia_id` bigint(20) DEFAULT NULL COMMENT '店家ID', `shangpin_name` varchar(100) NOT NULL COMMENT '商品名称', `shangpin_photo` varchar(255) DEFAULT NULL COMMENT '商品图片', `shangpin_types` int(11) DEFAULT NULL COMMENT '商品类型', `shangpin_kucun_number` int(11) DEFAULT '0' COMMENT '库存数量', `shangpin_old_money` decimal(10,2) DEFAULT NULL COMMENT '原价', `shangpin_new_money` decimal(10,2) NOT NULL COMMENT '现价', `shangxia_types` int(11) DEFAULT '1' COMMENT '是否上架', `shangpin_delete` int(11) DEFAULT '1' COMMENT '逻辑删除', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `shangjia_id_index` (`shangjia_id`), CONSTRAINT `shangpin_ibfk_1` FOREIGN KEY (`shangjia_id`) REFERENCES `shangjia` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; -
购物车表(cart) - 最易出错的关键表:
sql复制CREATE TABLE `cart` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `yonghu_id` bigint(20) NOT NULL COMMENT '用户ID', `shangpin_id` bigint(20) NOT NULL COMMENT '商品ID', `buy_number` int(11) DEFAULT '1' COMMENT '购买数量', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间', PRIMARY KEY (`id`), UNIQUE KEY `yonghu_shangpin_unique` (`yonghu_id`,`shangpin_id`), KEY `shangpin_id_index` (`shangpin_id`), CONSTRAINT `cart_ibfk_1` FOREIGN KEY (`yonghu_id`) REFERENCES `yonghu` (`id`), CONSTRAINT `cart_ibfk_2` FOREIGN KEY (`shangpin_id`) REFERENCES `shangpin` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';
4.2 数据库设计经验
-
外键约束的重要性:初期我没有为购物车表设置外键约束,结果在测试时出现了大量脏数据。添加外键后,数据完整性得到了保证,也减少了业务逻辑中的校验代码。
-
索引优化:在经常查询的字段上建立合适的索引,如用户表的手机号字段、商品表的店家ID字段等,查询性能提升了3-5倍。
-
字段类型选择:
- 金额使用DECIMAL(10,2)而不是FLOAT,避免精度问题
- 状态字段使用TINYINT而不是VARCHAR,节省存储空间
- 时间字段使用TIMESTAMP而不是DATETIME,自动处理时区
-
图片存储方案:千万不要把图片直接存数据库!我们最初这样做导致数据库体积暴涨。正确的做法是只存储图片路径,图片文件存放在服务器或云存储上。
5. 核心功能实现
5.1 用户登录与认证
采用JWT进行认证,后端配置Spring Security:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/yonghu/login").permitAll()
.antMatchers("/shangpin/list").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()));
}
}
前端在axios拦截器中处理token:
javascript复制// 请求拦截器
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
return response.data
},
error => {
if (error.response.status === 401) {
Message.error('登录已过期,请重新登录')
router.push('/login')
}
return Promise.reject(error)
}
)
5.2 购物车功能实现
购物车是系统的核心功能之一,主要业务逻辑包括:
-
添加商品到购物车:
java复制@PostMapping("/add") public R add(@RequestBody Cart cart) { // 检查商品是否存在 ShangpinEntity shangpin = shangpinService.getById(cart.getShangpinId()); if(shangpin == null || shangpin.getShangxiaTypes() != 1){ return R.error("商品已下架"); } // 检查是否已存在购物车记录 LambdaQueryWrapper<Cart> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Cart::getYonghuId, cart.getYonghuId()) .eq(Cart::getShangpinId, cart.getShangpinId()); Cart existCart = cartService.getOne(wrapper); if(existCart != null){ // 已存在则更新数量 existCart.setBuyNumber(existCart.getBuyNumber() + cart.getBuyNumber()); cartService.updateById(existCart); }else{ // 不存在则新增 cart.setCreateTime(new Date()); cartService.save(cart); } return R.ok(); } -
获取用户购物车列表:
java复制@GetMapping("/list") public R list(Long yonghuId) { List<CartView> cartViewList = cartService.getCartViewByYonghuId(yonghuId); return R.ok().put("data", cartViewList); } // 自定义查询方法 public List<CartView> getCartViewByYonghuId(Long yonghuId) { return baseMapper.selectCartView(yonghuId); } -
MyBatis-Plus自定义SQL:
xml复制<select id="selectCartView" resultType="com.example.entity.view.CartView"> SELECT c.id, c.buy_number, c.create_time, s.id as shangpin_id, s.shangpin_name, s.shangpin_photo, s.shangpin_new_money, s.shangpin_kucun_number, sj.shangjia_name FROM cart c LEFT JOIN shangpin s ON c.shangpin_id = s.id LEFT JOIN shangjia sj ON s.shangjia_id = sj.id WHERE c.yonghu_id = #{yonghuId} ORDER BY c.create_time DESC </select>
5.3 订单系统实现
订单系统是整个业务逻辑最复杂的部分,主要包含以下关键点:
-
订单状态机设计:
java复制public enum OrderStatus { WAIT_PAY(1, "待支付"), PAID(2, "已支付"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"), REFUNDED(5, "已退款"); private final int code; private final String desc; // 构造方法、getter省略 } -
下单业务逻辑:
java复制@Transactional public R createOrder(OrderCreateDTO dto) { // 1. 验证用户 YonghuEntity user = yonghuService.getById(dto.getYonghuId()); if(user == null) { return R.error("用户不存在"); } // 2. 验证购物车商品 List<Cart> carts = cartService.listByIds(dto.getCartIds()); if(carts.isEmpty()) { return R.error("购物车为空"); } // 3. 检查库存 for(Cart cart : carts) { ShangpinEntity goods = shangpinService.getById(cart.getShangpinId()); if(goods.getShangpinKucunNumber() < cart.getBuyNumber()) { return R.error(goods.getShangpinName() + "库存不足"); } } // 4. 创建订单 ShangpinOrderEntity order = new ShangpinOrderEntity(); order.setYonghuId(dto.getYonghuId()); order.setShangpinOrderUuidNumber(GenerateOrderNoUtil.getOrderNo()); order.setShangpinOrderTypes(OrderStatus.WAIT_PAY.getCode()); order.setShangpinOrderTruePrice(calculateTotalPrice(carts)); order.setCreateTime(new Date()); shangpinOrderService.save(order); // 5. 创建订单明细并扣减库存 List<ShangpinOrderListEntity> orderItems = new ArrayList<>(); for(Cart cart : carts) { ShangpinEntity goods = shangpinService.getById(cart.getShangpinId()); // 扣减库存 goods.setShangpinKucunNumber(goods.getShangpinKucunNumber() - cart.getBuyNumber()); shangpinService.updateById(goods); // 创建订单明细 ShangpinOrderListEntity item = new ShangpinOrderListEntity(); item.setShangpinOrderId(order.getId()); item.setShangpinId(cart.getShangpinId()); item.setBuyNumber(cart.getBuyNumber()); item.setShangpinOrderListTruePrice(goods.getShangpinNewMoney()); orderItems.add(item); } shangpinOrderListService.saveBatch(orderItems); // 6. 清空购物车 cartService.removeByIds(dto.getCartIds()); return R.ok().put("data", order); } -
支付回调处理:
java复制@PostMapping("/notify") public String payNotify(HttpServletRequest request) { // 验证支付结果 boolean verifyResult = verifyPayResult(request); if(!verifyResult) { return "fail"; } String orderNo = request.getParameter("out_trade_no"); ShangpinOrderEntity order = shangpinOrderService.getOne( new LambdaQueryWrapper<ShangpinOrderEntity>() .eq(ShangpinOrderEntity::getShangpinOrderUuidNumber, orderNo) ); if(order != null && order.getShangpinOrderTypes() == OrderStatus.WAIT_PAY.getCode()) { order.setShangpinOrderTypes(OrderStatus.PAID.getCode()); order.setUpdateTime(new Date()); shangpinOrderService.updateById(order); // 记录支付日志等后续操作 return "success"; } return "fail"; }
6. 前端关键实现
6.1 商品列表页
采用Vue+ElementUI实现商品列表展示和筛选功能:
vue复制<template>
<div class="goods-list">
<el-row :gutter="20">
<el-col :span="6" v-for="item in goodsList" :key="item.id">
<el-card :body-style="{ padding: '0px' }">
<img :src="item.shangpin_photo" class="goods-image">
<div style="padding: 14px;">
<span>{{ item.shangpin_name }}</span>
<div class="bottom">
<span class="price">¥{{ item.shangpin_new_money }}</span>
<el-button
type="text"
class="button"
@click="addToCart(item)"
:disabled="item.shangpin_kucun_number <= 0">
{{ item.shangpin_kucun_number > 0 ? '加入购物车' : '已售罄' }}
</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
data() {
return {
goodsList: [],
queryParams: {
page: 1,
limit: 12,
shangpinTypes: null,
orderBy: 'default'
}
}
},
methods: {
async getGoodsList() {
const res = await this.$http.get('/shangpin/list', {
params: this.queryParams
})
this.goodsList = res.data.list
},
async addToCart(item) {
try {
await this.$http.post('/cart/add', {
yonghuId: this.$store.state.user.id,
shangpinId: item.id,
buyNumber: 1
})
this.$message.success('添加成功')
} catch (error) {
this.$message.error(error.response.data.msg)
}
}
},
created() {
this.getGoodsList()
}
}
</script>
6.2 购物车页面
购物车页面需要实现以下功能:
- 展示购物车商品列表
- 支持数量修改
- 实时计算总价
- 批量删除和结算
vue复制<template>
<div class="cart-container">
<el-table
:data="cartList"
style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="商品信息" width="400">
<template slot-scope="scope">
<div class="goods-info">
<img :src="scope.row.shangpin_photo" class="goods-image">
<div class="goods-detail">
<div>{{ scope.row.shangpin_name }}</div>
<div class="shop-name">{{ scope.row.shangjia_name }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="单价" prop="shangpin_new_money" width="120">
<template slot-scope="scope">
¥{{ scope.row.shangpin_new_money }}
</template>
</el-table-column>
<el-table-column label="数量" width="150">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.buy_number"
:min="1"
:max="scope.row.shangpin_kucun_number"
@change="handleQuantityChange(scope.row)">
</el-input-number>
</template>
</el-table-column>
<el-table-column label="小计" width="120">
<template slot-scope="scope">
¥{{ (scope.row.shangpin_new_money * scope.row.buy_number).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-button
type="text"
@click="handleDelete(scope.row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="cart-footer">
<div class="total-price">
总计:¥{{ totalPrice.toFixed(2) }}
</div>
<el-button
type="primary"
:disabled="selectedItems.length === 0"
@click="handleCheckout">
结算({{ selectedItems.length }})
</el-button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
cartList: [],
selectedItems: []
}
},
computed: {
totalPrice() {
return this.selectedItems.reduce((total, item) => {
return total + (item.shangpin_new_money * item.buy_number)
}, 0)
}
},
methods: {
async getCartList() {
const res = await this.$http.get('/cart/list', {
params: { yonghuId: this.$store.state.user.id }
})
this.cartList = res.data.data
},
async handleQuantityChange(item) {
try {
await this.$http.post('/cart/update', {
id: item.id,
buyNumber: item.buy_number
})
} catch (error) {
this.$message.error('更新失败')
}
},
handleSelectionChange(val) {
this.selectedItems = val
},
handleCheckout() {
const cartIds = this.selectedItems.map(item => item.id)
this.$router.push({
path: '/order/confirm',
query: { cartIds: cartIds.join(',') }
})
}
},
created() {
this.getCartList()
}
}
</script>
7. 项目部署与测试
7.1 系统部署方案
项目采用前后端分离部署方式:
-
后端部署:
- 打包SpringBoot应用:
mvn clean package - 上传生成的jar包到服务器
- 使用nohup启动:
nohup java -jar restaurant.jar --server.port=8087 &
- 打包SpringBoot应用:
-
前端部署:
- 构建生产环境代码:
npm run build - 将dist目录内容上传到Nginx服务器
- 配置Nginx:
nginx复制server { listen 80; server_name yourdomain.com; location / { root /path/to/dist; index index.html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://localhost:8087; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
- 构建生产环境代码:
-
数据库部署:
- 安装MySQL 5.7
- 创建数据库并导入SQL脚本
- 配置连接池参数:
yaml复制spring: datasource: url: jdbc:mysql://localhost:3306/restaurant?useSSL=false&useUnicode=true&characterEncoding=utf8 username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5
7.2 系统测试方案
- 功能测试用例:
| 测试场景 | 测试步骤 | 预期结果 |
|---|---|---|
| 用户登录 | 1. 输入正确用户名密码 2. 点击登录 |
跳转到首页,显示用户信息 |
| 添加购物车 | 1. 在商品页点击"加入购物车" 2. 查看购物车页面 |
购物车显示新增商品,数量正确 |
| 下单流程 | 1. 在购物车选择商品 2. 点击结算 3. 确认订单信息 4. 选择支付方式 5. 完成支付 |
生成待支付订单→支付后状态变更为已支付→商家可看到新订单 |
| 库存校验 | 1. 将商品A库存设为1 2. 两个用户同时购买商品A |
只有一个用户能成功下单,另一个提示库存不足 |
-
性能测试要点:
- 使用JMeter模拟100并发用户浏览商品列表,响应时间应<500ms
- 测试购物车添加操作的并发控制,确保数据一致性
- 检查长时间运行后的内存泄漏情况
-
安全测试要点:
- SQL注入测试:尝试在输入框中输入SQL片段
- XSS测试:尝试输入脚本标签
- 越权测试:普通用户尝试访问管理员接口
8. 毕业答辩准备
8.1 答辩演示要点
-
演示流程设计:
- 用户注册/登录(1分钟)
- 浏览商品、加入购物车(2分钟)
- 下单支付完整流程(3分钟)
- 管理员处理订单(2分钟)
- 重点技术展示(2分钟)
-
技术亮点展示:
- 数据库表关系图(重点展示购物车与用户、商品的关联)
- 订单状态流转图
- 事务处理代码片段
- 性能优化措施(如缓存、索引)
-
常见问题准备:
- Q:如何保证订单数据的一致性?
A:通过数据库事务保证库存扣减和订单生成的原子性;使用乐观锁防止超卖 - Q:购物车数据如何在不同设备间同步?
A:基于用户ID关联,用户登录后自动加载服务器端购物车数据 - Q:系统有哪些安全措施?
A:前后端输入验证、SQL参数化查询、JWT认证、权限控制等
- Q:如何保证订单数据的一致性?
8.2 项目文档整理
完整的毕业设计文档应包括:
- 需求分析文档(含用例图)
- 系统设计文档(架构图、数据库ER图)
- 核心模块详细设计
- 测试报告
- 用户手册
- 源代码(带注释)
- 答辩PPT(15页左右)
9. 开发经验总结
通过这个项目的开发,我总结了以下几点重要经验:
-
数据库设计先行:良好的数据库设计是系统稳定的基础。特别是外键关系的设计,一定要在项目开始时就考虑清楚,避免后期大量重构。
-
事务处理要全面:对于订单这类核心业务,一定要考虑各种异常情况,使用事务确保数据一致性。我们最初没有处理网络超时等情况,导致出现了少量数据不一致的问题。
-
前端性能优化:
- 图片懒加载:商品列表图片很多,使用懒加载显著提升首屏速度
- 接口合并:购物车页面原本需要多个接口,合并后减少了一半的请求数
- 本地缓存:频繁访问且不常变的数据(如商品分类)做本地缓存
-
代码规范与注释:
- 遵循阿里巴巴Java开发手册
- 重要业务方法必须写清楚注释
- 使用Swagger维护API文档
-
版本控制策略:
- 使用Git进行版本控制
- 主分支(master)只存发布版本
- 开发在dev分支进行
- 每个功能开单独feature分支
这个项目从技术难度上来说不算太高,但完整地走完需求分析、设计、编码、测试、部署的全流程,让我对软件开发有了更全面的认识。特别是通过解决实际遇到的问题(如购物车数据关联、订单并发控制等),我的实战能力得到了很大提升。