作为一名有10年Java全栈开发经验的工程师,我最近完成了一个名为"图书森林"的共享图书管理系统。这个系统采用SpringBoot+Vue前后端分离架构,实现了图书的在线共享、借阅管理、用户权限控制等核心功能。系统特别适合作为高校计算机专业的毕业设计项目,因为它涵盖了企业级应用开发的完整技术栈和典型业务场景。
在实际开发过程中,我发现共享图书系统有几个关键痛点需要解决:首先是图书状态的实时同步问题,当一本书被借出时,需要立即更新所有用户的视图;其次是复杂的借阅规则实现,比如预约排队、借阅期限控制等;最后是系统的可扩展性,要能支持未来可能增加的新功能模块。
整个系统采用标准的B/S架构,前端使用Vue3+Element Plus实现响应式界面,后端基于SpringBoot 2.7.x构建,数据库选用MySQL 8.0。考虑到系统的并发量不会特别高,我们使用内嵌Tomcat作为应用服务器,没有引入额外的微服务组件。
技术栈的完整组成如下:
架构图如下:
code复制┌───────────────────────────────────────────────────┐
│ Browser (Vue) │
└───────────────┬───────────────────┬───────────────┘
│ │
┌───────────────▼───┐ ┌──────────▼──────────────┐
│ Nginx (反向代理) │ │ SpringBoot应用 │
└───────────────┬───┘ └──────────┬──────────────┘
│ │
┌───────────────▼──────────────────▼───────────────┐
│ MySQL + Redis │
└───────────────────────────────────────────────────┘
系统主要分为以下几个功能模块:
每个模块都采用标准的MVC结构,前后端通过RESTful API进行数据交互。特别要注意的是,我们使用Shiro进行细粒度的权限控制,比如普通用户只能查看和借阅图书,而管理员可以进行所有管理操作。
经过多次迭代,我们最终确定了以下核心表结构:
sql复制CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`status` tinyint DEFAULT '1' COMMENT '状态 0:禁用 1:正常',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `book_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`isbn` varchar(20) NOT NULL COMMENT 'ISBN编号',
`title` varchar(100) NOT NULL COMMENT '书名',
`author` varchar(50) NOT NULL COMMENT '作者',
`publisher` varchar(50) DEFAULT NULL COMMENT '出版社',
`publish_date` date DEFAULT NULL COMMENT '出版日期',
`cover_url` varchar(255) DEFAULT NULL COMMENT '封面URL',
`status` tinyint DEFAULT '1' COMMENT '状态 0:下架 1:可借阅 2:已借出',
`location` varchar(50) DEFAULT NULL COMMENT '存放位置',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `isbn` (`isbn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `borrow_record` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '借阅人ID',
`book_id` bigint NOT NULL COMMENT '图书ID',
`borrow_time` datetime NOT NULL COMMENT '借出时间',
`expect_return_time` datetime NOT NULL COMMENT '预计归还时间',
`actual_return_time` datetime DEFAULT NULL COMMENT '实际归还时间',
`status` tinyint DEFAULT '1' COMMENT '状态 1:借阅中 2:已归还 3:逾期',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_book_id` (`book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
系统中最复杂的业务逻辑集中在借阅流程上,涉及多表关联:
这里特别要注意事务处理,我们使用Spring的@Transactional注解确保数据一致性:
java复制@Transactional
public BorrowResult borrowBook(Long userId, Long bookId) {
// 1. 检查图书状态
Book book = bookMapper.selectById(bookId);
if(book.getStatus() != BookStatus.AVAILABLE) {
throw new BusinessException("该图书不可借阅");
}
// 2. 检查用户借阅数量
Integer count = borrowMapper.countBorrowingByUser(userId);
if(count >= MAX_BORROW_LIMIT) {
throw new BusinessException("已达到最大借阅数量");
}
// 3. 创建借阅记录
BorrowRecord record = new BorrowRecord();
record.setUserId(userId);
record.setBookId(bookId);
record.setBorrowTime(new Date());
record.setExpectReturnTime(calculateReturnDate());
record.setStatus(BorrowStatus.BORROWING);
borrowMapper.insert(record);
// 4. 更新图书状态
book.setStatus(BookStatus.BORROWED);
bookMapper.updateById(book);
// 5. 记录操作日志
logService.addLog(userId, LogType.BORROW, bookId);
return new BorrowResult(record);
}
系统使用Shiro进行认证和授权,核心配置如下:
java复制public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
User user = userService.findByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 添加角色
info.setRoles(userService.findRoles(user.getId()));
// 添加权限
info.setStringPermissions(userService.findPermissions(user.getId()));
return info;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null) {
throw new UnknownAccountException("用户不存在");
}
if(user.getStatus() == 0) {
throw new LockedAccountException("账号已被禁用");
}
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
getName()
);
}
}
java复制public class PasswordHelper {
private static final String ALGORITHM = "SHA-256";
private static final int HASH_ITERATIONS = 1024;
public static String encrypt(String password, String salt) {
return new SimpleHash(
ALGORITHM,
password,
ByteSource.Util.bytes(salt),
HASH_ITERATIONS
).toHex();
}
public static String generateSalt() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
}
}
图书状态变更是系统的核心难点之一,我们采用两种机制保证状态同步:
数据库事务:如前面borrowBook方法所示,借书操作包含多个数据库更新,必须放在同一事务中
Redis缓存:热门图书信息缓存在Redis中,使用发布订阅模式通知状态变更
java复制// 借书成功后发布事件
public BorrowResult borrowBook(Long userId, Long bookId) {
// ...借书逻辑
// 发布借书事件
redisTemplate.convertAndSend("book.channel",
new BookStatusEvent(bookId, BookStatus.BORROWED));
return result;
}
// 订阅处理
@Configuration
public class RedisConfig {
@Bean
public MessageListenerAdapter messageListener() {
return new MessageListenerAdapter(new BookStatusListener());
}
@Bean
public ChannelTopic topic() {
return new ChannelTopic("book.channel");
}
}
public class BookStatusListener {
public void handleMessage(BookStatusEvent event) {
// 更新本地缓存
cacheService.updateBookStatus(event.getBookId(), event.getStatus());
// 通知前端(WebSocket)
wsService.notifyStatusChange(event.getBookId(), event.getStatus());
}
}
我们采用分层测试策略:
java复制@ExtendWith(MockitoExtension.class)
class BookServiceTest {
@Mock
private BookMapper bookMapper;
@Mock
private BorrowMapper borrowMapper;
@InjectMocks
private BookServiceImpl bookService;
@Test
void borrowBook_shouldSuccessWhenBookAvailable() {
// 准备测试数据
Book book = new Book();
book.setId(1L);
book.setStatus(BookStatus.AVAILABLE);
when(bookMapper.selectById(1L)).thenReturn(book);
when(borrowMapper.countBorrowingByUser(1L)).thenReturn(0);
// 执行测试
BorrowResult result = bookService.borrowBook(1L, 1L);
// 验证结果
assertNotNull(result);
assertEquals(BorrowStatus.BORROWING, result.getStatus());
// 验证交互
verify(bookMapper).updateById(any(Book.class));
verify(borrowMapper).insert(any(BorrowRecord.class));
}
}
java复制@Testcontainers
@SpringBootTest
class BorrowServiceIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void registerPgProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private BorrowService borrowService;
@Test
void shouldSuccessfullyBorrowAndReturnBook() {
// 初始化测试数据
Long userId = createTestUser();
Long bookId = createTestBook();
// 借书
BorrowResult borrowResult = borrowService.borrowBook(userId, bookId);
assertNotNull(borrowResult.getId());
// 还书
ReturnResult returnResult = borrowService.returnBook(borrowResult.getId());
assertEquals(ReturnStatus.SUCCESS, returnResult.getStatus());
}
}
系统支持多种部署方式:
bash复制# 后端打包
mvn clean package -DskipTests
# 前端打包
npm run build
# 运行
java -jar book-forest-0.0.1-SNAPSHOT.jar
dockerfile复制# Dockerfile
FROM openjdk:11-jre
COPY target/book-forest-0.0.1-SNAPSHOT.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
yaml复制# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: book-forest
spec:
replicas: 2
selector:
matchLabels:
app: book-forest
template:
metadata:
labels:
app: book-forest
spec:
containers:
- name: app
image: book-forest:1.0.0
ports:
- containerPort: 8080
在实际开发这个系统的过程中,我积累了一些值得分享的经验:
事务边界划分:刚开始我把整个借阅流程放在一个大事务中,导致并发性能很差。后来拆分为多个小事务(检查 → 创建记录 → 更新状态),性能提升了3倍。
缓存策略:图书信息缓存最初采用简单的TTL过期,经常出现脏读。改为"缓存+事件通知"模式后,一致性问题得到解决。
前端优化:使用Vue的keep-alive缓存常用页面,减少API调用,提升用户体验。
对于想要扩展此项目的同学,我建议可以从以下几个方向进行:
这个项目完整实现了共享图书管理的核心业务流程,代码结构清晰,文档齐全,非常适合作为毕业设计项目。我在开发过程中特别注意了代码的可读性和可维护性,每个关键业务方法都有详细的注释,数据库设计也符合第三范式。