看到"购买狗"这个名字,第一反应多半是"这是不是卖狗粮的"。其实这是我整理Java毕设方案时给内部原型起的代号,它对应的是一个线上游戏购买平台:用户注册登录后浏览游戏商品、按分类搜索、加入购物车、下单并模拟支付;管理员在后台维护商品、处理订单、完成发货。整套项目用Spring Boot 3 + MySQL 8 + MyBatis-Plus实现,配套内容涵盖完整源码、数据库SQL脚本、项目文档、调试记录和代码讲解。这篇文章不是贴一份通宵赶出来的代码,而是把选题判断、核心设计、重点实现和踩坑经验都摊开讲一遍——如果你正处在Spring Boot毕设选题的阶段,这个方向值得认真考虑。1. 这道题好在哪:从选题雷区到工作量拆解1.1 选题雷区:太简单和太复杂各有什么风险每年的毕设选题季,我见过太多同学在两个极端之间反复横跳。一个极端是选"图书管理系统""新闻发布系统"这类纯增删改查项目,听起来很稳,但做到中期就发现没东西可写,答辩时评委一句"你觉得这个项目最大的难点是什么"就能让全场沉默。另一个极端是选"高并发秒杀系统""双十一购物平台"这类大厂业务题,光分布式锁和消息队列就够研究一个月,最后要么草草收尾,要么变成"PPT演示系统"。线上游戏购买平台恰好落在中间区域。它和普通图书管理的区别在于,游戏属于数字商品,天然带有库存、销量、上架状态、多图展示、分类检索这些业务规则;同时它又不涉及物流跟踪、售后签收、满减优惠券这些会无限扩充边界的电商模块。一个"商品—购物车—订单—支付—后台发货"的完整业务闭环,正好能覆盖毕设评审最看重的几个维度:技术覆盖面够不够、业务逻辑有没有讲清楚、跑起来能不能演示。这里也说一个很多人没意识到的点:毕设项目的选题名字本身不重要,重要的是业务闭环是否完整。你完全可以把这个项目改名叫"某数字商品交易平台""线上道具商城",甚至"开源游戏商店",核心架构都不用动。名字叫"购买狗"还是"聚玩游戏商城",对你的得分影响约等于零。1.2 工作量盘点:表多少张、接口多少个、页面几页在决定要不要选这个题目之前,最好先对工作量有个量化概念。我这里按一个人、六周、每周投入15小时左右来拆解:数据库核心表:8张(用户、分类、游戏商品、商品图片、购物车、订单主表、订单明细、支付记录)后端接口:约30个(用户认证、商品浏览、购物车、订单、后台管理)前台页面:6个左右(首页、商品列表、商品详情、购物车、结算页、个人中心)后台页面:5个左右(登录、商品管理、分类管理、订单管理、统计面板)开发节奏可以参考这样排:前两周搭建工程骨架和用户模块,第三周做商品浏览和购物车,第四周集中啃订单和模拟支付,第五周完成后台管理,第六周统一测试、补文档、录演示视频、准备答辩。如果选的是前后端分离方案,再加一到两周的前端工作量,这个后面会详细说。有一点必须提醒:很多同学拿到现成源码后,第一反应是背代码。这在大一课程设计里也许能混过去,但毕设答辩会现场问思路、改需求、看调试过程,一旦接不上就很容易穿帮。正确的姿势是先把项目完整跑起来,然后挑三四个核心接口自己手写一遍,比如登录鉴权、下单扣库存、发货状态流转,其余部分能讲清楚流程就行。2. Spring Boot 3 + MySQL 8:技术选型逻辑与项目骨架2.1 为什么毕设不建议一上来就微服务每年都有学生问:"现在企业都在用微服务,我毕设是不是也应该上Spring Cloud?"我的答案一直是不建议。评审老师对毕设的判断标准是"学生有没有正确理解并实现一个完整的业务系统",而不是"这套架构商业上可不可行"。单体应用一样可以包含多个模块、清晰的层级边界和良好的代码组织;微服务带来的网关、注册中心、配置中心、链路追踪,任何一个组件出问题都会消耗大量的调试时间,而这些时间原本应该花在核心业务逻辑上。Spring Boot 3 + JDK 17是目前最合理的组合。Spring Boot 3基于Spring 6,自动配置机制相当成熟,内嵌Tomcat让打包部署变得异常简单,运行java -jar直接把整个后端拉起来。对演示场景来说,这种"一键启动"的可靠性比花哨的架构重要得多。数据库选MySQL 8没有悬念。本地开发默认utf8mb4字符集,中文存储不乱码;支持窗口函数,万一统计面板里需要"销售Top5"这类聚合查询,写起来也比较顺手。配合MyBatis-Plus,单表CRUD零SQL,复杂查询再写XML,开发效率和可维护性之间会有比较好的平衡。2.2 依赖配置与工程目录:照着搭能少走哪些弯路工程基础依赖建议这样配,注意Spring Boot 3对应的MyBatis-Plus要引入mybatis-plus-spring-boot3-starter,而不是老项目里常见的mybatis-plus-boot-starter,这是个很典型的版本坑:<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.5</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>application.yml里有几处配置值得提前说清楚。serverTimezone=Asia/Shanghai明确指定时区,否则很容器出现数据库时间和北京时间差8小时的问题;map-underscore-to-camel-case让数据库下划线字段自动映射成Java驼峰属性;逻辑删除的开关开好,后面删商品就可以走软删除而不用物理删行:server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/buy_dog?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 thymeleaf: cache: false servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0工程内部的目录结构,我建议按这样的包来组织:com.example.buydog ├── config -- 配置类:分页插件、静态资源映射、拦截器注册 ├── controller -- 接口层,只做参数接收和结果返回 ├── service -- 业务层,写核心业务逻辑 ├── mapper -- MyBatis-Plus Mapper接口 ├── entity -- 数据库对应实体 ├── dto -- 接口入参出参对象 ├── common -- 统一返回体、全局异常处理 └── utils -- 工具类一个非常实用的小原则是:Controller里不要写业务逻辑,业务逻辑也不要堆在Service接口里。代码讲解和答辩的时候,评审老师主要会看Service层的实现,一个干净的Service比什么都更有说服力。3. 功能模块与业务闭环:从注册登录到订单发货怎么串起来3.1 两套角色与登录认证方案取舍这个项目需要两类角色:普通用户和管理员。在数据层面不需要建两张用户表,一张user表加一个role字段就够了,1代表普通用户,2代表管理员。密码存储是答辩高频问题。一定不要用明文存密码,推荐用BCrypt做哈希,每次注册生成随机盐,即使数据库泄露,攻击者拿到的也是一串没办法直接还原的密文。Spring Boot的spring-security-crypto包里自带BCryptPasswordEncoder,直接注入使用就可以了。登录态这块,我建议优先考虑Session方案:用户登录成功后把用户ID放进Session,自定义一个LoginInterceptor拦截需要权限的请求。这样代码量少、理解难度低,非常适合毕设。如果选了前后端分离的Vue方案,再引入JWT,用Authorization请求头携带令牌。两种方案各有适用场景,非前后端分离就别硬上JWT,增加复杂度不说,演示时反而更容易出问题。3.2 商品浏览、购物车与重复加购处理首页和商品列表页是这个项目的门面。列表接口至少要支持三件事:按关键词模糊搜索、按分类筛选、分页。MyBatis-Plus的分页插件配置好之后,通过Page对象就能完成分页,SQL层面不需要手动拼LIMIT。购物车的数据结构不复杂,cart_item表里记录用户ID、游戏ID、数量、选中状态。这里要注意一个很容易被忽略的细节:用户把同一款游戏反复加进购物车时,不应该插入两条购物车记录,而应该做一次存在性检查——如果购物车里已经有这个商品,就把数量加1,否则才插入新记录。你可以把这个逻辑画成流程图写进文档里,算得上一个小亮点。购物车的选中状态也要考虑清楚。用户勾选几款商品下单,结算时只提交选中的商品ID列表,后台根据这些ID去取购物车数据、生成订单。有了checked字段,整个交互流程会真实很多,而不是简单粗暴地"清空购物车全部结算"。3.3 订单状态机、模拟支付与超时订单扩展订单是这个项目的灵魂,也是最值得在答辩时展开讲的部分。订单状态可以定义为五个:待支付已支付已发货已完成已取消流转关系为:待支付可以变成已支付,也可以变成已取消;已支付能变成已发货;已发货能变成已完成。每一步状态变更都在代码里做一个前置校验,不允许跳状态,比如"已支付"不能直接到"已完成",必须经过"已发货"。这种状态机的设计思路本身就很有含金量,即使没有用状态模式这样的设计模式,只要逻辑清晰,也能够在答辩时加分。下单的核心方法要开事务。一次下单至少涉及这几步:读取选中的购物车记录,往订单主表插一条订单信息,往订单明细表插入商品快照,扣减游戏库存,最后清空购物车对应记录。任何一个环节失败,都应该通过事务整体回滚,保证不会出现"订单生成了但库存没扣"这种脏数据。订单明细必须做商品快照,也就是把下单那一刻的商品名称、封面图、单价、数量原样保存到订单明细表里。这背后的原因是:管理员后续修改商品价格或名称时,历史订单不应该跟着变,否则对账会乱。我见过不少项目把订单详情关联到商品表去取价格,答辩时被问到"商品改名后订单里显示什么",直接卡住。快照这个设计要讲出来,很加印象分。模拟支付不要接真实支付渠道。一个是涉及商户资质和资金监管,毕设阶段搞这个合规风险太高;另一个是真实支付通常需要回调接口,流程设计调试成本都不低。正确做法是单独建一张payment_info支付记录表,提供一个"模拟支付成功"的接口,在接口里生成支付流水号、把订单状态从待支付改成已支付、记录支付时间。这一段要注意讲清楚你理解"真实支付会有异步回调,当前是Mock实现替代了这个环节",就可以把短板变成加分项。超时未支付属于扩展加分点。可以用Spring自带的@Scheduled定时任务,每分钟扫描一次超过15分钟仍未支付的订单,将状态改为已取消并把库存回补。这个功能不强求,但做了之后答辩效果会明显好一截,因为评委能看出你考虑到了真实业务里的异常情况。3.4 后台管理:商品、订单与统计面板后台管理模块的定位是把运营动作闭环起来。商品管理包括新增、编辑、上下架、修改库存、上传封面图和描述;分类管理是简单的增删改;订单管理主要做两件事:查看订单列表、对已支付订单执行发货操作。很多同学会漏掉一个管理端入口的权限控制。管理员接口必须校验当前登录用户的role是否为2,不能只依赖前端隐藏按钮。这里可以写一个独立的AdminInterceptor,拦截所有/api/admin/**路径,Session里的用户角色不对就返回无权访问。这个设计很小,但体现的是对权限模型的理解。统计面板可用可不用,但做了会很出彩。不需要复杂的BI报表,直接写两条SQL统计"今日新增订单数""近七天销售额趋势",甚至查询销售Top5的游戏商品列表,再配合ECharts画个折线图或柱状图就足够了。数据库只有8张表,统计查询的复杂度不高,但视觉效果和谈资都能提升一个档次。4. 数据库设计:从用户到支付这几张表的关键细节4.1 八张核心表的职责划分与字段要点我认为一个合格的毕设数据库设计,不在于表多,而在于每张表的职责足够清晰。下面这八张表基本可以撑起整个业务闭环,没有一张是多余的:表名职责关键字段user用户与管理员账号username、password、role、statusgame_category游戏分类name、sort、statusgame_info游戏商品主体category_id、name、price、stock、sales、statusgame_image商品详情图(一对多)game_id、image_url、sortcart_item购物车条目user_id、game_id、count、checkedorder_info订单主表order_no、user_id、total_price、statusorder_item订单明细快照order_id、game_id、game_name、price、countpayment_info支付流水payment_no、order_id、amount、status以game_info为例,建表语句可以参考这样:CREATE TABLE `game_info` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `category_id` BIGINT NOT NULL COMMENT '分类ID', `name` VARCHAR(100) NOT NULL COMMENT '游戏名称', `cover_img` VARCHAR(200) COMMENT '封面图路径', `price` DECIMAL(10,2) NOT NULL COMMENT '售价', `original_price` DECIMAL(10,2) COMMENT '原价,用于展示划线价', `stock` INT NOT NULL DEFAULT 0 COMMENT '库存', `sales` INT NOT NULL DEFAULT 0 COMMENT '累计销量', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `description` TEXT COMMENT '游戏介绍', `created_time` DATETIME, `updated_time` DATETIME, `deleted` TINYINT NOT NULL DEFAULT 0, KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='游戏商品表';订单主表和订单明细表建议拆开,而不是把商品信息直接冗余在订单主表里。这样一张订单可以对应多款游戏,明细表采用快照字段存储名称和价格,后续统计销售额、导出订单清单都很方便。4.2 订单号生成、库存扣减与逻辑删除的取舍订单号千万不要用数据库自增ID对外展示,这个习惯很不好。自增ID会暴露平台的销量规模,也容易被枚举遍历。自己生成一个业务订单号就可以了,我常用的格式是"时间戳+四位随机数",代码很短但覆盖了主要需求:public static String generateOrderNo() { String time = LocalDateTime.now() .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); int random = ThreadLocalRandom.current().nextInt(1000, 10000); return time + random; }库存扣减是整个项目里最值得展开讲的点。不要在Java代码里先查出库存数量,判断大于零后再执行更新,因为两个用户同时下单时会出现"都查到有库存,一起扣"的情况,最后库存变成负数。正确做法是把库存判断和扣减合到一条SQL里完成:<update id="deductStock"> UPDATE game_info SET stock = stock - #{count}, sales = sales + #{count} WHERE id = #{gameId} AND stock >= #{count} AND status = 1 </update>这条SQL非常关键:stock >= #{count}是原子条件,数据库行锁保证同一时间只有一个事务能更新成功,如果影响行数为0,说明库存不足或商品已下架,Java层直接抛业务异常回滚整个下单事务。答辩时能讲清楚这个细节,就已经超过一大半的毕设项目了。逻辑删除是MyBatis-Plus非常方便的功能,开启后deleteById会变成UPDATE ... SET deleted=1。但这会带来一个坑:唯一索引和逻辑删除字段会冲突。比如用户表对username建了唯一索引,逻辑删除后再次注册同名用户会插入失败,因为旧记录还在表里。处理方案有两种,要么把username改成普通索引,在Java层做唯一性判断;要么把deleted字段参与唯一索引。最省事的方案是毕设里大部分主数据表只做逻辑删除,但用户表直接用物理删除,并能向评委解释清楚"为什么这张表可以物理删除"。5. 高频踩坑与排查实录:时区、库存竞争、上传图片5.1 数据库时间差八小时:一次时区问题排查这个坑基本属于Spring Boot项目的"必答题"。现象是控制台打印的时间是正常的,但数据库里存进去的时间比当前时间慢了8个小时,或者查询出来的时间又比预期快了8小时。排查链路是这样的:先看数据库连接URL有没有配置serverTimezone,如果没有,MySQL驱动会按照服务器默认时区进行转换;再看MySQL全局时区,执行SELECT NOW()确认数据库当前时间;最后看Java实体里的时间字段类型。我的统一建议是:连接配置固定写死serverTimezone=Asia/Shanghai,Java实体层统一使用LocalDateTime,数据库中时间字段全部用DATETIME,三层统一之后800年的坑都能避开。5.2 库存被扣成负数:update语句加一个条件解决如果你完全按照第4.2节的方式写库存扣减,正常情况下不会出问题。但很多人早期版本是这样写的:GameInfo game = gameInfoMapper.selectById(gameId); if (game.getStock() >= count) { game.setStock(game.getStock() - count); gameInfoMapper.updateById(game); }问题在于"先查再更"不是一个原子操作。用一个简单的并发场景来演示:两个买家同时提交购买同一款库存只剩1的游戏,两边都查到stock=1,都认为库存足够,然后各自执行库存减一,最终数据库里的库存变成-1。这个坑在单机演示时很难复现,但答辩时评委只要口头问一句"怎么保证不超卖",这个回答就会决定整个项目的含金量。我在整理这个项目时,把修复版SQL直接写进Mapper层的XML里,并加了一个单元测试模拟两次并发调用,确认影响行数只有1后,这条才真正过关。修复手法就是前面展示的那条带stock >= #{count}条件的UPDATE语句。5.3 商品图片404:上传路径和静态资源映射本地开发时上传图片通常很正常,但把工程打成jar包运行后,图片经常莫名其妙变成404。根因是很多人用相对路径保存文件,比如images/upload/xxx.jpg,开发期IDEA的工作目录恰好指向项目根目录,所以一切正常;jar包运行后工作目录变了,图片自然找不到。稳妥做法是使用绝对路径,并在配置类里增加静态资源映射:@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadDir = System.getProperty("user.dir") + File.separator + "upload" + File.separator; File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadDir); } }这里还有一个小细节:路径拼接不要写死/或者\\,用File.separator生成平台无关的路径。Windows和Linux上表现不一致的问题,我都在这套源码里同步修正过了。如果你在演示时突然发现图片打不开,先去看配置文件里有没有做静态资源映射,大概率就是它。5.4 登录拦截器把样式和图片一起拦截了注册了登录拦截器之后,最常见的"诡异现象"是页面HTML能打开,但CSS、JS、图片全部加载失败,浏览器控制台一片403或401。原因不难理解:拦截器把所有请求都拦截了,静态资源路径没有放行。解决方式是在注册拦截器时显式排除静态资源路径:registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/login", "/register", "/css/**", "/js/**", "/images/**", "/upload/**", "/error" );这个细节看起来不起眼,但我实在见过太多人因为这个原因,在答辩演示的时候当场社死。尤其是页面样式全部丢失后,整个项目看起来就像没做完一样。提前把静态资源放行逻辑写清楚,能让演示稳定性提升不少。6. 答辩演示与交付:让评委快速认可项目含金量6.1 五分钟演示脚本:把核心状态变化跑给评委看演示环节最怕的是"埋头操作三分钟,评委啥也没看懂"。我整理这套项目时,准备了一个固定的演示节奏,总共控制在5分钟以内,每一步都在讲业务状态的变化:先打开数据库,快速展示八张核心表,说明订单主表和明细表的拆分思路,时间控制在1分钟。注册一个新用户,登录后浏览商品列表,演示搜索和分类筛选,并把一款游戏加入购物车,时间约1分钟。打开购物车,勾选商品并提交订单,刻意停留在待支付状态,让评委看到订单状态和支付记录未生成;然后调用模拟支付接口,再次展示订单变成已支付、支付表多了一条流水,时间约1分半。切换到管理员账号,进入订单管理,找到刚才那笔已支付订单,执行发货操作,再回到前台看到订单变成已发货。时间约1分钟。最后回到订单详情或统计面板,用一句话说明状态机的整体流转,结束演示。重点一定是"状态变化",因为评委看不到你写代码的过程,能看到的就是这些数据和页面状态的前后差异。把差异讲清楚,项目逻辑就自然清楚了。讲解核心代码的时候,不需要逐行念,挑createOrder这一个方法就够了。打开下单接口,现场说清楚事务边界、库存扣减条件、订单明细快照这三个点,一屋子人的疑问基本都能被覆盖。6.2 常见追问与建议回答口径我把这几年见过的评委高频问题整理成一张表,你可以对照准备,但不要背答案,关键是要理解背后的原理:问题建议思路为什么用Spring Boot而不是其他框架?自动配置减少繁琐XML,内嵌Tomcat方便部署,生态成熟,适合快速开发单体业务系统订单号为什么不用自增ID?自增ID暴露销量,容易被遍历;业务订单号用时间+随机数生成,不可推测怎么防止库存超卖?库存判断和扣减合并为一条UPDATE,利用数据库行锁保证原子性,影响行数为0则回滚密码为什么不存明文?使用BCrypt哈希加盐存储,密文不可逆,数据库泄露也不会直接暴露密码模拟支付和真实支付有什么区别?真实支付需要商户资质和异步回调机制,毕设用支付流水表模拟这个环节,但理解了回调的业务语义如果用户一直不支付怎么办?定时任务扫描超时待支付订单,自动取消并回补库存这个项目最大的难点是什么?不是某个接口的增删改查,而是订单状态的一致性和并发场景下库存的原子扣减6.3 整套交付里应该包含哪些内容一套完整可交付的毕设项目,不止是源码本身。我这边整理并调试过的配套内容大致包括这几块:后端完整源码(含全部页面代码)、初始化SQL脚本(建库建表加样例数据,样例数据尽量多放一些,演示效果差别很大)、项目说明文档(需求分析、数据库设计说明书、接口文档、部署运行说明)、演示录屏视频,以及面向代码讲解用的注释版核心方法。这里想多说一句关于"全包"资源的理解。所谓全包,在一份合格的交付物里应该是"帮你把环境跑通、把代码讲透、把原理捋顺",而不是让你背一份天书一样的代码去应付答辩。更务实的做法是:拿到的源码先跑通,找一条完整链路从注册到发货走一遍,然后把订单、登录这两块核心代码自己默写一遍。这个过程花的时间并不多,但效果是质的区别——至少评委问起来,你能自然说出"这个字段为什么这么设计"。最后说一个我整理这套项目时最深的体会:这类中型业务系统的难度不在于某个技术点有多深,而在于把一堆状态的流转管理得足够清晰。用户有状态、订单有状态、商品有上下架状态,支付记录也有状态,当你把状态机理清楚之后,整个项目的代码结构会自然变得干净。如果你也选了或者准备选类似的线上商品交易题目,欢迎照着这套框架去搭;遇到卡住的地方再回头看一遍这篇里的踩坑记录,应该能帮你在调试路上省下不少时间。
看到"购买狗"这个名字,第一反应多半是"这是不是卖狗粮的"。其实这是我整理Java毕设方案时给内部原型起的代号,它对应的是一个线上游戏购买平台:用户注册登录后浏览游戏商品、按分类搜索、加入购物车、下单并模拟支付;管理员在后台维护商品、处理订单、完成发货。整套项目用Spring Boot 3 + MySQL 8 + MyBatis-Plus实现,配套内容涵盖完整源码、数据库SQL脚本、项目文档、调试记录和代码讲解。这篇文章不是贴一份通宵赶出来的代码,而是把选题判断、核心设计、重点实现和踩坑经验都摊开讲一遍——如果你正处在Spring Boot毕设选题的阶段,这个方向值得认真考虑。1. 这道题好在哪:从选题雷区到工作量拆解1.1 选题雷区:太简单和太复杂各有什么风险每年的毕设选题季,我见过太多同学在两个极端之间反复横跳。一个极端是选"图书管理系统""新闻发布系统"这类纯增删改查项目,听起来很稳,但做到中期就发现没东西可写,答辩时评委一句"你觉得这个项目最大的难点是什么"就能让全场沉默。另一个极端是选"高并发秒杀系统""双十一购物平台"这类大厂业务题,光分布式锁和消息队列就够研究一个月,最后要么草草收尾,要么变成"PPT演示系统"。线上游戏购买平台恰好落在中间区域。它和普通图书管理的区别在于,游戏属于数字商品,天然带有库存、销量、上架状态、多图展示、分类检索这些业务规则;同时它又不涉及物流跟踪、售后签收、满减优惠券这些会无限扩充边界的电商模块。一个"商品—购物车—订单—支付—后台发货"的完整业务闭环,正好能覆盖毕设评审最看重的几个维度:技术覆盖面够不够、业务逻辑有没有讲清楚、跑起来能不能演示。这里也说一个很多人没意识到的点:毕设项目的选题名字本身不重要,重要的是业务闭环是否完整。你完全可以把这个项目改名叫"某数字商品交易平台""线上道具商城",甚至"开源游戏商店",核心架构都不用动。名字叫"购买狗"还是"聚玩游戏商城",对你的得分影响约等于零。1.2 工作量盘点:表多少张、接口多少个、页面几页在决定要不要选这个题目之前,最好先对工作量有个量化概念。我这里按一个人、六周、每周投入15小时左右来拆解:数据库核心表:8张(用户、分类、游戏商品、商品图片、购物车、订单主表、订单明细、支付记录)后端接口:约30个(用户认证、商品浏览、购物车、订单、后台管理)前台页面:6个左右(首页、商品列表、商品详情、购物车、结算页、个人中心)后台页面:5个左右(登录、商品管理、分类管理、订单管理、统计面板)开发节奏可以参考这样排:前两周搭建工程骨架和用户模块,第三周做商品浏览和购物车,第四周集中啃订单和模拟支付,第五周完成后台管理,第六周统一测试、补文档、录演示视频、准备答辩。如果选的是前后端分离方案,再加一到两周的前端工作量,这个后面会详细说。有一点必须提醒:很多同学拿到现成源码后,第一反应是背代码。这在大一课程设计里也许能混过去,但毕设答辩会现场问思路、改需求、看调试过程,一旦接不上就很容易穿帮。正确的姿势是先把项目完整跑起来,然后挑三四个核心接口自己手写一遍,比如登录鉴权、下单扣库存、发货状态流转,其余部分能讲清楚流程就行。2. Spring Boot 3 + MySQL 8:技术选型逻辑与项目骨架2.1 为什么毕设不建议一上来就微服务每年都有学生问:"现在企业都在用微服务,我毕设是不是也应该上Spring Cloud?"我的答案一直是不建议。评审老师对毕设的判断标准是"学生有没有正确理解并实现一个完整的业务系统",而不是"这套架构商业上可不可行"。单体应用一样可以包含多个模块、清晰的层级边界和良好的代码组织;微服务带来的网关、注册中心、配置中心、链路追踪,任何一个组件出问题都会消耗大量的调试时间,而这些时间原本应该花在核心业务逻辑上。Spring Boot 3 + JDK 17是目前最合理的组合。Spring Boot 3基于Spring 6,自动配置机制相当成熟,内嵌Tomcat让打包部署变得异常简单,运行java -jar直接把整个后端拉起来。对演示场景来说,这种"一键启动"的可靠性比花哨的架构重要得多。数据库选MySQL 8没有悬念。本地开发默认utf8mb4字符集,中文存储不乱码;支持窗口函数,万一统计面板里需要"销售Top5"这类聚合查询,写起来也比较顺手。配合MyBatis-Plus,单表CRUD零SQL,复杂查询再写XML,开发效率和可维护性之间会有比较好的平衡。2.2 依赖配置与工程目录:照着搭能少走哪些弯路工程基础依赖建议这样配,注意Spring Boot 3对应的MyBatis-Plus要引入mybatis-plus-spring-boot3-starter,而不是老项目里常见的mybatis-plus-boot-starter,这是个很典型的版本坑:<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.5</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>application.yml里有几处配置值得提前说清楚。serverTimezone=Asia/Shanghai明确指定时区,否则很容器出现数据库时间和北京时间差8小时的问题;map-underscore-to-camel-case让数据库下划线字段自动映射成Java驼峰属性;逻辑删除的开关开好,后面删商品就可以走软删除而不用物理删行:server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/buy_dog?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 thymeleaf: cache: false servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0工程内部的目录结构,我建议按这样的包来组织:com.example.buydog ├── config -- 配置类:分页插件、静态资源映射、拦截器注册 ├── controller -- 接口层,只做参数接收和结果返回 ├── service -- 业务层,写核心业务逻辑 ├── mapper -- MyBatis-Plus Mapper接口 ├── entity -- 数据库对应实体 ├── dto -- 接口入参出参对象 ├── common -- 统一返回体、全局异常处理 └── utils -- 工具类一个非常实用的小原则是:Controller里不要写业务逻辑,业务逻辑也不要堆在Service接口里。代码讲解和答辩的时候,评审老师主要会看Service层的实现,一个干净的Service比什么都更有说服力。3. 功能模块与业务闭环:从注册登录到订单发货怎么串起来3.1 两套角色与登录认证方案取舍这个项目需要两类角色:普通用户和管理员。在数据层面不需要建两张用户表,一张user表加一个role字段就够了,1代表普通用户,2代表管理员。密码存储是答辩高频问题。一定不要用明文存密码,推荐用BCrypt做哈希,每次注册生成随机盐,即使数据库泄露,攻击者拿到的也是一串没办法直接还原的密文。Spring Boot的spring-security-crypto包里自带BCryptPasswordEncoder,直接注入使用就可以了。登录态这块,我建议优先考虑Session方案:用户登录成功后把用户ID放进Session,自定义一个LoginInterceptor拦截需要权限的请求。这样代码量少、理解难度低,非常适合毕设。如果选了前后端分离的Vue方案,再引入JWT,用Authorization请求头携带令牌。两种方案各有适用场景,非前后端分离就别硬上JWT,增加复杂度不说,演示时反而更容易出问题。3.2 商品浏览、购物车与重复加购处理首页和商品列表页是这个项目的门面。列表接口至少要支持三件事:按关键词模糊搜索、按分类筛选、分页。MyBatis-Plus的分页插件配置好之后,通过Page对象就能完成分页,SQL层面不需要手动拼LIMIT。购物车的数据结构不复杂,cart_item表里记录用户ID、游戏ID、数量、选中状态。这里要注意一个很容易被忽略的细节:用户把同一款游戏反复加进购物车时,不应该插入两条购物车记录,而应该做一次存在性检查——如果购物车里已经有这个商品,就把数量加1,否则才插入新记录。你可以把这个逻辑画成流程图写进文档里,算得上一个小亮点。购物车的选中状态也要考虑清楚。用户勾选几款商品下单,结算时只提交选中的商品ID列表,后台根据这些ID去取购物车数据、生成订单。有了checked字段,整个交互流程会真实很多,而不是简单粗暴地"清空购物车全部结算"。3.3 订单状态机、模拟支付与超时订单扩展订单是这个项目的灵魂,也是最值得在答辩时展开讲的部分。订单状态可以定义为五个:待支付已支付已发货已完成已取消流转关系为:待支付可以变成已支付,也可以变成已取消;已支付能变成已发货;已发货能变成已完成。每一步状态变更都在代码里做一个前置校验,不允许跳状态,比如"已支付"不能直接到"已完成",必须经过"已发货"。这种状态机的设计思路本身就很有含金量,即使没有用状态模式这样的设计模式,只要逻辑清晰,也能够在答辩时加分。下单的核心方法要开事务。一次下单至少涉及这几步:读取选中的购物车记录,往订单主表插一条订单信息,往订单明细表插入商品快照,扣减游戏库存,最后清空购物车对应记录。任何一个环节失败,都应该通过事务整体回滚,保证不会出现"订单生成了但库存没扣"这种脏数据。订单明细必须做商品快照,也就是把下单那一刻的商品名称、封面图、单价、数量原样保存到订单明细表里。这背后的原因是:管理员后续修改商品价格或名称时,历史订单不应该跟着变,否则对账会乱。我见过不少项目把订单详情关联到商品表去取价格,答辩时被问到"商品改名后订单里显示什么",直接卡住。快照这个设计要讲出来,很加印象分。模拟支付不要接真实支付渠道。一个是涉及商户资质和资金监管,毕设阶段搞这个合规风险太高;另一个是真实支付通常需要回调接口,流程设计调试成本都不低。正确做法是单独建一张payment_info支付记录表,提供一个"模拟支付成功"的接口,在接口里生成支付流水号、把订单状态从待支付改成已支付、记录支付时间。这一段要注意讲清楚你理解"真实支付会有异步回调,当前是Mock实现替代了这个环节",就可以把短板变成加分项。超时未支付属于扩展加分点。可以用Spring自带的@Scheduled定时任务,每分钟扫描一次超过15分钟仍未支付的订单,将状态改为已取消并把库存回补。这个功能不强求,但做了之后答辩效果会明显好一截,因为评委能看出你考虑到了真实业务里的异常情况。3.4 后台管理:商品、订单与统计面板后台管理模块的定位是把运营动作闭环起来。商品管理包括新增、编辑、上下架、修改库存、上传封面图和描述;分类管理是简单的增删改;订单管理主要做两件事:查看订单列表、对已支付订单执行发货操作。很多同学会漏掉一个管理端入口的权限控制。管理员接口必须校验当前登录用户的role是否为2,不能只依赖前端隐藏按钮。这里可以写一个独立的AdminInterceptor,拦截所有/api/admin/**路径,Session里的用户角色不对就返回无权访问。这个设计很小,但体现的是对权限模型的理解。统计面板可用可不用,但做了会很出彩。不需要复杂的BI报表,直接写两条SQL统计"今日新增订单数""近七天销售额趋势",甚至查询销售Top5的游戏商品列表,再配合ECharts画个折线图或柱状图就足够了。数据库只有8张表,统计查询的复杂度不高,但视觉效果和谈资都能提升一个档次。4. 数据库设计:从用户到支付这几张表的关键细节4.1 八张核心表的职责划分与字段要点我认为一个合格的毕设数据库设计,不在于表多,而在于每张表的职责足够清晰。下面这八张表基本可以撑起整个业务闭环,没有一张是多余的:表名职责关键字段user用户与管理员账号username、password、role、statusgame_category游戏分类name、sort、statusgame_info游戏商品主体category_id、name、price、stock、sales、statusgame_image商品详情图(一对多)game_id、image_url、sortcart_item购物车条目user_id、game_id、count、checkedorder_info订单主表order_no、user_id、total_price、statusorder_item订单明细快照order_id、game_id、game_name、price、countpayment_info支付流水payment_no、order_id、amount、status以game_info为例,建表语句可以参考这样:CREATE TABLE `game_info` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `category_id` BIGINT NOT NULL COMMENT '分类ID', `name` VARCHAR(100) NOT NULL COMMENT '游戏名称', `cover_img` VARCHAR(200) COMMENT '封面图路径', `price` DECIMAL(10,2) NOT NULL COMMENT '售价', `original_price` DECIMAL(10,2) COMMENT '原价,用于展示划线价', `stock` INT NOT NULL DEFAULT 0 COMMENT '库存', `sales` INT NOT NULL DEFAULT 0 COMMENT '累计销量', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `description` TEXT COMMENT '游戏介绍', `created_time` DATETIME, `updated_time` DATETIME, `deleted` TINYINT NOT NULL DEFAULT 0, KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='游戏商品表';订单主表和订单明细表建议拆开,而不是把商品信息直接冗余在订单主表里。这样一张订单可以对应多款游戏,明细表采用快照字段存储名称和价格,后续统计销售额、导出订单清单都很方便。4.2 订单号生成、库存扣减与逻辑删除的取舍订单号千万不要用数据库自增ID对外展示,这个习惯很不好。自增ID会暴露平台的销量规模,也容易被枚举遍历。自己生成一个业务订单号就可以了,我常用的格式是"时间戳+四位随机数",代码很短但覆盖了主要需求:public static String generateOrderNo() { String time = LocalDateTime.now() .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); int random = ThreadLocalRandom.current().nextInt(1000, 10000); return time + random; }库存扣减是整个项目里最值得展开讲的点。不要在Java代码里先查出库存数量,判断大于零后再执行更新,因为两个用户同时下单时会出现"都查到有库存,一起扣"的情况,最后库存变成负数。正确做法是把库存判断和扣减合到一条SQL里完成:<update id="deductStock"> UPDATE game_info SET stock = stock - #{count}, sales = sales + #{count} WHERE id = #{gameId} AND stock >= #{count} AND status = 1 </update>这条SQL非常关键:stock >= #{count}是原子条件,数据库行锁保证同一时间只有一个事务能更新成功,如果影响行数为0,说明库存不足或商品已下架,Java层直接抛业务异常回滚整个下单事务。答辩时能讲清楚这个细节,就已经超过一大半的毕设项目了。逻辑删除是MyBatis-Plus非常方便的功能,开启后deleteById会变成UPDATE ... SET deleted=1。但这会带来一个坑:唯一索引和逻辑删除字段会冲突。比如用户表对username建了唯一索引,逻辑删除后再次注册同名用户会插入失败,因为旧记录还在表里。处理方案有两种,要么把username改成普通索引,在Java层做唯一性判断;要么把deleted字段参与唯一索引。最省事的方案是毕设里大部分主数据表只做逻辑删除,但用户表直接用物理删除,并能向评委解释清楚"为什么这张表可以物理删除"。5. 高频踩坑与排查实录:时区、库存竞争、上传图片5.1 数据库时间差八小时:一次时区问题排查这个坑基本属于Spring Boot项目的"必答题"。现象是控制台打印的时间是正常的,但数据库里存进去的时间比当前时间慢了8个小时,或者查询出来的时间又比预期快了8小时。排查链路是这样的:先看数据库连接URL有没有配置serverTimezone,如果没有,MySQL驱动会按照服务器默认时区进行转换;再看MySQL全局时区,执行SELECT NOW()确认数据库当前时间;最后看Java实体里的时间字段类型。我的统一建议是:连接配置固定写死serverTimezone=Asia/Shanghai,Java实体层统一使用LocalDateTime,数据库中时间字段全部用DATETIME,三层统一之后800年的坑都能避开。5.2 库存被扣成负数:update语句加一个条件解决如果你完全按照第4.2节的方式写库存扣减,正常情况下不会出问题。但很多人早期版本是这样写的:GameInfo game = gameInfoMapper.selectById(gameId); if (game.getStock() >= count) { game.setStock(game.getStock() - count); gameInfoMapper.updateById(game); }问题在于"先查再更"不是一个原子操作。用一个简单的并发场景来演示:两个买家同时提交购买同一款库存只剩1的游戏,两边都查到stock=1,都认为库存足够,然后各自执行库存减一,最终数据库里的库存变成-1。这个坑在单机演示时很难复现,但答辩时评委只要口头问一句"怎么保证不超卖",这个回答就会决定整个项目的含金量。我在整理这个项目时,把修复版SQL直接写进Mapper层的XML里,并加了一个单元测试模拟两次并发调用,确认影响行数只有1后,这条才真正过关。修复手法就是前面展示的那条带stock >= #{count}条件的UPDATE语句。5.3 商品图片404:上传路径和静态资源映射本地开发时上传图片通常很正常,但把工程打成jar包运行后,图片经常莫名其妙变成404。根因是很多人用相对路径保存文件,比如images/upload/xxx.jpg,开发期IDEA的工作目录恰好指向项目根目录,所以一切正常;jar包运行后工作目录变了,图片自然找不到。稳妥做法是使用绝对路径,并在配置类里增加静态资源映射:@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadDir = System.getProperty("user.dir") + File.separator + "upload" + File.separator; File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadDir); } }这里还有一个小细节:路径拼接不要写死/或者\\,用File.separator生成平台无关的路径。Windows和Linux上表现不一致的问题,我都在这套源码里同步修正过了。如果你在演示时突然发现图片打不开,先去看配置文件里有没有做静态资源映射,大概率就是它。5.4 登录拦截器把样式和图片一起拦截了注册了登录拦截器之后,最常见的"诡异现象"是页面HTML能打开,但CSS、JS、图片全部加载失败,浏览器控制台一片403或401。原因不难理解:拦截器把所有请求都拦截了,静态资源路径没有放行。解决方式是在注册拦截器时显式排除静态资源路径:registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/login", "/register", "/css/**", "/js/**", "/images/**", "/upload/**", "/error" );这个细节看起来不起眼,但我实在见过太多人因为这个原因,在答辩演示的时候当场社死。尤其是页面样式全部丢失后,整个项目看起来就像没做完一样。提前把静态资源放行逻辑写清楚,能让演示稳定性提升不少。6. 答辩演示与交付:让评委快速认可项目含金量6.1 五分钟演示脚本:把核心状态变化跑给评委看演示环节最怕的是"埋头操作三分钟,评委啥也没看懂"。我整理这套项目时,准备了一个固定的演示节奏,总共控制在5分钟以内,每一步都在讲业务状态的变化:先打开数据库,快速展示八张核心表,说明订单主表和明细表的拆分思路,时间控制在1分钟。注册一个新用户,登录后浏览商品列表,演示搜索和分类筛选,并把一款游戏加入购物车,时间约1分钟。打开购物车,勾选商品并提交订单,刻意停留在待支付状态,让评委看到订单状态和支付记录未生成;然后调用模拟支付接口,再次展示订单变成已支付、支付表多了一条流水,时间约1分半。切换到管理员账号,进入订单管理,找到刚才那笔已支付订单,执行发货操作,再回到前台看到订单变成已发货。时间约1分钟。最后回到订单详情或统计面板,用一句话说明状态机的整体流转,结束演示。重点一定是"状态变化",因为评委看不到你写代码的过程,能看到的就是这些数据和页面状态的前后差异。把差异讲清楚,项目逻辑就自然清楚了。讲解核心代码的时候,不需要逐行念,挑createOrder这一个方法就够了。打开下单接口,现场说清楚事务边界、库存扣减条件、订单明细快照这三个点,一屋子人的疑问基本都能被覆盖。6.2 常见追问与建议回答口径我把这几年见过的评委高频问题整理成一张表,你可以对照准备,但不要背答案,关键是要理解背后的原理:问题建议思路为什么用Spring Boot而不是其他框架?自动配置减少繁琐XML,内嵌Tomcat方便部署,生态成熟,适合快速开发单体业务系统订单号为什么不用自增ID?自增ID暴露销量,容易被遍历;业务订单号用时间+随机数生成,不可推测怎么防止库存超卖?库存判断和扣减合并为一条UPDATE,利用数据库行锁保证原子性,影响行数为0则回滚密码为什么不存明文?使用BCrypt哈希加盐存储,密文不可逆,数据库泄露也不会直接暴露密码模拟支付和真实支付有什么区别?真实支付需要商户资质和异步回调机制,毕设用支付流水表模拟这个环节,但理解了回调的业务语义如果用户一直不支付怎么办?定时任务扫描超时待支付订单,自动取消并回补库存这个项目最大的难点是什么?不是某个接口的增删改查,而是订单状态的一致性和并发场景下库存的原子扣减6.3 整套交付里应该包含哪些内容一套完整可交付的毕设项目,不止是源码本身。我这边整理并调试过的配套内容大致包括这几块:后端完整源码(含全部页面代码)、初始化SQL脚本(建库建表加样例数据,样例数据尽量多放一些,演示效果差别很大)、项目说明文档(需求分析、数据库设计说明书、接口文档、部署运行说明)、演示录屏视频,以及面向代码讲解用的注释版核心方法。这里想多说一句关于"全包"资源的理解。所谓全包,在一份合格的交付物里应该是"帮你把环境跑通、把代码讲透、把原理捋顺",而不是让你背一份天书一样的代码去应付答辩。更务实的做法是:拿到的源码先跑通,找一条完整链路从注册到发货走一遍,然后把订单、登录这两块核心代码自己默写一遍。这个过程花的时间并不多,但效果是质的区别——至少评委问起来,你能自然说出"这个字段为什么这么设计"。最后说一个我整理这套项目时最深的体会:这类中型业务系统的难度不在于某个技术点有多深,而在于把一堆状态的流转管理得足够清晰。用户有状态、订单有状态、商品有上下架状态,支付记录也有状态,当你把状态机理清楚之后,整个项目的代码结构会自然变得干净。如果你也选了或者准备选类似的线上商品交易题目,欢迎照着这套框架去搭;遇到卡住的地方再回头看一遍这篇里的踩坑记录,应该能帮你在调试路上省下不少时间。