简介这份资源是北方民族大学计算机科学与工程学院2014届学士学位论文《基于Web的网上订餐系统的设计与实现》作者付立泰指导教师张小伍适合计算机相关专业学生作为毕业设计选题参考也适合想了解B/S架构Web项目完整开发流程的初学者。论文围绕网上订餐系统的构建展开涵盖国内外研究现状调研、开发平台与语言工具选型并依次论述系统可行性分析、功能需求分析、数据库设计、详细功能实现与系统测试等关键环节涉及用户注册登录、菜品浏览、购物车管理、在线下单、支付与订单处理、服务评价等模块对订单管理、用户评价等业务逻辑有较完整的梳理。资源包共1个doc文件约10.7MB为完整论文文档结构规范、章节齐全可直接用于选题借鉴、开题报告撰写与论文框架参考。目前已有928人学习下载可作为毕业设计阶段快速了解同类系统设计思路与实现路径的参考资料。1. 从一份菜单到一套系统网上订餐到底在做什么很多人第一次接触「基于web的网上订餐系统的设计与实现」脑子里浮现的是一个点菜页面加一个购物车。真动手做才发现难点根本不在页面上而在「一份菜单怎么变成一张订单、一张订单怎么变成一笔可结算的账」。我见过不少同学把前端做得漂漂亮亮结果下单接口一并发就超卖或者订单状态在「已支付」和「已取消」之间反复横跳最后只能推倒重来。这个方向适合两类人一类是课程设计或毕业设计需要一套完整可演示的系统另一类是想练手一个「有真实业务闭环」的 Web 项目。它覆盖了用户认证、商品管理、购物车、订单状态机、库存扣减、支付回调这些典型场景麻雀虽小五脏俱全。下面我按自己实际搭过的一套方案把选型、建表、接口、并发和排错讲清楚能照着复现。2. 技术选型与数据建模先把账算明白再写代码2.1 为什么我建议前后端分离而不是服务端渲染早期做这类系统很多人用 JSP 或者模板引擎直接渲染页面好处是简单坏处是订单状态一变就得整页刷新移动端适配也麻烦。现在常见做法是前后端分离前端负责展示和交互后端只出 JSON 接口。这样订单列表、购物车数量这些需要局部刷新的地方前端自己控制后端逻辑也更干净。技术栈上后端我一般选 Spring Boot 或 Express/Koa数据库用 MySQL缓存用 Redis。选 Spring Boot 是因为它的生态里订单、支付、定时任务都有成熟组件选 Node 系是因为上手快适合快速验证。两者都能落地关键看团队熟悉度。前端用 Vue 或 React 都行本文不绑定具体框架接口约定清楚即可。提示不要一上来就上微服务。订餐系统的量级单体应用加 Redis 足够撑住拆服务只会让事务更难处理。2.2 核心表结构用户、菜品、订单、订单明细数据建模是这套系统的地基。我见过最典型的翻车是把菜品价格直接冗余进订单明细却不留快照结果商家改价后历史订单金额全变了。正确做法是下单时把菜品名称、单价、数量全部快照进订单明细。-- 用户表 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, -- 只存哈希不存明文 phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 菜品表 CREATE TABLE dish ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL, price DECIMAL(10,2) NOT NULL, -- 金额用 DECIMAL别用 FLOAT stock INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, -- 1 上架 0 下架 category_id BIGINT ); -- 订单表 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, -- 对外单号防遍历 user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0待支付 1已支付 2已取消 3已完成 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单明细表价格快照 CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, dish_name VARCHAR(128) NOT NULL, -- 快照 unit_price DECIMAL(10,2) NOT NULL, -- 快照 quantity INT NOT NULL );金额字段用DECIMAL(10,2)而不是FLOAT是因为浮点数在累加时会出现0.1 0.2 ! 0.3这类误差订单金额对不上账就是事故。order_no单独生成而不是直接用自增 id 对外暴露是为了防止别人顺序遍历你的订单。订单明细里的dish_name和unit_price是下单那一刻的快照之后商家改价不影响历史订单。2.3 订单状态机别让状态随便跳订单状态是整个系统最容易出玄学的地方。我一般会先画一张状态迁移表明确哪些状态能到哪些状态代码里只允许合法迁移。当前状态允许迁移到触发动作0 待支付1 已支付支付回调成功0 待支付2 已取消用户取消 / 超时未付1 已支付3 已完成商家确认出餐1 已支付2 已取消退款成功任何不在表里的迁移比如「已完成」再回到「待支付」代码里直接拒绝。这样能避免大量脏数据。状态字段用 TINYINT 存数字前端做映射展示比直接存中文字符串更省空间也更稳定。3. 从下单到支付把核心链路跑通3.1 下单接口库存扣减与超卖防护下单是并发问题最集中的地方。两个人同时买最后一份菜如果先查库存再扣减中间有窗口期就会超卖。常见做法有两种数据库乐观锁和 Redis 预扣减。量不大时用乐观锁就够。-- 乐观锁扣库存影响行数为 0 说明库存不足或被抢完 UPDATE dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity};// 伪代码下单主流程 Transactional public OrderVO createOrder(Long userId, ListOrderItemDTO items) { BigDecimal total BigDecimal.ZERO; for (OrderItemDTO item : items) { // 1. 扣库存返回受影响行数 int affected dishMapper.decreaseStock(item.getDishId(), item.getQuantity()); if (affected 0) { throw new BizException(菜品库存不足 item.getDishId()); } // 2. 查菜品快照 Dish dish dishMapper.selectById(item.getDishId()); total total.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 写订单 明细 Order order buildOrder(userId, total); orderMapper.insert(order); orderItemMapper.batchInsert(order.getId(), items); return toVO(order); }关键在WHERE stock #{quantity}这个条件它把「判断」和「扣减」合并成一条原子 SQL数据库层面保证不会扣成负数。整个方法加Transactional任何一步失败都回滚。注意扣库存和写订单必须在同一个事务里否则会出现扣了库存没生成订单的情况。3.2 支付回调幂等是保命符支付平台的回调可能重复发送网络抖动、平台重试都会导致同一个订单收到多次「支付成功」。如果不做幂等用户付一次钱订单被改成已支付两次甚至触发两次发货。// 支付回调处理用订单号 状态做幂等 public String handlePayCallback(String orderNo, String tradeNo) { Order order orderMapper.selectByOrderNo(orderNo); if (order null) { return FAIL; } // 只有待支付状态才处理已支付直接返回成功 if (order.getStatus() ! OrderStatus.PENDING_PAY) { return SUCCESS; } int updated orderMapper.updateStatus(orderNo, OrderStatus.PENDING_PAY, OrderStatus.PAID, tradeNo); if (updated 0) { // 并发下被别人先改了同样视为成功 return SUCCESS; } // 后续发货、通知等逻辑 return SUCCESS; }这里的幂等靠的是「带原状态的条件更新」UPDATE orders SET status1 WHERE order_no? AND status0。只有从待支付改成已支付这一条路径能成功重复回调时status已经不是 0更新影响行数为 0直接返回成功即可。这个模式我称之为「状态机 条件更新」比加分布式锁简单得多。3.3 购物车存 Redis 还是存数据库购物车有两种存法。存数据库的好处是换设备也能看到坏处是每次加减都要写库。存 Redis 的好处是快坏处是 Redis 挂了购物车就没了。我的经验是登录用户的购物车存 Redis并设置合理过期时间下单成功后清空对应项未登录用户存前端本地。# Redis 购物车结构Hashkey 为用户维度field 为菜品 id HSET cart:user:1001 dish:2001 2 HSET cart:user:1001 dish:2002 1 HGETALL cart:user:1001用 Hash 而不是 String是因为加减某个菜品时可以直接HINCRBY不用把整个购物车读出来再写回去。cart:user:1001这个 key 命名带业务前缀方便排查和批量清理。过期时间我一般设 7 天太短用户觉得购物车会丢太长占内存。4. 避坑与排查那些让我加班到凌晨的问题4.1 订单金额对不上浮点数和快照缺失现象是用户看到的金额和后台统计差几分钱。原因通常是金额用了FLOAT/DOUBLE或者订单明细没存价格快照统计时又去关联菜品表取当前价。解决方式是所有金额字段统一DECIMAL下单时把单价和名称写进明细表统计只读明细。这个坑我在两个项目里都踩过血泪经验就是钱相关的字段永远不要相信浮点数。4.2 库存扣成负数并发下的检查与扣减分离现象是库存出现 -1、-2。原因是代码写成「先 SELECT 查库存再 UPDATE 扣减」两个请求都查到库存为 1都去扣就成负数了。解决办法是把判断条件写进 UPDATE 的 WHERE 里用影响行数判断成败。如果用了 Redis 预扣减要注意 Redis 和数据库的一致性一般用「Redis 扣减成功后再异步落库 定时对账」的方式兜底。4.3 订单状态乱跳缺少状态机约束现象是已取消的订单又变成已支付或者已完成的订单被重复发货。原因是代码里到处直接UPDATE orders SET status?没有校验原状态。解决办法是封装统一的updateStatus(orderNo, fromStatus, toStatus)方法所有状态变更都走它非法迁移直接抛异常。再配合数据库的status字段加注释团队协作时不容易写错。4.4 支付回调重复触发没有幂等控制现象是用户付一次款收到两条发货通知。原因是回调接口没有做幂等平台重试就重复处理。解决办法是回调处理前先查订单状态只有待支付才处理且用条件更新保证并发安全。另外回调接口要能正确处理「订单不存在」「金额不符」这些异常情况返回平台约定的失败标识让它重试或告警。4.5 接口被刷缺少限流和参数校验现象是下单接口被脚本高频调用数据库压力飙升。原因是接口没有限流参数也没校验。解决办法是在网关或应用层加限流比如令牌桶对下单、支付这类写接口按用户维度限制频率同时严格校验数量必须为正整数、菜品必须存在且上架。别小看参数校验一个quantity-1就能把库存刷成正数。5. 进阶技巧让系统从能跑到好用5.1 用定时任务关掉超时未支付订单待支付订单如果一直不关库存就被占着。常见做法是起一个定时任务每分钟扫描创建时间超过 15 分钟且状态为待支付的订单把它们改成已取消并回滚库存。// 定时关单注意回滚库存也要走条件更新 Scheduled(cron 0 * * * * ?) public void closeExpiredOrders() { ListOrder expired orderMapper.selectExpired( OrderStatus.PENDING_PAY, LocalDateTime.now().minusMinutes(15)); for (Order order : expired) { int updated orderMapper.updateStatus(order.getOrderNo(), OrderStatus.PENDING_PAY, OrderStatus.CANCELLED); if (updated 0) { // 只有真正关单成功才回滚库存 orderItemMapper.rollbackStock(order.getId()); } } }这里的关键是「先条件更新状态成功后再回滚库存」避免定时任务和用户支付并发时把已支付订单误关。回滚库存同样用stock stock quantity不需要判断上限因为回滚是加操作。5.2 订单号生成别用时间戳裸奔很多人用System.currentTimeMillis()当订单号高并发下会重复。我一般用「日期 用户 id 后几位 随机数」或者雪花算法。订单号要满足两个条件全局唯一、不暴露自增规律。下面是一个简单可用的生成方式。public String generateOrderNo(Long userId) { String date LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); String userPart String.format(%04d, userId % 10000); String random String.format(%06d, ThreadLocalRandom.current().nextInt(1000000)); return date userPart random; // 共 18 位 }这个方案在单机下够用如果多实例部署随机数部分要加机器标识或者直接用雪花算法。订单号一旦生成就不允许修改它是支付、退款、对账的唯一凭证。5.3 验证系统是否真的可靠三个自测场景写完不等于能用我一般会做三个验证。第一用压测工具对下单接口发 100 个并发请求买同一份库存为 10 的菜看最终库存是不是 0 而不是负数。第二手动重复调用支付回调接口 5 次看订单状态是否只变一次、发货通知是否只发一条。第三把订单创建时间改到 20 分钟前等定时任务跑一轮看订单是否被正确关闭且库存回滚。这三个场景过了系统的基本可靠性就有了。我自己的习惯是每加一个写接口先问自己「这个操作重复执行两次会怎样」。如果答案是不确定那就还没写完。订单系统里没有后悔药幂等和状态机就是那颗后悔药。希望帮到你。本文还有配套的精品资源点击获取