
简介这套基于Spring Boot的外卖点餐系统完整项目资料面向Java毕业设计人群覆盖用户下单、商家接单、骑手配送、后台管理等主流业务流程适合作为毕设选题或课程设计参考。压缩包共638个文件、约25.24MB以Java后端源码、Vue前端页面、HTML/JS脚本以及JPG/GIF界面截图为主另含SQL数据库脚本、XML配置与答辩PPT类型齐全可直接用于项目复现。目前已有134人学习下载。资料包含完整论文与PPT答辩文档论文按绪论、技术介绍、需求分析、系统设计、系统实现、系统测试等章节撰写并配有ER图和数据表设计源码中的用户前台、商家、管理员、骑手四类功能模块划分清晰便于理解分层结构与业务逻辑。无论是想完成毕设交付还是学习Spring Boot Vue的实践整合这份资料都能提供从文档到代码的完整闭环支持。1. 外卖点餐系统Spring Boot 毕设里的经典闭环Spring Boot 外卖点餐系统之所以能成为 Java 毕设的常青选题不是因为名字好听而是它刚好踩中了一套后端技术该有的全部落点用户管理、商品展示、购物车、下单、订单状态流转、后台管理横跨 CRUD、事务、鉴权、关联查询这些必考能力。这套资源包含可直接运行的源码、配套论文和 PPT 答辩稿意味着你不只是在下载一个 demo而是在拿一套「能跑、能讲、能答」的完整毕业设计。适合两类人一是时间紧、想基于成熟项目快速改造的毕业生二是想借一个完整业务场景把 Spring Boot 实战串一遍的开发者。系统默认按「用户端 商家端 管理端」三端设计角色清晰现场演示时不需要额外造数据。2. 功能拆解用户、商家、管理三类角色各管什么2.1 用户端注册登录、浏览菜单到提交订单的完整链路用户端是一条完整的前台购物链路也是答辩时演示的主线。注册登录之后用户按菜品分类浏览商品列表查看商品详情把想吃的菜加入购物车购物车页面可以改数量、删条目、自动算总价最后选择收货地址提交订单。订单提交后用户能在「我的订单」里看到状态变化待支付、待接单、配送中、已完成。这个链路里最核心的业务逻辑在「提交订单」这一步。它不是简单的 insert而是要在同一个事务里完成购物车条目读取、订单主表生成、订单明细批量写入、购物车清空。常见做法是把这几步放到一个Transactional方法里我在这个项目的 Service 层也是这么处理的Transactional(rollbackFor Exception.class) public Order submitOrder(OrderSubmitDTO dto) { // 1. 校验收货地址和购物车条件不满足直接抛业务异常 if (dto.getAddressId() null || cartService.cartEmpty(dto.getUserId())) { throw new BusinessException(收货地址或购物车为空); } // 2. 查出购物车全部条目逐条计算小计再累加得到订单总金额 ListCartItem cartItems cartService.listCartItems(dto.getUserId()); BigDecimal totalAmount cartItems.stream() .map(item - item.getPrice().multiply(new BigDecimal(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); // 3. 生成订单主表记录初始状态为待支付0 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setAddressDetail(dto.getAddressDetail()); order.setAmount(totalAmount); order.setStatus(OrderStatus.UNPAID.getCode()); orderMapper.insert(order); // 4. 遍历购物车条目生成订单明细冗余商品名和单价快照 for (CartItem item : cartItems) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setProductName(item.getProductName()); orderItem.setPrice(item.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); } // 5. 全部成功才清空购物车任何一个环节异常则整体回滚 cartService.clearCart(dto.getUserId()); return order; }这里有两个容易被忽略的参数细节。一是Transactional(rollbackFor Exception.class)里的rollbackFor必须写全Spring 默认只对 RuntimeException 回滚如果业务里抛的是不继承 RuntimeException 的自定义异常事务不会生效订单主表插进去了但明细没插进去数据就脏了。二是订单号generateOrderNo()我一般用「时间戳 用户ID后四位 随机数」拼接长度控制在 32 位以内避免并发下单时重复。用户端还有一个细节值得注意商品列表接口必须支持categoryId和keyword两个可选参数前者做分类筛选后者做关键字搜索。数据层用 MyBatis-Plus 的LambdaQueryWrapper做动态条件拼接比写死 SQL 灵活得多这也是答辩时「你的搜索是怎么实现的」的标准答案素材。2.2 商家端商品上架、改价与订单状态流转商家端的核心是商品管理和订单处理。商品管理看起来是标准 CRUD但有两个细节一是删除用逻辑删除而非物理删除否则历史订单的明细关联会断二是商品必须有上下架状态字段用户端查询时只查上架商品商家改状态即时生效不需要重启服务。商品上下架我一般用status字段0 下架、1 上架。这个项目里商品更新的 Controller 写法如下RestController RequestMapping(/api/product) public class ProductController { Autowired private IProductService productService; PostMapping(/update) public ResultVoid update(RequestBody Product product) { // 先判断商品是否存在不存在直接返回业务错误 Product existing productService.getById(product.getId()); if (existing null) { return Result.error(商品不存在); } // 只更新白名单里的字段防止越权改销量、创建时间 productService.lambdaUpdate() .eq(Product::getId, product.getId()) .set(Product::getName, product.getName()) .set(Product::getCategoryId, product.getCategoryId()) .set(Product::getPrice, product.getPrice()) .set(Product::getDescription, product.getDescription()) .set(Product::getStatus, product.getStatus()) .update(); return Result.success(); } }这个接口的参数校验点在于前端传来的Product对象里可能带了id、sales、createTime等字段后端只应该信任白名单字段。用lambdaUpdate().set(...)显式指定要更新的列比直接updateById(product)安全——后者会把对象里所有非空字段都更新一遍如果前端多传一个sales: 99999销量就被恶意刷了。答辩时被问「越权更新怎么防」能说出这一层就能拉开差距。订单处理的核心是状态流转管理。这个项目把订单状态定义为0 待支付、1 待接单、2 配送中、3 已完成、4 已取消。商家端的操作集中在两个动作接单1 → 2和完成配送2 → 3。状态变更的代码一定要放在 Service 层做前置校验不能直接在 Controller 里updateByIdpublic void updateOrderStatus(Long orderId, Integer targetStatus, Long operatorId) { Order order orderMapper.selectById(orderId); if (order null) { throw new BusinessException(订单不存在); } // 状态机校验只允许固定方向的流转其余全部拒绝 boolean allowed (order.getStatus() 1 targetStatus 2) || (order.getStatus() 2 targetStatus 3); if (!allowed) { throw new BusinessException(当前订单状态不允许该操作); } order.setStatus(targetStatus); orderMapper.updateById(order); }这里的allowed判断就是最简单的状态机。它挡住了两类问题一是用户或商家重复点击「接单」导致状态重复流转二是跳过中间状态直接「完成」比如把待接单的订单直接改成已完成数据就不符合业务流程了。答辩时如果老师问「订单状态为什么不用字符串而用数字」你可以回答数字存储省空间、比较快配合枚举常量类可读性也不差而且状态机校验逻辑统一收敛在一个方法里调用方不会误用。2.3 管理端分类管理、用户管理与订单统计管理端的存在意义是让系统形成完整闭环管理员维护菜品分类、管理用户状态、查看订单统计。分类表通常有sort字段控制用户端的展示顺序用户管理做到「禁用/启用」级别就够不需要做细粒度权限订单统计是答辩时最好演示的功能点。订单统计一般按时间范围和状态维度做分组查询展示订单总数、销售额、各状态订单数。统计放在 SQL 层用GROUP BY实现不要在 Java 里循环聚合——数据量一大两者性能差距非常明显SELECT DATE(create_time) AS order_date, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders WHERE create_time BETWEEN #{startTime} AND #{endTime} AND status 3 GROUP BY DATE(create_time) ORDER BY order_date这条 SQL 里status 3是容易忽略的业务口径只有已完成订单才计入销售额。如果把待支付订单也算进去统计数字会虚高得离谱因为用户可能下单后不支付直接关掉页面。答辩时如果老师追问「销售额统计口径是什么」回答「只统计已完成订单排除未支付和已取消的单」就是一个思考过业务细节的加分回答。管理端还有一个容易被忽视的点账号管理。这个项目里管理员账号和用户账号共用一张用户表用role字段区分。角色判断放在拦截器层面做后台接口统一要求role 1管理员前台接口只要登录就行。这么设计表少、链路简单对毕设的体量来说是合理的后续要扩展多角色权限再引入 Spring Security 或自定义注解鉴权也不迟。3. Spring Boot 实现细节分层架构、数据库设计与鉴权方案3.1 项目结构controller-service-mapper 三层怎么分这个项目用的是标准 Spring Boot 三层架构Controller 接收请求、Service 处理业务、Mapper 操作数据库实体统一放 entity 包通用类和工具单独放 common 包。分层的价值不是代码好看而是让复杂业务里每一层只干一件事订单提交这个操作Controller 里只做参数校验和结果封装事务和业务规则全在 Service 层Mapper 只负责 SQL。我见过很多毕设把业务逻辑写在 Controller 里看起来代码量小但一旦要加事务控制或者复用下单逻辑就非常痛苦。这个项目的包结构是教科书级的com.example.delivery ├── controller # 接口层接收请求、参数校验、返回 Result │ ├── UserController │ ├── ProductController │ ├── OrderController │ └── AdminController ├── service # 业务层事务、业务规则、状态流转 │ ├── OrderService │ ├── ProductService │ └── CartService ├── mapper # 数据层继承 BaseMapperCRUD 免写 XML │ ├── OrderMapper │ ├── ProductMapper │ └── UserMapper ├── entity # 实体类字段对应数据库表 │ ├── User │ ├── Order │ ├── OrderItem │ └── Product ├── common # 通用类Result 封装、异常、工具 │ ├── Result │ └── JwtUtil └── config # 配置类分页插件、拦截器、跨域 ├── MybatisPlusConfig └── WebMvcConfig这是 Spring Boot 项目最常规的布局也是论文里「系统设计」章节最好写的一张图。你在论文里画包结构图时直接基于这个目录展开每一层对应一段职责描述。各层职责用一张表就能说清层级职责典型内容Controller参数接收、简单校验、结果封装不写业务逻辑不直接碰 MapperService业务规则、事务控制、状态流转下单、支付回调、状态机校验MapperSQL 操作、分页、条件查询继承 BaseMapper复杂 SQL 用注解或 XMLEntity数据库表映射字段与表结构一一对应答辩时被问「为什么这么分层」核心答法是把易变的部分隔离开数据库字段变了只改 Entity 和 Mapper接口参数变了只改 Controller业务规则变了只改 Service互不牵连。这个回答比背分层概念要有说服力得多。3.2 数据库设计六张核心表与订单状态机数据库设计是毕业设计论文里最占篇幅的部分也是老师最爱挑刺的地方。这个项目核心表有六张用户表、分类表、商品表、购物车表、订单表、订单明细表。订单主表和订单明细表是一对多关系明细表冗余了product_name和price字段这是电商系统的常规做法——商品改名或改价后历史订单的展示仍要保留下单时的快照。订单表的核心字段大致如下字段类型说明idbigint主键自增order_novarchar(32)订单号业务唯一user_idbigint下单用户address_detailvarchar(255)收货地址快照amountdecimal(10,2)订单总金额statustinyint订单状态0待支付 1待接单 2配送中 3已完成 4已取消create_timedatetime下单时间pay_timedatetime支付时间可为空finish_timedatetime完成时间可为空订单状态用整数枚举而不是字符串好处是存储小、比较快坏处是代码里要维护状态含义。建议把状态常量定义成OrderStatus枚举类禁止在业务代码里写魔法数字。状态机的流转规则是待支付可以取消0 → 4或支付0 → 1待接单只能接单进入配送1 → 2配送只能完成2 → 3已完成是终态。数据库层面还要注意两点。一是金额字段必须用decimal(10,2)不能用float或double二进制浮点算钱会出精度问题这是电商系统的铁律。二是每张表都要有create_time和update_time配合 MyBatis-Plus 的MetaObjectHandler自动填充代码里不需要手动 set。这个自动填充在答辩时也是可以讲的细节数据库字段用datetimeJava 端用LocalDateTime配合全局配置里map-underscore-to-camel-case: true下划线命名和驼峰命名自动映射实体类就不需要写繁琐的TableField注解了。3.3 登录鉴权毕设场景下选 JWT 还是 Session登录鉴权是简历和答辩的高频考点。这个项目用的是 JWT 方案因为前后端分离场景下 JWT 无状态、服务器不存 Session前端拿到 token 存 localStorage每次请求放在 Authorization 头里。Session 方案的优势是服务端可控、可以随时踢人但前后端分离时跨域要配 Cookie 跨域麻烦得多JWT 的代价是 token 签发后没法主动失效只能等它过期。毕设规模下选 JWT 是更主流的选择。JWT 工具类核心逻辑是生成和解析两个方法public class JwtUtil { // 密钥和过期时间正式项目必须放配置文件这里仅为演示 private static final String SECRET delivery-secret-key; private static final long EXPIRE_MS 1000 * 60 * 60 * 24; // 24小时 public static String createToken(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_MS)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Long parseUserId(String token) { Claims claims Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }使用 JWT 时有三个容易踩的点。第一SECRET不要硬编码通过Value(${jwt.secret})读取application.yml里的配置换一台电脑部署不用改代码。第二拦截器里对OPTIONS预检请求必须直接放行否则前端跨域请求会在拦截器这一层就被拦下来返回 401 而不是真正的跨域错误排查起来非常迷惑。第三parseClaimsJws解析失败会抛异常要么在方法里 catch 后返回 null要么在拦截器里统一捕获否则 token 一过期用户看到的是 500 页面而不是跳转登录页。拦截器的注册与放行路径配置我放在了WebMvcConfig里Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/login, // 登录注册放行 /api/register, /api/product/**); // 商品浏览放行 } Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }这里excludePathPatterns(/api/product/**)是个很实际的取舍商品浏览是用户端首页就要加载的数据如果也要 token用户没登录就看不了菜单演示时第一屏就卡住了。权限策略应该是「浏览公开、下单登录、后台管理鉴权」而不是一刀切全拦。这套思路写进论文的「系统设计」章节比只贴代码有说服力得多。4. 本地复现环境准备、配置修改与跑通下单闭环4.1 环境版本搭配JDK、Maven、MySQL 怎么选复现这个项目之前先把环境版本对齐这是减少玄学报错最有效的一步。Spring Boot 2.7.x 用 JDK 8 或 11 都可以但如果你手头脚手架初始化出来的是 Spring Boot 3.x那必须配 JDK 17而且很多第三方 starter 的坐标和配置方式都不一样兼容性是个隐形工作量。我的建议很直接毕设别追新用项目源码自带的 Spring Boot 版本配 JDK 8 最稳遇到问题可复现的案例也最多。Maven 配置有两个容易忽略的点。一是settings.xml里配置阿里云镜像否则从中央仓库拉依赖会慢到让人怀疑人生二是确认 IDEA 的 Maven 配置用的是本地仓库不要每次构建都去解析远端索引。开始之前先确认三个基础环境java -version mvn -v mysql --version三条命令分别确认 JDK、Maven、MySQL 的版本。如果java -version显示的是 17 而项目是 Spring Boot 2.7先别急着换项目——直接把 JDK 切到 8 更省事IDEA 里 Project Structure 和 Settings 里的 SDK 都要改只改一处构建时还是会报错。版本搭配参考这张表组件推荐版本说明JDK1.8与 Spring Boot 2.7.x 兼容Maven3.6配阿里云镜像速度提升明显MySQL8.0驱动用 com.mysql.cj.jdbc.DriverIDEA2021默认配置即可无需额外插件环境对齐后执行依赖构建mvn clean install -DskipTests这一步如果报「依赖找不到」九成是网络问题导致依赖没拉全。排查顺序先看本地仓库对应目录下有没有.lastUpdated文件有就说明下载中断删除这个目录重新执行mvn clean install如果反复失败再检查镜像配置是否生效。一条一条查比把整个.m2仓库删掉重下高效得多。4.2 配置文件修改数据库连接与端口调整项目跑起来之前必改的只有一处application.yml里的数据源配置。把本地 MySQL 的账号、密码、端口、库名换成你自己的。关键配置贴出来每行都有注释说明server: port: 8080 # 后端端口前端接口请求走这里的 /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/delivery_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 # 改成你自己 MySQL 的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true # 数据库下划线自动映射驼峰 global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段 logic-delete-value: 1 logic-not-delete-value: 0三个配置细节值得说清楚。第一serverTimezoneAsia/Shanghai必须带MySQL 8.x 默认时区是 UTC不带这个参数启动时大概率报The server time zone value错误。第二MySQL 8.x 用com.mysql.cj.jdbc.DriverMySQL 5.7 用com.mysql.jdbc.Driver驱动类和数据库版本不匹配会直接启动失败。第三logic-delete-field一旦声明MyBatis-Plus 的删除操作会自动变成UPDATE ... SET deleted 1查询自动追加deleted 0条件——这个机制写论文时必须讲清楚不然老师看到数据删不掉会追问。注意改了application.yml之后先mvn clean再启动避免 IDEA 缓存了旧的 class 导致改动不生效。数据库导入顺序是先在 MySQL 里建库再执行 SQL 脚本建表。建库命令CREATE DATABASE delivery_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;然后用 Navicat 或命令行执行项目自带的delivery_db.sql脚本。如果脚本里带了初始数据就一并导入导入后务必检查product表是否有数据——很多系统首页是空的就是因为没导入初始菜单数据演示时第一眼就露怯。4.3 启动与验证按用户视角走一遍完整流程配置改完启动 Spring Boot 应用控制台出现Started Application in xx seconds就说明启动成功。这个项目默认端口是 8080如果被占用两种处理方式一是改application.yml里的server.port二是找到占用进程杀掉。Windows 下查询占用端口的命令netstat -ano | findstr 8080 taskkill /PID 进程号 /F功能验证我习惯按这条链路走注册用户 → 登录拿 token → 把 token 放到后续请求 Header → 浏览商品分类 → 加入购物车 → 提交订单 → 去数据库orders表确认记录生成。这条链路走完系统核心功能就全部验证过了。接口层面的自测用 Postman 或 Apifox几个核心接口如下接口方法说明是否需登录/api/loginPOST登录返回 token否/api/registerPOST注册否/api/product/listGET商品列表支持分类和关键字否/api/cart/addPOST加入购物车是/api/order/submitPOST提交订单是/api/order/listGET我的订单是如果项目里带了 Vue 前端构建产物放到src/main/resources/static目录启动后端直接访问http://localhost:8080就能看到页面。这是「vue 打包放进 springboot」的标准做法毕设演示时不需要单独开前端服务老师打开浏览器就能看现场翻车概率最低。前端构建命令npm install npm run build构建产物在dist目录把内容拷贝到static即可。这里两个细节一是前端路由如果用了 history 模式刷新会 404需要后端做静态资源 fallback毕设场景直接用 hash 模式最省事二是前端接口的baseURL要改成相对路径/api而不是写死http://localhost:8080/api否则换个端口部署又得改代码。另外启动时控制台那个默认的 Spring Boot banner 是可以个性化换掉的resources目录下放一个banner.txt可以用在线 banner 生成器生成一段 ASCII 艺术字答辩演示时这个小细节挺加分的。5. 避坑指南毕设复现路上的五个翻车点毕设开发过程中真正磨人的往往不是业务逻辑而是这些环境、配置、拦截器层面的问题。下面五条是我在这个项目上实打实踩过的坑每一条都按「现象 → 原因 → 解决」整理希望能帮你少走弯路。5.1 数据库连不上时区报错与驱动类不存在现象启动时控制台报The server time zone value йʱ is unrecognized或者ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因前者是 JDBC 连接串没带serverTimezoneMySQL 返回的默认时区名称和 JDBC 不匹配后者是驱动版本与 MySQL 版本对应不上——MySQL 8 装了 5.x 驱动或者 MySQL 5.7 装了 8.x 驱动。解决连接串统一加serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8确认pom.xml里mysql-connector-java的版本和本机 MySQL 大版本一致。改完配置后执行mvn clean重新编译一次避免 IDEA 缓存了旧的编译结果。如果还是报错用数据库客户端先手动连一下确认账号密码和端口本身没问题——很多时候是密码里有特殊字符没转义。5.2 静态资源 404拦截器把页面和接口一起拦了现象后端启动成功Vue 页面也放到了static目录但访问http://localhost:8080返回 404或者页面加载出来了但所有接口返回 401。原因登录拦截器配置了.addPathPatterns(/**)把静态资源和登录接口全拦了。排错的时候先看控制台有没有拦截日志或者给拦截器加一个临时的日志输出——能迅速定位是拦截器的问题还是路由的问题。解决拦截器拦截路径精确到/api/**放行/api/login、/api/register、/api/product/**静态资源路径/static/**、/css/**、/js/**全部放行。另外所有OPTIONS预检请求直接返回 true跨域请求在浏览器发正式请求前会先发一次 OPTIONS被拦截器挡掉的话页面上的每个请求都会报跨域错误而且控制台报错信息容易误导你去查 CORS 配置。5.3 中文乱码四处编码不在一条链上现象数据库里中文正常页面返回的中文却是???或乱码。原因编码链上任意一环断了就会乱码——数据库连接串没加characterEncodingutf8、IDEA 文件编码是 GBK、MySQL 表建成了latin1字符集、前端页面少了meta charsetUTF-8。毕设里最常见的是连接串漏参数其次是导入 SQL 脚本时表结构本身用了旧字符集。解决第一连接串加characterEncodingutf8第二IDEA 的 File Encoding 全部改成 UTF-8第三确保建表语句是DEFAULT CHARSETutf8mb4已经是 latin1 的表执行ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4。改完重启后端再验证一遍前端meta标签确认在链路上四处在同一个编码里才不乱码。这个问题的排查顺序就是从后端往前端哪里显示不对就先查哪里。5.4 分页查询不生效MyBatis-Plus 分页插件没注册现象分页接口返回的不是按页数据而是全量数据或者Page对象的total恒为 0。原因MyBatis-Plus 3.x 的分页功能依赖PaginationInnerInterceptor必须显式注册进MybatisPlusInterceptor。没注册时Page参数会被当成普通参数拼进 SQL分页逻辑完全不生效——代码不报错但行为不对属于最磨人的「黑匣子」问题。解决在MybatisPlusConfig配置类里注册分页拦截器指定数据库类型为 MySQLConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); // 单页最大条数防止恶意传超大 pageSize interceptor.addInnerInterceptor(pagination); return interceptor; } }注册之后分页 SQL 才会自动拼接LIMITtotal才会走 count 查询。setMaxLimit(500L)是我习惯加的一行保护如果前端传pageSize100000没有上限会把整张表查出来有这个兜底就安全了。另外注意如果你的代码里用了自定义 SQL 且带了分页需要在 mapper 方法参数里把Page放在第一位否则分页插件识别不到。5.5 前后端联调跨域接口通了但浏览器拦截现象前端页面能打开后端接口也能通但浏览器控制台报 CORS 错误。用 Postman 测接口完全正常一开浏览器就翻车。原因浏览器同源策略的拦截。前端地址是http://localhost:3000后端是http://localhost:8080端口不同就是跨域。Postman 不校验跨域所以 Postman 测不出这个问题。解决在WebMvcConfig的addCorsMappings里配置允许的来源和方法。开发阶段allowedOriginPatterns(*)够用但你要知道这个配置的代价任何网站都能调你的接口好在接口都有登录鉴权兜底。生产环境严谨的做法是白名单写死前端域名。这里如果前端用了axios的withCredentials: true后端allowCredentials(true)要配套开否则浏览器的拦截依旧存在。还有一个隐蔽坑如果你的拦截器先于跨域过滤器执行OPTIONS请求会在拦截器里被拦跨域就永远配不通——所以拦截器里对OPTIONS放行要写在最前面。6. 把毕设做出彩论文结构、答辩话术与一个加分扩展6.1 论文和 PPT 怎么用别把下载的文档直接交配套论文和 PPT 答辩稿是底稿不是终稿。我的习惯是先通读论文把截图替换成自己跑通系统后的真实截图把系统名称、功能描述、技术版本改成和自己的项目一致。查重固然重要但更重要的是答辩时「论文写的」和「现场演示的」不能对不上。PPT 控制在 15 页以内按「选题背景 → 技术栈 → 系统设计 → 核心功能演示 → 总结」推进每页讲 30 秒左右总时长压在 8 分钟以内留出时间给老师提问。答辩被问「系统有什么不足」时别说「没有不足」。我通常准备两个诚实且可控的回答一是「支付环节用的模拟支付没有对接真实支付网关」二是「当前没有做高并发场景的压力测试」。这两个回答真实但不致命反而说明你对项目边界有清醒认识。高频问题里「登录鉴权怎么做、订单状态怎么流转、项目为什么用三层架构」这三个第 3 章的代码和解释可以直接当回答素材。6.2 一个低成本加分扩展接入支付宝沙箱支付如果时间充裕建议加一个支付宝沙箱支付。沙箱环境免费申请接入流程和真实支付完全一致不需要商户资质。加完之后订单状态机从「待支付 → 待接单」有了真实回调链路后端生成支付二维码链接前端展示二维码用户扫码支付支付宝异步通知后端接口后端校验通知参数后把订单状态改为已支付。这就是支付回调的完整闭环答辩演示时扫码支付那一下的效果比任何话术都有说服力。做完这个扩展订单模块就从「简单 CRUD」升级成「有状态机、有回调、有幂等处理」的真实业务模块论文的「系统实现」章节能多写两页简历上「项目经历」也多一行可写的亮点。从那以后我每次拿到新的毕设项目源码都会强制自己先走一遍「注册 → 加购 → 下单 → 改状态」的完整链路再碰代码而不是上来就改界面换字段——顺序一反过来改完就分不清是功能坏了还是自己改坏了。希望这篇拆解能帮你把系统跑起来答辩顺利。本文还有配套的精品资源点击获取