### 全栈开发实战:SpringBoot在线图书借阅平台构建指南
在线图书借阅平台作为数字化转型的典型应用,结合SpringBoot技术栈能够快速构建高可用的管理系统。本文通过完整的项目实践,展示从需求分析到系统部署的全流程开发。
#### 项目架构设计与技术选型
**整体技术架构**
采用前后端分离模式,确保系统可扩展性和维护性:
- 后端框架:Spring Boot 2.7 + Spring Data JPA
- 安全框架:Spring Security + JWT
- 数据库:MySQL 8.0 + Redis缓存
- 前端技术:Vue3 + Element Plus
- 构建工具:Maven 3.6+
**项目依赖配置**
核心pom.xml配置:
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<"LaLiga.ACL.zqbty.cn">
<"Bundesliga.ACL.zqbty.cn">
<"UEFA.ACL.zqbty.cn">
```
#### 数据库设计与实体建模
**核心数据表结构**
```sql
-- 图书信息表
CREATE TABLE books (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
isbn VARCHAR(20) UNIQUE NOT NULL,
title VARCHAR(200) NOT NULL,
author VARCHAR(100) NOT NULL,
publisher VARCHAR(100),
publish_date DATE,
category VARCHAR(50),
total_copies INT DEFAULT 0,
available_copies INT DEFAULT 0,
location VARCHAR(100),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 借阅记录表
CREATE TABLE borrow_records (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
book_id BIGINT NOT NULL,
borrow_date DATE NOT NULL,
due_date DATE NOT NULL,
return_date DATE,
status ENUM('BORROWED', 'RETURNED', 'OVERDUE') DEFAULT 'BORROWED',
renew_count INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (book_id) REFERENCES books(id)
);
-- 用户表
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
phone VARCHAR(20),
role ENUM('ADMIN', 'USER') DEFAULT 'USER',
max_borrow_count INT DEFAULT 5,
status ENUM('ACTIVE', 'INACTIVE') DEFAULT 'ACTIVE',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
<"www.zt.zqbty.cn">
<"wap.zt.zqbty.cn">
<"m.zt.zqbty.cn">
```
**JPA实体类设计**
```java
@Entity
@Table(name = "books")
@Data
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false, length = 20)
private String isbn;
@Column(nullable = false, length = 200)
private String title;
@Column(nullable = false, length = 100)
private String author;
private String publisher;
@Column(name = "publish_date")
private LocalDate publishDate;
private String category;
@Column(name = "total_copies")
private Integer totalCopies = 0;
@Column(name = "available_copies")
private Integer availableCopies = 0;
private String location;
@CreationTimestamp
@Column(name = "create_time")
private LocalDateTime createTime;
@UpdateTimestamp
@Column(name = "update_time")
private LocalDateTime updateTime;
@OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
private List<BorrowRecord> borrowRecords = new ArrayList<>();
}
<"share.zt.zqbty.cn">
<"ei.zt.zqbty.cn">
<"movie.zt.zqbty.cn">
@Entity
@Table(name = "borrow_records")
@Data
public class BorrowRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "book_id", nullable = false)
private Book book;
@Column(name = "borrow_date", nullable = false)
private LocalDate borrowDate;
@Column(name = "due_date", nullable = false)
private LocalDate dueDate;
@Column(name = "return_date")
private LocalDate returnDate;
@Enumerated(EnumType.STRING)
private BorrowStatus status = BorrowStatus.BORROWED;
@Column(name = "renew_count")
private Integer renewCount = 0;
@CreationTimestamp
@Column(name = "create_time")
private LocalDateTime createTime;
public boolean isOverdue() {
return LocalDate.now().isAfter(dueDate) && status == BorrowStatus.BORROWED;
}
public enum BorrowStatus {
BORROWED, RETURNED, OVERDUE
}
}
```
#### 核心业务逻辑实现
**图书服务层**
```java
@Service
@Transactional
public class BookService {
@Autowired
private BookRepository bookRepository;
@Autowired
private BorrowRecordRepository borrowRecordRepository;
public Page<BookDTO> searchBooks(BookQuery query, Pageable pageable) {
Specification<Book> spec = Specification.where(null);
if (StringUtils.hasText(query.getTitle())) {
spec = spec.and((root, criteriaQuery, criteriaBuilder) ->
criteriaBuilder.like(root.get("title"), "%" + query.getTitle() + "%"));
}
if (StringUtils.hasText(query.getAuthor())) {
spec = spec.and((root, criteriaQuery, criteriaBuilder) ->
criteriaBuilder.like(root.get("author"), "%" + query.getAuthor() + "%"));
}
<"live.zt.zqbty.cn">
<"sport.zt.zqbty.cn">
<"football.zt.zqbty.cn">
if (StringUtils.hasText(query.getCategory())) {
spec = spec.and((root, criteriaQuery, criteriaBuilder) ->
criteriaBuilder.equal(root.get("category"), query.getCategory()));
}
return bookRepository.findAll(spec, pageable)
.map(this::convertToDTO);
}
public BorrowResult borrowBook(Long bookId, Long userId) {
Book book = bookRepository.findById(bookId)
.orElseThrow(() -> new ResourceNotFoundException("图书不存在"));
// 检查图书库存
if (book.getAvailableCopies() <= 0) {
throw new BusinessException("该图书暂无库存");
}
// 检查用户借阅数量
long currentBorrowCount = borrowRecordRepository
.countByUserIdAndStatus(userId, BorrowRecord.BorrowStatus.BORROWED);
if (currentBorrowCount >= 5) { // 最大借阅数量
throw new BusinessException("已达到最大借阅数量");
}
// 创建借阅记录
BorrowRecord record = new BorrowRecord();
record.setUser(new User(userId));
record.setBook(book);
record.setBorrowDate(LocalDate.now());
record.setDueDate(LocalDate.now().plusDays(30)); // 借期30天
borrowRecordRepository.save(record);
// 更新图书库存
book.setAvailableCopies(book.getAvailableCopies() - 1);
bookRepository.save(book);
return new BorrowResult(true, "借阅成功", record.getId());
}
public ReturnResult returnBook(Long recordId) {
BorrowRecord record = borrowRecordRepository.findById(recordId)
.orElseThrow(() -> new ResourceNotFoundException("借阅记录不存在"));
if (record.getStatus() == BorrowRecord.BorrowStatus.RETURNED) {
throw new BusinessException("该图书已归还");
}
// 更新归还信息
record.setReturnDate(LocalDate.now());
record.setStatus(BorrowRecord.BorrowStatus.RETURNED);
borrowRecordRepository.save(record);
// 恢复图书库存
Book book = record.getBook();
book.setAvailableCopies(book.getAvailableCopies() + 1);
bookRepository.save(book);
// 检查是否逾期
boolean isOverdue = record.getReturnDate().isAfter(record.getDueDate());
String message = isOverdue ? "归还成功,但已逾期" : "归还成功";
return new ReturnResult(true, message, isOverdue);
}
<"basketball.zt.zqbty.cn">
<"tv.zt.zqbty.cn">
<"trending.zt.zqbty.cn">
private BookDTO convertToDTO(Book book) {
return BookDTO.builder()
.id(book.getId())
.isbn(book.getIsbn())
.title(book.getTitle())
.author(book.getAuthor())
.publisher(book.getPublisher())
.publishDate(book.getPublishDate())
.category(book.getCategory())
.totalCopies(book.getTotalCopies())
.availableCopies(book.getAvailableCopies())
.location(book.getLocation())
.build();
}
}
```
#### 安全认证与权限控制
**JWT工具类**
```java
@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret:defaultSecretKey}")
private String jwtSecret;
@Value("${app.jwt.expiration:86400000}")
private long jwtExpiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
```
**安全配置类**
```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
<"crawler.zt.zqbty.cn">
<"gold.zt.zqbty.cn">
<"EPL.zt.zqbty.cn">
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/books/search").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(new JwtTokenFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
```
#### RESTful API设计
**图书管理控制器**
```java
@RestController
@RequestMapping("/api/books")
@Validated
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/search")
public ResponseEntity<PageResult<BookDTO>> searchBooks(
@RequestParam(required = false) String title,
@RequestParam(required = false) String author,
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
BookQuery query = new BookQuery(title, author, category);
Pageable pageable = PageRequest.of(page, size, Sort.by("title"));
Page<BookDTO> result = bookService.searchBooks(query, pageable);
return ResponseEntity.ok(PageResult.from(result));
}
@PostMapping("/{bookId}/borrow")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<BorrowResult> borrowBook(
@PathVariable Long bookId,
@AuthenticationPrincipal User user) {
BorrowResult result = bookService.borrowBook(bookId, user.getId());
return ResponseEntity.ok(result);
}
<"CFL.zt.zqbty.cn">
<"SerieA.zt.zqbty.cn">
<"LFP.zt.zqbty.cn">
@PostMapping("/return/{recordId}")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<ReturnResult> returnBook(@PathVariable Long recordId) {
ReturnResult result = bookService.returnBook(recordId);
return ResponseEntity.ok(result);
}
}
// 数据传输对象
@Data
@Builder
public class BookDTO {
private Long id;
private String isbn;
private String title;
private String author;
private String publisher;
private LocalDate publishDate;
private String category;
private Integer totalCopies;
private Integer availableCopies;
private String location;
}
```
#### 缓存优化与性能提升
**Redis缓存配置**
```java
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
return template;
}
}
@Service
public class BookCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String BOOK_KEY_PREFIX = "book:";
private static final Duration CACHE_DURATION = Duration.ofHours(1);
public void cacheBook(BookDTO book) {
String key = BOOK_KEY_PREFIX + book.getId();
redisTemplate.opsForValue().set(key, book, CACHE_DURATION);
}
public BookDTO getCachedBook(Long bookId) {
String key = BOOK_KEY_PREFIX + bookId;
return (BookDTO) redisTemplate.opsForValue().get(key);
}
public void evictBookCache(Long bookId) {
String key = BOOK_KEY_PREFIX + bookId;
redisTemplate.delete(key);
}
}
```
#### 系统监控与异常处理
**全局异常处理**
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
ErrorResponse error = ErrorResponse.builder()
.code(400)
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFoundException(ResourceNotFoundException e) {
ErrorResponse error = ErrorResponse.builder()
.code(404)
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
<"LaLiga.zt.zqbty.cn">
<"Bundesliga.zt.zqbty.cn">
<"UEFA.zt.zqbty.cn">
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException e) {
ErrorResponse error = ErrorResponse.builder()
.code(403)
.message("权限不足")
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
}
```
**系统健康检查**
```java
@Component
public class SystemHealthIndicator implements HealthIndicator {
@Autowired
private DataSource dataSource;
@Autowired
private BookRepository bookRepository;
@Override
public Health health() {
try {
// 检查数据库连接
dataSource.getConnection().close();
// 检查核心业务数据
long bookCount = bookRepository.count();
Health.Builder status = bookCount > 0 ? Health.up() : Health.down();
return status
.withDetail("database", "connected")
.withDetail("total_books", bookCount)
.withDetail("timestamp", LocalDateTime.now())
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
```
通过完整的SpringBoot在线图书借阅平台开发实践,我们展示了现代Java Web应用的标准开发流程。系统采用模块化设计,包含完整的用户认证、图书管理、借阅流程等核心功能,为类似项目的开发提供了可参考的架构模式和实现方案。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/19545/viewspace-3101369/ ,如需转载,请注明出处,否则将追究法律责任。