
1. 项目概述与需求拆解1.1 这个项目到底在解决什么问题海洋馆、水族馆这类场所表面看是旅游观光业态实际上它的收入结构非常复杂。除了门票还有文创商品、纪念品、动物主题周边、饮料零食、互动体验项目比如喂食、潜水合影等多种销售渠道。很多小型海洋馆至今还在用Excel表格或者纸面单据管理商品入库、销售、盘点节假日一忙起来库存数据经常对不上财务核算的时候更是头疼。这里要说的“海洋馆商品销售与经营管理系统”本质上就是一套围绕商品进销存、销售订单、会员管理、经营统计的业务管理系统。系统面向的典型角色有三类收银员/导购员负责日常开单和商品查询仓库管理员负责入库、出库、盘点馆长或运营经理查看销售报表和经营数据。技术选型上采用SpringBoot做后端服务、Vue做前端页面是Java Web方向非常经典的组合也特别适合作为计算机专业的毕业设计选题。这个系统能解决的实际问题包括商品信息统一管理、销售订单电子化、库存动态扣减、会员充值消费一体化、多维度销售统计。对毕设场景来说它的业务复杂度适中——不会简单到没有技术含量又不会复杂到一个人做不完是一个很合适的综合训练项目。1.2 功能模块的边界如何划定做毕设最容易犯的错就是一上来想把所有功能都塞进去。好好的一个销售管理系统非要加个论坛、加个App端、加个在线支付结果页面一堆代码一堆凑合起来全是Bug。这个项目在功能边界上控制得很好模块划分很清晰商品管理商品的分类、名称、图片、价格、库存上下限、上下架状态库存管理入库单、出库单、库存盘点、库存预警销售管理前台开单、订单列表、订单详情、退货退款会员管理会员档案、充值、消费记录、积分经营统计日/周/月销售报表、商品销量排行、会员消费排行系统管理登录认证、角色权限、操作日志每个模块都是“必需且闭环”的。比如库存模块入库单审核通过后商品可用库存增加销售订单生成后扣减库存退货后库存回补——整个链路是打通的而不是做一个孤立的增删改查页面这一点在答辩时非常加分。1.3 技术栈选型背后的逻辑SpringBoot Vue这套组合说实话不算新潮但胜在稳定和完善。后端选择SpringBoot核心原因是它把Spring家族的复杂配置大幅度简化了。以前用SSHSpring Struts Hibernate或者SSMSpring SpringMVC MyBatis搭一个项目要写一大堆XML配置文件而SpringBoot用自动配置和Starter依赖机制几行配置就能把Web环境跑起来。更关键的是它内置Tomcat不需要单独部署外部容器这对毕设环境来说太友好了——机房电脑、旧电脑都能跑。前端选择Vue原因也很实际。Vue的学习曲线比React平缓模板语法直观而且配套的Element UI组件库拿来即用表格、表单、弹窗、菜单这些后台管理界面的常用组件全都齐全。用Vue写后台管理系统的开发效率几乎是用原生JavaScript写页面的三到五倍。数据存储方面这个项目用的是MySQL配合MyBatis Plus做持久层操作。MySQL是开源数据库里最常见的选择线上资料多遇到问题好查MyBatis Plus在原生MyBatis的基础上提供了通用的CRUD接口和条件构造器开发全程基本不用手写简单的XML映射省下大量时间。为了降低开发成本系统还引入了Spring Security JWT做认证鉴权、Apache POI做导出、ECharts做图表展示。这些工具的选型都是“行业主流、资料丰富、效果好”的组合不是为了炫技而是让项目整体技术栈看起来完整又不容易踩坑。2. 系统核心架构设计2.1 前后端分离架构与目录组织项目整体采用前后端分离架构。前端是一个独立的Vue工程部署时使用的Node服务只在开发期启用最终构建成静态资源文件后端是一个独立的SpringBoot工程打包成Jar包运行。两边通过RESTful风格的JSON接口通信。这种架构最大的好处是职责清晰。前端负责页面渲染、表单校验、交互状态后端只关心业务逻辑和数据持久化。写接口的时候也不容易互相干扰——你改前端的按钮位置不会影响后端服务后端调整数据库字段也不会让页面结构崩掉。典型的目录组织方式是这样的sea-world-admin/ # 前端工程 ├── src/ │ ├── api/ # 所有接口请求模块 │ ├── assets/ # 静态资源 │ ├── components/ # 通用组件 │ ├── router/ # 路由配置 │ ├── store/ # Vuex状态管理 │ ├── views/ # 页面视图 │ ├── utils/ # 工具函数axios封装等 │ ├── App.vue │ └── main.js # 入口文件 └── package.json sea-world-server/ # 后端工程 ├── src/main/java/com/xxx/ │ ├── common/ # 通用工具、常量、异常处理 │ ├── config/ # 配置类安全配置、跨域配置等 │ ├── controller/ # 接口层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis持久层接口 │ ├── entity/ # 数据库实体类 │ ├── vo/ # 前端交互视图对象 │ └── SeaWorldApplication.java ├── src/main/resources/ │ ├── mapper/ # MyBatis XML文件 │ ├── application.yml │ └── sql/ # 数据库初始化脚本 └── pom.xml至少对毕设来说这种分层是“标准答案”。controller只做参数接收和结果返回service里写业务规则mapper里写数据库交互。项目越大这种分层的好处越明显就算项目不大答辩的时候被问到“项目结构怎么设计”也答得出来。2.2 数据库建模的思路与核心表结构数据库设计是很多毕设同学的痛点因为代码可以抄、页面可以抄唯独表结构需要自己动脑设计。这套系统在建模上采用了经典的范式设计核心表大概有八张左右很少但每张都不多余。以商品表为例主要字段包括CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(100) NOT NULL COMMENT 商品名称, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, price decimal(10,2) NOT NULL COMMENT 销售单价, cost_price decimal(10,2) DEFAULT NULL COMMENT 成本价, stock int(11) NOT NULL DEFAULT 0 COMMENT 当前库存, stock_warn int(11) DEFAULT 10 COMMENT 库存预警值, image varchar(255) DEFAULT NULL COMMENT 商品图片URL, status tinyint(4) DEFAULT 1 COMMENT 状态1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;用得上的关键设计细节有几点第一是价格用decimal(10,2)而不是float或double避免浮点精度问题第二是每个表都保留create_time和update_time审计和排查问题的时候有迹可循第三是逻辑状态字段如status代替物理删除方便恢复数据和保留历史记录。订单表需要特别说明因为它的结构直接决定了整个销售模块的写法。订单通常分为主表和明细表两张sale_order订单主表包含订单号、会员ID、总金额、实付金额、下单时间、订单状态等sale_order_item订单明细表关联订单ID、商品ID、购买数量、商品快照价格订单明细表必须存“商品快照”——就是下单那一刻的商品名称和价格要复制到明细表里。这样即使之后商品改名、调价甚至删除历史订单依然能正确显示当时买的是什么、多少钱。这是很多新手设计时容易忽略的关键点。商品名称和价格可以变历史订单不能变。2.3 接口设计的原则与统一响应格式前后端联调最怕各写各的前端期望返回{code: 0, data: {...} , message: success}后端返回一堆乱七八糟的字段联调的时候到处出Bug。这个项目在接口设计上很早就定下了统一规范。统一响应结构{ code: 200, message: 操作成功, data: { token: xxxxx, userInfo: {} } }code为200代表成功非200代表业务失败或者系统异常。data部分可能是对象、列表也可能是分页数据。前端axios拦截器统一处理只要code不是200就弹出错误提示页面代码里几乎不需要反复写try-catch和错误判断。分页接口采用当下主流的pageNum/pageSize约定返回分页数据结构为{ total: 100, list: [...] }。这样点开任何一列数据列表页的写法都是一模一样的不会出现某个页面多一个字段另一处少一个字段的情况。接口URL的命名也统一遵循资源名复数风格比如/api/products、/api/orders、/api/members。GET查询、POST新增/提交、PUT修改、DELETE删除语义统一前端调用后端一眼能看懂答辩时介绍接口设计也方便。3. 后端核心实现SpringBoot实战拆解3.1 依赖引入和基础配置用IDEA新建SpringBoot项目时直接勾选Web、MySQL Driver、MyBatis Plus等依赖也可以手动在pom.xml中引入。实际开发用到的核心依赖大概是这些dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependencyapplication.yml里重点关注的是数据库连接和MyBatis Plus配置spring: datasource: url: jdbc:mysql://localhost:3306/sea_world?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl数据库连接参数里有两个细节比较容易忽略一是serverTimezone必须设成Asia/Shanghai否则Java连接MySQL 8.x会报时间不同步的错二是characterEncodingutf8建议直接改成utf8mb4因为utf8存不了emoji表情和其他生僻字符而商品名称和备注里经常出现这类字符。套用“utf8mb4是真的稳utf8坑过无数人”这句话一点不夸张。3.2 登录认证与权限拦截管理系统的登录逻辑说起来很简单就是“用户输入账号密码后端验证通过后返回一个凭证前端下次请求带上这个凭证”。但实现上有不少门道。这个项目使用的是经典的JWT方案。用户提交账号密码后后端校验账号是否存在、密码哈希是否正确。密码存储采用BCrypt加密而不是明文这是安全底线。登录成功后生成一个JWT令牌令牌里封装了用户ID、用户名和角色信息设置过期时间一般两个小时返回给前端。前端把token存在localStorage里之后每次请求在请求头加上Authorization: Bearer {token}。后端做一个拦截器或者过滤器统一拦截需要认证的请求解析token从Redis或数据库加载用户信息判断是否有操作权限。这里我强烈建议的做法是写一个自定义注解RequirePermission(product:add)标注在controller方法上由AOP切面统一做权限校验。这样可以避免每个接口里手动写权限判断代码后续增加新权限也只需要在数据库权限表里配置代码层面完全不用改动。关于JWT有一个实际开发中容易踩的坑JWT是无状态的签发之后在过期之前无法主动作废。所以用户修改密码、被强制下线、账号禁用这类场景仅靠JWT是无法立即生效的。解法是在用户表里加一个token版本号字段如security_tokenJWT里存这个版本号校验时比对数据库中的值不一致就拒绝。代码量很小却能让整个系统在权限控制上的表现更像真实生产系统。3.3 商品管理接口的实现套路商品模块是最标准的增删改查但依然有不少值得展开的细节。商品列表接口最常做的是支持关键词搜索、分类筛选、状态筛选和分页。用MyBatis Plus的LambdaQueryWrapper写条件查询非常简练public IPageProduct pageProducts(ProductQuery query) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getName()), Product::getName, query.getName()) .eq(query.getCategoryId() ! null, Product::getCategoryId, query.getCategoryId()) .eq(query.getStatus() ! null, Product::getStatus, query.getStatus()) .orderByDesc(Product::getCreateTime); return productMapper.selectPage(new Page(query.getPageNum(), query.getPageSize()), wrapper); }$wrapper里用condition参数控制是否拼接条件比如query.getName()为空就不拼这个条件极其方便。代码里没有一行if判断拼接SQL完成后端接口的速度能快不少。新增商品的逻辑要注意两点一是商品编号建议由系统自动生成比如日期随机数业务上更好管理二是图片上传要单独处理前端把图片传到后端的文件上传接口返回一个图片URL再把URL保存到product表的image字段。文件上传接口一般把文件存在本地磁盘或云存储本地场景就放在项目uploads目录访问时通过一个映射路径暴露出去。删除商品这边需要小心。最简单粗暴的是直接DELETE但非常不推荐。正确做法是在service里判断该商品是否有关联订单如果有就不能物理删除——历史订单需要保留商品快照信息。稳妥方案是逻辑删除把status设为0下架或者在表里加一个deleted字段查询时自动过滤。MyBatis Plus的TableLogic注解可以直接实现逻辑删除省心省力。3.4 销售订单流程与事务控制订单模块是整个系统的核心也是最容易出Bug的地方。前端页面“开单”的流程是这样的收银员选择会员可选把商品一个个加入购物车修改数量前端实时计算总金额点击“提交订单”后一次性把所有商品明细发给后端。后端接收一个订单主表对象商品明细列表在一个事务内完成三件事生成订单主记录状态为“已支付”场景不涉及在线支付简化为直接完成或“待结算”遍历明细列表保存每条商品到订单明细表扣减每个商品的库存如果某个商品库存不足则抛出异常整体回滚这段逻辑必须加事务注解Transactional(rollbackFor Exception.class) public SaleOrder createOrder(OrderCreateDTO dto) { // 保存订单主表 // 保存订单明细 // 扣减库存校验库存是否充足 }为什么必须加事务因为如果第2步保存了明细第3步扣库存时才发现A商品库存不足没有事务的话订单主表已经写入、明细也已经写入数据就脏了。事务保证这三步要么全部成功、要么全部回滚数据库永远保持一致。库存扣减的并发问题是进阶内容。如果两个用户同时购买最后一个库存的商品用“先查库存是否够再更新库存”这种检查后操作的方式在高并发下会出现超卖。比如库存只剩1个两个人同时查到库存是1都认为可以买然后各自扣减库存就变成负数了。这不是理论问题是实际会发生的问题。最简单的解法是把扣减语句写成一条原子SQLUPDATE product SET stock stock - #{num} WHERE id #{productId} AND stock #{num}返回影响行数为1表示扣减成功为0表示库存不足。配合事务回滚从根上杜绝超卖。这个点如果在答辩时主动讲出来面试官和评委通常都会觉得你是真正理解业务系统的而不是只会写CRUD。4. 前端核心实现Vue工程实战拆解4.1 Vue工程结构与路由设计前端的工程结构围绕“功能模块公共资源”来组织。src/views下面按一级目录划分功能区域login、layout、dashboard、product、category、stock、sale、member、report、system每个目录下放该模块对应的页面组件。这是一种直观的按业务划分的目录组织法适合中小型项目方便定位代码。路由设计采用懒加载模式{ path: /product, name: Product, component: () import(/views/product/index.vue), meta: { title: 商品管理, icon: goods } }页面组件都通过动态import的方式引入这样首屏只加载必要的代码整体性能会好一些。虽然对后台系统的体量来说提升有限但习惯养成了接到更大的项目也不会踩性能坑。嵌套路由一般这样组织Layout作为最外层框架里面根据菜单配置动态渲染各个子页面{ path: /, component: Layout, redirect: /dashboard, children: [ { path: dashboard, component: () import(/views/dashboard/index.vue) } ] }菜单和路由是一一映射的左侧菜单展开折叠、高亮当前项都是Element UI的el-menu组件根据当前路由自动实现的。4.2 Axios封装与API管理前后端联调最烦的就是到处写this.$http.get(...)然后到处处理错误。这个项目的做法是把Axios做一层统一封装。封装层面从三个维度入手第一请求拦截。每次请求前从localStorage取出token放到请求头Authorization字段。没有token的页面比如纯公开接口可以跳过这一步。service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config })第二响应拦截。业务上所有接口都返回code/message/data的结构拦截器统一处理code。不是200就利用Element UI的Message组件弹出错误提示同时把错误继续抛出去页面代码可以做后续处理如果是401的code登录失效跳转到登录页并清除本地登录状态。第三统一API管理。把请求封装成一个一个函数放在src/api目录例如// src/api/product.js export function getProductPage(data) { return request({ url: /api/products/page, method: post, data }) } export function addProduct(data) { return request({ url: /api/products, method: post, data }) }页面里只调这些函数不直接拼URL。好处是后端接口改了URL只需要在api文件里改一处所有页面不用动。4.3 核心页面的交互实现页面的实现没有什么神秘的地方核心是拿Element UI的组件拼装。商品列表页是标准的“搜索表单 表格 分页 操作按钮”布局。搜索区放商品名称输入框、分类下拉选择、状态下拉选择表格用el-table展示商品数据价格列做格式化分页用el-pagination绑定pageNum/pageSize和total。这里一个需要留意的点是Element UI的表格在数据量大的时候性能一般但商品数据量撑死几千条完全够用不需要过度设计。订单页面稍微复杂一点因为订单主表和明细是两级数据。点开订单详情时弹窗里要展示订单基本信息加一个嵌套的明细表。在Vue里写嵌套表格很简单外层el-table的每一行用expand列展开一行区域区域内放一个只读的el-table展示明细。看起来功能很强实际上代码量很少。开单页的设计更讲究交互流畅性。页面分成左侧的商品列表区和右侧的已选商品区。左侧支持“点击商品加入购物车”右侧没有用传统的table而是用一个简单的列表清单位置展示每行有商品名、单价、数量加减按钮、小计底部显示总价。数量减少到0时自动移除该行。这种布局来源于实际运营中收银台的体验——点错可以马上减掉比一边选商品一边看长表格方便得多。开单页这种交互其实是在给用户“免返工”的体验。线下实体店前台特别忙选错了商品如果还要开一张新单体验会很差。前后端分离开单接口配合减库存逻辑返工成本能降低不少。5. 经营统计让数据替店铺说话5.1 销售报表的SQL与聚合技巧做经营管理系统菜单里功能再多再丰富最终运营方最关心的一定是经营数据。统计模块在这个系统里属于画龙点睛的存在代码量不大但非常能体现设计水平。销售统计最常见的是按天分组看销售额。MySQL里按日期分组非常简单SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS order_count, SUM(total_amount) AS total_sales FROM sale_order WHERE order_status ! CANCELED AND create_time #{startDate} AND create_time DATE_ADD(#{endDate}, INTERVAL 1 DAY) GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY day;商品销量排行就是按商品分组聚合销售数量SELECT p.name AS product_name, SUM(oi.quantity) AS total_quantity, SUM(oi.quantity * oi.price) AS total_amount FROM sale_order_item oi LEFT JOIN product p ON oi.product_id p.id WHERE oi.create_time BETWEEN #{startTime} AND #{endTime} GROUP BY oi.product_id, p.name ORDER BY total_quantity DESC LIMIT 10;统计口径上有一个容易忽略的坑订单状态。统计销售额时一定要排除掉“已取消”和“已退货”的订单否则数字会虚高。另外实体店常有“挂单”先打单、后付款的情况统计时是算挂单时间还是算支付时间前后端都要约定清楚切不可一会儿取下单时间一会儿取支付时间。5.2 图表展示的数据对接图表选的是ECharts轻量、灵活、配置丰富。前端只需要一个折线图展示每日销售额变化一个柱状图展示商品销量TOP10一个饼图展示分类销售占比。三个图表放在dashboard首页一眼就能看全整个店的经营走势。ECharts和Vue的集成有两个办法一个是直接在组件里init一个echarts实例watch数据变化后调用setOption更新图表另一个是封装一个BaseChart组件接收配置对象统一管理实例的创建和销毁。更推荐后者因为多个页面复用图表时不用反复写初始化代码。图表数据从后端统计接口拿返回结构就是一个简单的数组比如[{ day: 2024-05-01, total: 3200 }, ...]前端把数组拆成x轴数据和series数据。这里注意后端接口返回值里不要塞一堆时间字符串的格式化逻辑直接返回原始数据前端做格式化更灵活。有些毕设同学会在这个统计模块里加一个“导出报表”功能这个功能建议用EasyExcel实现。整体逻辑不难组装一个List导出Model调用ExcelWriter写出即可。导出的品种建议做成“今日销售明细”“月度汇总”两种足够展示能力又不会占用太多开发时间。6. 常见问题与排查技巧实录6.1 前后端联调的跨域问题前后端分离开发中跨域是最常见的问题没有之一。现象一般是前端请求接口时浏览器Console报错提示CORS policy或者请求发出去了但返回的数据为空。实际上跨域拦截的是浏览器SpringBoot后端做一次配置就能解决Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }配置里有一个细节allowedOrigins(*)在allowCredentials(true)的情况下是非法组合会报错。要用allowedOriginPatterns(*)或者明确指定前端地址。不少同学在这里卡半天就是没搞清这两个属性的区别。等到项目最后部署上线前后端放同一个域名和端口下跨域问题自然就没有了。但如果只是本地调试用Vite开发服务器配置proxy代理转发请求效果更好// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端的请求走Vite代理转发浏览器看到的是同源请求既不触发跨域生产部署前也不需要改前端代码。6.2 登录之后请求全部401某个接口在登录前能访问登录后反而报401这类问题排查思路基本是以下三步第一步确认前端是否把token带上了。打开浏览器开发者工具的Network面板查看请求头有没有Authorization: Bearer xxxxx字段。没有的话说明axios拦截器里取值有问题检查token的存储key是否跟前端写入时一致。第二步确认后端token校验逻辑是否正常。很多项目配置了Spring Security的过滤链但是过滤器的顺序配错了导致token校验过滤器没有拦到目标接口。检查SecurityConfig里的addFilterBefore是否正确注册。第三步确认JWT解析是否失败。如果后端使用了jjwt 0.9.1这个版本它依赖javax.xml.bindJDK 8以上需要额外引入jaxb-api依赖否则任何解析操作都会抛ClassNotFoundException表现就是登录后所有请求都401。这里分享一个调试技巧在token校验过滤器里加一行日志把解析成功、失败、异常三条路径分别打出来看到底走了哪条分支。比断点调试少走一半弯路。6.3 库存扣减出现负数或者超卖库存变成负数要么是前面说的并发问题没解决要么是代码逻辑顺序写了“先扣库存再判断是否足够”——这是逻辑错误先判断后扣减才正确。排查时按顺序查先看数据库当前库存值再看订单创建代码扣库存SQL是不是原子语句有没有AND stock #{num}再确认service方法上有没有Transactional注解。三个环节任何一个掉链子都可能出问题。修复方案在3.4节已经写过了用条件更新SQL原子扣减。实际开发中几乎无脑采用这个方案简单又可靠。如果你又想要“库存不足时给用户明确提示”可以先查一次库存用于提示再用条件更新做最终校验两条SQL共存也没有问题。6.4 项目启动失败的常见原因SpringBoot项目启动不了的排查顺序非常固定。第一看控制台报错。如果是端口被占用错误提示是Port 8080 was already in use。解决方式二选一找到占用进程杀掉或者在application.yml改端口。不建议直接改默认端口后面所有的前端代理、接口文档、部署脚本都得跟着改。第二看数据库连接报错。如果提示Access denied for user检查账号密码如果提示Unknown database检查数据库名是否建好如果提示Public Key Retrieval is not allowed在连接参数里加allowPublicKeyRetrievaltrue。记住这几个常见配置基本能应对九成的数据库连接问题。第三看依赖冲突。把IDEA右侧Maven面板打开点刷新看依赖树里有没有版本冲突特别注意java-jwt和jjwt同时引入、或者mybatis-plus版本和springboot版本不兼容这类问题。最简单的办法是统一用一个“版本体检”的工具或沿用项目原有的依赖版本不要自己随便升级。6.5 线上部署时静态资源无法访问项目最后想要部署上线给导师演示或者给别人看效果经常出现前端能打开但图片全部404的问题。排查路径先确认图片上传后保存到了哪个目录本地磁盘的uploads目录再确认访问图片的URL路径是否映射到了那个目录。SpringBoot里加一个静态资源映射配置就可以解决Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceLocations(file: System.getProperty(user.dir) /uploads/); } }这个配置的意思是把/uploads/**开头的请求指向项目运行目录下的uploads文件夹。注意这个路径是相对当前运行目录的部署时如果换了目录需要相应调整。如果你把jar包丢到服务器上运行图片路径建议用绝对路径存数据库比如/data/sea-world/uploads/xxx.jpg别存相对路径否则在不同目录下启动服务图片全部会失效。这个坑我见过不止一次部署时踩的最多的就是路径问题。7. 答辩准备与个人实操心得7.1 这个项目的答辩亮点怎么讲很多同学做完项目答辩的时候只会说这是商品管理、这是订单管理、这是会员管理没了。这会让评委觉得你没思考。换个方式讲效果会明显不同。建议从“要点细节”的角度组织答辩话术。比如我做的这个系统核心亮点是库存扣减用了条件更新原子操作防止超卖订单明细保存商品快照保证历史数据一致统一响应结构配合前端拦截器让前后端联调更顺畅权限控制基于接口级的注解通过AOP切面来实现扩展起来方便。每一个点对应的业务场景都讲清楚再补充一点踩坑经历比如跨域配置、JWT的token过期问题评委大概率会觉得你是有真实实践经验的。毕设的本质不是“做出来”而是“讲清楚你为什么这么做”。7.2 我实际做这个项目的几个经验第一点先把数据库表和接口文档定下来再动代码。很多同学喜欢先写页面页面写到一半发现需要的字段后端没有返工非常痛苦。我通常的做法是花一个晚上把表结构和所有接口清单列出来用Excel就能做然后按“基础模块-核心模块-统计模块-系统功能”的优先级逐个实现每个模块完成后先自己测试一遍再进入下一个。第二点善用MyBatis Plus的代码生成器。项目初期手动写实体类、Mapper、Service、Controller一套CRUD字段变一下就要改五六个文件浪费时间也容易出错。用MyBatis Plus的AutoGenerator代码生成器连数据库表结构自动生成全套基础代码然后在这些代码上修改业务逻辑开发效率翻了不止一倍。第三点数据初始化脚本非常重要。系统跑通后一定要写一段数据填充的SQL或者一个系统启动时执行的DataInitializer把演示需要的商品分类、商品、会员、管理员账号都准备好。这样部署到任何一个环境启动就能看到效果不用每次手动输一堆测试数据。有一次我在答辩现场临时切换演示环境正是初始化脚本起了作用所有页面填充了数据演示过程非常流畅效果比现场一点点录入好得多。7.3 后续可以扩展的方向这个项目如果做完了想继续深入有几个方向可以参考。一是引入Redis缓存热门商品信息和系统配置提升接口响应速度二是把在线支付对接进来订单流程从“直接完成”变成“支付回调”驱动三是用WebSocket做库存变动的实时通知让运营人员第一时间掌握热销商品库存告急的情况四是给后端加一个简单的监控模块记录接口响应时间和异常日志。每一项扩展都是在现有架构上“加模块”不需要推翻重来这也体现了当初选择基础技术栈时留出的扩展空间。做毕设是这个逻辑进入工作之后做真实业务系统也是这个逻辑——先把地基打稳再考虑盖几层楼的问题。最后说一点个人的体会。这类管理系统的核心从来不是某一个炫酷的技术点而是数据的一致性和业务的完整闭环。商品能增删改查不难库存能和订单联动、退货能回补库存、报表能还原真实经营情况这些细节才是系统真正“能用”的关键。做这个项目我把大部分时间花在了订单流程、库存并发、统计口径这些不显眼的地方到最后答辩和实际运行验证下来这些地方恰恰是整套系统最经得起推敲的部分。如果你准备做或正在做类似的项目不妨也把重心放在这些看不见的细节上它们才是这套系统真正的骨架。