每年都有同学来问我,说毕业设计不知道选什么题。与其去跟风做一个没有业务逻辑的电商平台,倒不如做点能完整展示全栈能力、又有点趣味性的东西。漫画信息管理网站,就是这样一个很合适的选题:它麻雀虽小但五脏俱全,涉及用户体系、权限控制、内容管理、文件上传、搜索分页、前后端联调这些真实业务系统的核心环节,做完之后你对 Spring Boot 和 Vue 的理解,绝对比照着教程敲一遍深刻得多。
这篇文章就围绕这个项目来写。我会从题目拆解、架构设计、数据库建模,到后端接口、前端页面、上线部署,再到答辩准备,把整个做毕设的链路完整过一遍。如果你正打算做类似的系统,或者手里已经有这个题目但不知道从哪下手,可以拿着这篇文章当路线图用,照着一步步走就行。文中会穿插我实际开发中踩过的坑和解决办法,这些是课程设计报告里不会写的东西。
1. 选题价值与技术选型:为什么这个题目值得做
1.1 “信息管理”三个字背后的真实需求
很多同学第一次看到“漫画信息管理网站”这个题目,第一反应是:这不就是个增删改查吗?确实,往简单了做,它就是一个对漫画列表的 CRUD。但如果你把“信息管理”四个字拆开看,会发现它其实包含了三类核心需求。
第一类是内容组织需求。漫画不同于普通商品,它有连载属性、分类属性、状态属性,还要有封面图和章节列表。这就意味着它不能只做一张漫画主表,至少要拆成“漫画表 + 章节表”两层结构,再关联一个分类表。
第二类是用户需求。网站不能只有管理员一个人在用,要有普通用户浏览、注册、登录,甚至收藏漫画、记录阅读进度、发表评论。这就把系统从“单用户管理后台”拉高到了“多角色在线平台”,而多角色意味着必须有权限控制,这是答辩时最能撑场面的一个点。
第三类是运营需求。管理员要能上下架漫画、管理用户评论、统计访问量。这些功能看着不起眼,但它是“信息管理”和“信息展示”的根本区别。
把这三点想清楚,你的选题说明答辩稿基本不需要临时编词,因为每一句话都是从业务需求里长出来的。
1.2 为什么不选 SSM + JSP,而要选 Spring Boot + Vue
我见过不少学校还在推荐 SSM(Spring + Spring MVC + MyBatis)加 JSP 的体系,不是说这套技术栈不行,而是对非科班同学或想认真学点东西的同学来说,它有几个现实问题。
第一个问题是前后端耦合。JSP 页面里既有 Java 代码又有 HTML,调试的时候改个样式都可能重启项目,非常痛苦。而 Spring Boot + Vue 是前后端分离结构,后端只负责返回 JSON 数据,前端负责渲染页面,两边可以并行开发,也贴近企业里真实的工作方式。
第二个问题是生态和资料。Spring Boot 的自动配置、Starter 机制、内置容器,能让一个新手在半天内跑起一个带数据库链接的服务,这比手动配置 SSH 那一大堆 XML 舒心得多。Vue 的官方中文文档和社区资料也极其丰富,遇到问题基本搜得到答案。
第三个问题是演示效果。JSP 做出来的页面,放在 2024 年的毕设答辩现场,视觉冲击力确实弱了。Vue + Element UI 或者 Vue + 自己的 CSS,能轻松做出现代化界面的观感,不用懂太多设计也能做出漂亮的卡片式漫画展示页。
需要说明的是,这种选型逻辑不是绝对的,如果你所在学校明确要求必须用 JSP,那就老老实实用 JSP。但只要是允许自选技术路线的题目,Spring Boot + Vue 确实是对学生最友好、对答辩最有帮助的组合。
1.3 一套可以抄作业的裸机技术清单
下面是我实际项目里用的技术组合,给大家做参考,没有什么是必须花钱的:
- 后端框架:Spring Boot 2.7.x(不要一上来就上 3.x,2.7 的资料最多,各种兼容性问题最少)
- ORM 框架:MyBatis-Plus(不用手写大量 XML,分页插件直接封装)
- 数据库:MySQL 5.7 或 8.0
- 安全框架:Spring Security + JWT(能用,且能讲出东西来;不想用 Spring Security 也可以用拦截器手动实现,后面我会讲区别)
- 前端:Vue 2 + Element UI 或 Vue 3 + Element Plus,我推荐 Vue 2 + Element UI,因为网上现成的后台管理系统模板多,改起来快
- 构建工具:Maven 和 npm
- 本地部署:前端
npm run build打出静态文件丢进 Nginx,后端java -jar直接跑
这套组合的最大优势就是“学得会、讲得清、查得到”。答辩的时候无论老师问哪个技术点,你都能从自己项目里找到对应的一行代码去解释。
2. 从零开始的设计与数据模型:先把地基画清楚
2.1 角色边界:管理员和普通用户到底各能干什么
动手写代码之前,我强烈建议你先把系统的角色权限边界画出来,这比写代码本身更重要。我见过太多同学后端的 CRUD 都写完了,一问管理员和用户的权限区分却答不上来,这就很尴尬。
这个项目我建议做两种角色:
管理员端:管理员负责漫画信息的录入、编辑、删除,分类的管理,用户的管理,以及评论的审核或者删除。管理员不看漫画,也不收藏漫画,这是它的身份边界。
普通用户端:用户可以注册、登录、浏览漫画列表、按分类筛选、搜索漫画、查看漫画详情与章节列表、阅读漫画内容、收藏喜欢的漫画、记录阅读进度。用户不能修改任何漫画数据,只能对漫画做“个人化”操作。
这两种角色的边界一定要在系统里体现出来,而不能只靠前端隐藏按钮。也就是说,后端接口必须做角色校验,管理员修饰改接口只能管理员调,用户收藏接口只能当前登录用户调自己的数据。这是一个安全设计原则,也是答辩时老师很可能深挖的点。
2.2 数据库表结构:七张表解决全部需求
表结构设计是整个项目的地基,表一旦建错了,后期改起来非常痛苦。我画了一张比较经典的模型,一共涉及七张核心表。你可以直接参考,也可以根据自己需求删减。
| 表名 | 用途 | 核心字段(举例) |
|---|---|---|
user | 用户表 | id、username、password(密文存储)、role、nickname、avatar、create_time |
category | 漫画分类表 | id、name、sort(排序权重) |
comic | 漫画信息表 | id、title、author、category_id、cover_url、status、description、click_count、create_time |
chapter | 章节表 | id、comic_id、chapter_no、title、content(富文本/图片列表)、create_time |
favorite | 收藏表 | id、user_id、comic_id、create_time |
reading_history | 阅读记录表 | id、user_id、comic_id、chapter_id、update_time |
comment | 评论表 | id、comic_id、user_id、content、create_time、status |
这里面有几个需要特别注意的设计点。
第一,漫画和章节是一对多的关系,漫画删除时章节最好做级联删除。但这里要注意,不要图方便在数据库里直接配许多 ON DELETE CASCADE,因为业务上可能希望是“软删除”。
第二,收藏表一定要设置user_id和comic_id的联合唯一索引,这样用户不会对同一本漫画产生多条收藏记录。重复收藏这个问题,前端可以做判断,但后端索引才是真正的兜底。
第三,阅读记录表比较特殊,它不需要用id来做主索引,反而应该用user_id和comic_id联合起来做业务 ID。因为一个用户对同一本漫画只需要一条阅读记录,每次阅读时更新章节和更新时间就够了。这样可以避免无限胀表。我在实际项目里就是先这么设计的,后期统计“本周在读人数”直接count(comic_id)就出来了。
如果需要扩展评分功能,可以再加一张rating表,把分数和评价分开。做毕设的话,七张表已经足够撑起整个系统了,再多就容易讲不清楚。
2.3 分类树还是分类列表:一个能说清楚的小决策
漫画的分类,通常就是纯列表——校园、热血、悬疑、恋爱这种。但在设计后台的时候,我建议你把分类的排序字段设计的灵活一点,让管理员可以自定义展示顺序。
不要做无限级分类树。原因很简单:漫画网站的分类通常是一级分类,最多两级(例如“国漫 -> 悬疑”),无限级树对这种业务来说属于过度设计,会白白消耗你写递归遍历的精力。真要做树形,留给以后的扩展,不要在毕设的第一版就埋进去。
另一个容易被忽略的点是:漫画分类的变化频率极低,几乎可以缓存。我在项目里就把分类列表放进了 Redis 缓存(如果不熟悉 Redis,也可以用本地 Map 做缓存,面试时电商场景的缓存思路是相通的)。第一次查到分类之后放缓存,后台修改分类时删掉缓存键,下次再查重新加载。这个点讲出来,答辩老师会觉得你考虑到了“读写比例不均衡”这个真实问题。
3. 后端落地:从登录鉴权到漫画检索,一条链路打通
3.1 登录和鉴权:JWT 方案的核心链路
登录模块是整个后端最容易出问题、也最值得好好写的地方。我强烈建议不要只写一个简单的 Session 登录,而是用 JWT 来做无状态鉴权。原因一是前后端分离架构下 JWT 更方便,二是面试和答辩时老师几乎必问。
我用的方案是:Spring Security + JWT + Redis 黑名单(可选)。如果同学觉得 Spring Security 的过滤器链太难理解,手动写拦截器也可以,但 Spring Security 能帮你省下很多安全细节。
核心逻辑长这样:
用户提交用户名和密码到POST /api/auth/login,后端用PasswordEncoder校验密码。密码存储绝对不能用明文,要用 BCrypt 加密,Spring Security 自带的BCryptPasswordEncoder就够用。
校验通过后,生成一个 JWT token,其中 payload 里放进用户的userId和role,然后返回给前端。前端之后每次请求都在Authorization请求头里带上Bearer token。
后端再写一个过滤器或拦截器,每次收到请求时解析 token,验证签名和过期时间,然后把用户信息放进SecurityContext或者直接放进ThreadLocal,供后续业务代码取用。
我用一段伪代码来简化这个过程,实际代码也不太复杂:
// 登录接口核心逻辑(简化版) @PostMapping("/api/auth/login") public Result login(@RequestBody LoginDTO dto) { // 1. 根据用户名查用户 User user = userService.findByUsername(dto.getUsername()); // 2. 检查密码(BCrypt加密后的比对) if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.fail("用户名或密码错误"); } // 3. 生成JWT token,里面放 userId 和 role String token = jwtUtils.generateToken(user.getId(), user.getRole()); return Result.ok(new LoginVO(token, user.getNickname(), user.getRole())); }这个过程中有几个细节是非常值得在答辩时展开讲的:
JWT 的过期时间怎么设置?我的建议是普通 token 设 2 小时,也不要太长。如果你想做“记住我”功能,可以再生成一个 refresh token,但毕设阶段做这个是锦上添花,不做也没人挑你的问题。
密码加密用的 BCrypt,每次加密结果都不同,但matches方法可以比对成功。这个“同密码不同密文”的设计是安全的加分项,一定要弄明白它为什么这么设计——它的核心目的是防止彩虹表攻击。
如果你用了 Redis,可以把“用户退出登录”做成把 token 加入黑名单,这样 JWT 就可以被服务端主动失效,解决 JWT“签发了就不能撤回”的痛点。这个点说出来,老师会知道你理解 JWT 的边界。
3.2 数据校验与统一异常处理:一个成熟的系统必须有这东西
很多同学做毕设的时候,接口里全是 if-else 判断参数是否为空,然后返回各种乱七八糟的 JSON。这样能用,但真的太丑了,而且答辩时老师一看你的 controller 里全是业务判断,就会觉得你没有分层意识。
我的做法是三层分离:
第一层,用 Spring 自带的 JSR-303 注解做参数校验。比如@NotBlank、@Size这些加在 DTO 字段上,再在 Controller 方法参数上写@Validated,参数不合法时框架会自动抛异常。
public class ComicDTO { @NotBlank(message = "漫画名称不能为空") private String title; @NotNull(message = "分类不能为空") private Long categoryId; }第二层,用@RestControllerAdvice做全局统一异常处理。不管业务异常还是参数校验异常,返回给前端的数据结构永远是统一的{ code: 0, message: "xxx", data: null }。这样前端 axios 拦截器只需要统一处理这一个结构就行,不用每请求写一遍错误处理。
第三层,Controller 里只做参数接收和结果返回,不写任何业务逻辑。业务逻辑全部下沉到 Service 层,事务注解@Transactional也加在 Service 方法上,这样如果数据保存中途出错,事务可以回滚,不会出现半截数据。
这三层做完,你的系统从代码结构上看已经和“课设风格”拉开了很大差距,而这种差距是答辩老师靠肉眼就能看出来的。
3.3 分页查询与漫画检索:别把所有数据怼给前端
漫画列表页,如果一上来就把所有漫画一次性查出来返回给前端,那就是给自己挖坑。漫画数量一多,响应体就变得巨大,页面卡顿,接口也很不稳定。
用 MyBatis-Plus 的分页插件,三行代码搞定分页查询:
Page<Comic> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Comic> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(keyword)) { wrapper.like(Comic::getTitle, keyword); } if (categoryId != null) { wrapper.eq(Comic::getCategoryId, categoryId); } wrapper.orderByDesc(Comic::getCreateTime); IPage<Comic> result = comicMapper.selectPage(page, wrapper);分页返回的数据结构里,要包含total、records、current、size这些字段。前端拿到之后可以用来渲染分页组件。注意total如果不查,会影响前端分页器的总页数计算,这个字段必须要。
搜索功能不需要用复杂的搜索引擎,用 MySQL 的LIKE查询就够了。但你要知道它的局限:LIKE '%keyword%'用不了索引,数据量大时会变慢,因此我只能建议你在这里直接向老师解释“这是中小数据量场景下的合理选择,如果数据量大可以引入全文索引或 Elasticsearch 做大分词检索”。这句话一说,说明你对性能有思考、对瓶颈有认知,而不是只会调接口。
另外,漫画详情页要记点击量。不要每访问一次就执行一次UPDATE comic SET click_count = click_count + 1这种实时更新,高并发时会造成锁竞争。更合理的做法是用 Redis 的INCR自增统计,再定时批量刷回数据库。如果毕设压力不大,直接在查询详情时顺手做一个自增更新也能说得过去,但如果你写了 Redis 的方案,绝对会比大多数同学高一个档次。
3.4 漫画章节管理:文件上传和富文本处理要注意什么
漫画章节的内容,我建议不要用 HTML 富文本,而是存图片列表,也就是一章漫画包含若干张图片地址,前端展示时用<image>组件按顺序排列。这样设计,一方面更贴近真实漫画站点的结构,另一方面也回避了富文本编辑器里 XSS 安全问题的麻烦。
文件上传方案,最简单可靠的是后端接口接收 MultipartFile,然后写入服务器本地磁盘,并把访问映射配好。我用过的方案是这样:
在 Spring Boot 的配置文件里设置上传路径,比如D:/upload(本地开发)或/usr/local/upload(服务器部署),再用自定义静态资源配置把这个路径映射成访问链接,比如http://localhost:8080/upload/xxx.jpg。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将本地的 /upload/** 路径映射到磁盘目录 registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }这个方案的好处是不依赖第三方对象存储,演示时也不受外网限速影响,自己就能复现。但要注意几个安全问题:上传文件要做类型校验,只允许图片扩展名,并且最好能改造文件名为 UUID 随机名拼上原扩展名,避免用户上传恶意文件或者文件名冲突。同一用户上传大量图片时,建议按日期分目录存储,比如2025/06/01/uuid.jpg,这样后面清理和管理会方便很多。
4. 前端 Vue 部分:页面交互与权限控制的核心实现
4.1 页面路由结构:前台展示和后台管理分开
Vue 项目我建议直接用 Vue CLI 或 Vite 脚手架创建,目录结构可以这样组织:
src |-- api // 所有请求接口封装 |-- router // 路由配置 |-- store // 登录状态管理(Vuex/Pinia) |-- views | |-- front // 前台页面:首页、列表、详情、阅读、登录注册 | |-- admin // 后台页面:漫画管理、分类管理、用户管理 | |-- common // 登录页、404等 |-- components // 通用组件前台和后台的路由必须分开,这在权限控制上很重要。/admin开头的所有路由,都要走“管理员角色校验”才能访问。如果用户没登录或者不是管理员,直接重定向回登录页。
这里说一个关键点:前端路由权限只能算“体验优化”,不是真正的安全屏障。真正的安全屏障一定是后端接口校验。那前端做权限控制的意义在哪?在于不给普通用户展示他们用不到的入口,减少被乱调用接口的概率。所以前端权限和后端权限是各司其职的,缺一个都显得不专业。
4.2 Axios 拦截器:统一处理 token 和错误态
前端请求封装上,我每次都会在项目开工第一天就把 axios 拦截器配好。它是所有请求的“收费站”,能帮你省掉大量重复代码。
请求拦截器里,从 store 里取出 token 塞到 header 的 Authorization 里。响应拦截器里,如果后端返回code != 0,统一弹错误提示;如果返回 401,说明 token 过期,清理登录状态并跳转回登录页。
这里踩过一个坑:如果后端返回的 token 过期时间是“在中间某一次请求时失效”,前端拿到 401 后应该做一个“二次确认”,弹出提示让用户重新登录,而不是默默刷新页面。否则用户正在填的收藏表单数据就丢了,体验很差。
请求封装代码大致是这样的:
// 简化版 axios 实例封装 service.interceptors.request.use(config => { const token = store.state.token if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { Message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { store.commit('clearAuth') router.push('/login') } return Promise.reject(error) } )这段代码不神奇,但它是整个前端工程质量的地基。有同学问我“为什么我的 Vue 项目每写一个页面都要重复写一堆错误判断”,原因就是没在拦截器这层统一处理好。
4.3 漫画展示和分类筛选:卡片式布局 + 前端筛选状态
前台首页和漫画列表页,我推荐做卡片式布局。每个卡片包含封面图、标题、作者、分类标签,下方放一个“热度”或者“收藏数”展示。这种布局用 Element UI 的el-card加 Flex 布局就能实现,不需要复杂的 CSS 功底。
分类筛选逻辑也不难:页面上方一排分类标签,点击某个分类后,携带categoryId参数重新请求接口。这一步有一个小优化:不用每次点击都全页面刷新,而是只替换列表区域的数据,用 Vue 的响应式特性自动刷新 DOM。
搜索框也放在这个页面。前端搜索逻辑有两个思路,一个是输入后回车直接跳转到/search?keyword=xxx的搜索结果页,另一个是在列表页内部直接搜索,通过参数重新请求漫画分页接口。我实际用的是第二个思路,因为页面状态不用丢失。收藏按钮的交互则要关注几点:未登录时点击,弹出登录提示;已登录时点击,调收藏接口并更新按钮状态。
4.4 阅读进度记录:首页继续阅读模块是怎么做的
这个功能虽然不是题目的硬性要求,但我强烈建议加上,理由有两点:一是它体现了用户体系的深度,二是它让你的系统不像一个“只存了数据库的展示站”。
核心逻辑是:用户打开一个漫画的某个章节时,前端向后端发送一条记录,内容是“当前用户 + 当前漫画 + 看的第几章”。后端拿到之后进行 upsert(存在就更新,不存在就插入)。
用户回到首页时,“继续阅读”模块根据 user_id 查最新的阅读记录,推荐三到五本最近在看的漫画。推荐列表的排序逻辑就是reading_history.update_time倒序。
这个功能实现起来非常简单,但做出来之后用户体验会上升一个档次。做毕设的系统,最忌讳的是“账号体系只是个装饰品”,用户登录前后看到的东西完全一样——有了阅读记录和收藏,账号体系才有存在感。
5. 开发中踩过的坑:从跨域到部署,附排查思路
5.1 跨域问题:前后端不在同一个端口,浏览器不认账
前后端分离开发的时候,Vue 跑在 5173 或 8080,Spring Boot 跑在 8080,两边一联调就会出现跨域问题:浏览器拦截了后端返回的数据。不解决这个,所有的接口调用都会失败。
推荐方案一:开发环境下用 Vite 或 Vue CLI 的 proxy 代理。在vue.config.js里配置/api开头的请求全部转发到http://localhost:8080,这样浏览器端看到的只是同源请求,跨域问题被代理层消化了。
推荐方案二:后端 Spring Boot 里配置CorsConfiguration,允许跨域请求。这个方法适合你把前后端分别部署到不同域名的时候用。
我实际开发时两个都配了,开发期用代理,上线后靠后端 CORS 配置兜底。出现问题时先看浏览器 network 面板,如果请求发出去了但没有返回,大概率是后端路径错了;如果请求被浏览器预检拦截掉报 CORS 错,那基本是 CORS 配置没生效,检查后端是否有拦截器把预检请求也拦了。
5.2 MyBatis-Plus 和 Jackson 字段映射问题
这个坑我印象特别深:数据库字段是create_time,实体类属性按驼峰命名成createTime,按理说 MyBatis-Plus 开启了驼峰映射后应该没问题。但有一次前端死活拿不到createTime,只拿到create_time,查到最后发现是 Jackson 序列化时没找到对应的 getter,原因是我把 Lombok 的@Data用错了位置,而且实体类里手动写了不带 getter 的公共字段。
解决方案也很朴素:优先统一用 Lombok 的@Data,不要手动混用 getter/setter;数据库字段统一用下划线命名;配置mybatis-plus.configuration.map-underscore-to-camel-case=true。这些配置一定在项目开始就配好,而不是等接口报错再慢慢查。
另一个常见问题是:LocalDateTime序列化格式默认是数组或带 T 的 ISO 字符串,前端拿到很难看。在application.yml里加一行全局时间格式配置,把LocalDateTime统一格式化为yyyy-MM-dd HH:mm:ss就行。这一步提前做掉,能少挨两次测试同学的骂。
5.3 文件上传路径的“本地能用,服务器不行”
文件上传的坑非常经典:本地开发时把文件存在D:/upload跑得好好的,部署到 Linux 服务器后就出问题。
一个是路径分隔符的问题。写D:/upload在 Windows 没问题,但换到 Linux 环境就没有这个目录了。所以必须把上传路径做成配置项,放到application.yml里,不要写死在代码里。
另一个是 Nginx 的代理映射问题。前端页面放在 80 端口,后端接口在 8080 端口,前端展示的图片地址是http://localhost:8080/upload/xxx.jpg,那用户访问时就得确保 Nginx 把/upload/路径也反代到后端,或者直接把后端上传目录做一个静态目录映射。否则就会出现“管理后台传了图,前台页面看不到”的诡异问题。
总结一句排查经验:部署后图片加载不出来,第一件事是先把图片 URL 单独打开,看看是 404、403 还是 500,能立刻判断出到底是路径还是权限还是代理的问题。
5.4 前端 token 过期 vs 后端 401 死循环
我在做 axios 拦截器时遇到过一个典型的坑:如果 token 过期,后端返回 401,前端拦截器收到 401 就跳转登录页,这一步没问题。但问题是如果跳转登录页时,登录页自身也发了一个请求去获取用户信息,那个请求又带着过期的 token,又触发 401,于是又跳转登录页——结果就死循环了。
解决办法是:在 axios 实例里加一个判断,当请求 URL 是登录请求或不需要鉴权的白名单接口时,不往 header 里塞 token,也不做 401 跳转。或者用一个全局标志,在跳转登录页时做好防重入处理。这个坑虽然小但很恶心,提前在代码里写清楚能省去不少调试时间。
还有一个小经验:如果后端用 JWT,token 过期时间不要太短也不要太长。太短的话用户写一条长评论提交时就过期了,体验很差;太长的话安全性又下降。我建议 2 小时就是一个不错的平衡点。答辩时如果老师问“你这个登录状态有效期多久”,你可以把这两边的取舍都讲出来,比单纯背一个参数强得多。
6. 让项目能落地:部署、演示和答辩准备
6.1 本地部署 vs 服务器部署:两种演示方案怎么选
很多同学做的毕设只能在自己电脑上跑,一旦换了演示机器就各种报错。这里我建议提前准备两种部署方案:
本地演示方案:前端npm run dev起开发服务器,后端直接java -jar起服务。这个方案最快,但不适合在答辩现场演示,因为对方电脑没有你的 Java 环境和 Node 环境,现场起 service 容易翻车。
服务器部署方案:买一台便宜的云服务器,装好 JDK、MySQL、Nginx。前端npm run build生成 dist 静态文件,放到 Nginx 的html目录;后端打包成 jar,用nohup java -jar xxx.jar > log.txt 2>&1 &后台运行。Nginx 再配置反向代理,把/api/路径转发到后端的 8080 端口。
只要你把服务器部署做好了,无论答辩在哪个教室,只要有网就能演示,而且看着也专业。不要因为害怕 Linux 命令就放弃部署,部署的过程其实就是把你整个毕设从“能运行”变成“能交付”的关键一步。
6.2 演示数据准备:别在答辩现场暴露空表
演示数据的准备,是我见过毕设翻车最高频的原因之一。很多人平时只插了一条测试数据,到了答辩现场,页面空白一片,老师第一印象直接就差了。
我的经验是,提前准备一批高质量的假数据:漫画封面图建议用真实风格的封面占位图,不要用纯色方块;漫画数量至少 12 本以上,并且覆盖 5 个分类;每个漫画至少有 3 个章节;每个章节有 5 张以上的内容图。这些数据可以写一个初始化 SQL 脚本,也可以写一个后端 DataInitializer 类在系统启动时自动灌入。
演示的时候最好准备一份“演示路线图”:第一步展示首页的推荐和分类导航,第二步演示用户注册登录,第三步搜索一个关键词,第四步看漫画详情和阅读页,第五步演示收藏和继续阅读,第六步切到管理员账号,演示漫画的新增编辑和用户管理。这套流程走下来不到五分钟,但每个核心功能都覆盖到了。
6.3 老师爱问的高频问题:答不出来就拿分变扣分
答辩时老师最喜欢问的不是代码本身能用与否,而是“你为什么要这么设计”和“你这个方案有什么局限性”。我根据自己的经历和身边同学的反馈,整理了这几个高频问题:
为什么用 JWT 不用 Session?“因为系统是前后端分离的架构,Vue 部署在静态服务器、后端部署在应用服务器,完全属于两个不同的域名,Session 的 cookie 跨域访问不方便;另外 JWT 支持无状态扩展,后台服务和前端页面可以分别横向扩容。”这个回答能同时体现架构理解。
密码是怎么存的?“用 BCrypt 加盐哈希,不存明文,防止数据库泄露后密码被直接拖走。”就这一句就够。
如果漫画数量达到百万级,你这个分页查询还扛得住吗?“目前的
LIKE模糊查询和普通分页是中小数据量的合理方案;真要扩展到百万级,我会考虑引入 Elasticsearch 做全文检索,或者给表结构加全文索引,同时在列表查询中接 Redis 做热点缓存。”回答的重点是表态:你知道你的方案有边界,也知道下一步该怎么演进。阅读进度为什么要单独建一张表而不是在漫画表里存个章节号?“因为阅读进度是按用户维度存储的,漫画表里存章节号只能存一个全局默认进度,但每个用户看的章可能不一样。单独建表才能做到千人千面。”
前端权限控制好还是后端权限控制好?“前端权限是为了用户体验,隐藏无权限的入口;后端权限才是安全防御,防止直接调接口越权。两个都要做,只做前端等于裸奔,只做后端则用户界面会很笨拙。”
这些问题如果你都提前演练过,答辩时基本不会慌。即使老师问到你没准备过的,也可以老实说“我目前的实现是……如果换一个更高并发的场景我会考虑……”,这种真诚的表述远比编造硬伤要好。
6.4 项目演示中的加分小设计
最后分享几个我做完项目之后才加进去的小设计,过了很久回头看,它们对演示效果提升非常显著。
一个是给系统做了一个“数据概览”首页块,管理员登录后台后,能看到总用户数、总漫画数、今日新增评论数等。别小看这几个数字,它会让评委觉得你的系统是“有人用的”,而不是一个冷冰冰的开发产物。
第二个是在漫画详情页加了“相关推荐”模块,根据当前漫画的分类找几本同类漫画做推荐。实现起来只是多写一个查询接口,但页面饱满度和用户体验提升很明显。
第三个是做了一个“游客也可以浏览”的设计,未登录用户能看所有漫画列表和详情,但是收藏、评论、阅读进度都需要登录后使用。这就让功能边界非常清晰,而且能避免那种“一进来就让登录,看到登录框就想关掉”的坏体验。
这些细节不需要多少技术量,但它们展示的是一个开发者对产品完整度的敏感度,而这种敏感度,恰恰是毕设评分里超出平均水平的那一项。
7. 后续可以怎么扩展:把毕设变成作品集
如果你做完这个漫画信息管理网站之后,发现自己的能力还能再往上走一段,我非常建议你做几个小扩展。一个方向是给漫画加上“评分系统”,用户可以打星并留言,这能锻炼你聚合查询的能力;另一个方向是做“漫画浏览历史排行榜”和“热门漫画推荐”,这需要你对统计和缓存有一定掌握;还有一个方向是做一个简单的“爬虫导入工具”,从公开的数据源导入漫画信息,但注意别碰版权红线,做演示数据可以,发布上线不行。
这些扩展可以从同一个项目里长出来,只要主结构不乱,也不会花太多时间。我当年就是在这个基础上加了一个评论区过滤功能,把敏感词检测也做进去了,虽然只是简单的前置匹配,但是在作品集项目描述里写上一句“实现基于敏感词库的评论过滤”,看起来就有说服力得多。
我个人的体会是:毕设做出来之后的收获往往比做的时候大。很多同学做毕设的时候觉得“不就是一个管理系统嘛”,但当你认真把它当成一个真实项目去设计权限、考虑安全、搭部署链路、准备演示数据、演练答辩问题之后,你是真的能感觉到自己从一个“按教程敲代码的人”变成了“能独立交付一个完整系统的人”。这个转变才是整个毕设最值钱的部分。这套流程走下来,不管最后拿多少分,你简历上的项目经验这一栏,已经真实地面向了企业场景。