ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

溢香园餐饮管理系统:Web与Android双端毕业设计实战指南

溢香园餐饮管理系统:Web与Android双端毕业设计实战指南 简介这是一套面向高校计算机相关专业毕业设计与课程设计场景的「溢香园餐饮管理系统」完整源码包采用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 端下单时传的是tableIdAndroid 端传的是tableNo后端就得写两套逻辑。我的做法是所有接口的入参和出参都用统一的 DTO 对象字段名在接口文档里写死两端都按这个来。下面是一个下单接口的 Controller 示例用 Spring Boot 写。RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; // 下单接口Web端和Android端共用 PostMapping(/create) public ResultOrderVO 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 ResultListTableVO getTableStatus() { return Result.success(orderService.getAllTableStatus()); } }OrderCreateDTO里用NotNull和Min做参数校验比如quantity最小为 1orderType只能是 1 或 2。这样 Android 端传错参数时后端直接返回 400 和具体错误信息而不是抛一个空指针异常让前端去猜。参数说明tableId在堂食时必填外带时传 nullitems列表不能为空且每个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; charsetutf-8); private static final Gson gson new Gson(); // 通用POST请求回调在主线程执行 public static T void post(String url, Object body, ClassT clazz, CallbackT 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 CallbackT { 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 端的点餐页面通常是一个左侧分类、右侧菜品列表的布局底部有一个购物车悬浮条。用户点击菜品时不是直接下单而是先加入本地购物车最后点「确认下单」才调接口。购物车用MapInteger, Integer存dishId到quantity的映射每次增减都更新底部总价。// 购物车管理类 public class CartManager { private static final MapInteger, Integer cart new LinkedHashMap(); private static final MapInteger, 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.EntryInteger, Integer entry : cart.entrySet()) { Dish dish dishCache.get(entry.getKey()); total total.add(dish.getPrice().multiply(new BigDecimal(entry.getValue()))); } return total; } public static ListOrderItemDTO buildOrderItems() { ListOrderItemDTO items new ArrayList(); for (Map.EntryInteger, 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.10.20.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-size5MB超过会抛异常前端要捕获并提示。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代替localhostAndroid 官方模拟器如果是真机调试要用电脑的局域网 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:usesCleartextTraffictrue否则请求直接被系统拦截日志里只报一个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-OriginAndroid 端其实不受同源策略限制但如果你用 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:...}有的直接返回数组。定义一个ResultT类所有接口都用它包装前端处理起来逻辑一致也显得你工程素养到位。public class ResultT { private int code; private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg 成功; r.data data; return r; } public static T ResultT fail(String msg) { ResultT r new Result(); r.code 500; r.msg msg; return r; } }最后说一个我自己的习惯答辩前把整个系统在另一台电脑上重新部署一遍从装 JDK、MySQL 到启动 Jar 包、安装 APK全程计时。如果超过 30 分钟说明你的部署文档写得不够细或者有隐藏的环境依赖。把这个过程写成一份README.md放在项目根目录老师如果问「你这个怎么跑起来」直接给他看文档比口头解释一百句都管用。希望帮到你。本文还有配套的精品资源点击获取
返回列表