☰
SpringBoot+Vue前后端分离文学社区论坛项目实战:从需求到完整管理系统搭建
2026/9/26 14:23:03 网站建设 项目流程

做文学创作类社区的人,多少都会遇到一个相同的困惑:功能看来看去就是发文章、写评论、点赞收藏加关注,好像谁都能做。可真要把一条“作者创作、平台审核、读者互动、运营管理”的完整链路跑通,琐碎程度远超预期。这篇博客就围绕一个完整的SpringBoot+Vue项目来复盘这件事。项目代号xabo,技术栈是Java+MySQL+MyBatis,覆盖用户端、作者端和管理端三块,前后端分离,带完整源码。无论你是正在做类似选题的开发者,还是想快速搭一套内容社区骨架,这篇文章都能给你一条能直接落地的路线。

1. 项目到底在做什么:把需求拆成一条完整的业务闭环

技术类文章一上来就贴代码是最劝退的写法。不管用什么框架,第一步都应该先把业务想清楚。文学创作社交论坛和普通博客系统的差别很大,普通博客的核心是“我写你看”,而文学论坛的核心是“作者持续创作,读者持续互动,平台持续治理”,所以它本质上是一条内容闭环。

1.1 文学创作社区的核心业务模型

我先按角色把整个系统拆开。用户端最基础的是注册、登录、浏览作品、搜索分类、阅读章节、评论、点赞、收藏、关注作者。作者端则要支持创建作品、管理章节、查看自己作品的阅读数据和评论通知。管理端承担的是审核、治理、统计和配置,缺了管理端,内容和用户一多就会失控。

这个模型决定了数据库建模和接口设计的大方向:用户、作品、章节、评论、点赞记录、收藏关系、关注关系、公告通知、敏感词、操作日志,这些是一套文学社区最少也要覆盖的数据实体。

文学平台还有一个特殊点:连载。一部作品下挂几十上百个章节,章节需要拆分独立管理。如果不把作品和章节拆成两张表,编辑某个章节时就得把整部作品数据捞出来再塞回去,性能差,逻辑也容易乱。所以数据建模上,作品表只存元信息,章节表单独存正文,这是第一个关键决定。

1.2 xabo管理系统到底管什么

xabo是项目的代号,负责的是整个平台的后台管理。很多练手项目最常犯的毛病,是只做了用户端,觉得能注册能发帖就完事了。真实情况是,一个内容平台上线后,管理者必须处理的日常事务非常多:新作品要不要通过审核、用户举报评论如何处理、异常用户要不要禁言、当日新增内容多少、活跃用户多少、哪些分类最热门。这些需求不做,项目就只能停留在demo层面,谈不上“系统”。

所以xabo管理端至少有三个功能域:

  • 内容审核:作品发布后进入待审状态,管理员查看详情后通过或驳回,驳回要填原因。
  • 用户治理:用户列表、角色调整、禁言、封禁,以及作者认证审批。
  • 数据运营:每天新增作品数、新增用户数、评论数、点赞数,按时间维度统计,用报表呈现。

管理端独立还有一个隐含好处:权限边界清晰。普通用户和作者永远接触不到管理接口,避免越权操作。开发时把管理端路由独立成一套布局,也方便后期做更细粒度的权限控制。

1.3 功能清单与版本裁剪策略

一次把所有社区功能全做完,是不现实的。设计这个项目时应该有明确的主次。我按优先级把功能分成P0、P1、P2三档:

优先级功能模块说明
P0用户注册登录、作品发布、章节管理、作品列表与详情、评论、后台审核没有这些,社区闭环不成立
P1点赞、收藏、关注、个人主页、我的书架、公告、数据看板完善社交属性,提升粘性
P2打赏、积分系统、消息通知、专题活动、排行榜增强运营能力,可以后续迭代

P0是必须做完的,P1是项目中期补全的,P2属于加分项。很多人在做这类项目时喜欢一上来就设计一个巨大无比的表结构,结果开发周期拖长,代码写不完。我的建议是先跑通P0闭环,再逐步加P1,工程上这叫MVP思维,放在个人项目里非常实用。

2. 技术选型:为什么这套组合拳扛得住

SpringBoot+Vue+MySQL+MyBatis,这个组合在国内的Java Web项目里已经称得上“经典款”。它不是最炫的,但绝对是最稳的。我在这里把每一环的选型逻辑说清楚,方便你判断自己的场景适不适合照抄。

2.1 SpringBoot做后端:省心的关键是“约定优于配置”

SpringBoot的价值不在于它有什么黑科技,而在于它把Spring生态里的配置成本压到了最低。要做SSM那套老流程,你要手动配置数据源、事务管理器、MyBatis的SqlSessionFactory、扫描器,任何一个环节配置错,启动就报错。SpringBoot用自动配置把这些全部接管了,写个application.yml就能跑起来。

更实际的好处是生态成熟。做登录认证有Spring Security和JWT方案,做参数校验有Validation注解,做接口文档有Knife4j或者SpringDoc,做定时任务有@Scheduled。这些组件都能无缝嵌进SpringBoot工程。对个人开发者和中小团队而言,这是一个性价比极高的起点。

我在xabo项目里选择Java 8加SpringBoot 2.x,很多人会问为什么不直接上Java 17和SpringBoot 3.x。原因很简单:生态兼容。很多公司内部或毕业设计环境还在大量使用MyBatis、PageHelper等传统组件,这些组件对SpringBoot 2.x的兼容性最好,查问题也最容易找到资料。如果你的环境完全可控,再考虑新版本。

2.2 Vue做前端:前后端分离不是炫技

前端用Vue,核心原因只有一个:交互复杂,模板渲染撑不住。文学创作论坛的页面不是简单的服务端渲染就能搞定的,创作中心要实时保存草稿,作品详情页要异步加载章节内容,管理端有大量表单和表格联动。这些场景用原生JS写,维护成本会迅速失控。

Vue提供了组件化、响应式数据和路由管理。项目里我把前端分成两个部分:前台门户和后台管理。前台门户面向用户,页面要清爽;后台管理面向管理员,大量的表格、表单、弹窗,适合用Element UI这类组件库快速搭建。Vue Router负责页面跳转,Vuex或Pinia负责保存用户信息和登录状态。

前后端分离还有一层好处是开发时可以并行。后端的接口只要约定好返回格式,前端就能用Mock数据先开发页面,不需要等后端写完。项目里所有接口统一返回{code, message, data}结构,前端在Axios的响应拦截器里统一处理code,避免每个页面都写一遍错误判断。

2.3 MySQL建模:表结构和索引怎么设计才不坑

MySQL是这套技术栈里最不该换掉的部分。关系型数据库的ACID事务是这个项目最需要的,作者发布章节、用户点赞、收藏操作,每一步都涉及数据一致性,不像日志类系统可以用NoSQL宽松处理。

建模时有几个经验值得记下来。第一,字符集必须用utf8mb4而不是utf8,因为utf8在MySQL里最多存3字节,用户昵称和评论里一旦出现emoji,utf8就直接写入报错。第二,所有表都建议带上自增主键id、create_time、update_time,这三个字段能让后续开发和排查省很多事。第三,不要过度使用数据库外键。逻辑外键就够了,物理外键在删除和分表时会变成枷锁。

索引设计上,查询频率最高的场景一定要提前分析。作品列表页最常见的操作是按分类查、按状态过滤、按时间排序,所以(work)表至少要建(work)的联合索引。评论表最常见的操作是按作品或章节查评论,所以(comment)表至少要建(work_id + create_time)的联合索引。点赞记录表要建唯一索引,这是防重复点赞的关键。

2.4 MyBatis:把SQL掌控权握在自己手里

MyBatis和JPA的选择,是很多团队争论过的话题。我的个人判断是:这个项目必须用MyBatis,因为管理端的统计报表、作品列表的多条件筛选,都依赖复杂动态SQL。JPA虽然写单表CRUD很快,但一旦查询条件多、需要动态拼SQL,调试成本会直线上升。MyBatis的XML里可以显式写出每条SQL,出了问题直接复制出来在Navicat里跑一遍就知道对错。

如果再用上MyBatis-Plus,单表CRUD也能自动生成,配合条件构造器QueryWrapper,日常开发效率不比JPA低。在xabo项目里我采用了MyBatis加PageHelper组合,分页插件用起来简单,startPage之后紧跟着查询就自动分页,管理端的作品列表、用户列表全依赖它。

3. 核心功能设计与关键实现细节

技术选型说完了,接下来进入最核心的部分:每个关键功能是怎么落地实现的。这一节我会重点讲数据模型和实现思路,避免只讲概念。真在代码里填过坑的人会发现,很多问题的根源其实都在设计阶段。

3.1 用户认证:JWT加拦截器的完整落地

前后端分离项目里,Session方案不太好用,因为浏览器和后端不在同一个域,跨域时Cookie管理很麻烦。我选了JWT方案,本质就是用户登录成功后,服务端生成一个带签名信息的Token字符串下发到前端,前端在后续每次请求的Header里带Authorization,后端解析Token就知道请求人是谁。

登录的流程是这样:用户提交用户名密码,后端拿到后先用BCrypt去校验密码哈希,匹配成功就提取用户ID和角色,用密钥生成Token,Token里包含过期时间。接下来写一个拦截器,拦截除登录注册之外的接口,从Header取Token,解析失败就直接返回401,解析成功就把用户ID塞进ThreadLocal,业务代码里随时可以取当前登录人。

这套方案里最容易被忽略的两个点是密钥管理和登出。密钥不能写在代码里,要放到配置文件里,最好加入环境变量。JWT本身是无状态的,服务端没法主动让Token失效,所以登出功能要么依赖前端删掉Token,要么引入简单的黑名单机制。个人项目用前端删除Token方案就够,别把复杂度抬得太高。

3.2 作品发布与章节管理:内容主数据建模

文学平台的数据核心是作品和章节。作品表记录作者ID、书名、分类、简介、封面图、状态、总字数、总点赞数,章节表记录所属作品ID、章节序号、标题、正文、字数、发布时间。

为什么作品和章节必须拆开?一部连载作品可能有200章,如果不拆表,作者想改第35章的标题,就要把整部作品的内容都更新一遍,数据库会做大量无效的写操作。拆开后,作品表的更新只涉及元信息,章节表的更新只涉及单条记录,还可以针对章节做分页查询和单独的阅读计数。

章节正文我建议用MEDIUMTEXT类型,小说一章几千字很正常,如果一章长达数万字,TEXT字段就会兜不住。作者发布章节时,前端用Markdown编辑器或者富文本编辑器,后端要统一做一遍XSS过滤,不能直接把HTML内容存进数据库。这个细节在第五章会展开讲。

作品的审核状态是这个项目的机构性设计。作者创建作品后状态是草稿,提交审核后变成待审核,管理员通过后才是连载中,最终完结,违规的则下架。这个状态流转贯穿整个项目,作品列表、作者创作中心、管理端审核页,全部以状态字段作为过滤条件。

3.3 评论、点赞、收藏:社交互动的数据模型

互动功能看起来是小事,做不好会让人怀疑整个项目的水准。先说评论表,至少要有id、用户ID、作品ID、章节ID、内容、父评论ID、创建时间。父评论ID支持楼中楼回复,但要注意层级最好不要无限递归,最多支持两级就好,回复某人的评论时,前端直接展示“回复@某人”即可,数据查询反而简单。

再重点说点赞防重的实现。很多新手写点赞逻辑是这样的:

// 错误示范 if (likeMapper.exists(userId, workId)) { likeMapper.delete(userId, workId); workMapper.decreaseLikeCount(workId); } else { likeMapper.insert(userId, workId); workMapper.increaseLikeCount(workId); }

这段代码在并发请求下有明显的竞态问题,用户快速点两下,两条记录同时进数据库,会出现重复点赞。正确的做法是依赖数据库唯一索引兜底,不管并发来多少请求,唯一索引都会把重复数据挡掉,再用INSERT IGNORE这种写法,后台更新计数就够了。

计数更新还有一个坑:不要用count(*)实时统计点赞数。每次列表查询都去count一遍点赞表,数据库IO扛不住。常规做法是在作品表里冗余一个like_count字段,每次点赞成功就+1,取消点赞就-1。虽然极端情况下会有误差,但配合定时任务重新校准,完全够用。

3.4 管理端看板:统计SQL与运营功能实现

xabo管理端里最有含金量的部分是数据看板。它的SQL并不复杂,但确实能体现MyBatis写自定义SQL的优势。比如要统计最近7天每日新增用户数,就按日期分组:

SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM sys_user WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY day;

同类SQL还可以扩展出每日新增作品数、每日新增评论数、作品分类分布、用户角色分布。把这些汇总结果返回给前端,前端用ECharts画折线图和饼图,管理端看起来就专业得多。

审核和治理功能同样要落到表设计上。作品表里的status字段配合一个简单的审核备注字段,管理员驳回时能看到理由。用户表里加一个status字段,表示正常、禁言、封禁三种状态,登录拦截器每次都要校验用户状态,禁言的用户不能发评论。这些字段在设计阶段就留下,比上线后靠打补丁强太多。

4. 完整实操流程:从建库到前后端联调跑通

前面讲的偏架构和设计,这一节是真正的落地流程。如果你照着做,按这套步骤能把整个项目跑起来。我会按数据库、后端、前端、联调部署四个环节来还原。

4.1 数据库初始化:核心建表SQL与初始数据

建数据库时建议单独建一个初始化脚本,脚本里包含建库、建表、插入初始管理员账号三部分。下面是几个核心表的精简版结构,实际项目里字段会更多,但骨架就是这几张表:

CREATE DATABASE IF NOT EXISTS literature_forum DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE literature_forum; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50) DEFAULT '', avatar VARCHAR(255) DEFAULT '', bio VARCHAR(255) DEFAULT '', role TINYINT DEFAULT 1 COMMENT '1-用户 2-作者 3-管理员', status TINYINT DEFAULT 1 COMMENT '1-正常 2-禁言 3-封禁', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT '用户表'; CREATE TABLE work ( id BIGINT PRIMARY KEY AUTO_INCREMENT, author_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, category VARCHAR(30) DEFAULT '', summary VARCHAR(500) DEFAULT '', cover VARCHAR(255) DEFAULT '', status TINYINT DEFAULT 0 COMMENT '0-草稿 1-待审核 2-连载中 3-完结 4-已下架', word_count INT DEFAULT 0, like_count INT DEFAULT 0, favorite_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_author (author_id), INDEX idx_status_time (status, create_time) ) COMMENT '作品表'; CREATE TABLE chapter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, work_id BIGINT NOT NULL, chapter_no INT NOT NULL, title VARCHAR(100) NOT NULL, content MEDIUMTEXT, word_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_work (work_id) ) COMMENT '章节表'; CREATE TABLE comment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, work_id BIGINT NOT NULL, chapter_id BIGINT DEFAULT NULL, user_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL, parent_id BIGINT DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT '1-正常 2-删除', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_work_time (work_id, create_time) ) COMMENT '评论表'; CREATE TABLE like_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, target_type TINYINT NOT NULL COMMENT '1-作品 2-评论', target_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_target (user_id, target_type, target_id) ) COMMENT '点赞记录表';

初始化数据最少要包含一个管理员账号,密码用BCrypt加密后的字符串插入。这样启动项目后直接用管理员账号登录管理端,不用再想办法注册管理员。

4.2 后端工程搭建:配置与代码结构

后端工程用Maven管理,依赖主要包含spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、pagehelper-spring-boot-starter、jjwt、hutool这些。Hutool是我比较喜欢集成的工具库,里面有很多省时间的静态方法。

application.yml里几个关键配置项如下:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/literature_forum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.xabo.entity configuration: map-underscore-to-camel-case: true pagehelper: helper-dialect: mysql reasonable: true

map-underscore-to-camel-case必须开启,这会让数据库的create_time自动映射到Java实体里的createTime,少写很多resultMap。PageHelper的reasonable设为true后,页码超大或超小时会自动修正,避免前端传page=9999直接把查询拖垮。

后端包结构推荐这样分:controller、service、mapper、entity、common、config。common放统一返回结果、全局异常处理、JWT工具类,config放WebMvc配置和拦截器注册。经验之谈,包结构里不要放util这种大杂烩目录,每个工具类都放进对应职责的包层,代码后期好找。

4.3 核心接口落地:从Service到Mapper的完整写法

我以“作品列表分页查询”为例,这条链路覆盖了Controller、Service、Mapper三层:

// Controller @GetMapping("/work/list") public Result<PageInfo<WorkVO>> list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Integer status, @RequestParam(required = false) String category) { return Result.success(workService.pageQuery(pageNum, pageSize, status, category)); }
// Service public PageInfo<WorkVO> pageQuery(Integer pageNum, Integer pageSize, Integer status, String category) { PageHelper.startPage(pageNum, pageSize); List<WorkVO> list = workMapper.selectByCondition(status, category); return new PageInfo<>(list); }
<select id="selectByCondition" resultType="com.xabo.entity.WorkVO"> SELECT id, title, category, summary, cover, status, word_count, like_count, create_time FROM work <where> <if test="status != null"> AND status = #{status} </if> <if test="category != null and category != ''"> AND category = #{category} </if> </where> ORDER BY create_time DESC </select>

代码的核心关键是PageHelper.startPage一定要生命周期短,它只对下一条执行的SQL生效。所以我习惯在Service方法的第一行调用它,且这个Service方法里只有一条mapper查询。如果有人在一段代码里穿插多条查询,分页数据就会跑到别的查询上去,这是使用分页插件最容易踩的坑之一。

Service层的其他地方,事务注解@Transactional的作用会被低估。发布章节这个操作,既要插入章节记录,又要更新作品的总字数和总章节数,任何一个失败都会导致数据不一致,所以发布接口必须加事务。不要觉得事务是数据库专家才关心的事,这种跨表写入的场景,事务就是基础要求。

4.4 前端工程搭建:Vue环境与请求封装

前端工程用Vue CLI创建后,统一安装vue-router、axios、element-ui、vuex这几个依赖。请求封装是一个必须做好的环节,否则每个页面都写重复的axios调用,代码肉眼可见地烂。

我在项目里把axios封装成request.js,核心逻辑是:请求拦截器从localStorage取出Token,拼到Header的Authorization字段;响应拦截器统一处理HTTP状态码,后端返回code=401时跳转登录页,code=200时直接return data。这样每个页面的API调用都极其简洁:

// api/work.js import request from '@/utils/request'; export function getWorkList(params) { return request({ url: '/work/list', method: 'get', params }); }

路由部分我分成两层:前台路由和管理端路由。管理端路由统一挂在Layout组件下面,并加前置路由守卫:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path.startsWith('/admin') && !token) { next('/login'); } else { next(); } });

这个守卫只做了最基础的登录判断,更细的权限校验还是得靠后端接口鉴权。前端路由守卫是体验层面的东西,真正的安全边界永远在后端。

4.5 联调、打包与部署:本地与生产环境的差别

本地联调最常见的痛点是跨域。Vue开发服务器默认在8081端口,后端接口在8080端口,浏览器直接请求会跨域。开发环境我推荐用Vue CLI的devServer代理,而不是在后端开CORS:

// vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }

这样前端请求统一写/api/work/list,代理到后端就是/work/list,生产环境把/api路径交给Nginx反向代理,前端代码不用改动,联调体验非常顺畅。

生产部署时,前端执行npm run build生成dist目录,把dist里的静态文件放到Nginx的HTML目录,再配置一个反向代理,把/api的请求转发到Java后端。SpringBoot后端就一个jar包,java -jar运行即可。如果服务器内存紧,可以在启动命令里限制JVM堆内存:java -Xms256m -Xmx512m -jar xabo.jar,避免一台小机器同时跑MySQL和Java后端起不来。

5. 常见问题与排查技巧实录

这节是我最想写的部分。很多项目能跑起来,但只有真的踩过坑,才知道问题出在哪。下面这些坑全部是我在实际开发里遇到过的,有些查了大半天才定位,希望你看完能直接绕开。

5.1 MyBatis的坑:从占位符到分页插件

先说#{}和${}的区别。很多人不知道这个区别,会在动态SQL里把参数直接用${}拼接。比如排序字段,如果写ORDER BY ${orderBy},参数被拼成SQL的一部分,存在SQL注入风险。#{}是预编译占位符,安全,但它只适合值传递,不能用于表名、字段名、排序方式这种结构性位置。像动态排序列这种场景,我更推荐用白名单映射,把前端传过来的字符串映射到代码里预定义好的安全列名。

再说XML文件里的特殊字符。查询时间范围时经常要用到小于号,比如create_time < #{endTime},这种SQL直接写在XML里会报错。解决办法是转义,或者用 包起来:

<![CDATA[ AND create_time < #{endTime} ]]>

这个报错信息很误导人,初看像SQL语法错误,实际是XML解析问题。报错后先检查XML里有没有裸写特殊字符,是我排错的第一反应。

最后是PageHelper的经典误区。在同一段逻辑里,先调startPage再调用了一连串的mapper查询,分页会作用在错误的SQL上。我踩过一次就是循环里分页,同事写的循环里查了两次list,结果分页数据一半对一半错,非常难排查。用分页插件就记住一条铁律:startPage之后只允许跟一条查询语句。

5.2 前后端联调:跨域、Token与状态持久化

前后端联调经常会遇到“接口明明通了,但浏览器里报跨域”。如果你用了Nginx代理或devServer代理,基本上不会出现这个问题。如果确实要后端开CORS,记得把allowedOrigin配成具体的域名,不要用*,尤其是允许携带Cookie时,通配符会直接被浏览器拦掉。

另一个高频问题是前端页面一刷新,用户登录状态就丢了。原因在于Vuex是内存状态,刷新后就清空了。解决方法是把Token存到localStorage,页面加载时在路由守卫里检查localStorage,同时调用一个获取当前用户信息的接口去刷新Vuex里的用户数据。顺序要对,先拿Token,再拉用户信息,最后放行路由。

Token还有一个容易被忽略的坑:机器时间不一致。后端签发Token时用的过期时间是服务器时间,如果前端的电脑时间比服务器快了几分钟,请求走一遍代理,就有可能出现“刚登录就被提示过期”的诡异现象。排查时先看两边的系统时间是否一致,这个方向很多人想不到。

5.3 内容安全与性能细节

文学创作社区的评论、标题、正文都会面临内容安全问题。基础的做法是后端做一层XSS过滤,即使前端编辑器已经处理过,后端也不能信任任何输入。富文本内容尤其麻烦,最简单稳妥的方案是配置一个白名单工具,只允许p、br、strong、em、img、a等常见标签,其余的统统剥掉。直接用正则去黑名单匹配脚本标签,很容易被各种编码绕过。

SQL注入防护前面说了用#{},这里再说一个容易漏的点:like查询。用户搜索关键词时,如果直接拼接%加keyword再加%,就引入了通配符注入风险。搜索接口的参数也要走预编译,用CONCAT('%', #{keyword}, '%')的方式,既满足模糊查询,又不会把用户输入当成SQL结构。

性能上最值得优化的就是作品列表页。分类筛选加时间排序这套查询,如果没有合适索引,数据量上到几万条就会出现明显卡顿。我在work表建的idx_status_time联合索引,就是为这类查询准备的。真实项目里大列表页如果还要做深分页,pageNum超过一万页时会很吃力,优化手段有几种,可以用lastId游标方式替代offset翻页,也可以限制最大查询页数。对个人项目来说,限制页数是性价比最高的办法。

5.4 上线前建议检查的几件事

项目的开发环境和生产环境配置必须分离。如果直接把application.yml里连本地数据库的账号密码打成jar包扔到服务器上,等于公开漏洞。我用SpringBoot的profile机制来切换,本地用application-dev.yml,生产用application-prod.yml,启动时加--spring.profiles.active=prod,数据库密码还可以用环境变量引用。

上线前最好把日志配置补齐。项目里默认的日志只有控制台输出,一旦jar包在后台跑,日志除了写进nohup.out之外,最好再配置一个按天滚动生成的文件大小限制。不用额外引入日志框架,logback-spring.xml配一下就能实现。

还有一个容易被忽略的细节:前端build之后,静态资源首次加载会比较大。处理办法很简单,在Vue Router里配置路由懒加载,让每个页面的JS按需加载,首屏性能能提升不少。加上Nginx开启gzip压缩,几个大文件压缩完体积能减少一半,访问体验完全是两回事。

做这类全栈项目,我个人最大的体会是,代码写得好不好往往不在某个技巧上,而在你是否能从第一行就清楚整个系统的数据流向。xabo这个项目用最经典的技术栈,把用户端、作者端、管理端完整串起来,很适合拿来当作理解Web全栈开发的完整样本。如果你正在做类似的系统,我建议你从管理端开始做,先把审核和统计打通,再去打磨用户端体验,顺序反了很容易陷进无穷无尽的前端样式调整里。

最后再分享一个小技巧:所有的请求和响应日志一定要尽早加上。不是那种print语句,而是用一个拦截器统一打印请求路径、参数、耗时和响应状态。千万别小看这个东西,项目联调阶段,它能帮你省下至少一半的排错时间。等你真的靠着这份日志在十分钟内定位了一个看似玄学的线上问题,就会回来感谢这个习惯。

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

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

立即咨询