在数字化阅读日益普及的今天,传统图书流转效率低下和社区分享互动性弱的问题日益凸显。一款基于SSH(Struts2 + Spring + Hibernate)整合框架开发的图书共享与推荐平台应运而生,通过线上化的租借流程与智能推荐机制,有效盘活闲置图书资源,构建以书会友的互动社区。
系统架构与技术栈
该平台采用经典的三层架构设计,各层职责分明。表现层使用Struts2框架处理用户请求与页面跳转,通过Action类接收前端表单数据并调用业务逻辑;业务层由Spring框架进行管理,利用IoC容器实现服务组件的依赖注入与事务控制;数据持久层依托Hibernate实现对象关系映射,简化对图书信息、用户订单等数据的CRUD操作。
技术栈配置如下:
<!-- Struts2核心配置 -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.5.30</version>
</dependency>
<!-- Spring Web集成 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.18</version>
</dependency>
<!-- Hibernate核心 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.7.Final</version>
</dependency>
数据库设计亮点分析
借阅业务核心表设计
borrow_book表作为租借业务的核心,采用多外键关联设计确保数据完整性:
CREATE TABLE `borrow_book` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` varchar(255) DEFAULT NULL COMMENT '借阅编号',
`createTime` datetime DEFAULT NULL COMMENT '创建时间',
`isDelete` int(11) DEFAULT NULL COMMENT '是否删除',
`address_id` int(11) DEFAULT NULL COMMENT '地址ID',
`book_id` int(11) DEFAULT NULL COMMENT '图书ID',
`user_id` int(11) DEFAULT NULL COMMENT '用户ID',
PRIMARY KEY (`id`),
KEY `FK_o68m15drmbidjf8nuh8jlddnt` (`address_id`),
KEY `FK_219lk7xgyf8pjxmio6fqx84ws` (`book_id`),
KEY `FK_b23nxw5ngbtebsghdi1r0ipmp` (`user_id`),
CONSTRAINT `FK_219lk7xgyf8pjxmio6fqx84ws` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`),
CONSTRAINT `FK_b23nxw5ngbtebsghdi1r0ipmp` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
CONSTRAINT `FK_o68m15drmbidjf8nuh8jlddnt` FOREIGN KEY (`address_id`) REFERENCES `address` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='借阅图书表'
设计亮点包括:
- 软删除机制:
isDelete字段实现逻辑删除,保留历史数据追溯能力 - 业务编码唯一性:
code字段存储借阅编号,支持业务查询和跟踪 - 多维度索引优化:针对
address_id、book_id、user_id建立外键索引,提升关联查询性能 - 时间戳管理:
createTime自动记录业务发生时间,便于数据分析和统计
地址信息分级存储设计
address表采用四级行政区划分离存储,支持灵活的地址管理和配送优化:
CREATE TABLE `address` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`address` varchar(255) DEFAULT NULL COMMENT '详细地址',
`code` varchar(255) DEFAULT NULL COMMENT '邮政编码',
`isDelete` int(11) DEFAULT NULL COMMENT '是否删除',
`jiedao` varchar(255) DEFAULT NULL COMMENT '街道',
`name` varchar(255) DEFAULT NULL COMMENT '收货人姓名',
`phone` varchar(255) DEFAULT NULL COMMENT '联系电话',
`sheng` varchar(255) DEFAULT NULL COMMENT '省份',
`shi` varchar(255) DEFAULT NULL COMMENT '城市',
`xian` varchar(255) DEFAULT NULL COMMENT '县区',
`user_id` int(11) DEFAULT NULL COMMENT '用户ID',
PRIMARY KEY (`id`),
KEY `FK_7rod8a71yep5vxasb0ms3osbg` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='地址表'
分级地址设计支持按地域进行图书配送优化和用户群体分析,为后续的区域化推荐提供数据基础。
图书信息扩展性设计
book表设计充分考虑了平台的可扩展性:
CREATE TABLE `book` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`bookUrl` varchar(255) DEFAULT NULL COMMENT '图书图片URL',
`isDelete` int(11) DEFAULT NULL COMMENT '是否删除',
`isJd` int(11) DEFAULT NULL COMMENT '是否京东图书',
`name` varchar(255) DEFAULT NULL COMMENT '图书名称',
`oneType` int(11) DEFAULT NULL COMMENT '一级分类',
`price` varchar(255) DEFAULT NULL COMMENT '原价',
`sumNum` int(11) DEFAULT NULL COMMENT '库存数量',
`twoType` int(11) DEFAULT NULL COMMENT '二级分类',
`ms` varchar(6000) DEFAULT NULL COMMENT '图书描述',
`salePrice` varchar(255) DEFAULT NULL COMMENT '销售价格',
`user_id` int(11) DEFAULT NULL COMMENT '用户ID',
`jduser_id` int(11) DEFAULT NULL COMMENT '京东用户ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=65 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='图书表'
特殊设计包括:
- 双用户关联:同时支持普通用户和京东用户图书上架
- 大文本描述:
ms字段采用6000字符长度,满足详细图书介绍需求 - 价格策略分离:原价与销售价格分开存储,支持灵活的价格策略
- 分类层级管理:两级分类设计支持更精细的图书归类
核心功能实现
智能图书推荐引擎
平台集成了基于用户行为的协同过滤推荐算法,通过Spring服务封装实现个性化推荐:
@Service("bookRecommendService")
@Transactional
public class BookRecommendServiceImpl implements BookRecommendService {
@Autowired
private UserBehaviorDao userBehaviorDao;
@Autowired
private BookDao bookDao;
/**
* 基于用户协同过滤的图书推荐
*/
@Override
public List<Book> getPersonalizedRecommendations(Integer userId, int limit) {
// 1. 获取目标用户的行为数据
List<UserBehavior> targetUserBehaviors = userBehaviorDao.findByUserId(userId);
// 2. 查找相似用户
Map<Integer, Double> similarUsers = findSimilarUsers(userId, targetUserBehaviors);
// 3. 生成推荐图书列表
List<Book> recommendedBooks = generateRecommendations(userId, similarUsers, limit);
return recommendedBooks;
}
/**
* 计算用户相似度(余弦相似度算法)
*/
private Map<Integer, Double> findSimilarUsers(Integer targetUserId,
List<UserBehavior> targetBehaviors) {
Map<Integer, Double> similarityScores = new HashMap<>();
// 构建目标用户向量
Map<Integer, Double> targetUserVector = buildUserVector(targetBehaviors);
// 获取其他用户数据
List<Object[]> otherUsers = userBehaviorDao.findOtherUsers(targetUserId);
for (Object[] otherUser : otherUsers) {
Integer otherUserId = (Integer) otherUser[0];
List<UserBehavior> otherBehaviors = userBehaviorDao.findByUserId(otherUserId);
Map<Integer, Double> otherUserVector = buildUserVector(otherBehaviors);
double similarity = calculateCosineSimilarity(targetUserVector, otherUserVector);
if (similarity > 0.2) { // 设置相似度阈值
similarityScores.put(otherUserId, similarity);
}
}
return similarityScores;
}
/**
* 余弦相似度计算
*/
private double calculateCosineSimilarity(Map<Integer, Double> vector1,
Map<Integer, Double> vector2) {
double dotProduct = 0.0;
double norm1 = 0.0;
double norm2 = 0.0;
// 计算点积
for (Integer bookId : vector1.keySet()) {
if (vector2.containsKey(bookId)) {
dotProduct += vector1.get(bookId) * vector2.get(bookId);
}
norm1 += Math.pow(vector1.get(bookId), 2);
}
for (Double value : vector2.values()) {
norm2 += Math.pow(value, 2);
}
if (norm1 == 0 || norm2 == 0) {
return 0.0;
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
}

分布式事务管理的租借流程
租借业务涉及多个数据表的更新操作,采用Spring声明式事务确保数据一致性:
@Controller("borrowAction")
@Scope("prototype")
public class BorrowAction extends BaseAction<BorrowBook> {
@Autowired
private BorrowService borrowService;
@Autowired
private BookService bookService;
@Autowired
private UserService userService;
/**
* 执行图书租借操作
*/
@Action(value = "borrow_executeBorrow",
results = {
@Result(name = "success", type = "redirect", location = "borrow_list.action"),
@Result(name = "error", location = "/WEB-INF/pages/error.jsp")
})
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public String executeBorrow() {
try {
// 1. 验证图书库存
Book book = bookService.getById(model.getBookId());
if (book.getSumNum() <= 0) {
addActionError("图书库存不足");
return ERROR;
}
// 2. 验证用户积分是否足够
User user = userService.getById(model.getUserId());
double requiredPoints = calculateRequiredPoints(book);
if (user.getPoints() < requiredPoints) {
addActionError("用户积分不足");
return ERROR;
}
// 3. 生成租借记录
BorrowBook borrowRecord = new BorrowBook();
borrowRecord.setCode(generateBorrowCode());
borrowRecord.setCreateTime(new Date());
borrowRecord.setBookId(model.getBookId());
borrowRecord.setUserId(model.getUserId());
borrowRecord.setAddressId(model.getAddressId());
borrowService.save(borrowRecord);
// 4. 更新图书库存
book.setSumNum(book.getSumNum() - 1);
bookService.update(book);
// 5. 扣除用户积分
user.setPoints(user.getPoints() - requiredPoints);
userService.update(user);
// 6. 记录用户行为用于推荐算法
recordUserBehavior(user.getId(), book.getId(), "BORROW");
addActionMessage("图书租借成功!");
return SUCCESS;
} catch (Exception e) {
// 事务自动回滚
addActionError("租借过程发生错误:" + e.getMessage());
return ERROR;
}
}
/**
* 生成唯一的租借编号
*/
private String generateBorrowCode() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String timestamp = sdf.format(new Date());
Random random = new Random();
int randomNum = random.nextInt(9000) + 1000;
return "BORROW_" + timestamp + "_" + randomNum;
}
}

购物车业务逻辑实现
购物车模块支持批量操作和价格实时计算:
@Service("shopCarService")
@Transactional
public class ShopCarServiceImpl implements ShopCarService {
@Autowired
private ShopCarDao shopCarDao;
@Autowired
private BookDao bookDao;
@Override
public void addToCart(Integer userId, Integer bookId, Integer quantity) {
// 检查是否已存在购物车记录
ShopCar existingItem = shopCarDao.findByUserAndBook(userId, bookId);
if (existingItem != null) {
// 更新数量
existingItem.setNum(existingItem.getNum() + quantity);
updateItemPrice(existingItem);
shopCarDao.update(existingItem);
} else {
// 新增记录
ShopCar newItem = new ShopCar();
newItem.setUserId(userId);
newItem.setBookId(bookId);
newItem.setNum(quantity);
newItem.setIsDelete(0);
updateItemPrice(newItem);
shopCarDao.save(newItem);
}
}
/**
* 更新购物车项价格信息
*/
private void updateItemPrice(ShopCar shopCar) {
Book book = bookDao.getById(shopCar.getBookId());
if (book != null) {
// 使用销售价格计算
double price = Double.parseDouble(book.getSalePrice());
shopCar.setPrice(String.valueOf(price));
shopCar.setTotalPrice(String.valueOf(price * shopCar.getNum()));
}
}
@Override
public List<ShopCarDTO> getCartDetails(Integer userId) {
List<ShopCar> cartItems = shopCarDao.findByUserId(userId);
List<ShopCarDTO> result = new ArrayList<>();
for (ShopCar item : cartItems) {
ShopCarDTO dto = new ShopCarDTO();
BeanUtils.copyProperties(item, dto);
// 关联图书信息
Book book = bookDao.getById(item.getBookId());
dto.setBookName(book.getName());
dto.setBookImage(book.getBookUrl());
dto.setAvailableStock(book.getSumNum());
result.add(dto);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchDelete(Integer userId, Integer[] itemIds) {
for (Integer itemId : itemIds) {
ShopCar item = shopCarDao.getById(itemId);
if (item != null && item.getUserId().equals(userId)) {
item.setIsDelete(1); // 软删除
shopCarDao.update(item);
}
}
}
}

图书信息管理模块
图书管理支持多条件查询和批量操作:
@Controller("bookAction")
@Scope("prototype")
public class BookAction extends BaseAction<Book> {
@Autowired
private BookService bookService;
private String searchKeyword;
private Integer searchType;
private Date startDate;
private Date endDate;
/**
* 分页查询图书列表
*/
@Action(value = "book_list",
results = @Result(name = "success", location = "/WEB-INF/pages/book/list.jsp"))
public String list() {
try {
// 构建查询条件
Map<String, Object> params = new HashMap<>();
if (StringUtils.isNotBlank(searchKeyword)) {
params.put("searchKeyword", "%" + searchKeyword + "%");
}
if (searchType != null) {
params.put("type", searchType);
}
if (startDate != null && endDate != null) {
params.put("startDate", startDate);
params.put("endDate", endDate);
}
// 分页查询
pageMap = bookService.findByPage(params, getPage(), getRows());
return SUCCESS;
} catch (Exception e) {
addActionError("查询图书列表失败:" + e.getMessage());
return ERROR;
}
}
/**
* 图书上架处理
*/
@Action(value = "book_save",
results = {
@Result(name = "success", type = "redirect", location = "book_list.action"),
@Result(name = "input", location = "/WEB-INF/pages/book/add.jsp")
})
public String save() {
try {
// 设置默认值
model.setIsDelete(0);
model.setCreateTime(new Date());
model.setSumNum(1); // 租借图书默认库存为1
// 处理图书图片上传
if (imageFile != null) {
String imagePath = uploadImage(imageFile, imageFileFileName);
model.setBookUrl(imagePath);
}
bookService.save(model);
addActionMessage("图书上架成功!");
return SUCCESS;
} catch (Exception e) {
addActionError("图书上架失败:" + e.getMessage());
return INPUT;
}
}
/**
* 图片上传处理
*/
private String uploadImage(File file, String fileName) throws Exception {
String basePath = ServletActionContext.getServletContext().getRealPath("/upload/images");
String fileExtension = fileName.substring(fileName.lastIndexOf("."));
String newFileName = UUID.randomUUID().toString() + fileExtension;
String fullPath = basePath + File.separator + newFileName;
File destFile = new File(fullPath);
FileUtils.copyFile(file, destFile);
return "/upload