简介:这份资源是面向高校计算机相关专业学生的Java Web课程设计完整方案,以校园二手图书交易系统为主题,适合正在准备课设、大作业或需要Java Web实战练手的同学参考。项目源码经本地编译调试,可正常运行,评审得分在95分以上,难度适中,内容经助教老师审定,能满足学习与使用需求。压缩包共510个文件,约28.32MB,涵盖118个Java源文件、136个XML配置、9个SQL脚本及Vue前端组件、图片资源与编译产物等,前后端结构完整,数据库脚本齐备。已有119人学习下载。读者可从中获得一套可直接运行的校园二手图书交易系统实现,包括用户、图书、订单等核心业务模块的代码组织方式与数据库设计思路,便于对照理解Java Web项目的分层架构、接口编写与前端页面交互,也可作为课设答辩与二次开发的基础素材。
1. 从零到一:校园二手图书交易系统到底在做什么
每年毕业季,宿舍楼下堆成小山的教材和考研资料,最后大多按斤卖给了废品站。与此同时,低年级学生开学第一周就在各个群里刷屏求购《数据结构》《计算机网络》的二手书。这个供需错配的场景,就是校园二手图书交易系统要解决的核心问题。它不是一个简单的商品列表页,而是一套包含用户认证、图书发布、分类检索、订单流转、站内消息的完整 Javaweb 应用。技术栈上,常见做法是 Servlet + JSP + MySQL 或者 Spring Boot + MyBatis + Thymeleaf,前者适合课程设计展示底层原理,后者更贴近企业开发流程。这篇文章会从数据库表结构设计一路讲到部署排错,把我在带课设和做外包时踩过的坑都摊开说清楚。如果你正在找一个能写进简历、能跑起来、能讲明白的 Javaweb 项目,这个方向值得认真做一遍。
2. 数据库表结构设计:七张表撑起整个交易闭环
2.1 为什么是七张表而不是五张
很多同学拿到课设题目第一反应是打开 Navicat 直接建表,结果做到一半发现订单状态没法追溯、图书分类改不了、用户信誉没地方存。我一般会先把业务动作列出来:注册登录、发布图书、浏览搜索、下单、确认收货、评价、举报下架。每个动作背后至少对应一张表或者一个字段。
核心表清单如下:
| 表名 | 作用 | 关键字段 | 预估数据量 |
|---|---|---|---|
| user | 用户信息 | user_id, username, password_hash, phone, credit_score | 500-2000 |
| book | 图书主表 | book_id, title, author, isbn, category_id, price, status | 2000-5000 |
| category | 分类字典 | category_id, name, parent_id, sort_order | 20-50 |
| order | 订单主表 | order_id, buyer_id, seller_id, total_amount, status | 1000-3000 |
| order_item | 订单明细 | item_id, order_id, book_id, price | 1000-3000 |
| message | 站内消息 | msg_id, from_user, to_user, content, is_read | 5000+ |
| review | 评价记录 | review_id, order_id, rating, comment | 500-1500 |
注意book表里的status字段,它决定了图书是「在售」「已预订」「已售出」还是「已下架」。这个字段的设计直接影响到并发下单时的锁策略。我见过太多课设把状态硬编码在 JSP 页面里用 if-else 判断,后期加一个「预订」状态就要改十几个文件。
2.2 建表 SQL 与索引策略
下面是我在 MySQL 8.0 上验证过的建表语句,字符集统一用utf8mb4,排序规则utf8mb4_0900_ai_ci:
CREATE TABLE `book` ( `book_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `title` VARCHAR(120) NOT NULL COMMENT '书名', `author` VARCHAR(60) DEFAULT NULL, `isbn` VARCHAR(20) DEFAULT NULL, `category_id` INT UNSIGNED NOT NULL DEFAULT 1, `price` DECIMAL(8,2) NOT NULL DEFAULT 0.00, `original_price` DECIMAL(8,2) DEFAULT NULL, `cover_img` VARCHAR(255) DEFAULT NULL, `description` TEXT, `seller_id` BIGINT UNSIGNED NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0在售 1预订 2售出 3下架', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`book_id`), KEY `idx_category_status` (`category_id`, `status`), KEY `idx_seller` (`seller_id`), KEY `idx_title_prefix` (`title`(20)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='二手图书主表';逻辑说明:idx_category_status是复合索引,因为最高频的查询是「某个分类下所有在售图书」,把category_id放左边、status放右边,能直接命中索引。idx_title_prefix只对书名前 20 个字符建索引,因为校园场景下搜索关键词很少超过 20 字,前缀索引能省一半以上的索引空间。
参数说明:DECIMAL(8,2)表示最大 999999.99,二手书价格不会超过这个数。status用TINYINT而不是ENUM,因为ENUM加值要改表结构,TINYINT配合 Java 枚举更灵活。
2.3 订单状态流转的字段设计
订单表最容易翻车的地方是状态字段。我见过用is_paid、is_shipped、is_received三个布尔字段的,结果要查「所有待发货订单」就得写WHERE is_paid=1 AND is_shipped=0,一旦业务加一个「退款中」状态就彻底崩了。
正确做法是用单一status字段配合状态机:
CREATE TABLE `order` ( `order_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '业务订单号', `buyer_id` BIGINT UNSIGNED NOT NULL, `seller_id` BIGINT UNSIGNED NOT NULL, `total_amount` DECIMAL(10,2) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待付款 1已付款 2已发货 3已完成 4已取消 5退款中', `remark` VARCHAR(200) DEFAULT NULL, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `pay_time` DATETIME DEFAULT NULL, `finish_time` DATETIME DEFAULT NULL, PRIMARY KEY (`order_id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_buyer_status` (`buyer_id`, `status`), KEY `idx_seller_status` (`seller_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;order_no用唯一索引而不是主键,是因为业务订单号需要暴露给前端展示,而自增主键暴露出去容易被遍历。生成规则常见做法是「日期 + 用户ID后四位 + 随机数」,在 Java 层用String.format拼接即可。
3. 后端接口实现:从 Servlet 到 Spring Boot 的选型与落地
3.1 两种技术路线的取舍
课设题目写的是「基于 Javaweb」,这个表述其实很宽泛。我一般会先问自己:这个项目是要展示对 HTTP 协议和 Servlet 容器的理解,还是要展示工程化能力?如果是前者,用原生 Servlet + JSP + JDBC 手写,每个请求都走doGet/doPost,能清楚看到HttpServletRequest和HttpServletResponse的原始面貌。如果是后者,直接上 Spring Boot + MyBatis-Plus,用注解把 SQL 和 Java 方法绑定,开发效率高很多。
我的建议是:如果时间充裕(两周以上),用 Servlet 写核心的图书发布和订单模块,用 Spring Boot 写消息和评价模块,两者通过同一个数据库连接。这样既展示了底层原理,又体现了对主流框架的掌握。如果只有一周,直接 Spring Boot 全家桶,把精力花在业务逻辑和边界处理上。
3.2 图书发布接口的完整实现
以 Spring Boot 为例,图书发布接口需要处理表单上传、图片存储、参数校验三件事。下面是一个可运行的 Controller 方法:
@PostMapping("/book/publish") @ResponseBody public Result publishBook(@Validated BookPublishDTO dto, @RequestParam("cover") MultipartFile cover, HttpSession session) { // 1. 从 session 取当前登录用户 User user = (User) session.getAttribute("loginUser"); if (user == null) { return Result.fail(401, "请先登录"); } // 2. 校验图片类型和大小 if (cover != null && !cover.isEmpty()) { String contentType = cover.getContentType(); if (!"image/jpeg".equals(contentType) && !"image/png".equals(contentType)) { return Result.fail(400, "仅支持 JPG/PNG 格式"); } if (cover.getSize() > 2 * 1024 * 1024) { return Result.fail(400, "图片不能超过 2MB"); } } // 3. 保存图片到本地磁盘,生成访问路径 String coverPath = null; if (cover != null && !cover.isEmpty()) { String fileName = UUID.randomUUID() + ".jpg"; File dest = new File(uploadDir, fileName); try { cover.transferTo(dest); coverPath = "/upload/" + fileName; } catch (IOException e) { log.error("图片保存失败", e); return Result.fail(500, "图片上传失败"); } } // 4. 组装实体并入库 Book book = new Book(); book.setTitle(dto.getTitle()); book.setAuthor(dto.getAuthor()); book.setPrice(dto.getPrice()); book.setCategoryId(dto.getCategoryId()); book.setSellerId(user.getUserId()); book.setCoverImg(coverPath); book.setStatus(0); bookService.save(book); return Result.ok(book.getBookId()); }逻辑说明:第一步从 session 取用户,这是最朴素的登录态校验,比 JWT 简单但足够课设使用。第二步校验图片类型和大小,防止上传可执行文件或者超大文件打满磁盘。第三步用 UUID 重命名文件,避免中文名和重复名问题。第四步入库时status固定为 0(在售)。
参数说明:uploadDir建议配置在application.yml里,不要硬编码。@Validated配合 DTO 上的@NotBlank、@Min注解做参数校验,比在方法里写一堆 if 干净得多。
3.3 搜索接口的 SQL 优化
校园二手书的搜索场景很特殊:用户输入的关键词往往很短(「高数」「线代」「408」),而且对排序不敏感,但对响应速度敏感。我一般用LIKE前缀匹配而不是全文索引,因为 MySQL 的ngram分词器对中文短词效果一般,配置还麻烦。
SELECT book_id, title, price, cover_img, seller_id FROM book WHERE status = 0 AND title LIKE CONCAT(#{keyword}, '%') ORDER BY create_time DESC LIMIT 20 OFFSET #{offset}注意LIKE CONCAT(#{keyword}, '%')是前缀匹配,能命中idx_title_prefix索引。如果写成LIKE CONCAT('%', #{keyword}, '%'),索引直接失效,5000 条数据就能感觉到卡顿。这是血泪经验,面试官也爱问。
4. 前端页面与交互:JSP 和 Thymeleaf 怎么选
4.1 JSP 的九个内置对象在课设里的实际用途
如果走 Servlet + JSP 路线,request、session、application这三个对象必须吃透。request用来传单次请求的数据,比如图书详情;session存登录用户和购物车;application存全局配置比如分类列表。我见过把分类列表存在 session 里的,每个用户一份,内存直接爆炸。
一个典型的图书列表 JSP 片段:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>二手书列表</title></head> <body> <c:forEach items="${bookList}" var="book"> <div class="book-card"> <img src="${pageContext.request.contextPath}${book.coverImg}" alt="封面"> <h3>${book.title}</h3> <p>作者:${book.author}</p> <p class="price">¥${book.price}</p> <a href="${pageContext.request.contextPath}/book/detail?id=${book.bookId}">查看详情</a> </div> </c:forEach> </body> </html>${pageContext.request.contextPath}用来拼上下文路径,避免部署到 Tomcat 后路径不对。<c:forEach>是 JSTL 标签,比在 JSP 里写 Java 代码块干净得多。
4.2 Thymeleaf 的天然优势与一个隐藏坑
Spring Boot 官方推荐 Thymeleaf,最大好处是页面可以直接用浏览器打开预览,不依赖容器。但有一个坑:Thymeleaf 默认会缓存模板,开发阶段改了 HTML 刷新页面看不到变化。必须在application.yml里关掉:
spring: thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html encoding: UTF-8cache: false只在开发环境设,生产环境要改回true,否则每次请求都读磁盘,QPS 上不去。
5. 避坑与排查:课设答辩前必须过的五道关
5.1 中文乱码:现象是书名变成问号,原因是字符集不统一
现象:图书发布后,列表页显示「?????」,数据库里用SELECT看也是乱码。
原因:三个环节的字符集不一致——JSP 页面pageEncoding、Tomcat 的URIEncoding、MySQL 的character_set_server。常见做法是全部统一为UTF-8。
解决:JSP 头部加<%@ page contentType="text/html;charset=UTF-8" %>;Tomcat 的server.xml里<Connector>加URIEncoding="UTF-8";MySQL 建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci。如果是 Spring Boot,在application.yml的 JDBC URL 后面加?useUnicode=true&characterEncoding=utf8。
5.2 数据库连接池耗尽:现象是页面卡死,原因是 Connection 没关
现象:连续点几次图书列表,页面转圈然后报 500,日志里一堆Timeout waiting for idle object。
原因:在 Servlet 或 DAO 里手动DriverManager.getConnection之后忘了close(),连接池里的连接被占满。
解决:用try-with-resources语法,把Connection、PreparedStatement、ResultSet都放在 try 的括号里。如果用的是 Spring 的JdbcTemplate或 MyBatis,框架会自动关,但手动写 JDBC 时必须自己管。我一般会在 DAO 层加一个finally块兜底。
5.3 订单重复提交:现象是同一本书生成两笔订单,原因是没做幂等
现象:用户手快点了两次「立即购买」,数据库里出现两条order记录,库存扣了两次。
原因:前端没有防重复提交,后端也没有幂等校验。
解决:前端在点击后禁用按钮;后端在order表加唯一索引UNIQUE KEY uk_buyer_book (buyer_id, book_id, status),或者在 Redis 里用SETNX加锁。课设环境没有 Redis 的话,用数据库唯一索引最简单粗暴。
5.4 图片上传后访问 404:现象是封面裂图,原因是虚拟路径没配
现象:图片明明上传到了/upload/目录,但浏览器访问http://localhost:8080/upload/xxx.jpg返回 404。
原因:Tomcat 或 Spring Boot 没有把磁盘目录映射为静态资源路径。
解决:Spring Boot 里加一个配置类:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); } }file:前缀表示磁盘路径,user.dir是项目运行目录。注意路径末尾的斜杠不能少,否则拼接会出错。
5.5 答辩时被问「并发下单怎么处理」:现象是答不上来,原因是没想过
现象:老师问「如果两个人同时买同一本书怎么办」,你只能说「加锁」,但说不清加什么锁。
原因:课设通常不涉及高并发,但答辩一定会问。
解决:标准回答分三层——数据库层面用UPDATE book SET status=1 WHERE book_id=? AND status=0,根据affectedRows判断是否抢到;应用层面用synchronized或ReentrantLock按book_id加锁;分布式层面用 Redis 或 ZooKeeper。课设答到第二层就够,但要知道第三层的存在。
6. 让课设拿高分的三个进阶技巧
第一个技巧是给搜索加一个「按成色筛选」的维度。在book表加一个condition_level字段(1-5 星),前端用滑块选择,后端 SQL 加一个AND condition_level >= #{minLevel}。这个功能实现成本极低,但答辩时能讲出「用户画像」和「个性化推荐」的延伸,老师会觉得你有产品思维。
第二个技巧是用EXPLAIN验证你的索引。在 MySQL 命令行里执行EXPLAIN SELECT * FROM book WHERE category_id=3 AND status=0,看type列是不是ref,key列是不是idx_category_status。如果是ALL,说明索引没生效,检查字段类型是否一致。这个动作在答辩时演示一遍,比说一百句「我做了优化」都有用。
第三个技巧是准备一份「如果重做我会怎么改」的清单。比如「我会把图片存到对象存储而不是本地磁盘」「我会用 JWT 替代 session 做无状态登录」「我会把订单状态机抽成独立服务」。这份清单不用真的实现,但能展示你对技术边界的认知。我带过的学生里,凡是答辩时能主动说出自己方案局限性的,分数没有低于 90 的。
最后一个习惯:每次改完代码,先git commit再运行。课设周期短,改崩了想回滚是常有的事,没有版本控制就只能靠记忆重写。这个习惯我从第一次做课设保持到现在,希望帮到你。
本文还有配套的精品资源,点击获取