
简介面向计算机科学与技术等专业的 Java Web 课程设计场景这份网上书店后台管理系统文档提供了较完整的选题参考与实现思路。内容围绕基于 B/S 架构的管理系统展开覆盖课程设计任务书、需求分析、模块结构、流程图以及注册登录、购物车、后台登录、用户管理、图书管理等核心功能并涉及 JSP、Servlet、JavaBean 与 MySQL 的配合使用。包内共 1 个 doc 文件压缩包约 845KB属于文档资料类型适合在撰写任务书、设计数据库表和整理报告时对照查阅。文中还给出 userdetail、books 等数据表字段设计及部分页面代码片段可帮助读者理解用户权限、图书库存与价格管理的基本实现路径。目前已有 548 人学习对需要完成同类课程设计、快速形成系统框架与文档结构的学生具有一定参考价值。1. 从课程设计到能跑起来java-web 网上书店系统先要迈过的坎课程设计答辩前一晚最尴尬的不是功能少而是 Tomcat 起来了、首页打得开点「加入购物车」就抛 500。java-web 网上书店系统这个题目看着老坑却集中得很典型Servlet 生命周期没理清、JDBC 连接忘了关、POST 中文变问号、下单扣库存没加事务任何一条没处理演示当场翻车。下面按一个人从零把小型网上书店系统做到能演示的顺序写先定分层和数据表再用能抄的代码跑通图书检索、购物车、下单三个闭环然后处理并发扣库存、乱码、连接池、日志排查这些真正卡人的参数最后落到怎么把它做成拿得出手的作品。适合正在做 java-web 课程设计的人也适合想回头复习 Servlet JDBC 这套老底子的在职工程师。2. 网上书店系统的分层与数据模型怎么定选型阶段最容易犯的错是「先写页面表后面再补」。小型网上书店系统的实体关系并不复杂但一旦表结构定歪后面订单和库存的代码会反复返工。这一章把分层边界、表结构、连接管理三件事一次说清。2.1 java-web 分层怎么切Servlet、Service、DAO 的边界我见过最多的课程设计写法是把 JDBC 调用直接塞在doPost里十几行参数解析混着 SQL 拼串最后再来一句response.sendRedirect。这种写法跑一个「图书列表」没问题可只要加一个「按分类 关键字 价格区间」的筛选就得在 Servlet 里改一大坨。更稳的分层是四包结构com.bookstore.webServlet只负责取参数、调 Service、把结果放进 request 或输出 JSONcom.bookstore.service业务规则比如库存够不够、订单能不能取消、事务边界在哪com.bookstore.dao只碰 SQL 和 ResultSet 映射不做业务判断com.bookstore.entity/util实体类和DBUtil、PageResult这类工具WebServlet(/book/list) public class BookListServlet extends HttpServlet { // Servlet 里只持有 Service 接口方便以后换实现或写单元测试 private final BookService bookService new BookServiceImpl(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 参数解析只出现在这一层Service 不认识 HttpServletRequest String kw req.getParameter(kw); int page parsePositiveInt(req.getParameter(page), 1); int size parsePositiveInt(req.getParameter(size), 10); PageResultBook result bookService.search(kw, page, size); req.setAttribute(pageResult, result); req.getRequestDispatcher(/WEB-INF/jsp/book_list.jsp).forward(req, resp); } private int parsePositiveInt(String raw, int defaultValue) { if (raw null || raw.trim().isEmpty()) return defaultValue; try { int v Integer.parseInt(raw.trim()); return v 0 ? v : defaultValue; // 负数和 0 一律回落到默认值 } catch (NumberFormatException e) { return defaultValue; // 前端传了脏数据也不能让页面 500 } } }逻辑说明parsePositiveInt看着啰嗦但分页参数是最容易被手工改地址栏改坏的地方page-1会让LIMIT直接抛 SQL 异常。把它下沉成一个私有方法比在每个 Servlet 里写 try-catch 干净得多。参数说明page从 1 开始转成 SQL 偏移量时是(page - 1) * sizesize上限建议卡在 50 以内否则一个size100000的请求就能把内存吃满。JSP 放WEB-INF下面浏览器无法直接访问所有页面必须走 Servlet 转发。提示Service 接口的参数里一旦出现HttpServletRequest这套分层就白做了单元测试时你没法在没有容器的情况下构造它。2.2 网上书店系统的数据表设计与建表 SQL实体不多但字段类型要一次定对。金额用DECIMAL(10,2)别用FLOAT否则 39.9 累加几次就变成 39.900000000000006库存用INT NOT NULL DEFAULT 0并配合更新条件防负数订单号单独建唯一索引别拿自增主键当业务单号。表名作用关键字段索引book图书主数据isbn、title、price、stock、statusuk_isbn、idx_titlecategory分类name、sort_nouk_nameuser用户username、password_hash、roleuk_usernameorders订单头order_no、user_id、total_amount、statusuk_order_no、idx_userorder_item订单明细order_id、book_id、price、qtyidx_ordercart_item持久化购物车user_id、book_id、qtyuk_user_bookCREATE TABLE book ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) NOT NULL, title VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT NULL, category_id INT NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 上架 0 下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_isbn (isbn), KEY idx_title (title) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 待支付 1 已支付 2 已取消, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明字符集统一utf8mb4utf8在 MySQL 里只有三字节遇到 emoji 或部分生僻字入库就报Incorrect string value。order_item里必须冗余一份下单时的单价因为图书价格随时会改订单金额不能跟着变。2.3 从 DriverManager 到连接池什么时候值得引框架原生写法DriverManager.getConnection()每请求建一次 TCP 连接加一次认证压测 50 并发就明显变慢而且连接泄漏时数据库会直接报Too many connections。课程设计里加一个连接池是性价比最高的优化。方案启动成本性能适合场景DriverManager零差每请求新建连接只写两三页的验证 demoDruid一个 jar 一个配置文件好自带监控页课程设计、小型网上书店系统HikariCP一个 jar很好参数少追求极致简单# src/main/resources/db.properties jdbc.urljdbc:mysql://127.0.0.1:3306/bookstore?useUnicodetruecharacterEncodingutf8\ serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.passwordroot123 druid.initialSize5 druid.maxActive20 druid.maxWait3000 druid.validationQuerySELECT 1 druid.testWhileIdletrue druid.timeBetweenEvictionRunsMillis60000参数说明maxActive20对小项目足够配合 MySQL 默认max_connections151能留出余量maxWait3000表示拿不到连接三秒就抛异常比无限等待更容易定位问题testWhileIdle加timeBetweenEvictionRunsMillis用来剔除被 MySQLwait_timeout悄悄掐掉的空闲连接能消掉那类「隔夜第一次访问必报错」的怪问题。3. 图书检索、购物车、下单三个闭环的实现数据表定了之后真正要写的就是三条链路。这三条链路各有各的坑检索怕拼 SQL购物车怕丢数据下单怕事务边界画错位置。3.1 图书分页检索PreparedStatement 的最小写法DAO 层永远用?占位关键字也不要拼进字符串。排序字段是唯一需要动态拼接的地方必须走白名单。public PageResultBook search(String kw, int page, int size) { String like % (kw null ? : kw.trim()) %; String countSql SELECT COUNT(*) FROM book WHERE status 1 AND (title LIKE ? OR author LIKE ?); String listSql SELECT id, isbn, title, author, price, stock FROM book WHERE status 1 AND (title LIKE ? OR author LIKE ?) ORDER BY id DESC LIMIT ? OFFSET ?; try (Connection conn DBUtil.getConnection(); PreparedStatement psCount conn.prepareStatement(countSql); PreparedStatement psList conn.prepareStatement(listSql)) { psCount.setString(1, like); psCount.setString(2, like); psList.setString(1, like); psList.setString(2, like); psList.setInt(3, size); psList.setInt(4, (page - 1) * size); long total 0; try (ResultSet rs psCount.executeQuery()) { if (rs.next()) total rs.getLong(1); } ListBook rows new ArrayList(); try (ResultSet rs psList.executeQuery()) { while (rs.next()) rows.add(mapRow(rs)); } return new PageResult(rows, total, page, size); } catch (SQLException e) { throw new DaoException(查询图书失败, kw kw, e); } }逻辑说明先查总数再查当页两次查询共用同一个like参数保证分页总数和列表口径一致。用 try-with-resources 让连接、语句、结果集自动关闭即使中途抛异常也不会泄漏。参数说明LIMIT ? OFFSET ?里的 OFFSET 在数据量大时很慢因为 MySQL 要先扫过前面所有行。课程设计数据量小无所谓真到几十万行时改成「记住上一页最后一行的 id下一页用WHERE id ?」的游标分页。ORDER BY id DESC里如果换成用户可选的排序字段一定用switch映射到固定字符串绝不能把参数直接拼进 SQL。3.2 购物车放 Session 还是放数据库这是小型网上书店系统里争论最多的一个点。两种做法都合理取决于你要演示什么。维度Session 购物车数据库购物车实现成本低一个ListCartItem中多一张表和一套增删改查关闭浏览器后依赖 JSESSIONID Cookie可能丢不丢服务重启丢不丢多端同步不支持支持需要序列化是CartItem必须Serializable不需要课程设计阶段我一般先用 Session 把功能跑通再加一个「登录后把 Session 购物车合并进数据库」的动作这个动作本身就是很好的答辩亮点。public class CartService { // 加购同本书累加数量不重复占行 public void add(HttpSession session, Book book, int qty) { if (qty 0) qty 1; SuppressWarnings(unchecked) MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(CART); if (cart null) { cart new LinkedHashMap(); // LinkedHashMap 保证列表顺序稳定 session.setAttribute(CART, cart); } CartItem item cart.get(book.getId()); if (item null) { item new CartItem(book.getId(), book.getTitle(), book.getPrice(), 0); cart.put(book.getId(), item); } // 数量不超过库存避免用户加到 999 再去下单才发现失败 int target item.getQty() qty; item.setQty(Math.min(target, book.getStock())); } }逻辑说明用MapInteger, CartItem而不是List加购是 O(1) 查找不用遍历去重。加购时就把数量卡在库存上限能减少「提交订单才报库存不足」的挫败感。参数说明CartItem需要实现SerializableTomcat 在重启时会把 Session 序列化到work目录不实现接口会抛NotSerializableException。session.setMaxInactiveInterval(1800)把默认 30 分钟维持住就行改成几秒会让演示中途掉登录。3.3 下单与扣库存的事务写法事务边界必须画在 Service 层因为一次下单要动四张表扣库存、写订单头、写订单明细、清购物车。DAO 里各自commit是最常见的错误写法会导致「库存扣了但订单没写」。public String submitOrder(int userId, CollectionCartItem items) { if (items null || items.isEmpty()) { throw new BizException(购物车为空); } String orderNo BS System.currentTimeMillis() userId; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 手动开事务 BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { // 带 stock ? 的条件更新返回 0 行说明库存不够直接回滚 int affected bookDao.decreaseStock(conn, item.getBookId(), item.getQty()); if (affected 0) { throw new BizException(《 item.getTitle() 》库存不足); } total total.add(item.getPrice() .multiply(BigDecimal.valueOf(item.getQty()))); } long orderId orderDao.insert(conn, orderNo, userId, total); orderItemDao.batchInsert(conn, orderId, items); cartDao.clear(conn, userId); conn.commit(); return orderNo; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignore) { } } throw (e instanceof BizException) ? (BizException) e : new BizException(下单失败, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignore) { } } } }逻辑说明所有 DAO 方法都接收外部传进来的Connection绝不自己开新连接这样它们才在同一个事务里。库存不足时抛BizExceptioncatch 块统一回滚。金额用BigDecimal累加别用double。参数说明orderNo用「前缀 毫秒时间戳 userId」单机足够唯一如果要多节点部署改成雪花算法或数据库序列。finally里恢复autoCommit再关连接很重要——连接池归还的是同一个物理连接不改回来下一个借到它的人会莫名其妙地没有自动提交。4. 库存超卖、乱码与部署排错的参数清单功能跑通只算及格。真正拉开差距的是这些边界问题两个人同时点「立即购买」会不会超卖中文参数什么时候变问号war 丢进 Tomcat 之后为什么起不来。4.1 库存超卖条件更新比锁更简单先看一个错误写法SELECT stock FROM book WHERE id?读到 5业务代码判断5 1再UPDATE book SET stock 5 - 1。两个线程同时读到 5就会卖出两本实际库存变成 4 而不是 3。这就是超卖。三种改法对比悲观锁SELECT ... FOR UPDATE简单但会阻塞事务长了容易锁等待超时乐观锁表加version字段UPDATE ... WHERE version ?失败就重试条件更新UPDATE book SET stock stock - ? WHERE id ? AND stock ?最省事-- 条件更新返回影响行数0 表示库存不足业务层直接回滚 UPDATE book SET stock stock - 2 WHERE id 1001 AND stock 2; -- 悲观锁写法适合需要先读出来算折扣的场景 SELECT stock, price FROM book WHERE id 1001 FOR UPDATE;参数说明第二条UPDATE在 InnoDB 下会加行锁AND stock ?保证了「检查」和「扣减」在一条语句里原子完成。第一条SELECT FOR UPDATE必须在事务里执行autocommit1时锁会立刻释放形同虚设。注意MySQL 默认隔离级别是REPEATABLE READ普通SELECT走快照读可能读到旧库存但UPDATE ... WHERE走的是当前读会读到最新已提交数据这正是条件更新能防超卖的原因。4.2 中文乱码、时间与连接池的必调参数乱码的根源只有一个字节用一种编码写进去用另一种编码读出来。java-web 链路上有三处编码要一致。// 1. POST 表单必须在读取任何参数之前调用 request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); // 2. JSP 页面顶部 // % page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %!-- 3. Tomcat conf/server.xmlGET 请求的查询串编码 -- Connector port8080 protocolHTTP/1.1 connectionTimeout20000 URIEncodingUTF-8 redirectPort8443 /参数说明setCharacterEncoding只对请求体POST生效对 URL 查询串无效所以 GET 中文要靠 Connector 的URIEncoding。Tomcat 8 以后默认已是 UTF-8但老版本或改过配置的环境一定要确认。数据库连接串里的characterEncodingutf8和serverTimezoneAsia/Shanghai属于同一类问题前者管字符后者管时区。少了时区参数DATETIME字段可能差 8 小时。参数推荐值作用druid.maxActive20上限过高会把数据库连接数吃光druid.maxWait3000拿不到连接快速失败避免请求堆积druid.removeAbandonedtrue打印泄漏连接的堆栈排查忘记 closedruid.removeAbandonedTimeout1800超过 30 分钟未归还视为泄漏serverTimezoneAsia/Shanghai修正 JDBC 与 MySQL 的时区差4.3 Tomcat 部署与日志排查的顺序打包后把bookstore.war丢进webapps启动看logs/catalina.out。启动失败的九成是这几类报错原因处理ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动 jar 没进WEB-INF/libMaven 里scope别写providedNo suitable driver foundurl 前缀写错如写成jdbc:mysq核对jdbc:mysql://Access denied for user账号密码或授权库不对GRANT ALL ON bookstore.* TO ...Communications link failure数据库没起或端口不通先telnet 127.0.0.1 3306The web application appears to have started a thread but has failed to stop it自己起了线程池没关在contextDestroyed里 shutdown排查顺序建议固定下来先看 catalina.out 最下面一段异常栈找到第一个Caused by再看WEB-INF/lib里 jar 是否齐全最后才怀疑代码逻辑。日志别只用System.out.printlnDAO 层把实际 SQL 和参数打到 DEBUG出问题时能省掉大量猜测。5. 把小型网上书店系统做成拿得出手的作品功能齐了之后最后一步是让评审和面试官两分钟就能看出你的工程习惯而不是只看到「能点」。5.1 用 curl 快速回归下单链路写页面测试慢命令行自测更快。先登录拿 Cookie再依次加购和下单一条链路几十秒跑完# 1. 登录把 Cookie 存到文件 curl -s -c /tmp/c.txt -X POST http://127.0.0.1:8080/bookstore/login \ -d usernametompassword123456 # 2. 加入购物车bookId1001数量 2 curl -s -b /tmp/c.txt -X POST http://127.0.0.1:8080/bookstore/cart/add \ -d bookId1001qty2 # 3. 提交订单观察返回的订单号 curl -s -b /tmp/c.txt -X POST http://127.0.0.1:8080/bookstore/order/submit \ -H Content-Type: application/x-www-form-urlencoded # 4. 验证库存是否真的减了 2 mysql -uroot -proot123 bookstore -e SELECT id, title, stock FROM book WHERE id 1001;参数说明-c写 Cookie、-b读 Cookie是模拟会话最省事的方式。第三步故意不带参数用来验证空购物车时服务端是否返回友好提示而不是 500。第四步的 SQL 是验证事务是否真的提交的唯一标准——光看页面成功不算数。5.2 一键初始化演示数据答辩现场最怕数据被改乱。准备一个初始化脚本每次演示前跑一遍把库存和订单清回干净状态-- init_demo.sql演示前一键还原 SET FOREIGN_KEY_CHECKS 0; TRUNCATE TABLE order_item; TRUNCATE TABLE orders; TRUNCATE TABLE cart_item; TRUNCATE TABLE book; SET FOREIGN_KEY_CHECKS 1; INSERT INTO book (isbn, title, author, category_id, price, stock, status) VALUES (9787111213826, Java 编程思想, Bruce Eckel, 1, 108.00, 5, 1), (9787115546081, 深入理解 Java 虚拟机, 周志明, 1, 129.00, 3, 1), (9787121362217, SQL 基础教程, MICK, 2, 59.00, 20, 1);参数说明TRUNCATE会重置自增 id比DELETE更适合演示场景FOREIGN_KEY_CHECKS临时关掉避免清表顺序不对导致外键报错。库存故意留成个位数方便现场演示「库存不足」的提示效果。5.3 一个容易被忽略的加分项把 Servlet 的响应统一成 JSON而不是每个接口各自print字符串。抽一个JsonResult包装{code, msg, data}再配一个BaseServlet用反射分发action参数页面侧用fetch调用。这件事花不了两小时但它让整个项目从「JSP 拼页面」变成前后端接口分明面试时也更容易讲清你负责的那一层。真正决定评价高低的往往不是多写了几个页面而是别人问到「并发下单会怎样」「连接泄漏怎么查」时你能直接翻出对应的代码和日志。本文还有配套的精品资源点击获取