在传统图书行业数字化转型的浪潮中,一个高效、集成的管理解决方案显得尤为重要。本文介绍的系统正是针对这一需求,采用经典的JSP+Servlet技术栈,构建了一个集前台在线商城与后台库存管理于一体的综合性平台。该系统将图书的采购、入库、上架、销售、订单处理等业务流程进行了数字化整合,实现了全流程的闭环管理。
技术架构与设计模式
系统严格遵循MVC设计模式,确保了代码的良好分层与可维护性。Servlet作为控制器层,负责拦截所有客户端请求,进行业务逻辑处理和数据路由。模型层由一系列JavaBean构成,封装了核心的业务实体和数据处理逻辑。视图层则使用JSP技术,结合JSTL标签库和EL表达式,实现动态页面的渲染,有效避免了在页面中嵌入过多的Java代码。
数据持久化层基于JDBC直接连接MySQL数据库,通过编写高效的SQL语句和执行事务管理,保证了数据操作的准确性和一致性。整个系统部署于Tomcat等Servlet容器中,无需依赖复杂的框架,结构清晰,易于理解和二次开发。
数据库设计剖析
数据库设计是系统的基石,共设计了6张核心表来支撑业务运转。以下重点分析其中几个关键表的设计。
1. 图书信息表
图书表的设计充分考虑了图书商品的各类属性,并建立了与分类表的外键关联。
CREATE TABLE books (
book_id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author VARCHAR(255) NOT NULL,
publisher VARCHAR(255),
publish_date DATE,
isbn VARCHAR(20) UNIQUE,
category_id INT,
price DECIMAL(10, 2) NOT NULL,
stock_quantity INT NOT NULL DEFAULT 0,
description TEXT,
image_url VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE SET NULL
);
该表的亮点在于:
- 使用
AUTO_INCREMENT自增主键,确保每本图书的唯一标识。 isbn字段设置唯一约束,符合图书行业的规范,防止重复录入。price字段采用DECIMAL(10,2)类型,精确存储货币值。stock_quantity字段实时反映库存数量,是库存管理的核心。- 包含
created_at和updated_at两个时间戳字段,用于审计和数据追踪。 - 通过
category_id外键与分类表关联,实现图书的分类管理。
2. 订单表
订单表的设计体现了电子商务的核心,它记录了交易的关键信息,并与用户和订单明细表关联。
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total_amount DECIMAL(10, 2) NOT NULL,
status ENUM('pending', 'confirmed', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
shipping_address TEXT NOT NULL,
payment_method VARCHAR(50),
payment_status ENUM('unpaid', 'paid', 'refunded') DEFAULT 'unpaid',
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
该表的亮点在于:
- 使用
ENUM类型定义订单状态和支付状态,确保状态值的规范性和有效性,如'pending'(待处理)、'shipped'(已发货)等。 total_amount字段记录订单总金额,由订单明细项计算得出。user_id外键关联用户表,清晰记录订单归属。ON DELETE CASCADE约束确保当用户被删除时,其关联的订单也被自动清理,维护数据一致性。
3. 订单明细表
此表是订单与图书的关联表,解决了订单与商品的多对多关系,是业务逻辑的关键。
CREATE TABLE order_items (
item_id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
book_id INT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
unit_price DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE,
FOREIGN KEY (book_id) REFERENCES books(book_id)
);
该表的亮点在于:
- 记录了购买时图书的单价(
unit_price),这是一个重要的历史快照。即使后续图书价格变动,订单中的价格也不会改变。 quantity字段设置了CHECK约束,确保购买数量为正数。- 通过
order_id和book_id两个外键,将订单、图书、购买数量和价值紧密联系起来。
核心功能实现解析
1. 用户登录与会话管理
用户认证是系统安全的第一道屏障。登录Servlet负责处理认证逻辑,并创建用户会话。
// LoginServlet.java
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
String role = request.getParameter("role"); // "user" or "admin"
UserService userService = new UserService();
User user = userService.authenticate(username, password, role);
if (user != null) {
HttpSession session = request.getSession();
session.setAttribute("user", user);
session.setAttribute("role", role);
if ("admin".equals(role)) {
response.sendRedirect("admin/dashboard.jsp");
} else {
response.sendRedirect("user/home.jsp");
}
} else {
request.setAttribute("errorMessage", "Invalid username, password, or role.");
request.getRequestDispatcher("login.jsp").forward(request, response);
}
}
}

此段代码的关键点在于:
- 使用
@WebServlet注解配置Servlet映射,简化部署描述符配置。 - 根据
role参数区分普通用户和管理员,导向不同的主页。 - 认证成功后,将完整的
User对象和角色信息存入HttpSession,便于后续请求中识别用户身份和权限。 - 认证失败时,通过
request.setAttribute设置错误信息,并转发回登录页显示。
2. 购物车功能实现
购物车是电商系统的核心功能,采用Session来临时存储用户的选购商品。
// CartServlet.java
@WebServlet("/cart/*")
public class CartServlet extends HttpServlet {
private BookService bookService = new BookService();
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String pathInfo = request.getPathInfo();
HttpSession session = request.getSession();
Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart");
if (cart == null) {
cart = new HashMap<>();
session.setAttribute("cart", cart);
}
if ("/add".equals(pathInfo)) {
int bookId = Integer.parseInt(request.getParameter("bookId"));
int quantity = Integer.parseInt(request.getParameter("quantity"));
Book book = bookService.getBookById(bookId);
if (book != null) {
CartItem item = cart.get(bookId);
if (item != null) {
item.setQuantity(item.getQuantity() + quantity);
} else {
item = new CartItem(book, quantity);
cart.put(bookId, item);
}
}
response.sendRedirect(request.getContextPath() + "/book/list.jsp");
} else if ("/update".equals(pathInfo)) {
// ... 更新购物车商品数量逻辑
} else if ("/remove".equals(pathInfo)) {
// ... 移除商品逻辑
}
}
}
购物车项CartItem是一个标准的JavaBean,封装了商品信息和数量。
// CartItem.java
public class CartItem {
private Book book;
private int quantity;
public CartItem(Book book, int quantity) {
this.book = book;
this.quantity = quantity;
}
// 计算此项目总价
public BigDecimal getTotalPrice() {
return book.getPrice().multiply(new BigDecimal(quantity));
}
// Getters and Setters
public Book getBook() { return book; }
public void setBook(Book book) { this.book = book; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
}

此设计的特点:
- 购物车以
Map<Integer, CartItem>形式存储在Session中,键为图书ID,值为购物车项,便于通过ID快速查找和更新。 CartItem类不仅存储数量,还持有Book对象的引用,方便在页面上显示完整的图书信息。- 业务逻辑如计算单项总价封装在
CartItem中,符合面向对象设计原则。
3. 订单生成与库存同步
提交订单是系统最关键的事务性操作,必须确保库存减少和订单创建的一致性。
// OrderServlet.java
@WebServlet("/order/place")
public class OrderServlet extends HttpServlet {
private OrderService orderService = new OrderService();
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart");
if (user == null) {
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
if (cart == null || cart.isEmpty()) {
request.setAttribute("errorMessage", "Your cart is empty.");
request.getRequestDispatcher("/cart/view.jsp").forward(request, response);
return;
}
String shippingAddress = request.getParameter("shippingAddress");
String paymentMethod = request.getParameter("paymentMethod");
try {
// 调用Service层方法创建订单,该方法包含事务管理
Order order = orderService.createOrder(user.getUserId(), cart, shippingAddress, paymentMethod);
// 清空购物车
session.removeAttribute("cart");
request.setAttribute("order", order);
request.getRequestDispatcher("/order/confirmation.jsp").forward(request, response);
} catch (Exception e) {
e.printStackTrace();
request.setAttribute("errorMessage", "Failed to create order: " + e.getMessage());
request.getRequestDispatcher("/cart/checkout.jsp").forward(request, response);
}
}
}
订单生成的核心业务逻辑在Service层,它需要在一个数据库事务中完成多个操作。
// OrderService.java
public class OrderService {
private OrderDAO orderDAO = new OrderDAO();
private BookDAO bookDAO = new BookDAO();
public Order createOrder(int userId, Map<Integer, CartItem> cart, String shippingAddress, String paymentMethod) throws Exception {
Connection conn = null;
try {
conn = DatabaseUtil.getConnection();
conn.setAutoCommit(false); // 开启事务
// 1. 计算订单总金额并检查库存
BigDecimal totalAmount = BigDecimal.ZERO;
for (CartItem item : cart.values()) {
Book book = item.getBook();
if (book.getStockQuantity() < item.getQuantity()) {
throw new Exception("Insufficient stock for book: " + book.getTitle());
}
totalAmount = totalAmount.add(item.getTotalPrice());
}
// 2. 插入订单主记录
Order order = new Order();
order.setUserId(userId);
order.setTotalAmount(totalAmount);
order.setShippingAddress(shippingAddress);
order.setPaymentMethod(paymentMethod);
int orderId = orderDAO.insertOrder(conn, order);
// 3. 插入订单明细并扣减库存
for (CartItem item : cart.values()) {
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(orderId);
orderItem.setBookId(item.getBook().getBookId());
orderItem.setQuantity(item.getQuantity());
orderItem.setUnitPrice(item.getBook().getPrice());
orderDAO.insertOrderItem(conn, orderItem);
// 更新图书库存
int affectedRows = bookDAO.updateStock(conn, item.getBook().getBookId(), -item.getQuantity());
if (affectedRows == 0) {
throw new Exception("Failed to update stock for book ID: " + item.getBook().getBookId());
}
}
conn.commit(); // 提交事务
order.setOrderId(orderId);
return order;
} catch (Exception e) {
if (conn != null) {
try {
conn.rollback(); // 回滚事务
} catch (SQLException ex) {
ex.printStackTrace();
}
}
throw e;
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}

此事务处理的关键点:
- 手动管理数据库连接和事务,通过
setAutoCommit(false)开启事务,commit()提交,rollback()回滚。 - 在事务内依次执行:库存检查、插入订单、插入订单明细、更新库存。任何一步失败,整个事务回滚,防止数据不一致。
- 库存检查在事务内进行,可以防止高并发下的超卖问题。
4. 后台图书管理
管理员可以对图书进行增删改查操作,以下是更新图书信息的Servlet。
// AdminBookServlet.java
@WebServlet("/admin/book/update")
public class AdminBookServlet extends HttpServlet {
private BookService bookService = new BookService();
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 权限检查
HttpSession session = request.getSession();
if (!"admin".equals(session.getAttribute("role"))) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
try {
int bookId = Integer.parseInt(request.getParameter("bookId"));
String title = request.getParameter("title");
String author = request.getParameter("author");
BigDecimal price = new BigDecimal(request.getParameter("price"));
int stock = Integer.parseInt(request.getParameter("stockQuantity"));
int categoryId = Integer.parseInt(request.getParameter("categoryId"));
Book book = new Book();
book.setBookId(bookId);
book.setTitle(title);
book.setAuthor(author);
book.setPrice(price);
book.setStockQuantity(stock);
book.setCategoryId(categoryId);
// 设置其他字段...
boolean success = bookService.updateBook(book);
if (success) {
response.sendRedirect(request.getContextPath() + "/admin/book/list.jsp?message=Book updated successfully");
} else {
request.setAttribute("errorMessage", "Failed to update book.");
request.getRequestDispatcher("/admin/book/edit.jsp?id=" + bookId).forward(request, response);
}
} catch (Exception e) {
e.printStackTrace();
request.setAttribute("errorMessage", "Error: " + e.getMessage());
request.getRequestDispatcher("/admin/book/edit.jsp").forward(request, response);
}
}
}

5. 数据访问层实现
以图书DAO为例,展示基础的CRUD操作和数据库连接管理。
// BookDAO.java
public class BookDAO {
public Book getBookById(int bookId) {
String sql = "SELECT * FROM books WHERE book_id = ?";
try (Connection conn = DatabaseUtil.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, bookId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return extractBookFromResultSet(rs);
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public List<Book> getBooksByCategory(int categoryId) {
List<Book> books = new ArrayList<>();
String sql = "SELECT * FROM books WHERE category_id = ?";
// ... 类似的数据库操作逻辑
return books;
}
private Book extractBookFromResultSet(ResultSet rs) throws SQLException {
Book book = new Book();
book.setBookId(rs.getInt("book_id"));
book.setTitle(rs.getString("title"));
book.setAuthor(rs.getString("author"));
book.setPrice(rs.getBigDecimal("price"));
book.setStockQuantity(rs.getInt("stock_quantity"));
// 设置其他字段...
return book;
}
}
6. 前台图书列表展示
JSP页面使用JSTL和EL表达式展示图书列表,代码清晰易读。
<%-- book-list.jsp --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<div class="book-grid">
<c:forEach var="book" items="${bookList}">
<div class="book-item">
<img src="${book.imageUrl}" alt="${book.title}" class="book-cover">
<h3>${book.title}</h3>
<p>作者: ${book.author}</p>
<p class="price">¥${book.price}</p>
<c:choose>
<c:when test="${book.stockQuantity > 0}">
<span class="stock in-stock">有货 (${book.stockQuantity}本)</span>
<form action="cart/add" method="post" class="add-to-cart-form">
<input type="hidden" name="bookId" value="${book.bookId}">
<input type="number" name="quantity" value="1" min="1" max="${book.stockQuantity}">
<button type="submit">加入购物车</button>
</form>
</c:when>
<c:otherwise>
<span class="stock out-of-stock">缺货</span>
</c:otherwise>
</c:choose>
</div>
</c:forEach>
</