
简介这份资源是一套基于 Java SQLServer2008 的 Web 网上书店管理系统课程设计项目覆盖游客、会员、管理员三类角色的完整业务流程包括图书分类浏览、关键字检索、会员注册登录、购物车下单、订单处理与后台发货、新闻发布、评论管理等模块适合 Java Web 初学者及课程设计、毕业设计参考。压缩包共 460 个文件大小 9.89MB主要包含 88 个 JSP 页面与 29 个 Java 类控制业务逻辑配合 29 个 class 编译文件、6 个数据库相关文件db、sql、5 个 jar 依赖库及 6 个 CSS 样式文件同时含 198 个 gif 与 76 个 jpg 图片素材用于页面展示结构清晰便于按模块对照学习。已有 376 人学习下载。资源内附完整源代码、数据库脚本及项目部署所需文件可据此快速理解前后台交互、购物车实现及订单状态流转逻辑是巩固 JSP、Servlet、JDBC 综合应用能力的实用案例。1. 网上书店管理系统项目到底在做什么一个 JavaSQLServer2008 的 Web 课设模板“基于 JavaSQLServer2008 实现Web网上书店管理系统”这个标题十有八九是计算机专业课程设计或毕业设计里的经典题目。它的形态是一个 B/S 架构的网上书店前台让用户注册、登录、浏览图书、把书加进购物车、下单后台让管理员维护图书分类、图书信息、订单和公告。它不追求高并发不拆微服务核心就一件事——用最朴素的 JSP Servlet JDBC 把业务流完整跑通再连上一个老牌的 SQL Server 2008 数据库把增删改查做扎实。如果你正在为课设发愁或者想用一个完整 Web 项目练手这套东西就是最值得照着改的底子对于要接手老系统的 Java 工程师它也是理解老式 Java Web 程序如何对接旧版 SQL Server 的一扇窗口。这类带编号的项目包一般会给出源码、SQL 脚本和运行说明解压后最容易翻车的不是代码逻辑而是环境。数据库连不上、驱动加载失败、中文乱码这三个问题能劝退一半以上的人。这篇笔记按我自己平时带项目的顺序来写先拆表结构再搞定 JDBC 连接然后跑通登录、图书检索和下单最后把 SQLServer 2008 特有的几个坑集中说清楚。你不需要迷信这个标题有多先进把它当成一个能跑、能改、能答辩的骨架即可。2. 先拆数据模型再写代码网上书店的表结构设计与三层架构选型2.1 为什么是 JSPServletJDBC而不是 Spring Boot看到标题里的 SQLServer 2008就应该明白这不是一个追逐新框架的项目。SQL Server 2008 是 2008 年发布的数据库年代比 Spring Boot 早得多。课程设计模板之所以坚持用 JSP Servlet JDBC一方面是教学路线要求另一方面是 Spring Boot 的自动配置和依赖管理对老版本 SQL Server 的驱动兼容并不友好光是 javax.xml.bind 缺失、TLS 版本协商这些问题就够折腾半天。JDBC 直连反而最稳定依赖最少Tomcat 解压后扔进去就能跑答辩现场也不容易被环境问题卡住。我一般会把这类项目按三层架构组织JSP 负责页面展示Servlet 负责接收请求和跳转DAO 层负责 JDBC 数据访问。业务简单的时候可以在 Servlet 里直接写 JDBC但为了后续扩展还是建议至少把数据库访问抽到一个 DAO 类里。一个典型的 Web 项目结构长这样BookStoreWeb/ ├── src/ │ ├── com/bookstore/dao/ # 数据访问层 │ ├── com/bookstore/model/ # 实体类 │ ├── com/bookstore/servlet/ # 控制器 │ └── com/bookstore/util/ # DBHelper、分页工具 ├── WebContent/ │ ├── admin/ # 后台管理页面 │ ├── css/ images/ js/ │ ├── WEB-INF/ │ │ ├── lib/ # 放 sqljdbc4.jar │ │ └── web.xml │ └── index.jsp └── sql/ # 初始化脚本这种分层不是玄学它带来的直接好处是改页面不碰业务逻辑换数据库只改连接配置和 DAO 里的 SQL。很多 Java 工程师面试时被问的三层架构落到这个项目里就是这套目录。对课设而言它还能让老师一眼看出你项目结构是清楚的而不是堆一堆 JSP 草稿。2.2 八张核心表从用户表到订单明细表网上书店的业务可以拆成几个闭环用户注册登录、浏览图书、购物车、下单、后台管理。支撑这些闭环的表不需要多但一定得把关系理顺。我通常建议至少设计八张表用户表、管理员表、图书分类表、图书表、购物车表、订单表、订单明细表、公告表。核心字段关系如下表名核心字段作用usersuser_id, username, password, phone前台用户信息adminadmin_id, username, password后台管理员登录categorycategory_id, category_name图书分类booksbook_id, category_id, book_name, author, price, stock图书基本信息与库存cartcart_id, user_id, book_id, quantity临时购物车ordersorder_id, user_id, order_no, total_amount, status订单主表一个订单对应一个用户order_itemsitem_id, order_id, book_id, book_name, price, quantity订单明细一个订单对应多本书noticenotice_id, title, content, create_time首页公告课设加分项订单主表和订单明细表必须分开因为一个订单会包含多本不同的书。主表存用户、订单号、总金额和状态明细表冗余一份书名和价格快照这样即使以后图书信息改了历史订单依然能显示当时的购买内容。购物车表只做临时关联真正下单后要从购物车搬进订单明细再清空对应记录。图书表里的 stock 字段是关键后面下单事务扣库存时它就是并发的锁目标。公告表可有可无但加上它会让后台多一个可操作模块课设的功能数量看起来更完整。2.3 用 SQL 脚本初始化数据库2008 能直接跑的写法把表结构落到 SQL Server 2008 时别照抄 MySQL 的习惯很多语法细节不一样。下面是一套能直接执行的初始化脚本建库时就把排序规则指定好后面中文乱码会少一大半CREATE DATABASE BookStore COLLATE Chinese_PRC_CI_AS; GO USE BookStore; GO CREATE TABLE users ( user_id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL UNIQUE, password NVARCHAR(50) NOT NULL, real_name NVARCHAR(50), phone NVARCHAR(20), email NVARCHAR(50), create_time DATETIME DEFAULT GETDATE() ); GO CREATE TABLE category ( category_id INT IDENTITY(1,1) PRIMARY KEY, category_name NVARCHAR(50) NOT NULL ); GO CREATE TABLE books ( book_id INT IDENTITY(1,1) PRIMARY KEY, category_id INT FOREIGN KEY REFERENCES category(category_id), book_name NVARCHAR(100) NOT NULL, author NVARCHAR(50), publisher NVARCHAR(100), price DECIMAL(10,2), stock INT DEFAULT 0, cover_image NVARCHAR(200), description NVARCHAR(MAX), create_time DATETIME DEFAULT GETDATE() ); GO CREATE TABLE cart ( cart_id INT IDENTITY(1,1) PRIMARY KEY, user_id INT FOREIGN KEY REFERENCES users(user_id), book_id INT FOREIGN KEY REFERENCES books(book_id), quantity INT DEFAULT 1, add_time DATETIME DEFAULT GETDATE() ); GO CREATE TABLE orders ( order_id INT IDENTITY(1,1) PRIMARY KEY, user_id INT FOREIGN KEY REFERENCES users(user_id), order_no NVARCHAR(30) NOT NULL UNIQUE, total_amount DECIMAL(10,2) DEFAULT 0, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT GETDATE() ); GO CREATE TABLE order_items ( item_id INT IDENTITY(1,1) PRIMARY KEY, order_id INT FOREIGN KEY REFERENCES orders(order_id), book_id INT FOREIGN KEY REFERENCES books(book_id), book_name NVARCHAR(100), price DECIMAL(10,2), quantity INT ); GO CREATE TABLE admin ( admin_id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL, password NVARCHAR(50) NOT NULL ); GO INSERT INTO category(category_name) VALUES(NJava编程),(N前端开发),(N数据库); INSERT INTO books(category_id, book_name, author, publisher, price, stock) VALUES(1, NJava核心技术, NCay S. Horstmann, N机械工业出版社, 119.00, 20); INSERT INTO books(category_id, book_name, author, publisher, price, stock) VALUES(1, NEffective Java, NJoshua Bloch, N人民邮电出版社, 89.00, 15); INSERT INTO admin(username, password) VALUES(Nadmin, Nadmin); GO这段脚本里有几个值得注意的设计点用户名和密码字段用 NVARCHAR 而不是 VARCHARSQL Server 2008 的 VARCHAR 在处理中文字符时容易受代码页影响NVARCHAR 统一走 Unicode配合 Chinese_PRC_CI_AS 排序规则基本能根治中文乱码。价格字段用 DECIMAL(10,2)不用 FLOAT避免浮点数在金额计算上出现的误差。自增主键用 IDENTITY(1,1)和 MySQL 的 AUTO_INCREMENT 是同一回事。外键约束我建议还是加上虽然它会在插入数据时多一点检查成本但对课设来说有外键能直观告诉别人表关系是设计过的。图书表里的 stock 默认 0后面下单时用带条件的 UPDATE 来扣减比先查后改安全得多。初始数据里管理员账号密码都写了 admin方便一启动就能进入后台。3. 让 Java 连上 SQLServer 2008JDBC 驱动、连接串与最小环境配置3.1 驱动版本选择sqljdbc4.jar 能覆盖 Java 6/7/8Java 连 SQL Server 2008 不是一个空壳问题很多人在 “java如何连接sqlserver2008” 的答案里拿到一行连接串结果一跑就是 ClassNotFoundException。原因是驱动 jar 根本没进来。SQL Server 2008 对应的官方 JDBC 驱动是 sqljdbc4.jar它实现了 JDBC 4.0在 Java 6、7、8 上都能正常加载。如果你用的 JDK 是 8那么 sqljdbc4.jar 就是最稳妥的选择。这个 jar 从哪来最常见的是从 SQL Server 安装目录里找一般在安装路径下的 JDBC 子目录中也可以找单独的 Microsoft JDBC Driver 4.x 包。拿到后把它放到 WebContent/WEB-INF/lib 下面IDE 里还需要把它 add 到构建路径。如果是 Tomcat 跑重启 Tomcat 后驱动才会被加载。一个坑是如果本地 JDK 已经升到 11 或更高sqljdbc4.jar 会出现 “UnsupportedClassVersionError” 或 TLS 握手失败这时候得换更高版本的 mssql-jdbc 驱动。课程设计环境最好直接固定在 Java 8 sqljdbc4.jar省掉一堆兼容性问题。驱动选错不会报语法错误只会给你一个看起来莫名其妙的连接失败这也是我把版本选择放在第一位的原因。3.2 关键配置SQL Server 2008 的 TCP/IP 与混合认证代码写得再对如果 SQL Server 2008 本身没开远程连接连接字符串永远只会超时或拒绝。很多人一开始在本地用 SSMS 连得好好的Java 端却失败问题基本都出在这三步。第一步打开 SQL Server Configuration Manager展开“SQL Server 网络配置”找到对应实例的“协议”把 TCP/IP 启用。接着双击 TCP/IP 的属性切到“IP 地址”页确认 IPAll 的 TCP 端口是 1433。改完必须到“SQL Server 服务”里右键重启实例配置才会生效。第二步用 SSMS 连接数据库右键服务器属性切到“安全性”把服务器身份验证改成“SQL Server 和 Windows 身份验证模式”。这是让 Java 能用账号密码登录的前提。只改这里还不够要顺手把 sa 账号启用并设置一个强密码或者新建一个专用登录名比如 bookstore_user分配好对应数据库的权限。第三步如果代码在另一台机器上或本地开了防火墙1433 端口不放行同样连不上。Windows 系统检查防火墙入站规则加一条允许 1433 端口的规则。这一步经常被忽略表现为驱动类加载成功、连接也不报错但就是超时。这三步做完连接串里的 localhost 才能通向真实的数据库服务。记住一张图Java 进程 → 1433 端口 → SQL Server 实例 → 登录名 → 数据库。任一级断开报错都不一样。3.3 写一个 DBHelper 管理连接把连接串放进配置文件驱动和实例都准备好了接下来要写 Java 端的连接代码。不要把连接串硬编码在 DAO 里我一般会用一个 db.properties 文件加上一个 DBHelper 工具类统一管理。先看配置文件jdbc.drivercom.microsoft.sqlserver.jdbc.SQLServerDriver jdbc.urljdbc:sqlserver://localhost:1433;databaseNameBookStore;characterEncodingUTF-8 jdbc.usernamebookstore_user jdbc.password123456 jdbc.connectionTimeout5连接串里的 databaseName 就是刚才建库时的 BookStore注意是数据库名不是表名characterEncodingUTF-8 用来指定请求数据的字符集和数据库排序规则配合解决中文问题。connectionTimeout 设成 5 秒避免数据库没开启时页面白等。package com.bookstore.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import java.io.InputStream; public class DBHelper { private static String driver; private static String url; private static String username; private static String password; static { try (InputStream in DBHelper.class.getResourceAsStream(/db.properties)) { Properties props new Properties(); props.load(in); driver props.getProperty(jdbc.driver); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); Class.forName(driver); } catch (Exception e) { throw new RuntimeException(数据库初始化失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }代码逻辑不复杂静态代码块在类加载时读配置文件、注册驱动getConnection 每次调用都通过 DriverManager 创建新连接。Class.forName(driver) 在 JDBC 4.0 之后可以省略但写了不会错也能让你更清楚驱动是怎么注册的。close 方法三个参数从左到右释放资源ResultSet 和 Statement 不关会导致连接池耗尽老项目里这种坑特别多。配置文件放在 src 根目录时类加载路径是 /db.properties。IDE 里改了配置后记得重新构建输出目录否则会拿到旧的 class 文件。DBHelper 写完后可以先写一个 main 方法测试 getConnection打印一下连接对象能打出来就说明环境和代码都通了。这一步通过之前不要急着写业务否则后面所有报错都会混在一起很难定位。4. 把网上书店的核心流程跑通登录、图书检索、购物车与下单4.1 登录用 PreparedStatement 防注入登录模块是网上书店的入口也是最能体现代码功底的地方。最基础但绝对不能省的操作是把 SQL 参数化。直接拼字符串的写法在 java 面试题里属于是必死的反例比如String sql SELECT * FROM users WHERE username username AND password password ;这种写法只要在用户名输入框里填 OR 11就能绕过密码校验。课设阶段不强调安全但答辩时老师喜欢问这个点。我一般会这样写 UserDAOpublic User findByLogin(String username, String password) { String sql SELECT user_id, username, real_name FROM users WHERE username? AND password?; try (Connection conn DBHelper.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setUserId(rs.getInt(user_id)); user.setUsername(rs.getString(username)); user.setRealName(rs.getString(real_name)); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; }这里的 ? 占位符由 PreparedStatement 处理用户输入会被当作纯字符串而不是 SQL 命令。setString 的索引从 1 开始按 SQL 里的问号顺序来。登录成功后把 User 对象放进 session页面就能根据 session 判断是否显示“欢迎 xxx”和购物车入口。若返回 null 则跳转到登录页并带一个 error 参数。密码存储是个单独的注意项。课程设计常用明文但只要有时间建议在注册时用 MD5 存哈希值登录时把输入密码也 MD5 后再比对。SQLServer 2008 的 users 表中 password 字段宽度我留了 50是因为 MD5 的十六进制串是 32 位如果是 SHA-256 就得 64 位预留大一点总有后悔药。4.2 图书检索分页查询用 ROW_NUMBER() 替代 LIMIT图书列表不能把所有数据一次性全查出来分页是必须的。MySQL 里写LIMIT ?, ?很舒服但 SQL Server 2008 不支持这个语法直接写会报“关键字 LIMIT 附近有语法错误”。我一般使用 ROW_NUMBER() 窗口函数做分页SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY book_id) AS rowIndex, b.* FROM books b ) AS t WHERE t.rowIndex BETWEEN ? AND ?子查询先对 books 表按 book_id 排序并生成行号外层再用 BETWEEN 截取第 N 页。换算规则是第 pageNo 页每页 pageSize 条起始行 (pageNo - 1) * pageSize 1结束行 pageNo * pageSize。对应的 DAO 方法public ListBook findBooksByPage(int pageNo, int pageSize) { ListBook list new ArrayList(); int start (pageNo - 1) * pageSize 1; int end pageNo * pageSize; String sql SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY book_id) AS rowIndex, b.* FROM books b) AS t WHERE t.rowIndex BETWEEN ? AND ?; try (Connection conn DBHelper.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, start); ps.setInt(2, end); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book book new Book(); book.setBookId(rs.getInt(book_id)); book.setBookName(rs.getString(book_name)); book.setAuthor(rs.getString(author)); book.setPrice(rs.getDouble(price)); book.setStock(rs.getInt(stock)); list.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return list; }分页参数要注意两个坑一是 pageNo 小于 1 时要在 Servlet 里校正否则 start 变成 0 或负数BETWEEN 会查出空结果二是 pageSize 不要允许前端传 10000后台要限制一个最大值否则一次查询能拖垮整个列表页。还需要一个求总数的方法SELECT COUNT(*) FROM books用来计算总页数这个值通常缓存起来避免每次翻页都全表统计。4.3 购物车与订单在一个事务里扣库存和生成订单下单是整个系统里唯一必须保证原子性的业务。用户点结算后系统要做的事依次是读购物车明细、生成订单主表、生成订单明细、扣减图书库存、清空购物车。这几步如果分散执行中间任何一步失败都会留下脏数据比如扣了库存却没生成订单。因此必须用一个数据库事务把这些操作包起来。我一般把下单逻辑放在 OrderService 或 OrderDAO 的一个方法里连接从 DBHelper 获取后先关闭自动提交public void createOrder(int userId) throws Exception { String orderNo B System.currentTimeMillis() userId; Connection conn null; try { conn DBHelper.getConnection(); conn.setAutoCommit(false); // 1. 汇总购物车金额并插入订单主表 String sumSql SELECT SUM(b.price * c.quantity) FROM cart c JOIN books b ON c.book_id b.book_id WHERE c.user_id ?; PreparedStatement psSum conn.prepareStatement(sumSql); psSum.setInt(1, userId); ResultSet rsSum psSum.executeQuery(); rsSum.next(); double total rsSum.getDouble(1); rsSum.close(); psSum.close(); String insertOrder INSERT INTO orders(user_id, order_no, total_amount) VALUES(?, ?, ?); PreparedStatement psOrder conn.prepareStatement(insertOrder); psOrder.setInt(1, userId); psOrder.setString(2, orderNo); psOrder.setDouble(3, total); psOrder.executeUpdate(); psOrder.close(); // 2. 取出自增订单号 int orderId; PreparedStatement psGetId conn.prepareStatement(SELECT CAST(SCOPE_IDENTITY() AS INT)); ResultSet rsId psGetId.executeQuery(); rsId.next(); orderId rsId.getInt(1); rsId.close(); psGetId.close(); // 3. 购物车明细写入订单明细并扣库存 String cartSql SELECT book_id, quantity FROM cart WHERE user_id ?; PreparedStatement psCart conn.prepareStatement(cartSql); psCart.setInt(1, userId); ResultSet rsCart psCart.executeQuery(); String insertItem INSERT INTO order_items(order_id, book_id, book_name, price, quantity) SELECT ?, book_id, book_name, price, ? FROM books WHERE book_id ?; String updateStock UPDATE books SET stock stock - ? WHERE book_id ? AND stock ?; PreparedStatement psItem conn.prepareStatement(insertItem); PreparedStatement psStock conn.prepareStatement(updateStock); while (rsCart.next()) { int bookId rsCart.getInt(book_id); int quantity rsCart.getInt(quantity); psItem.setInt(1, orderId); psItem.setInt(2, quantity); psItem.setInt(3, bookId); psItem.executeUpdate(); psStock.setInt(1, quantity); psStock.setInt(2, bookId); psStock.setInt(3, quantity); if (psStock.executeUpdate() 0) { throw new Exception(库存不足book_id bookId); } } rsCart.close(); psCart.close(); psItem.close(); psStock.close(); // 4. 清空购物车并提交 String clearCart DELETE FROM cart WHERE user_id ?; PreparedStatement psClear conn.prepareStatement(clearCart); psClear.setInt(1, userId); psClear.executeUpdate(); psClear.close(); conn.commit(); } catch (Exception e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }这段代码有四个关键点。第一setAutoCommit(false) 之后所有 SQL 都进入同一个事务只有最后 commit 才会整体生效。第二获取自增订单号用 SELECT CAST(SCOPE_IDENTITY() AS INT)它在同一个连接、同一次会话内拿到最后插入的自增 ID比 getGeneratedKeys 在这个老驱动组合下更可靠。第三扣库存时 UPDATE 语句带stock ?条件如果库存不够会返回 0 行更新立刻抛异常并回滚从根上避免了超卖。第四finally 里把自动提交恢复为 true否则连接还回池子后状态是污染的下一个人用同一连接会把多条 SQL 合进一个意外事务。事务是这类系统的底线。很多入门项目把下单代码写在 JSP 页面里一条一条 SQL 执行表面能跑通但并发测试一压就原形毕露。你不需要买服务器做压测只要打开两个浏览器同时下同一本书就能复现库存扣成负数的情况。5. SQLServer 2008 连 Java 的五个典型坑从驱动 ClassNotFoundException 到中文乱码5.1 驱动与连接阶段jar 缺失、TCP/IP 没开、登录失败现象 1Tomcat 启动或访问页面时报java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver。原因很简单sqljdbc4.jar 不在 WEB-INF/lib 里或 IDE 没有把它加到运行库。解决方法是把 jar 复制到 WEB-INF/lib然后在 IDE 里对项目右键重新构建确认输出目录的 lib 下有这个文件。如果用的 IntelliJ IDEA还要看一下 Artifacts 里是否包含了这个 jar光在 Modules 里加了依赖打包时也可能漏掉。现象 2程序卡一会儿后报Connection refused或connect timed out。原因基本是 SQL Server 2008 的 TCP/IP 协议没启用或者端口不是 1433再或者防火墙拦截。解决方法是按 3.2 节的内容在 SQL Server Configuration Manager 里启用 TCP/IP确认 IPAll 端口重启 SQL Server 服务再检查防火墙。不要觉得本地连接没开防火墙就没事Windows 自带的防火墙对 localhost 访问一般放行但某些安全软件会拦。现象 3报Login failed for user sa。原因一个是被连接串传的密码不对另一个是 SQL Server 实例还停留在 Windows 身份验证模式根本不接受 SQL 账号密码。解决方法是先在 SSMS 中确认能用 sa 登录然后用管理员身份打开 SSMS把服务器属性里的身份验证模式改成混合模式启用 sa 并重设密码再重启服务。如果改完还是不行看 SQL Server 日志里是否有“用户 sa 登录失败”的具体错误码错误码 18456 后面的小数字能辅助判断具体原因。5.2 数据读写阶段中文乱码、LIMIT 语法错现象 4页面上显示的中文变成问号或乱码数据库中存进去的中文也是乱码。原因通常是三个地方的编码不统一数据库排序规则、JDBC 连接串、JSP 页面编码。解决方法是建库时用COLLATE Chinese_PRC_CI_AS连接串里加characterEncodingUTF-8JSP 首行写明pageEncodingUTF-8并且在 web.xml 或过滤器中统一设置 request 和 response 的编码。老项目里常见的乱码根源是页面用 UTF-8数据库默认排序规则却是 Latin1_General这时存进去的汉字直接变成问号。filter filter-nameEncodingFilter/filter-name filter-classcom.bookstore.util.EncodingFilter/filter-class /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping过滤器里只需要三行request.setCharacterEncoding(UTF-8)、response.setCharacterEncoding(UTF-8)、chain.doFilter。做了这一步再加上数据库和连接串的编码统一乱码基本能根治。现象 5分页执行时报Incorrect syntax near LIMIT。原因很好识别SQL Server 2008 没有 MySQL 的 LIMIT 子句。解决方法是改用 ROW_NUMBER() OVER或者用SELECT TOP (?)写法。TOP 在动态拼下一页时很麻烦需要先算上一页最大的 book_id 再过滤而 ROW_NUMBER 的 BETWEEN 方案语义清楚、易读易改。这里要特别提醒把 SQL 从 MySQL 迁移到 SQL Server 时除了 LIMIT反引号和 IFNULL 这类函数也是重灾区不要以为换个连接串就万事大吉。6. 上线前给订单流程加一把锁用事务和存储过程固化下单逻辑第 4 章把下单逻辑写在了 Java 代码里事务是 Java 端控制的。如果项目还要继续往前走一步我会建议把整个下单过程封装成一个存储过程让数据库端自己完成事务控制。这样做的好处主要有三个第一从 Java 到数据库只有一次远程调用而不是七八次来回性能更高第二事务在存储过程内部连接断了数据库也会自动回滚比靠 Java 代码小心翼翼地在 finally 里恢复状态可靠第三DBA 可以直接审查存储过程对后续做并发安全的调优更有抓手。下面是一个简化版的存储过程骨架接收用户 ID在内部生成订单、扣库存并返回新订单号CREATE PROCEDURE usp_CreateOrder userId INT, newOrderId INT OUTPUT AS BEGIN DECLARE total DECIMAL(10,2); SET NOCOUNT ON; BEGIN TRANSACTION; BEGIN TRY SELECT total SUM(b.price * c.quantity) FROM cart c JOIN books b ON c.book_id b.book_id WHERE c.user_id userId; INSERT INTO orders(user_id, order_no, total_amount) VALUES(userId, CONCAT(B, REPLACE(CONVERT(VARCHAR, GETDATE(), 112), -, ), userId), total); SET newOrderId CAST(SCOPE_IDENTITY() AS INT); INSERT INTO order_items(order_id, book_id, book_name, price, quantity) SELECT newOrderId, book_id, book_name, price, quantity FROM cart c JOIN books b ON c.book_id b.book_id WHERE c.user_id userId; UPDATE books SET stock stock - c.quantity FROM books b JOIN cart c ON b.book_id c.book_id WHERE c.user_id userId AND b.stock c.quantity; IF ROWCOUNT 0 BEGIN RAISERROR(N库存不足, 16, 1); END DELETE FROM cart WHERE user_id userId; COMMIT TRANSACTION; END TRY BEGIN CATCH ROLLBACK TRANSACTION; THROW; END CATCH END GO存储过程里的 UPDATE 用多表联合更新并且带上b.stock c.quantity的条件一次语句就完成扣库存和校验。如果库存不够ROWCOUNT 会是 0RAISERROR 触发后直接回滚。Java 端调用这个存储过程时只需要一个 CallableStatement 设置输入参数并注册输出参数业务代码比第 4 章的版本简洁很多事务控制的复杂度也从 Java 转移到了 SQL Server。我自己第一次做课设时把下单逻辑整个摊在 Servlet 里结果两个浏览器同时下一本库存只剩 1 的书库存直接变成负数翻车翻得很彻底。后来才长记性凡是涉及扣减账目或金额的操作要么用代码事务要么干脆交给存储过程。如果你时间只够做一件事我强烈建议把下单这块改成事务版本再去跑一轮并发验证——这是整个网上书店里最有技术含量也最值得在答辩时拿出来讲的点。希望帮到你。本文还有配套的精品资源点击获取