ARTICLE DETAIL

资讯详情

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

SSM框架下的校园订餐系统:从数据库设计到部署实践

SSM框架下的校园订餐系统:从数据库设计到部署实践 简介这份压缩包是面向Java初、中级学习者和毕业设计开发的校园订餐微信小程序完整项目基于SSM框架与MySQL数据库实现覆盖管理员、用户、商家三大角色包含餐厅信息管理、美食分类与展示、购物车、订单管理、用户充值、收藏等典型业务模块。包内共有1414个文件以Java源码、Vue后台页面、微信小程序wxml/wxss/js组件为主png/svg等图片资源用于界面图标与展示xml/properties作为配置支撑并附带SQL数据库脚本和环境安装、启动打包脚本便于本地部署和二次开发。整包约24.77MB目录按前端、后台、数据库等分层组织已有107人浏览学习。借助这套源码可快速理解SSM与小程序前后端的交互流程梳理多角色权限控制和订单状态设计思路对完成课程设计、毕业设计或学习真实项目结构具有直接参考价值。1. 校园订餐为什么还要选 SSM 这套“老组合”在毕设和中小型项目里SSM 并没有被 Spring Boot 完全取代。原因很直接Spring MVC 把路由和参数绑定放在注解里MyBatis 的 SQL 可控性强对 MySQL 的复杂查询比如餐厅—美食—订单关联比 JPA 直观。这个项目把微信小程序当作客户端管理后台用 Vue后端就是标准 SSM结构上把用户、商家、管理员三种身份拆开正好对应校园订餐里“谁来点、谁来接、谁来管”的边界。适合想从零看 Java Web 全栈的人也适合毕业设计要展示分层设计、并需要快速部署的场景。很多人拿到源码第一件事是跑通但真正要交的是“能不能讲清楚订单和余额为什么这么设计”。以下内容全部基于这套常见工程结构展开。2. SSM 后端拆分从 MySQL 表设计到 Mapper 层的边界2.1 数据表餐厅、美食、订单和余额的状态机先看核心表结构。标准校园订餐会把用户、商家、餐厅、美食、美食类型、购物车、订单、订单详情、充值记录、收藏建为 10 张表。订单表是最复杂的它的状态机直接影响前端按钮展示和后端并发处理方式。状态值含义可执行操作0待支付用户取消订单或订单超时释放库存1已支付商家接单用户可申请退款2配送中商家标记出餐用户等待3已完成用户确认收货订单归档4已取消系统关闭无法恢复用户表user里有openid唯一索引、balance余额和role区分用户/商家。美食表food要冗余冗余餐厅名称或restaurant_id外键方便列表页联查。这里的关键是order_no不能依赖自增 ID因为支付回调要拿它做幂等前端生成订单号时如果只靠System.currentTimeMillis()同一毫秒并发会碰撞建议用时间戳加用户 ID 加随机数。在UserMapper.xml中更新余额用一行 SQLUPDATE user SET balance balance #{amount} WHERE user_id #{userId}为什么不用先在 Java 里查余额再加再写回因为两个并发订单同时读余额会出现丢失更新。这条 SQL 在数据库行锁级别保证加减是原子的。充值表和用户表不要用“先查后更”直接用上述 SQL再插入一条recharge流水即可。2.2 Controller 只做参数转发业务规则下沉SSM 项目的经典错误是业务逻辑写在 Controller 里。下面这段OrderController示例只做参数校验和路由Controller RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; PostMapping(/create) ResponseBody public Result create(RequestBody OrderCreateParam param, RequestAttribute(userId) Integer userId) { if (param.getFoodList() null || param.getFoodList().isEmpty()) { return Result.error(购物车不能为空); } return orderService.createOrder(userId, param); } }注意RequestAttribute(userId)来自拦截器拦截器在HandlerInterceptor里解析 token 后写入 request attribute。这样做的好处是 Controller 层不需要重复解析 token后续的商家接口、管理端接口也复用同一套身份处理。订单创建的核心事务方法在 Service 层Transactional(rollbackFor Exception.class) public Result createOrder(Integer userId, OrderCreateParam param) { BigDecimal total calculateTotal(param.getFoodList()); if (total.compareTo(BigDecimal.ZERO) 0) { return Result.error(订单金额异常); } User user userMapper.selectById(userId); if (user.getBalance().compareTo(total) 0) { return Result.error(余额不足); } String orderNo generateOrderNo(); // 锁定库存update food set stockstock-#{num} where food_id#{id} and stock#{num} // 返回影响行数为 0 时说明库存不足 // 扣除余额update user set balancebalance-#{total} where user_id#{userId} orderMapper.insert(order); orderItemMapper.insertBatch(param.getFoodList()); return Result.success(orderNo); }Transactional要指定rollbackFor Exception.class否则RuntimeException之外的自定义异常不会回滚。库存扣减要放在创建订单之前且 SQL 自带stock #{num}条件避免超卖。2.3 Mapper 层联表查询与分页SSM 项目里使用 MyBatis 分页插件 PageHelper 很常见。下面是订单列表联表查询的 Mapper XMLselect idselectOrderPageByUser resultTypecom.example.vo.OrderVO SELECT o.order_no, o.total_amount, o.status, r.name AS restaurant_name, GROUP_CONCAT(f.name SEPARATOR 、) AS food_names FROM orders o LEFT JOIN restaurant r ON o.restaurant_id r.restaurant_id LEFT JOIN order_item oi ON o.order_id oi.order_id LEFT JOIN food f ON oi.food_id f.food_id WHERE o.user_id #{userId} GROUP BY o.order_id, o.order_no, o.total_amount, o.status, r.name ORDER BY o.create_time DESC /select这里用GROUP_CONCAT把一次订单一口气查出来避免在 Java 里再循环查 food 名称形成 N1 查询。PageHelper 用法是在紧邻 Mapper 方法的第一行设置页码和页大小PageHelper.startPage(pageNum, pageSize); ListOrderVO list orderMapper.selectOrderPageByUser(userId); PageInfoOrderVO pageInfo new PageInfo(list);注意PageHelper.startPage和Mapper方法之间不能插入其他 SQL 调用否则分页会被污染。真实项目中我一般会把分页参数封装成PageParam对象避免每个方法都写两个形参。3. 小程序端与 Vue 管理后台的权限协作3.1 微信小程序登录态wx.login 换 token校园订餐小程序端登录几乎都走同一套wx.login拿临时 code传给后端换取 openid再生成自定义登录态。小程序端代码wx.login({ success: async (res) { const loginRes await request.post(/user/login, { code: res.code }); const { token, userInfo } loginRes.data; wx.setStorageSync(token, token); wx.setStorageSync(userInfo, userInfo); } });后端拿到 code 后调用微信接口换取 openid再把 openid 和自增 user_id 绑定到 token。token 用 JWT 还是 UUID 都行但注意校园项目里 token 要能快速失效商家账户被审核拒绝后要能踢下线所以建议把 token 存 Redis 而不是只靠 JWT 的过期时间。后续请求统一封装const request { get(url, params) { return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${url}, data: params, header: { Authorization: wx.getStorageSync(token) }, success: resolve, fail: reject }); }); } };BASE_URL建议放在一个独立config.js文件里真机调试时用局域网 IP不要用 localhost否则手机访问不到电脑上的 Tomcat。如果后端接口报 401先看请求头里 Authorization 是否带上再排查拦截器是否把/user/login列入了白名单。3.2 Vue 管理后台从 .bak 文件看构建工程拿到压缩包后你会看到一堆.vue.bak文件比如IndexMain.vue.bak、IndexAsideStatic.vue.bak、update-password.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak。这是用 Vue Element Admin 之类的后台模板改的.bak是开发者在改坏页面或升级组件前留下的备份。update-password.vue.bak通常里面对应个人中心修改密码的静态页面IndexMain.vue.bak是主布局IndexHeader.vue.bak是顶栏IndexAsideStatic.vue.bak是侧边栏菜单。这些文件被保留下来恰好可以用来对比菜单权限的写法。管理后台在src/router/index.js里添加路由和 meta 权限{ path: /merchant/restaurant, component: () import(/views/merchant/restaurant.vue), meta: { roles: [admin, merchant] } }在路由守卫里判断角色防止用户直接输入 URL 访问商家页面。注意这个判断要以后端返回的role为准不要去解析 token 里的字段。角色权限建议做成一张表角色可访问模块admin用户管理、商家审核、系统管理、订单总览merchant餐厅管理、美食管理、订单处理user首页、餐厅信息、购物车、我的订单、收藏下面三个脚本是本项目的构建入口# 1-install.bat 安装依赖 npm install --registryhttps://registry.npmmirror.com # 2-run.bat 本地开发 npm run dev # 3-build.bat 打包上线 npm run build1-install.bat里一般还要补一句npm install后的提示检查node_modules是否生成。构建后 dist 目录由 Nginx 或 Spring Boot 静态目录托管这里有一个常见误区不要直接用file://打开dist/index.html因为路由是 history 模式跳转子页面会白屏要交给 web 服务器处理。3.3 商家上传美食图片的路径问题商家在小程序端上传餐厅 Logo 和美食图片后端用MultipartFile接收。SSM 的spring-mvc.xml要配上传大小限制bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value5242880 / /bean保存文件的物理路径不要写在代码里放到properties里同时用虚拟目录映射到对外 URLString realPath uploadConfig.getDir(); String fileName System.currentTimeMillis() _ file.getOriginalFilename(); file.transferTo(new File(realPath, fileName));上传完成后把img/文件名存库小程序端展示时拼上服务器地址。如果图片加载 404先确认是不是浏览器能访问http://localhost:8080/img/xx.jpg不能访问就检查虚拟目录路径不要怀疑小程序代码。4. 订单、购物车与充值SSM 事务下的资金流实现4.1 购物车勾选与一次性下单购物车表按user_id food_id存一份数量可增减。小程序端结算页把选中商品的foodIdList传到后端。后端需要校验食品是否下架、是否属于同一家餐厅。一份订单只能对应一家餐厅所以先判断购物车里食品的restaurant_id是否一致如果不一致就提示“只能同时结算同一家餐厅的商品”。很多校园订单误操作都从这里来接口返回提示文案要明确到“餐厅名”。下单接口里不要直接使用前端传入的price否则用户改请求参数就能把价格改成 0。正确做法是从数据库中根据 food_id 批量查出最新价格与前端数量做乘法得出总价。如果平台有满减活动还需要在 Service 层单独计算优惠金额而不是信任前端传过来的discount字段。4.2 余额充值与模拟支付微信小程序个人主体的支付能力经常被限制这就是为什么很多毕设项目把支付换成余额充值。充值的核心是生成充值单然后跳到微信支付的模拟页面。实际实现public Result recharge(Integer userId, BigDecimal amount) { String rechargeNo RE System.currentTimeMillis() ThreadLocalRandom.current().nextInt(1000); rechargeMapper.insert(new Recharge(rechargeNo, userId, amount, PENDING)); return Result.success(rechargeNo); }小程序端拿到rechargeNo把用户引到一个确认页点击“去支付”后直接调用后端的payCallback模拟支付成功。payCallback里必须做幂等Transactional(rollbackFor Exception.class) public void payCallback(String rechargeNo) { Recharge r rechargeMapper.selectByNoForUpdate(rechargeNo); if (r null || SUCCESS.equals(r.getStatus())) { return; } rechargeMapper.updateStatus(rechargeNo, SUCCESS); userMapper.updateBalance(r.getUserId(), r.getAmount()); }selectByNoForUpdate是行级锁两个请求同时进来时只有一个能更新SUCCESS判断保证重复回调不重复加钱。订单支付同理orders表加唯一索引order_no更新时用where order_no#{orderNo} and status0。如果你接的是真实微信支付回调函数必须返回 XML 给微信服务器这里不展开但模拟支付的接口设计和真实回调路径保持一致后面接支付服务商时改动很小。4.3 常见事务坑MyBatis 一级缓存与事务回滚在同一个事务里先selectByNo再update再selectByNo第二次查到的是一级缓存里的老数据造成“状态没变”的错觉。解决办法是在更新方法后面重新查询原生字段或者更直接地根据影响行数判断。商品库存扣减的 XMLupdate iddeductStock UPDATE food SET stock stock - #{num} WHERE food_id #{foodId} AND stock #{num} /update如果影响行数为 0抛出库存不足异常整个订单事务回滚。注意在Transactional方法里如果调用同类中的另一个Transactional方法是不会生效的要自行注入自身或拆到不同 Service。这个坑在面试时也常被问到记下来很有用。5. 部署时被忽略的细节数据库初始化、日志与真机联调SSM 项目从压缩包解压到浏览器能访问中间有三个隐藏点。一是数据库初始化压缩包经常不带init.sql但会留.sql文件或db.properties配置。你可以在 MySQL 里先建库再用source命令导入。Tomcat 部署时如果中文乱码需要修改server.xml的 connector 加上URIEncodingUTF-8。两个容易卡住的地方是微信小程序项目未配置合法域名以及开发者工具提示“登录用户不是该小程序的开发者”。前者在开发阶段直接勾选“不校验合法域名”后者要去微信公众平台成员管理里把你微信号添加为开发者。真机调试时手机和电脑保持同一局域网config.js里的地址不能填localhost要填电脑的局域网 IP。最后一个技巧是用抓包工具定位小程序请求超时。常见有 Charles 和 Fiddler设置抓包后优先看POST /user/login和POST /order/create的返回。如果返回时间超过 3 秒多半是数据库连接池配置太小或 SQL 没加索引。orders表的user_id和status要建联合索引否则随着订单表增长联表查询会越来越慢。就按这个顺序把项目从跑通到优化走一遍答辩或上线都不会心虚。本文还有配套的精品资源点击获取
返回列表