多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

微信点餐小程序毕设:SSM+MySQL完整实现与避坑指南

微信点餐小程序毕设:SSM+MySQL完整实现与避坑指南 简介微信点餐小程序毕业设计完整资料基于微信小程序与SSM框架、MySQL数据库实现面向计算机相关专业毕业生及Java Web学习者。资源覆盖用户登录注册、喜欢列表、热菜推荐和订单生成等核心功能模块从需求分析、总体设计到系统测试均有文档支撑可帮助快速完成毕业设计或课程项目。压缩包共1262个文件约60.63MB包括java后端源码、vue前端页面、小程序wxml/wxss、SQL数据库脚本、项目配置文件、毕业论文doc与答辩pptx以及视频演示mp4等结构完整便于对照学习。已有133人学习下载适合需要完整项目方案、开题与答辩材料或想参考实际小程序点餐业务逻辑的开发者。1. 微信点餐小程序毕设为什么这套技术栈值得选以及你将要面对的真实工作量微信点餐小程序作为毕业设计选题这几年一直是 Java 方向学生的热门选择。它的核心价值在于一条完整链路用户在微信小程序里浏览菜品、加入购物车、提交订单后端用 SSM 框架处理业务逻辑MySQL 负责数据持久化。技术栈不算新但覆盖面正好卡在课程重点和面试常问之间而且天然自带移动端交互场景演示效果比纯网页直观得多。这套方案适合两类人一是想稳扎稳打完成毕设、把基础框架完整走通的学生二是想拿它做课程项目或外包练手、需要一套可复用底座的开发者。接下来的内容按数据库设计、后端接口、小程序对接、联调避坑、答辩收尾五个环节展开全程围绕能跑通、能讲清、能答辩这三个目标。2. 先把数据库立住点餐业务的 7 张表设计与订单状态机2.1 为什么要先设计表结构而不是先写代码做毕设最容易翻车的方式就是打开 IDE 直接写 Controller写到一半发现购物车数据没地方放、订单明细拆不出来又回头改表。数据库是整套系统的地基表结构一旦定下来后端 Service 怎么写、小程序页面怎么渲染都跟着它走。对于点餐这类业务核心诉求非常明确用户能浏览菜品、把菜品加入购物车、提交订单、查看订单状态。围绕这四个动作数据模型可以拆成用户—菜品两个主维度再用购物车和订单把行为串起来。这套设计在答辩时也很有优势。评委大概率会问为什么这么设计表如果你的回答是用户表存登录态、菜品表存商品信息、订单表和明细表是一对多关系就已经把数据库设计的核心逻辑讲清楚了。比一上来谈索引优化、分库分表要务实得多也更符合毕设的评分预期。2.2 7 张核心表的建表语句与字段设计我一般会建 7 张表用户表、菜品分类表、菜品表、购物车表、订单表、订单明细表、轮播图表。下面直接给可执行的建表 SQL字段注释已经写在里面了复制到 Navicat 或命令行执行即可。-- 用户表一个微信用户对应一条记录openid 是唯一标识 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信 openid用户唯一标识, nickname VARCHAR(64) DEFAULT NULL COMMENT 用户昵称, avatar_url VARCHAR(255) DEFAULT NULL COMMENT 头像 URL, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号预留字段, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;-- 菜品分类表如主食、小吃、饮品 CREATE TABLE category ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 分类名称, sort INT DEFAULT 0 COMMENT 排序权重数字越小越靠前, status TINYINT DEFAULT 1 COMMENT 1 启用0 停用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品分类表;-- 菜品表价格用 DECIMAL 而不是 FLOAT避免金额精度问题 CREATE TABLE dish ( id INT NOT NULL AUTO_INCREMENT, category_id INT NOT NULL COMMENT 所属分类 ID, name VARCHAR(64) NOT NULL COMMENT 菜品名称, price DECIMAL(10,2) NOT NULL COMMENT 单价精确到分, image VARCHAR(255) DEFAULT NULL COMMENT 菜品图片 URL, description VARCHAR(255) DEFAULT NULL COMMENT 菜品描述, status TINYINT DEFAULT 1 COMMENT 1 在售0 下架, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表;-- 购物车表user_id dish_id 唯一避免同一用户重复添加同一菜品 CREATE TABLE cart ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 用户 ID, dish_id INT NOT NULL COMMENT 菜品 ID, quantity INT DEFAULT 1 COMMENT 数量, checked TINYINT DEFAULT 1 COMMENT 是否勾选1 勾选 0 未勾选, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_dish (user_id, dish_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车表;-- 订单表一个订单对应一个用户状态用 TINYINT 存储 CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号前端展示用, user_id INT NOT NULL COMMENT 下单用户 ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT DEFAULT 0 COMMENT 0 待付款 1 待制作 2 待取餐 3 已完成 4 已取消, address VARCHAR(255) DEFAULT NULL COMMENT 收货地址自取可留空, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;-- 订单明细表记录每个订单包含哪些菜品与订单表是一对多关系 CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL COMMENT 订单 ID, dish_id INT NOT NULL COMMENT 菜品 ID, dish_name VARCHAR(64) NOT NULL COMMENT 菜品名称下单时快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, quantity INT NOT NULL COMMENT 购买数量, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;-- 轮播图表首页顶部广告位毕设加分项 CREATE TABLE banner ( id INT NOT NULL AUTO_INCREMENT, image VARCHAR(255) NOT NULL COMMENT 轮播图 URL, sort INT DEFAULT 0 COMMENT 排序, status TINYINT DEFAULT 1 COMMENT 1 启用0 停用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT轮播图表;几点设计说明user表的openid必须加唯一索引因为微信登录后拿到 openid 就要按它查用户菜品价格用DECIMAL(10,2)而不是FLOAT金额用浮点存储会出现 0.1 0.2 不等于 0.3 的精度问题order_item里的dish_name和price是快照字段等用户下单之后再改菜品价格历史订单依然能显示正确的订单快照。cart表加uk_user_dish唯一索引是为了防止同一个用户把同一道菜加出多条购物车记录。2.3 订单状态机用一张表说清状态流转订单状态是答辩时最容易深度追问的点建议一开始就按状态机设计。我常用 5 个状态用数字表示比字符串更省空间业务判断时也更快。状态值含义可流转到的状态0待付款1、41待制作2、42待取餐33已完成无终态4已取消无终态这个流转规则要先写死在脑子里再写进代码。最简单的做法是在 Service 层定义一个状态校验方法每次更新前查一次当前状态不允许跳变。比如用户点击取消订单只在状态为 0待付款时才放行已经进入 1待制作的订单必须走商家取消逻辑。这块在答辩时能体现你对业务完整性的考虑很加分。提示订单状态不要用字符串直接存在数据库里。PAYED、FINISHED 这类写法查询慢、容易拼错TINYINT 加注释才是稳妥做法。3. 用 SSM 把后端接口写出来工程结构、核心 Controller 与 MyBatis 映射3.1 Maven 工程结构与关键依赖SSM 指的是 Spring SpringMVC MyBatis 的组合。Spring 管对象SpringMVC 管接口路由MyBatis 管数据库操作三者各司其职。工程结构我习惯按 controller、service、mapper、entity、common 五个包组织清晰也符合课程设计的规范。先看一下 pom.xml 里最关键的几个依赖版本号按你自己本地的 Spring 环境对齐即可!-- SpringMVC处理接口请求 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.x/version /dependency !-- MyBatis 与 Spring 整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.x/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.x/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.x/version /dependency !-- 数据库连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.x/version /dependency !-- JSON 序列化 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.x/version /dependency如果你的工程是用 IDEA 建的 Spring Initializr 项目只需要把 mybatis、mybatis-spring、mysql-connector-java、druid 手动加进去其余 Spring 系列依赖会由父工程统一管理。注意 mybatis-spring 的版本要和 Spring 版本兼容2.0.x 对应 Spring 5别混用。3.2 Spring 与 MyBatis 的整合配置SSM 整合的配置文件比 Spring Boot 繁琐一些核心是 spring-mybatis.xml。这个文件把数据源、SqlSessionFactory、Mapper 扫描三件事一次性搞定写完基本不用再碰。?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd !-- 读取数据库配置 -- context:property-placeholder locationclasspath:jdbc.properties/ !-- Druid 数据源 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean !-- SqlSessionFactory指定数据源和 mapper.xml 位置 -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.demo.canteen.entity/ /bean !-- 扫描 Mapper 接口 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.demo.canteen.mapper/ /bean /beansjdbc.properties 文件放在 src/main/resources 下jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/canteen?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的数据库密码serverTimezoneAsia/Shanghai必须加不加会报时区错误这是新手最常见的报错之一。useSSLfalse是本地开发时减少警告用的。数据库名 canteen 换成你自己创建的库名即可。3.3 登录接口与下单接口从 Controller 到 MyBatis 映射我先写最核心的登录接口。微信小程序端调用wx.login拿到一个临时 code后端拿这个 code 去微信接口换 openid。这个调用只能在后端做因为需要 AppSecret不能暴露在小程序代码里。RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; /** * 微信登录小程序传 code后端换 openid查不到就自动注册 */ PostMapping(/login) public Result login(RequestBody MapString, String params) { String code params.get(code); if (StringUtils.isBlank(code)) { return Result.error(code不能为空); } // 调用微信 code2Session 接口传入 appid、secret、code返回 openid String openid userService.getOpenid(code); // 按 openid 查用户查不到说明是新用户自动注册 User user userService.findByOpenid(openid); if (user null) { user userService.register(openid); } return Result.ok(user); } }这段代码要注意两个职责区分getOpenid负责网络请求findByOpenid和register负责数据库操作Service 层把两块逻辑串起来。Result是我定义的工具类统一包装返回体为 code、msg、data 三个字段小程序端解析时只用判断 code 是否为 0。再看下单接口这是业务逻辑最密集的一个接口涉及金额计算、订单生成、明细写入、购物车清空四步。我用Transactional保证多表操作要么全成功要么全回滚。RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; /** * 提交订单从购物车勾选的菜品生成订单 */ PostMapping(/create) public Result create(RequestBody OrderVO orderVO) { // 1. 根据用户 ID 查询购物车中勾选的菜品列表 // 2. 遍历菜品计算总金额 // 3. 生成唯一订单号保存订单主表 // 4. 批量保存订单明细 // 5. 删除已下单的购物车记录 Long orderId orderService.createOrder(orderVO.getUserId(), orderVO.getAddress(), orderVO.getRemark()); return Result.ok(orderId); } }Service public class OrderServiceImpl implements OrderService { Autowired private CartMapper cartMapper; Autowired private OrderMapper orderMapper; Autowired private OrderItemMapper orderItemMapper; Override Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, String address, String remark) { // 查询购物车勾选的菜品 ListCartVO cartList cartMapper.selectCheckedByUserId(userId); if (cartList null || cartList.isEmpty()) { throw new BusinessException(购物车为空); } // 计算总金额 BigDecimal total BigDecimal.ZERO; for (CartVO cart : cartList) { total total.add(cart.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); } // 生成订单号时间戳 用户ID 随机数 String orderNo System.currentTimeMillis() userId RandomUtil.randomNumbers(4); // 插入订单主表 Orders order new Orders(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(total); order.setStatus((byte) 0); order.setAddress(address); order.setRemark(remark); orderMapper.insert(order); // 插入订单明细 for (CartVO cart : cartList) { OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setDishId(cart.getDishId()); item.setDishName(cart.getDishName()); item.setPrice(cart.getPrice()); item.setQuantity(cart.getQuantity()); orderItemMapper.insert(item); } // 清空购物车 cartMapper.deleteCheckedByUserId(userId); return order.getId(); } }金额计算用BigDecimal而不是double。double计算金额会出现 0.1 0.2 0.30000000000000004 这类精度问题下单金额和数据库对不上非常难受。BigDecimal.valueOf(quantity)把 int 转成 BigDecimal 再参与乘法保证金额精确到分。MyBatis 的 XML 映射文件写在 resources/mapper 目录下比如 CartMapper.xml 里的查询勾选菜品select idselectCheckedByUserId resultTypecom.demo.canteen.vo.CartVO SELECT c.id AS id, c.dish_id AS dishId, c.quantity AS quantity, d.name AS dishName, d.price AS price, d.image AS image FROM cart c LEFT JOIN dish d ON c.dish_id d.id WHERE c.user_id #{userId} AND c.checked 1 /select这里用LEFT JOIN关联菜品表查出菜品名称和价格快照。resultType 直接映射到 CartVO因为 CartVO 里有 dishId、dishName、price、quantity 这些字段和查询结果别名一一对应就不需要写 resultMap 了。如果你发现某个字段查出来是 null先检查 SQL 别名和 VO 字段名是否一致这是 MyBatis 映射最常见的玄学问题其实根本不是玄学就是名字对不上。4. 小程序端对接登录态、购物车与下单流程的代码级实现4.1 wx.request 的统一封装避免每个页面重复写请求逻辑小程序端没有 axios所有网络请求都走wx.request。如果每个页面都写一遍完整配置代码会非常冗余而且出错时排查困难。我习惯在一开始就封装一个request.js统一管理 BaseURL、超时时间、错误提示。// utils/request.js const BASE_URL http://localhost:8080 // 后端接口地址上线时改成 HTTPS 域名 function request(url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method || GET, data: data || {}, header: { Content-Type: application/json }, timeout: 10000, success: (res) { // 后端统一返回 { code:0, msg:ok, data:... } if (res.data.code 0) { resolve(res.data.data) } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }) reject(res.data) } }, fail: (err) { wx.showToast({ title: 网络异常请检查后端服务, icon: none }) reject(err) } }) }) } module.exports request封装之后每个页面的请求代码就从十几行缩减到两三行而且错误提示统一。这里有个关键点后端返回的 code 必须约定好0 表示成功非 0 表示业务失败。不要在 success 里只判断 HTTP 状态码因为 HTTP 200 不代表业务成功业务异常同样会返回 HTTP 200 但 code 非 0。这个坑如果不提前约定小程序端会反复出现明明有数据但页面不渲染的情况。4.2 登录态处理从 wx.login 到 openid 的完整流程微信小程序的登录态逻辑是固定的app.js 的 onLaunch 里调用wx.login拿 code传给后端换 openid再把用户信息存到 storage。不传 code 直接让用户填手机号登录的方案绕开了微信体系反而会在答辩时被问住。标准流程如下// app.js const request require(./utils/request) App({ onLaunch: function () { // 先检查本地是否已有用户信息 const userInfo wx.getStorageSync(userInfo) if (userInfo userInfo.id) { this.globalData.userInfo userInfo return } // 调用 wx.login 获取临时 code wx.login({ success: (res) { const code res.code // 传给后端后端用 code 换 openid 并返回用户数据 request(/api/user/login, POST, { code: code }) .then((data) { wx.setStorageSync(userInfo, data) this.globalData.userInfo data }) } }) }, globalData: { userInfo: null } })wx.login得到的 code 有效期很短只能使用一次所以每次冷启动都要重新调。后端拿到 code 后换 openid再查数据库。用户的 id 就是后续所有业务接口的入参购物车、下单都要用到。不要尝试用昵称、手机号做用户标识小程序端拿到的昵称可以随时改openid 才是微信体系里用户维一的身份标识这个观念会在答辩时被重点确认。4.3 购物车与提交订单页面逻辑的数据流购物车页面是点餐小程序交互最核心的部分。用户从首页菜品列表点加入购物车到购物车页修改数量、勾选、提交订单整个数据流是这样的首页把 dishId 传给购物车接口后端在购物车表插入或更新数量购物车页加载时调查询接口展示菜品名、单价、数量、勾选态提交时把所有勾选的菜品的 quantity 汇总传给订单接口。// pages/cart/cart.js const request require(../../utils/request) Page({ data: { cartList: [], totalAmount: 0.00, selectedCount: 0 }, onShow: function () { this.loadCart() }, // 加载购物车列表 loadCart: function () { const userInfo wx.getStorageSync(userInfo) if (!userInfo || !userInfo.id) { return } request(/api/cart/list?userId${userInfo.id}, GET) .then((data) { const cartList data.map(item { // 后端返回的 price 是字符串转成浮点数用于前端计算展示 return { ...item, price: parseFloat(item.price).toFixed(2) } }) this.setData({ cartList: cartList }) this.calcTotal() }) }, // 勾选/取消勾选 toggleCheck: function (e) { const id e.currentTarget.dataset.id const cartList this.data.cartList const index cartList.findIndex(item item.id id) cartList[index].checked !cartList[index].checked this.setData({ cartList: cartList }) this.calcTotal() }, // 计算总金额 calcTotal: function () { let total 0 let count 0 this.data.cartList.forEach(item { if (item.checked) { total item.price * item.quantity count item.quantity } }) this.setData({ totalAmount: total.toFixed(2), selectedCount: count }) }, // 提交订单 submitOrder: function () { const userInfo wx.getStorageSync(userInfo) const checkedItems this.data.cartList.filter(item item.checked) if (checkedItems.length 0) { wx.showToast({ title: 请先勾选菜品, icon: none }) return } request(/api/order/create, POST, { userId: userInfo.id, address: 食堂自取, remark: }).then((orderId) { wx.showToast({ title: 下单成功, icon: success }) // 下单成功后跳转订单详情页 wx.redirectTo({ url: /pages/order/detail?id${orderId} }) }) } })购物车页面的parseFloat(item.price).toFixed(2)是必要操作因为后端返回的 DECIMAL 字段在 JSON 序列化后可能是字符串。前端参与计算时必须显式转成数字否则 12.00 8.50 会变成字符串拼接的 12.008.50。这个坑几乎每个人都踩过列在避坑章里重点讲。提示小程序端不要直接操作数据库所有数据变更必须走后端接口。在控制台里手动改 storage 里的数据只是改本地缓存刷新后会被后端数据覆盖。5. 联调与部署避坑5 个真实翻车点与排查路径5.1 小程序请求一直 fail域名校验和开发者工具设置现象小程序页面打开所有请求都进入 fail 回调控制台报 request:fail。原因微信开发者工具默认开启合法域名校验小程序要求所有请求域名必须在微信公众平台配置为 HTTPS 白名单。本地开发时后端跑在 http://localhost:8080根本不在白名单里请求直接被拦截。解决开发阶段在微信开发者工具的详情 → 本地设置里勾选不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书这个问题就消失了。上线前必须把后端部署到 HTTPS 域名并在公众平台配置 request 合法域名否则真机体验版也会请求失败。5.2 后端启动报时区错误MySQL 连接串少了一个参数现象Tomcat 启动时控制台报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者数据库查询正常但保存时间比本地早 8 小时。原因MySQL 8.x 的驱动对时区敏感连接串里没有指定 serverTimezone 时会读取系统时区报错或默认使用 UTC。解决把 jdbc.url 改成jdbc:mysql://localhost:3306/canteen?serverTimezoneAsia/Shanghai问题立即解决。如果已经出现时间字段误差改完连接串重启服务即可。顺带建议所有时间字段都用 DATETIME 类型统一存北京时间不要混用 TIMESTAMP减少后续时间处理的混乱。5.3 购物车重复添加同一个菜品唯一索引加 UPSERT现象用户把同一道红烧肉加了三次购物车列表出现三行一样的菜各自数量为 1。用户还得手动删体验很差。原因插入购物车时用INSERT INTO cart (user_id, dish_id, quantity) VALUES (?, ?, 1)每次加购都插入新记录没有处理已存在的场景。解决利用uk_user_dish唯一索引把插入语句改成INSERT ... ON DUPLICATE KEY UPDATE quantity quantity 1。这样第一次加购插入新记录第二次加购命中唯一索引后直接把数量加 1一行 SQL 解决重复问题。对应的 MyBatis mapper 里这样写insert idaddToCart INSERT INTO cart (user_id, dish_id, quantity, checked) VALUES (#{userId}, #{dishId}, 1, 1) ON DUPLICATE KEY UPDATE quantity quantity 1 /insert5.4 金额显示错乱DECIMAL 与前端浮点运算的精度现象下单总金额显示 24.300000000000004后端入账金额 24.30前后端对不上。原因后端用 BigDecimal 计算没问题但前端用0.1 * 3这类浮点运算时JavaScript 的 IEEE 754 浮点表示会产生精度误差。解决前端每次收到金额字段就parseFloat(item.price).toFixed(2)参与计算时统一先转数字再算最后用 toFixed(2) 格式化展示。前端参与金额计算只有这几种场景加购计算、购物车勾选计算、订单详情展示单独封装一个formatPrice函数复用不要每个页面各写一套。5.5 订单状态被跳过后端不校验状态流转现象用户提交订单后状态是 0待付款跳过付款直接给后端发请求把状态改成 3已完成订单显示完成但商家根本没收到新订单。原因更新订单状态的后端接口没有做前置校验只要传订单 ID 和新状态就执行 UPDATE。解决Service 层加状态机校验只有当前状态符合流转规则才允许更新/** * 校验订单状态流转是否合法 */ public void checkStatusTransition(byte currentStatus, byte targetStatus) { if (currentStatus 0 (targetStatus 1 || targetStatus 4)) return; if (currentStatus 1 (targetStatus 2 || targetStatus 4)) return; if (currentStatus 2 targetStatus 3) return; throw new BusinessException(非法的订单状态流转); }这段逻辑放在 OrderService 里所有修改订单状态的入口都先调用它。状态机校验在答辩时也是很好的加分讲点说明你考虑到了越权操作和业务完整性。6. 答辩演示与论文收尾让评委快速看懂你的系统6.1 演示路径设计两分钟讲完核心流程答辩演示不要一上来就展示代码评委更关心系统能不能跑通。我建议走一条固定路径打开小程序首页展示菜品列表和轮播图加两道菜到购物车进入购物车页修改数量提交订单展示订单状态从待付款变为待制作打开订单列表展示历史订单。整个过程控制在 90 秒到两分钟。提前把后端服务启动好、数据库跑起来别在答辩现场等 Tomcat 启动这是血泪经验。6.2 论文结构与写作节奏论文的骨架按软件工程标准来可行性分析、需求分析、系统设计、系统实现、系统测试。需求分析里画用例图系统设计里放数据库 ER 图和表结构实现部分按模块贴关键代码测试部分写测试用例表。代码不要整段贴选核心的逻辑比如登录接口、下单的金额计算、状态机校验每个贴 20 到 30 行并加注释说明设计思路。论文的系统测试章节容易被忽略其实很简单写一张功能测试用例表列清测试步骤、输入数据、预期结果、实际结果页数就起来了。6.3 答辩高频问题与回答角度三个必被问的问题提前准备回答角度。第一个是为什么用 SSM 不用 Spring Boot回答课程体系用的 SSM可以用 XML 和配置更直观理解 Spring 的核心机制Spring Boot 是对 SSM 的封装使用 SSM 更能体现基础掌握。第二个是订单并发怎么办回答当前场景是单体应用MySQL 的事务保证数据一致性后续可以引入分布式锁或把订单号生成改为发号器。第三个是openid 是什么回答微信登录时通过 code 换取的该用户在当前小程序内的唯一标识服务端保存用户状态用它。答出这三点答辩基本稳了。这套点餐系统做完之后我最大的感受是毕设不是越花哨越好而是每个模块都要能讲清楚为什么这么做。数据库表设计、订单状态机、登录态流程这三个点是整套系统的骨架也是答辩时评委最常下钻的地方。把这三个讲透了比堆一堆炫技的代码更有说服力。希望帮到你做完这套方案之后你会发现微信点餐这个方向后续还能延伸出商家端、取餐叫号、数据分析报表底子打好了这些扩展都只是时间问题。本文还有配套的精品资源点击获取
返回列表