
“水族馆销售与经营管理系统”这类题目这几年在计算机毕业设计里出现频率相当高。乍一看像是个普通的管理系统但仔细拆一下标题——销售、经营、海洋馆智慧运营、电商服务你会发现它其实是把“进销存 会员营销 线上商城 数据报表”揉在一起的综合业务系统。对于Java方向的学生来说这个选题的妙处在于技术栈足够主流Spring Boot Vue前后端分离业务场景足够完整答辩时有话可讲而且每块功能都能跟面试八股文里的知识点挂上钩。这篇博文我就以这套系统的完整实现为主线把从需求拆解、技术选型、数据库设计、核心代码实现到答辩亮点的过程全部过一遍。内容比较多我会把关键步骤和踩坑点都放在对应章节里不管是刚拿到题目的学弟学妹还是已经写了一半想补强功能、准备答辩的同学都能直接抄作业。1. 系统定位与核心需求拆解1.1 题目背面的三类业务场景先把这个题目读透。“水族馆销售与经营管理系统”不是让你只做一个商品展示页面它实际上覆盖了三类完全不同的业务场景第一类是传统电商销售也就是用户在线上商城浏览观赏鱼、水族器材、鱼粮药品等商品下单购买管理员在后台处理订单。这一块对应的是“销售管理”和“电商服务系统”这两个关键词。第二类是线下门店经营水族馆往往同时有实体门店需要给商品做库存管理、进货入库、调拨盘点还需要管理会员、办卡充值、计次消费。这一块对应的是“经营管理系统”这个词。第三类是场馆票务与运营作为一个“海洋馆”或“水族馆”门票销售是营收大头需要设计门票种类、场次时段、预约核销这类功能。这一块对应的是“智慧运营平台”。很多同学做这类题目容易犯的错就是把所有模块做成一锅粥实体类之间关系混乱报表数据对不上。我建议一开始就按照“商城 门店 票务”三个中心来划分模块边界后续开发会清晰很多。从答辩角度说这三块业务分别能引出不同的技术点商城相关可以讲高并发下单、库存扣减门店相关可以讲角色权限、库存预警定时任务票务相关可以讲订单状态流转和核销机制。一次答辩技术覆盖面非常完整。1.2 技术选型Spring Boot 为什么是毕业设计的“标准解”题目里直接写了Java和Spring Boot但不同学生可能拿到的题目描述不完全一样有的写的是“基于SSM”有的要求“前后端分离”有的甚至要求微服务架构。我的建议非常明确毕设场景下Spring Boot Vue 前后端分离是性价比最高的组合。为什么这么说第一Spring Boot 把SSM时代繁琐的XML配置几乎全部去掉了自动装配机制让项目跑起来只需要一个启动类这正好规避了新手最容易卡住的“配置地狱”问题。第二它内置了Tomcat打包成Jar直接运行部署演示很方便。第三社区资料量巨大任何报错信息几乎都能在CSDN、GitHub、Stack Overflow上搜到现成答案这对时间紧张的毕设来说太重要了。关于版本2025年这个时间节点我强烈建议使用Spring Boot 2.7.x版本搭配JDK 8或JDK 11而不是一上来就追最新的Spring Boot 3.x和JDK 17。原因后面第2章和第5章会详细展开这里先记住一个结论毕业设计要的是“稳定跑通优先于技术尝鲜”版本太晚会碰到很多国产中间件不兼容的问题版本太新又会碰到依赖API变更导致的老教程全失效问题。另外ORM框架我推荐MyBatis-Plus而不是JPA。原因很实际MyBatis-Plus几乎是为懒人设计的基础的增删改查不需要手写SQL代码生成器一键扫表生成实体和Mapper热部署改接口也很灵活而且面试时“MyBatis-Plus你用过吗”基本是Java后端的高频问题做了毕设正好有话讲。2. 架构设计与技术准备2.1 前后端分离的整体架构有了模块划分之后下一步就是把系统的物理结构定下来。推荐架构是前端Vue 3 Element Plus Axios ECharts用Vite构建独立的admin端管理后台和用户端商城页面可以合并到一个前端项目里通过路由和菜单权限区分。后端Spring Boot 2.7.x按“Controller - Service - Mapper”三层结构分包配合MyBatis-Plus做数据持久化。数据库MySQL 5.7或8.0存储业务数据使用Redis作为缓存和验证码存存储如果没有部署条件Redis也可以不接后续扩展时再加。认证JWT 拦截器实现前后端分离下的登录认证和接口鉴权。这样一套架构的技术含量在毕设里属于“比上不足比下有余”但胜在每一项都是当前企业级开发的主流方案。答辩时老师问“为什么用前后端分离”你可以从“前后端并行开发、接口复用、部署灵活、可扩展多端小程序、App共用一套接口”这几方面回答保准有内容说。后端项目的包结构大概是这样的com.aquarium ├── config // 配置类跨域、MyBatis-Plus分页、拦截器注册 ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto // 前端参数接收对象 ├── vo // 前端返回对象 ├── common // 统一返回结果、异常处理、常量 ├── utils // JWT工具、日期工具等 └── QuartzJob // 定时任务库存预警、订单超时关闭2.2 环境版本组合选稳定不选最新很多同学上来就喜欢装最新版软件Spring Boot直接拉到3.3.xJDK直接OpenJDK 21前端用Vue 3.5。结果一跑项目先报spring-boot-starter-validation找不到依赖再报com.sun.mail不存在搜了半天发现是javax变动成jakarta导致的然后心态就崩了。我建议直接抄这套经过验证的版本组合组件版本说明JDK1.8 或 11老项目兼容性最好出问题资料最多Spring Boot2.7.18最后一个支持JDK8的主流版本稳定性极高MySQL8.05.7资料也能用8.0是主流MyBatis-Plus3.5.3.1和Spring Boot 2.x搭配非常成熟Redis5.x或6.x用于缓存后端用Spring Data RedisNode.js16.20.x对Vue3Vite支持稳定的版本Maven3.8.x配合IDEA内置绑定无需单独折腾这套组合在2025年依然有海量教程资料支持。如果你不想跟Spring Boot 3.x的javax.sqlvsjakarta.sql折腾就老老实实用这个组合。这里要特别说一句毕设的评分标准是“功能完整、逻辑清晰、运行正常”没有任何老师会因为你的Spring Boot版本比别人旧扣分但会有很多老师因为系统跑不起来而让答辩难堪。2.3 数据库设计九张表串起整个水族馆业务数据库设计是这类系统最核心的环节表结构设计得合理后续写代码会非常顺利设计得混乱后面每写一个功能都要回头加字段。我根据业务场景拆出了九张核心表直接给出建表SQL供参考。-- 用户表前台注册用户 后台管理员统一存放在这里用角色区分 CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, 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, role tinyint NOT NULL DEFAULT 1 COMMENT 1-普通用户 2-店员 3-管理员, balance decimal(10,2) DEFAULT 0.00 COMMENT 账户余额用于商城购物和门票购买, points int DEFAULT 0 COMMENT 积分, status tinyint DEFAULT 1 COMMENT 1-正常 0-禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 商品分类表 CREATE TABLE category ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, parent_id bigint DEFAULT 0, sort int DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品分类表; -- 商品表水族器材、观赏鱼、鱼粮药品等 CREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 商品名称, category_id bigint DEFAULT NULL, price decimal(10,2) NOT NULL COMMENT 售价, cost_price decimal(10,2) DEFAULT NULL COMMENT 进价, stock int NOT NULL DEFAULT 0 COMMENT 库存, sales int DEFAULT 0 COMMENT 销量, image varchar(255) DEFAULT NULL, detail text COMMENT 商品详情, status tinyint DEFAULT 1 COMMENT 1-上架 0-下架, version int DEFAULT 0 COMMENT 乐观锁版本号, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; -- 门票表 CREATE TABLE ticket ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 门票名称成人票/儿童票/年卡, type tinyint DEFAULT 1 COMMENT 1-单次票 2-年卡 3-次卡, price decimal(10,2) NOT NULL, valid_days int DEFAULT 1 COMMENT 有效期天数, description varchar(500) DEFAULT NULL, stock int DEFAULT 0, status tinyint DEFAULT 1, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT门票表; -- 订单主表 CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint NOT NULL, order_type tinyint NOT NULL COMMENT 1-商品订单 2-门票订单, total_amount decimal(10,2) NOT NULL, pay_amount decimal(10,2) NOT NULL, pay_type tinyint DEFAULT 1 COMMENT 1-余额支付 2-微信支付模拟, status tinyint NOT NULL DEFAULT 0 COMMENT 0-待支付 1-已支付 2-已发货 3-已完成 4-已取消, receiver_name varchar(50) DEFAULT NULL, receiver_phone varchar(20) DEFAULT NULL, receiver_address varchar(200) DEFAULT NULL, pay_time datetime DEFAULT NULL, delivery_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; -- 订单明细表一个订单对应多个商品/多张门票 CREATE TABLE order_item ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL, order_no varchar(32) NOT NULL, product_id bigint DEFAULT NULL COMMENT 商品ID门票订单则填写门票ID, product_name varchar(100) NOT NULL, product_image varchar(255) DEFAULT NULL, price decimal(10,2) NOT NULL, quantity int NOT NULL, subtotal decimal(10,2) NOT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表; -- 会员卡表计次卡、年卡等 CREATE TABLE member_card ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, card_no varchar(32) NOT NULL, ticket_id bigint DEFAULT NULL COMMENT 关联门票类型, total_times int DEFAULT 0, used_times int DEFAULT 0, expire_time datetime DEFAULT NULL, status tinyint DEFAULT 1, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_card_no (card_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员卡表; -- 库存出入库记录表 CREATE TABLE stock_log ( id bigint NOT NULL AUTO_INCREMENT, product_id bigint NOT NULL, change_type tinyint NOT NULL COMMENT 1-入库 2-销售扣减 3-退货回补 4-盘点调整, change_count int NOT NULL, before_stock int NOT NULL, after_stock int NOT NULL, remark varchar(200) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存出入库记录表; -- 销售统计表按天汇总便于报表展示 CREATE TABLE daily_sales ( id bigint NOT NULL AUTO_INCREMENT, stat_date date NOT NULL, product_sales decimal(10,2) DEFAULT 0.00, ticket_sales decimal(10,2) DEFAULT 0.00, order_count int DEFAULT 0, new_user_count int DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_stat_date (stat_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售统计表;这九张表已经覆盖了商城购物、门票购买、会员计次、库存流水、销售报表五大核心功能。要注意的是我把“用户”和“管理员”放在同一张表里用role字段区分这样做对毕设来说逻辑简单、登录也好判断没必要像企业级权限系统一样单独建五张表。但为了体现“专业性”你可以引入 Sa-Token 或 Spring Security 做角色权限控制这在第4章会讲。3. 核心业务模块的设计与实现3.1 商品库存模块防超卖与库存回滚的实现商品库存是整个销售系统里最容易出Bug的地方也是面试官最爱问的“高并发扣库存”问题。如果你在做毕设时能把这个点做对并讲清楚含金量直接提升一个档次。最简单也最有效的方案是MyBatis-Plus的乐观锁。在product表里加一个version字段在代码里给实体类加Version注解然后配置一个MyBatis-Plus的乐观锁插件。这样扣库存时MP会自动生成类似UPDATE product SET stock stock - #{count}, version version 1 WHERE id #{id} AND stock #{count}的SQL既保证不会超卖又不需要手动加锁性能很好。核心Service代码逻辑如下Service public class ProductServiceImpl extends ServiceImplProductMapper, Product implements ProductService { Transactional(rollbackFor Exception.class) Override public boolean reduceStock(Long productId, Integer count) { // 方案一直接在Mapper里写乐观锁更新SQL推荐可控性强 int result baseMapper.reduceStock(productId, count); if (result 0) { throw new BusinessException(库存不足或商品已下架); } // 记录库存流水 Product product getById(productId); StockLog log new StockLog(); log.setProductId(productId); log.setChangeType(2); log.setChangeCount(-count); log.setBeforeStock(product.getStock() count); log.setAfterStock(product.getStock()); log.setRemark(销售扣减); stockLogService.save(log); return true; } }对应的Mapper XML里写update idreduceStock UPDATE product SET stock stock - #{count}, sales sales #{count}, version version 1 WHERE id #{productId} AND stock #{count} AND status 1 /update核心思路就是让数据库在更新时自动检查库存是否充足不满足条件就返回0代码里抛出异常并让整个事务回滚。这样即使同一时刻有多个用户同时下订单MySQL的行锁也会保证只有一条更新成功。实际做项目时我还会给orders表加上“发布订单”这个环节也就是下单时先把订单状态置为0待支付同时立即扣库存如果用户一直不支付比如30分钟后还没有付款就需要定时任务把订单状态改为已取消同时把库存回补。回补操作也很关键如果不做库存就会被“僵尸订单”白白消耗光这在毕设演示时很容易被老师发现。3.2 会员与储值模块把“营销”落到代码里水族馆的会员体系和超市、健身房不太一样它既有实体会员卡的办卡充值又有像“年卡、次卡”这种门票类会员产品。所以我把会员相关的功能拆成两层一层是账户资产也就是user表里的balance和points另一层是权益资产也就是member_card表里的各类计次卡。余额支付这个功能非常适合展示事务处理。比如用户下单时选择余额支付就同时要做两件事扣减用户余额、更新订单状态为已支付。这两件事必须放在同一个事务里否则会出现“钱扣了但订单还是待支付”的严重问题。代码大概长这样Transactional(rollbackFor Exception.class) Override public Boolean payByBalance(Long orderId, Long userId) { Orders order orderMapper.selectById(orderId); if (order null || !order.getUserId().equals(userId) || order.getStatus() ! 0) { throw new BusinessException(订单状态异常); } // 扣减余额并校验余额是否充足 int result userMapper.deductBalance(userId, order.getPayAmount()); if (result 0) { throw new BusinessException(余额不足请先充值); } // 更新订单状态 order.setStatus(1); order.setPayTime(new Date()); orderMapper.updateById(order); // 增加积分金额1比1累计 userMapper.addPoints(userId, order.getPayAmount().intValue()); return true; }这里用userMapper.deductBalance也采用条件更新SQLWHERE id #{userId} AND balance #{amount}配合事务逻辑非常稳妥。次卡核销这块我建议设计成“一码核销”管理员在后台扫描用户的卡号或手机号调用核销接口对应member_card表的used_times加1核销前要判断used_times total_times并且当前时间在有效期内。这段逻辑实现起来不难但必须要防止“同一个卡在同一个瞬间被核销两次”所以更新SQL同样要加限制条件比如UPDATE member_card SET used_times used_times 1 WHERE id #{cardId} AND used_times total_times。3.3 订单流程设计状态机与超时自动关单订单模块是所有电商系统的主心骨。我在设计订单状态时只用了5个状态值0-待支付、1-已支付、2-已发货、3-已完成、4-已取消。这里要避免把状态拆得太细毕设系统里“退款售后”这类流程不是必须的能省就省不然工作量会翻倍。在这5个状态的基础上状态流转关系是待支付 → 已支付 → 已发货 → 已完成待支付 → 已取消已支付 → 已取消在发货前用户可以申请退款管理员确认后取消订单并回补库存。这套流转用一张图就能讲清楚答辩时可以在PPT里画出来会显得你设计思路很清晰。订单超时自动关单的实现方式最经典的方案就是用定时任务。Spring Boot 2.x自带Scheduled注解一行注解就能跑定时任务但如果你想故意加一个技术亮点可以使用Quartz框架把“扫描超时订单”和“扫描库存预警”做成两个独立的Job在配置里设定执行频率。针对“springboot quartz”这个高频搜索词我给你一个最小可运行的配置Component public class OrderTimeoutJob { Autowired private OrderService orderService; Scheduled(cron 0 */1 * * * ?) // 每分钟执行一次 public void closeTimeoutOrders() { // 找出创建时间超过30分钟且状态为0的订单 ListOrders timeoutOrders orderService.list(new LambdaQueryWrapperOrders() .eq(Orders::getStatus, 0) .lt(Orders::getCreateTime, DateUtil.offsetMinute(new Date(), -30))); for (Orders order : timeoutOrders) { orderService.cancelOrder(order.getId(), 系统超时自动关闭); } } }实际开发时我会把扫描间隔设成1分钟30分钟未支付自动关闭。演示时不需要等30分钟把配置改成5分钟即可这个细节可以在答辩时跟老师说明。3.4 经营统计与报表让数据变成管理决策一个销售系统如果没有数据报表那你做的只是一个“记账本”。为了让系统更像一个真正的“智慧运营平台”我建议在后台增加一个“经营分析”页面用ECharts展示4张核心图表近7天/30天销售趋势折线图区分商品销售额和门票销售额商品销售排行榜Top10柱状图每日订单量与客单价趋势图会员增长曲线图后端提供一个聚合统计接口用SQL分组汇总。比如查询近7天每日销售总额GetMapping(/sales/trend) public ResultListMapString, Object salesTrend(RequestParam Integer days) { LocalDate startDate LocalDate.now().minusDays(days - 1); return Result.success(baseMapper.selectDailySummary(startDate)); }对应的Mapper SQLselect idselectDailySummary resultTypemap SELECT stat_date AS date, SUM(product_sales) AS productSales, SUM(ticket_sales) AS ticketSales, SUM(order_count) AS orderCount FROM daily_sales WHERE stat_date #{startDate} GROUP BY stat_date ORDER BY stat_date /select这里需要把daily_sales表里的汇总数据做定时增量更新。最简单的方式是每天凌晨跑一个Quartz Job统计昨天的订单数据插入到统计表中报表接口直接查这张表就可以了不会因为数据量大查询变慢。4. 从零搭建Spring Boot项目的实操过程4.1 项目初始化与基础配置在IntelliJ IDEA里新建Spring Boot项目我用的是Spring Initializr。选择语言Java、构建工具Maven、Spring Boot 2.7.18版本依赖勾选“Spring Web”、“MySQL Driver”、“Lombok”、“Validation”、“Spring Data Redis”如果计划用缓存。项目生成后再手动在pom.xml里加入MyBatis-Plus、JWT、Hutool工具包和Quartz的依赖。pom.xml里加这一堆依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-quartz/artifactId /dependency然后是application.yml的核心配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/aquarium?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 0 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:mapper/**/*.xml jwt: secret: your-256-bit-secret-key-your-256-bit-secret-key expire-hours: 72有一个特别容易被忽略的配置MyBatis-Plus的分页插件。如果不配置你写的page(new Page(1, 10), wrapper)根本不会真正分页而是把所有数据查出来再内存分页。这个坑我见太多人踩过。一定要加一个配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }4.2 实现一个完整的商品上下架与库存扣减接口以管理员新增商品为例。Controller层接收前端传参Service层做业务逻辑处理Mapper层负责持久化。一个完整的接口就是三层各负责一件事。RestController RequestMapping(/api/admin/product) public class AdminProductController { Autowired private ProductService productService; PostMapping public ResultBoolean addProduct(RequestBody Validated ProductDTO dto) { Product product new Product(); BeanUtils.copyProperties(dto, product); product.setStatus(1); productService.save(product); return Result.success(true); } PutMapping(/stock) public ResultBoolean updateStock(RequestBody StockUpdateDTO dto) { productService.adjustStock(dto.getProductId(), dto.getType(), dto.getCount(), dto.getRemark()); return Result.success(true); } PutMapping(/status/{id}) public ResultBoolean changeStatus(PathVariable Long id, RequestParam Integer status) { Product product productService.getById(id); product.setStatus(status); productService.updateById(product); return Result.success(true); } }商品管理部分最需要考虑的是“库存调整”要不要记录日志。前台的销售扣减一定要记录到stock_log表后台人工盘点调整也不能漏掉。这样系统会显得非常严谨后续查询某个商品的入库、出库流水都有据可查这种设计思路在答辩时也很加分。4.3 登录鉴权与JWT实现保护你的后台接口前后端分离项目里JWT是用的最多的认证方案。用户登录成功后后端生成一个带过期时间的Token返回给前端前端把Token存在localStorage里每次请求都在请求头里带上Authorization: Bearer xxx后端写一个拦截器统一校验Token并解析出用户ID。JWT工具类核心代码Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.expire-hours}) private Integer expireHours; public String generateToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireHours * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }拦截器里要做的事情是放行登录、注册接口和商品查询接口其他接口必须校验Token。在WebMvcConfig里注册拦截器并配置排除路径Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns( /api/auth/login, /api/auth/register, /api/product/**, /api/ticket/** ); } }/api/admin/**接口除了要判断登录还要判断角色是否为管理员否则普通用户也能访问后台管理接口那就是严重的安全漏洞。我在拦截器里只做登录校验角色校验放在Controller层用AOP或者自定义注解完成你可以简单点直接在每个管理端Controller方法的开头判断user.getRole() 3虽然不优雅但足够用。5. 常见问题与避坑指南5.1 Spring Boot 版本兼容与环境坑这个章节写出来就是想帮大家少走弯路。2025年网上搜索“springboot”高频热词里出现频率最高的就是“springboot版本太高”。为什么会这样因为Spring Boot 3.x把javax命名空间迁移到了jakarta导致大量网上教程和开源项目里的老代码直接报编译错误。而且Spring Boot 3要求JDK 17以上很多学校机房或者个人电脑装的还是JDK 8就会出现“编译不通过、启动直接失败”的尴尬。另外还有两个经典的报错java: 警告: 源发行版 17 需要目标发行版 17和java: You arent using a compiler supported by lombok。前者是IDEA里Project Structure里配置的JDK版本和Maven编译版本不一致导致的解决方法是把Project SDK、Project language level、Maven的java.version全部统一成同一个版本。后者是Lombok版本和JDK不匹配升级Lombok插件或者确认annotationProcessorPaths配置正确即可。如果你非要用Spring Boot 3.x也不是不行但要做好三件事第一JDK必须17第二所有教程里的javax.*要手动改成jakarta.*第三一些老牌中间件比如某些短信SDK可能不支持需要额外适配。我的建议依然是如果毕设时间紧张用Spring Boot 2.7最省心。5.2 循环依赖、自动装配与Bean注入问题Spring Boot面试八股文里最常问的几个点在项目实操里也会遇到。循环依赖就是典型比如订单Service里注入了用户Service用户Service又反向注入了订单Service项目启动时直接报The dependencies of some of the beans in the application context form a cycle。解决办法最简单的是把其中一个注入方式改成Lazy延迟加载或者用Autowired改在字段上加Spring Boot 2.6之后默认禁止循环依赖如果非要用需要在application.yml里设置spring.main.allow-circular-referencestrue。更合理的做法是重新设计一下Service调用关系把互相调用的逻辑抽到一个新的Service里。再说自动装配Spring Boot启动时为什么不需要配置那么多XML因为SpringBootApplication里有EnableAutoConfiguration它会去读META-INF/spring.factories或AutoConfiguration.imports把各个starter里的XxxAutoConfiguration自动加载出来。理解了这个原理你在排查“为什么Redis没生效”“为什么数据源报了错”这类问题时思路就会清晰很多。实际开发中另一个高频坑注入RestTemplate报找不到Bean。原因很简单RestTemplate不是Spring Boot自动配置的一部分你必须自己写一个配置类声明Bean public RestTemplate restTemplate() {...}。遇到“NoSuchBeanDefinitionException”时先想想你要用的类到底有没有被starter自动装配。5.3 跨域、文件上传与资源映射前后端分离开发时前端运行在http://localhost:5173后端接口在http://localhost:8080会有跨域问题。解决跨域最直接的方案是后端写一个允许跨域的配置类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); } }另外水族馆系统的商品图片、场馆图片需要上传和展示。文件上传后的存储路径一定要是绝对路径不能是相对路径否则打Jar包部署后图片路径会因为工作目录变化而找不到。建议把上传目录配置成D:/aquarium/upload/这种绝对路径同时为上传目录做一个虚拟路径映射让前端通过/upload/xxx.jpg访问到物理文件Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath file: uploadDir; registry.addResourceHandler(/upload/**).addResourceLocations(uploadPath); }5.4 分页、时间格式化等后端细节坑分页插件配好之后还有一个常见问题前台传入的pageNum、pageSize参数命名不统一。有的前端传pageNum有的传page后端的Page对象默认构造方法是new Page(current, size)名称不匹配就查出来一大坨数据。建议在全局统一约定前端传page和limit接收后转换成Page对象。时间格式化也是老生常谈。前端展示时间时如果出现2025-01-01T12:00:00.00008:00这种格式就是序列化问题。在Spring Boot里统一配置时间序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8加了这行之后接口返回的时间就变成2025-01-01 12:00:00前端不用再做额外处理。6. 答辩包装与系统扩展空间6.1 项目亮点怎么讲才会有技术含量大部分毕设项目的功能都是“你有我有全都有”但答辩分数高低往往取决于你怎么讲。我个人建议围绕三个点包装第一高并发库存安全。把第3.1节的乐观锁扣库存逻辑拿出来讲说明你不仅会CRUD还考虑到了并发场景下的数据一致性。如果被追问还可以补充“为什么不用悲观锁”回答“乐观锁在读写冲突不高的场景下性能更好适合商城秒杀之外的常规下单场景”。第二模块化设计。把“电商 门店 票务”的划分逻辑讲清楚以及订单状态机的设计思路这体现了你的业务抽象能力。第三自动化任务。用Quartz或Spring Task实现订单超时关单、库存预警、每日销售统计让系统具备“运营”能力而不只是一个CRUD增删改查。当然如果你想再加一个硬核亮点可以给系统集成Redis缓存。把商品列表、门票列表这些查询频率高、更新频率低的数据缓存到Redis缓存穿透用空值缓存解决缓存更新用“先更新数据库再删除缓存”的方式。这一套讲下来技术栈里就多了“缓存中间件”含金量又上去了。6.2 系统还可以往哪些方向扩展如果题目允许或者你后期想优化有几个扩展方向成本低、效果好一是接入RabbitMQ或Kafka消息队列把订单创建后的积分发放、短信通知变成异步流程这样在下单接口里就不需要等这些非核心操作执行完接口响应速度能明显提升。二是增加数据分析模块用Python或者直接写SQL做一些用户画像分析比如“购买观赏鱼的客户通常也会购买鱼粮”“周六日门票销量是工作日的几倍”这些结论非常能打动评委因为它是真正从数据里挖掘出来的“智慧运营”价值。三是开发微信小程序端。小程序扫码购票、会员卡绑定是水族馆场景非常自然的需求。前端用uni-app开发几天就能出一个基础版复用现有java后端接口整体系统的完整度会显得非常成熟。四是Docker部署。把Spring Boot项目打成镜像、MySQL和Redis用docker-compose编排做到一键启动整个系统。这个在“springboot打包到docker desktop”热搜词背后也是典型需求但要注意镜像基础镜像用openjdk:8-jre-alpine或eclipse-temurin:8-jre不要用大而全的镜像打包发布前先Maven clean package确保Jar包是新的。最后分享一个我做这类系统时的小习惯开发顺序上先做用户登录注册再做商品管理再做订单购物流程再做会员卡最后做统计报表。前面的模块是后面的基础按依赖顺序推进每完成一个阶段都能启动起来演示不容易出现“最后一周期末冲刺”的窘境。做毕设最忌讳的是一上来闷头写了一大堆表、画了一堆页面图结果核心流程还没走通到答辩前夜才慌慌张张联调。先把主链路跑通再充实细节这是我这几年带毕业设计项目下来最深的体会。