尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于Spring Boot与Vue3的文学网站全栈开发实战:从架构设计到部署上线

发布时间:2026/9/26 1:49:59

资讯中心
01
ARTICLE

基于Spring Boot与Vue3的文学网站全栈开发实战:从架构设计到部署上线

基于Spring Boot与Vue3的文学网站全栈开发实战:从架构设计到部署上线
1. 项目缘起与整体设计思路做文学类网站这件事我从接需求到最终上线前后折腾了差不多两个月。中间踩过的坑、推翻过的方案、半夜爬起来改配置的经历说实话比写代码本身还多。这篇文章不讲虚的就把一个基于WEB的文学网从需求梳理、技术选型、核心功能实现到最终部署上线的完整过程摊开来讲适合正在做课程设计的学生、想练手全栈项目的开发者以及需要快速搭建内容型网站的朋友参考。文学网这个品类表面上看就是文章列表详情页后台管理三件套但真正动手做的时候你会发现它涉及的东西远比想象中杂用户体系、内容审核、富文本编辑、搜索、分类标签、评论互动、SEO友好、响应式布局、静态资源加速……每一项单拎出来都有讲究。我见过太多人一开始雄心勃勃要搞个大而全的平台结果卡在环境配置上就放弃了。所以我的思路很明确先跑通最小闭环再逐步叠加功能保证每一步都有可运行的产物。1.1 为什么选WEB而不是其他形态有人会问现在做内容站为什么不直接上小程序或者App我的判断依据有三点。第一文学类内容的消费场景以长文阅读为主PC端和移动端浏览器都能覆盖WEB的触达成本最低用户不需要下载安装。第二搜索引擎对WEB页面的收录是天然的流量入口文学站很依赖搜索某篇文章/某个作者进来的自然流量这一点小程序做不到。第三开发和迭代成本WEB一套代码适配多端对于个人或小团队来说是最优解。当然WEB也有它的短板比如离线阅读体验不如App、推送能力弱。但对于一个以内容展示和阅读为核心的文学网来说这些不是刚需。我的取舍逻辑就是优先满足核心阅读体验非核心能力后置。1.2 技术选型的取舍逻辑技术栈这块我最终定的是Spring Boot MyBatis-Plus MySQL Redis Vue3 Nginx。为什么这么选逐个说。后端用Spring Boot理由很直接生态成熟、上手快、社区资料多遇到问题基本都能搜到答案。对于文学网这种以CRUD为主、并发量中等的项目Spring Boot完全够用没必要上微服务那一套把简单问题复杂化。我见过有同学做个课程设计硬上Spring Cloud结果光服务注册就调了两天得不偿失。ORM层选MyBatis-Plus而不是JPA是因为文学网涉及大量自定义查询——比如按分类标签关键词时间范围组合筛选文章MyBatis-Plus的Wrapper和自定义SQL写起来更顺手性能也更好控制。数据库用MySQL这个没什么争议关系型数据、事务支持、成熟稳定。缓存用Redis主要解决两个问题一是热门文章的阅读计数避免每次阅读都写库二是首页和分类页的数据缓存降低数据库压力。前端用Vue3配合Element Plus组件库开发效率高响应式布局好做。这里要说明一下如果你对前端不熟用Thymeleaf做服务端渲染也完全可以SEO还更友好。我选前后端分离主要是考虑到后续可能扩展移动端接口复用方便。部署用Nginx做反向代理和静态资源服务这是标配后面部署章节会详细讲配置。1.3 整体架构分层整个系统的架构我分成四层来理解这样排查问题时能快速定位是哪一层出了毛病层级职责涉及组件接入层请求分发、静态资源、负载Nginx应用层业务逻辑、接口服务Spring Boot缓存层热点数据、会话、计数Redis数据层持久化存储MySQL这个分层看起来简单但实际开发中很多人会把缓存逻辑和业务逻辑混在一起写导致后期维护困难。我的做法是缓存操作统一封装成工具类业务层只调用方法不直接碰RedisTemplate这样后续换缓存方案或者调整策略时改动面很小。2. 核心功能模块的细节拆解文学网的功能看着多但真正核心的就那么几块。我把它们按优先级排了个序先做能跑通主流程的再补锦上添花的。下面逐个拆解每个模块我都会说清楚做什么、怎么做、注意什么。2.1 用户体系与权限设计用户体系是地基做不好后面全是坑。我的设计是三种角色游客、注册用户、管理员。游客能浏览文章列表和详情注册用户可以评论、收藏、点赞管理员负责内容审核和用户管理。权限控制我用的是Spring Security JWT的方案。这里有个细节要提醒很多人做JWT只存用户ID结果每次请求都要查库拿用户信息性能很差。我的做法是把用户ID、角色、昵称等常用信息都塞进Token的payload服务端解析后直接用只有需要敏感操作时才回查数据库确认状态。密码存储必须加密我用的是BCrypt这是Spring Security自带的加盐哈希安全性足够。千万不要用MD5现在彩虹表随便就能撞出来。// 密码加密示例 String rawPassword user_input_password; String encoded passwordEncoder.encode(rawPassword); // 校验时 boolean matches passwordEncoder.matches(rawPassword, encoded);注意JWT的密钥一定要放在配置文件里不要硬编码在代码中且长度要足够建议256位以上。密钥泄露等于所有用户的登录态都被人伪造。2.2 文章发布与富文本处理文章发布是文学网的核心功能。这里最大的坑是富文本编辑器的选型和XSS防护。编辑器我对比了几个方案UEditor太重且停止维护CKEditor功能全但配置复杂最终选了WangEditor轻量、中文文档友好、Vue集成方便。它输出的HTML结构比较干净后续处理省心。但富文本最大的风险是XSS攻击。用户如果在文章里插入恶意脚本其他读者打开就会中招。我的防护策略是白名单过滤用Jsoup对提交的HTML做清洗只允许安全的标签和属性通过。// 使用Jsoup做白名单过滤 Safelist safelist Safelist.relaxed() .addTags(h1, h2, h3, blockquote, pre, code) .addAttributes(img, src, alt, title) .addProtocols(img, src, http, https); String cleanHtml Jsoup.clean(rawHtml, safelist);这段代码的意思是只放行常见的排版标签img只允许http和https协议的src其他一律干掉。实测下来这样既能保留正常的排版效果又能挡住绝大多数注入尝试。文章内容我建议同时存两份一份是原始HTML用于展示一份是纯文本用于全文搜索和摘要生成。纯文本用Jsoup的text()方法提取即可。2.3 分类、标签与检索文学网的内容组织我用的是分类标签的双维度。分类是树形结构比如小说下面分言情悬疑科幻标签是扁平的比如治愈虐心经典。为什么两个都要因为分类是强归属一篇文章只能属于一个分类标签是弱关联一篇文章可以打多个标签。这样用户既能按大类浏览也能按兴趣标签发现内容。检索这块数据量小的时候用MySQL的LIKE就能扛但数据量上到十万级就会明显变慢。我的方案是先用MySQL全文索引过渡数据量大了再上Elasticsearch。MySQL全文索引对中文支持不好需要配合分词插件或者用ngram解析器。-- 创建全文索引ngram解析器支持中文 ALTER TABLE article ADD FULLTEXT INDEX ft_title_content (title, content) WITH PARSER ngram; -- 查询 SELECT * FROM article WHERE MATCH(title, content) AGAINST(关键词 IN BOOLEAN MODE);提示ngram的token size默认是2也就是按两字切分。如果你的检索需求以单字为主需要调整这个参数但会增大索引体积要权衡。2.4 评论与互动机制评论功能看似简单实则暗藏玄机。我总结了几个必须处理的问题第一评论的层级。是平铺还是嵌套我选的是两级主评论回复。再深的层级用户体验反而差而且查询复杂度飙升。实现上用parent_id字段标识回复关系parent_id0是主评论。第二评论的审核。文学网很容易被灌广告必须做过滤。我的做法是敏感词库频率限制双管齐下。敏感词用DFA算法构建前缀树匹配效率高频率限制用Redis的计数器同一IP一分钟内超过5条就拦截。第三评论的计数。文章列表要显示评论数如果每次都COUNT(*)查询性能很差。我的方案是在文章表冗余一个comment_count字段评论增删时同步更新用Redis做缓冲定时回写数据库。// 评论计数缓冲示例 public void incrementCommentCount(Long articleId) { String key article:comment:count: articleId; redisTemplate.opsForValue().increment(key); // 定时任务每5分钟把Redis计数同步到数据库 }2.5 阅读计数与防刷阅读量是文学网的重要指标但也是最容易被刷的数据。如果每次刷新都1作者自己刷一刷就能上热门不公平。我的防刷策略是基于用户标识时间窗口去重登录用户用userId游客用IPUserAgent的哈希在Redis里存一个带过期时间的标记比如同一用户对同一篇文章30分钟内只计一次。public boolean recordRead(Long articleId, String userKey) { String key read: articleId : userKey; Boolean success redisTemplate.opsForValue() .setIfAbsent(key, 1, 30, TimeUnit.MINUTES); if (Boolean.TRUE.equals(success)) { redisTemplate.opsForValue().increment(article:read:count: articleId); return true; } return false; }setIfAbsent是原子操作并发下不会重复计数这个很关键。阅读量同样走Redis缓冲定时回写。3. 从零到一的实操落地过程前面讲的是设计层面的东西这一节我把实际操作过程完整走一遍包括环境搭建、关键代码、配置细节。你可以直接照着做。3.1 开发环境准备与项目初始化环境这块我列一下我用的版本避免版本不一致导致的玄学问题组件版本说明JDK17Spring Boot 3.x要求17Maven3.9.x构建工具MySQL8.0注意字符集用utf8mb4Redis7.x缓存Node.js18.x前端构建Nginx1.24部署项目初始化我用Spring Initializr生成骨架依赖勾选Spring Web、Spring Security、MyBatis-Plus、MySQL Driver、Redis、Lombok、Validation。生成后先跑一下mvn clean package确认能编译通过再开始写业务。数据库字符集一定要用utf8mb4不然emoji和某些生僻字会乱码。建库语句CREATE DATABASE literature_web DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;3.2 数据库表结构设计核心表我设计了这几张用户表、文章表、分类表、标签表、文章标签关联表、评论表、收藏表。这里重点说文章表因为字段最多、设计最讲究。CREATE TABLE article ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, summary VARCHAR(500), content LONGTEXT NOT NULL, plain_content LONGTEXT, cover_url VARCHAR(500), category_id BIGINT NOT NULL, author_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0草稿 1待审 2已发布 3下架, view_count INT DEFAULT 0, like_count INT DEFAULT 0, comment_count INT DEFAULT 0, is_top TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_author (author_id), INDEX idx_status_time (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个设计要点解释一下。summary是摘要列表页展示用避免每次都读大字段content。plain_content是纯文本用于搜索。status用数字枚举而不是字符串节省空间且查询快。索引方面idx_status_time是复合索引因为列表页最常见的查询就是已发布的文章按时间倒序这个索引能直接命中。注意content用LONGTEXT单篇长文的存储没问题。但要注意MySQL的max_allowed_packet配置默认4MB如果文章特别长可能插入失败需要调大。3.3 后端接口开发要点接口我按RESTful风格设计统一返回格式{ code: 200, message: success, data: {} }分页查询用MyBatis-Plus的Page对象配合自定义的Wrapper。文章列表接口的核心逻辑public PageArticleVO listArticles(ArticleQuery query) { PageArticle page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperArticle wrapper new LambdaQueryWrapper(); wrapper.eq(Article::getStatus, 2); // 只查已发布 if (query.getCategoryId() ! null) { wrapper.eq(Article::getCategoryId, query.getCategoryId()); } if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Article::getTitle, query.getKeyword()); } wrapper.orderByDesc(Article::getIsTop) .orderByDesc(Article::getCreateTime); PageArticle result articleMapper.selectPage(page, wrapper); return convertToVO(result); }这里有个性能细节列表页只需要标题、摘要、封面、作者等字段不需要content。所以我用select指定字段避免查出大字段浪费带宽和内存。wrapper.select(Article::getId, Article::getTitle, Article::getSummary, Article::getCoverUrl, Article::getAuthorId, Article::getViewCount, Article::getCreateTime);3.4 前端页面与交互实现前端我用Vue3 Vite Element Plus。页面结构分三大块首页、文章详情页、个人中心。首页是文章流我做的是瀑布流分页加载。滚动到底部自动加载下一页用IntersectionObserver监听。这里要注意防抖不然快速滚动会触发多次请求。const observer new IntersectionObserver((entries) { if (entries[0].isIntersecting !loading.value hasMore.value) { loadMore() } }, { threshold: 0.1 })文章详情页的重点是阅读体验。字号、行高、段落间距都要调舒服。我用的正文字号16px行高1.8段落间距1.5em最大宽度限制在800px左右太宽了眼睛扫行会累。响应式布局用CSS的媒体查询移动端把侧边栏隐藏正文占满宽度。断点我设在768px。3.5 部署上线全流程部署这块是重头戏我按步骤来。第一步服务器环境准备。装JDK、MySQL、Redis、Nginx。MySQL和Redis建议用Docker部署省去配置烦恼docker run -d --name mysql -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyour_password \ -v /data/mysql:/var/lib/mysql \ mysql:8.0 --character-set-serverutf8mb4 docker run -d --name redis -p 6379:6379 \ -v /data/redis:/data redis:7 --requirepass your_redis_password第二步打包后端。mvn clean package -DskipTests生成jar包用nohup java -jar xxx.jar 后台运行。更规范的做法是写systemd服务开机自启、崩溃重启。第三步打包前端。npm run build生成dist目录把静态文件放到Nginx的html目录。第四步配置Nginx。这是关键配置好坏直接影响访问速度和稳定性server { listen 80; server_name your_domain.com; # 前端静态资源 location / { root /var/www/literature/dist; try_files $uri $uri/ /index.html; expires 7d; } # 后端接口反向代理 location /api/ { 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 ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { root /var/www/literature/dist; expires 30d; add_header Cache-Control public, immutable; } }try_files那行很重要它保证前端路由刷新不会404。expires设置静态资源缓存减少重复请求。反向代理的X-Real-IP头要带上不然后端拿到的都是Nginx的IP防刷和日志分析会失效。提示如果前端路由用的是history模式try_files必须配否则用户刷新非首页路由会报404。用hash模式则没这个问题但URL难看。第五步配置HTTPS。现在浏览器对HTTP站点会标记不安全影响信任度。用Lets Encrypt的证书免费且自动续期。4. 常见问题排查与避坑经验这一节是我踩坑最多的地方也是最有价值的部分。我把遇到的问题整理成速查表再补充几个典型案例的排查过程。4.1 高频问题速查表问题现象可能原因排查方向解决方案启动报数据库连接失败连接串/账号密码错检查application.yml核对host、port、库名、密码中文乱码字符集不统一查库、表、连接串字符集统一utf8mb4接口跨域报错前后端不同源看浏览器控制台配置CORS或Nginx代理静态资源404Nginx路径配错查root和location核对dist实际路径刷新页面404history路由未配查try_files加try_files配置Redis连接超时密码/网络问题用redis-cli测试核对密码和防火墙文章列表慢缺索引或查大字段看慢查询日志加索引、指定select字段4.2 跨域问题的完整解决跨域是前后端分离项目绕不开的坎。我一开始在Controller上加CrossOrigin但发现带Cookie的请求还是失败。后来才搞明白CrossOrigin默认不允许携带凭证需要显式配置allowCredentialstrue而且此时allowedOrigins不能用*必须指定具体域名。更彻底的方案是用Nginx做同源代理前端请求/api/xxxNginx转发到后端这样浏览器看来就是同源根本没有跨域问题。生产环境我推荐这个方案开发环境用CORS配置即可。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }注意这里用的是allowedOriginPatterns而不是allowedOrigins前者支持通配符且兼容allowCredentials后者在Spring Boot 2.4之后用*配合凭证会报错。4.3 富文本XSS防护的实战教训这个坑我印象最深。测试阶段有个同事在文章里插了个scriptalert(1)/script结果详情页真的弹窗了。当时吓出一身冷汗赶紧补防护。但防护也不能一刀切。我一开始用Jsoup的basic()白名单结果发现img标签被过滤了文章里的配图全没了。后来换成relaxed()再手动加需要的标签才平衡了安全和功能。还有一个隐蔽的坑富文本内容在存储和展示两个环节都要过滤。有人只在存储时过滤展示时直接v-html渲染如果数据库被直接篡改比如通过其他漏洞照样会中招。我的做法是存储时过滤一次展示前再过滤一次双保险。4.4 部署后访问慢的排查过程上线后我发现首页加载要3秒多用户体验很差。排查过程分享给大家。第一步打开浏览器开发者工具的Network面板看哪个请求慢。发现是一个文章列表接口耗时2秒。第二步看后端日志发现SQL执行了1.8秒。把SQL拿出来在数据库里EXPLAIN发现typeALL全表扫描。第三步分析原因。查询条件是status2 ORDER BY is_top DESC, create_time DESC而我的索引是(status, create_time)is_top不在索引里导致排序用不了索引。第四步调整索引为(status, is_top, create_time)重新执行typeref耗时降到50ms。这个案例说明索引要跟着查询模式走尤其是排序字段一定要考虑进复合索引的顺序里。4.5 缓存与数据库一致性处理用了Redis缓存后最头疼的是数据一致性。比如文章被编辑了缓存里的旧数据什么时候失效我的策略是更新数据库后立即删除缓存而不是更新缓存。为什么删而不是更新因为更新缓存可能失败而且并发下更新顺序难保证删除更简单可靠。下次读取时缓存未命中自然从数据库加载最新数据。Transactional public void updateArticle(Article article) { articleMapper.updateById(article); // 删除缓存下次读取时重建 redisTemplate.delete(article:detail: article.getId()); }这里有个经典问题先删缓存还是先更新数据库我的选择是先更新数据库再删缓存。虽然理论上仍有极小概率的不一致窗口但配合缓存的过期时间兜底实际影响可以忽略。追求强一致就得上分布式锁或订阅binlog对文学网这种场景属于过度设计。5. 性能优化与后续扩展方向项目能跑起来只是第一步跑得好才是本事。这一节聊聊我做的优化和后续可以扩展的方向。5.1 前端性能优化实操前端优化我做了几件事。路由懒加载把不同页面的代码分割成独立chunk首屏只加载必要的。图片懒加载列表里的封面图用loadinglazy滚动到可视区域才加载。接口请求合并首页需要的多个数据用一个聚合接口返回减少请求数。打包体积也要关注。我用webpack-bundle-analyzer分析发现Element Plus全量引入占了很大体积。改成按需引入后打包体积从2MB降到800KB。// vite.config.js 按需引入配置 import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default { plugins: [ AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] }5.2 后端接口响应优化后端优化核心是减少数据库交互。我做了几件事批量查询代替循环单查比如文章列表要显示作者名先用IN查出所有作者再映射而不是循环里一个个查。热点数据加缓存分类列表、标签列表这种变动少的数据缓存10分钟。异步处理非核心逻辑比如阅读计数、日志记录用Async丢到线程池不阻塞主流程。Async(taskExecutor) public void asyncRecordRead(Long articleId, String userKey) { // 计数逻辑不阻塞接口返回 }线程池要自定义配置别用默认的默认线程池在任务堆积时可能OOM。5.3 后续可扩展的功能项目上线后我列了几个后续想做的方向。全文检索升级到Elasticsearch支持分词、高亮、相关度排序。推荐系统根据用户阅读历史推荐相似文章初期可以用简单的协同过滤。评论审核自动化接入内容安全接口减少人工审核成本。多端适配把接口复用给小程序。这些扩展不是必须的但如果你想把这个项目做成作品集里的亮点挑一两个深入做比堆一堆半成品强得多。5.4 安全加固清单最后列一份安全加固清单都是实战中验证有效的所有用户输入做校验和过滤前端校验只是体验后端校验才是防线接口做频率限制防止暴力破解和爬虫敏感操作改密码、删文章二次验证数据库账号最小权限应用账号不给DROP权限定期备份数据库且要验证备份可恢复日志脱敏不要把密码、Token打进日志依赖定期更新关注安全漏洞公告我个人在实际操作中的体会是做WEB项目最怕的不是技术难而是想得太多做得太少。先把最小闭环跑通哪怕界面丑一点、功能少一点只要核心流程能走通后面就是不断迭代优化的过程。我见过太多人卡在选什么框架用什么架构的纠结里最后项目胎死腹中。动手做遇到问题解决问题这才是最快的成长路径。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。