☰
基于Spring Boot与Vue3的文学网站全栈开发实战:从架构设计到部署上线
2026/9/26 1:49:55 网站建设 项目流程

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_id=0是主评论。

第二,评论的审核。文学网很容易被灌广告,必须做过滤。我的做法是敏感词库+频率限制双管齐下。敏感词用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,游客用IP+UserAgent的哈希,在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要求17+
Maven3.9.x构建工具
MySQL8.0注意字符集用utf8mb4
Redis7.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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个设计要点解释一下。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 Page<ArticleVO> listArticles(ArticleQuery query) { Page<Article> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Article> 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); Page<Article> 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_PASSWORD=your_password \ -v /data/mysql:/var/lib/mysql \ mysql:8.0 --character-set-server=utf8mb4 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站点会标记"不安全",影响信任度。用Let's 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默认不允许携带凭证,需要显式配置allowCredentials=true,而且此时allowedOrigins不能用*,必须指定具体域名。

更彻底的方案是用Nginx做同源代理,前端请求/api/xxx,Nginx转发到后端,这样浏览器看来就是同源,根本没有跨域问题。生产环境我推荐这个方案,开发环境用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防护的实战教训

这个坑我印象最深。测试阶段有个同事在文章里插了个<script>alert(1)</script>,结果详情页真的弹窗了。当时吓出一身冷汗,赶紧补防护。

但防护也不能一刀切。我一开始用Jsoup的basic()白名单,结果发现<img>标签被过滤了,文章里的配图全没了。后来换成relaxed()再手动加需要的标签,才平衡了安全和功能。

还有一个隐蔽的坑:富文本内容在存储和展示两个环节都要过滤。有人只在存储时过滤,展示时直接v-html渲染,如果数据库被直接篡改(比如通过其他漏洞),照样会中招。我的做法是存储时过滤一次,展示前再过滤一次,双保险。

4.4 部署后访问慢的排查过程

上线后我发现首页加载要3秒多,用户体验很差。排查过程分享给大家。

第一步,打开浏览器开发者工具的Network面板,看哪个请求慢。发现是一个文章列表接口耗时2秒。

第二步,看后端日志,发现SQL执行了1.8秒。把SQL拿出来在数据库里EXPLAIN,发现type=ALL,全表扫描。

第三步,分析原因。查询条件是status=2 ORDER BY is_top DESC, create_time DESC,而我的索引是(status, create_time),is_top不在索引里,导致排序用不了索引。

第四步,调整索引为(status, is_top, create_time),重新执行,type=ref,耗时降到50ms。

这个案例说明,索引要跟着查询模式走,尤其是排序字段,一定要考虑进复合索引的顺序里。

4.5 缓存与数据库一致性处理

用了Redis缓存后,最头疼的是数据一致性。比如文章被编辑了,缓存里的旧数据什么时候失效?

我的策略是更新数据库后立即删除缓存,而不是更新缓存。为什么删而不是更新?因为更新缓存可能失败,而且并发下更新顺序难保证,删除更简单可靠。下次读取时缓存未命中,自然从数据库加载最新数据。

@Transactional public void updateArticle(Article article) { articleMapper.updateById(article); // 删除缓存,下次读取时重建 redisTemplate.delete("article:detail:" + article.getId()); }

这里有个经典问题:先删缓存还是先更新数据库?我的选择是先更新数据库,再删缓存。虽然理论上仍有极小概率的不一致窗口,但配合缓存的过期时间兜底,实际影响可以忽略。追求强一致就得上分布式锁或订阅binlog,对文学网这种场景属于过度设计。

5. 性能优化与后续扩展方向

项目能跑起来只是第一步,跑得好才是本事。这一节聊聊我做的优化和后续可以扩展的方向。

5.1 前端性能优化实操

前端优化我做了几件事。路由懒加载,把不同页面的代码分割成独立chunk,首屏只加载必要的。图片懒加载,列表里的封面图用loading="lazy",滚动到可视区域才加载。接口请求合并,首页需要的多个数据用一个聚合接口返回,减少请求数。

打包体积也要关注。我用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项目最怕的不是技术难,而是想得太多做得太少。先把最小闭环跑通,哪怕界面丑一点、功能少一点,只要核心流程能走通,后面就是不断迭代优化的过程。我见过太多人卡在"选什么框架""用什么架构"的纠结里,最后项目胎死腹中。动手做,遇到问题解决问题,这才是最快的成长路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询