
简介一套基于Java和MySQL的校园二手书交易平台毕业设计完整资料包面向软件工程及相关专业本科生尤其适合正在准备毕业设计的同学。资源包含项目源码、高分论文、数据库脚本及文档说明覆盖需求分析、系统设计、功能实现与系统测试全流程有助于理解互联网技术对传统图书信息管理的改进并掌握Java后端与MySQL数据库在实际项目中的协作方式。包体共809个文件约25.09MB涉及Java源码、Vue前端、HTML、CSS、JavaScript、SQL脚本、Markdown说明等前后端分层明确便于按模块查阅同时含安装/运行脚本及大量图标、动图资源可直接用于本地部署和界面还原。附带的论文PDF与Word版本提供了完整写作框架、测试方法与参考文献适合作为毕业设计参考模板。目前已有82人学习下载可用于选题参考、系统复现或二次功能扩展。1. 为什么“校园二手书交易平台”是Java课设里性价比最高的选题每到课设季总有一批人卡在选题上。基于Java和MySQL的校园二手书交易平台属于那种看着不惊艳、但做起来最划算的题目用户、书籍、订单三个核心对象就能把CRUD、事务、关联查询全串起来难度刚好卡在“能做完、能讲清、能过答辩”的位置。这篇文章按我实际做过的方案把技术选型、数据库设计、核心业务代码、常见的翻车点和论文答辩的叙述技巧整理成一条可以直接照做的路线覆盖从拿到题目到提交论文的全过程。新手照着走能交差熟手也能从并发扣库存、事务边界这些细节里找到答辩加分点。2. 技术选型和工程骨架Spring Boot MyBatis MySQL 的配置细节2.1 Spring Boot 2.7 JDK 8 MyBatis为什么这套组合最省心常见的做法是直接上 Spring Boot不用传统的 Servlet JSP。Servlet 方案虽然学校可能教过但写一个带登录、发布、下单、搜索的项目Servlet 的代码量至少多三分之一而且很多学校的 Java Web 课已经不在课堂上讲 JSP 了。Spring Boot 2.7.x 配 JDK 8 是课设场景最稳妥的组合网上资料最多遇到问题基本都能搜到现成答案。Spring Boot 3.x 要求 JDK 17如果你的机器默认装了 JDK 8没必要为了版本折腾环境。ORM 层我推荐 MyBatis 而不是 Spring Data JPA。原因很简单答辩的时候老师大概率会问“这条 SQL 是怎么写的”“这个查询为什么慢”MyBatis 的 Mapper XML 里 SQL 就明明白白写在那边你能讲清楚每个条件、每个参数。JPA 虽然写起来快但自动生成的 SQL 绕了一层被追问时容易露怯。MyBatis 配合 mybatis-spring-boot-starter 使用依赖配置很少。数据库用 MySQL 8.0本地开发直接装 8.0 就好。要注意 8.0 的连接配置和 5.7 有差别具体坑在第 5 章展开。还有一个容易被忽略的点MySQL 服务装好后要确认系统服务里已经启动命令行里mysql -uroot -p能登进去再开始写代码不然后面所有报错都混在一起。2.2 目录结构controller、service、mapper 谁放什么工程结构决定了后面写代码顺不顺手。我一般会按这样的包结构组织com.example.secondhand ├── controller # 接收请求、返回结果 ├── service # 业务逻辑接口 │ └── impl # 业务逻辑实现 ├── mapper # MyBatis 数据访问接口 ├── entity # 数据库表对应的实体类 ├── config # 配置类拦截器、文件上传等 ├── common # 统一返回结果 Result、业务异常类 └── SecondhandApplication.javaController 只负责参数接收和结果返回业务逻辑写在 Service 里数据访问写在 Mapper 里。很多翻车的项目都是把所有代码堆在 Controller 里一个方法写两三百行问题在于第一事务注解不好加同一个类里方法互相调用事务会失效第二答辩时老师问你“业务层在哪”你说不出来。Service 接口加 impl 这种写法在课设里不算过度设计反而能体现分层意识。2.3 最小可运行配置application.yml 与 pom.xml 的关键参数依赖直接用 Spring Initializr 生成即可加上 mybatis starter 和 mysql driver 就够了。关键的配置在 application.yml 里server: port: 8080 servlet: encoding: charset: UTF-8 enabled: true spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/second_hand?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true这段配置有几个参数值得单独说。driver-class-name用com.mysql.cj.jdbc.Driver这是 8.0 驱动的新类名5.7 时代用的com.mysql.jdbc.Driver在新驱动里已过时。characterEncodingutf8解决中文乱码serverTimezoneAsia/Shanghai解决时间差 8 小时allowPublicKeyRetrievaltrue解决 MySQL 8.0 的认证握手问题——这三个参数缺一个就会踩第 5 章的某个坑。map-underscore-to-camel-case打开后数据库的create_time能自动映射到实体类的createTime少写很多 resultMap。mapper-locations指向 XML 文件目录约定放在 resources 下的 mapper 文件夹里。配置完成后启动项目看到 Tomcat 端口启动成功再用 Navicat 或命令行建好数据库跑一条select 1验证连接这一步通了再往下写业务代码。3. 数据库设计把“二手书交易”拆成 8 张表DDL 从哪写起3.1 核心表清单8张表各自管什么数据库设计是这类项目的灵魂。很多人的课设只建了用户表和书籍表订单随便放几个字段结果写代码时发现“我的订单”页面查不出历史记录。我把核心表拆成 8 张覆盖从登录到交易完成的完整链路表名中文名职责user用户表买家、卖家、管理员统一存放用 role 区分book书籍表书籍基本信息、价格、成色、上下架状态category分类表教材、小说、考试资料等分类cart购物车表收藏/暂存想买的书orders订单主表一笔订单的总金额、买家、卖家、状态order_item订单明细表订单里每本书的快照信息message留言表买家对书籍的咨询/议价留言notice公告表首页轮播或系统公告其中 category 和 notice 属于锦上添花的表不加也能跑通核心流程。但 orders 和 order_item 必须拆开这是后面讲“为什么拆”的重点。cart 表可以根据需求决定是否保留——如果你们的演示场景要求有“加入购物车”再下单就建如果直接“立即购买”可以去掉。3.2 user 表和 book 表的建表 DDL字段类型、默认值、注释都给你先看用户表注意密码字段不要明文存储MD5 或 BCrypt 都行演示项目用 MD5 就够了CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT MD5加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, phone varchar(20) DEFAULT NULL COMMENT 手机号, email varchar(100) DEFAULT NULL COMMENT 邮箱, role tinyint NOT NULL DEFAULT 0 COMMENT 0-普通用户 1-管理员, status tinyint NOT NULL DEFAULT 1 COMMENT 0-禁用 1-正常, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;字段类型值得展开说几点。整型用bigint和tinyint不用int也无所谓但 bigint 更保险。状态字段用tinyint配注释不要直接存字符串原因很简单Java 里拿到的就是一个 int代码里book.getStatus() 1比on_sale.equals(book.getStatus())干净得多且数据库里占的空间也小。时间字段用datetime而不是timestampdatetime 的范围更大不存在 2038 年问题课设答辩老师一般也不挑这个。书籍表是这个项目的核心表字段要一次设计到位CREATE TABLE book ( id bigint NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 书名, author varchar(50) DEFAULT NULL COMMENT 作者, publisher varchar(100) DEFAULT NULL COMMENT 出版社, isbn varchar(20) DEFAULT NULL COMMENT ISBN号, original_price decimal(10,2) DEFAULT NULL COMMENT 原价, price decimal(10,2) NOT NULL COMMENT 售价, condition_level tinyint DEFAULT NULL COMMENT 成色9-全新 8-九成新 7-七成新 6-有笔记, category_id bigint DEFAULT NULL COMMENT 分类ID, description text COMMENT 书籍描述, cover_image varchar(255) DEFAULT NULL COMMENT 封面图URL, seller_id bigint NOT NULL COMMENT 发布人ID, stock int NOT NULL DEFAULT 1 COMMENT 库存数量, status tinyint NOT NULL DEFAULT 1 COMMENT 0-待审核 1-在售 2-已下架 3-已售出, view_count int NOT NULL DEFAULT 0 COMMENT 浏览量, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 发布时间, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_seller (seller_id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT书籍表;价格字段一定要用decimal(10,2)不能用float或double。浮点数在二进制里存不精确0.1 加 0.2 会变成 0.30000000000000004虽然前端展示时看不出问题但涉及订单金额计算和Decimal比较就会出偏差。Java 端也要用BigDecimal接这个字段。stock加进去是为了第 4 章并发扣库存的演示一本书默认库存 1但可以加库存卖多本同款书。seller_id和category_id都要建索引因为列表页会按这两个字段筛选。3.3 orders 与 order_item为什么订单要拆成主表和明细表订单拆两张表不是凑表数是标准的“一主多明细”设计。主表存一次交易的整体信息谁买的、谁卖的、总金额、整体状态明细表存这次交易里具体买了哪几本书每本书当时的书名和价格。这样设计的直接好处是订单表不会因为书籍字段变更而影响历史数据。先看主表 DDLCREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号, buyer_id bigint NOT NULL COMMENT 买家ID, seller_id bigint NOT NULL COMMENT 卖家ID, total_amount decimal(10,2) NOT NULL COMMENT 总金额, status tinyint NOT NULL DEFAULT 0 COMMENT 0-待确认 1-已完成 2-已取消, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, pay_time datetime DEFAULT NULL COMMENT 付款/确认时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_buyer (buyer_id), KEY idx_seller (seller_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;order_no不要省略它有三个作用展示给用户看、作为业务唯一键、答辩时讲“系统订单号怎么生成”。生成规则我一般用时间戳 3位随机数比如BK20250112103045123够用但不算复杂。buyer_id和seller_id都要建索引因为“我买到的”和“我卖出的”两个页面分别按这两个字段查。明细表把下单那一刻的书名和价格冗余了一份CREATE TABLE order_item ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL COMMENT 订单ID, book_id bigint NOT NULL COMMENT 书籍ID, title varchar(100) NOT NULL COMMENT 下单时书名快照, price decimal(10,2) NOT NULL COMMENT 下单时价格快照, quantity int NOT NULL DEFAULT 1 COMMENT 数量, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;实名冗余 title 和 price 是故意的书籍表里的书名和价格随时可能被卖家修改如果订单明细直接关联 book 表用户翻开历史订单发现书名变了、价格变了体验很差。快照设计是课设答辩里的一个加分点可以主动讲给老师听。3.4 状态字段、时间字段和索引建表时就要想好的细节状态字段像book.status、orders.status这类我统一用tinyint并且在注释里把每个枚举值对应的含义写清楚。这样写 SQL 时where status 1你知道是“在售”别人看表结构也能直接读懂。不要用字符串枚举也不要一个状态存两个含义——比如有人把“已下单”和“已支付”合并成一个 1后面统计就不方便了。时间字段统一用datetimecreate_time都用DEFAULT CURRENT_TIMESTAMP自动填充update_time用ON UPDATE CURRENT_TIMESTAMP。这样插入时 Java 代码里就不用手动 set 时间MyBatis 的 insert 语句少两个字段。索引不要乱建。主键自动有聚簇索引外键关联字段建普通索引这是基本规则。像book.title这种字段虽然列表页会按书名模糊搜索但like %xxx%用不上索引建了也是白建。order_no建唯一索引因为它是业务唯一键。一个常见的错误是在status这种低区分度字段上建索引比如一张表就 3 种状态索引帮不上忙反而拖慢写入。提示在建表阶段就统一字符集为utf8mb4不要用默认的latin1。如果你用的是 Navicat 图形界面建库建库时记得选字符集为 utf8mb4否则后面改起来很麻烦。4. 核心业务代码发布书籍、下单扣库存、列表分页的落地写法4.1 发布书籍上传封面、插入记录的完整链路书籍发布是卖家这侧的核心入口。按分层写法Controller 接收表单参数和封面文件Service 组织业务逻辑Mapper 做最终插入。先看 Controller 这一段PostMapping(/book) public Result addBook(RequestParam(title) String title, RequestParam(price) BigDecimal price, RequestParam(categoryId) Long categoryId, RequestParam(value cover, required false) MultipartFile cover) { Book book new Book(); book.setTitle(title); book.setPrice(price); book.setCategoryId(categoryId); book.setSellerId(loginUser.getId()); book.setStatus(1); book.setStock(1); if (cover ! null !cover.isEmpty()) { String originalFilename cover.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName System.currentTimeMillis() suffix; cover.transferTo(new File(uploadDir fileName)); book.setCoverImage(/upload/ fileName); } bookService.addBook(book); return Result.success(book.getId()); }这一段有几个点需要说明。RequestParam接收表单字段前端 form 表单用enctypemultipart/form-data提交即可。loginUser从拦截器里塞进 ThreadLocal 或者 session 里拿实际项目建议写一个LoginUserHolder工具类统一存取避免每次在 Controller 里从 session 读再强转。封面文件名直接用System.currentTimeMillis()拼后缀避免中文文件名和重名问题但也意味着每次上传都会生成一个新文件不会覆盖旧文件。Service 层这里其实没太多业务规则重点在状态默认值status 1表示直接上架在售。如果作品要求有“管理员审核”环节就把默认值改成 0然后在后台管理页面加一个审核通过的更新操作。stock默认 1如果卖家用同一本书有多个副本可以在发布表单里加一个库存输入框。上传目录uploadDir在配置类里读 application.yml 中的自定义属性不要写死在代码里也不要存到项目 target 目录下否则重新打包会丢文件。比较稳的做法是配一个磁盘绝对路径外用 Nginx 或 Spring Boot 静态资源映射出去。4.2 下单一个事务里完成库存扣减和订单生成下单是整个项目里最值得花时间写清楚的地方也是答辩时老师最爱追问的“并发安全”问题所在。我要的效果是点击“立即购买”后库存扣减、订单主表插入、订单明细插入、书籍状态更新这四件事要么全部成功要么全部失败。Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long bookId, Long buyerId) { // 1. 查询书籍校验状态和卖家 Book book bookMapper.selectById(bookId); if (book null) { throw new BusinessException(书籍不存在); } if (book.getSellerId().equals(buyerId)) { throw new BusinessException(不能购买自己发布的书籍); } if (book.getStatus() ! 1) { throw new BusinessException(这本书已下架或已售出); } // 2. 乐观锁扣减库存update ... where stock 0 int rows bookMapper.deductStock(bookId); if (rows 0) { throw new BusinessException(手慢了这本书已被买走); } // 3. 检查扣减后的库存如果扣到 0 则更新状态为已售出 Book latest bookMapper.selectById(bookId); if (latest.getStock() 0) { bookMapper.updateStatus(bookId, 3); } // 4. 生成订单号并插入主表 String orderNo BK System.currentTimeMillis() ThreadLocalRandom.current().nextInt(1000, 9999); Order order new Order(); order.setOrderNo(orderNo); order.setBuyerId(buyerId); order.setSellerId(book.getSellerId()); order.setTotalAmount(book.getPrice()); order.setStatus(0); orderMapper.insert(order); // 5. 插入订单明细冗余书名和价格快照 OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setBookId(bookId); item.setTitle(book.getTitle()); item.setPrice(book.getPrice()); orderItemMapper.insert(item); OrderVO vo new OrderVO(); vo.setOrderNo(orderNo); vo.setTotalAmount(book.getPrice()); return vo; }这段代码的关键在于第 2 步的扣库存 SQL。对应的 Mapper 语句是update iddeductStock update book set stock stock - 1 where id #{id} and stock 0 /update这个写法的核心逻辑是把“先查库存再扣库存”两步合并成一条带条件的 update。并发场景下两个请求同时读到库存为 1同时执行这条 updateMySQL 的行锁保证只有一条能更新成功另一条的stock 0条件不成立影响行数为 0于是抛异常回滚事务。这样就不会出现“超卖”——库存扣成负数的情况。相比select for update的悲观锁这种方式在扣减场景更轻也不容易死锁。Transactional(rollbackFor Exception.class)这个注解必须加在 public 方法上并且注意调用方式。如果是在 Controller 里直接调用这个 service 方法没问题但如果是在 Service 内部另一个方法里this.createOrder()这样调用事务会失效原因在第 5 章讲。还需要注意bookMapper.deductStock(bookId)返回的是受影响行数 int而不是 boolean判断时用 0而不是 false。ThreadLocalRandom.current().nextInt(1000, 9999)生成三位随机数配合毫秒级时间戳基本不会重复order_no的唯一索引会兜底。4.3 列表查询分页、关键字、分类筛选的一条SQL首页和列表页是买家看到最多的页面。常见需求是按分类筛选、按关键字搜书名或作者、按价格排序、分页展示。我习惯手写分页而不是引入 PageHelper因为手写能让自己搞清楚 limit 和 count 的配合答辩被问到也能讲明白。select idselectBookPage resultTypecom.example.secondhand.entity.Book select id, title, author, price, cover_image, stock, status, create_time from book where if testtitle ! null and title.trim() ! and (title like concat(%, #{title}, %) or author like concat(%, #{title}, %)) /if if testcategoryId ! null and category_id #{categoryId} /if if teststatus ! null and status #{status} /if /where choose when testsort price_asc order by price asc /when when testsort price_desc order by price desc /when otherwise order by create_time desc /otherwise /choose limit #{offset}, #{pageSize} /selectwhere标签会自动去掉多余的 and这是 MyBatis 动态 SQL 的标准做法。注意这里的 title 参数同时匹配作者字段这是个低成本的小优化买家搜“高等数学”时即使只记得作者名也能找到。排序用choose实现默认按发布时间倒序价格升序和降序分别处理不能让用户传入的排序字符串直接拼进 SQL否则就是 SQL 注入点。分页参数offset和pageSize从 Java 端算好再传进来规则是offset (pageNum - 1) * pageSize。对应的 count 查询复制同样的where条件把 select 字段换成count(*)不需要排序和 limit。这两个查询一个取值一个取总量配合返回给前端就能算出总页数。注意like 条件里的concat(%, #{title}, %)要写在 XML 里而不是 Java 里拼好再传。前者是预编译参数后者等于把用户输入直接拼 SQL存在注入风险。面试或答辩时被问到“MyBatis 怎么防注入”这就是标准答案。5. 避坑记录连接、乱码、时区、事务失效和并发扣库存的 5 个现场5.1 MySQL 8.0 连接报错 Public Key Retrieval is not allowed现象项目启动或第一次查询时报SQLNonTransientConnectionException重点是Public Key Retrieval is not allowed这段英文。原因MySQL 8.0 默认的认证插件是caching_sha2_passwordJDBC 驱动需要获取服务器的公钥来完成加密握手。在不安全的非 SSL 连接下驱动默认不允许自动获取公钥于是直接抛异常。这个问题在新装 MySQL 8.0 的环境里几乎必现。解决在连接 URL 后面加allowPublicKeyRetrievaltrueuseSSLfalse。配合serverTimezoneAsia/Shanghai和characterEncodingutf8这组参数是 8.0 连接的标准配置。如果用了 MySQL 5.7驱动类名用com.mysql.cj.jdbc.Driver也是可行的但要确认 MySQL 驱动 jar 是 8.x 版本。还有一个连带坑如果在 porm.xml 里引入了旧版 5.x 驱动即使 URL 配对了也会报别的错建议直接依赖mysql-connector-j8.0.x。5.2 中文乱码表、连接、IDE 三处要对齐现象发布书籍时输入中文书名数据库里存成???页面回显也乱码。原因三层的问题。第一层建库时字符集不是 utf8mb4第二层JDBC 连接 URL 少了characterEncodingutf8第三层有人遇到控制台打印中文乱码其实是 IDE 终端编码问题跟数据库无关。解决按顺序排查。先查表结构show create table book;看 DEFAULT CHARSET 是不是 utf8mb4不是就 ALTER 改掉。再检查 URL 参数确认有characterEncodingutf8。最后如果数据库里存的是正常中文但 IDEA 控制台乱码改 IDEA 的Help - Edit Custom VM Options加-Dfile.encodingUTF-8重启即可。这个坑的麻烦在于三层都可能出问题但表现都是“中文不对”所以要一层一层排。5.3 数据库时间和本地时间差 8 小时现象发布书籍后列表页显示创建时间比本地北京时间慢了 8 个小时。原因MySQL 驱动读取数据库时间时会按连接指定的时区解释。如果 URL 里没有serverTimezoneAsia/Shanghai驱动默认取 MySQL 服务器的系统时区。很多 MySQL 实例部署在 Docker 容器里默认时区是 UTC存进去的时间是 UTC读出来自然差 8 小时。解决最快的方式是在连接 URL 里显式指定serverTimezoneAsia/Shanghai。还有一个连带方案建表时create_time用DEFAULT CURRENT_TIMESTAMP让 MySQL 服务器自己写当前时间Java 端不用手动 new Date 再 set。如果用了 Docker 跑 MySQL启动容器时加-e TZAsia/Shanghai也能根治。这个坑属于那种“查了半天代码没问题最后发现是环境问题”的典型建议配置里一次配好。5.4 Transactional 不生效订单主表有数据、明细表没有现象下单接口调用后orders 表有了记录order_item 表和库存扣减却没执行。检查代码方法确实加了Transactional。原因Spring Boot 的事务靠 AOP 代理实现。如果事务方法被同类内部的另一个方法通过this调用代理不生效——它拦截的是外部对代理对象的调用this调用绕过了代理。另一种常见原因是方法里 catch 了异常没有重新抛出Spring 看到没有异常抛出就认为事务正常提交了。解决保证事务方法从外部调用让 Controller 直接调用 Service 的公开方法不要在同类的普通方法里this.createOrder()。如果确实需要内部调用两种方案把事务方法拆到另一个 Service 类里注入进来调用或者Autowired注入自身后通过代理调用。另外注意 catch 块里要throw new BusinessException(...)并且Transactional(rollbackFor Exception.class)要指定这个参数因为默认只回滚 RuntimeExceptionchecked exception 不回滚。5.5 并发把库存减成负数扣减条件怎么写才安全现象两个用户同时点“立即购买”同一本库存只有 1 的书两个订单都创建成功了book 表的 stock 字段变成 -1。原因这是“先查后改”的经典并发问题。代码里先selectById拿到库存判断大于 0再执行update set stock stock - 1。两步之间存在时间窗口两个请求都完成了第一次查询都认为库存是 1都执行了扣减于是库存变成 -1。解决把扣减动作合并成一条带条件的 updateupdate book set stock stock - 1 where id ? and stock 0。这条 SQL 在 MySQL 里执行时会对命中行加锁第二个请求要等第一个请求提交后才能执行此时stock 0已经不成立影响行数为 0代码里判断rows 0就抛业务异常。第 4 章的下单代码已经按这个思路写了可以直接用。这个点答辩时值得主动讲因为能体现“考虑到并发”的意识。6. 论文与答辩把项目讲成“高分论文”的收尾技巧6.1 论文里最值得写的三个地方论文写作有一个容易被忽略的事实老师评高分看的不是你写了多少字而是图的完整性和技术的解释力。三个地方值得多花篇幅。第一是需求分析和用例图把买家、卖家、管理员三个角色各画一张用例图不用画太复杂教科书风格即可。第二是数据库设计把第 3 章的 8 张表全部贴进论文每张表配字段说明表格再加一张 ER 图。第三是核心功能实现选“下单”这一个模块展开写流程图和核心代码重点解释为什么用条件更新扣库存这段解释就是和普通课设拉开差距的地方。论文结构按“选题背景、需求分析、概要设计、详细设计、系统实现、系统测试、总结”走这是最稳妥的模板。系统测试部分不要只写“测试通过”列一张功能测试表格填测试用例、预期结果、实际结果再补一段并发扣库存的测试描述说明用两个线程同时下单验证库存不会变负。6.2 演示和答辩要提前演练的三个场景演示顺序我建议固定成一条链路注册登录、发布一本新书、首页搜索这本书、下单购买、在“我买到的”里看到订单。这条链路走一遍核心功能都覆盖了。要特别留意三件事提前确认 MySQL 服务是启动的。很多现场翻车都发生在最后一刻数据库没起来项目一启动就报连接错误。准备一个干净的数据库脚本把建库、建表、初始数据放在一个 SQL 文件里演示前执行一遍不要用以前测试过的脏数据。答辩问答提前准备三个最可能被问的问题为什么不用 JPA 要用 MyBatis并发场景下怎么保证库存不超卖数据库索引建在哪些字段上。这三个问题在前面章节都有对应内容能口头讲清楚就足够应付了。我自己的教训是最后一天一定要完整走一遍“从拉代码到演示成功”的流程连数据库初始化和配置文件都重来一遍别在演示的时候凭记忆操作。设备换了一台环境变量、MySQL 密码这些细节全部要重来。一次靠谱的演示胜过一万字的论文描述希望帮到你。本文还有配套的精品资源点击获取