作为一名有10年Java开发经验的程序员,我经常被问到如何选择一个合适的毕业设计项目。基于SpringBoot的小说阅读平台是一个非常适合计算机专业学生作为毕业设计的选题。这个项目不仅涵盖了Web开发的完整技术栈,还包含了丰富的业务逻辑,能够全面展示学生的技术能力。
小说阅读平台本质上是一个内容管理系统(CMS),它需要处理用户注册登录、小说分类管理、章节内容展示、阅读记录跟踪等核心功能。选择这个项目作为毕设有几个明显优势:
这个项目特别适合已经掌握Java基础和Web开发基础的同学。如果你对Spring框架有一定了解,但还没有完整的项目经验,这个项目能帮助你快速提升实战能力。
在开始编码前,我们需要对技术栈做出合理的选择。经过对各种技术方案的比较,我最终确定了以下技术组合:
后端技术栈:
前端技术栈:
数据库:
这个技术组合的选择主要基于以下几点考虑:
系统采用经典的三层架构,分为表示层、业务逻辑层和数据访问层:
这种分层架构的优势在于:
数据库设计遵循第三范式,主要包含以下核心表:
用户表(user):存储用户基本信息
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`avatar` varchar(255) 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 CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
小说表(novel):存储小说基本信息
sql复制CREATE TABLE `novel` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '小说标题',
`author` varchar(50) NOT NULL COMMENT '作者',
`cover` varchar(255) DEFAULT NULL COMMENT '封面图',
`description` text COMMENT '简介',
`category_id` bigint DEFAULT NULL COMMENT '分类ID',
`word_count` int DEFAULT '0' COMMENT '字数',
`status` tinyint DEFAULT '0' COMMENT '状态:0-连载中,1-已完结',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小说表';
章节表(chapter):存储小说章节内容
sql复制CREATE TABLE `chapter` (
`id` bigint NOT NULL AUTO_INCREMENT,
`novel_id` bigint NOT NULL COMMENT '小说ID',
`title` varchar(100) NOT NULL COMMENT '章节标题',
`content` longtext COMMENT '章节内容',
`word_count` int DEFAULT '0' COMMENT '字数',
`chapter_order` int DEFAULT '0' COMMENT '章节顺序',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_novel` (`novel_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='章节表';
阅读记录表(reading_history):记录用户阅读进度
sql复制CREATE TABLE `reading_history` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '用户ID',
`novel_id` bigint NOT NULL COMMENT '小说ID',
`chapter_id` bigint NOT NULL COMMENT '章节ID',
`progress` int DEFAULT '0' COMMENT '阅读进度(百分比)',
`last_read_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '最后阅读时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_novel` (`user_id`,`novel_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='阅读记录表';
数据库设计时特别注意了以下几点:
用户认证是系统的门户,我们采用Shiro框架实现安全的认证和授权机制。下面是核心实现代码:
java复制@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 设置登录页面
factoryBean.setLoginUrl("/login");
// 设置未授权页面
factoryBean.setUnauthorizedUrl("/unauthorized");
// 定义过滤器链
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/register", "anon");
filterChainDefinitionMap.put("/**", "authc");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
@Bean
public Realm realm() {
return new CustomRealm();
}
}
java复制public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 这里可以添加角色和权限
return authorizationInfo;
}
// 认证
@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复制@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public void register(UserRegisterDTO dto) {
// 检查用户名是否已存在
if (userMapper.existsByUsername(dto.getUsername())) {
throw new BusinessException("用户名已存在");
}
// 生成随机盐
String salt = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 20);
// 加密密码
String encryptedPassword = new SimpleHash(
"MD5",
dto.getPassword(),
ByteSource.Util.bytes(salt),
2
).toHex();
// 创建用户
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(encryptedPassword);
user.setSalt(salt);
user.setNickname(dto.getNickname());
user.setEmail(dto.getEmail());
user.setStatus(1);
userMapper.insert(user);
}
}
在实现用户认证模块时,有几个关键点需要注意:
小说管理模块主要包括小说的CRUD操作和章节管理。下面是核心实现:
java复制@RestController
@RequestMapping("/api/novel")
public class NovelController {
@Autowired
private NovelService novelService;
@GetMapping
public PageResult<NovelVO> list(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long categoryId
) {
QueryWrapper<Novel> wrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(keyword)) {
wrapper.like("title", keyword).or().like("author", keyword);
}
if (categoryId != null) {
wrapper.eq("category_id", categoryId);
}
wrapper.orderByDesc("update_time");
IPage<Novel> novelPage = novelService.page(new Page<>(page, size), wrapper);
List<NovelVO> novelVOs = novelPage.getRecords().stream()
.map(this::convertToVO)
.collect(Collectors.toList());
return new PageResult<>(
novelVOs,
novelPage.getTotal(),
novelPage.getSize(),
novelPage.getCurrent()
);
}
private NovelVO convertToVO(Novel novel) {
NovelVO vo = new NovelVO();
BeanUtils.copyProperties(novel, vo);
// 补充其他字段
return vo;
}
}
java复制@Service
public class ChapterServiceImpl implements ChapterService {
@Autowired
private ChapterMapper chapterMapper;
@Autowired
private NovelMapper novelMapper;
@Transactional
@Override
public void saveChapter(ChapterDTO dto) {
Chapter chapter;
if (dto.getId() != null) {
// 更新章节
chapter = chapterMapper.selectById(dto.getId());
if (chapter == null) {
throw new BusinessException("章节不存在");
}
chapter.setTitle(dto.getTitle());
chapter.setContent(dto.getContent());
chapterMapper.updateById(chapter);
} else {
// 新增章节
chapter = new Chapter();
chapter.setNovelId(dto.getNovelId());
chapter.setTitle(dto.getTitle());
chapter.setContent(dto.getContent());
chapter.setChapterOrder(getNextChapterOrder(dto.getNovelId()));
chapterMapper.insert(chapter);
// 更新小说字数统计
updateNovelWordCount(dto.getNovelId());
}
}
private int getNextChapterOrder(Long novelId) {
Integer maxOrder = chapterMapper.selectMaxChapterOrder(novelId);
return maxOrder == null ? 1 : maxOrder + 1;
}
private void updateNovelWordCount(Long novelId) {
Integer totalWords = chapterMapper.selectSumWordCount(novelId);
Novel novel = new Novel();
novel.setId(novelId);
novel.setWordCount(totalWords);
novelMapper.updateById(novel);
}
}
在实现小说管理模块时,有几个经验值得分享:
阅读功能是系统的核心,需要处理好以下几个关键点:
下面是核心实现代码:
java复制@RestController
@RequestMapping("/api/read")
public class ReadController {
@Autowired
private ChapterService chapterService;
@Autowired
private ReadingHistoryService readingHistoryService;
@GetMapping("/chapter/{chapterId}")
public Result<ChapterReadVO> readChapter(
@PathVariable Long chapterId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "2000") int size
) {
// 获取章节信息
Chapter chapter = chapterService.getById(chapterId);
if (chapter == null) {
throw new BusinessException("章节不存在");
}
// 分页处理内容
String content = chapter.getContent();
int totalPages = (int) Math.ceil((double) content.length() / size);
page = Math.min(page, totalPages - 1);
int start = page * size;
int end = Math.min(start + size, content.length());
String pageContent = content.substring(start, end);
// 获取相邻章节
Chapter prevChapter = chapterService.getPrevChapter(chapterId, chapter.getNovelId());
Chapter nextChapter = chapterService.getNextChapter(chapterId, chapter.getNovelId());
// 记录阅读进度
Long userId = SecurityUtils.getCurrentUserId();
if (userId != null) {
readingHistoryService.recordReadingProgress(
userId,
chapter.getNovelId(),
chapterId,
(int) ((double) start / content.length() * 100)
);
}
// 构建返回对象
ChapterReadVO vo = new ChapterReadVO();
vo.setChapterId(chapterId);
vo.setTitle(chapter.getTitle());
vo.setContent(pageContent);
vo.setCurrentPage(page);
vo.setTotalPages(totalPages);
vo.setPrevChapterId(prevChapter != null ? prevChapter.getId() : null);
vo.setNextChapterId(nextChapter != null ? nextChapter.getId() : null);
return Result.success(vo);
}
}
阅读进度记录需要考虑并发更新问题,下面是优化后的实现:
java复制@Service
public class ReadingHistoryServiceImpl implements ReadingHistoryService {
@Autowired
private ReadingHistoryMapper readingHistoryMapper;
@Override
public void recordReadingProgress(Long userId, Long novelId, Long chapterId, int progress) {
// 使用MySQL的ON DUPLICATE KEY UPDATE处理并发
readingHistoryMapper.insertOrUpdateProgress(
userId,
novelId,
chapterId,
progress,
new Date()
);
}
}
// Mapper中的SQL
@Insert({
"INSERT INTO reading_history(user_id, novel_id, chapter_id, progress, last_read_time) ",
"VALUES(#{userId}, #{novelId}, #{chapterId}, #{progress}, #{lastReadTime}) ",
"ON DUPLICATE KEY UPDATE ",
"chapter_id = VALUES(chapter_id), ",
"progress = VALUES(progress), ",
"last_read_time = VALUES(last_read_time)"
})
void insertOrUpdateProgress(
@Param("userId") Long userId,
@Param("novelId") Long novelId,
@Param("chapterId") Long chapterId,
@Param("progress") int progress,
@Param("lastReadTime") Date lastReadTime
);
前端阅读界面使用Vue实现,核心代码如下:
vue复制<template>
<div class="reader-container">
<div class="reader-header">
<h2>{{ chapter.title }}</h2>
<div class="chapter-nav">
<button
@click="goToChapter(prevChapterId)"
:disabled="!prevChapterId"
>
上一章
</button>
<span>第 {{ currentPage + 1 }} 页/共 {{ totalPages }} 页</span>
<button
@click="goToChapter(nextChapterId)"
:disabled="!nextChapterId"
>
下一章
</button>
</div>
</div>
<div class="reader-content" ref="content">
{{ chapter.content }}
</div>
<div class="reader-footer">
<input
type="range"
v-model="currentPage"
:max="totalPages - 1"
@change="loadPage"
>
</div>
</div>
</template>
<script>
import { getChapter } from '@/api/read';
export default {
data() {
return {
chapterId: null,
chapter: {
title: '',
content: ''
},
currentPage: 0,
totalPages: 1,
prevChapterId: null,
nextChapterId: null
};
},
methods: {
async loadChapter(chapterId, page = 0) {
try {
const res = await getChapter(chapterId, page);
this.chapterId = chapterId;
this.chapter.title = res.data.title;
this.chapter.content = res.data.content;
this.currentPage = res.data.currentPage;
this.totalPages = res.data.totalPages;
this.prevChapterId = res.data.prevChapterId;
this.nextChapterId = res.data.nextChapterId;
// 滚动到顶部
this.$refs.content.scrollTop = 0;
} catch (error) {
console.error(error);
}
},
loadPage() {
this.loadChapter(this.chapterId, this.currentPage);
},
goToChapter(chapterId) {
if (chapterId) {
this.$router.push({
path: `/read/${chapterId}`
});
}
}
},
created() {
this.chapterId = this.$route.params.chapterId;
this.loadChapter(this.chapterId);
},
watch: {
'$route.params.chapterId'(newVal) {
this.loadChapter(newVal);
}
}
};
</script>
在实现阅读功能时,有几个实用技巧:
SpringBoot项目打包部署非常简单,以下是标准流程:
bash复制mvn clean package -DskipTests
bash复制# 上传jar包到服务器
scp target/novel-platform-1.0.0.jar user@server:/path/to/app
# 启动应用
nohup java -jar novel-platform-1.0.0.jar --spring.profiles.active=prod > app.log 2>&1 &
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /static/ {
alias /path/to/static/files/;
expires 30d;
}
}
数据库优化
缓存策略
前端优化
JVM调优
完善的监控和日志系统对生产环境至关重要:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yaml复制management:
endpoints:
web:
exposure:
include: "*"
metrics:
export:
prometheus:
enabled: true
xml复制<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.6</version>
</dependency>
xml复制<!-- logback-spring.xml -->
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>
在实际开发过程中,我遇到了不少问题,以下是几个典型问题及其解决方案:
问题描述:当小说数量达到10万级别时,分页查询变慢,特别是跳转到靠后的页码时。
解决方案:
sql复制-- 传统分页(性能差)
SELECT * FROM novel ORDER BY id DESC LIMIT 100000, 20;
-- 优化后的分页(性能好)
SELECT * FROM novel WHERE id < ? ORDER BY id DESC LIMIT 20;
问题描述:某些章节内容非常大(超过1MB),直接加载影响性能。
解决方案:
问题描述:多个设备同时更新阅读进度时可能出现覆盖问题。
解决方案:
问题描述:用户上传的内容可能包含敏感词汇。
解决方案:
java复制@Service
public class SensitiveWordFilter {
private static final String SENSITIVE_WORDS = "敏感词1,敏感词2,敏感词3";
private Set<String> sensitiveWords;
private Map<String, String> sensitiveWordMap;
@PostConstruct
public void init() {
sensitiveWords = Arrays.stream(SENSITIVE_WORDS.split(","))
.collect(Collectors.toSet());
sensitiveWordMap = new HashMap<>();
for (String word : sensitiveWords) {
sensitiveWordMap.put(word, "*".repeat(word.length()));
}
}
public String filter(String text) {
if (StringUtils.isBlank(text)) {
return text;
}
String result = text;
for (Map.Entry<String, String> entry : sensitiveWordMap.entrySet()) {
result = result.replaceAll(entry.getKey(), entry.getValue());
}
return result;
}
}
完成基础功能后,可以考虑以下几个扩展方向提升项目价值:
实现这些扩展功能时,建议采用模块化开发方式,逐步迭代完善系统。每个新功能都应该有明确的需求分析和设计文档,确保系统架构不会因为功能增加而变得混乱。