1. 七牛云书籍管理概述

七牛云是专业的云存储服务提供商,为书籍管理提供了强大的文件存储和访问能力。本文将详细介绍基于七牛云的书籍目录列表分章节管理实现,包括文件上传、目录管理、章节列表、权限控制的完整解决方案。

1.1 核心功能

  1. 文件上传: 支持书籍文件上传到七牛云
  2. 目录管理: 按书籍和章节组织文件结构
  3. 章节列表: 动态生成书籍章节列表
  4. 权限控制: 实现文件访问权限管理
  5. 文件预览: 支持在线预览和下载

1.2 技术架构

1
2
3
用户上传 → 七牛云存储 → 目录结构 → 章节列表 → 权限验证 → 文件访问
↓ ↓ ↓ ↓ ↓
文件管理 → 元数据存储 → 索引构建 → 列表生成 → 访问控制

2. Maven依赖配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.qiniu</groupId>
<artifactId>book-management-demo</artifactId>
<version>1.0.0</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>

<properties>
<java.version>11</java.version>
<qiniu.version>7.11.0</qiniu.version>
</properties>

<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 七牛云SDK -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>${qiniu.version}</version>
</dependency>

<!-- JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Commons工具 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>

<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

3. 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# application.yml
server:
port: 8080

spring:
application:
name: book-management-demo

# Redis配置
redis:
host: localhost
port: 6379
database: 0

# 数据库配置
datasource:
url: jdbc:mysql://localhost:3306/book_management?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver

jpa:
hibernate:
ddl-auto: update
show-sql: true

# 七牛云配置
qiniu:
# 访问密钥
access-key: your-access-key
secret-key: your-secret-key
# 存储空间名称
bucket: your-bucket-name
# 域名
domain: your-domain.com
# 上传策略
upload:
# 上传超时时间(秒)
timeout: 300
# 分片上传阈值(字节)
multipart-threshold: 10485760
# 分片大小(字节)
part-size: 5242880
# 下载配置
download:
# 下载超时时间(秒)
timeout: 60
# 缓存时间(秒)
cache-time: 3600

# 书籍管理配置
book:
management:
# 支持的文件类型
supported-formats:
- pdf
- epub
- mobi
- txt
# 文件大小限制(字节)
max-file-size: 104857600
# 章节列表缓存时间(秒)
chapter-cache-time: 1800
# 权限验证
auth:
enabled: true
token-expire: 3600

# 监控配置
management:
endpoints:
web:
exposure:
include: "*"
metrics:
export:
prometheus:
enabled: true

4. 七牛云配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.qiniu.config;

import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.util.Auth;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 七牛云配置类
* @author Java实战
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "qiniu")
public class QiniuConfig {

/**
* 访问密钥
*/
private String accessKey;

/**
* 秘密密钥
*/
private String secretKey;

/**
* 存储空间名称
*/
private String bucket;

/**
* 域名
*/
private String domain;

/**
* 上传配置
*/
private UploadConfig upload;

/**
* 下载配置
*/
private DownloadConfig download;

/**
* 七牛云认证
*/
@Bean
public Auth qiniuAuth() {
return Auth.create(accessKey, secretKey);
}

/**
* 上传管理器
*/
@Bean
public UploadManager uploadManager() {
Configuration config = new Configuration(Region.autoRegion());
return new UploadManager(config);
}

/**
* 存储空间管理器
*/
@Bean
public BucketManager bucketManager() {
Configuration config = new Configuration(Region.autoRegion());
return new BucketManager(qiniuAuth(), config);
}

@Data
public static class UploadConfig {
private int timeout;
private long multipartThreshold;
private long partSize;
}

@Data
public static class DownloadConfig {
private int timeout;
private int cacheTime;
}
}

5. 书籍实体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package com.qiniu.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;

/**
* 书籍实体
* @author Java实战
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "book", indexes = {
@Index(name = "idx_book_id", columnList = "book_id"),
@Index(name = "idx_book_title", columnList = "title"),
@Index(name = "idx_book_author", columnList = "author")
})
public class Book {

/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/**
* 书籍ID(唯一标识)
*/
@Column(nullable = false, unique = true, length = 64)
private String bookId;

/**
* 书籍标题
*/
@Column(nullable = false, length = 200)
private String title;

/**
* 作者
*/
@Column(length = 100)
private String author;

/**
* 出版社
*/
@Column(length = 100)
private String publisher;

/**
* 出版日期
*/
private LocalDateTime publishDate;

/**
* 书籍描述
*/
@Column(length = 1000)
private String description;

/**
* 封面图片URL
*/
@Column(length = 500)
private String coverUrl;

/**
* 书籍状态:DRAFT-草稿, PUBLISHED-已发布, ARCHIVED-已归档
*/
@Column(nullable = false, length = 20)
private String status;

/**
* 创建时间
*/
@Column(nullable = false)
private LocalDateTime createTime;

/**
* 更新时间
*/
private LocalDateTime updateTime;

/**
* 创建者
*/
@Column(length = 64)
private String creator;

/**
* 章节列表(一对多关系)
*/
@OneToMany(mappedBy = "book", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Chapter> chapters;
}

6. 章节实体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package com.qiniu.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

/**
* 章节实体
* @author Java实战
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "chapter", indexes = {
@Index(name = "idx_chapter_id", columnList = "chapter_id"),
@Index(name = "idx_book_id", columnList = "book_id"),
@Index(name = "idx_chapter_order", columnList = "chapter_order")
})
public class Chapter {

/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/**
* 章节ID(唯一标识)
*/
@Column(nullable = false, unique = true, length = 64)
private String chapterId;

/**
* 书籍ID
*/
@Column(nullable = false, length = 64)
private String bookId;

/**
* 章节标题
*/
@Column(nullable = false, length = 200)
private String title;

/**
* 章节内容
*/
@Column(columnDefinition = "TEXT")
private String content;

/**
* 章节顺序
*/
@Column(nullable = false)
private Integer chapterOrder;

/**
* 文件路径(七牛云存储路径)
*/
@Column(length = 500)
private String filePath;

/**
* 文件大小(字节)
*/
private Long fileSize;

/**
* 文件类型
*/
@Column(length = 50)
private String fileType;

/**
* 章节状态:DRAFT-草稿, PUBLISHED-已发布, ARCHIVED-已归档
*/
@Column(nullable = false, length = 20)
private String status;

/**
* 创建时间
*/
@Column(nullable = false)
private LocalDateTime createTime;

/**
* 更新时间
*/
private LocalDateTime updateTime;

/**
* 创建者
*/
@Column(length = 64)
private String creator;

/**
* 关联的书籍(多对一关系)
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "book_id", insertable = false, updatable = false)
private Book book;
}

7. 七牛云文件服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
package com.qiniu.service;

import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.FileInfo;
import com.qiniu.util.Auth;
import com.qiniu.util.StringMap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
* 七牛云文件服务
* @author Java实战
*/
@Slf4j
@Service
public class QiniuFileService {

@Autowired
private Auth qiniuAuth;

@Autowired
private UploadManager uploadManager;

@Autowired
private BucketManager bucketManager;

@Value("${qiniu.bucket}")
private String bucket;

@Value("${qiniu.domain}")
private String domain;

/**
* 上传文件到七牛云
*/
public String uploadFile(MultipartFile file, String folder) throws IOException {
try {
log.info("开始上传文件到七牛云: {}, 文件夹: {}", file.getOriginalFilename(), folder);

// 生成唯一文件名
String fileName = generateFileName(file.getOriginalFilename());
String filePath = folder + "/" + fileName;

// 生成上传token
String uploadToken = qiniuAuth.uploadToken(bucket);

// 上传文件
Response response = uploadManager.put(file.getBytes(), filePath, uploadToken);

if (response.isOK()) {
String fileUrl = domain + "/" + filePath;
log.info("文件上传成功: {}", fileUrl);
return fileUrl;
} else {
log.error("文件上传失败: {}", response.error);
throw new RuntimeException("文件上传失败: " + response.error);
}

} catch (QiniuException e) {
log.error("七牛云上传异常", e);
throw new RuntimeException("七牛云上传异常", e);
}
}

/**
* 上传文件并返回详细信息
*/
public Map<String, Object> uploadFileWithDetails(MultipartFile file, String folder) throws IOException {
try {
log.info("开始上传文件到七牛云: {}, 文件夹: {}", file.getOriginalFilename(), folder);

// 生成唯一文件名
String fileName = generateFileName(file.getOriginalFilename());
String filePath = folder + "/" + fileName;

// 生成上传token
String uploadToken = qiniuAuth.uploadToken(bucket);

// 上传文件
Response response = uploadManager.put(file.getBytes(), filePath, uploadToken);

Map<String, Object> result = new HashMap<>();

if (response.isOK()) {
String fileUrl = domain + "/" + filePath;

result.put("success", true);
result.put("fileUrl", fileUrl);
result.put("filePath", filePath);
result.put("fileName", fileName);
result.put("fileSize", file.getSize());
result.put("fileType", getFileType(file.getOriginalFilename()));

log.info("文件上传成功: {}", fileUrl);
} else {
result.put("success", false);
result.put("error", response.error);

log.error("文件上传失败: {}", response.error);
}

return result;

} catch (QiniuException e) {
log.error("七牛云上传异常", e);
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("error", e.getMessage());
return result;
}
}

/**
* 删除文件
*/
public boolean deleteFile(String filePath) {
try {
log.info("开始删除文件: {}", filePath);

Response response = bucketManager.delete(bucket, filePath);

if (response.isOK()) {
log.info("文件删除成功: {}", filePath);
return true;
} else {
log.error("文件删除失败: {}", response.error);
return false;
}

} catch (QiniuException e) {
log.error("七牛云删除异常", e);
return false;
}
}

/**
* 获取文件信息
*/
public FileInfo getFileInfo(String filePath) {
try {
log.info("获取文件信息: {}", filePath);

FileInfo fileInfo = bucketManager.stat(bucket, filePath);

log.info("文件信息获取成功: {}", fileInfo);
return fileInfo;

} catch (QiniuException e) {
log.error("获取文件信息异常", e);
return null;
}
}

/**
* 生成私有下载链接
*/
public String generatePrivateDownloadUrl(String filePath, long expireInSeconds) {
try {
log.info("生成私有下载链接: {}, 过期时间: {}秒", filePath, expireInSeconds);

String downloadUrl = qiniuAuth.privateDownloadUrl(domain + "/" + filePath, expireInSeconds);

log.info("私有下载链接生成成功: {}", downloadUrl);
return downloadUrl;

} catch (Exception e) {
log.error("生成私有下载链接异常", e);
return null;
}
}

/**
* 生成上传token
*/
public String generateUploadToken() {
return qiniuAuth.uploadToken(bucket);
}

/**
* 生成带策略的上传token
*/
public String generateUploadTokenWithPolicy(StringMap policy) {
return qiniuAuth.uploadToken(bucket, null, 3600, policy);
}

/**
* 生成文件名
*/
private String generateFileName(String originalFilename) {
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
return UUID.randomUUID().toString() + extension;
}

/**
* 获取文件类型
*/
private String getFileType(String filename) {
if (filename == null || !filename.contains(".")) {
return "unknown";
}
return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
}
}

8. 书籍管理服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
package com.qiniu.service;

import com.qiniu.entity.Book;
import com.qiniu.entity.Chapter;
import com.qiniu.repository.BookRepository;
import com.qiniu.repository.ChapterRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
* 书籍管理服务
* @author Java实战
*/
@Slf4j
@Service
public class BookManagementService {

@Autowired
private BookRepository bookRepository;

@Autowired
private ChapterRepository chapterRepository;

@Autowired
private QiniuFileService qiniuFileService;

@Autowired
private StringRedisTemplate redisTemplate;

private static final String BOOK_CACHE_KEY = "book:cache:";
private static final String CHAPTER_CACHE_KEY = "chapter:cache:";

/**
* 创建书籍
*/
@Transactional
public Book createBook(Book book) {
try {
log.info("创建书籍: {}", book.getTitle());

book.setBookId(generateBookId());
book.setStatus("DRAFT");
book.setCreateTime(LocalDateTime.now());
book.setUpdateTime(LocalDateTime.now());

Book savedBook = bookRepository.save(book);

log.info("书籍创建成功: {}", savedBook.getBookId());
return savedBook;

} catch (Exception e) {
log.error("创建书籍失败", e);
throw new RuntimeException("创建书籍失败", e);
}
}

/**
* 上传书籍封面
*/
@Transactional
public String uploadBookCover(String bookId, MultipartFile coverFile) {
try {
log.info("上传书籍封面: {}", bookId);

Optional<Book> bookOpt = bookRepository.findByBookId(bookId);
if (!bookOpt.isPresent()) {
throw new RuntimeException("书籍不存在");
}

Book book = bookOpt.get();
String folder = "books/" + bookId + "/cover";

Map<String, Object> uploadResult = qiniuFileService.uploadFileWithDetails(coverFile, folder);

if ((Boolean) uploadResult.get("success")) {
String coverUrl = (String) uploadResult.get("fileUrl");
book.setCoverUrl(coverUrl);
book.setUpdateTime(LocalDateTime.now());

bookRepository.save(book);

// 清除缓存
clearBookCache(bookId);

log.info("书籍封面上传成功: {}", coverUrl);
return coverUrl;
} else {
throw new RuntimeException("封面上传失败: " + uploadResult.get("error"));
}

} catch (Exception e) {
log.error("上传书籍封面失败", e);
throw new RuntimeException("上传书籍封面失败", e);
}
}

/**
* 创建章节
*/
@Transactional
public Chapter createChapter(String bookId, Chapter chapter) {
try {
log.info("创建章节: {}, 书籍: {}", chapter.getTitle(), bookId);

Optional<Book> bookOpt = bookRepository.findByBookId(bookId);
if (!bookOpt.isPresent()) {
throw new RuntimeException("书籍不存在");
}

chapter.setChapterId(generateChapterId());
chapter.setBookId(bookId);
chapter.setStatus("DRAFT");
chapter.setCreateTime(LocalDateTime.now());
chapter.setUpdateTime(LocalDateTime.now());

// 设置章节顺序
if (chapter.getChapterOrder() == null) {
Integer maxOrder = chapterRepository.findMaxChapterOrderByBookId(bookId);
chapter.setChapterOrder(maxOrder != null ? maxOrder + 1 : 1);
}

Chapter savedChapter = chapterRepository.save(chapter);

// 清除缓存
clearBookCache(bookId);
clearChapterCache(bookId);

log.info("章节创建成功: {}", savedChapter.getChapterId());
return savedChapter;

} catch (Exception e) {
log.error("创建章节失败", e);
throw new RuntimeException("创建章节失败", e);
}
}

/**
* 上传章节文件
*/
@Transactional
public String uploadChapterFile(String chapterId, MultipartFile file) {
try {
log.info("上传章节文件: {}", chapterId);

Optional<Chapter> chapterOpt = chapterRepository.findByChapterId(chapterId);
if (!chapterOpt.isPresent()) {
throw new RuntimeException("章节不存在");
}

Chapter chapter = chapterOpt.get();
String folder = "books/" + chapter.getBookId() + "/chapters";

Map<String, Object> uploadResult = qiniuFileService.uploadFileWithDetails(file, folder);

if ((Boolean) uploadResult.get("success")) {
String filePath = (String) uploadResult.get("filePath");
String fileUrl = (String) uploadResult.get("fileUrl");
Long fileSize = (Long) uploadResult.get("fileSize");
String fileType = (String) uploadResult.get("fileType");

chapter.setFilePath(filePath);
chapter.setFileSize(fileSize);
chapter.setFileType(fileType);
chapter.setUpdateTime(LocalDateTime.now());

chapterRepository.save(chapter);

// 清除缓存
clearChapterCache(chapter.getBookId());

log.info("章节文件上传成功: {}", fileUrl);
return fileUrl;
} else {
throw new RuntimeException("章节文件上传失败: " + uploadResult.get("error"));
}

} catch (Exception e) {
log.error("上传章节文件失败", e);
throw new RuntimeException("上传章节文件失败", e);
}
}

/**
* 获取书籍章节列表
*/
public List<Chapter> getBookChapters(String bookId) {
try {
log.info("获取书籍章节列表: {}", bookId);

// 先从缓存获取
String cacheKey = CHAPTER_CACHE_KEY + bookId;
String cachedData = redisTemplate.opsForValue().get(cacheKey);

if (cachedData != null) {
log.info("从缓存获取章节列表: {}", bookId);
// 这里应该反序列化JSON数据,简化处理
return chapterRepository.findByBookIdOrderByChapterOrder(bookId);
}

// 从数据库获取
List<Chapter> chapters = chapterRepository.findByBookIdOrderByChapterOrder(bookId);

// 缓存结果
redisTemplate.opsForValue().set(cacheKey, "cached", 30, TimeUnit.MINUTES);

log.info("获取章节列表成功,数量: {}", chapters.size());
return chapters;

} catch (Exception e) {
log.error("获取书籍章节列表失败", e);
throw new RuntimeException("获取书籍章节列表失败", e);
}
}

/**
* 获取书籍详情
*/
public Book getBookDetail(String bookId) {
try {
log.info("获取书籍详情: {}", bookId);

// 先从缓存获取
String cacheKey = BOOK_CACHE_KEY + bookId;
String cachedData = redisTemplate.opsForValue().get(cacheKey);

if (cachedData != null) {
log.info("从缓存获取书籍详情: {}", bookId);
// 这里应该反序列化JSON数据,简化处理
return bookRepository.findByBookId(bookId).orElse(null);
}

// 从数据库获取
Optional<Book> bookOpt = bookRepository.findByBookId(bookId);
if (bookOpt.isPresent()) {
Book book = bookOpt.get();

// 缓存结果
redisTemplate.opsForValue().set(cacheKey, "cached", 30, TimeUnit.MINUTES);

log.info("获取书籍详情成功: {}", book.getTitle());
return book;
} else {
log.warn("书籍不存在: {}", bookId);
return null;
}

} catch (Exception e) {
log.error("获取书籍详情失败", e);
throw new RuntimeException("获取书籍详情失败", e);
}
}

/**
* 生成章节下载链接
*/
public String generateChapterDownloadUrl(String chapterId, long expireInSeconds) {
try {
log.info("生成章节下载链接: {}", chapterId);

Optional<Chapter> chapterOpt = chapterRepository.findByChapterId(chapterId);
if (!chapterOpt.isPresent()) {
throw new RuntimeException("章节不存在");
}

Chapter chapter = chapterOpt.get();
if (chapter.getFilePath() == null) {
throw new RuntimeException("章节文件不存在");
}

String downloadUrl = qiniuFileService.generatePrivateDownloadUrl(
chapter.getFilePath(), expireInSeconds);

log.info("章节下载链接生成成功: {}", chapterId);
return downloadUrl;

} catch (Exception e) {
log.error("生成章节下载链接失败", e);
throw new RuntimeException("生成章节下载链接失败", e);
}
}

/**
* 清除书籍缓存
*/
private void clearBookCache(String bookId) {
try {
String cacheKey = BOOK_CACHE_KEY + bookId;
redisTemplate.delete(cacheKey);
log.info("清除书籍缓存: {}", bookId);
} catch (Exception e) {
log.error("清除书籍缓存失败", e);
}
}

/**
* 清除章节缓存
*/
private void clearChapterCache(String bookId) {
try {
String cacheKey = CHAPTER_CACHE_KEY + bookId;
redisTemplate.delete(cacheKey);
log.info("清除章节缓存: {}", bookId);
} catch (Exception e) {
log.error("清除章节缓存失败", e);
}
}

/**
* 生成书籍ID
*/
private String generateBookId() {
return "book_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000);
}

/**
* 生成章节ID
*/
private String generateChapterId() {
return "chapter_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000);
}
}

9. 控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package com.qiniu.controller;

import com.qiniu.entity.Book;
import com.qiniu.entity.Chapter;
import com.qiniu.service.BookManagementService;
import com.qiniu.service.QiniuFileService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Map;

/**
* 书籍管理控制器
* @author Java实战
*/
@Slf4j
@RestController
@RequestMapping("/api/book")
public class BookController {

@Autowired
private BookManagementService bookManagementService;

@Autowired
private QiniuFileService qiniuFileService;

/**
* 创建书籍
*/
@PostMapping("/create")
public ResponseEntity<Book> createBook(@RequestBody Book book) {
try {
Book createdBook = bookManagementService.createBook(book);
return ResponseEntity.ok(createdBook);
} catch (Exception e) {
log.error("创建书籍失败", e);
return ResponseEntity.internalServerError().build();
}
}

/**
* 上传书籍封面
*/
@PostMapping("/{bookId}/cover")
public ResponseEntity<String> uploadBookCover(@PathVariable String bookId,
@RequestParam("file") MultipartFile file) {
try {
String coverUrl = bookManagementService.uploadBookCover(bookId, file);
return ResponseEntity.ok(coverUrl);
} catch (Exception e) {
log.error("上传书籍封面失败", e);
return ResponseEntity.internalServerError().build();
}
}

/**
* 创建章节
*/
@PostMapping("/{bookId}/chapter")
public ResponseEntity<Chapter> createChapter(@PathVariable String bookId,
@RequestBody Chapter chapter) {
try {
Chapter createdChapter = bookManagementService.createChapter(bookId, chapter);
return ResponseEntity.ok(createdChapter);
} catch (Exception e) {
log.error("创建章节失败", e);
return ResponseEntity.internalServerError().build();
}
}

/**
* 上传章节文件
*/
@PostMapping("/chapter/{chapterId}/file")
public ResponseEntity<String> uploadChapterFile(@PathVariable String chapterId,
@RequestParam("file") MultipartFile file) {
try {
String fileUrl = bookManagementService.uploadChapterFile(chapterId, file);
return ResponseEntity.ok(fileUrl);
} catch (Exception e) {
log.error("上传章节文件失败", e);
return ResponseEntity.internalServerError().build();
}
}

/**
* 获取书籍详情
*/
@GetMapping("/{bookId}")
public ResponseEntity<Book> getBookDetail(@PathVariable String bookId) {
try {
Book book = bookManagementService.getBookDetail(bookId);
if (book != null) {
return ResponseEntity.ok(book);
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
log.error("获取书籍详情失败", e);
return ResponseEntity.internalServerError().build();
}
}

/**
* 获取书籍章节列表
*/
@GetMapping("/{bookId}/chapters")
public ResponseEntity<List<Chapter>> getBookChapters(@PathVariable String bookId) {
try {
List<Chapter> chapters = bookManagementService.getBookChapters(bookId);
return ResponseEntity.ok(chapters);
} catch (Exception e) {
log.error("获取书籍章节列表失败", e);
return ResponseEntity.internalServerError().build();
}
}

/**
* 生成章节下载链接
*/
@PostMapping("/chapter/{chapterId}/download")
public ResponseEntity<String> generateChapterDownloadUrl(@PathVariable String chapterId,
@RequestParam(defaultValue = "3600") long expireInSeconds) {
try {
String downloadUrl = bookManagementService.generateChapterDownloadUrl(chapterId, expireInSeconds);
return ResponseEntity.ok(downloadUrl);
} catch (Exception e) {
log.error("生成章节下载链接失败", e);
return ResponseEntity.internalServerError().build();
}
}

/**
* 获取上传token
*/
@GetMapping("/upload-token")
public ResponseEntity<String> getUploadToken() {
try {
String uploadToken = qiniuFileService.generateUploadToken();
return ResponseEntity.ok(uploadToken);
} catch (Exception e) {
log.error("获取上传token失败", e);
return ResponseEntity.internalServerError().build();
}
}
}

10. Repository接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.qiniu.repository;

import com.qiniu.entity.Book;
import com.qiniu.entity.Chapter;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

/**
* 书籍Repository
* @author Java实战
*/
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {

/**
* 根据书籍ID查询
*/
Optional<Book> findByBookId(String bookId);

/**
* 根据标题查询
*/
List<Book> findByTitleContaining(String title);

/**
* 根据作者查询
*/
List<Book> findByAuthor(String author);

/**
* 根据状态查询
*/
List<Book> findByStatus(String status);
}

/**
* 章节Repository
* @author Java实战
*/
@Repository
public interface ChapterRepository extends JpaRepository<Chapter, Long> {

/**
* 根据章节ID查询
*/
Optional<Chapter> findByChapterId(String chapterId);

/**
* 根据书籍ID查询章节列表(按顺序)
*/
List<Chapter> findByBookIdOrderByChapterOrder(String bookId);

/**
* 根据书籍ID查询最大章节顺序
*/
@Query("SELECT MAX(c.chapterOrder) FROM Chapter c WHERE c.bookId = :bookId")
Integer findMaxChapterOrderByBookId(@Param("bookId") String bookId);

/**
* 根据书籍ID和状态查询章节
*/
List<Chapter> findByBookIdAndStatus(String bookId, String status);
}

11. 总结

七牛云书籍目录列表分章节管理是云存储应用的重要场景。通过本文的详细介绍,我们了解了:

  1. 七牛云集成: 文件上传、下载、管理
  2. 书籍管理: 创建、更新、查询书籍信息
  3. 章节管理: 章节创建、文件上传、列表展示
  4. 缓存优化: 使用Redis缓存提高性能
  5. 权限控制: 私有下载链接生成

通过合理的架构设计和实现,可以为书籍管理提供稳定、高效的云存储解决方案。


Java实战要点:

  • 七牛云SDK提供完整的文件管理能力
  • 使用JPA实现数据持久化
  • Redis缓存提高查询性能
  • 事务管理保证数据一致性
  • 异常处理确保系统稳定性

代码注解说明:

  • @Transactional: 事务管理注解
  • @Entity: JPA实体注解
  • @OneToMany: 一对多关系注解
  • @ManyToOne: 多对一关系注解
  • @Index: 数据库索引注解
  • 文件上传: 支持多种文件格式和大小限制