简介:这是一套基于Spring Boot与Vue.js开发的完整漫画网站源码,专为计算机相关专业本科生毕业设计打造,已通过导师审核并获评98分高分毕设,同样适用于课程设计、期末大作业及前端+后端全栈实战训练。资源包共802个文件,涵盖115个Java后端业务与接口代码、45个Vue组件页面、164个JS交互逻辑、79个GIF动画资源、53个CSS样式文件及39个JPG/PNG图片素材,整体压缩包仅16.65MB,轻量易部署。已有109人下载学习,适合零基础入门到中阶进阶的学习者快速掌握前后端分离开发流程。用户可直接导入IDE运行项目,包含完整登录注册、漫画分类浏览、章节阅读、用户收藏、后台管理等核心模块;预置3个批处理脚本(install/run/build)简化环境搭建,同时保留.bak备份文件便于版本比对与调试参考,结构清晰、注释规范、无任何已知Bug。 我一直觉得,毕业设计这关最考验人的不是技术天花板有多高,而是你能不能用一个恰到好处的复杂度,把大学四年的积累完整地串起来。太简单,答辩老师觉得你在划水;太复杂,做到一半心态直接崩盘。而“基于Spring Boot和Vue的漫画网站”恰好卡在一个非常舒服的位置:前端有交互、后端有业务逻辑、数据模型有讲究、权限体系能落地,甚至还能往上加缓存、搜索、视频播放这些进阶点。今天我就完整拆解这个项目,从选题逻辑到前后端设计,再到部署和答辩,把该说的坑和该给的方案一次讲清。
这个项目最适合谁?三类人。第一类,Java后端方向但前端经验不多,想用Vue补齐全栈能力的学生;第二类,已经会SSM但还没接触主流前后端分离架构,想跟上企业开发节奏的人;第三类,手里捏着题目但脑子一团浆糊,想找一个可靠技术方案直接落地的同学。无论你属于哪种,这篇文章都会给你一条清晰得能踩出脚印的技术路线。
1. 选题定位与项目全景:为什么漫画网站是毕业设计的“安全牌”
毕业设计选题有个很现实的逻辑:素材好不好找、架构够不够主流、功能能不能讲出亮点。漫画网站这题,天然满足了前两点。素材不用我说,公开的漫画信息一抓一大把;架构上,Spring Boot加Vue这对组合在国内Java岗位的覆盖率,决定了你答辩时不用费劲跟老师解释“为什么用这个技术”。但真正让我推荐它的原因,是这个业务场景本身足够“立体”。
你仔细想,漫画网站的核心数据模型是“漫画—章节—图片”的三层嵌套结构,这和普通博客系统的“文章—评论”完全不同。它更贴近真实的内容型产品:一本漫画有封面、作者、状态、分类、热度;每本漫画下挂着一批章节;每个章节里又排列着几十张图片。这种层级关系在数据库设计、接口设计、前端路由设计上都会带来连锁反应,而恰恰是这些连锁反应,让你的毕业设计看起来不那么“玩具”。
而且漫画网站有一个天然优势:用户行为数据很丰富。游客可以浏览,注册用户可以收藏、点赞、评论、记录阅读历史。这些功能单个都不难,但组合在一起,你就能在答辩时讲出“用户维度”和“内容维度”两条业务线,系统立马丰满起来。
1.1 从课题要求反推项目边界
拿到题目,第一步不是写代码,而是反推边界。毕业设计通常要求你完成“系统分析、系统设计、系统实现、系统测试”四个环节,所以项目不能只是一个能跑的Demo,至少要有完整的业务闭环。
以我实现的版本为例,最终定的功能边界是这样:
- 前台用户端:注册登录、首页推荐、漫画分类筛选、关键词搜索、漫画详情、在线阅读器、收藏管理、历史记录、个人中心。
- 后台管理端:管理员登录、漫画管理(增删改查)、章节管理、分类管理、用户管理、数据统计仪表盘。
- 公共支撑:统一异常处理、JWT身份认证、跨域配置、参数校验、日志记录。
这套功能清单的妙处在于:它没有碰任何“高不可攀”的技术,但覆盖了Web开发最核心的增删改查、权限控制、状态流转和关联查询。答辩老师问“你这个系统有哪些角色”,你可以清晰答出“游客、普通用户、管理员”三种角色;问“权限怎么控制的”,你可以讲出“前端的路由守卫加后端的JWT拦截器双层控制”。每一步都在射程之内,但每一条都能说出设计意图。
1.2 技术栈选型的核心逻辑
这里必须说清楚一件事:不同选型组合在答辩老师眼里分量完全不同。
| 技术项 | 保守方案(及格) | 推荐方案(良好/优秀) |
|---|---|---|
| 后端框架 | Spring Boot 2.x | Spring Boot 2.x + MyBatis-Plus |
| 权限方案 | Session + 拦截器 | Spring Security + JWT |
| 数据库 | MySQL 8.x | MySQL 8.x + Redis缓存热点数据 |
| 前端框架 | Vue 2 + Element UI | Vue 2 + Element UI(生态最稳) |
| 构建工具 | npm/webpack | Maven + npm |
| 部署方式 | 本机运行 | Docker Compose / Nginx反向代理 |
我的建议很明确:不要把毕业设计当成新技术试验场,选最成熟、资料最多的组合。Vue 2加Element UI的组合至今依然是国内中小型管理系统的主流,网上随便一搜就是一堆踩坑记录,遇到问题你能在十分钟内找到答案,这在赶工阶段就是救命稻草。
1.3 功能模块划分与项目结构
代码结构一开始就要分清楚,否则写到后面想哭。我采用的是经典的前后端分离目录:
comic-web/ # 前端工程(Vue CLI) ├── src/ │ ├── api/ # 接口请求统一封装 │ ├── assets/ # 静态资源 │ ├── components/ # 通用组件 │ ├── router/ # 路由配置 │ ├── store/ # Vuex状态管理 │ ├── views/ # 页面视图 │ ├── utils/ # 工具函数(含request封装) │ ├── App.vue │ └── main.js └── vue.config.js comic-server/ # 后端工程(Spring Boot) ├── src/main/java/com/example/comic/ │ ├── controller/ # 接口层 │ ├── service/ # 业务层 │ ├── mapper/ # 数据访问层 │ ├── entity/ # 实体类 │ ├── dto/ # 数据传输对象 │ ├── config/ # 配置类(跨域、安全、Redis) │ ├── common/ # 公共组件(统一返回、异常处理) │ └── utils/ # 工具类 └── src/main/resources/ ├── mapper/ # MyBatis XML文件 └── application.yml前后端分离的目录约定,不只是为了让代码整洁,更是为了给论文画系统架构图时有一个现成的脉络——三层架构怎么体现、前后端交互边界在哪里,一目了然。
2. 数据库设计:漫画领域独有的三层嵌套与查询优化
数据库设计是整个项目的地基。漫画网站的核心表结构我踩过不少坑,这里直接给出一个经过验证的可靠版本。核心是五张表:用户表、漫画表、章节表、图片表、分类表,外加收藏和阅读历史两张关联表。
2.1 核心表结构设计与字段解析
漫画表(comic)是整个业务的中枢:
CREATE TABLE `comic` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '漫画标题', `author` varchar(50) DEFAULT NULL COMMENT '作者', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面图地址', `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `status` tinyint(1) DEFAULT 1 COMMENT '状态:1连载中 2已完结', `description` text COMMENT '简介', `click_count` int(11) DEFAULT 0 COMMENT '点击量', `is_recommend` tinyint(1) DEFAULT 0 COMMENT '是否推荐', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里的索引设计很关键。category_id和status是查询频率最高的筛选条件,必须建索引。我见过很多学生在这张表上瞎建七八个索引,结果写入变慢不说,查询也没快多少。漫画表就那么几万条数据,两个最常用的索引绰绰有余。
章节表(chapter):
CREATE TABLE `chapter` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `comic_id` bigint(20) NOT NULL COMMENT '所属漫画ID', `title` varchar(100) NOT NULL COMMENT '章节标题', `sort_order` int(11) NOT NULL COMMENT '排序号', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_comic_sort` (`comic_id`, `sort_order`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;图片表(chapter_image):
CREATE TABLE `chapter_image` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `chapter_id` bigint(20) NOT NULL COMMENT '章节ID', `image_url` varchar(255) NOT NULL COMMENT '图片地址', `sort_order` int(11) NOT NULL COMMENT '图片排列序号', PRIMARY KEY (`id`), KEY `idx_chapter` (`chapter_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;为什么要把章节和图片拆成两张表?很多新手会把所有图片路径用逗号拼成一个字符串放在章节表里,这样看起来省事,但后续一旦要做“当前页/总页数”的翻页逻辑,字符串拆分就是一场灾难。拆成独立表之后,阅读器只需要按chapter_id查到图片列表,再按sort_order排序,逻辑干干净净。
2.2 关联表设计:收藏与阅读历史的取舍
收藏表:
CREATE TABLE `favorite` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `comic_id` bigint(20) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_comic` (`user_id`, `comic_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;收藏表上必须加唯一索引,这是防止重复收藏的最简单手段。业务层写代码时还可以用INSERT IGNORE或先查后插来保证幂等,但数据库层面的唯一约束是最后的兜底。
阅读历史表的坑比较多。一开始我按常规思维设计成(user_id, comic_id, last_chapter_id, update_time),但后来发现用户可能对同一本漫画追更,历史记录需要不断更新最后阅读章节和时间。最终稳定的方案是加一个comic_id + user_id的唯一索引,每次阅读时执行ON DUPLICATE KEY UPDATE:
CREATE TABLE `read_history` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `comic_id` bigint(20) NOT NULL, `chapter_id` bigint(20) NOT NULL, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_comic` (`user_id`, `comic_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;2.3 查询性能的实战优化
漫画首页通常会查“最新更新”“热门推荐”“分类浏览”等列表。最粗暴的写法是连查三张表再在内存里处理,但数据量上来后响应会变得很难看。我的优化手段有三个:
第一,列表页只查当前页需要的字段,不查description这种大字段。MyBatis-Plus里可以通过select()指定查询列,避免全表大对象传输。
第二,热点数据加Redis缓存。首页的推荐列表、分类列表这些“千人一面”的数据,适合缓存到Redis里,设置5分钟的过期时间。用户每次刷新首页,先查缓存,缓存没有再回源数据库。
// 缓存示例:查询首页推荐列表 public List<ComicVO> getRecommendList() { String cacheKey = "comic:recommend"; String cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return JSON.parseArray(cached, ComicVO.class); } List<ComicVO> list = comicMapper.selectRecommendList(); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(list), 5, TimeUnit.MINUTES); return list; }第三,阅读历史列表的联表查询,用JOIN时注意驱动表和被驱动表的选择。MySQL优化器大部分时候能自己做出正确决定,但你不妨在开发环境EXPLAIN一下,确认没有出现文件排序或全表扫描。
提示:数据库这块答辩时老师极大概率会问索引和缓存。你哪怕只是把上面这套逻辑讲明白,就已经超过了80%的毕业生。
3. 后端实现:Spring Boot的业务闭环与接口设计
后端是整个系统的承重墙。这里我按照“认证授权、核心业务、统一规范”三条主线来讲,每一段都是可以直接落地的代码逻辑。
3.1 基于JWT的认证授权实现
前后端分离项目里,Session的方案越来越不讨喜,JWT加Spring Security是主流选择。但Spring Security的配置复杂程度劝退过很多人,所以我建议用一个务实的组合:Spring Security做过滤器链的框架,JWT做Token的生成与校验,但不用它的完整OAuth2体系。
核心思路是这样:
- 用户登录成功后,后端签发一个JWT Token,里面包含用户ID、用户名、角色。
- 前端把Token存在
localStorage,每次请求在Authorization头里带上。 - 后端写一个
JwtAuthenticationFilter,继承OncePerRequestFilter,在过滤器中解析Token,把用户信息放进SecurityContextHolder。 - Spring Security配置里放行登录、注册、首页、漫画详情等公开接口,其余接口统一要求认证。
JWT工具类的关键代码:
public String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }这里要特别注意secretKey的管理。硬编码在代码里可以应付毕业设计,但论文里如果能提一句“生产环境应使用配置中心或环境变量管理密钥”,会显得你考虑问题更全面。
自定义过滤器解析Token的模板:
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); try { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); Long userId = claims.get("userId", Long.class); String role = claims.get("role", String.class); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, Collections.singletonList(new SimpleGrantedAuthority(role))); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { // Token无效,不设置认证信息,后续拦截器会返回401 } } chain.doFilter(request, response); } }3.2 漫画核心业务接口链路
漫画模块的接口设计需要覆盖前台和后台两种视角。前台接口的路径设计遵循RESTful风格:
| 方法 | 路径 | 功能说明 |
|---|---|---|
| GET | /api/comic/home | 首页聚合数据(轮播、推荐、最新) |
| GET | /api/comic/list | 分页查询,支持分类、状态、排序参数 |
| GET | /api/comic/{id} | 漫画详情 |
| GET | /api/comic/{id}/chapter | 章节列表 |
| GET | /api/chapter/{id}/content | 章节图片列表 |
| GET | /api/comic/search?keyword=xxx | 关键词搜索 |
| POST | /api/favorite/{comicId} | 收藏漫画 |
| DELETE | /api/favorite/{comicId} | 取消收藏 |
业务层的实现要点在“读多写少”的优化上。漫画详情页是访问压力最大的接口,它要同时返回漫画基本信息、分类名称、最新章节标题,还要判断当前用户是否已收藏。这里我用了一个聪明的做法:详情接口的返回值里包含一个favorite布尔字段,但这个字段只有在用户携带Token并认证后才做查询,游客直接返回false。
章节内容接口有个值得注意的细节:图片地址不直接返回MySQL里存的原始字符串,而是经过一次拼接处理,存相对路径、返回绝对路径。这样以后换了OSS或者迁移了图床,后端只需要改一处拼接规则,前端完全无感。
3.3 统一返回体与全局异常处理
这个部分是最容易被忽视但最影响代码质量的地方。很多学生写接口时每个方法返回类型不一样,有的返回Map,有的直接返回实体,有的干脆void,前端那边各种res.data.data.data嵌套,查起bug来欲哭无泪。
我用的统一返回体是经典的三件套:
@Data public class Result<T> { private Integer code; // 200成功,500业务异常,401未认证 private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }全局异常处理用@RestControllerAdvice加@ExceptionHandler,把业务异常、参数校验异常、未知异常分类处理。这样做的直接好处是:后端永远不会把一堆堆栈信息裸奔给前端,前端拿到异常后也能统一弹提示。
3.4 搜索功能的实现:从SQL模糊到接口封装
关键词搜索是漫画网站的高频入口。最简单的方案是MySQL的LIKE '%keyword%',数据量不大的时候完全够用。搜索接口的实现:
public PageResult<ComicVO> search(String keyword, int page, int size) { // 简单防注入处理:去掉特殊字符 String cleanKeyword = keyword.replaceAll("[%_]", ""); // 转义后查询标题、作者、简介字段 LambdaQueryWrapper<Comic> wrapper = new LambdaQueryWrapper<Comic>() .like(StringUtils.isNotBlank(cleanKeyword), Comic::getTitle, cleanKeyword) .or() .like(StringUtils.isNotBlank(cleanKeyword), Comic::getAuthor, cleanKeyword) .orderByDesc(Comic::getClickCount); return comicMapper.selectPage(new Page<>(page, size), wrapper); }注意,LIKE里包含%和_是通配符,用户输入这两个字符会导致查询结果异常,必须做转义或直接清除。这个小细节在答辩现场演示时很容易被隐藏,但如果你主动说出来,老师会觉得你考虑问题很周全。
至于要不要上Elasticsearch,我的建议很干脆:不要。毕业设计的体量用ES属于杀鸡用牛刀,部署一个ES就要吃掉至少512MB内存,还会给你答辩引入一堆“为什么不用MySQL全文索引”“ES和MySQL如何同步数据”的追问。你只需要在论文里写明“当数据量增长到一定规模后,可以考虑引入Elasticsearch”,这个扩展性意识就到位了。
3.5 安全防护的几点实操
热搜词里提到了XSS攻击,这是我在项目里实际处理过的。漫画网站最典型的XSS风险在用户评论和漫画简介这两个输入口。我的做法是:
第一,前端在提交内容时做一次转义,将<script>等标签转成HTML实体。第二,后端在Jackson反序列化时配置StringEscapeUtils.escapeHtml4,对输入字符串统一转义。第三,如果用了富文本编辑器,必须结合白名单标签过滤,不是所有标签都允许。
@Component public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response); } }需要提醒的是,XSS防护是一个组合拳,前端转义防的是反射型XSS,后端的过滤防的是存储型XSS,二者缺一不可。这也是答辩时技术含量比较高的一个话题,值得多准备几句。
4. 前端工程化实践:Vue项目如何从零搭建到页面收官
前端这块,很多做Java后端的学生心里发怵。但Vue这个框架对新手极其友好:组件化思维、单文件组件、脚手架工具,三件套组合下来,你不需要是前端专家也能写出清晰可维护的代码。
4.1 项目初始化和开发环境搭建
用Vue CLI创建项目:
npm install -g @vue/cli vue create comic-web创建过程中有几个选项需要提前想清楚:Router选上,Vuex选上,CSS预处理器选SCSS,Linter选ESLint + Prettier。项目创建完别急着写页面,先把这些事干完:
安装Element UI、Axios、Moment(日期处理工具):
npm install element-ui axios moment然后配置vue.config.js的代理,解决开发环境的跨域问题:
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }这个代理配置的核心逻辑是:前端开发服务器监听8081端口,当请求路径以/api开头时,自动转发到后端的8080端口。这样在开发阶段,前端请求的URL可以写成相对路径/api/comic/list,不写IP和端口,避免跨域问题的同时也方便后续部署。
4.2 路由与页面架构设计
路由设计要和后台的接口设计对齐。用户端和后台管理端要分开:
const routes = [ { path: '/', component: Layout, children: [ { path: '', name: 'Home', component: Home }, { path: 'comic/:id', name: 'ComicDetail', component: ComicDetail }, { path: 'reader/:chapterId', name: 'Reader', component: Reader }, { path: 'category', name: 'Category', component: Category }, { path: 'search', name: 'Search', component: Search }, { path: 'favorite', name: 'Favorite', component: Favorite }, { path: 'history', name: 'History', component: History }, { path: 'login', name: 'Login', component: Login }, { path: 'register', name: 'Register', component: Register } ] }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, role: 'ADMIN' }, children: [ { path: 'dashboard', component: Dashboard }, { path: 'comic', component: AdminComic }, { path: 'chapter', component: AdminChapter }, { path: 'category', component: AdminCategory }, { path: 'user', component: AdminUser } ] } ]路由守卫是前端权限控制的关键一环。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); const role = localStorage.getItem('role'); if (to.matched.some(r => r.meta.requiresAuth)) { if (!token) { next({ path: '/login', query: { redirect: to.fullPath } }); } else if (to.meta.role && role !== to.meta.role) { next({ path: '/' }); } else { next(); } } else { next(); } });4.3 Axios封装与状态管理
Axios封装是前端工程化的第一个硬要求。我通常会在utils/request.js里做统一封装:
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动附加Token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => Promise.reject(error)) // 响应拦截器:统一处理异常 service.interceptors.response.use(response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { Message.error(error.message || '网络异常') return Promise.reject(error) }) export default service这套封装的价值在后续写业务代码时体现得淋漓尽致。每个接口模块就是一个几十行的函数集合,没有任何重复的Token、错误处理逻辑:
// api/comic.js import request from '@/utils/request' export function getHomeData() { return request.get('/comic/home') } export function getComicList(params) { return request.get('/comic/list', { params }) } export function getComicDetail(id) { return request.get(`/comic/${id}`) }Vuex状态管理主要管三件事:用户登录信息、用户收藏状态、全局加载状态。不需要把所有的接口数据都塞进Vuex,那样反而会让状态管理变得混乱。记住一个原则:多页面共享的数据才需要放Vuex,单页面内部的数据放在组件自己的data里就够了。
4.4 漫画阅读器和视频播放的落地细节
漫画阅读器是这个项目前端体验的核心。我的实现方案是:
阅读器页面进入时携带chapterId参数,调接口拿图片列表,然后用一个简单的左右翻页机制渲染。
<template> <div class="reader-container"> <div class="reader-toolbar"> <span>第 {{ currentPage }} / {{ totalPages }} 页</span> <button @click="prevPage">上一页</button> <button @click="nextPage">下一页</button> </div> <div class="reader-image-wrap"> <img :src="images[currentPage - 1]" /> </div> </div> </template>热搜词里提到了vue播放m3u8,这个确实很实用。如果你在项目里加了“漫画预告片”或者“漫评视频”功能,就需要用到视频播放。m3u8是HLS流媒体协议的视频切片索引文件,浏览器原生<video>标签没法直接播放,需要在前端挂一个hls.js库:
npm install hls.jsimport Hls from 'hls.js' export function initPlayer(videoElement, videoUrl) { if (videoUrl.endsWith('.m3u8') && Hls.isSupported()) { const hls = new Hls() hls.loadSource(videoUrl) hls.attachMedia(videoElement) hls.on(Hls.Events.MANIFEST_PARSED, () => { videoElement.play() }) } else { videoElement.src = videoUrl } }hls.js的原理是把它MediaSource扩展和HTMLVideoElement连接起来,浏览器端每播放一段,就会按需请求对应的.ts切片文件。这个方案不像Flash那么依赖插件,兼容性也好。如果只在后台管理里做视频上传,那么存储直接用本地文件或OSS都行。
4.5 Element UI后台管理页面的快速实现
后台管理页面属于“重要但不难”的部分。Element UI的el-table、el-form、el-pagination组合基本能覆盖所有需求。我花了整整两天在这个模块上,但真正写代码的时间并不多,大部分时间都在调分页组件的交互——什么时候刷新列表、删除后回到哪一页、搜索条件变化后页码怎么重置。
这里分享三条经验:
第一,el-table加row-key属性,避免选中态和数据更新冲突。第二,删除操作务必弹el-popconfirm二次确认,别让用户一点就删。第三,表格列宽不要全都固定死,description这类长文本列用show-overflow-tooltip,超长自动省略号加悬浮提示,页面会整洁很多。
5. 部署实战与答辩准备:从代码到分数
项目写完了,部署是最后一公里。同时,答辩准备的充分程度直接决定了你的最终分数。这一节把两条线都讲到。
5.1 使用Docker Compose完成一键部署
部署方案上,我强烈推荐使用Docker Compose。它可以把MySQL、Redis、后端应用、前端Nginx串成一条命令启动,这在论文里也能写出一段很漂亮的“系统部署设计”。
首先,后端项目的Dockerfile:
FROM maven:3.8-openjdk-8 AS build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM openjdk:8-jre-slim COPY --from=build /target/comic-server.jar /app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app.jar"]前端项目构建时有一点要特别注意:Vue项目打包出来的dist目录要交给Nginx服务,而Nginx的配置文件里要做两件事——第一,将前端路由的history模式回退到index.html,否则刷新页面时会404;第二,将/api路径反向代理到后端容器。
server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://comic-server:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }然后写docker-compose.yml把四个服务编排起来:
version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: comic_db ports: - "3306:3306" volumes: - ./mysql_data:/var/lib/mysql redis: image: redis:6-alpine ports: - "6379:6379" comic-server: build: ./comic-server depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod ports: - "8080:8080" comic-web: build: ./comic-web depends_on: - comic-server ports: - "80:80"一条docker-compose up -d --build命令,整个系统就跑起来了。这一步在答辩现场演示时非常加分,看着四个容器依次启动,比你在IDE里点运行按钮炫酷得多。
5.2 答辩常问问题清单与回答要点
答辩时老师的问题通常集中在几个方向:设计思路、技术选型、安全性和可扩展性。我把高频问题列出来,你提前准备好答案:
| 问题 | 回答要点 |
|---|---|
| 为什么用JWT不用Session? | 前后端分离场景下JWT无状态、易扩展、适合多端共享;Session存在服务器内存中,分布式部署时要做会话共享 |
| 项目有哪些安全措施? | 密码MD5加盐存储、JWT拦截未授权请求、输入参数XSS过滤、SQL语句使用预编译防止注入 |
| 如果用户量变大了怎么办? | 水平扩展后端服务,加一层负载均衡;数据库读写分离;热点数据缓存到Redis;图片迁移到CDN |
| 漫画图片存储在哪? | 开发环境存本地路径,生产环境可存OSS/COS对象存储,数据库只存URL |
| 你是怎么测试的? | 功能测试用Postman自测接口;前端手工走查流程;简单场景做了并发测试验证系统稳定性 |
最后一个问题是很多学生答不好的:“这个项目你觉得哪里最复杂?”别再说“都挺简单的”,也别硬编一个自己说不清的高深问题。你可以说“漫画章节的层级数据模型设计”或者“JWT认证的流程串联”,把话题引到自己最熟的地方,然后展开讲两分钟,这题就算过了。
5.3 我踩过的最深的三个坑
第一个坑,是前端调用接口时出现跨域,但前后端都配了跨域还是报错。排查到最后发现是Spring Security的过滤器链执行顺序问题——跨域过滤器必须注册在Security过滤器之前。解决方案是在Security配置里显式加上.cors().and()启用Spring MVC的CORS支持,并且用@CrossOrigin注解或CorsFilter统一处理。
第二个坑,是后端服务正常启动但前端请求一直404。排查发现是Controller的@RequestMapping路径和前端请求路径差了/api前缀。因为Nginx里已经做了/api的反向代理,后端的Controller就不应该再写/api路径,否则会变成/api/api永远匹配不上。这个坑很蠢,但特别容易犯。
第三个坑,是Vue项目build之后访问页面空白。通常有两种原因:一是publicPath配置错误,资源路径是绝对路径导致加载失败,要在vue.config.js里配publicPath: './';二是路由用了history模式但Nginx没有做try_files回退。这两个问题排查起来都不难,但网上搜到的解决方案五花八门,有的还会把你带偏,认准这两条就行。
5.4 从毕业设计到简历项目的升级思路
如果你的目标是让这个项目在简历上也发光,可以在现在的基础上做三个低成本但高感知的升级。
第一,给项目加上Swagger接口文档,统一访问/swagger-ui.html就能看到所有接口的注释文档。这个会让面试官觉得你有接口规范意识。
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency>第二,把后台管理的登录从单一管理员账号改成RBAC模型,引入角色表、菜单表、角色菜单关联表。这样做之后你可以在答辩时理直气壮地说“实现了基于RBAC的权限管理模型”,比“我用一个if判断了管理员”高级得多。
第三,给阅读器增加阅读进度记忆功能。后端已有了read_history表,前端只要在进入阅读器时拉取上次进度,点击章节时上报进度,这个功能就完成闭环了。面试时讲这个功能,比讲一百遍CRUD有感染力得多。
写在最后
这些年看了太多毕业设计翻车的案例,绝大多数不是因为题目有多难,而是因为在错误的时间纠结了错误的问题。有人花两周纠结“到底要不要用微服务”,有人一上来就研究“怎么用K8s部署”,这些都是方向性错误。漫画网站这个题目的正确打开方式,是先把核心链路跑通——用户注册登录、漫画列表、详情、阅读器、后台管理,然后在这个基础上逐层加细节。技术选型上,守住Spring Boot加Vue这条主线;数据库设计上,吃透漫画小说这个三层嵌套模型;部署上,用Docker Compose一键搞定。如果你正好在做这个课题,按照上面的思路一步步走,拿个优秀毕业设计是大概率事件。
本文还有配套的精品资源,点击获取