☰
溢香园餐饮管理系统:Web与Android双端毕业设计实战指南
2026/10/6 12:47:43 网站建设 项目流程

简介:这是一套面向高校计算机相关专业毕业设计与课程设计场景的「溢香园餐饮管理系统」完整源码包,采用Web端与Android端双端架构,适合需要完成餐饮管理类课题、希望理解前后端分离与移动端协同开发的学生参考。系统覆盖用户管理、菜品管理、订单流转、库存监控、报表分析与评论评分等模块,并涉及JWT身份验证、数据加密、RESTful接口设计及云服务器部署等工程实践。压缩包共960个文件,约77.01MB,以367个png与141个gif界面素材、125个java与89个xml源码、37个jsp页面、35个js与35个css样式脚本为主,另含sql建库脚本、gradle构建文件及pdf、docx说明文档,便于按模块梳理目录结构。目前已有126人学习下载,可作为从需求分析到测试部署全流程的实战参考,帮助读者快速理解双端系统的整体架构与关键实现思路。

1. 溢香园餐饮管理系统:一个标题里藏着 Web 与 Android 双端的完整交付

如果你正在搜「毕业设计 餐饮管理系统 web端 Android端」,大概率你手里已经有一个压缩包,或者正在纠结要不要选这个方向。溢香园餐饮管理系统这个标题,本质上描述的是一个典型的双端业务系统:Web 端给餐厅管理员和收银员用,Android 端给服务员点餐或者顾客自助用,两端共享同一套订单、菜品和桌台数据。它解决的核心问题是把点餐、下单、结账、菜品管理这条链路从纸质流程搬到线上,适合计算机毕业设计选题里想做完整业务闭环、又不想碰硬件和算法的同学。这个方向的好处是业务逻辑清晰、技术栈成熟、演示效果好,坏处是如果只做增删改查,答辩时容易被问「你的创新点在哪」。所以这篇笔记不讲空泛的选题意义,而是按一个真实可跑通的思路,把 Web 端和 Android 端怎么分工、数据库怎么设计、接口怎么对齐、哪些地方最容易翻车,一步步拆开讲。你照着做,至少能拿到一个能演示、能讲清楚、能经得起追问的系统。

2. 双端餐饮系统的技术选型与数据模型怎么定

2.1 Web 端和 Android 端各自该用什么技术栈

常见做法是 Web 端用 Spring Boot 做后端,前端用 Vue 或 Thymeleaf 做页面,Android 端用原生 Java 或者 Kotlin 写 Activity,通过 HTTP 调后端接口。为什么这么选?因为毕业设计的时间窗口通常只有两三个月,Spring Boot 的生态最全,遇到问题搜得到答案;Vue 的学习曲线比 React 平缓,配合 Element UI 能快速出管理后台;Android 端如果不想折腾 Gradle 版本兼容,直接用 Java 写 Activity 加 OkHttp 发请求是最稳的。我一般会建议把后端和 Web 前端放在同一个 Spring Boot 项目里,Android 端单独一个工程,这样部署的时候只需要启动一个 Jar 包,演示时少一个变量。

数据库选 MySQL 8.0,表设计围绕「用户、菜品、分类、桌台、订单、订单详情」六张核心表展开。这里有个容易忽略的点:订单表要区分「堂食」和「外带」,桌台表要记录状态(空闲、占用、预订),否则服务员在 Android 端点餐时无法判断哪张桌子可用。下面是一个最小化的建表 SQL,你可以直接拿去改字段名。

-- 菜品分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '分类名,如热菜、凉菜、饮品', sort_order INT DEFAULT 0 COMMENT '排序权重' ); -- 菜品表 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, image_url VARCHAR(255) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT '1上架 0下架', FOREIGN KEY (category_id) REFERENCES category(id) ); -- 桌台表 CREATE TABLE dining_table ( id INT PRIMARY KEY AUTO_INCREMENT, table_no VARCHAR(20) NOT NULL UNIQUE COMMENT '桌号,如A01', capacity INT DEFAULT 4 COMMENT '座位数', status TINYINT DEFAULT 0 COMMENT '0空闲 1占用 2预订' ); -- 订单主表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号,用时间戳+随机数生成', table_id INT DEFAULT NULL COMMENT '堂食关联桌台,外带为空', user_id INT NOT NULL COMMENT '下单用户', total_amount DECIMAL(10,2) NOT NULL, order_type TINYINT DEFAULT 1 COMMENT '1堂食 2外带', status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已完成 3已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (table_id) REFERENCES dining_table(id) ); -- 订单详情表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, price DECIMAL(10,2) NOT NULL COMMENT '下单时的单价,防止菜品调价影响历史订单', FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (dish_id) REFERENCES dish(id) );

这段 SQL 里最关键的是order_item表里的price字段。很多同学直接关联dish表查价格,结果菜品调价后历史订单金额全变了,答辩时被老师一问就露馅。把下单时的单价冗余存一份,是餐饮系统里必须做的「后悔药」。另外orders表的order_no不要用自增 ID 直接暴露给前端,用时间戳加随机数生成一个字符串,避免被人猜到订单量。

2.2 接口对齐:Web 端和 Android 端怎么共享同一套 API

双端系统最容易翻车的地方不是代码写不出来,而是两端对同一个接口的理解不一致。比如 Web 端下单时传的是tableId,Android 端传的是tableNo,后端就得写两套逻辑。我的做法是:所有接口的入参和出参都用统一的 DTO 对象,字段名在接口文档里写死,两端都按这个来。下面是一个下单接口的 Controller 示例,用 Spring Boot 写。

@RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; // 下单接口,Web端和Android端共用 @PostMapping("/create") public Result<OrderVO> createOrder(@RequestBody @Valid OrderCreateDTO dto) { // dto里包含:tableId(堂食必填)、userId、orderType、items列表 // items里每个元素包含:dishId、quantity OrderVO vo = orderService.createOrder(dto); return Result.success(vo); } // 查询桌台状态,Android端点餐前先调这个 @GetMapping("/table/status") public Result<List<TableVO>> getTableStatus() { return Result.success(orderService.getAllTableStatus()); } }

OrderCreateDTO里用@NotNull和@Min做参数校验,比如quantity最小为 1,orderType只能是 1 或 2。这样 Android 端传错参数时,后端直接返回 400 和具体错误信息,而不是抛一个空指针异常让前端去猜。参数说明:tableId在堂食时必填,外带时传 null;items列表不能为空,且每个dishId必须在数据库里存在且状态为上架。如果 Android 端在弱网环境下重复提交,后端要用order_no做幂等,或者在前端按钮上加防抖,否则会出现同一桌台两个订单的玄学问题。

3. Android 端点餐流程怎么跑通:从扫码到下单

3.1 用 OkHttp 封装一个能复用的请求工具类

Android 端不需要每个 Activity 都写一遍网络请求,封装一个单例的HttpUtil是标准做法。下面这个类用 OkHttp 做底层,处理了 JSON 序列化和统一的错误提示。

public class HttpUtil { private static final OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); private static final Gson gson = new Gson(); // 通用POST请求,回调在主线程执行 public static <T> void post(String url, Object body, Class<T> clazz, Callback<T> callback) { String json = gson.toJson(body); Request request = new Request.Builder() .url(url) .post(RequestBody.create(json, JSON)) .build(); client.newCall(request).enqueue(new okhttp3.Callback() { @Override public void onFailure(Call call, IOException e) { new Handler(Looper.getMainLooper()).post(() -> callback.onFail("网络异常")); } @Override public void onResponse(Call call, Response response) throws IOException { String result = response.body().string(); T data = gson.fromJson(result, clazz); new Handler(Looper.getMainLooper()).post(() -> callback.onSuccess(data)); } }); } public interface Callback<T> { void onSuccess(T data); void onFail(String msg); } }

这个工具类的关键点是回调切回主线程。Android 不允许在子线程更新 UI,如果你直接在 OkHttp 的onResponse里弹 Toast,会直接崩。用Handler(Looper.getMainLooper())是最简单的解法。参数说明:connectTimeout设 10 秒,readTimeout设 30 秒,因为餐厅高峰期后端响应可能变慢,设太短会导致请求被取消。Gson用来把 JSON 转成 Java 对象,注意后端返回的字段名要和你的实体类一致,否则解析出来全是 null。

3.2 点餐页面的购物车逻辑与桌台绑定

Android 端的点餐页面通常是一个左侧分类、右侧菜品列表的布局,底部有一个购物车悬浮条。用户点击菜品时,不是直接下单,而是先加入本地购物车,最后点「确认下单」才调接口。购物车用Map<Integer, Integer>存dishId到quantity的映射,每次增减都更新底部总价。

// 购物车管理类 public class CartManager { private static final Map<Integer, Integer> cart = new LinkedHashMap<>(); private static final Map<Integer, Dish> dishCache = new HashMap<>(); public static void addDish(Dish dish) { int id = dish.getId(); cart.put(id, cart.getOrDefault(id, 0) + 1); dishCache.put(id, dish); } public static void removeDish(int dishId) { if (cart.containsKey(dishId)) { int qty = cart.get(dishId); if (qty <= 1) { cart.remove(dishId); } else { cart.put(dishId, qty - 1); } } } public static BigDecimal getTotalPrice() { BigDecimal total = BigDecimal.ZERO; for (Map.Entry<Integer, Integer> entry : cart.entrySet()) { Dish dish = dishCache.get(entry.getKey()); total = total.add(dish.getPrice().multiply(new BigDecimal(entry.getValue()))); } return total; } public static List<OrderItemDTO> buildOrderItems() { List<OrderItemDTO> items = new ArrayList<>(); for (Map.Entry<Integer, Integer> entry : cart.entrySet()) { OrderItemDTO item = new OrderItemDTO(); item.setDishId(entry.getKey()); item.setQuantity(entry.getValue()); items.add(item); } return items; } }

这里用LinkedHashMap是为了让购物车里的菜品按加入顺序显示,用户体验更自然。BigDecimal做金额计算是必须的,用double会出现 0.1+0.2=0.30000000000000004 的经典问题,答辩时被问到就尴尬了。下单前要检查桌台是否已被占用:如果tableId对应的桌台状态不是空闲,弹窗提示「该桌台已被占用,请刷新后重试」。这个检查放在后端做更可靠,但 Android 端也要做一次前置判断,减少无效请求。

4. Web 端管理后台的菜品与订单管理怎么做

4.1 菜品图片上传与静态资源映射

Web 端管理后台需要支持菜品图片上传,常见做法是把图片存到服务器本地目录,数据库只存相对路径。Spring Boot 里要配置静态资源映射,否则上传的图片通过 URL 访问不到。

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /upload/** 映射到本地磁盘的 upload 目录 registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); } }

上传接口用MultipartFile接收,保存时用 UUID 重命名,避免中文文件名和重复文件名的问题。参数说明:上传目录建议放在项目运行目录下的upload文件夹,不要放在src/main/resources里,因为打包成 Jar 后 resources 目录是只读的,写不进去。图片大小限制在application.yml里配spring.servlet.multipart.max-file-size=5MB,超过会抛异常,前端要捕获并提示。

4.2 订单列表的分页查询与状态流转

Web 端订单管理页面需要支持按状态筛选、按时间范围查询、分页展示。后端用 MyBatis-Plus 或者 JPA 都可以,核心是 SQL 里要加索引。订单表数据量大了以后,create_time和status字段要建联合索引,否则查询会越来越慢。

-- 订单表加联合索引 ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);

订单状态流转的规则要在 Service 层写死:待支付 → 已支付 → 已完成,已支付可以取消变成已取消,已完成不能改。每次状态变更都要记录操作日志,方便对账。下面是一个状态变更的 Service 方法。

@Transactional public void updateOrderStatus(int orderId, int newStatus) { Orders order = orderMapper.selectById(orderId); if (order == null) { throw new BizException("订单不存在"); } int oldStatus = order.getStatus(); // 已完成和已取消的订单不允许再变更 if (oldStatus == 2 || oldStatus == 3) { throw new BizException("当前状态不允许修改"); } // 待支付只能变已支付或已取消 if (oldStatus == 0 && newStatus != 1 && newStatus != 3) { throw new BizException("待支付订单只能支付或取消"); } order.setStatus(newStatus); orderMapper.updateById(order); // 记录日志 logService.saveOrderLog(orderId, oldStatus, newStatus); }

这段代码里@Transactional保证状态更新和日志写入在同一个事务里,要么都成功要么都回滚。参数说明:newStatus只允许传 1、2、3,前端按钮要根据当前状态动态显示,比如待支付订单显示「确认收款」和「取消订单」,已支付订单显示「完成订单」。如果并发情况下两个管理员同时操作同一订单,要用乐观锁或者select ... for update锁行,否则会出现状态覆盖的血泪教训。

5. 双端联调与部署上线的避坑清单

5.1 Android 端访问本地后端的 IP 配置问题

这是新手最容易翻车的地方:Android 模拟器里用localhost或127.0.0.1访问不到你电脑上的 Spring Boot 服务。原因是模拟器有自己的网络栈,localhost指向模拟器自身。正确做法是用10.0.2.2代替localhost(Android 官方模拟器),如果是真机调试,要用电脑的局域网 IP,比如192.168.1.100,并且确保手机和电脑在同一个 WiFi 下。

// Android 端配置 BASE_URL public class ApiConfig { // 模拟器用 10.0.2.2,真机用电脑局域网IP public static final String BASE_URL = "http://10.0.2.2:8080/api/"; }

另外 Android 9.0 以后默认禁止明文 HTTP 请求,需要在AndroidManifest.xml的application标签里加android:usesCleartextTraffic="true",否则请求直接被系统拦截,日志里只报一个CLEARTEXT communication not permitted,不熟悉的人能查半天。

5.2 数据库连接池和跨域配置的常见报错

Spring Boot 默认用 HikariCP 连接池,如果并发稍微高一点就报Connection is not available,通常是连接池最大连接数太小。在application.yml里把maximum-pool-size调到 20,并且检查有没有在代码里手动getConnection()后忘记关闭的情况。

spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000

跨域问题在 Web 端和 Android 端都会遇到。Web 端浏览器控制台报Access-Control-Allow-Origin,Android 端其实不受同源策略限制,但如果你用 WebView 加载页面就会遇到。最省事的做法是在后端加一个全局 CORS 配置。

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

注意addAllowedOriginPattern("*")和setAllowCredentials(true)一起用时,不能用addAllowedOrigin("*"),否则 Spring 会抛异常。这是 Spring 的版本差异坑,网上很多老教程写的配置在新版本里跑不通。

5.3 演示环境的数据初始化与回滚方案

答辩演示最怕的是数据库里没数据,或者演示到一半数据乱了。我的习惯是准备一个init.sql,包含建表语句和几条测试数据,每次演示前先执行一遍。测试数据要覆盖各种状态:一个空闲桌台、一个占用桌台、一个待支付订单、一个已完成订单。这样演示时不管老师点哪个功能,都有数据可看。

-- 初始化测试数据 INSERT INTO category (name, sort_order) VALUES ('热菜', 1), ('凉菜', 2), ('饮品', 3); INSERT INTO dish (category_id, name, price, status) VALUES (1, '宫保鸡丁', 38.00, 1), (1, '鱼香肉丝', 32.00, 1), (2, '拍黄瓜', 12.00, 1), (3, '可乐', 5.00, 1); INSERT INTO dining_table (table_no, capacity, status) VALUES ('A01', 4, 0), ('A02', 4, 1), ('B01', 6, 0);

如果演示过程中把数据改乱了,直接重新执行init.sql里的TRUNCATE加INSERT部分,十秒钟恢复原状。这个后悔药比任何解释都管用。

6. 从能跑到能讲:答辩演示的节奏控制与代码组织技巧

答辩演示的时间通常只有 5 到 8 分钟,你不可能把每个功能都点一遍。我的经验是提前设计一条「黄金演示路径」:先用 Web 端登录管理员账号,展示菜品管理里新增一个菜品并上传图片;然后切到 Android 端,用服务员账号登录,选择桌台 A01,点两个菜下单;再切回 Web 端,展示订单列表里出现了刚才的订单,点击「确认收款」;最后展示订单状态变成已完成,桌台 A01 恢复空闲。这条路径覆盖了双端交互、数据流转和状态变更,老师一看就明白系统是通的。

代码组织上,不要把所有的 Controller 写在一个文件里,按业务模块分包:controller、service、mapper、entity、dto、vo各一层。Android 端按activity、adapter、model、util、api分包。这样老师在翻代码时能快速找到对应功能,印象分直接拉高。下面是一个推荐的项目目录结构,你可以对照调整。

src/main/java/com/example/restaurant/ ├── controller/ // 接口层 │ ├── OrderController.java │ ├── DishController.java │ └── TableController.java ├── service/ // 业务逻辑 │ ├── OrderService.java │ └── DishService.java ├── mapper/ // 数据库操作 │ ├── OrderMapper.java │ └── DishMapper.java ├── entity/ // 数据库实体 ├── dto/ // 入参对象 ├── vo/ // 出参对象 └── config/ // 配置类

还有一个容易被忽略的细节:接口返回的统一格式。不要有的接口返回{code:200, data:...},有的直接返回数组。定义一个Result<T>类,所有接口都用它包装,前端处理起来逻辑一致,也显得你工程素养到位。

public class Result<T> { private int code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "成功"; r.data = data; return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } }

最后说一个我自己的习惯:答辩前把整个系统在另一台电脑上重新部署一遍,从装 JDK、MySQL 到启动 Jar 包、安装 APK,全程计时。如果超过 30 分钟,说明你的部署文档写得不够细,或者有隐藏的环境依赖。把这个过程写成一份README.md放在项目根目录,老师如果问「你这个怎么跑起来」,直接给他看文档,比口头解释一百句都管用。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询