多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于SpringBoot的在线文学创作与阅读互动平台设计与实战

基于SpringBoot的在线文学创作与阅读互动平台设计与实战 我和很多准备毕设的同学聊过一听到“网络文学交流分享平台”这名字第一反应往往是“又一个博客系统”。但真把题目拆开看就会发现它远不止写文章、发评论那么简单。这个选题表面上是一个内容管理系统实际做下来它是一整套完整的用户体系、内容创作流程、互动反馈机制和数据统计链路的组合。再加上限定用 SpringBoot 做后端技术上要处理的点比想象中多得多——登录鉴权、富文本编辑、分类检索、点赞收藏、评论回复、阅读量统计、热门榜单甚至作品导出和定时任务都是可以在答辩时拿出来重点讲的东西。这篇文章围绕“基于SpringBoot的在线文学创作与阅读互动平台”这个毕设主题把整个项目从需求拆解、数据库设计、核心接口实现到部署上线的完整过程讲清楚。我把当时踩过的坑、查过的资料、最后沉淀下来的稳定方案都整理在下面内容偏向直接可落地的实操不是为了凑字数的理论复述。无论你是准备自己从零写一遍还是打算把一个开源项目读懂后二次开发这份整理都能帮你省不少查资料的功夫。1. 项目到底要做什么先把需求边界划清楚1.1 从标题拆出三类核心用户“文学交流分享平台”这八个字里面藏着三类截然不同的角色这一点必须在一开始就认清因为后续所有功能设计都是围绕角色展开的。第一类是普通读者。他们的核心动作是浏览、搜索、阅读、评论、点赞、收藏。读者的体验好不好直接决定平台有没有留存的可能所以列表页加载速度、搜索准确性、阅读页排版这些看起来不起眼的地方恰恰是工作量的大头。第二类是创作者。他们要注册、登录、创建作品、分章节发布、修改删除、查看自己的数据统计。创作者最关心两件事发作品顺不顺畅数据反饋直不直观。所以作品管理模块要做得简洁章节编辑要顺手数据统计要一眼能看懂而不是堆一堆花哨图表。第三类是管理员。管理员不参与内容创作但要对整个平台负责具体来说就是审核作品、管理评论、处理用户举报、封禁违规账号、查看整体运营数据。后台管理功能在毕设答辩时非常加分因为很多同学只做前端展示根本不做管理端能拿出来一套完整后台的老师会明显高看一眼。1.2 核心功能列表按优先级排序毕设项目最忌讳“什么都想要最后什么都没做完整”。我按“必须做”“强烈建议做”“有余力再做”分了三批。必须做的基础功能用户注册与登录密码不能明文存要加密处理作品发布、章节管理、编辑、删除作品列表、分类筛选、关键字搜索作品详情页、章节阅读页评论、回复、点赞、收藏简单的个人中心能看自己发过的内容和互动记录强烈建议做的加分功能基于 Redis 的阅读量统计与热门榜单后台管理端用户管理、作品审核、数据概览作品数据统计可视化图表展示每天的阅读量趋势敏感词过滤这个在文学平台特别实用答辩时能说出一套过滤逻辑比单纯说“我做了个评论功能”有说服力得多有余力再做用户关注关系与私信作品分享海报生成基于 HanLP 的标签自动提取全文搜索升级为 Elasticsearch1.3 为什么这个选题在毕设里“性价比高”这个题目难度定位很巧妙。它不是一个纯 CRUD 的简单系统但也远没到高并发分布式的复杂程度正好卡在“能体现工作量、又不至于做不完”的最佳区间。从技术覆盖面来说一个完整作品下来SpringBoot、MyBatis、Redis、Vue、MySQL、Maven、Linux 部署这些主流技术栈全能练一遍。从工作量角度说前后端加起来二三十个页面十来个核心接口节奏合理的话两个月能完成得比较完整。最关键的是这个项目有真实的使用场景不是那种“学生管理系统”式的应付作业拿出去也能写在简历项目经验里不会被面试官一眼看穿是照着教程敲的。2. 技术选型与整体设计为什么这么搭2.1 SpringBoot 版本选择的一个大原则热词里有一条“springboot版本太高”特别扎眼因为这是真实存在的坑。SpringBoot 版本过高一方面对 JDK 版本有硬性要求比如 2.7 之后开始要求 JDK8 以上3.x 系列则必须 JDK17另一方面部分第三方依赖的兼容没跟上整合 MyBatis 的时候会莫名其妙报错查都查不清原因。我的建议很直接毕设项目用 SpringBoot 2.7.x 系列JDK 用 1.8不要追新。不是新版本不好而是毕设的评分點是“功能完整 技术运用合理 能跑起来”不是“用上了最新框架”。稳定压倒一切。我身边不止一个同学因为用了 SpringBoot 3.2 JDK21结果 MyBatis 整合时碰到 javax 到 jakarta 命名空间迁移的问题排查了两三天最后灰溜溜换回 2.7。有些弯路没必要走。2.2 前后端方案独立前端还是模板引擎我这次用的是前后端分离方案后端纯 RESTful API前端用 Vue3 Element Plus。有一个细节要重点说开发时前后端分开跑部署时可以把 Vue 打包后的静态文件直接放进 SpringBoot 的 resources/static 目录这样整站就是一个 jar 包部署成本极低也不需要单独配 Nginx 托管前端。这就是热词里“vue打包放进springboot中”的实际应用场景。不选 JSP 或 Thymeleaf 的原因很简单一是 JSP 这套东西确实有点老了写在简历里不好看二是前后端分离模式下分工清楚你写接口就是返回 JSON你写前端就是调接口渲页面排查问题时的思路会清晰很多三是将来如果你想把这个项目往简历写前后端分离是很多公司前端的默认形态你提前熟悉这套协作模式面试时也多些谈资。2.3 核心依赖清单SpringBoot 2.7.18稳定之选资料多遇到问题能找到一堆解决方案MyBatis-Plus 3.5.x条件构造器比手写 XML 省太多事分页插件也好用MySQL 8.0主流版本记住 JDBC 驱动名是com.mysql.cj.jdbc.Driver别用老的com.mysql.jdbc.DriverRedis 5.x 及以上做阅读量统计、热门榜、缓存JWT无状态登录认证比 Session 更适合前后端分离Vue 3 Element Plus Axios前端三件套Hutool工具类库雪花ID、加密、日期处理都能用Lombok少写 getter/setter2.4 项目目录结构一图看懂literature-platform/ ├── src/main/java/com/example/literature/ │ ├── controller/ # 控制层接口入口 │ ├── service/ # 业务层核心逻辑 │ ├── mapper/ # 数据访问层 │ ├── entity/ # 实体类 │ ├── vo/ # 视图对象接口返回的数据模型 │ ├── dto/ # 数据传输对象接收前端参数 │ ├── config/ # 配置类拦截器、跨域、Redis、分页插件 │ ├── common/ # 通用类统一返回结果、异常处理、常量 │ └── util/ # 工具类 ├── src/main/resources/ │ ├── application.yml # 核心配置 │ └── mapper/ # MyBatis XML 文件 ├── src/main/java/.../LiteratureApplication.java # 启动类 └── pom.xml # Maven 依赖配置这个结构是面试官最喜欢看的类型按技术层次分包而不是按功能模块分包。一眼扫过去就知道你不是新手是理解过分层架构的。3. 数据库设计一张好表顶十次重构3.1 核心表结构设计数据库设计是很多人忽略、但实际上最值得花时间的部分。表设计得不好后面写代码处处别扭。我这版经过三轮重构最终沉淀下来的核心表有五张。用户表userid、username、passwordBCrypt 加密后的密文、nickname、avatar、email、role区分普通用户/管理员、status正常/禁用、create_time。作品表workid、user_id作者、title、category分类、intro简介、cover_image封面图、status连载中/已完结、words_count字数、view_count阅读量、like_count、create_time、update_time。这里注意阅读量和点赞数我是直接冗余在作品表上的而不是每次实时 count。虽然严格来说有数据冗余但列表页展示时不用连表查计数性能好非常多。章节表chapterid、work_id、chapter_no章节序号、title、content正文长文本用LONGTEXT类型、word_count、create_time、update_time。这里的关键是chapter_no要单独存一个字段而不是靠 ID 排序——因为章节可能会有插入、删除操作靠 ID 排序会出错。评论表commentid、work_id、user_id、parent_id回复哪条评论0 表示顶级评论、content、like_count、create_time。parent_id这个字段是实现楼层式回复的关键有了它才能区分“评论”和“回复”前端的嵌套展示就靠这个字段递归组装。用户行为表user_actionid、user_id、target_id目标作品ID、type1点赞2收藏3关注、create_time。这张表专门存用户的点赞和收藏关系做“我赞过的”“我收藏的”列表就是查这张表。唯一索引(user_id, target_id, type)防重复这是硬性的数据完整性约束。3.2 索引怎么建一张表讲清楚读多写少是内容平台的核心特征所以索引策略重点是查询优化。work表category上加普通索引user_id上加普通索引create_time上加索引用于按时间排序chapter表work_id上加普通索引查询某本书的所有章节就是WHERE work_id ? ORDER BY chapter_no没有这个索引全表扫会卡comment表work_id加索引评论区加载就是查这张表user_action表联合唯一索引uk_user_target_type (user_id, target_id, type)这块是防重复操作的关键屏障3.3 敏感词的存储方案文学平台一定会有内容审核需求。我的做法是建一个sensitive_word表管理员可以在后台动态维护敏感词库。查询时启动阶段一次性把词库加载到内存然后用DFA确定性有限自动机算法做敏感词匹配比纯遍历字符串数组效率高一个数量级而且实现起来也不复杂核心就是一个 HashMap 构建的字典树。这一步我强烈建议做进去因为答辩时“你们平台怎么做内容安全的”是高频问题。你有这套敏感词过滤逻辑直接就能答出一二三四比含糊地说“我们靠管理员人工审核”要强太多。4. 核心功能实操实现关键代码与实现逻辑4.1 配置文件的细节很多人栽在这里一个典型的application.yml长这样很多启动报错都出在这一步的配置上。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/literature_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 timeout: 3000ms mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-secret-key-please-change expire: 604800几个容易被忽视的点allowPublicKeyRetrievaltrue必须加MySQL8 默认在没有公钥检索时如果用的是 caching_sha2_password 认证方式会报连接错误这个参数当年卡住了不少人serverTimezoneAsia/Shanghai不加的话时间字段会差 8 小时map-underscore-to-camel-case必须开否则数据库的create_time映射不到 Java 的createTime属性查出来全是 nulllogic-delete-field配置了逻辑删除之后MyBatis-Plus 执行删除操作会自动变成UPDATE SET deleted1而不是物理 DELETE数据还在只是查不出来。毕设项目强烈建议做逻辑删除答辩时被问到“数据能恢复吗”就能优雅作答4.2 登录注册JWT 认证的完整链路用户认证这块完整的链路是前端提交用户名密码 → 后端校验 → 签发 JWT → 前端存储 Token → 后续请求带在 Header 里 → 后端拦截器解析 Token → 放行或拒绝。核心代码分三块注册密码一定要加密存储用 BCrypt 而不是 MD5因为 MD5 可以通过彩虹表反查明文存储更是答辩雷区。Service public class UserServiceImpl implements UserService { Override public void register(RegisterDTO dto) { // 校验用户名是否已存在 Long count userMapper.selectCount( new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); if (count 0) { throw new BusinessException(用户名已存在); } // BCrypt 加密后入库 User user new User(); user.setUsername(dto.getUsername()); user.setPassword(BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt())); user.setNickname(dto.getNickname()); user.setRole(USER); user.setStatus(1); userMapper.insert(user); } }登录校验通过后签发 JWT把用户基本信息塞进 token 里。Override public LoginVO login(LoginDTO dto) { User user userMapper.selectOne( new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); if (user null) { throw new BusinessException(用户不存在); } // BCrypt 校验密码 if (!BCrypt.checkpw(dto.getPassword(), user.getPassword())) { throw new BusinessException(密码错误); } if (user.getStatus() 0) { throw new BusinessException(账号已被禁用请联系管理员); } // 签发 JWT默认有效期7天 String token JwtUtil.createToken(user.getId(), user.getUsername(), user.getRole()); LoginVO vo new LoginVO(); vo.setToken(token); vo.setUserInfo(user); return vo; }拦截器这是 JWT 认证的核心每个请求进来先过这一关。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行 OPTIONS 预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { // 解析成功说明 token 有效 Claims claims JwtUtil.parseToken(token); // 把用户ID放到 request 里后续 Controller 就不用重复解析 request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { // 解析失败说明 token 过期或伪造 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }拦截之后还要在配置类里注册并配置放行的白名单路径。Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns( /api/auth/login, /api/auth/register, /api/work/list, /api/work/detail/**, /api/work/chapter/** ); } Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }注意跨域配置里的.allowCredentials(true)要和前端的请求配置匹配否则浏览器会拦截响应。前后端分离的开发模式下这个配置不写好联调时天天报跨域错。4.3 作品发布与章节管理富文本与字数统计作品创作流程是创建作品填标题、简介、分类→ 添加章节填标题 正文→ 章节列表展示。这里有个重点章节正文用的是富文本还是 Markdown。富文本的话前端用 WangEditor 或 TinyMCE 都行后端保存 HTML 字符串预览时直接用v-html渲染实现成本最低。Markdown 的话要引入marked.js做解析展示效果更清爽但用户输入就不那么直观。我的建议是富文本因为用户群体是一般读者他们平时写公众号、写博客都是所见即所得编辑你给他一个 Markdown 编辑器反而会劝退。保存章节时有一个细节要做好就是字数统计。前端可以统计但后端必须重新算一遍防止前端传的假数据。后端统计对中文内容来说就是去除 HTML 标签后content.length()这段逻辑在作品列表和作者中心都要用到——一部小说的总字数就是所有章节字数的加和。4.4 阅读量统计Redis 缓存 定时落库阅读量是内容平台最核心的数据指标但如果每次访问都直接UPDATE work SET view_count view_count 1高并发下数据库撑不住还容易有锁竞争。我的方案是 Redis 先行异步落库。具体做法用户访问作品详情页时后端对 Redis 里的浏览计数器做INCR。然后启动一个定时任务每 5 分钟从 Redis 里把增量同步到 MySQL。SpringBoot 的定时任务开启方式很简单。EnableScheduling Configuration public class SchedulingConfig { }然后写一个定时任务类。Component public class ViewCountSyncTask { Resource private StringRedisTemplate stringRedisTemplate; Resource private WorkMapper workMapper; // 每5分钟执行一次把 Redis 里的阅读量增量同步到数据库 Scheduled(cron 0 */5 * * * ?) public void syncViewCount() { // 从 Redis 里取所有作品的阅读量 key比如 work:view:101 SetString keys stringRedisTemplate.keys(work:view:*); if (keys null || keys.isEmpty()) { return; } for (String key : keys) { String workId key.substring(work:view:.length()); Object viewCount stringRedisTemplate.opsForValue().get(key); if (viewCount ! null) { // 同步到数据库然后删除 key下一轮重新累计 workMapper.updateViewCount(Long.valueOf(workId), Long.valueOf(viewCount.toString())); stringRedisTemplate.delete(key); } } } }这个设计的妙处在于数据库的 update 操作从“每次访问一次”降级为“每5分钟批量一次”数据库压力暴减而且阅读量从“最终一致性”角度看完全能接受——读者看到数字差个几千几百根本不会在意。面试时把这种“缓存 异步落库”的链路讲清楚考官会认为你真的懂性能优化而不是只会照着教程敲。排名榜的做法也同理。Redis 里用 ZSet 结构work:rankscore 就是阅读量每次阅读 1 时同步对这个 ZSet 做ZINCRBY。取热榜直接ZREVRANGE work:rank 0 9拿到前 10 名整个过程不碰数据库响应是毫秒级的。4.5 评论系统嵌套结构的数据库设计与前端组装评论区是文学平台互动感的核心它的核心难点不在于写一条评论而在于楼中楼的嵌套展示。表结构我已经在上文说了核心是parent_id字段。前端展示时加载某个作品的所有评论按parent_id分组先渲染顶级评论再在每条顶级评论下面递归渲染它的子回复。这个递归逻辑前端写起来不难但要注意两点第一深楼层防无限递归。一般平台最多支持两层展示评论 回复下的回复就折叠你可以限制parent_id链的深度超过 3 级就直接挂在第 3 级下面不做更深层的嵌套。第二分页策略。顶级评论用分页子回复用树形加载也就是先加载第一页顶级评论点开某条评论时再异步加载它的所有子回复。一次性全量加载在数据量大的时候页面会卡死这个坑我实际踩过。后端查询子回复的 SQL 大概是SELECT * FROM comment WHERE work_id #{workId} AND parent_id #{parentId} ORDER BY create_time ASC顶级评论就是parent_id 0的查询。两条 SQL 就搞定整个嵌套楼层很好理解。4.6 作品导出与后端模板引擎毕设材料里可能需要一个“作品导出”的功能或者体现你处理文档的能力。实际使用场景是读者看一本小说连载到一半想导出全部内容离线看作者想把作品导出备份。这个功能如果前端做复杂的排版在浏览器里很难控制但后端用模板引擎就轻松多了。我用的方案是Freemarker XWPF生成.docx格式的文档。核心逻辑是查询作品基本信息 全部章节 → 组装文档内容 → 输出为文件 → 通过接口返回给前端下载。有些同学做前端用的是 Vue下载文件时会遇到 blob 类型处理的问题这个后面常见问题里我会展开讲。4.7 Vue 打包放进 SpringBoot 的完整操作这个步骤很多人问到其实操作本身不复杂关键是顺序。# 前端项目根目录下执行 npm run build构建完成后前端dist目录下会生成一堆静态文件index.html、js、css、图片等。把 dist 里所有文件复制到 SpringBoot 项目的src/main/resources/static目录下然后重新打包 SpringBoot 项目。mvn clean package -DskipTests打完包后一个xxx.jar里既包含后端接口也包含前端页面。启动 jar 后浏览器直接访问http://服务器IP:8080看到的就是你的平台首页不需要再单独部署前端。但这里有一个大坑必须说如果前端的 Vue Router 用的是history模式URL 里没有#刷新某个子页面时会报 404因为后端不认识/work/detail/101这种路径。解决方式两种前端用hash模式URL 带#例如http://ip:8080/#/work/detail/101这种模式不会出现刷新 404 的问题最省事后端加一个ErrorController或者自定义转发规则把所有未知路径转发到index.html毕设项目用hash模式是性价比最高的方案。5. 常见问题与排查技巧实录5.1 启动类直接报错常见坑汇总这一小节是我从各大讨论区顺手整理的高发问题结合自己的实践验证大部分问题照着改就能解决。报错场景原因解决方案启动报Failed to configure a DataSource数据库连接配置有误或没加数据源相关依赖检查application.yml的 url/username/password确认 pom 里有mysql-connector-java启动报Consider defining a bean of type xxxMapperMapper 接口没被扫描到启动类加MapperScan(com.example.mapper)或每个 Mapper 加Mapper访问接口报 401但确认已登录拦截器放行路径没配好把登录、注册、公开查询路径加到excludePathPatterns中文乱码数据库连接没指定编码或前端响应头没设置连接串加characterEncodingutf8SpringBoot 可以配server.servlet.encoding.charsetUTF-8上传图片体积大后报错SpringBoot 默认上传限制 1MB配置文件里加spring.servlet.multipart.max-file-size: 50MB5.2 跨域问题排查思路前后端分离开发时前端在 8081 端口跑后端在 8080浏览器会拦截跨域请求。很多人配了跨域还是报错我总结排查顺序是先看后端是否以OPTIONS请求收到了预检再看响应头里有没有Access-Control-Allow-Origin最后检查前端的请求配置里是否带了自定义 Header带自定义 Header 会导致预检请求如果后端没放行对应的allowedHeaders请求就过不去。我自己遇到最隐蔽的坑是和 JWT 拦截器搭配时拦截器没有放行OPTIONS请求导致预检请求直接 401前端看到的就是“跨域不通过”。解决办法我刚才已经给过了就是拦截器里第一行判断OPTIONS.equals(request.getMethod())直接放行。5.3 Docker 跑 MySQL 的注意点不少同学的本地环境没装 MySQL习惯了用 Docker 起容器这没问题但有两个细节必须避开。docker run --name mysql8 \ -e MYSQL_ROOT_PASSWORDyourpassword \ -p 3306:3306 \ -d mysql:8.0第一默认情况下 MySQL8 容器只允许localhost连接但项目部署在另一台机器或者本地主机的 SpringBoot 进程去连接时会被拒绝。要修改 root 用户的 host 为%ALTER USER root% IDENTIFIED WITH mysql_native_password BY yourpassword; FLUSH PRIVILEGES;这里的mysql_native_password是解决“客户端认证插件不兼容”问题的关键。新版 MySQL8 默认用caching_sha2_password老客户端不支持改回mysql_native_password就稳了。第二容器里时区默认是 UTC写入的时间会差 8 小时启动时要挂载时区docker run -e TZAsia/Shanghai ...5.4 文件名和下载接口的编码问题做作品导出下载的时候后端返回的文件名如果是中文浏览器解析会乱码下载出来的文件名全是一堆%E4%BD%A0%E5%A5%BD。解决办法是响应头里把文件名做 URL 编码String fileName URLEncoder.encode(作品名称.docx, UTF-8); response.setHeader(Content-Disposition, attachment; filename\ fileName \);前端 Axios 下载文件时还要注意响应类型文件接口必须用responseType: blob否则拿回来的是一堆乱码字符串Blob 对象转不出来。6. 部署上线一台云服务器从头到尾6.1 服务器环境安装毕设做完之后至少要通过公网访问让老师看一眼这就需要部署。很多同学本地跑得飞起一到服务器就懵我这里把最省心的方案写出来。服务器系统选 CentOS 7 或 Ubuntu 22.04 都行安装基础环境# Ubuntu 示例 sudo apt update sudo apt install openjdk-8-jdk maven mysql-server redis-server nginxMySQL 装好后要创建一个库CREATE DATABASE literature_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这里用utf8mb4而不是utf8就是因为文学平台要存 emoji 表情和生僻字utf8在 MySQL 里最多存 3 字节一些特殊字符会直接报“Incorrect string value”。6.2 项目打包上传启动# 后端打包 mvn clean package -DskipTests # 上传到服务器 scp target/literature-platform-0.0.1-SNAPSHOT.jar rootyour_server_ip:/opt/app/ # 启动 cd /opt/app nohup java -jar literature-platform-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod app.log 21 # 查看日志 tail -f /opt/app/app.lognohup加让应用在后台持续运行。这里有一个极容易被忽略的坑很多云服务器厂商默认防火墙是全部放行的或者默认只放行 80 端口你 8080 端口在外面根本访问不到。在云厂商控制台的安全组里加一条“放行 TCP 8080 端口”的规则。我当时在这个上面卡了小半天本地怎么 swagger 调都通就是外网访问不了最后发现是安全组没配。6.3 Nginx 反代与 HTTPS如果你想让访问地址变成http://你的域名/而不是http://IP:8080就用 Nginx 做反向代理。配置很固定server { listen 80; server_name your_domain.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; } }这一配外网访问直接走 80 端口看起来专业多了。7. 用户体验优化很多毕设只有功能没有细节功能跑通只是及格想让老师或面试官眼前一亮一定是在细节体验上多下功夫这三个优化点是我认为投入产出比最高的。7.1 骨架屏与加载状态页面请求数据拿到之前不能只显示一个白屏或者一个干巴巴的 loading 文字。用 Element Plus 的骨架屏组件el-skeleton在列表页、详情页铺一层占位数据返回后再切换成真实内容。这个看起来是小事但使用体验完全不一样。代码上也不复杂一个变量isLoading控制切换就行。7.2 空数据与错误提示空数据状态比想象中重要。用户搜索一个词没有结果你不能直接显示空白要显示图片 “没有找到相关作品换个关键词试试”这类引导。后端统一的ResultVO返回结构把code/message/data定好前端统一拦截 HTTP 错误码和业务错误码。Axios 拦截器里统一处理// 后端返回 code!200 时触发的统一提示 service.interceptors.response.use( (response) { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, (error) { ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } )这套统一异常处理链路做好前后端联调时能省非常多沟通成本。7.3 轻量埋点与数据看板平台上线后不能是一团迷雾你要能回答“今天有多少人访问了作品页”“哪个分类的作品最受欢迎”“最近 7 天阅读量趋势如何”。我的方案是轻量埋点阅读事件发生时在 Redis 里按天做计数器key 形如view:20250328。定时任务把数据汇总到stat_daily表后台管理端用 ECharts 展示折线图。功能不大但从“系统”到“数据驱动运营”的认知提升答辩时能给老师很大的惊喜感。8. 毕设之外这个项目如何写进简历 / 二次扩展思路做完一个完整的毕设项目最后一步是把它转化成简历上的一段经历。这里有一个大多数学生容易犯的错误只写“负责 XX 系统的前后端开发”一句话带过完全没有细节。一个更好的写法应该是分点列出基于 SpringBoot Vue 设计并实现了网络文学交流分享平台涵盖用户认证、作品管理、章节发布、评论互动、后台审核等核心模块基于 JWT 实现无状态登录认证与接口权限拦截密码采用 BCrypt 加密存储基于 Redis 的阅读量统计与热门榜单方案通过定时任务批量落库数据库压力降低约 80%实现了内容安全过滤基于 DFA 算法的敏感词检测与拦截支持敏感词动态维护每条都是一个“技术点 实现方式 效果量词”的组合有技术含量有实施细节有量化结果。面试官看到这样的描述才有进一步聊下去的兴趣。二次扩展方向的话优先推荐两个一个是把全文搜索从 MySQLLIKE升级为 Elasticsearch体现你对搜索引擎的理解另一个是引入 RabbitMQ 做消息异步解耦比如点赞通知、私信提醒这种业务场景用消息队列来处理就很合适。这两个方向都是在现有架构上加模块不会推翻重来属于稳妥的进阶路线。写到这里这个项目的核心链路已经完整了。从需求拆解到技术选型从表结构设计到接口实现从本地联调到了解部署每一步都是实打实能落地的方案。你在参考这份整理动手做的时候不用追求一步到位先把核心流程跑通再逐步加上加分功能。项目本身不难难的是把每个细节都做扎实。踩过的坑我都写在上面了按着这个路子走你会比自己摸索省下很多时间。
返回列表