
简介这是一套基于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.xSpring Boot 2.x MyBatis-Plus权限方案Session 拦截器Spring Security JWT数据库MySQL 8.xMySQL 8.x Redis缓存热点数据前端框架Vue 2 Element UIVue 2 Element UI生态最稳构建工具npm/webpackMaven 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的索引设计很关键。category_id和status是查询频率最高的筛选条件必须建索引。我见过很多学生在这张表上瞎建七八个索引结果写入变慢不说查询也没快多少。漫画表就那么几万条数据两个最常用的索引绰绰有余。章节表chapterCREATE 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;图片表chapter_imageCREATE 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么要把章节和图片拆成两张表很多新手会把所有图片路径用逗号拼成一个字符串放在章节表里这样看起来省事但后续一旦要做“当前页/总页数”的翻页逻辑字符串拆分就是一场灾难。拆成独立表之后阅读器只需要按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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;收藏表上必须加唯一索引这是防止重复收藏的最简单手段。业务层写代码时还可以用INSERT IGNORE或先查后插来保证幂等但数据库层面的唯一约束是最后的兜底。阅读历史表的坑比较多。一开始我按常规思维设计成(user_id, comic_id, last_chapter_id, update_time)但后来发现用户可能对同一本漫画追更历史记录需要不断更新最后阅读章节和时间。最终稳定的方案是加一个comic_id user_id的唯一索引每次阅读时执行ON DUPLICATE KEY UPDATECREATE 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.3 查询性能的实战优化漫画首页通常会查“最新更新”“热门推荐”“分类浏览”等列表。最粗暴的写法是连查三张表再在内存里处理但数据量上来后响应会变得很难看。我的优化手段有三个第一列表页只查当前页需要的字段不查description这种大字段。MyBatis-Plus里可以通过select()指定查询列避免全表大对象传输。第二热点数据加Redis缓存。首页的推荐列表、分类列表这些“千人一面”的数据适合缓存到Redis里设置5分钟的过期时间。用户每次刷新首页先查缓存缓存没有再回源数据库。// 缓存示例查询首页推荐列表 public ListComicVO getRecommendList() { String cacheKey comic:recommend; String cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return JSON.parseArray(cached, ComicVO.class); } ListComicVO 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?keywordxxx关键词搜索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 ResultT { private Integer code; // 200成功500业务异常401未认证 private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }全局异常处理用RestControllerAdvice加ExceptionHandler把业务异常、参数校验异常、未知异常分类处理。这样做的直接好处是后端永远不会把一堆堆栈信息裸奔给前端前端拿到异常后也能统一弹提示。3.4 搜索功能的实现从SQL模糊到接口封装关键词搜索是漫画网站的高频入口。最简单的方案是MySQL的LIKE %keyword%数据量不大的时候完全够用。搜索接口的实现public PageResultComicVO search(String keyword, int page, int size) { // 简单防注入处理去掉特殊字符 String cleanKeyword keyword.replaceAll([%_], ); // 转义后查询标题、作者、简介字段 LambdaQueryWrapperComic wrapper new LambdaQueryWrapperComic() .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预处理器选SCSSLinter选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 classreader-container div classreader-toolbar span第 {{ currentPage }} / {{ totalPages }} 页/span button clickprevPage上一页/button button clicknextPage下一页/button /div div classreader-image-wrap img :srcimages[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串成一条命令启动这在论文里也能写出一段很漂亮的“系统部署设计”。首先后端项目的DockerfileFROM 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 --frombuild /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 groupIdio.springfox/groupId artifactIdspringfox-boot-starter/artifactId version3.0.0/version /dependency第二把后台管理的登录从单一管理员账号改成RBAC模型引入角色表、菜单表、角色菜单关联表。这样做之后你可以在答辩时理直气壮地说“实现了基于RBAC的权限管理模型”比“我用一个if判断了管理员”高级得多。第三给阅读器增加阅读进度记忆功能。后端已有了read_history表前端只要在进入阅读器时拉取上次进度点击章节时上报进度这个功能就完成闭环了。面试时讲这个功能比讲一百遍CRUD有感染力得多。写在最后这些年看了太多毕业设计翻车的案例绝大多数不是因为题目有多难而是因为在错误的时间纠结了错误的问题。有人花两周纠结“到底要不要用微服务”有人一上来就研究“怎么用K8s部署”这些都是方向性错误。漫画网站这个题目的正确打开方式是先把核心链路跑通——用户注册登录、漫画列表、详情、阅读器、后台管理然后在这个基础上逐层加细节。技术选型上守住Spring Boot加Vue这条主线数据库设计上吃透漫画小说这个三层嵌套模型部署上用Docker Compose一键搞定。如果你正好在做这个课题按照上面的思路一步步走拿个优秀毕业设计是大概率事件。本文还有配套的精品资源点击获取