ARTICLE DETAIL

资讯详情

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

Spring Boot+MyBatis-Plus旅游餐饮管理系统实战:从设计到答辩全解析

Spring Boot+MyBatis-Plus旅游餐饮管理系统实战:从设计到答辩全解析 1. 从毕业设计题目到完整系统这个项目到底在做什么先别急着打开IDE敲代码我见过太多同学拿到“旅游餐饮管理系统”这个题目后第一反应就是找个模板改个名字交差结果答辩被问到三个问题就卡壳。这个题目其实很有讲究它不是一个简单的CRUD堆砌而是把旅游和餐饮两条业务线拧在一起既要管线路、景点、酒店又要管餐厅、菜品、订餐还得处理用户下单、支付、评论这些横跨两条线的数据流转。先说清楚这个东西的定位这是基于Spring生态通常用Spring Boot Spring MVC MyBatis/MyBatis-Plus MySQL开发的B/S结构管理系统用户端实现旅游产品浏览、线路预订、餐厅选择、菜品下单、订单管理管理端实现景点线路管理、餐饮分类与菜品管理、订单审核与统计、用户管理。它属于典型的“前后台分离角色权限控制”的课程设计/毕业设计项目难易程度适中但覆盖的知识点非常全Spring IoC/DI、Spring MVC请求链路、MyBatis持久层、事务管理、拦截器或Spring Security权限控制、以及最容易被忽略的业务状态设计。为什么我会专门提“业务状态设计”因为这个项目的核心难点不在“增删改查”而在订单。旅游订单一套状态待支付、已支付、出行中、已完成、已取消餐饮订单一套状态待接单、制作中、待上菜、已完成两个模块要在同一个用户体系下联动还要考虑“出行日期和餐位的关联”“景点线路下的推荐餐厅”这类跨表逻辑。论文里如果能把这层讲清楚答辩分数直接上一个档。这篇博文我会基于我自己的实操经验把从技术选型、数据库建表、后端接口设计、前端页面骨架、到最后的论文写作和答辩准备完整拆开讲。面向的主要是两类人一类是正在做毕业设计、需要从零搭建这个系统的学生另一类是已经会写基础Spring Boot CRUD、想进阶理解业务系统设计的开发者。不管你是哪种这篇的目标都是让你看完之后能动手复现而不是看完只会复制粘贴。2. 技术选型不是越新越好为什么我用Spring Boot 2.7 MyBatis-Plus MySQL 82.1 版本组合的取舍逻辑先说一个很多同学容易犯的错误一上来就梭哈最新版。Spring Boot 3.x确实出来很久了但3.x要求JDK 17起步很多学校的答辩环境、服务器环境、甚至你自己电脑上装的JDK可能还是8或者11更别提有些学长学姐给你留的旧代码根本跑不到3.x上。我的建议是如果这是毕业设计用Spring Boot 2.7.x JDK 8稳如老狗。2.7.x是Spring Boot 2.x的最后一个主线版本官方一直维护到2023年底安全补丁和文档都非常齐全。网上你能搜到的绝大多数Spring Boot教程、博客、踩坑文章都是基于2.x写的你会少踩非常多“版本不兼容”的坑。JDK 8就更不用说了学校机房装JDK 8是最常见的答辩演示的时候不至于因为环境问题翻车。然后是持久层框架。纯MyBatis写SQL虽然能体现你的基本功但这个项目表多、关联复杂手写一大串ResultMap和动态SQL会耗尽你的耐心。MyBatis-Plus在MyBatis基础上提供了一键CRUD、分页插件、逻辑删除、自动填充几个注解就能搞定大部分单表操作复杂的多表查询再用注解SQL或XML实现效率和可维护性平衡得最好。MySQL选8.0.x注意是8.0不是5.7。5.7虽然也能跑但8.0在窗口函数、JSON支持、默认字符集utf8mb4这些方面明显更适合新项目。唯一要注意的是MySQL 8的驱动类是com.mysql.cj.jdbc.Driver和5.7的com.mysql.jdbc.Driver不一样配置别抄错了。2.2 为什么不用Spring Security很多毕业设计的选题是“XX管理系统”然后论文里写“本系统采用Spring Security实现权限控制”看着是很高大上但实际开发中你会发现两个问题第一Spring Security的配置项多、过滤器链概念复杂本地跑通要花不少时间第二答辩时老师如果顺着Security的过滤器链往下问三层很多人就露馅了。我的建议是用一个自定义HandlerInterceptor拦截器 用户角色字段效果完全够用而且你能讲清楚每一步。用户表里设一个role字段0-普通用户1-管理员写一个LoginInterceptor在preHandle里判断Session或Token里有没有用户信息、访问的路径是否在管理员白名单内没有就重定向到登录页完事。你自己写的代码答辩时问你怎么实现权限拦截你能从拦截器注册讲到你用了哪些映射路径头头是道。反而比一个你只配过两行配置的Spring Security更有说服力。当然如果你确实学了Spring Security用也无妨但至少在论文里要把“认证流程”描述清楚不能说“引入依赖即实现权限控制”。2.3 前端方案Thymeleaf还是前后端分离这也是个经典纠结。纯前后端分离Vue RESTful API是现在的主流但毕业答辩场景下我反而推荐服务端渲染的Thymeleaf。理由很简单你不需要维护两套代码和一个跨域配置开发量直接减半整个应用的入口只有一个部署和演示都简单不需要启动前端Node服务Thymeleaf和Spring Boot天然集成model.addAttribute(list, list)之后直接在前端页面th:each遍历逻辑清晰写论文时也更好描述页面如何从后端拿数据。如果你自己会Vue觉得写Vue更顺手也不是不行但请务必把跨域CORS、Token传递、前端路由拦截这几个点处理好答辩时这些细节都很容易被深挖。我这篇后半段的讲解也以Thymeleaf为主你如果坚持分离式架构接口设计部分照样能复用。3. 数据库设计是论文得分的半壁江山核心表结构与关联关系的取舍3.1 从业务出发拆解需要哪些表设计表之前先盘业务。这个系统咱们可以拆成三个端用户端注册登录、浏览旅游线路/景点、查看餐厅和菜品、下订单、支付模拟、评论管理端旅游部分管理景点、线路、酒店/住宿、班次管理端餐饮部分管理餐厅分类、餐厅、菜品、餐位、订单接单、上菜状态。注意旅游和餐饮不是各做各的。用户可能是在某个旅游线路的行程中需要在某个景点附近的餐厅订餐。所以线路和餐厅要有一种“推荐/归属”关联最简单的做法是给餐厅表加一个scenic_id字段表示该餐厅位于哪个景点附近/属于哪个线路这样在旅游线路详情页就能顺带展示周边餐厅也方便联表查询。基于业拆出来的表我建议的核心表结构如下括号内为主要字段表名主要字段说明t_userid, username, password, nickname, phone, avatar, role, create_time用户表role区分用户角色t_scenicid, name, description, address, ticket_price, open_time, image景点表t_travel_lineid, line_name, scenic_ids, days, price, start_date, end_date, description, status旅游线路表用scenic_ids存关联景点ID拼接串t_hotelid, name, address, price, star, image, description住宿酒店表可选t_restaurant_categoryid, category_name, sort餐厅分类表火锅、川菜、小吃等t_restaurantid, name, category_id, scenic_id, address, phone, business_hours, description, image, status餐厅表scenic_id关联景点t_dishid, dish_name, restaurant_id, price, image, description, status菜品表t_travel_orderid, order_no, user_id, line_id, order_date, people_count, contact_name, contact_phone, total_price, status, pay_time旅游订单表t_food_orderid, order_no, user_id, restaurant_id, dish_ids, total_price, status, remark, create_time餐饮订单表t_order_dishid, food_order_id, dish_id, dish_name, dish_price, dish_count餐饮订单-菜品子表多对多拆一对多t_commentid, user_id, target_type, target_id, content, score, create_time评论表支持景点/餐厅/线路多类型评论线程表不一定要建很多张这些已经够用的。旅游订单和餐饮订单分开是因为它们的状态流转完全不一样一个跟着行程走一个跟着餐位走硬要并表会导致大量冗余字段。但订单编号生成规则可以统一比如用时间戳随机数生成唯一order_no任何一张订单表都唯一。3.2 关键表字段设计的细节与坑我只挑几个容易栽跟头的字段细说。状态字段不要用String存中文。这是新手重灾区。比如旅游订单状态status列如果直接存“待支付”“已支付”“已完成”以后想统计“有多少已支付订单”就得where status 已支付索引直接失效。正确做法是存字符串编码0表示待支付1表示已支付2表示已完成3表示已取消。展示层再用枚举或Map做映射。同理餐厅接单状态、菜品上下架状态都是这个套路。金额字段精度。Java侧用BigDecimal数据库侧用decimal(10,2)不要用double或float。餐饮这种涉及大量“价格计算”的场景出现0.10.20.30000000000000004这种经典浮点问题你论文例子就可能用出这个未减一分。这是必须“在代码里用BigDecimal 数据库decimal”的地基问题没得商量。多余字段该冗余就冗余。在t_order_dish子表里我故意加了一列dish_name和dish_price这是下单时的快照。为什么冗余因为菜品表的价格和名字可能被管理员修改如果子表只存dish_id用户查看历史订单时就跟着当前菜品表的价格/名称走了逻辑上是不对的。快照字段不影响查询却能在订单历史展示上免去两次联表实战里非常实用。同理t_food_order里的restaurant_id也可以冗余restaurant_name省得查订单列表时每行都去餐厅表查一次名字。别忘了逻辑删除字段。MyBatis-Plus的逻辑删除依赖一个标识位。推荐在每个业务表都加deleted字段0未删1已删避免硬删除造成的历史数据断链。比如餐厅被删了但用户的历史订单还引用这个餐厅ID硬删会导致页面报空指针。逻辑删除则通过全局配置搞定。3.3 多表查询的设计思路什么时候用一张SQL什么时候查多次这个项目的查询很多是跨表的。举两个典型例子。场景一旅游线路列表页要展示“线路包含的景点名称”。两种方案用t_travel_line.scenic_ids存的是1,3,5拼接串查询前先SELECT * FROM t_travel_line然后遍历每条线路再按ID集合查景点表拼名字N1问题或者用SQL的FIND_IN_SET或JOIN。我推荐的做法是如果是列表页小数据量先查线路列表再批量查景点然后在Java内存里组装。如果数据量超几百条就得在数据库层面把scenic_ids拆成子表t_line_scenicline_id, scenic_id。毕业设计级别用拼接串 内存组装完全够重点是你要在论文中把选择理由写清楚——数据量小、避免复杂嵌套查询、代码可读性好。场景二餐饮订单列表要显示用户名 餐厅名 菜品摘要。这个主查询可以JOIN用户表和餐厅表拿到名字但菜品摘要“鱼香肉丝x2、麻婆豆腐x1”没法一行JOIN出来毕竟是一对多。我的方案分两步先分页查t_food_order并关联出用户/餐厅信息拿到本页订单ID集合再用WHERE food_order_id IN (...) GROUP BY food_order_id查出菜品子表最多两三条SQL性能很好。这就是“先查主表拿ID集合再查子表批量组装”的思路写论文时把这步单独画个时序图讲两次就非常加分。4. 后端核心代码实现从登录鉴权到订单状态机流转4.1 项目初始化与分层架构创建的是Standard Spring Boot项目我习惯分包如下com.xxxx.travel ├── controller // 控制器层 │ ├── admin // 管理端接口 │ └── web // 用户端接口 ├── service // 业务逻辑层 │ └── impl ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端入参出参对象 ├── vo // 视图对象比如订单列表VO ├── config // 配置类拦截器、WebMvc配置等 ├── interceptor // 登录拦截器 ├── common // 统一返回结果、异常处理、常量类 └── util // 工具类比如订单号生成器分层的意义不光是照猫画虎而是每个层都有清晰职责出问题好定位。答辩时老师问“MVC的理解”你直接用你的项目结构来说Controller只做参数接收和结果返回Service里写业务规则Mapper对数据库操作——这就是最标准的三层结构。启动类别忘了加MapperScan扫描mapper包或者每个Mapper接口上单独加Mapper。MyBatis-Plus的分页插件需要在配置类里注册MybatisPlusInterceptor不注册分页会失效这是最常见的坑之一。4.2 统一返回结果与全局异常处理写接口时让每个Controller直接返回R对象或叫Result包含code、message、data三个字段。用户端调用接口拿到的永远是同一种结构前端页面的th:if判断code做提示也方便。Data public class R { private Integer code; // 200成功500失败 private String message; private Object data; public static R ok() { return ok(null); } public static R ok(Object data) { R r new R(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static R fail(String msg) { R r new R(); r.setCode(500); r.setMessage(msg); return r; } }配合RestControllerAdvice做全局异常处理理论上这个能hold住所有业务异常。比如订单金额算负数、库存菜品下架不能下单你就抛一个自定义的BusinessException全局捕获后返回R.fail(e.getMessage())。这样做之后Controller里就几乎没有try-catch了代码看起来非常干净。答辩时问“你项目里的异常怎么处理”你就把全局异常处理器类贴出来比临时解释“我每个方法都try-catch了一下”体面得多。4.3 登录注册与拦截器Session还是JWT在Thymeleaf情境下Session方案最简单可靠。登录成功之后session.setAttribute(user, user)需要当前用户信息的地方(User) session.getAttribute(user)就行。配合拦截器判断Session是否包含用户没有就redirect到/login。拦截器的注册方式要注意Spring Boot 2.7里通过WebMvcConfigurer实现不继承旧的WebMvcConfigurerAdapterComponent public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(user); if (user null) { // 是Ajax请求就返回JSON否则重定向 String requestedWith request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { response.setContentType(application/json;charsetutf-8); response.getWriter().write({\code\:401,\message\:\未登录\}); } else { response.sendRedirect(/login); } return false; } return true; } }Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) // 拦截所有请求 .addPathPatterns(/**) // 放行登录注册、静态资源 .excludePathPatterns(/login, /register, /css/**, /js/**, /images/**); } }管理员权限的校验再到拦截器里判断user.getRole()是不是1不是就返回403提示。代码量不大但确实能在这个项目里体现了一个很完整的用户认证授权链路。密码存储别用明文。至少用MD5加盐或者直接用BCryptPasswordEncoder。只是毕业项目不引入Spring Security全家桶单独拿spring-security-crypto这个工具包里的BCryptPasswordEncoder来加密完全可行既安全又不引入复杂的过滤器配置链。4.4 核心业务旅游订单的下单流程实现这个是重头戏用好这个部分就能体现“业务逻辑的实现不止是CRUD”。旅游下单的请求参数线路ID、出行日期、出行人数、联系人姓名、联系人电话。后端流程分为六步参数校验出行日期不能早于今天、人数必须大于0、线路ID存在计算金额去查线路表line.getPrice() * peopleCount用BigDecimal计算创建订单生成唯一订单号状态设为0待支付插入t_travel_order扣减库存可做可不做如果线路表有stock字段下单后需要UPDATE t_travel_line SET stock stock - 1 WHERE id ? AND stock 0这行SQL要保证原子性不能先查询再更新会有并发超卖问题模拟支付毕业设计一般不做真实支付写一个/pay/{orderNo}接口把订单状态从0改为1记录支付时间返回订单详情页面跳转到订单详情页展示订单信息。整个流程必须加Transactional任何一个步骤失败全部回滚。特别是扣库存和插入订单离开事务很容易出现“订单创建了但库存没扣”这种脏数据。答辩时老师大概率会问“你怎么保证下单和扣库存的一致性”你回答“基于Spring声明式事务在Service默认方法加Transactional”这就是想拿高分的同学应该掌握的答题姿势。4.5 餐饮订单与购物车更复杂的子表操作餐饮下单比旅游多了个“多菜品”关系所以我要实现购物车和订单子表。用户的购物车其实可以存在Session里一个MapInteger, Integerkey是菜品IDvalue是数量。用户结算时遍历购物车逐条构造OrderDish子记录计算总价批量插入t_food_order和t_order_dish。这里有个非常重要的点创建主表和子表必须处理主键回填。因为子表要存food_order_id你得先插入主表拿到自增ID。MyBatis-Plus里insert后实体类的主键会自动回填前提是实体类主键配置了TableId(type IdType.AUTO)直接用foodOrder.getId()就好。Transactional public FoodOrder createFoodOrder(Long userId, Long restaurantId, MapLong, Integer dishCountMap, String remark) { // 1. 校验餐厅状态 Restaurant restaurant restaurantMapper.selectById(restaurantId); if (restaurant null || restaurant.getStatus() ! 1) { throw new BusinessException(餐厅不存在或已关闭); } // 2. 构建订单主表 FoodOrder order new FoodOrder(); order.setOrderNo(OrderNoGenerator.generate()); order.setUserId(userId); order.setRestaurantId(restaurantId); order.setStatus(0); order.setTotalPrice(BigDecimal.ZERO); order.setRemark(remark); foodOrderMapper.insert(order); // 3. 构建子表和累计金额 BigDecimal total BigDecimal.ZERO; ListOrderDish orderDishList new ArrayList(); for (Map.EntryLong, Integer entry : dishCountMap.entrySet()) { Dish dish dishMapper.selectById(entry.getKey()); if (dish null || dish.getStatus() ! 1) { throw new BusinessException(菜品不存在或已下架); } BigDecimal subTotal dish.getPrice().multiply(BigDecimal.valueOf(entry.getValue())); total total.add(subTotal); OrderDish od new OrderDish(); od.setFoodOrderId(order.getId()); od.setDishId(dish.getId()); od.setDishName(dish.getName()); od.setDishPrice(dish.getPrice()); od.setDishCount(entry.getValue()); orderDishList.add(od); } // 4. 批量插入子表 orderDishService.saveBatch(orderDishList); // 5. 更新订单总金额 order.setTotalPrice(total); foodOrderMapper.updateById(order); return order; }这套代码每行都有业务含义你在答辩时可以一行一行讲。尤其是“为什么先插主表再插子表”、“为什么子表要冗余菜名和菜价”每一个问题你都能答得有条理。4.6 订单状态流转的控制毕业设计管理系统里状态机不需要引入复杂状态机框架用一个常量类 Service方法里判断状态即可。但不要“谁想改就改”每个状态变更都应该有对应的Service方法。旅游业用户支付0→1、管理员核销出行1→2、用户取消0→3但已支付不能取消或进入退款流程。餐饮业用户下单0、管理员接单0→1、开始制作1→2、完成上菜2→3、用户确认完成3→4。每个Service方法开头先查订单当前状态不是期望状态就抛异常FoodOrder order foodOrderMapper.selectById(orderId); if (order null) { throw new BusinessException(订单不存在); } if (order.getStatus() ! 0) { throw new BusinessException(订单状态不允许接单); } order.setStatus(1); foodOrderMapper.updateById(order);这种“状态守卫”代码思路虽然简单却避免了你在页面上的按钮乱点导致状态乱套。答辩时一定能举出“连续点击两次按钮”的例子来说明其必要性。5. 前端页面骨架用Thymeleaf把后端数据映射到可交互页面5.1 页面划分与路由设计用Thymeleaf做服务端渲染前端没有“路由”这个概念但你要约定好URL的规划/门户首页展示推荐线路和餐厅/scenic/list景点列表页/line/detail/{id}线路详情页展示路线包含的景点和周边餐厅/restaurant/list餐厅列表页支持按分类、按景点筛选/restaurant/detail/{id}餐厅详情页展示菜品列表和用户评论/cart购物车页面/food/order/confirm餐饮订单确认页/order/list用户订单列表页旅游餐饮合并展示/admin/scenic、/admin/restaurant、/admin/dish、/admin/order管理端的各管理页面。在项目里根据这个URL规范去建对应Controller的GetMapping就一目了然。管理端页面建议把公共片段抽取成fragments/admin_sidebar.html用th:replace生成面包屑避免每个页面重复粘贴侧边栏代码。这也是模板引擎的应有的正确用法。5.2 列表页分页查询与条件筛选旅游线路列表页是最典型的“搜索分页”场景。接收参数lineName模糊搜索、days天数筛选、pageNum、pageSize后端组装LambdaQueryWrapper条件再交给MyBatis-Plus分页插件。页面端用th:each遍历records底部用th:href拼接分页URL。GetMapping(/line/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 6) Integer pageSize, String lineName, Integer days, Model model) { PageTravelLine page new Page(pageNum, pageSize); LambdaQueryWrapperTravelLine wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(lineName), TravelLine::getLineName, lineName) .eq(days ! null, TravelLine::getDays, days) .orderByDesc(TravelLine::getCreateTime); IPageTravelLine result travelLineService.page(page, wrapper); // 这里遍历result.getRecords()批量查询景点名称组装到VO model.addAttribute(page, result); if (StringUtils.hasText(lineName)) { model.addAttribute(lineName, lineName); } if (days ! null) { model.addAttribute(days, days); } return line/list; }浏览层返回的是页面字符串而不是JSON数据都放在Model里页面通过th:text${line.lineName}访问。5.3 详情页组装聚合数据并展示餐饮详情页需要的数据结构是餐厅基本信息、菜品List、评论List。Controller里先查餐厅再按restaurant_id查菜品、按target_type restaurant和target_id 餐厅ID查评论都塞进Model。页面展示需要注意价格的格式化Thymeleaf里直接输出BigDecimal是没问题的但如果你要拼接“”符号和价格显示两位小数可以用#numbers.formatDecimal(price, 1, 2)。另外菜品的图片路径要填写正确如果统一上传到项目本地/upload目录记得做好静态资源配置映射Spring Boot默认不直接暴露本地磁盘路径你需要在配置类里加WebMvcConfigurer.addResourceHandlers。如果你是前后端分离开发这个页面逻辑就换成前端调API组装数据后端返回的是JSON而不是Model视图原理一致。5.4 订单流程页面状态显示与操作按钮的权限控制订单列表页的最核心细节是按钮的状态条件渲染。比如餐饮订单当状态为0(待接单)时管理员才能看到“接单”按钮当状态是0或1(待支付或已支付)时用户侧才能看到“取消”、“去支付”等按钮。用Thymeleaf的th:if${order.status 0}做条件渲染button th:if${order.status 0 and #strings.equals(session.user.role,0)} th:attrdata-order-no${order.orderNo}去支付/button button th:if${order.status 1 and #strings.equals(session.user.role,1)} th:attrdata-order-no${order.orderNo}核销出行/button会话用户取用${session.user.role}注意它的语法不需要额外操作。这样同一个订单列表页面可以同时给普通用户和管理员用靠状态角色双条件决定按钮。这是Thymeleaf相比Vue方案更简单的地方——不用额外做只显示哪个按钮的权限设置。操作提交方式可以用一个统一模板form th:action{/admin/food/order/accept} methodpost直接POST刷新页面简单可靠不引入任何异步请求也能完成业务流程。如果想要更好的用户体验再引入jQuery Ajax但注意Ajax提交后需要前端维护全局状态。6. 数据看板与辅助功能让项目比“普通CRUD”多出亮点6.1 管理员首页的数据统计很多管理系统把它做成“登录后空页面”很可惜。做一个简洁的图形化数据看板不仅好看还能让论文内容丰富不少今日/本月订单数、营业额SELECT COUNT(*), SUM(total_price) FROM t_food_order WHERE status 1 AND create_time BETWEEN ...热门景点Top5从t_travel_order按line_id分组统计热门菜品Top10从t_order_dish按dish_id分组统计并关联出菜名近7日餐饮订单走势按日期分组统计。统计SQL写起来不复杂但展示效果很好。前端页面上用ECharts的CDN画个折线图和饼图代码量不大却能让整套系统的“完整度”立刻提升一个档次。而且这部分很容易在论文的“系统实现”章节里作为“数据可视化功能”去描述。6.2 评论与评分用户产生内容UGC模块不强制每个选题都做评论但旅游餐饮这种面向C端的系统评论功能跟业务是贴合得很的。用户可以给景点、餐厅、线路打分和写评价管理端可以审核/删除违规评论。表结构之前已经设计了t_comment其中target_type通过一个字符串区分评论的对象类型。后端需要注意的是同一用户在同一个目标下只能评论一次可以加UNIQUE KEY uk_user_target (user_id, target_type, target_id)。这个坑如果你提前做掉能帮你避开不少答辩时的“容错性”提问。页面展示评论时按时间倒序还要控制一下评论长度太长的文本在前端截断或用th:utext处理换行。这个模块很能体现“用户参与感”也为你凑足论文中的“功能性需求”提供了非常好的支撑。6.3 文件上传餐厅Logo和菜品图片怎么处理管理端上传菜品图片建议直接用Spring MVC的MultipartFile接收存储到本地指定目录把相对路径/upload/xxxx.jpg存到数据库即可。代码模式PostMapping(/admin/dish/save) public String saveDish(Dish dish, RequestParam(file) MultipartFile file) throws IOException { if (!file.isEmpty()) { String filename UUID.randomUUID().toString().replace(-, ) file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(.)); file.transferTo(new File(uploadDir filename)); dish.setImage(/upload/ filename); } dishService.saveOrUpdate(dish); return redirect:/admin/dish/list; }注意本地路径要用绝对路径或配置在application.yml里如file.upload-dir: D:/upload/不要写在代码里。还有一个坑Spring Boot的默认上传文件大小限制是1MB上传高清菜品图容易报错在配置里调大spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB图片读取路径要在配置类中注册Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir); }6.4 页面搜索与自动填充几个提升细节度的功能景点列表页提供按价格区间筛选、按名称模糊搜索查询条件要在Controller里组合拼接好餐厅页面支持按分类Tab切换列表用th:each渲染分类Tab并高亮当前选中项t_user表创建时间和t_travel_order的创建时间等字段使用MyBatis-Plus的自动填充在实体上增加TableField(fill FieldFill.INSERT)并实现MetaObjectHandler接口省去手动set时间。这些功能单独看不大但拼在一起项目就从“能跑”变成了“好用”。7. 联调、打包与部署本地能跑还不算完7.1 后端自测与联调常用方式写完接口不要直接上浏览器先用单元测试或直接工具访问一下接口。我自己习惯先做几个自测场景未登录访问受保护接口是否跳转到登录页或者返回401 JSON用户下餐饮订单后管理员能否正确看到订单并能完成接单→制作→上菜流程并发下同一线路下单库存会不会变成负数测试并发就要用线程并发模拟调用我写接口时故意加了个“库存1”的线路开两个线程同时下单确认只有一个成功数据库里的金额字段是否和前端展示一致有没有四舍五入不统一的地方。接口调通之后再连前端页面优先走一遍用户完整闭环注册→登录→看线路→下旅游订单→支付→看订单列表再走一遍管理员完整闭环登录→新增景点→新增线路关联刚建的景点→新增餐厅和菜品→处理用户订单。7.2 项目打包Maven打包与启动参数详解Spring Boot项目用Maven打包。在pom.xml里确保引入了spring-boot-maven-plugin。打包命令mvn clean package -DskipTests打包完成后target目录下会生成一个xxx-0.0.1-SNAPSHOT.jar。部署时直接java -jar xxx-0.0.1-SNAPSHOT.jar如果要改数据库配置又不想改包就用外部配置覆盖java -jar xxx.jar --spring.datasource.urljdbc:mysql://localhost:3306/travel_food_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai常见打包失败原因就是测试类编译失败或者单元测试报错加-DskipTests跳过还有个坑是用了Lombok但没在IDEA插件或编译插件中启用注解处理报找不到getter/setter检查Maven的annotationProcessor配置没问题的就能通过。7.3 一个我踩过的环境大坑我这里说要提的一个环境坑把项目从本机往毕业设计演示环境迁移比如换一台电脑或学校机房最常出问题的三个点MySQL连接时区问题数据库地址忘了写serverTimezoneAsia/Shanghai会报时区异常数据库SQL脚本导出不全表结构里有外键或唯一索引没导进去启动时初始化数据失败本地图片上传路径是D:/upload换到别的电脑就报目录不存在要记得启动前先创建对应目录或者在配置里由代码自动创建推荐后者PostConstruct public void init() { File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } }这种启机校验的代码看起来不起眼但在你答辩现场环境不可控的情况下它能大大减少“当场翻车”的概率。8. LW文档毕业设计论文怎么和代码配合结构与常见丢分点论文这半个LW是容易被低估分数占比高的环节。下面按我经验梳理一个适合本系统的高分写作框架每个章节都能和代码形成对应关系。8.1 论文各章节写作要点摘要可以写清楚系统采用基于Spring Boot MyBatis-Plus MySQL的B/S架构实现了旅游线路、餐饮管理、订单、评论等核心业务以及用户与管理员双角色体系。关键词就填Spring Boot、Spring MVC、MyBatis-Plus、旅游餐饮、MySQL。第一章“绪论”除了模板性背景一定加实际调研。你可以写“在旅游高峰期景区周边餐饮信息分散游客难以及时获取可靠的餐厅和菜品信息于是选择该课题”这就是业务驱动的立项依据。第二章“相关技术介绍”别写成教科书。介绍Spring IoC时结合你代码里Service、Autowired的用法介绍Spring MVC时结合项目里的请求链路“浏览器请求→DispatcherServlet→Controller→Service→Mapper→MySQL→视图渲染”。第三章“系统分析”要画出用例图用Visio或draw.io把普通用户和管理员分别能用什么功能罗列清楚。这是调研阶段的抽象产物你可以在论文中把前面提到的各个页面功能拆成用例描述的表格。第四章“系统设计”要放架构图、功能模块图、数据库ER图。表结构可以整理成表清单并注明“详见附录建库脚本SQL”。第五章“系统实现”是你最有话可说的部分按“登录注册模块”“旅游线路模块”“餐饮购物车模块”“订单状态管理模块”“管理端统计分析”五个小节展开配合关键代码片段截图。特别注意代码截图别太糊格式统一关键方法名用文字描述别放一百行的长代码。第六章“测试”别只写“能运行”。你至少要写测试目的、测试环境、测试用例表输入/预期结果/实际结果、性能测试比如用JMeter简单压一下列表接口的响应时间以及一两个缺陷修复记录。测试用例的表格按我经验是你最容易得分的部分因为只要你有真实测试的记录导师就很难挑毛病。8.2 答辩前必须准备的问题清单根据这个系统当面答辩大概率出这些题提前按自己项目理好答案Spring IoC和DI在项目中哪里有体现答案Service标注业务类Autowired注入依赖Spring容器统一管理Bean生命周期MyBatis-Plus和MyBatis区别是什么你为什么选择前者答案强调整体简化单表CRUD开发复杂查询依然可自定义SQLTransactional在你的下单流程中如何保证原子性答案数据库事务提交/回滚任何异常都会回滚前面的插入更新操作两张订单表为什么要分开答案旅游订单和餐饮订单的状态机不同、字段差异大合并会造成空字段冗余分开更清晰如果数据库访问量增大系统哪里会出现瓶颈怎么优化答列表页的模糊查询没有索引可以加索引热点线路缓存到Redis分页加缓存优化这个系统你觉得还有哪些不足答弱密码登录存在隐患没接入真实支付未做Redis缓存未做消息队列削峰这些就是未来展望。9. 如何把项目从“能交”提升到“优秀”几条我实操过的小建议最后分享几个实操层面的加分技巧不会大幅增加你的开发负担但在评分上很有意义。建议一统一项目命名与包名。把com.example改成有辨识度的com.travelfood之类前端页面的标题统一“XX旅游餐饮管理系统”。这些小细节会给老师“用心做了”的第一印象。建议二做一个“系统说明”页面或已Readme。在项目根目录写清演示账号管理员admin/admin123用户test/123456、启动步骤、数据库导入方式。答辩演示时直接从说明页面打开显得很专业也方便老师快速上手。建议三把建库脚本整理成独立的SQL文件并加中文注释。这个项目少说10张表以上如果老师的验重环节要求你演示建库你两分钟内导入SQL成功效果非常好。另外在SQL脚本里就把测试数据、演示账号插入进去不要交一个空库给我。建议四每个Controller方法尽量添加Swagger注解或注释业务流程。Spring Boot集成Springdoc或Swagger UI版本选择要注意和Boot版本匹配但如果说不想引入依赖至少每个Controller头上写清接口作用每个Service的关键方法加业务注释。答辩时现场敲几个方法名能说明“这个接口是干嘛的”老师对你的代码熟悉度评价会非常高。建议五准备一个小“亮点Demo”。找个1-2分钟的小场景演示“用户在旅游线路详情页看到周边餐厅→点击进入餐厅下了一单→管理员在后台接单→用户看到订单状态变化”把这条完整链路走一遍比任何PPT都有说服力。这也是我在前文反复强调旅游和餐饮要联动的原因——这个系统的特色就在这两条业务线的交叉答辩时把这个交叉讲好整个项目的立题价值就有了。从选题到源码从页面到论文毕业设计是一场“编代码讲故事”的综合工程。代码是骨架论文是血肉答辩是出场表演。我写的这些经验是基于我当时做这类管理系统一把一把踩出来的坑你应该能靠着它少走许多弯路。记住稳扎稳打功能先跑通再考虑炫技你的“旅游餐饮管理系统”肯定能保质保量收官。
返回列表