简介:基于Java实现的网上书店及书店管理系统,是一份可直接用于课程设计、期末大作业或毕业设计的完整源码包,面向计算机相关专业学生及需要项目实战的Java学习者。系统分顾客与管理员双角色:顾客可浏览、注册、登录、按类别/名称/作者查询图书并购买,还能管理购物车、订单与个人信息;管理员可通过后台对图书、订单、用户信息进行增删改查。技术栈采用原生Vue结合Bootstrap构建前端,后端由Java Servlet与JDBC连接SQL Server数据库,项目结构清晰。压缩包共1664个文件,约25.68MB,覆盖前端html/less/css/js、后端java/class、图片png/jpg/svg、依赖jar/xml、配置文件yml及说明文档md等,类型齐全。已有536人学习使用,适合用来快速上手B/S开发或作为毕设基础进行二次开发。
1. 用 Java 做的网上书店:半只脚踩进真实 Web 项目的那道坎
很多人在学完 Java SE 之后,第一份“拿得出手”的东西往往不是图书管理系统,就是网上书店。原因很简单:这类项目能把 Servlet、JDBC、Session、前端渲染全串起来,做完一遍,你对 Java Web 的认知就不再是一坨散装知识点。这份基于 Java 实现的网上书店及书店管理系统源码,走的是「Vue + Bootstrap 前端、Servlet + JDBC 后端、SQL Server 数据库」的组合路线,用户端覆盖注册、登录、分类/关键字查书、购买、购物车、订单管理,管理端覆盖图书、订单、用户三类核心数据的增删改查。说白了,这是一份能当课程设计交差、也能拿来当 Java Web 入门练手的完整项目,源码和项目说明都齐,适合正在做毕设、期末大作业的计算机相关专业学生。
2. 业务模型与数据表设计:先把五种角色之间的牵扯理清
网上书店这类系统,第一眼看上去全是页面跳转,实际上真正决定项目能不能跑通的是数据表之间的关系。这份资源里的后端代码基于 JDBC 操作 SQL Server,表结构并不复杂,但订单与订单明细的拆分方式、购物车与订单的衔接逻辑,值得先搞清楚。
2.1 核心表拆解:BOOK、USER、ORDER、ORDER_ITEM、CART
从源码里的各个 DaoImpl 类往回推,项目里至少存在这样几张核心表:图书表(BOOK)、用户表(USER)、订单表(ORDER)、订单明细表(ORDER_ITEM)、购物车表(CART)。这里有一个容易被忽视的点:订单明细不是简单地在订单表里塞几个字段。
| 表名 | 核心字段 | 作用 |
|---|---|---|
| USER | user_id, username, password, phone, address | 顾客与管理员共用,用 role 字段区分身份 |
| BOOK | book_id, book_name, author, category, price, stock, cover | 图书信息,支撑主页浏览和三种查询方式 |
| CART | cart_id, user_id, book_id, quantity | 购物车条目,记录用户未下单的图书 |
| ORDER | order_id, user_id, order_time, total_price, status | 订单主表,一个订单对应一次购买行为 |
| ORDER_ITEM | item_id, order_id, book_id, quantity, price | 订单明细,保存下单时的快照信息 |
为什么 ORDER 和 ORDER_ITEM 要分开?因为同一笔订单可以包含多本书,如果把图书信息直接冗余在订单表里,订单表的一行就得存多本书的数据,后续查询订单详情时要么拆字符串、要么做连表查询,反而更麻烦。拆成主表和明细表之后,订单主表记录整笔订单的总额和状态,明细表每行记录一本书的购买数量与成交价格,是最经典的一对多建模方式。
这里有一个容易翻车的设计细节:订单明细表里的 price 字段存的是下单那一刻的书价,而不是实时去 BOOK 表里查。因为图书价格是可以调整的,如果用户在购买后管理员改了价格,用户的订单记录跟着变,这在真实业务里是用户完全不能接受的。快照保存是这类系统必须有的意识,哪怕课程设计里不会真有人去改价格,这个字段的冗余也值得保留。
2.2 顾客与管理员两条主流程的状态流转
系统里有两种角色,各自的流程边界是不同的。顾客侧的路径是:注册 → 登录 → 浏览/查询图书 → 加入购物车 → 提交订单 → 管理订单和个人信息。管理员侧的路径是:用管理员账号登录后台 → 进入管理界面 → 对图书、订单、用户做增删改查。
从状态流转的角度看,购物车和订单是最容易写乱的地方。一个常见做法是:顾客把书加入购物车时,CART 表插入一条记录;顾客点击“去结算”时,系统根据当前用户的购物车记录生成一笔 ORDER,同时把购物车里的条目转成 ORDER_ITEM 并清空 CART。这里要注意事务边界:生成订单和清空购物车这两个操作必须放在同一个事务里,否则会出现订单生成了但购物车没清空、或者购物车清了但订单明细没落库的诡异状态。JDBC 里手动控制事务的方式是conn.setAutoCommit(false),执行完再commit(),异常则rollback()。
管理员的增删改查也不是简单的一行 SQL 完事。图书删除要处理外键约束问题:如果 BOOK 表被 ORDER_ITEM 或 CART 引用,直接执行 DELETE 会报外键冲突。很多人在这一步卡住,然后给图书表加外键级联删除,这是比较省事的做法,但课程设计的答辩环节里,能说出「我先检查这个图书有没有被未完成订单引用,有则拒绝删除」这句话,比级联删除更让老师认可。
2.3 图书查询的三种方式:一个模糊查询方法吃透
需求里写了「图书类别、图书名称、作者名字」三种查询方式,很多人的第一反应是写三个 Dao 方法、三条 SQL 语句、三个 Servlet。但实际上,这三种查询完全可以用一个动态 SQL 方法解决。
public List<Book> searchBooks(String category, String bookName, String author) { StringBuilder sql = new StringBuilder("SELECT * FROM BOOK WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (category != null && !category.trim().isEmpty()) { sql.append("AND category LIKE ? "); params.add("%" + category.trim() + "%"); } if (bookName != null && !bookName.trim().isEmpty()) { sql.append("AND book_name LIKE ? "); params.add("%" + bookName.trim() + "%"); } if (author != null && !author.trim().isEmpty()) { sql.append("AND author LIKE ? "); params.add("%" + author.trim() + "%"); } return queryList(sql.toString(), params.toArray()); }这段代码的逻辑是:先拼一个恒真的1=1,之后每个查询条件按需追加,参数通过?占位符传入。queryList是 BaseDao 里的通用查询方法,内部通过PreparedStatement执行 SQL,把ResultSet转成List<Book>。参数说明:所有转发过来的查询参数都先做了trim()和空判断,避免 SQL 参数是空字符串时走无意义的条件过滤。用PreparedStatement而不是直接字符串拼接,是为了规避 SQL 注入,这也是课程设计答辩时老师大概率会问到的点。
2.4 角色区分的设计:一张表还是两张表
这项目的角色区分设计值得展开,因为这是「顾客和管理员」需求在数据库层面最常见的分歧点。一部分人会设计 USER 和 ADMIN 两张表,另一部分人会在 USER 表上加一个 role 字段。
这份资料对应的源码走向是后者。理由是两张表的用户数据存在大量重复字段(用户名、密码、联系方式),管理员工号在业务上并不需要;一张表加 role 字段,顾客登录和后台登录走同一个校验逻辑,只是登录成功后根据 role 值跳转到不同页面。
不过这里有一个要注意的地方:用一张表时,顾客注册功能绝不能允许用户设置 role 字段。我见过一些人图省事,前端表单直接提交 role 字段,然后被懂行的同学用浏览器开发者工具改成 admin 就进后台了。正确的做法是:注册接口里后端强制把 role 设置为默认值(比如 0 表示顾客),完全忽略前端传过来的 role 参数,这样才能保证权限不被越权篡改。
3. 从下载到跑起来:配置 SQL Server、部署 Tomcat、初始化数据库
拿到源码包之后,最快的跑通路径是:先改数据库连接配置,再初始化 SQL Server 数据库,最后把项目挂到 Tomcat 上。这一节把三个步骤拆开,每一步都对应实际会发生的问题。
3.1 修改 jdbc.properties:五个参数决定数据库能不能连上
JDBC 连接信息一般会集中放在jdbc.properties文件里,或者直接写在 DBUtil 类的静态代码块中。参数量不多,但每一个都值得逐一核对。
jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver jdbc.url=jdbc:sqlserver://localhost:1433;DatabaseName=bookstore jdbc.username=sa jdbc.password=123456参数说明:jdbc:sqlserver://是 SQL Server 的 JDBC 协议前缀;localhost:1433指数据库服务地址和默认端口,如果你的 SQL Server 实例命名过,这里要改成localhost\\实例名或者localhost:端口号;DatabaseName=bookstore指定默认连接的库,如果建库时用了别的名字,这里必须同步修改。username和password用你本地 SQL Server 的登录凭据,注意 SQL Server 默认开 Windows 身份验证模式,不开混合模式的话,sa账号连不上,这是新手最容易卡住的一步。
3.2 初始化数据库脚本:先建库再建表
源码包里一般会附带 SQL 脚本文件,文件名类似bookstore.sql。用 SQL Server Management Studio 打开并执行时,常遇到两个问题:一是脚本里带了USE bookstore语句,但你的实例里还没建这个库,会导致脚本在中间报错;二是中文字段注释出现乱码。
IF DB_ID('bookstore') IS NULL CREATE DATABASE bookstore; GO USE bookstore; GO CREATE TABLE [USER] ( user_id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL UNIQUE, password NVARCHAR(100) NOT NULL, role INT NOT NULL DEFAULT 0, phone NVARCHAR(20), address NVARCHAR(200) );这段脚本的思路是:先判断bookstore库是否存在,不存在则创建;USE切换到目标库后建表。字段类型用NVARCHAR而不是VARCHAR,是为了让中文数据在 SQL Server 里正常存储和读取,这是实际项目里非常救命的一个习惯。如果你执行脚本时发现中文插入后变成问号,十有八九是字段类型用了VARCHAR或者客户端连接字符集不对。
3.3 部署到 Tomcat:IDEA 里配置 Artifact 与启动参数
后端是 Servlet 项目,跑起来需要一个 Servlet 容器。常规做法是用 IDEA 社区版或旗舰版,配置 Tomcat 后以 exploded 方式部署 war 包。你需要核对三个东西:
- Tomcat 版本与 JDK 版本兼容性,Tomcat 9 配 JDK 8/11 是稳妥组合;
- 项目的 Artifact 是 war exploded 还是 jar,Java Web 项目必须选 war exploded;
- 启动前确认 Tomcat 的端口没被占用,默认 8080,如果被其他进程占了,改
conf/server.xml里的 Connector 端口。
# 以命令行方式启动 Tomcat(Windows) cd C:\apache-tomcat-9.0.xx\bin startup.bat # Linux/macOS 下等价命令 ./startup.sh启动完成后,浏览器访问http://localhost:8080/项目上下文路径/,看到主页说明后端已经跑通。如果启动瞬间崩溃,去logs/catalina.out(Linux)或logs/catalina.xxx.log(Windows)看最后的异常栈,最常见的几种是 SQLServerDriver 类找不到(驱动 jar 没放对位置)、端口被占用(改 server.xml)、Servlet 类加载失败(编译输出目录没配置对)。
3.4 连接池要不要换:课程设计的边界掌握
这份源码用的是最朴素的 JDBC 直连方式,也就是每次请求都通过DriverManager.getConnection()拿一个连接,用完之后关闭。这种方式在并发量不高的课程设计场景里完全够用,代码也更容易看懂,每一步都明明白白。
但如果你的机器上装了多个数据库,或者你想让这个项目更接近生产环境,可以把它换成 Druid 连接池:引入druid-1.2.x.jar,在工具类里初始化一个DruidDataSource,后续所有获取连接的操作都从池里取。这里有一个风险要提醒:如果你不熟悉连接池,换完之后可能出现连接不释放的问题,调式起来的成本比直接用 JDBC 高不少。课程设计的场景,不建议折腾。
4. 后端三层实现:Servlet、Service、Dao 的边界划分与分页查询
源码的文件列表里都是 XXXDaoImpl 和 XXXImpl 这类类名,说明项目在主结构上是有意往 MVC 方向靠的。这一章把这三层剖开,重点看每一层的边界在哪、哪些逻辑放哪层会导致项目变烂。
4.1 Servlet 负责接参和跳转,不负责算钱
Servlet 这一层在项目里的定位是 HTTP 入口。doGet 和 doPost 里的代码应该只做三件事:接收请求参数、调用 Service 层方法、根据返回结果转发或重定向。
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("list".equals(action)) { List<Book> books = bookService.getAllBooks(); request.setAttribute("books", books); request.getRequestDispatcher("/book_list.jsp").forward(request, response); } else if ("detail".equals(action)) { int bookId = Integer.parseInt(request.getParameter("bookId")); Book book = bookService.getBookById(bookId); request.setAttribute("book", book); request.getRequestDispatcher("/book_detail.jsp").forward(request, response); } else { response.sendRedirect(request.getContextPath() + "/index.jsp"); } }逻辑说明:这段代码用action参数做分发,list走列表查询,detail走详情查询,都不是同路径时重定向回主页。参数说明:Integer.parseInt在解析非法参数时会抛NumberFormatException,实际项目中应该做一下防御性判断,比如用正则先过滤掉非数字内容。forward和sendRedirect的区别要弄清楚:forward 是服务器内部转发,地址栏不变,适合带数据去渲染页面;sendRedirect 是浏览器重新发一次请求,地址栏变化,适合登录后跳转,避免表单重复提交。
查询全部图书的代码在课程设计里毫无压力,但如果图书数量上千,一次SELECT * FROM BOOK返回全部数据,页面渲染会开始发飘。分页查询的 SQL 在 SQL Server 里用OFFSET ... FETCH或者老式ROW_NUMBER()都行。
SELECT * FROM BOOK ORDER BY book_id OFFSET ? ROWS FETCH NEXT ? ROWS ONLY;参数含义:第一个?是起始偏移量(0 表示从第一条开始),第二个?是每页行数。翻页时计算偏移量的逻辑是(pageNum - 1) * pageSize。这条 SQL 在 SQL Server 2012 及以上版本才支持,SQL Server 2008 需要换成ROW_NUMBER() OVER (ORDER BY book_id)的写法,课程设计一般碰不到 2008,但如果你的机器装的是老版 SQL Server,这块会成为一个隐藏的雷。
4.2 DaoImpl 的职责:SQL 拼接与对象映射的重复劳动
Dao 层是所有数据库操作的收口位置。BaseDao 里一般封装了通用的增删改查方法,子类 DaoImpl 负责传 SQL 和参数,以及把 ResultSet 里的行数据转换成 Java 对象。
public class BookDaoImpl extends BaseDao implements BookDao { @Override public Book findById(int id) { String sql = "SELECT * FROM BOOK WHERE book_id = ?"; Object[] params = { id }; List<Book> list = queryList(sql, Book.class, params); return list.isEmpty() ? null : list.get(0); } @Override public int updateStock(int bookId, int newStock) { String sql = "UPDATE BOOK SET stock = ? WHERE book_id = ?"; Object[] params = { newStock, bookId }; return update(sql, params); } }逻辑说明:findById返回单个对象,内部复用queryList后取第一条,避免单独写一条查询逻辑;updateStock是典型的修改库存操作,返回int表示受影响的行数,调用方可以根据返回值判断库存扣减是否成功。参数说明:params数组里的顺序必须和 SQL 里?出现的顺序严格一致,?的下标从 1 开始,这是 JDBC 使用中最容易踩的坑之一。如果你在调试时发现「参数索引超出范围」之类的异常,优先检查的就是参数顺序和数量。
4.3 购物车到订单的转换:事务保护和金额计算
购物车提交订单是整份源码里业务逻辑最重的部分,因为它不止涉及一张表。顾客点击结算后,后端需要做的事情按顺序排开是:
- 查出当前用户购物车里的所有条目;
- 计算订单总金额;
- 往订单主表插入一条记录,拿到自增订单号;
- 遍历购物车条目,逐条插入订单明细表;
- 扣减图书库存;
- 清空购物车。
这六步少了任何一步,都会产生脏数据。典型问题:往订单明细表插入时拿不到订单主表的自增主键。SQL Server 获取自增 ID 的方式是通过executeQuery("SELECT @@IDENTITY")或者SCOPE_IDENTITY(),很多人会忘了在插入订单后立刻获取这个 ID,导致明细表外键字段为空。
金额计算也是个需要小心的地方。如果把单价、数量都从页面传过来,用户完全可以改请求参数压低价格。常见的做法是后端根据 BOOK 表里的现价重新计算:
long totalPrice = 0; for (CartItem item : cartItems) { Book book = bookDao.findById(item.getBookId()); BigDecimal subtotal = book.getPrice().multiply(new BigDecimal(item.getQuantity())); totalPrice += subtotal.longValue(); }这是我比较推荐的做法:价格以数据库存的为准,前端传过来的数量只做加减法触发条件,单价永远从后端查。BigDecimal 而不是 double 计算金额,这是 Java 财务计算的基本规矩,虽然课程设计里的金额精度要求没那么高,但答辩时说出「用 BigDecimal 避免精度丢失」是明显加分项。
4.4 Session 与登录状态:购物车到底跟着谁走
购物车在系统里存在两种形态:已登录用户的购物车数据落库(存在 CART 表里),未登录用户的购物车只能存在 Session 里。这份资源的主流程要求「先注册登录再购买」,所以购物车落库是主路径。但这里有一个很常见的课程设计缺陷:用户把书加入购物车之后,如果 Session 过期,再次访问时购物车数据还在不在?答案是:只要 CART 表里的 user_id 能对上,数据就还在,因为读购物车时是拿当前登录用户的 ID 去查表,而不是从 Session 里取。
Session 相关的代码有一个硬伤要避免:在 Servlet 里取用户 ID 时,应该取登录时放入 Session 的对象,不要每次从数据库里查。登录成功时执行session.setAttribute("user", user),后续获取用户的 ID 用((User) session.getAttribute("user")).getUserId()。如果每次都要从数据库查一遍再判断,代码会变得很笨重,也容易在 Session 与之不一致时制造神秘 bug。
5. 前端 Vue 与 Bootstrap:查询表单、数据渲染和页面跳转的那些别扭
前后端分离并不是这个项目的架构,Vue 和 Bootstrap 在这里是渐进增强的角色:Bootstrap 管样式和栅格布局,Vue 管局部数据渲染和交互。
5.1 Vue 挂载位置:不是整页接管,只是在需要的区域里用
一个常见的认知误区是 Vue 项目就要用脚手架,然后把整个前端工程化。这份资源里的 Vue 是直接引入 CDN 或本地 js 文件,在 JSP 页面里局部使用的。挂载区域之前,需要先确认页面的 DOM 结构在挂载时已经存在,否则 Vue 会找不到容器。
<div id="bookSearchApp"> <input type="text" v-model="keyword" placeholder="输入书名或作者"> <select v-model="searchType"> <option value="bookName">按书名</option> <option value="author">按作者</option> <option value="category">按类别</option> </select> <button @click="searchBooks">搜索</button> <table> <tr v-for="book in books" :key="book.bookId"> <td>{{ book.bookName }}</td> <td>{{ book.author }}</td> <td>{{ book.price }}</td> </tr> </table> </div> <script> new Vue({ el: "#bookSearchApp", data: { keyword: "", searchType: "bookName", books: [] }, methods: { searchBooks: function () { var self = this; axios.get("bookSearch", { params: { type: self.searchType, keyword: self.keyword } }).then(function (response) { self.books = response.data; }); } } }); </script>逻辑说明:v-model绑定搜索条件和关键字,点击按钮后通过 axios 向bookSearch这个 Servlet 发起 GET 请求,返回的 JSON 数组直接赋值给books,Vue 自动完成列表渲染。参数说明:params对象的字段名要跟后端request.getParameter("type")、request.getParameter("keyword")严格对应,大小写都不能错;后端返回 JSON 需要先设置response.setContentType("application/json;charset=utf-8"),否则前端拿到的可能是字符串而非对象,.data的值就会变成 String 而不是 Array。
这个写法里有一个实际项目中经常会遇到的问题:Vue 实例挂载后,如果 JSP 里还嵌套了别的标签库或模板引擎,{{ }}会被冲突解析。解决办法有两个:一是把 Vue 的数据绑定改成v-text或v-bind,不用 Mustache 语法;二是设置delimiters: ['${', '}']改变 Vue 的插值分隔符,这个技巧在后端模板和前端框架需要在同一页面共存时非常管用。
5.2 图书列表页:Bootstrap 栅格与卡片布局
图书列表页是顾客打开网站的第一个视觉落点。Bootstrap 在这里的典型用法是栅格系统,把页面分成一行四列的图书卡片,每张卡片包含封面、书名、作者、价格和「加入购物车」按钮。
Bootstrap 栅格的核心参数是col-lg-3 col-md-4 col-sm-6:lg是桌面大屏,每行放 4 本;md是中屏,每行 3 本;sm是小屏,每行 2 本。这个响应式断点设置不需要额外写 CSS 媒体查询。图书卡片的信息直接从后端request.setAttribute("books", list)里取,用 JSTL 的c:forEach循环输出,而不是用 Vue 渲染。
这里要强调的是:服务端渲染和客户端渲染在同一页面里的混用不应该互相打架。图书列表走 JSP + JSTL 循环渲染,页面加载时直接看到结果,SEO 和首屏都更友好;搜索区域和跳转交互走 Vue + axios,局部刷新不重载整个页面。如果你发现搜索响应后整个页面闪烁或白屏,多半是用了location.href而不是 axios 局部刷新,这不算错误,但体验差一截。
5.3 购物车数量修改与前端联动
购物车页面上用户点加减按钮时,可以只改本地数量显示,等点「去结算」时再把数量统一提交;也可以每次加减时直接发请求改 CART 表。前者简单但可能出现「页面显示数量与数据库不一致」的问题,后者实时的数据库压力不大,但在慢网络下会有一种「点了没反应」的错觉。
常见的折中方案是:加减按钮只改变前端数据,同时记录哪些行发生了变化,点结算时把整批数据提交。这里不去猜测源码里的具体实现,但有一点是通用的:提交购物车数据时,每条明细要带上cartId或bookId,后端根据这个 ID 来更新对应记录,不是把整个购物车清掉重新插入——后者虽然写了简单,但会丢失购物车条目的创建时间等字段,也浪费自增 ID。
5.4 Vue 和 axios 在 Servlet 项目里的版本选择
如果你本地跑的时候发现 CDN 的 Vue 文件加载不出来,或者 axios 请求报跨域问题,原因通常不是代码写错,而是 CDN 路径被网络环境干扰了。把 vue.min.js 和 axios.min.js 下载到项目webapp/js目录下,用本地相对路径引用,是最省心的方式。
Vue 2 的语法和 Vue 3 不完全兼容,项目里如果写的是new Vue({ el: ... }),就说明用的是 Vue 2,不要图新鲜去引 vue.global.js 的 Vue 3 文件,否则el选项直接失效。axios 的版本一般不用挑,但要注意在新版本中axios.get(...).then(...).catch(...)的写法没变,后端返回的 HTTP 状态码非 2xx 时,错误会走到catch而不是then。
6. 避坑与常见问题排查:十个能让你少熬三个晚上的真实踩坑
课程设计项目的坑,很多不是技术深奥,而是藏得深。根据实操经验,把最有代表性的几个列出来,每个都按「现象 → 原因 → 解决」的方式说清,这些如果你遇到了,直接对号入座。
6.1 登录成功后跳转回主页,但购物车数据「没了」
现象:用户登录前浏览了图书,登录成功后返回主页,购物车里的条目变成了空的。
原因:购物车数据存在 Session 里,而登录成功后执行了session.invalidate()(为了清掉旧登录痕迹),旧 Session 里的购物车全被清掉了;或者购物车数据确实存在数据库,但前端展示购物车时用的是从 Session 里取的数据,没有去数据库查。
解决:购物车数据落库的情况下,每次展示购物车前以当前登录用户的 ID 去 CART 表查询,不要依赖 Session 里的快照。登录跳转时保留购物车数据的需求,可以把旧 Session 的购物车数据复制到新 Session,或者在用户未登录时把购物车临时数据合并到登录后的 Session 中。
6.2 图书查询接口能返回数据,但 Vue 页面渲染出来全是 undefined
现象:axios 请求成功,response.data里也能在开发者工具里看到 JSON 数组,但页面上书籍名称、作者都显示 undefined。
原因:后端返回的 JSON 字段名是数据库列名(比如book_name),而前端 Vue 模板里用的是 Java 属性的驼峰写法(bookName),两者对不上。
解决:两边统一命名。推荐做法:后端把 ResultSet 映射成 Book 对象后,用 Jackson 或 Gson 序列化时通过@JsonProperty("bookName")指定 JSON 字段名;前端按实际的 JSON 字段名调整模板。如果想让「数据库列名 — Java 属性名 — JSON 字段名」三者全局一致,一个治本的办法是在建表时就使用驼峰命名,SQL Server 里下划线和小写混用在 JDBC 映射时很容易造成这一串连锁问题。
6.3 执行 SQL 脚本后表中无数据,但页面能查到书
现象:数据库脚本执行成功,但打开 BOOK 表发现没数据,而网页上图书列表却是满的。
原因:网页连的数据库和 SSMS 里看的数据库不是同一个库。最典型的情况是jdbc.url里写的DatabaseName=bookstore,但脚本和手动建的表在另一个实例的默认库(比如 master)里。另外如果jdbc.url写的是localhost:1433而 SQL Server 实例名带后缀,jdbc 协议连到的实例可能跟 SSMS 默认连的实例不同。
解决:在 SSMS 里执行SELECT DB_NAME()确认当前连接的是哪个库,再执行SELECT @@SERVERNAME确认实例名。然后在 JDBC url 里手动指定库名,不用默认库。这一步验证成本很低,但能排除一大批「数据看起来丢了」的假象。
6.4 注册用户时中文用户名变成问号
现象:注册页输入中文名字,提交后数据库里存的是???。
原因:JDBC 连接串里没有指定字符编码,SQL Server 的排序规则和客户端的编码不一致。或者前端页面本身是 GBK 编码,而数据库表字段是VARCHAR类型。
解决:把jdbc.url后追加;characterEncoding=UTF-8参数,表字段类型改用NVARCHAR,并在 Servlet 里设置request.setCharacterEncoding("UTF-8")后再取参。如果还不行,检查 JSP 页面头部的pageEncoding是否也声明成 UTF-8,Java Web 项目从页面到数据库整条链路上的编码保持一致,是中文不乱码的唯一治本方案。
6.5 购物车结算后订单生成,但库存没有扣减
现象:订单表里能看到新订单,订单明细也正确,但 BOOK 表的 stock 字段原封不动。
原因:扣减库存的 SQL 没有和生成订单的代码放在同一个事务里。代码可能执行了插入订单的executeUpdate()后,又执行扣减库存的executeUpdate(),但两个操作各自独立自动提交,中间任何一个环节抛了异常,就会造成只有订单没扣库存的脏状态。
解决:在 Service 层获得一个 Connection,手动设置setAutoCommit(false),把「插入订单主表 → 插入订单明细 → 扣库存 → 清购物车」四步全部用同一个 Connection 执行,全部成功后再commit(),任何一步异常就rollback()并抛出异常。检查源码时,如果这四个操作里有一个是自己DriverManager.getConnection()拿的新连接,那它就不可能和其他操作在同一个事务里。
6.6ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver
现象:Tomcat 启动或访问页面时报找不到驱动类。
原因:sqljdbc4.jar或mssql-jdbc-xxx.jar没有放进WEB-INF/lib目录,或者只在 IDE 的 module dependency 里加了而没复制到部署目录。
解决:确认 jar 包在webapp/WEB-INF/lib下,并且 IDE 部署时勾选了「build resources 时包含库」。如果是在 Tomcat 的 lib 目录里加载的驱动,项目本身不打包也行,但放进 WEB-INF/lib 是最不容易出错的方案。
6.7 页面显示 500:ResultSet已关闭或连接已关闭
现象:第一次访问页面正常,刷新后频繁出现「ResultSet closed」或「Connection closed」。
原因:在 DAO 方法里提前关闭了Connection、Statement、ResultSet,而对象映射还发生在连接关闭之后;或者写了静态连接对象,多个请求共用同一个连接,后一个请求把前一个的连接关闭了。
解决:严格遵循「先取数据后关连接」的顺序。JDBC 标准里ResultSet依赖Statement,Statement依赖Connection,必须按 ResultSet → Statement → Connection 的顺序关闭。如果要在关闭连接后继续使用查询结果,就把ResultSet的数据尽早映射成 Java 对象。用静态连接对象的写法在并发场景是必炸的,一定要让每个请求通过连接池或DriverManager获取独立连接。
6.8 前端 Vue 只在首次加载时渲染,搜索后页面数据不变
现象:进入搜索页时表格有数据,点击搜索后表头空白或者是旧数据,控制台也没报错。
原因:axios 请求是异步的,回调函数里this指向的不是 Vue 实例,而是 window 或 undefined。代码里如果用了this.books = response.data而回调是普通函数,this已经丢失了。
解决:把 Vue 实例对象存到var self = this,在 axios 回调里用self.books = response.data;或者用箭头函数() => {},箭头函数不绑定自己的this。排查时控制台打印一下this是什么,问题就很直观了。
6.9 图书查询时 SQL 报错:列名 'xxx' 无效
现象:按书名查询时正常,按作者查询时 SQL 执行报错,但数据库里确实有 author 列。
原因:拼 SQL 时条件列名写错了,或者用了保留字没有加方括号。比如author在 SQL Server 里不是保留字,但order是;如果某张表里有order或user这样的字段名,必须写成[order]、[user],否则 SQL Server 会解析成关键字。
解决:给表名和字段名统一加不加方括号的规则:列名是 SQL Server 保留字时必须加[]。项目里USER表和ORDER表是重灾区,查询、更新、删除语句里凡是要写表名的地方,养成写[USER]、[ORDER]的习惯,能从根上避免这一类问题。
6.10 管理员修改图书信息后,前台列表没有变化
现象:后台改了图书价格或库存,回到前台刷新页面还是旧数据。
原因:浏览器的 HTTP 缓存。JSP 页面如果服务端没有设置Cache-Control: no-cache,浏览器从本地缓存里直接读页面,不会重新请求服务端。
解决:修改完数据后强制刷新(Ctrl+F5),或者在 Servlet 的响应里加response.setHeader("Cache-Control", "no-cache")。如果用的是 Vue + axios 局部渲染,一般不会出现这个问题;如果整个页面是服务端渲染的,在响应头里加缓存控制是最稳的。
7. 把这份源码焊进自己的知识体系:三个进阶调试技巧
跑通项目只是第一步,真正让你在课程设计答辩或项目复盘时不虚的,是能把这些代码里的隐性知识和个人技巧说出来。单独聊聊三个我实际操作时觉得收益最大的点。
第一个技巧是给 JDBC 操作加一个极简的 SQL 日志出口。很多人调试 JDBC 项目时两眼一抹黑,不知道 SQL 到底执行了什么。我给 BaseDao 的queryList和update方法里加一个可开关的打印逻辑:当项目处于调试模式时,把最终拼好的 SQL 和参数数组用System.out.println打出来。这样做的好处是,页面报错时你能直接看出查询语句是SELECT * FROM [ORDER] WHERE user_id = ?还是SELECT * FROM ORDER WHERE user_id = ?,很多 SQL Server 保留字的问题扫一眼日志就明白了。等到上线时再把这个开关关掉,不影响性能。
第二个技巧是把报错堆栈读完整。Tomcat 的报错页面只显示异常消息第一行,真正的根因往往在完整堆栈的Caused by里。比如java.sql.SQLException: 对象名 'BOOK' 无效这个信息本身已经很明确了,但如果是连接池配置错误,堆栈前几行全是连接池框架的代码,真正的错误信息埋在最后面几行。我习惯在 IDEA 的控制台里直接点击堆栈中的项目代码行,跳转到对应源码位置,这样排查效率比一行行读要快得多。
第三个技巧是动手前先把手里的源码用「横向对比」的心态读一遍。读别人的网上书店代码时,不要只看「它写了什么」,要多问一句「它为什么不写什么」。比如这份源码里 SQL Server 的分页写法、订单明细表的快照设计、按角色区分登录逻辑,背后都是生产经验换来的。你在课程设计里把这些点讲清楚,比堆砌十个功能页面更有说服力。
从那以后我每次拿到一份课程设计源码,都强制自己先跑通再拆解,跑完通看表结构,看完表结构看事务边界,最后才看页面。走完这套流程,这份资源才算真正属于你。希望帮到你。
本文还有配套的精品资源,点击获取