2026/10/18 22:03:07

Java网上书店系统源码解析:从Servlet+JDBC到Vue+SQL Server

Java网上书店系统源码解析:从Servlet+JDBC到Vue+SQL Server 简介基于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。这里有一个容易被忽视的点订单明细不是简单地在订单表里塞几个字段。表名核心字段作用USERuser_id, username, password, phone, address顾客与管理员共用用 role 字段区分身份BOOKbook_id, book_name, author, category, price, stock, cover图书信息支撑主页浏览和三种查询方式CARTcart_id, user_id, book_id, quantity购物车条目记录用户未下单的图书ORDERorder_id, user_id, order_time, total_price, status订单主表一个订单对应一次购买行为ORDER_ITEMitem_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 ListBook searchBooks(String category, String bookName, String author) { StringBuilder sql new StringBuilder(SELECT * FROM BOOK WHERE 11 ); ListObject 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()); }这段代码的逻辑是先拼一个恒真的11之后每个查询条件按需追加参数通过?占位符传入。queryList是 BaseDao 里的通用查询方法内部通过PreparedStatement执行 SQL把ResultSet转成ListBook。参数说明所有转发过来的查询参数都先做了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.drivercom.microsoft.sqlserver.jdbc.SQLServerDriver jdbc.urljdbc:sqlserver://localhost:1433;DatabaseNamebookstore jdbc.usernamesa jdbc.password123456参数说明jdbc:sqlserver://是 SQL Server 的 JDBC 协议前缀localhost:1433指数据库服务地址和默认端口如果你的 SQL Server 实例命名过这里要改成localhost\\实例名或者localhost:端口号DatabaseNamebookstore指定默认连接的库如果建库时用了别的名字这里必须同步修改。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 部署到 TomcatIDEA 里配置 Artifact 与启动参数后端是 Servlet 项目跑起来需要一个 Servlet 容器。常规做法是用 IDEA 社区版或旗舰版配置 Tomcat 后以 exploded 方式部署 war 包。你需要核对三个东西Tomcat 版本与 JDK 版本兼容性Tomcat 9 配 JDK 8/11 是稳妥组合项目的 Artifact 是 war exploded 还是 jarJava Web 项目必须选 war exploded启动前确认 Tomcat 的端口没被占用默认 8080如果被其他进程占了改conf/server.xml里的 Connector 端口。# 以命令行方式启动 TomcatWindows cd C:\apache-tomcat-9.0.xx\bin startup.bat # Linux/macOS 下等价命令 ./startup.sh启动完成后浏览器访问http://localhost:8080/项目上下文路径/看到主页说明后端已经跑通。如果启动瞬间崩溃去logs/catalina.outLinux或logs/catalina.xxx.logWindows看最后的异常栈最常见的几种是 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)) { ListBook 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 }; ListBook 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 idbookSearchApp input typetext v-modelkeyword placeholder输入书名或作者 select v-modelsearchType option valuebookName按书名/option option valueauthor按作者/option option valuecategory按类别/option /select button clicksearchBooks搜索/button table tr v-forbook in books :keybook.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 数组直接赋值给booksVue 自动完成列表渲染。参数说明params对象的字段名要跟后端request.getParameter(type)、request.getParameter(keyword)严格对应大小写都不能错后端返回 JSON 需要先设置response.setContentType(application/json;charsetutf-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-6lg是桌面大屏每行放 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里写的DatabaseNamebookstore但脚本和手动建的表在另一个实例的默认库比如 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后追加;characterEncodingUTF-8参数表字段类型改用NVARCHAR并在 Servlet 里设置request.setCharacterEncoding(UTF-8)后再取参。如果还不行检查 JSP 页面头部的pageEncoding是否也声明成 UTF-8Java 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 页面显示 500ResultSet已关闭或连接已关闭现象第一次访问页面正常刷新后频繁出现「ResultSet closed」或「Connection closed」。原因在 DAO 方法里提前关闭了Connection、Statement、ResultSet而对象映射还发生在连接关闭之后或者写了静态连接对象多个请求共用同一个连接后一个请求把前一个的连接关闭了。解决严格遵循「先取数据后关连接」的顺序。JDBC 标准里ResultSet依赖StatementStatement依赖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浏览器从本地缓存里直接读页面不会重新请求服务端。解决修改完数据后强制刷新CtrlF5或者在 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 的分页写法、订单明细表的快照设计、按角色区分登录逻辑背后都是生产经验换来的。你在课程设计里把这些点讲清楚比堆砌十个功能页面更有说服力。从那以后我每次拿到一份课程设计源码都强制自己先跑通再拆解跑完通看表结构看完表结构看事务边界最后才看页面。走完这套流程这份资源才算真正属于你。希望帮到你。本文还有配套的精品资源点击获取