☰
无线点餐系统毕设实战:局域网扫码点餐、并发扣库存与答辩避坑
2026/10/30 5:13:31 网站建设 项目流程

简介:这份毕业设计资料包围绕无线点餐系统展开,面向计算机相关专业准备毕业设计的学生,以及需要 Android 点餐项目参考的开发者,可用于选题落地、系统功能对照与源码学习。包内共 365 个文件,约 9.99MB,以 Java 源码和编译后的 class 为主体,配合 XML 布局、JPG/GIF 界面素材、jar 依赖及少量数据库与说明文档,能够呈现从移动端界面到后台业务处理的较完整结构。已有 239 人学习,说明该题目在毕设场景中具有一定关注度。读者可从中了解 Android 客户端中登录、菜单浏览、桌台选择、下单与支付等业务模块的组织方式,并参考服务端 Servlet/JSP 处理、数据库表设计与资源配置思路,快速搭建可运行的演示环境,适合作为功能拆解、代码研读和答辩准备的材料。

1. 无线点餐系统毕设到底交付什么:从"能演示"到"能答辩"的三条硬线

带过几届毕业设计之后我发现一个规律:选无线点餐系统的同学,八成不是卡在写不出代码,而是卡在答辩现场那台笔记本连不上手机。这类毕业设计题目的评分点从来不在功能数量,而在三件事——下单闭环能不能一次跑通、无线链路断掉之后系统会不会自己缓过来、数据库里那几张表经不经得起追问。这份资源围绕的正是这三条硬线:一套跑在局域网上的点餐系统,顾客端扫码点菜,后厨端出单,管理端改菜价,不依赖公网、不依赖云服务器,一台笔记本开热点就能完整演示。适合正在做同类毕业设计、手上已有半成品或者准备从零搭的同学,也适合需要给作品做验收参考的指导角色。

2. 系统骨架选型:端、网、库三层怎么拼才不用返工

动手之前先把三层的边界划清楚,后面能省掉大量返工。端指的是顾客手机上的点餐页、后厨的屏幕、管理端的后台;网指的不是"互联网",而是那一段无线局域网;库指菜品、订单、桌台这几张核心表。很多人一上来就冲着功能列表堆代码,结果写到一半发现桌号带不进去、订单状态没法回推、换台机器演示就全崩,只能推倒重来。这一章按端、网、库的顺序把选型的理由讲透,每条都落到具体配置上。

2.1 顾客端选小程序还是原生 App:答辩现场的决定因素

顾客端的第一诉求不是性能,是"评委掏出手机就能点"。原生 Android 要签名、要装包、部分机型还会拦截来路不明的 APK,现场演示时让评委等三十秒装应用,观感已经很差。小程序或者 H5 的路径是扫码即开,浏览器和小程序容器都能兜住,这是我在同类项目里优先推荐的方案。用一套跨端框架(uni-app、Taro 这类)写一份代码,编译到小程序和 H5 两个目标,小程序跑不通时立刻切 H5 顶上,属于答辩现场的后悔药。

选它要付的代价是网络策略。小程序类平台对请求域名有校验,必须是已备案的域名加 HTTPS,局域网 IP 直连是过不去的。开发阶段可以在开发者工具里勾上"不校验合法域名",真机预览时打开调试模式也能临时绕过;但打包成体验版分发给评委,这条限制会重新生效。所以更稳的路径是:顾客端主体用 H5(浏览器地址栏输入局域网 IP 就能访问,无任何校验),小程序版本作为加分项演示,两套共用同一份页面代码和同一套接口。H5 的代价是首屏加载略慢,但局域网内网速完全够用。

页面结构上别贪多,顾客端只要四个屏:桌号确认页、分类加菜品的列表页、购物车、订单详情。分类用左右联动列表,别做瀑布流,手指在手机上滑两下就乱。菜品图片按 200×200 压缩后存本地目录,不要引用外部图床,现场断网的场景下外链图片全挂是常见的翻车点。

2.2 后端从单体起步:别在毕设里上微服务

后端的形态直接影响部署难度。微服务、注册中心、网关这套东西放在生产环境有意义,放在一个要在一台笔记本上跑起来的毕业设计里,纯粹是给自己找麻烦——多一个进程就多一个启动失败的可能。我一般会用单体加内嵌容器的组合,打成一个可执行 jar,双击或者一行命令启动,端口固定,日志落到文件。

配置集中在一个application.yml里,下面这份是常用的最小集,端口、数据源、日志三块,多余的东西一概不留:

server: port: 8080 # 固定端口,二维码里的地址依赖它,演示前不要改 address: 0.0.0.0 # 必须监听全网卡,只绑 127.0.0.1 手机连不进来 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/dining?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: dining password: dining123 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 # 十来桌的并发,10 个连接绰绰有余 connection-timeout: 3000 mybatis: mapper-locations: classpath:mapper/*.xml logging: file: name: ./logs/app.log # 落盘,答辩出问题时直接翻日志,不靠猜 level: root: info

address: 0.0.0.0这一行是最容易被忽略的参数。默认只监听回环地址时,本机 curl 一切正常,手机怎么都连不上,排查半天以为是防火墙,其实是监听范围的问题。serverTimezone必须是Asia/Shanghai,缺了它订单时间会整体偏八小时,答辩时被问到"你这单是几点下的"会很难解释。

打包和启动用两条命令就够:

# 跳过测试打包,毕设阶段测试用例往往没维护,跑起来反而卡住 mvn clean package -DskipTests # 后台启动并把控制台输出重定向,避免关掉终端进程就没了 nohup java -jar target/dining-0.0.1.jar > ./logs/console.log 2>&1 &

第一条命令里的-DskipTests不是偷懒,是因为很多同学的测试用例依赖本地数据库状态,换台机器就红,打包直接失败。第二条的nohup ... &保证进程脱离终端,2>&1把错误流并进同一个文件,出问题时只看一个日志文件就够了。

2.3 局域网无线链路:固定 IP 是整套演示的地基

整套系统最脆的一环不是代码,是 IP。手机扫码拿到的链接里带着服务器地址,这个地址一旦在演示当天变了,二维码就全部作废。我见过不止一次现场翻车:前一天晚上调通,第二天换了间教室、换了个路由器,DHCP 重新分配,地址从 192.168.1.7 变成 192.168.1.12,二维码扫出来是空白页。

解决办法有两层。第一层是让地址尽量固定:用路由器管理页把服务器的 MAC 和 IP 做静态绑定,或者在操作系统里配静态地址,避开 DHCP 池。第二层更重要——二维码里的地址不要在代码里写死,而是运行时取当前请求的 host 动态拼。顾客端入口页拿到location.host传给后端,后端用它生成桌台二维码,换网段之后重新生成一次就行,不用改代码。

// 生成带桌号的点餐链接,host 用运行时值,不写死 IP function buildTableUrl(tableNo) { const host = location.host; // 例如 192.168.1.7:8080,演示当天是什么就是什么 const base = location.protocol === 'https:' ? 'https://' : 'http://'; return `${base}${host}/h5/index.html?tableNo=${encodeURIComponent(tableNo)}`; }

location.host自带端口,省掉单独拼端口的麻烦。encodeURIComponent不能省,桌号里出现A-01这类带连字符或中文的值时,不编码会被截断。

连接方式上,教室和实验室常见两种:一是服务器机器开热点,手机连这个热点,此时手机和服务器的网段由系统决定,Windows 热点默认段和 Linux 的不一样,要现场确认;二是所有人接同一台路由器的同一个 SSID,注意别让服务器连 5G 频段而手机连 2.4G 频段的不同 SSID,那是两个独立的逻辑网络,互相不通。第二种更稳,建议演示走路由器方案,热点方案留作备用。

2.4 数据库选 MySQL 还是 SQLite:看答辩会不会追问并发

SQLite 的优势是零部署,一个文件拷走就能跑,对只想交个能跑的作品来说很省事。但无线点餐这个题目天然带并发语义——多桌同时下单、后厨同时改状态、库存同时扣减,答辩时只要问一句"两个人同时点最后一份菜会怎样",SQLite 的单写锁就很难讲圆。我的建议是选 MySQL,用绿色版或者容器起一个,演示前导入脚本,成本并不高。

维度SQLiteMySQL
部署成本一个文件,零配置需起服务,导入账号和库
并发写库级写锁,写多时排队行级锁,条件更新可控
事务隔离支持,但锁粒度粗支持,可显式选隔离级别
答辩可讲性偏弱,难展开并发话题强,能讲行锁与死锁避免
数据迁移类型宽松,迁 MySQL 要清数据类型明确,脚本即真相

容器方式启动一条命令,账号密码和库名一次定好,团队成员拿到脚本照着跑就行:

docker run -d --name dining-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e MYSQL_DATABASE=dining \ -e MYSQL_USER=dining \ -e MYSQL_PASSWORD=dining123 \ -v "$PWD/mysql-data:/var/lib/mysql" \ mysql:8.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci

-v把数据目录挂到宿主机,容器删了数据还在,避免每次重来都要重新导一遍菜谱。字符集两个参数必须显式给,否则容器的默认字符集可能不是 utf8mb4,中文菜名会变成问号,这个坑在后面的排查章还会提到。如果你确实只能用 SQLite,那至少把金额存成整数分、把状态用固定枚举,这两条在迁移时能省掉大量清洗工作。

注意:数据库账号密码写在配置文件里没问题,但别把 root 账号直接给应用用,权限收敛到单库单表,答辩被问到安全设计时有话可说。

3. 数据模型与点餐主流程:五张表跑通一次完整下单

骨架搭好后,真正决定系统能不能讲清楚的是数据模型。餐桌、分类、菜品、订单、订单明细,这五张表的关系一旦理清,后面所有功能都是增删改查的组合。这一章从建表开始,一路写到一个完整下单事务,再到后厨状态流转,每一步都给可执行的代码和参数说明。

3.1 五张核心表与字段取值约定

建表阶段有三条约定值得坚持:金额一律存整数分,避免浮点误差;状态一律用 tinyint 加注释,不用字符串;桌号加唯一索引,防止同一桌产生并行的未结订单。下面这段是核心部分的建表语句,字段注释写全,答辩翻代码时能直接讲。

CREATE TABLE `dish` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `category_id` BIGINT NOT NULL COMMENT '所属分类', `name` VARCHAR(64) NOT NULL COMMENT '菜品名', `price_cent` INT NOT NULL COMMENT '单价,单位分', `stock` INT NOT NULL DEFAULT 0 COMMENT '可售份数,沽清置 0', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `image_path` VARCHAR(128) DEFAULT NULL COMMENT '相对路径,不放外链', `sort_no` INT NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_cat_status` (`category_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品'; CREATE TABLE `orders` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '业务单号,演示时便于口述', `table_no` VARCHAR(16) NOT NULL COMMENT '桌号', `total_cent` INT NOT NULL COMMENT '订单总额,单位分', `state` TINYINT NOT NULL DEFAULT 0 COMMENT '0待接单 1制作中 2已完成 3已取消', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_table_state` (`table_no`, `state`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; CREATE TABLE `order_item` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_id` BIGINT NOT NULL, `dish_id` BIGINT NOT NULL, `dish_name` VARCHAR(64) NOT NULL COMMENT '冗余快照,菜品改名后历史单不受影响', `price_cent` INT NOT NULL COMMENT '下单时单价快照', `qty` INT NOT NULL, PRIMARY KEY (`id`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细';

order_item里冗余dish_name和price_cent看着像重复设计,实际是必须的:菜品改价改名之后,历史订单还得显示下单当时的价格和名字。orders表的uk_order_no唯一索引在防重复提交上很关键,顾客手抖连点两次,第二次插入直接撞唯一键失败,比在业务层判重可靠。

编码统一 utf8mb4,不是 utf8。utf8 在部分版本里只有三字节,存不下商品名里偶尔出现的特殊符号,写进去直接报错。

3.2 下单接口:一个事务里做完校验、扣减、落单

下单是整个系统里唯一必须严格事务的地方,扣库存、写主单、写明细三步要么全成要么全废。常见写法是逐个校验再逐条插入,中间任何一步抛异常都由事务兜底回滚。

@Service public class OrderService { @Resource private DishMapper dishMapper; @Resource private OrderMapper orderMapper; @Resource private OrderItemMapper itemMapper; /** * 提交订单 * @param tableNo 桌号,扫码时由入口页带进来,不依赖登录态 * @param items 购物车条目,元素为 dishId + qty * @return 订单 id */ @Transactional(rollbackFor = Exception.class) public Long submit(String tableNo, List<CartItem> items) { if (items == null || items.isEmpty()) { throw new BizException("购物车为空"); } // 按 dishId 升序,保证多笔并发订单的加锁顺序一致,降低死锁概率 items.sort(Comparator.comparing(CartItem::getDishId)); int totalCent = 0; List<OrderItem> details = new ArrayList<>(items.size()); for (CartItem it : items) { Dish dish = dishMapper.selectById(it.getDishId()); if (dish == null || dish.getStatus() != 1) { throw new BizException("菜品已下架:" + it.getDishId()); } // 条件扣减:库存不足时返回 0,立刻抛异常让整个事务回滚 int affected = dishMapper.deductStock(it.getDishId(), it.getQty()); if (affected == 0) { throw new BizException("已售罄:" + dish.getName()); } OrderItem d = new OrderItem(); d.setDishId(dish.getId()); d.setDishName(dish.getName()); d.setPriceCent(dish.getPriceCent()); d.setQty(it.getQty()); details.add(d); totalCent += dish.getPriceCent() * it.getQty(); } Order order = new Order(); order.setOrderNo("T" + tableNo + System.currentTimeMillis()); order.setTableNo(tableNo); order.setTotalCent(totalCent); order.setState(0); // 0 = 待接单 orderMapper.insert(order); // 自增 id 回填到 order.id for (OrderItem d : details) { d.setOrderId(order.getId()); itemMapper.insert(d); } return order.getId(); } }

对应的扣减 SQL 是这套逻辑的核心,它把"判断库存够不够"和"扣减"合并成一条原子语句:

<update id="deductStock"> update dish set stock = stock - #{qty} where id = #{dishId} and status = 1 and stock >= #{qty} </update>

where里的stock >= #{qty}是关键。把它放在条件里,数据库的行锁保证同一行的并发更新串行执行,库存不足时影响行数为 0,Java 侧拿到 0 就抛异常。如果改成先select查库存、再判断、再update,两条并发请求可能都查到"还剩 1 份",然后各扣一次,最终库存变成负数,这就是最典型的超卖。

排序那一行也值得单独说。两笔订单如果按相反顺序去扣 A、B 两个菜品,很容易形成 A 等 B、B 等 A 的死锁。统一按 dishId 升序加锁,顺序固定,死锁概率大幅下降。rollbackFor = Exception.class显式指定所有异常都回滚,默认只回滚运行时异常,受检异常会漏网。

3.3 后厨出单:状态机与轮询间隔怎么定

订单状态只有四种,流转方向必须单向:待接单(0)→ 制作中(1)→ 已完成(2),任何一步都可以转成已取消(3)。后厨端的核心职责就是把 0 变 1、把 1 变 2,并且让顾客端看得见变化。

状态更新要用带前置状态的条件更新,防止两个后厨账号重复点击:

-- 接单:只有当前是待接单才能改成制作中,影响行数为 0 说明已被别人接过 update orders set state = 1 where id = #{orderId} and state = 0; -- 完成:同理,只允许从制作中流转 update orders set state = 2 where id = #{orderId} and state = 1;

顾客端获取状态用轮询就够,毕设规模下没必要上推送。轮询间隔取 3 秒是个经验值:太短给数据库添无谓压力,太长顾客会觉得卡。页面切到后台时要停掉定时器,切回来再恢复,否则手机锁屏时请求还在跑,耗电且无意义。

// 订单状态轮询,页面隐藏时暂停,可见时恢复 let timer = null; const POLL_INTERVAL = 3000; // 3 秒,毕设场景的平衡点 function startPoll(orderId) { stopPoll(); timer = setInterval(async () => { const res = await request({ url: `/api/order/${orderId}/state` }); if (res.data.state === 2) { stopPoll(); // 已完成就停,别一直空转 updateView('已完成'); } else { updateView(stateText[res.data.state]); } }, POLL_INTERVAL); } function stopPoll() { if (timer) { clearInterval(timer); timer = null; } } // 页面生命周期钩子里挂上,切后台暂停、回前台立即拉一次 onHide(() => stopPoll()); onShow(() => startPoll(currentOrderId));

后厨端还有个容易被忽略的点:新订单要能主动提醒。简单做法是轮询未接单数量,数量变化时播放一段本地音频,别用在线音频链接,局域网断外网时放不出来。音量调大,把平板音量锁死,现场嘈杂时靠声音比看屏幕可靠。

3.4 沽清与库存:演示前必须做的一次压力自测

菜品售罄的展示效果很直观,但也是最容易露馅的地方。演示前建议做一次最小自测:把某个菜品的库存改成 1,然后用两台手机(或者两个浏览器窗口)几乎同时下单这个菜,观察结果——应该是一单成功、一单提示售罄,数据库里库存为 0,订单明细只有一条。

自测脚本可以用简单的并发请求压一下接口:

# 用 ab 或 curl 并发打同一个接口,观察成功数是否等于库存数 # -n 总请求数,-c 并发数,两者相等即为瞬时并发 ab -n 10 -c 10 -p order.json -T application/json \ http://127.0.0.1:8080/api/order/submit

order.json里放的就是那份 dishId 加 qty 的购物车结构,-T指定 Content-Type,缺了它后端可能收不到 body。如果 10 个并发里有 9 个返回"售罄"、1 个成功,说明条件更新生效;如果成功了好几个,回去检查扣减 SQL 的where条件是不是漏了库存判断,或者事务是不是被@Transactional的同类内部调用绕过——同一个类里方法 A 调方法 B,B 上的事务注解是不生效的,这是 Spring 代理机制的经典坑。

库存这块还有一个产品层面的取舍:扣减发生在下单时还是结账时。下单扣减能防超卖,但顾客取消订单必须把库存还回去,取消接口里要做一次反向加库存,且只能加一次,用状态条件更新控制。结账扣减则相反,展示上不会出现"点着点着菜没了",但对账复杂。毕设推荐下单即扣,逻辑闭环清晰,答辩好讲。

4. 无线链路的坑:从"手机连不上"到"多端不同步"

代码在本地跑通只是及格线,能不能在无线环境下稳定跑起来才是分水岭。这一章集中处理网络层的问题,从最基础的连通性排查,到通信方式的取舍,再到断线之后如何不中断演示。

4.1 同网段判定与三步排查法

手机连不上服务器时,不要一上来就翻代码,先按网络层往上层走三步,绝大多数问题止步于第二步。

# 第一步:在服务器上确认自己监听的地址和端口 # Windows 用 netstat -ano | findstr 8080,Linux/macOS 用下面这条 ss -lntp | grep 8080 # 期望看到 0.0.0.0:8080,如果看到 127.0.0.1:8080 说明只监听本机,手机必然连不上 # 第二步:从另一台同网段的机器验证连通性 ping 192.168.1.7 # 先确认能通,不通就是网段或防火墙 curl -v http://192.168.1.7:8080/api/ping # 再确认端口和 HTTP 层,能返回就说明服务正常 # 第三步:在服务器上反查客户端,确认手机真的在同一网段 arp -a # 看手机 IP 是否出现在列表里,前三段应与服务器一致

三步对应的结论很清楚:第一步不通说明服务监听范围不对,改server.address;第二步 ping 通但 curl 超时,八成是系统防火墙拦了端口,Windows 要在入站规则里放行,Linux 用firewall-cmd --add-port=8080/tcp --permanent加--reload;第三步手机 IP 前三段和服务器不一致,说明连的不是同一个网络,回头检查 SSID 和频段。

还有一个隐蔽情况:服务器同时连着有线和无线,两张网卡各有 IP,服务监听了0.0.0.0,从手机访问哪个都能通,但生成的二维码里丢进去的是有线网卡那个 IP,手机自然打不开。演示前把不用的网卡禁掉,只留一张,这种玄学问题就不会出现。

4.2 轮询、SSE 还是 WebSocket:毕设规模下的取舍

通信方式的选择要把实现成本算进去,不能只看理论先进程度。

方式实现成本实时性断线恢复适用端
短轮询最低,一个 setInterval秒级天然无状态,天然恢复顾客端、后厨端
SSE中,需处理流式响应准实时需自己重连后厨看板
WebSocket高,需心跳与重连逻辑实时必须手写重连多端协同、叫号

十来桌的并发,短轮询完全撑得住,一个 3 秒间隔、单次响应几十字节的接口,对数据库的压力可以忽略。SSE 的好处是服务端单向推,后厨看板用它体验更好,缺点是部分容器和代理会缓冲响应,需要显式关掉缓冲头,否则消息会攒着一起到,看着像卡死。WebSocket 在毕设里最大的风险不是写不出来,是断线之后不自动重连,现场网络抖一下,页面从此不再更新,评委看到的就是一套"死"系统。

我的取舍是:顾客端和后厨端都用轮询,只把订单状态这类小数据量接口暴露出来;如果时间允许,后厨看板额外加一条 SSE 通道做叫号提醒,轮询保留作为兜底。别把 WebSocket 当成必选项,除非题目本身要求实时协同。

4.3 断线重连与本地缓存:让演示不中断

无线环境下的抖动是常态,系统要做的是抖动之后能自己缓过来,而不是把错误弹窗甩给顾客。前端请求层统一加超时、重试和失败缓存,顾客端的购物车直接落到本地存储,页面刷新不丢。

// 统一请求封装:超时、单次重试、失败落本地缓存 const CACHE_KEY = 'cart_cache'; async function request(options, retry = 1) { const { url, method = 'GET', data = {} } = options; try { const res = await uni.request({ url: BASE_URL + url, method, data, timeout: 5000, // 5 秒够局域网用了,再长用户会以为卡死 }); if (res.statusCode !== 200) throw new Error('HTTP ' + res.statusCode); return res.data; } catch (e) { if (retry > 0) { await sleep(800); // 稍等再试,避免瞬间重试撞上同一个抖动点 return request(options, retry - 1); } // 最终失败:购物车落本地,网络恢复后由用户手动重提,不静默丢弃 if (url.includes('/order/submit')) { uni.setStorageSync(CACHE_KEY, data); } throw e; } }

timeout: 5000是个务实的值,局域网正常响应在 100 毫秒级,5 秒还没回来说明链路已经断了,继续等没有意义。重试只做一次,做多了会让用户感觉双击了两次,反而增加重复下单风险——真正的防重靠后端的唯一索引和幂等设计。缓存只在提交订单失败时写入,普通的查询失败不缓存,避免展示过期数据。

后厨端还要处理一种情况:平板息屏之后浏览器的定时器被系统冻结,唤醒后第一件事应该是立即拉一次全量未完成订单,而不是等下一个轮询周期。这个补拉逻辑放在页面可见性变化的回调里,一行判断就能补上。

4.4 二维码与桌号绑定:扫码进来怎么知道是哪一桌

桌号是整条链路的入口参数,它的传递方式决定了后面要不要做登录。最省事的方案是把桌号编进二维码的 URL 里,顾客扫码直接落到带参数的页面,不需要注册、不需要选桌。

// 入口页解析桌号,解析不到就落到手输页,别直接白屏 const params = new URLSearchParams(location.search); const tableNo = params.get('tableNo'); if (!tableNo) { showTableInput(); // 兜底:让顾客手输桌号,演示时能救场 } else { uni.setStorageSync('table_no', tableNo); // 存下来,后续请求都带它 loadMenu(); }

URLSearchParams在现代浏览器和小程序容器里都能用,不需要额外引库。手输兜底这条分支一定要留,二维码贴纸被磨损、光线太暗扫不出来的时候,现场能手动输入,比重新打印二维码快得多。

桌号状态还要和订单关联:同一桌在下单后、结账前,再次扫码应当能看到当前未结订单,而不是重新开一单。实现上就是按table_no加state in (0,1)查一次,查到就跳订单详情,查不到才进点餐页。这个逻辑不做,演示时评委扫两次码会看到两个购物车,解释起来很被动。

5. 答辩前的避坑清单:五个真实翻车现场与修法

下面这五条都是同类项目里反复出现的,每条按现象、原因、解决写,演示前一天照着过一遍,能省掉大部分现场事故。

5.1 手机扫码打开是空白页

现象:二维码能识别,跳转之后一直白屏,浏览器地址栏的 IP 和端口都正常。

原因:九成是前端静态资源打包时的publicPath配成了绝对路径/,页面从子目录或者带参数的地址打开时,JS 和 CSS 相对路径解析错位,资源 404,页面自然是白的。剩下一成是构建产物没同步到服务端的静态目录,本地开发时用的是 dev server,打包之后忘了拷。

解决:把构建配置的publicPath改成./,用相对路径;部署脚本里加一步校验,确认index.html引用的 JS 文件在磁盘上真实存在。排查时按 F12 看 Network 面板,红色 404 会直接指出是哪个文件。

5.2 菜品图片全挂,列表只剩文字

现象:本地开发时图片正常,换到演示环境全部裂开。

原因:图片地址写的是localhost:8080/upload/xxx.jpg或者外部图床链接。前者在手机上localhost指向手机自己,当然取不到;后者在无外网的教室环境里必然超时。

解决:图片路径一律存相对路径(如/upload/xxx.jpg),前端拼接时用运行时的BASE_URL,不写任何主机名。所有图片随项目一起打包,放在服务端静态目录下。

5.3 订单时间整体差八小时

现象:数据库里存的时间和实际下单时间对不上,差整整八小时,后厨看到的单号时间也是错的。

原因:JDBC 连接串少了serverTimezone参数,或者数据库服务端时区是 UTC,而应用侧按东八区解析,两头不一致。容器启动时没指定时区也会出现同样结果。

解决:连接串固定带上serverTimezone=Asia/Shanghai;容器启动加-e TZ=Asia/Shanghai;应用启动参数里加-Duser.timezone=Asia/Shanghai。三处都对齐之后,时间不会再有偏差。

5.4 端口被占,服务起不来

现象:启动日志报Port 8080 was already in use,换个端口重启,二维码里的地址又对不上了。

原因:上一次的进程没有真正退出,nohup起的 java 进程还在后台占着端口;或者演示机器上装了别的服务默认也用 8080。

解决:先查占用进程再决定杀不杀,别直接换端口,换了端口二维码和配置全要改。

# Linux/macOS:查占用 8080 的进程并结束 lsof -i:8080 kill -9 <PID> # Windows:先找 PID 再结束 netstat -ano | findstr :8080 taskkill /PID <PID> /F

5.5 中文菜名变成问号或乱码

现象:管理端录入的菜名一切正常,顾客端显示成问号;或者反过来,数据库里存的就是乱码。

原因:链路上有任意一环字符集不是 utf8mb4——数据库建库语句、表字符集、JDBC 连接串的characterEncoding、容器启动参数,四处只要有一处是 latin1 或者三字节 utf8,中文就会坏在那一环。

解决:四处统一成 utf8mb4。建库用CREATE DATABASE dining DEFAULT CHARSET utf8mb4,连接串带characterEncoding=utf8(驱动会映射到 utf8mb4),容器启动带--character-set-server=utf8mb4。已经存坏的字段修不回来,只能清表重导。

提示:演示前把数据库整库导出一次,命名带上日期,出问题直接回滚到这份快照,比现场修数据快得多。

6. 把演示跑成一段可控流程:预演脚本与验收清单

功能做完不等于能演示。答辩那十几分钟里,你需要的是一条不会出岔子的操作路径,而不是一堆散落的功能。我的习惯是准备一个一键启动脚本加一份按秒计的演示序列,提前把每一步都跑熟。

启动脚本把数据库、应用、静态资源三件事串起来,中间加健康检查,起不来就直接报错,不要等到扫码才发现:

#!/usr/bin/env bash set -e # 任一步失败立即退出,别带着半个环境继续演示 echo "[1/3] 启动数据库容器" docker start dining-mysql >/dev/null 2>&1 || docker run -d --name dining-mysql \ -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root123 -e MYSQL_DATABASE=dining \ -e MYSQL_USER=dining -e MYSQL_PASSWORD=dining123 \ -v "$PWD/mysql-data:/var/lib/mysql" mysql:8.0 --character-set-server=utf8mb4 echo "[2/3] 等待数据库就绪" for i in $(seq 1 30); do if docker exec dining-mysql mysqladmin ping -uroot -proot123 --silent; then break; fi sleep 1 done # 最多等 30 秒,容器冷启动通常 10 秒内就绪 echo "[3/3] 启动应用并做健康检查" nohup java -jar target/dining-0.0.1.jar > ./logs/console.log 2>&1 & for i in $(seq 1 30); do if curl -sf http://127.0.0.1:8080/api/ping >/dev/null; then echo "服务已就绪:$(hostname -I | awk '{print $1}'):8080" exit 0 fi sleep 1 done echo "启动超时,请查看 ./logs/console.log" >&2 exit 1

set -e是整段脚本的保险丝,任一步失败立刻停,避免后面带着残缺环境继续跑。健康检查循环等的是接口返回而非进程存在,进程活着但 Spring 上下文没起来的情况很常见,只看进程会误判。最后打印本机 IP,直接照着它生成二维码。

演示序列建议固定成下面这七步,每步控制在三十秒内,行数不够就砍功能,绝不临场加戏:

顺序操作观察点兜底方案
1展示桌台二维码手机扫码落到点餐页手输桌号入口
2加两个菜下单提示下单成功、生成单号提前备好的截图
3切后厨看板3 秒内出现待接单手动刷新页面
4接单转制作中顾客端状态同步变化口头说明状态机
5菜品沽清再次下单提示售罄用接口直接改库存
6完成订单并结账桌台释放、可再次下单清空该桌数据重试
7管理端改价顾客端刷新显示新价跳过,不作为重点

第三步和第四步之间留一点等待时间,让评委亲眼看到状态变化,这比嘴上讲十句实时同步有用。第五步的沽清是加分项,它顺带把库存并发的话题引出来,主动权在你手上。

再补两个现场技巧。一是准备第二个二维码,内容指向一个纯静态的菜品图页面,万一主系统彻底挂了,至少还能展示界面设计,答辩不至于冷场。二是把日志窗口缩到副屏或者分屏的一角,出问题时能立刻说是哪个接口超时,而不是站在那里猜——这种时候能报出具体错误码,比功能本身更能体现工程素养。

血泪经验是:每次演示前,我都会强制走一遍"拔网线再插回来"的流程,确认系统能自己恢复。有一回就是因为这个动作,提前发现了后厨看板在断网重连后不再轮询的问题,改起来只花了十分钟,但如果留到现场,就是一次无法解释的停摆。希望帮到你。

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

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

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

立即咨询