Spring Boot+Vue二手家电管理系统:从设计到部署的毕设实战指南
说句大实话每年到了毕业设计开题季“Spring Boot Vue”的管理系统永远是出现频率最高的一类题目。但这套二手家电管理系统是我在所有同类题目里最愿意推荐的一个——它难度适中、技术栈主流、业务闭环完整既不像图书管理系统那样单薄也不至于复杂到一个人搞定不了。下面的内容是我基于一套完整的二手家电管理系统包含Spring Boot后端、Vue前端、MySQL数据库脚本和配套文档做的复盘从选题价值、技术选型、数据库设计到前后端实现、联调部署、避坑经验一次性说透。打算照这个方向做毕设的同学可以直接当成操作指南来读只是想评估这个项目值不值得做、能不能做的同学也能从中找到判断依据。1. 这个项目到底在做什么选题价值与功能边界1.1 为什么是“二手家电”这个业务场景很多同学挑毕业设计题目的时候习惯先看技术再看业务。但我的经验恰恰相反业务场景选得对不对直接决定了项目做出来之后的完成度和答辩时的说服力。二手家电交易这个场景天然就比“图书管理”“学生信息管理”多了一层真实感。它的核心是信息发布加交易流转卖家发布一件闲置家电买家浏览、搜索、下单、确认收货管理员在后台维护类目和用户。整个过程涉及商品状态、订单状态、用户权限多个维度比单纯增删改查有东西可写。同时它又是一个“简化版的C2C平台”没有真实电商那么重的库存、支付、物流、售后拆解一个人完全能在毕业设计周期内做完。用一句话总结这个选题的定位像一个精简版二手交易平台保留最核心的交易闭环砍掉与业务无关的复杂度。它的功能边界也非常典型用户注册登录普通用户和管理员两种角色商品发布、编辑、上下架、图片上传商品分类浏览、关键词搜索、分页展示买家下单、卖家发货、买家确认收货个人中心我发布的、我买到的、我卖出的管理后台用户管理、商品管理、类目管理1.2 系统角色与核心功能清单用一个表把角色和功能理清楚做需求分析的时候也能直接搬过去用。角色核心功能说明游客浏览、搜索、查看商品详情不需要登录也能看信息普通用户买家/卖家注册登录、发布商品、下单购买、管理订单、个人资料一个人同时具备买卖两种身份用当前登录用户ID区分管理员类目管理、用户状态管理、商品状态管理、订单监控有独立后台页面不走正常交易流程普通用户和管理员是同一个用户表通过 role 字段区分这是大多数同类系统的标准做法。管理员可以冻结违规账号、下架不合规商品但管理员不参与交易。对于毕业设计来说功能做到这个程度已经足够覆盖需求分析、系统设计、测试几个论文章节了。1.3 交付物怎么理解源码、数据库、文档这个题目标注了“源码数据库文档”三个交付物很多同学看到这六个字反而不知道要准备什么。我拆开讲源码不是一整个文件夹甩过去而是后端和前端两个独立工程。后端是Spring Boot的Maven工程前端是Vue的工程前后端通过接口通信。代码要能直接跑起来最好附带一份简短的README写清楚数据库怎么建、账号密码是什么、端口怎么配。数据库配一份可执行SQL脚本里面包含建库、建表、初始化数据。初始化数据至少要有一个管理员账号、几个家电分类、几台示例商品这样演示的时候打开首页才不至于空荡荡。文档对应毕业论文或者设计说明书。核心内容是需求分析、系统设计、核心代码说明、测试结果。下面第7章我会专门讲文档怎么组织更拿得出手。这三个交付物不是孤立的数据库脚本是后端运行的前提文档里的系统截图和代码说明来自源码。把它们当成一个整体来准备答辩的时候才能前后呼应。2. 技术选型剖析为什么Spring Boot Vue是稳妥组合2.1 后端为什么选Spring Boot后端技术栈现在很多Spring Cloud、Go、Node.js都有人用但毕业设计这个场景下Spring Boot几乎是性价比最高的选择。Spring Boot最大的价值是“自动配置”和“内嵌容器”。以前用SSMSpring Spring MVC MyBatis搭环境要写一堆XML配置文件配置一个事务就可能折腾半天。Spring Boot通过starter依赖把常用的场景整合好约定优于配置启动一个Web项目用不了几步。内置Tomcat意味着不需要单独装服务器打包成jar直接跑这对部署和答辩演示都非常友好。再加上Java生态本身成熟面试和求职时Spring Boot也是高频考点做一个基于它的项目简历上写起来有底气。作为一个带过不少课题的人我见过太多人在SSH、SSM上耗掉大把时间调配置最后核心业务代码根本没写几行。这种吃亏的事没必要再踩一遍。2.2 前端为什么选Vue二手家电系统需要几十个页面交互用原生HTML和jQuery当然也能做但代码组织会非常痛苦。Vue这种组件化框架把页面拆成多个组件每个组件只维护自己的数据和方法配合Element UI或Element Plus这类组件库复杂页面很快就能搭起来。Vue最核心的优势是数据和视图的双向绑定。比如搜索框的值输入之后下面列表的请求参数自动跟着变不需要手动操作DOM。这个开发体验对前端基础一般的同学来说非常友好因为你不用花大量精力去处理DOM节点可以把注意力集中在业务逻辑上。Vue 2还是Vue 3的问题我在实践中的建议是如果你的导师偏向稳妥、网上教程以Vue 2为主那就用Vue 2 Element UI如果希望项目更现代化也愿意花时间适应那直接上Vue 3 Element Plus。两种方案的代码结构差异不算太大核心业务逻辑几乎一样。2.3 前后端分离架构的工作流程这套系统是典型的前后端分离架构先弄懂它的运行链路后面联调才不慌前端Vue工程开发时跑在一个开发服务器上默认端口通常为5173Vite或8081Vue CLI页面在这里渲染。前端通过axios向后端发起HTTP请求访问的接口路径以/api开头。后端Spring Boot运行在8080端口接收请求后处理业务逻辑返回JSON数据。浏览器里页面展示的数据全部来自后端返回的JSON。开发阶段最大的麻烦是“跨域”前端端口和后端端口不一样浏览器会拦截跨域请求。解决办法后面第6章会讲到一个是后端配置CORS另一个是前端开发服务器代理转发。理解了这个流程再去看那些报错信息就不慌了。2.4 主要依赖与版本建议框架版本尽量别用太新的太新的容易踩兼容坑。下面这套是我实际用过比较稳的组合组件建议版本说明JDK1.8 或 11选最高支持到Spring Boot 2.7的版本即可Spring Boot2.7.x稳定、资料多不建议一上来就上3.xMyBatis-Plus3.5.x增强MyBatis内置增删改查省大量代码MySQL8.0版本主流兼容性好镜像资料也多Vue2.6 Element UI 2.15教程最多的组合Vue备选3.4 Element Plus更现代评审观感好但需要适应组合式APINode.js16 或 18用来运行前端工程版本锁定这个问题越早定越好。我看到过有人用Spring Boot 3.x搭项目然后发现javax.servlet变成jakarta.servlet网上很多教程代码直接不兼容白白浪费一个周末。选2.7.x不是技术落后是为了把精力留给业务代码。3. 数据库设计五张核心表撑起整个交易体系3.1 核心表结构与你该关心的字段数据库设计是整个系统最值得认真对待的部分。表设计好了后端代码写起来非常顺表设计乱了后面所有查询、统计、联调都会跟着难受。这套系统最核心的是user、category、product、orders、comment这五张表。我逐个说关键字段用户表userid主键自增username用户名唯一索引password密码必须存BCrypt哈希不能存明文nickname昵称phone手机号avatar头像地址role角色0普通用户1管理员status状态0正常1禁用create_time注册时间分类表categoryid主键name分类名称比如冰箱、洗衣机、空调sort排序权重create_time商品表productid主键seller_id卖家用户ID关联user表category_id分类ID关联category表title商品标题description商品描述price售价用DECIMAL(10,2)original_price原价可为空status商品状态0上架、1下架、2已售出cover_image封面图images轮播图多个地址用逗号分隔view_count浏览量create_time、update_time订单表orders注意order是MySQL关键字表名要加s或者用反引号建议直接用ordersid主键order_no订单编号唯一用时间戳加随机数生成product_id商品IDbuyer_id买家用户IDseller_id卖家用户IDtotal_price成交价格status订单状态0待付款、1待发货、2待收货、3已完成、4已取消create_time下单时间pay_time、ship_time、complete_time各状态流转时间评论表commentid主键user_id评论用户IDproduct_id被评论商品IDcontent评论内容rating评分1到5create_time建表SQL不需要写得太花哨但要加注释。比如user表一个够用的建表语句长这样CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码(BCrypt加密), nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint NOT NULL DEFAULT 0 COMMENT 角色:0普通用户,1管理员, status tinyint NOT NULL DEFAULT 0 COMMENT 状态:0正常,1禁用, create_time datetime DEFAULT NULL COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci COMMENT用户表;3.2 表关系与关键约束这五张表的关系并不复杂user 与 product一对多一个用户可以发布多个商品category 与 product一对多一个分类下可以有多个商品orders 与 product一个订单对应一个商品orders 与 user买家、卖家都是user表的引用我在设计时推荐“逻辑关联”而不是“物理外键”。意思是在Java代码层用卖家ID、买家ID做逻辑关联但数据库层不强制建立FOREIGN KEY外键约束。原因是毕业设计后期经常要改数据物理外键会造成很多隐藏的坑比如删除用户时被外键挡住没法删。用逻辑关联查询时通过表的字段自己控制编写的SQL反而更清晰答辩时也更容易讲明白。订单表同时存buyer_id和seller_id很多人会问为什么不在查询时JOIN商品表再拿卖家ID。直接存这两个ID是因为订单生成后如果商品被删除、卖家改名订单仍需要保留完整快照。这种“冗余设计”在实际系统中非常常见也是值得在答辩时拿出来讲的亮点。3.3 数据库初始化脚本让系统跑起来的第一关交付物里的SQL脚本只建表是不够的还要提供初始化数据这样评审老师打开系统就能看到内容。初始化数据应包含一个管理员账号role1密码必须是BCrypt加密后的字符串五个左右的商品分类冰箱、洗衣机、空调、电视、小家电每个分类下面放两三台模拟商品包含标题、价格、封面图、描述关于管理员密码不要直接在SQL脚本里写一个假的哈希值。正确做法是先把后端跑起来通过注册接口注册一个账号再把数据库里这条记录的role改成1最后把改好的数据导出到脚本里。这样能确保密码哈希是真实可用的不会出现在登录时怎么都登不进去的窘境。另外要注意SQL脚本的执行顺序先建库再建表再插入初始化数据。字符集统一用utf8mb4因为用户昵称或者商品描述里可能出现生僻字或特殊符号。4. 后端核心实现从登录鉴权到订单状态流转4.1 项目分层结构与包规划后端工程拿到手第一步不是写业务而是把包结构定好。清晰的包结构能帮你快速定位代码也让导师一眼看出你的工程素养。我推荐的包结构是controller接收前端请求调用service层返回统一结果service业务逻辑比如下单时要校验商品状态、更新浏览量mapper数据访问层继承MyBatis-Plus的BaseMapperentity实体类对应数据库表config配置类比如跨域配置、MyBatis-Plus分页插件配置common工具类和统一返回结果比如Result类、JwtUtil包结构定了之后再定统一返回结果。所有接口都返回同一个结构前端解析起来非常方便。一个简单的Result类大概是public class Result { private Integer code; // 200成功其他失败 private String message; private Object data; public static Result success(Object data) { return new Result(200, 操作成功, data); } public static Result error(String message) { return new Result(500, message, null); } }前后端所有接口都围绕这个格式通信异常处理也走这个格式调试时一看响应就能判断问题出在哪。4.2 JWT登录鉴权与拦截器用户登录之后系统怎么知道后续的请求是谁发起的最直接的做法是用JWT令牌。流程是这样的用户输入用户名密码后端校验通过后用JWT工具生成一个tokentoken里包含用户ID和角色信息返回给前端。前端把token存在localStorage里之后每次请求在请求头里带上Authorization: Bearer xxx。后端通过拦截器校验token解析出当前用户ID放到请求上下文里。登录Controller的代码大体是PostMapping(/api/auth/login) public Result login(RequestBody LoginDTO dto) { User user userService.findByUsername(dto.getUsername()); if (user null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } if (user.getStatus() 1) { return Result.error(账号已被禁用); } String token JwtUtil.generateToken(user.getId(), user.getRole()); return Result.success(token); }JWT拦截器的作用是保护那些需要登录才能访问的接口。比如发布商品、下单、查看个人订单这些接口必须校验token。可以直接继承Spring MVC的HandlerInterceptorComponent public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { Claims claims JwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.get(userId)); return true; } response.setStatus(401); return false; } }然后在配置类里把它注册到拦截器链上同时排除登录、注册、商品浏览这些公开接口。这里有几个容易被忽略的细节拦截器排除路径要仔细设计比如/api/products/**里包含商品详情和商品列表但发布商品接口是POST /api/products就不能简单把所有products路径全放行需要根据HTTP方法区分或者在Controller里再做一层登录判断。很多新手在这里只排除不拦截结果发布商品接口也被公开演示时被老师一追问就露馅。4.3 商品发布与图片上传商品发布页要传标题、分类、价格、描述、图片其中图片上传是独立接口。前端先调用上传接口拿回图片地址再把地址和其他信息一起提交到发布接口。上传接口的实现不难但有两个坑要提前处理。第一个是文件保存路径Spring Boot工程打成jar之后如果在打包目录下写upload文件夹每次重新部署文件可能丢失。建议用一个固定的外部目录比如/data/upload然后把上传路径写到application.yml里如果不想改代码上传接口里用System.getProperty(user.dir) /upload也够用不过要记得后续部署时把upload目录一并拷贝。第二个坑是静态资源映射。图片上传成功之后前端需要通过URL访问它比如返回/upload/abc.jpg后端必须配置资源映射否则访问404Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); }上传文件大小的限制也很容易踩。Spring Boot默认单文件最大1MB上传一张手机照片就直接报错。需要修改配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB4.4 订单流程用状态机管理交易订单是交易系统的核心整个订单模块写得好不好答辩时一目了然。我建议把订单状态设计成常量类或枚举而不是到处写魔法数字。可以用数字加常量类的方式简化public class OrderStatus { public static final int WAIT_PAY 0; // 待付款 public static final int WAIT_SHIP 1; // 待发货 public static final int WAIT_RECEIVE 2; // 待收货 public static final int FINISHED 3; // 已完成 public static final int CANCELLED 4; // 已取消 }订单状态流转的规则是买家发布下单请求后端校验商品存在且状态为上架创建订单状态为待付款买家模拟付款毕业设计通常没有真实支付直接调一个付款接口把状态改为待发货卖家发货把状态改为待收货买家确认收货状态改为已完成个别情况下买家或卖家取消订单状态改为已取消一个典型的下单Service方法是Transactional public Order createOrder(Long productId, Long buyerId) { Product product productMapper.selectById(productId); if (product null || product.getStatus() ! 0) { throw new RuntimeException(商品不可购买); } if (product.getSellerId().equals(buyerId)) { throw new RuntimeException(不能购买自己发布的商品); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setProductId(productId); order.setBuyerId(buyerId); order.setSellerId(product.getSellerId()); order.setTotalPrice(product.getPrice()); order.setStatus(OrderStatus.WAIT_PAY); orderMapper.insert(order); // 商品状态改为已售出 product.setStatus(2); productMapper.updateById(product); return order; }这段代码里两个细节很加分一是买家不能买自己发布的商品二是用Transactional保证订单创建和商品状态修改要么都成功要么都回滚。这些都是在答辩时能讲出来的业务思考。4.5 MyBatis-Plus配置与分页MyBatis-Plus让数据访问层简单了很多实体类通过注解映射到表Mapper接口继承BaseMapper增删改查方法就齐了。在没有MyBatis-Plus的年代要手写大量的XML映射文件花的时间都比业务逻辑多。分页是所有列表页的刚需首页显示商品分页、后台管理分页都靠它。MyBatis-Plus的分页插件需要手动配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }List查询也可以用条件构造器LambdaQueryWrapper实现比如前端传个关键字和分类ID过来LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Product::getTitle, keyword); } if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } wrapper.orderByDesc(Product::getCreateTime); IPageProduct page productMapper.selectPage(new Page(current, size), wrapper);条件构造器最大的好处是不用手写动态SQL代码可读性高key通常对应数据库的列名靠MyBatis-Plus的驼峰映射自动转换。如果数据库字段是下划线风格实体类是驼峰风格默认映射就能处理不需要额外配置。5. 前端核心实现页面组织与接口对接5.1 前端工程结构与页面清单前端的价值是把后端接口变成用户能操作的页面。工程结构建议按功能拆目录src/ api/ # 所有接口请求方法 assets/ # 静态资源 components/ # 公共组件 router/ # 路由配置 store/ # 状态管理用户信息 views/ # 页面组件对应的页面与功能如下路由路径页面功能/首页商品列表、搜索、分类筛选/login登录页用户登录/register注册页用户注册/product/:id商品详情查看详情、立即购买/publish发布商品发布表单、图片上传/user/orders我的订单查看买家、卖家订单执行状态操作/user/products我的发布管理我发布的商品/admin后台管理用户管理、商品管理、类目管理每个页面对应一个view文件公共部分如顶栏导航、登录状态才能操作的后台入口放到components里。5.2 Axios封装前端请求的统一出口如果每个页面都直接调axios代码会非常重复而且token注入、错误提示这些逻辑散得到处都是。建议在src/api/request.js里统一封装axios实例import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, // 开发环境走代理生产环境也走nginx代理 timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use(response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.response?.message || 服务器异常) return Promise.reject(error) }) export default request封装之后每个接口只需要定义成函数export function getProductList(params) { return request.get(/products, { params }) } export function publishProduct(data) { return request.post(/products, data) } export function createOrder(data) { return request.post(/orders, data) }这样做的最大好处是如果后端接口地址变了只需要改api目录下的一个文件以后换后端、加权限也只需要改拦截器。这是值得写进文档里的工程化细节。5.3 关键页面的实现要点首页是系统的门面需要同时处理商品展示、分类筛选、分页搜索。我的建议是页面加载时请求分类接口和第一页商品数据用户点击分类或搜索时带着参数重新请求商品列表。用Vue响应式数据控制一个query对象结构清晰const query reactive({ categoryId: null, keyword: , page: 1, size: 12 })商品详情页要注意一个细节进入详情页时最好调用一次浏览量自增接口。这个功能虽小但体现了“数据都在流动”的思想。商品详情页还要区分情况展示操作按钮如果是自己发布的商品显示下架和编辑如果是别人的商品显示“立即购买”。对自己发布商品也显示购买按钮会让业务逻辑很搞笑这也是0.5分和1分的差距所在。发布商品页的核心是图片上传组件。Element UI或Element Plus的上传组件upload配置好action地址为/api/upload拿到返回的url后回显到表单中。要注意的是上传接口需要登录必须在标签上加请求头或者在上传前确保请求拦截器注入了token。我的订单页是最能体现前端交互细节的地方。页面用几个Tab分隔订单状态全部、待付款、待发货、待收货、已完成、已取消。根据订单状态显示不同的操作按钮用v-if控制不复杂但要严谨待付款显示“去付款”“取消订单”待发货如果是卖家显示“发货”如果是买家只显示“等待发货”待收货显示“确认收货”5.4 路由守卫没有token就别进后台路由守卫解决的核心问题是用户没登录能不能访问那些需要登录的页面。答案是不能。Vue Router提供了beforeEach钩子来做统一拦截router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.path /admin localStorage.getItem(role) ! 1) { next(/) } else { next() } })这里的判断逻辑尽量简单不要在前端做太复杂的权限控制因为真正的安全校验永远在后端。前端的路由守卫只是用户体验层没登录就引导去登录页不是管理员就看不到后台菜单。敏感操作比如下架商品、冻结用户后端接口里必须再次校验角色前端只管页面展示。6. 联调、部署与常见问题排查实录6.1 跨域问题的两种解法前后端分离开发时前端跑在开发服务器上后端跑在8080两个端口不同浏览器跨域拦截就会让你抓狂。解决办法有两个我建议开发阶段用代理、生产阶段用反向代理。开发阶段Vue CLI项目在根目录创建vue.config.jsVite项目在vite.config.js配置代理devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端代码里请求还是写/api/products浏览器看到的是同源请求但实际被转发到了后端。好处是开发环境不污染后端代码改动立即可生效。生产阶段部署到Nginx后端与前端静态文件都挂在同一个Nginx下也通过/api路径反向代理到后端。这个我在下一小节一起讲。还有另一种做法是Spring Boot后端加CorsFilter允许跨域。不是不能用但我在实践中发现放开跨域后浏览器调试容易混淆权限错误和跨域错误反而不如用代理干净。6.2 打包部署流程部署并不是毕业设计的必选项但如果你能现场演示一个跑在服务器上的系统印象分会好很多。部署步骤其实不复杂后端打包在工程根目录执行mvn clean package会在target目录下生成一个jar包然后在服务器上执行java -jar second-hand-app.jar后端默认端口是8080如果被占用就换成别的端口同时保证MySQL已启动数据库脚本已执行。前端构建npm run build会在dist目录生成静态文件把dist里的内容放到Nginx的html目录下Nginx配置加上反向代理规则server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }最后一行的try_files非常关键。Vue Router使用history模式后直接刷新某个子路由时Nginx找不到对应的物理文件会返回404这一行把所有路径都回退到index.html由前端路由接管。部署完成后打开浏览器直接访问域名就能看到完整页面。6.3 新手最常见的5个问题与处理我整理了一份高频问题排查表都是实际遇到过的典型坑做这个系统时只要出现类似报错先对照这张表排查现象常见原因处理办法前端请求时报CORS错误跨域未处理开发环境加代理生产环境加Nginx反向代理启动后端报数据库连接失败MySQL连接URL缺少时区、驱动不匹配、MySQL没启动检查application.yml里的url加上serverTimezoneAsia/Shanghai和useSSLfalse确认驱动是mysql-connector-j图片上传364错误上传文件超过默认1MB限制在application.yml里配置multipart文件大小前端刷新后404路由是history模式静态服务器没有回退Nginx的try_files配置到index.htmlnpm install卡住或报错Node版本与依赖不匹配、网络问题用固定的Node版本npm install失败时删除node_modules再重装或者换国内镜像源6.4 折腾半天发现是版本问题的经典案例这里分享一个我印象很深的排查经历。有同学从前端页面发请求到后端后端返回200但前端总是报“Cannot read properties of undefined”一看就是接口返回的数据格式跟前端预期不一致。后端返回的是{code: 200, data: {...}}但前端axios封装里多了一层包装相当于数据被包了一层。最终定位下来是响应拦截器里在没有校验code字段就直接返回了response.data.data而后端有个接口返回结构不符合约定导致前端取不到值。解决方法是统一所有后端接口的返回值格式。只要有一个Controller直接返回了Map或者String前端不兼容后面查起来非常浪费时间。所以我会反复强调统一返回Result的重要性这不是代码洁癖是给联调省时间。另一个版本坑是Spring Boot 2.x用javax.servletSpring Boot 3.x用jakarta.servlet网上教程里大量import javax.servlet.*代码在Spring Boot 3下编译不过。如果你跟着教程写最好一开始就把版本定成2.7.x否则改import就够你改半天。7. 从毕设角度复盘演示、答辩和文档组织7.1 演示时最容易被追问的几个点演示时千万不要只点几个页面说“能用”而是要提前准备好几个容易被追问的细节第一为什么订单表里同时存买家和卖家的ID答订单生成后商品字段可能变化直接把ID冗余到订单里查询订单列表时不需要反复JOIN商品表性能更好数据也更稳定。第二买家下单时万一商品已经被别人买了怎么办答下单方法里用了数据库事务和商品状态判断先查询商品状态确认是上架状态才允许创建订单创建成功后立刻把商品状态改为已售出。并发极端情况下可以通过给商品表增加版本号字段做乐观锁这是一个可以现场讲出来的优化方案。第三密码为什么不是明文答使用BCrypt哈希这是一种不可逆的加密算法用户登录时把输入的密码哈希后与数据库比对。这个问题的潜台词是检查你有没有基本的安全意识。第四如果别人绕过前端直接调用接口怎么办答后端接口有JWT拦截器保护没有有效token会返回401管理员接口还会校验角色字段。前端只是用户体验层真正的安全控制在后端。把这些问题想明白答辩时就不会一问就慌。不会回答的问题不要现场硬编坦诚说“这个目前没有考虑后续可以如何改进”反而比瞎编加分。7.2 文档怎么组织更稳妥文档不是流水账它的价值是向读者证明“你确实设计并实现了一个系统”。结构建议按下面这个顺序来核心是图、表、代码说明三件套绪论写背景和意义介绍二手交易的市场需求需求分析画用例图、功能需求表、非功能性需求性能、安全性系统设计给出系统架构图、技术选型、数据库ER图、核心表结构说明系统实现每个模块放2到3张页面截图加上关键代码的核心逻辑注释说明系统测试列出测试用例表包括登录、商品发布、下单、订单状态流转等场景总结与展望总结完成的工作提出可以优化的方向支付接入、消息通知、推荐系统数据库脚本和代码要能对应上文档里的描述。比如文档里写“订单状态有四种”代码里就必须是四种文档里写“用户可以取消订单”前端就必须有取消按钮并调用后端接口。很多同学翻车就翻在文档和代码不一致老师只要照着文档随手验证一个功能就发现了。宁可文档写得短一点也要保证每一句话都能在代码里找到依据。关于三件套源码、数据库、文档的关系我的建议是把数据库脚本理解成源码的一部分因为系统必须有数据才能跑文档是前两者的“使用说明书”。上传交付时可以按以下目录组织项目根目录/ 后端源码/ 前端源码/ 数据库/ init.sql 论文文档/ 设计说明书.docx 部署说明.md部署说明里写清环境要求、启动步骤和默认账号。别小看这个文件它能省下你自己复现环境的大量时间也是专业素养的体现。最后再分享一个小技巧不要在答辩前一晚才第一次完整部署系统。我见过太多在答辩现场打开浏览器——页面空白、数据库连接超时、图片全挂的情况。提前三天把系统从零搭在一个干净的机器上跑通完整流程所有截图单独放一份备份。系统本身可能只占项目一半的分稳定演示和清晰讲解能补齐剩下的一半。做这套系统的过程最值钱的部分不是代码量而是从“设计表结构”到“状态流转”这条完整链路被你亲自走了一遍。它让你真正体会到开发不是堆功能是梳理清楚每一个数据变化的前因后果。把订单状态画清楚、把权限边界想明白这套系统无论用在毕设还是未来更复杂的项目里都能给你兜底。