ARTICLE DETAIL

资讯详情

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

校园外卖小程序毕设:SSM后端从建表到接口跑通全流程

校园外卖小程序毕设:SSM后端从建表到接口跑通全流程 简介本资源为校园外卖平台小程序毕业设计完整项目包面向计算机相关专业需要完成毕设的学生及Java全栈入门者。项目基于微信小程序、SSM框架与MySQL数据库开发划分管理员、用户、商家三种角色涵盖个人中心、用户与商家管理、菜品分类与信息管理、购买菜品、订单信息与订单领取、系统管理等模块商家可维护菜品并查看订单用户可浏览购买菜品并查询订单具备完整业务闭环。压缩包共946个文件约28.48MB包含113个java后端源码、119个vue前端页面、121个js脚本、38个wxss与37个wxml小程序页面、2个sql数据库脚本以及png、svg、jpg等界面素材另附毕业论文与mp4视频演示便于对照理解系统结构与运行流程。目前已有295人学习下载适合作为毕设参考、课程设计模板或SSM与小程序联调的学习案例。1. 校园外卖小程序毕设从选题到跑通一套 SSM 后端到底要写多少东西每年到了毕设季问得最多的就是「校园外卖平台小程序」这个题目还能不能做、怎么做才不像烂大街的模板。我前后带过几届学生做这类系统也帮人改过从网上直接扒下来的源码说句实在话这个题目的技术栈——微信小程序 SSM MySQL——本身没有任何问题问题出在大部分人只把它当成一个「能跑就行」的作业结果答辩时被问三句就露馅。这篇笔记不讲虚的就按一个真实可交付的校园外卖系统来拆小程序端要几个页面、SSM 后端要几张表、订单状态怎么流转、数据库怎么设计才不会在答辩时被追问到哑口。适合正在做计算机毕业设计、选了 Java 方向又想做点移动端东西的人也适合已经拿到一份源码但看不懂里面在干什么、想自己重写一遍的人。读完你至少能判断手上这份东西值不值得改还是推倒重来更快。2. 先想清楚校园外卖和美团之间差的那几个功能点2.1 为什么不能照搬通用外卖系统的表结构通用外卖平台要考虑骑手调度、多商家结算、跨区域配送表结构里一堆delivery_zone、settlement_bill之类的字段。校园场景完全不是这个逻辑商家就是食堂窗口或者校内小店配送基本靠学生兼职或者自提营业时间跟着课表走。如果你直接把美团那套表结构抄过来会出现大量永远为空的字段答辩老师一眼就能看出来是拼凑的。我一般建议把核心实体压缩到六张表用户表、商家表、菜品表、订单表、订单明细表、地址表。其中地址表在校园场景里可以简化成「宿舍楼 房间号」两个字段不需要省市区三级联动。订单表里最关键的字段是order_status它决定了整个系统的复杂度。2.2 订单状态机整个系统最容易翻车的地方校园外卖的订单状态比你想的要绕。一个典型流程是待支付 → 已支付/待接单 → 已接单/备餐中 → 待取餐/配送中 → 已完成另外还有已取消和已退款两个终态。很多从网上下的源码只写了三四个状态结果测试的时候发现「商家拒单」这个场景没法处理。public enum OrderStatus { PENDING_PAYMENT(0, 待支付), PAID(1, 已支付待接单), ACCEPTED(2, 已接单备餐中), DELIVERING(3, 配送中), COMPLETED(4, 已完成), CANCELLED(5, 已取消), REFUNDED(6, 已退款); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } // 根据code反查枚举用于数据库读取后转换 public static OrderStatus fromCode(int code) { for (OrderStatus s : values()) { if (s.code code) return s; } throw new IllegalArgumentException(未知订单状态: code); } }这段枚举的关键在于fromCode方法。数据库里存的是 intJava 里用的是枚举如果没有这个转换方法你会在 Service 层写一堆if (status 1)的魔法数字后期加状态时改到崩溃。参数说明code从 0 开始连续编号方便数据库用 tinyint 存储desc直接用于小程序端展示省去前端再维护一份映射表。2.3 小程序端页面清单与 SSM 接口的对应关系很多人做毕设是先把小程序页面画出来再倒推后端接口结果接口设计得七零八落。正确的顺序是先定接口再画页面。下面这张表是我带学生时用的标准对照你可以直接拿去改小程序页面核心接口请求方式关键参数首页商家列表/api/shop/listGETpage, size, keyword商家详情菜品/api/dish/listByShopGETshopId购物车本地存储不调接口--提交订单/api/order/createPOSTshopId, items[], addressId订单列表/api/order/listByUserGETuserId, status, page订单详情/api/order/detailGETorderId个人中心/api/user/infoGETuserId这张表的价值在于你拿着它去写 Controller 的时候每个方法对应哪个页面一目了然不会出现「这个接口到底给谁用」的困惑。另外注意购物车放在本地存储这是校园外卖的常见做法因为用户量小、不需要跨设备同步省掉一张购物车表能少写不少代码。3. 把 SSM 后端跑起来从建表到第一个接口返回 JSON3.1 数据库建表六个核心表的字段与索引先给建表语句再说为什么这么建。以下以 MySQL 8.0 为例字符集统一用utf8mb4排序规则utf8mb4_general_ci。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(64) DEFAULT NULL, avatar_url VARCHAR(255) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, dorm_building VARCHAR(32) DEFAULT NULL COMMENT 宿舍楼, dorm_room VARCHAR(16) DEFAULT NULL COMMENT 房间号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE shop ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL, logo_url VARCHAR(255) DEFAULT NULL, location VARCHAR(128) DEFAULT NULL COMMENT 校内位置描述, open_time VARCHAR(32) DEFAULT NULL COMMENT 营业时间段, status TINYINT DEFAULT 1 COMMENT 1营业 0休息, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dish ( id INT NOT NULL AUTO_INCREMENT, shop_id INT NOT NULL, name VARCHAR(64) NOT NULL, price DECIMAL(8,2) NOT NULL, image_url VARCHAR(255) DEFAULT NULL, stock INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id), KEY idx_shop_id (shop_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id INT NOT NULL, shop_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, order_status TINYINT NOT NULL DEFAULT 0, remark VARCHAR(255) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, order_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(64) NOT NULL COMMENT 冗余存名称防止菜品改名, price DECIMAL(8,2) NOT NULL, quantity INT NOT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE address ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, contact_name VARCHAR(32) NOT NULL, contact_phone VARCHAR(20) NOT NULL, dorm_building VARCHAR(32) NOT NULL, dorm_room VARCHAR(16) NOT NULL, is_default TINYINT DEFAULT 0, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个设计点值得展开说。第一orders表用order_no而不是自增 id 作为业务主键对外暴露这是为了避免用户通过订单号推测出系统总单量。第二order_item里冗余了dish_name和price因为菜品可能改价或下架订单历史必须保留快照。第三idx_user_status这个联合索引是专门为「查我的订单列表」这个高频查询建的没有它的话订单量一上来查询就会明显变慢。3.2 SSM 整合配置三个 XML 文件到底在配什么SSM 的配置是新手最容易卡住的地方。我见过太多人从网上复制了三份 XML跑起来报错却不知道从哪查。这里把每个文件的作用说清楚。applicationContext.xml管的是 Spring 容器本身核心是数据源和 MyBatis 的整合!-- 数据源 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/campus_takeout?useUnicodetrueamp;characterEncodingutf8mb4amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ property nameinitialSize value5/ property namemaxActive value20/ /bean !-- MyBatis SqlSessionFactory -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.campus.entity/ /bean !-- 扫描 Mapper 接口 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.campus.mapper/ /beanspring-mvc.xml管的是 Web 层核心是注解驱动和 JSON 转换mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property namesupportedMediaTypes list valueapplication/json;charsetUTF-8/value /list /property /bean /mvc:message-converters /mvc:annotation-driven context:component-scan base-packagecom.campus.controller/ !-- 静态资源放行小程序开发阶段可能用到 -- mvc:default-servlet-handler/web.xml是入口负责加载上面两个容器并配置 DispatcherServlet。这三个文件的关系是web.xml启动时先加载applicationContext.xml创建父容器Service 和 DAO再加载spring-mvc.xml创建子容器Controller。子容器能访问父容器的 Bean反过来不行——这就是为什么 Controller 能注入 Service但 Service 里拿不到 Controller。3.3 第一个能返回 JSON 的接口订单创建配置跑通后先写一个最核心的接口验证链路创建订单。这个接口涉及参数校验、库存扣减、订单号生成、事务控制能跑通它基本说明后端没问题了。RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; PostMapping(/create) public ResultMapString, Object create(RequestBody OrderCreateDTO dto) { // 参数校验 if (dto.getUserId() null || dto.getItems() null || dto.getItems().isEmpty()) { return Result.error(参数不完整); } try { String orderNo orderService.createOrder(dto); MapString, Object data new HashMap(); data.put(orderNo, orderNo); return Result.success(data); } catch (BizException e) { return Result.error(e.getMessage()); } } }Service 层的核心逻辑Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private DishMapper dishMapper; Autowired private OrderItemMapper orderItemMapper; Override Transactional(rollbackFor Exception.class) public String createOrder(OrderCreateDTO dto) { // 1. 校验库存并扣减 BigDecimal total BigDecimal.ZERO; for (OrderItemDTO item : dto.getItems()) { Dish dish dishMapper.selectById(item.getDishId()); if (dish null || dish.getStatus() 0) { throw new BizException(菜品已下架: item.getDishId()); } if (dish.getStock() item.getQuantity()) { throw new BizException(库存不足: dish.getName()); } // 乐观锁扣库存防止超卖 int affected dishMapper.reduceStock(item.getDishId(), item.getQuantity()); if (affected 0) { throw new BizException(库存扣减失败请重试); } total total.add(dish.getPrice().multiply(new BigDecimal(item.getQuantity()))); } // 2. 生成订单号时间戳 用户ID后四位 String orderNo System.currentTimeMillis() String.format(%04d, dto.getUserId() % 10000); // 3. 插入订单主表 Orders order new Orders(); order.setOrderNo(orderNo); order.setUserId(dto.getUserId()); order.setShopId(dto.getShopId()); order.setTotalAmount(total); order.setOrderStatus(OrderStatus.PENDING_PAYMENT.getCode()); orderMapper.insert(order); // 4. 插入订单明细 for (OrderItemDTO item : dto.getItems()) { OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setDishId(item.getDishId()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); } return orderNo; } }reduceStock对应的 Mapper XML 是update idreduceStock UPDATE dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity} /update这里的关键是WHERE stock #{quantity}这个条件。它利用 MySQL 的行锁实现了乐观锁效果如果库存不够update 影响行数为 0Service 层就抛异常回滚。这比先 select 再 update 要安全得多高并发下不会超卖。参数说明Transactional(rollbackFor Exception.class)确保任何异常都回滚默认只回滚 RuntimeException加上rollbackFor更保险。4. 小程序端对接登录、请求封装和列表加载更多4.1 微信登录换 openid 的正确姿势小程序端不能直接拿用户信息必须先调wx.login拿 code传给后端换 openid。后端用 code 调微信接口拿到 openid 后查库没有就注册。// 小程序端 app.js App({ globalData: { userId: null, openid: null }, onLaunch() { wx.login({ success: (res) { if (res.code) { wx.request({ url: http://localhost:8080/api/user/login, method: POST, data: { code: res.code }, success: (resp) { if (resp.data.code 200) { this.globalData.userId resp.data.data.userId; this.globalData.openid resp.data.data.openid; } } }); } } }); } });后端处理逻辑用 code 加上小程序的 appid 和 secret 去调微信的jscode2session接口返回 openid 和 session_key。注意 appid 和 secret 不要硬编码在代码里放到配置文件里。另外这个接口必须在服务端调用不能在小程序端直接调否则 secret 会泄露。4.2 请求封装统一处理 token 和错误提示每个页面都写一遍wx.request是灾难。封装一个request.jsconst BASE_URL http://localhost:8080; function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { content-type: application/json }, success: (res) { if (res.data.code 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };参数说明BASE_URL在开发阶段指向本地上线要改成备案域名。code 200是后端统一返回格式的约定建议所有接口都返回{code, message, data}三段式结构前端只判断 code 就行。4.3 订单列表分页加载onReachBottom 与防重复请求校园外卖的订单列表用分页加载是标配。小程序里用onReachBottom触发加载下一页但必须加一个loading标志位防止重复请求。Page({ data: { orders: [], page: 1, size: 10, hasMore: true, loading: false }, onLoad() { this.loadOrders(); }, onReachBottom() { if (!this.data.hasMore || this.data.loading) return; this.setData({ page: this.data.page 1 }); this.loadOrders(); }, loadOrders() { this.setData({ loading: true }); request({ url: /api/order/listByUser, data: { userId: getApp().globalData.userId, page: this.data.page, size: this.data.size } }).then((list) { this.setData({ orders: this.data.orders.concat(list), hasMore: list.length this.data.size, loading: false }); }).catch(() { this.setData({ loading: false }); }); } });hasMore的判断逻辑是如果返回的记录数等于 pageSize说明可能还有下一页小于 pageSize 则说明到底了。这个判断比让后端返回 total 更简单校园场景数据量不大够用。5. 避坑与排查那些让答辩当场卡住的细节5.1 中文乱码从数据库到小程序的完整链路现象小程序端显示菜品名称是问号或者方块。原因通常出在三个地方之一数据库字符集不是 utf8mb4、JDBC 连接串没加 characterEncoding、或者 Tomcat 的 URIEncoding 没配。解决顺序是先确认建表时用了DEFAULT CHARSETutf8mb4再检查 JDBC url 里有没有characterEncodingutf8mb4最后看 Tomcat 的 server.xml 里 Connector 有没有URIEncodingUTF-8。三个都对了基本不会乱码。5.2 订单号重复时间戳方案的隐患现象极少数情况下插入订单报唯一键冲突。原因是用System.currentTimeMillis()生成订单号同一毫秒内如果有两个请求就会重复。解决在时间戳后面再加一个随机数或者用AtomicLong自增序列。更稳妥的做法是用雪花算法但毕设场景加个 3 位随机数就够了。5.3 小程序请求本地接口失败域名校验与开发者工具设置现象开发者工具里请求http://localhost:8080报错「不在以下 request 合法域名列表中」。原因小程序默认只允许 https 域名。解决在微信开发者工具右上角「详情」→「本地设置」里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」。注意这个选项只在开发阶段用上线必须配 https 域名。5.4 MyBatis 返回 null字段名与属性名映射不上现象查询返回的对象里某些字段是 null但数据库里明明有值。原因数据库字段用下划线命名如create_timeJava 属性用驼峰createTimeMyBatis 默认不会自动转换。解决在applicationContext.xml的 SqlSessionFactory 里加property nameconfiguration refmybatisConfig/然后配置mapUnderscoreToCamelCase为 true。或者在每个 Mapper XML 里手写 resultMap但那样太累。5.5 事务不生效Service 内部方法直接调用现象在 Service 的 A 方法里直接调 B 方法B 方法上标了Transactional但回滚不生效。原因Spring 的事务是基于代理的同一个类内部方法调用不走代理对象。解决把 B 方法挪到另一个 Service 里或者通过AopContext.currentProxy()获取代理对象再调。毕设里最简单的做法就是拆到两个 Service。6. 让毕设多拿十分论文里怎么把 SSM 和微信小程序写出技术深度6.1 论文技术章节的写法别只贴代码很多人写论文的技术实现章节就是大段贴代码答辩老师翻两页就不想看了。正确的写法是先画一张系统架构图用 Visio 或者 draw.io 画别用截图然后按「表现层 → 业务层 → 数据层」三层来写。表现层讲小程序的页面结构和请求封装业务层讲 Service 的职责划分和事务边界数据层讲表结构设计和索引优化。每一层配一段 200 字左右的说明代码只贴关键片段比如订单状态枚举和库存扣减的 SQL。6.2 性能测试数据用 JMeter 跑一组有说服力的数字毕设里加一组性能测试数据会让论文看起来扎实很多。用 JMeter 对订单创建接口做 100 并发测试记录平均响应时间和吞吐量。我一般会跑三组无索引、加索引、加索引连接池调优然后做对比。下面是一个参考表格格式场景并发数平均响应时间(ms)吞吐量(TPS)错误率无索引1008501180%加 idx_user_status1003203120%加索引连接池 maxActive501001805550%这组数据不需要多精确但能说明你确实做了优化而不是随便写了个能跑的代码就交差。6.3 视频演示的录制技巧三分钟讲清楚核心流程视频演示不要从头到尾录一遍所有页面老师没耐心看。三分钟足够前 30 秒展示小程序首页和商家列表中间 90 秒完整走一遍「选菜 → 下单 → 支付 → 商家接单 → 订单状态变化」最后 60 秒打开数据库客户端展示订单表和明细表的数据。录制用 OBS 或者系统自带录屏都行关键是提前把测试数据准备好别录到一半发现购物车是空的。6.4 源码整理让老师能跑起来比什么都重要最后说一个血泪经验答辩前一定要把源码整理成一个能直接导入运行的工程。我见过太多人代码在自己电脑上跑得好好的换台机器就各种报错。整理清单数据库导出 sql 文件包含建表测试数据、配置文件里的数据库密码改成通用的、README 写清楚导入步骤和依赖版本。别小看这个老师如果跑不起来你的代码印象分直接砍半。希望帮到你。本文还有配套的精品资源点击获取
返回列表