1. 项目概述:文学创作社交论坛到底在做什么
先说结论:这不是一个简单的博客或内容管理系统,而是一个把“文学创作”和“社区互动”深度绑定的垂直类社交平台。文学创作是内容端,社交论坛是互动端,两者通过用户体系、作品体系和行为体系串成一条完整链路。
我最初接手这个项目时,团队内部争论过一轮:要不要直接用现成的Discuz或者WordPress搭个论坛?最终放弃的原因很简单——这类通用建站系统在“作品管理”和“作者成长体系”上完全不给力。纯文学社区的核心资产是长篇小说连载、章节管理、创作数据统计、读者互动关系,通用论坛连“把一篇10万字的作品拆成32个章节并自动生成目录”这种基础需求都做得很别扭。所以最终决定自研,技术栈锁定为 SpringBoot + Vue3 + MyBatis + MySQL,前后端完全分离。
这个系统的核心业务逻辑其实可以拆成三层:
- 内容创作层:作者创建作品、维护章节、管理分类、设置完结状态。
- 社交互动层:读者阅读、评论、点赞、打赏、关注作者、收藏作品、发帖讨论。
- 平台治理层:用户注册登录、敏感词过滤、内容审核、管理员后台。
文中会涉及完整的数据库设计、后端API拆分、前端Vue3组件化实现,以及部署上线时那些不出现在官方文档里的坑。适合谁看?正在做毕业设计或课程项目的同学、想从单体应用升级到前后端分离架构的Java开发者、以及准备接手这类社区系统的初级工程师。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心技术与架构选型:为什么这套组合能扛起内容社交场景
2.1 后端SpringBoot的角色定位:不仅是框架,更是“业务流程编排器”
SpringBoot在这个项目里承担的职责远不止“提供RESTful API”。它更像是整个系统的业务流程编排器:接收前端请求、调用MyBatis的Mapper接口操作数据库、执行业务校验、触发消息通知、返回统一格式的响应体。
实际开发中我建议把项目按功能域分包,而不是按传统Controller/Service/Mapper三层分包。这套系统功能模块清晰,按域分包后维护成本会明显降低:
bash复制com.xxx.literary
├── common # 通用工具类、统一响应体、异常处理
├── config # 跨域配置、MyBatis配置、拦截器配置
├── security # 登录拦截、JWT工具、密码加密
├── user # 用户模块:注册登录、个人中心
├── work # 作品模块:作品CRUD、章节管理
├── social # 社交模块:评论、点赞、关注、帖子
├── admin # 后台管理:用户管理、审核
└── files # 文件上传(封面图、头像)
每个业务域内部保留Controller/Service/Mapper/Entity的标准结构,模块之间通过Service方法调用,避免跨Controller直接互调导致耦合。
2.2 为什么选MyBatis而不是MyBatis-Plus或JPA
这个问题我每次分享都会被问到。三个持久层框架我都用过,在这个项目里的决策逻辑如下:
| 维度 | MyBatis | MyBatis-Plus | JPA |
|---|---|---|---|
| SQL可控性 | 完全手写,灵活度最高 | 内置CRUD,复杂SQL需手写 | 自动生成,复杂查询难掌控 |
| 复杂多表关联 | 手动编写,明确可控 | 手写或注解 | 容易产生性能隐患 |
| 学习门槛 | 中等,需理解SQL映射 | 较低,CRUD零代码 | 中高,需理解持久化上下文 |
| 性能调优空间 | 最高 | 中等 | 低 |
| 适合场景 | 大型复杂业务、性能敏感 | 中小型项目、快速开发 | 简单模型、标准CRUD居多 |
最终选了原生MyBatis,核心原因是文学创作社区天然存在大量复杂多表查询,比如“查找一部作品的连载动态和最近互动作者”这种跨work、user、social多表关联,手写SQL能精确控制join时机和返回列,不会出现JPA那种N+1问题,也不会被Plus的复杂条件构造器绕晕。
2.3 数据库设计:五张核心表的字段与关联逻辑
文学创作论坛的数据模型是典型的内容+关系混合结构。最核心的5张表如下:
用户表t_user:
id:自增主键username:唯一用户名(登录凭证)password:BCrypt加密密文nickname:展示昵称avatar:头像URLsignature:个性签名/个人简介role:对应用户权限(1普通用户、2签约作者、3管理员)status:账号状态(0正常、1封禁)create_time:注册时间
作品表t_literary_work:
id:自增主键user_id:作者ID,关联用户表(创建索引)title:作品标题description:作品简介cover_url:封面图URLcategory_id:分类(玄幻、科幻、悬疑、言情等)status:状态(0连载中、1已完结、2草稿)word_count:总字数(每次更新章节时累加)like_count/favorite_count:互动数据冗余字段create_time/update_time
章节表t_chapter:
id:自增主键work_id:所属作品ID(创建联合索引(work_id, sort_order))chapter_title:章节标题content:长文本内容(使用TEXT类型)word_count:本文字数sort_order:章节排序号
帖子表t_post(论坛讨论区):
id:自增主键user_id:发布者IDtitle:帖子标题content:帖子内容category_id:版块IDview_count:浏览量like_count:点赞数comment_count:评论数(冗余,避免COUNT查询)create_time:发布时间
评论表t_comment:
id:自增主键content:评论内容user_id:评论者IDentity_type:评论属于什么对象(1作品章节、2论坛帖子)entity_id:对应对象的IDparent_id:父评论ID(用于楼中楼回复)create_time:评论时间
这5张表基本覆盖了系统的生态闭环:用户发布作品、读者看作品、读者评论作品、读者发帖聊作品。互动表(点赞、关注、收藏)我额外设计了独立表,不要把用户的收藏关系硬塞进作品表里加个字段,那等于给自己埋雷。
2.4 前后端分离的实际落地:Vue3如何与SpringBoot协同工作
前后端分离的真正价值不在于“代码不在一起”,而在于团队可以并行开发、接口能够独立演化、前端可以独立打包部署。
Vue3这边选用的技术栈明确:
- Vite:新一代构建工具,开发环境下热更新速度优于Webpack,实测项目规模中等时基本秒级刷新
- Pinia:Vue3官方推荐的全局状态管理库,替代Vuex
- Vue Router 4:前端路由,实现各页面切换
- Axios:HTTP请求库,统一添加JWT Token、处理响应拦截
前端开发模式下的代理配置(vite.config.js):
javascript复制export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
这样配置后,前端开发环境请求/api/user/login会转发到后端8080端口,既解决了跨域问题,又方便本地联调。
后端的跨域配置则使用SpringBoot的CORS机制作为兜底,因为生产环境中前后端会部署在不同域名或不同端口(比如前端部署Nginx的80端口,后端独立Tomcat容器部署):
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
提示:
allowCredentials(true)时allowedOrigins("*")是无效的,必须使用allowedOriginPatterns("*")。这个坑踩过一次才记住。
3. 核心功能模块与实现方案:从用户登录到作品连载全链路
3.1 用户模块:Spring Security还是不用的纠结
文学创作论坛虽然涉及用户体系,但我没有引入Spring Security全家桶。原因很直接:项目需要的是轻量级登录拦截和密码加密,不需要Spring Security那种复杂的过滤器链和授权机制。引入它会显著增加配置文件的复杂度和学习成本。
替代方案是JWT + 自定义拦截器:
java复制@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行OPTIONS预检请求
if ("OPTIONS".equals(request.getMethod())) {
return true;
}
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token) || !token.startsWith("Bearer ")) {
throw new BusinessException(401, "未登录");
}
token = token.substring(7);
// 解析JWT,获取用户信息
Claims claims = JwtUtil.parseToken(token);
if (claims == null) {
throw new BusinessException(401, "登录已过期");
}
// 用户信息存入ThreadLocal,供后续业务获取
UserContext.setUserId(Long.valueOf(claims.get("userId").toString()));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.clear();
}
}
注册时密码加密用BCryptPasswordEncoder(来自spring-security-crypto独立包),不需要引入Spring Security全家桶。这样既获得了安全防护能力,又避免了配置复杂度:
code复制dependency上只需要添加:
org.springframework.security:spring-security-crypto
同时需要引入:commons-logging(BCrypt的依赖项)
JWT工具类封装了生成和解析Token的逻辑:
java复制public class JwtUtil {
private static final String SECRET = "your-secret-key-min-32-bytes";
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 7天
public static String generateToken(Long userId, String username) {
// 使用 jjwt 库生成token,携带userId和username信息
}
public static Claims parseToken(String token) {
// 解析token,返回Claims;若校验失败返回null
}
}
3.2 文学创作模块:作品发布、章节管理、字数统计
这是整个系统的灵魂模块,也是和其他普通论坛拉开差距的核心功能。作品的创建、更新、删除、章节管理、字数统计、状态流转都需要严谨设计。
作品发布流程:
- 前端表单填写标题、简介、分类、封面图(El-Plus的Upload组件)
- 后端接收并创建
draft状态的作品记录 - 作者进入作品管理页,新增章节
- 章节保存后自动累加作品总字数
- 全部章节写完可手动切换为“完结”状态
java复制@Service
public class WorkServiceImpl implements WorkService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createChapter(ChapterCreateDTO dto) {
// 1. 校验作品是否存在且属于当前用户
LiteraryWork work = workMapper.selectById(dto.getWorkId());
if (work == null) {
throw new BusinessException("作品不存在");
}
if (!work.getUserId().equals(UserContext.getUserId())) {
throw new BusinessException("无权操作该作品");
}
// 2. 计算当前最大sortOrder,实现追加
Integer maxSortOrder = chapterMapper.selectMaxSortOrder(dto.getWorkId());
int newSortOrder = (maxSortOrder == null ? 0 : maxSortOrder) + 1;
// 3. 保存章节、统计本文字数
Chapter chapter = new Chapter();
chapter.setWorkId(dto.getWorkId());
chapter.setChapterTitle(dto.getChapterTitle());
chapter.setContent(dto.getContent());
chapter.setSortOrder(newSortOrder);
chapter.setWordCount(dto.getContent().length());
chapterMapper.insert(chapter);
// 4. 累加作品总字数(更新冗余字段,避免后续COUNT查询大文本字段)
work.setWordCount(work.getWordCount() + dto.getContent().length());
workMapper.updateWordCount(work.getId(), work.getWordCount());
return chapter.getId();
}
}
章节删除时一定要注意:删除章节后需要同步扣减作品总字数,并且后序章节的sort_order需要重新排序。如果排序乱了,前端目录会非常难看。我专门写了一个重建排序的脚本:
java复制@Transactional(rollbackFor = Exception.class)
public void rebuildSortOrder(Long workId) {
List<Chapter> chapters = chapterMapper.selectByWorkIdOrderBySort(workId);
int order = 1;
for (Chapter chapter : chapters) {
chapterMapper.updateSortOrder(chapter.getId(), order);
order++;
}
}
3.3 社交互动模块:评论与点赞的性能取舍
评论表的设计要点在于多态关联:同一张评论表同时服务“章节评论”和“帖子评论”。实现方式是entity_type + entity_id组合。虽然不满足严格的数据库第三范式,但胜在简单直接——不用为不同评论类型建多张表,查询时按类型过滤即可。
服务的查询接口(查某个作品章节课的所有评论):
java复制public List<CommentVO> listChapterComments(Long workId, Long chapterId, int page, int size) {
// 先查作品,确认chapter属于该作品,防止越权访问
Chapter chapter = chapterMapper.selectById(chapterId);
if (chapter == null || !chapter.getWorkId().equals(workId)) {
throw new BusinessException("章节不存在");
}
// 查询评论,同时关联用户信息(昵称、头像)
// 使用PageHelper分页
PageHelper.startPage(page, size);
List<Comment> comments = commentMapper.selectByEntity(1, chapterId);
// 组装VO,追加用户昵称头像,处理parent_id嵌套
}
点赞功能我用了Redis缓存 + 定时刷库的策略。用户点赞先写入Redis的Set(like_entity_{type}_{entityId}),点赞数直接读取SCARD命令获取集合大小。每30分钟通过定时任务将Redis中的数据同步到MySQL的冗余字段like_count。这个方案能在高并发下保护数据库,代码也不复杂:
bash复制用户点赞逻辑:
1. SADD like_entity_1_{chapterId} {userId}
2. SREM like_entity_1_{chapterId} {userId} # 取消点赞
3. 前端需要知道当前用户是否点赞 → SISMEMBER like_entity_1_{chapterId} {userId}
4. 获取点赞总数 → SCARD like_entity_1_{chapterId}
提示:Redis的Set天然去重,一个用户对同一对象只能有一个点赞状态。比每次查询MySQL判断是否存在快得多,也避免了并发重复写入问题。
3.4 论坛帖子模块:搜索与列表分页
论坛帖子是典型的“读多写少”场景。列表接口需要支持分页、按分类筛选、按时间/热度排序。我用的是MyBatis 动态SQL + PageHelper:
xml复制<select id="selectPostPage" resultType="com.example.vo.PostVO">
SELECT
p.id, p.title, p.view_count, p.like_count,
p.comment_count, p.create_time,
u.nickname, u.avatar
FROM t_post p
LEFT JOIN t_user u ON p.user_id = u.id
<where>
<if test="categoryId != null">
AND p.category_id = #{categoryId}
</if>
<if test="keyword != null and keyword != ''">
AND (p.title LIKE CONCAT('%', #{keyword}, '%')
OR p.content LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
ORDER BY
<choose>
<when test="sortType == 'hot'">
p.like_count DESC, p.comment_count DESC
</when>
<otherwise>
p.create_time DESC
</otherwise>
</choose>
</select>
关键词搜索初期直接用的MySQL的LIKE '%keyword%'。实测在10万条数据量级下,配合索引依然存在性能问题。如果后续数据量继续增长,建议引入全文索引或搜索引擎(比如Elasticsearch,但那是后话了)。当前阶段用LIKE足够,不要过度设计。
4. 部署与运行:从0到1把系统跑起来
4.1 本地开发环境清单
- JDK 8+(实际用的JDK 8或11,不建议直接用Java 17除非SpringBoot版本匹配)
- Maven 3.6+
- MySQL 5.7+(8.0也可以,但驱动版本和连接串略有差异)
- Node.js 14+(Vite 3建议Node 14.18+或16+)
- Redis 5.0+(用于点赞、Token存储、验证码)
4.2 后端启动步骤(五分钟完成)
第一步,初始化数据库:
bash复制mysql -u root -p
create database literary_forum default character set utf8mb4 collate utf8mb4_general_ci;
注意:utf8mb4必选!因为文学创作社区的评论和内容里会出现生僻字和特殊符号,utf8mb4才能完整支持emoji和四字节Unicode字符。用utf8后期数据迁移会非常痛苦。
第二步,导入项目SQL脚本。在项目的sql目录下会有一个init.sql,包含建表和初始数据。直接用:
bash复制mysql -u root -p literary_forum < init.sql
第三步,修改application.yml:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/literary_forum?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
database: 0
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
map-underscore-to-camel-case: true这个配置非常关键。数据库字段create_time会自动映射到实体属性createTime,省去大量的@Alias注解和结果映射配置。
第四步,项目根目录下执行:
bash复制mvn clean install -DskipTests
mvn spring-boot:run
后端默认端口8080,启动成功后访问http://localhost:8080/api/test应该返回一段JSON确认接口可用。
4.3 前端启动步骤
第一步,安装依赖:
bash复制cd literary-forum-frontend
npm install
这个环节最容易出错的是依赖版本冲突。如果报错ERR! code ERESOLVE,可以先试:
bash复制npm install --legacy-peer-deps
这个场景基本都发生在Element Plus和Vue版本号不匹配的时候,通过--legacy-peer-deps跳过严格依赖校验收效概率很高。
第二步,启动开发服务器:
bash复制npm run dev
默认端口3000,控制台会输出访问地址。此时改前端代码,浏览器页面会实时刷新。
第三步,构建生产包:
bash复制npm run build
构建产物在dist/目录。我强烈建议本地至少构建一次,很多依赖问题在开发模式下不会暴露,但生产构建会做代码压缩和语法降级,如果某个插件版本不兼容,一构建就报错。提前发现比上线时再发现要好得多。
4.4 生产环境的Nginx配置
生产环境我采用典型方案:Nginx托管前端静态资源 + 反向代理后端API。这是前后端分离项目最常见的部署方式,不需要再单独启动前端服务。
nginx复制server {
listen 80;
server_name your-domain.com;
# 前端静态资源
location / {
root /var/www/literary-forum;
index index.html;
try_files $uri $uri/ /index.html;
}
# 后端API反向代理
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;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
其中try_files是前端路由的命门。Vue Router的history模式在刷新页面时会请求真实路径,比如访问/works/123,如果没有try_files兜底,Nginx会返回404。加上这行配置后,所有找不到的路径都会回退到index.html,交给前端路由处理,页面才能正常显示。
提示:如果生产环境下前端请求报
401或CORS错误,优先检查后端接口地址前缀是否统一加了/api,以及Nginx的proxy_pass后面的路径是否有斜杠。proxy_pass http://127.0.0.1:8080;和proxy_pass http://127.0.0.1:8080/;是两种完全不同的代理行为,前者保留原始URL路径,后者会替换路径前缀。这是我见过最常见的配置错误。
5. 常见问题排查与避坑指南
以下问题全部来自实际踩坑记录,按频率从高到低排列:
| 问题现象 | 原因分析 | 解决方案 |
|---|---|---|
| 前端请求接口报404 | 后端缺少/api前缀或Nginx代理路径不匹配 |
统一在application.yml配置context-path: /api |
| 上传图片失败 | SpringBoot默认文件大小限制1MB | 配置spring.servlet.multipart.max-file-size=10MB和max-request-size=20MB |
| 中文乱码 | 数据库连接串未指定characterEncoding=utf8mb4 |
按2.4节配置修改连接串 |
| 刷新页面404 | Vite构建后未配置try_files |
在Nginx配置中添加try_files $uri $uri/ /index.html; |
| Node版本不兼容 | Vite3不支持太老的Node | 升级至Node 16+或使用nvm切换版本 |
| MyBatis分页失效 | 分页插件拦截不到SQL | 检查PageHelper版本与MyBatis版本兼容性,官方推荐SpringBoot starter |
| 日期时间差8小时 | 时区未设置为Asia/Shanghai | JDBC连接串添加serverTimezone=Asia/Shanghai |
| 重复提交创建了多个作品 | 前端快速点击 + 后端未做幂等校验 | 前端使用loading状态禁用按钮,后端根据用户最近创建记录做防抖 |
5.1 MyBatis动态SQL里最容易翻车的小细节
在写<if>判断时,!= null的写法经常被写成not null,或者传入的是Integer型参数时判断条件写成了!='',这会导致条件永远为true。这里有个规范性建议:字符串用空串判断,数字只用null判断,混合在一起极易出错。
另外一个高频问题:多表查询时字段名重叠。比如t_user有id和create_time,t_post也有id和create_time,直接SELECT *会导致结果集里同名字段被覆盖。解决方法是显式指定列别名:
sql复制SELECT
p.id AS post_id,
p.create_time AS post_create_time,
u.id AS user_id,
u.nickname AS user_nickname
FROM t_post p
LEFT JOIN t_user u ON p.user_id = u.id
5.2 用户头像和封面图上传:存储路径与访问映射
文件上传如果直接写到本地磁盘,重启应用或者部署到不同环境时路径容易出问题。我采用的做法是存储目录与代码目录完全分离:
yaml复制file:
upload-dir: /data/literary-forum/uploads
access-prefix: /files
上传接口保存文件到指定目录,然后通过一个简单的静态资源映射对外暴露:
java复制@Configuration
public class FileResourceConfig implements WebMvcConfigurer {
@Value("${file.upload-dir}")
private String uploadDir;
@Value("${file.access-prefix}")
private String accessPrefix;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(accessPrefix + "/**")
.addResourceLocations("file:" + uploadDir + "/");
}
}
这样页面里就可以直接通过/files/avatar/xxx.jpg访问图片,而不用经过Controller转发流出二进制数据,效率更高。
5.3 千万别忘了登录拦截器的白名单配置
很多初学者在配置LoginInterceptor后,发现登录接口本身也被拦截了,前端一登录直接报401。这就是忘了放行白名单。正确的处理方式:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/api/user/login",
"/api/user/register",
"/api/work/list",
"/api/work/detail/**",
"/api/post/list",
"/api/files/**"
);
}
}
白名单策略是“允许游客访问的接口放开,其余全部拦截”。上面示例中游客能看作品列表、作品详情、帖子列表,但发评论、点赞、关注等都必须在登录状态下执行。
6. 运维监控与性能优化:让论坛跑得更稳的关键细节
6.1 MySQL慢查询排查
论坛系统上线后,最需要的性能优化手段就是MySQL慢查询日志。在编码阶段写出低效SQL不可避免,关键是上线后能否快速定位。
打开慢查询日志:
sql复制SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
然后在项目里用EXPLAIN分析执行计划。这里要特别注意一个典型的索引失效场景:在t_comment表的entity_type, entity_id上建了联合索引,但如果查询时只传entity_id而没传entity_type(业务上可以只按ID查评论),联合索引就无法完全生效。所以实际查询中要保证entity_type和entity_id始终成对出现。如果确实需要单独按entity_id查,那就再建一个单列索引。前期建表时多花半小时考虑索引,后期数据库响应性能会好非常多。
6.2 接口响应数据的“瘦身”
这个优化点特别容易被忽略。我用Jackson的JsonInclude注解把所有返回对象里的null值字段全部过滤掉,前端不再收到大量无意义的"field": null内容:
java复制@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result<T> {
private Integer code;
private String message;
private T data;
}
同时,列表接口绝不直接返回实体类对象,而是返回VO(View Object)。因为数据库里的content字段是全篇文章的TEXT,列表页根本不需要完整正文,只需要章节数量和标题。直接返回实体会导致前端传参量大几倍,浪费带宽也拖慢渲染。建立VO后,列表接口只查询必需字段,详情接口再查询完整数据。
6.3 Redis监控:别让点赞缓存成为内存黑洞
使用Redis存点赞数据一定要设置过期策略。如果点赞数据一直堆积,Redis内存会被慢慢吃光。我给每个点赞集合设置了过期时间(比如30天),并通过定时任务定期刷新到MySQL后删除。
bash复制EXPIRE like_entity_1_{chapterId} 2592000 # 30天
热门作品如果点赞数持续增长,Redis会自动续期,过期时间从最后一次操作重新计算。这是一个很实用的兜底策略:即便定时任务挂了一次,Redis也不会因为无限增长而内存耗尽。
6.4 定时任务的替代方案:延迟双删缓存
在论坛系统中,作品详情页非常热门,我用了简单的Redis缓存来降低MySQL压力。但更新作品内容后,缓存怎么同步?我的方案是“先更库、再删缓存、再延迟删一次缓存”,也就是业界常说的延迟双删策略:
java复制1. 更新MySQL数据库
2. 删除Redis缓存
3. 等待500毫秒
4. 再次删除Redis缓存(防止并发期间读线程写入的旧值)
这个方案不能100%保证极端并发下绝对一致,但在大部分实际业务场景下已经够用。如果对一致性要求极高,应该引入消息队列实现缓存重建,但复杂度会成倍上升。做一个文学论坛,用延迟双删绰绰有余。
7. 扩展思路:这套架构还能怎么进化
在完成了整套SpringBoot + Vue3 + MyBatis的文学创作社交论坛后,我发现这套系统的设计思路其实不止适用于“文学社区”,它可以被快速改造成多种内容社区类产品:
- 短篇小说平台:把“作品/章节”结构调整为“单篇作品”,去掉章节管理,保留创作与评论,即可快速切成短篇内容社区。
- 知识问答社区:把“作品表”替换成“问题表”,“章节表”替换成“回答表”,保留用户体系和点赞关注逻辑,就完成了从创作平台到问答社区的变身。
- 开源技术博客平台:在作品模块增加Markdown支持,在帖子里增加代码高亮,配上标签系统,能改造成一个面向开发者的技术社区。
- 多语言国际化:如果用户群扩到海外,模板引擎继续保留,前端Vue3有成熟的i18n方案,只需在数据库中增加多语言字段映射,改造起来并不困难,但需要提前判断需求优先级。
这个系统真正的价值在于业务模型的合理性——它把“内容生产者”、“内容消费者”和“平台运营者”三方的诉求都落到了具体的表结构和业务流程中。这些设计思路是所有内容社区类产品的通用底盘,换皮不换骨。
我在实际开发中最大的体会是:架构选型不一定要多先进,但每一层的边界一定要清晰。SpringBoot负责接口稳定,Vue3负责交互体验,MyBatis负责SQL可控,MySQL负责数据可靠。四者各司其职,配合得当的情况下,这个系统从开发到上线可以非常顺畅,维护成本也低。
最后分享一个小技巧:在Controller层统一使用Result<T>包装响应,并且规范code语义(200成功、400业务错误、401未登录、500系统异常),前端Axios响应拦截器里统一处理错误提示。这个习惯能让你在联调阶段直接省掉一半的“为什么没反应”沟通成本,前端拿到错误码就知道自己是没登录还是参数传错。一个整洁的接口规范,比任何黑科技都更能提升团队协作和项目交付效率。
