1. 项目前瞻与技术选型
1.1 这个平台到底是做什么的
这个“基于Spring Boot + Vue的中华诗词文化交流平台”,源码、数据库脚本、部署文档全套在手,是我近期做得最完整的一个全栈项目。简单说,它不是一个只有页面效果的静态网站,而是一个真正能跑通注册、登录、浏览、搜索、收藏、评论、后台管理的前后端分离应用。用户进来可以按朝代、按诗人逛诗词库,也能在搜索框里直接输入“月”“李白”“边塞”这类关键词,系统会从标题、正文、作者名、朝代标签里帮你把人找出来。注册登录之后还能把自己喜欢的诗收进个人收藏夹,在诗词详情页下面发表品读心得。管理员端则负责维护诗词数据、审核评论、管理用户状态,整个运营闭环是完整的。
为什么拿诗词文化做切入点?因为它天生自带一套非常适合全栈练手的数据关系。一个诗人属于一个朝代,一首诗属于一个诗人,一首诗可以挂多个文化标签,一个用户可以收藏很多诗,也可以对同一首诗发表多条评论。这些关系玩明白了,后面去接触电商、内容管理、甚至是简单的社交系统,其实都是同一套设计思路。你不可能在练手项目里一开始就碰到那种几百张表的大系统,但诗词平台的表结构刚好卡在“有复杂度但没有变态复杂度”的位置上,拿来理解外键、中间表、索引、全文检索再合适不过。
这个项目适合谁?第一类是正在准备毕业设计的学生,这个主题有文化底蕴,功能完整度够,答辩时也好讲;第二类是刚学完Spring Boot和Vue基础,想找一个能打通前后端的完整案例来跟练的开发者;第三类是想快速搭一个内容管理平台做二次开发的人,直接把源码拿来改改就能变成博客、名人格言、成语故事之类的站。
1.2 版本选择:为什么不盲目追新
热词里有个“springboot版本太高”,这个我太有感触了。一开始我图新鲜,直接上了Spring Boot 3.2,结果一大堆老依赖全都要改坐标,javax变成了jakarta,MyBatis-Plus的兼容包也要换,连很多教程里的写法都直接失效。折腾两天之后我老老实实换回Spring Boot 2.7.18,配JDK8和Maven 3.8,所有组件一把过,编译、启动、调试都极其顺手。这件事让我学到一个原则:全栈项目能不能跑起来,很大程度上不取决于你用了多新的版本,而在于你选的这一整套技术栈是不是互相兼容的。
前端我选的是Vue 3 + Vite + Element Plus + Pinia。Vue 3是当前绝对主流,Vite的冷启动速度比webpack快很多,Element Plus的组件库覆盖后台管理场景非常全。Pinia用来管理登录态和用户信息,比Vuex的样板代码少一大截。Vue Router做路由跳转,配合路由守卫控制页面访问权限。这套组合的社区活跃度很高,源码多,文档全,遇到问题大概率能在半小时内找到答案。
后端持久层我用了MyBatis-Plus,而不是原生MyBatis或JPA。原因很实际:这个平台大量操作是标准的增删改查和条件分页查询,MyBatis-Plus的BaseMapper直接省掉大部分SQL,QueryWrapper也能灵活拼条件,代码量减少很多。JPA的学习成本其实更高,遇到复杂查询反而不如MyBatis-Plus直观。如果你希望项目更贴近国内多数公司的技术栈,MyBatis-Plus也是比较稳妥的选择。
提示:不要看到“软件版本越高越好”就盲目升级。实际开发里,稳定性和可复现性比新特性重要得多。Spring Boot 2.7.x + JDK8 + MyBatis-Plus 3.5.x,这个组合在2025年的今天依然是大量生产项目的选择,教程和踩坑资料也最齐全。
1.3 整体模块划分与工程目录
我把项目拆成四块:用户端前端、管理端前端、后端接口服务、数据库脚本。虽然用户端和管理端在物理上都是Vue项目,但我把页面目录分开了,实际执行时是同一个前端工程的两个路由分支,这样既省一个项目的启动成本,又保留清晰的角色边界。
前端目录结构大概是这样:
- src/views:用户端页面和admin后台页面分开存放
- src/api:所有请求接口按模块拆分,比如user.js、poem.js、admin.js
- src/components:通用组件,比如诗词卡片、分页条、空状态提示
- src/store:Pinia的模块化状态管理
后端目录按经典分层来:
- controller:接收参数,做基础校验,调用service
- service:业务逻辑,事务管理
- mapper:数据访问层,和数据库表对应
- entity:实体类,和MySQL表字段映射
- config:全局配置,比如拦截器、跨域、Knife4j
这套目录结构本身没什么新意,但胜在“一眼能看懂”。很多初学者喜欢把所有代码塞进controller或者一个service里面,短期看起来方便,一旦功能多起来,改一个需求就要翻半天文件。分层不是为了装样子,而是为了让你在加功能的时候知道该去哪里改,接口挂了的时候知道去哪里排查。
2. 数据库设计:从实体关系到建表细节
2.1 核心实体关系拆解
数据库是这个平台的底座,我在建表之前先花了一晚上把关系理清,用文字表述出来就是:用户和角色是多对一关系,一个角色下可以有多个用户;诗人和朝代是多对一关系,一个朝代下有很多诗人;诗词和诗人是多对一关系,一首诗只能属于一个作者;诗词和标签是多对多关系,所以需要诗标签中间表;用户和诗词因为收藏行为产生多对多关系,所以需要收藏中间表;用户和诗词之间还可以通过评论表建立一对多关系,一个用户可以对多首诗发表评论,一首诗下面也可以有多条评论。
我总共设计了8张表:用户表user、角色表role、诗人表poet、朝代表dynasty、诗词表poem、标签表tag、诗标签关联表poem_tag、收藏表favorite、评论表comment。实际还有一张管理员操作日志表,但那个不涉及核心业务,这里就不展开。
这套关系设计的好处是:每个表都有清晰的存在理由,没有为了凑数而设计的冗余字段。比如收藏表favorite我故意不叫collection,因为它只记录用户和诗词之间的收藏行为,字段就是id、user_id、poem_id、create_time,干净利落。
2.2 关键表字段说明
先看用户表。用户名唯一,密码存BCrypt加密后的hash,头像字段允许为空,role_id默认值为2,也就是普通用户。这里说个细节:注册的时候后端只往user表插一条记录,不需要额外处理角色,因为默认值已经保证新用户拿不到管理员权限。
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `nickname` varchar(50) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `role_id` bigint NOT NULL DEFAULT 2, `status` tinyint NOT NULL DEFAULT 1, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个值得注意的点。第一,密码字段长度至少100,因为BCrypt加密后的字符串长度是60,留出余量。第二,表名使用user,在MySQL里不算保留字,但在某些数据库方言里可能会有歧义,所以实际建库时我加了反引号,或者干脆改名app_user更省心。如果你沿用user,每次写SQL都得小心。
诗人表和朝代表比较常规。朝代表只有id、name、description、sort_order,诗人表多加了一个dynasty_id外键、生卒年字段和一句生平简介。这里的外键物理建不建我是刻意做了取舍:业务代码里维护关联关系,不建数据库级外键约束,因为数据量大了之后外键会影响插入删除的效率。但逻辑外键一定加上索引,保证查询不走全表扫描。
诗词表是这个平台最重要的表。content字段用text类型存正文,favorites_count和views_count是统计冗余字段,用来避免每次查看列表都count一遍。为了让搜索能走索引,我建了ngram全文索引:
CREATE TABLE `poem` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL, `content` longtext NOT NULL, `poet_id` bigint NOT NULL, `dynasty_id` bigint NOT NULL, `favorites_count` int DEFAULT 0, `views_count` int DEFAULT 0, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_poet_id` (`poet_id`), KEY `idx_dynasty_id` (`dynasty_id`), FULLTEXT KEY `ft_title_content` (`title`, `content`) WITH PARSER ngram ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;收藏表favorite的联合唯一索引是关键,用户对同一首诗不能重复收藏:
CREATE TABLE `favorite` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `poem_id` bigint NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_poem` (`user_id`, `poem_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;评论表comment则挂上user_id和poem_id两个外键逻辑关联,内容用varchar或text都行,但一定要记create_time,前端显示评论时间要用。
2.3 索引与查询优化思路
建表之后不能急着写接口,先把查询路径想清楚。我按实际业务归纳出四类核心查询:按诗人查诗、按朝代筛选、关键词搜索、个人收藏列表。所以我在poem表的poet_id和dynasty_id分别建普通索引,在favorite表的user_id字段建索引,再在user表username上建唯一索引。
全文索引这里有个大坑。MySQL 8.0的ngram解析器默认分词长度是2,也就是说你搜索“春”“月”这种单字关键词,全文索引直接不干活,返回空结果。我在接口层做了这样的处理:如果关键词长度小于2,就退化成普通title like查询;关键词长度大于等于2,才使用MATCH AGAINST全文搜索。这样做能兼顾用户体验和查询性能。
还有一个细节是统计字段的一致性。收藏成功后,在同一个事务里不仅要insert到favorite表,还要update poem表的favorites_count加1。如果收藏失败但计数变了,数据就对不上。这个操作我放在@Transactional里,回滚时两个操作一起回滚。类似的还有取消收藏、浏览详情时对views_count加1。
3. 后端Spring Boot实现:分层、鉴权与检索接口
3.1 工程结构与统一响应封装
后端工程创建时我用了Spring Initializr,核心依赖是spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok和jjwt。数据库连接池用的HikariCP,这也是Spring Boot默认自带的,性能足够好,不用额外配Druid。
为了让所有接口返回格式一致,我先封装了一个Result类:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.message = "成功"; r.data = data; return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.code = code; r.message = message; return r; } }所有controller方法统一返回Result,前端拦截器只要判断code是不是200就行。这个设计虽然简单,但避免了每个接口各写各的返回样式,也让Swagger文档看起来非常整齐。如果你拿到手的项目没有这个统一返回,建议自己先加上,后面调试接口会省很多事。
3.2 登录鉴权:JWT加拦截器的轻量方案
这个平台是前后端分离,登录态我用JWT而不是Session。用户登录成功后,后端根据用户id和角色生成一个token,默认有效期7天。前端把token存到localStorage,每次请求时在axios请求头带上。后端拦截器解析token,再把用户信息放进ThreadLocal,方便service层取当前用户。
我没有用Spring Security,原因是这个项目需要的权限控制其实就两条规则:需要登录才能访问的接口、需要管理员才能访问的后台接口。Security的过滤器链和配置类对业务价值不大,反而增加学习负担。用HandlerInterceptor实现,代码直观且可控。
@Component public class AuthInterceptor implements HandlerInterceptor { private final JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !jwtUtil.validateToken(token)) { response.setStatus(401); return false; } UserContext.setUserId(jwtUtil.getUserId(token)); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }注册一个拦截器配置类,把需要保护的路由加进去:
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register", "/api/poem/**"); } }注意,有些需要公开访问的诗词浏览接口,比如首页列表、诗词详情,要放到排除路径里。后台管理接口管理员权限则是另一个角色拦截器,我给它单独建了一个AdminInterceptor。
注意事项:拦截器里的ThreadLocal一定要在afterCompletion里清理。线程池复用线程时,如果你不清理,后面的用户会拿到上一个用户的id。这个bug在并发量稍微上来一点就会间歇性出现,非常恶心。
3.3 诗词检索接口的两种实现方案
检索是平台的核心功能。我的接口规格是:用户传一个keyword,后端同时匹配标题、正文、诗人名、朝代名。最直白的实现是用MyBatis-Plus的QueryWrapper做like查询:
public IPage<PoemVO> search(String keyword, int page, int size) { LambdaQueryWrapper<Poem> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Poem::getTitle, keyword) .or() .like(StringUtils.hasText(keyword), Poem::getContent, keyword); return poemMapper.selectPage(new Page<>(page, size), wrapper); }这样写能跑,但有一个问题:or条件会把其他and条件都带偏。比如你后面要加一个朝代筛选,直接塞and条件进去,优先级会乱。更好的做法是在service层先把关键词处理成最终的查询逻辑,再用一个自定义SQL方法。
我自己用的是MySQL全文索引方案,查询速度比like快很多:
@Select("SELECT * FROM poem WHERE MATCH(title, content) AGAINST(#{keyword} IN NATURAL LANGUAGE MODE)") List<Poem> searchByFullText(@Param("keyword") String keyword);实测下来,ngram对中文的效果在短文本场景下非常理想。但正如前面提到的,单字搜索会失效,所以我在接口里加了长度判断,小于2就走title like,大于等于2才走全文索引。这个分支非常便宜,但能救回很多“为什么搜不到”的反馈。
3.4 数据安全:密码加密与XSS过滤
数据安全不能等出了问题再补。用户密码我用的BCrypt加密,也就是Spring Security里的BCryptPasswordEncoder,但是我只单独引了spring-security-crypto这个依赖,不用整套Security。这样既不引入复杂的过滤器链,又能拿到可靠的密码Hash工具。
管理员录入诗词时,前端富文本编辑器会提交HTML内容。这就有XSS风险。我的方案是后端统一用Jsoup白名单过滤:
String safeHtml = Jsoup.clean(html, Safelist.relaxed());Safelist.relaxed()会保留p、br、strong、em等基础标签,但把script、iframe、onclick这类全部清理掉。这个过滤一定要在服务端做,因为前端过滤只能防普通用户,防不了Postman之类的接口调用。评论内容也是一样的处理,评论虽然不支持富文本,但用户输入的表情文本里也可能带特殊字符,入库前统一转义更稳妥。
4. 前端Vue 3工程化与联调细节
4.1 初始化与Node环境配置
前端工程初始化用Vite:
npm create vite@latest poem-web -- --template vue如果你是第一次跑Vue 3项目,最可能卡住的不是项目代码,而是本地Node环境。Vite 5以上要求Node 18以上,如果你机器上是旧版Node,第一步就起不来。装依赖之前先检查:
node -v npm -vnpm源如果下载慢,可以临时切换镜像源,这也是网上问得最多的问题。组件库方面,Element Plus我用了按需自动导入,配合unplugin-auto-import和unplugin-vue-components,好处是打包体积小,坏处是某些样式可能不会被自动引入,需要手动import样式文件。如果你不想折腾,直接全量引入Element Plus也能跑。
4.2 路由与权限控制
前端页面分两个区:用户端和管理后台。我在路由配置里给每个路由加了meta信息:
const routes = [ { path: '/', component: Home, meta: { title: '首页' } }, { path: '/poem/:id', component: PoemDetail, meta: { title: '诗词详情' } }, { path: '/collect', component: CollectList, meta: { requiresAuth: true } }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, requiresAdmin: true } } ];然后在全局前置守卫里判断:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); const role = localStorage.getItem('role'); if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }); } else if (to.meta.requiresAdmin && Number(role) !== 1) { next({ path: '/403' }); } else { next(); } });路由守卫解决的是页面访问层面的控制,真正的接口权限还是在后端。很多人误解这次,以为前端拦住了就安全了,其实接口照样能被外部调。前端守卫只是为了用户体验。
4.3 axios封装与Token处理
axios封装是前后端联调的关键。我单独建了一个request模块:
import axios from 'axios'; import { ElMessage } from 'element-plus'; import router from '@/router'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; } return config; });响应拦截器做了两件事:统一处理业务错误码;遇到401时清理本地token并跳到登录页。这里有个细节,如果同时有几个请求都返回401,会连续跳转登录页很多次。我加了一个跳转锁:
let isRedirecting = false; service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message || 'error')); } return res; }, error => { if (error.response?.status === 401 && !isRedirecting) { isRedirecting = true; localStorage.removeItem('token'); router.push('/login'); } ElMessage.error(error.message || '网络错误'); return Promise.reject(error); } );这个小锁在真机上体验提升明显,不会出现弹一堆报错框的情况。
4.4 联调时最容易踩的细节
第一是时间格式。后端LocalDateTime默认序列化成ISO格式,前端展示是“2024-05-20T10:30:00”,不好看。我在后端实体字段上加了@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),全局生效。第二是分页参数。Element Plus的el-pagination组件默认返回的事件参数很多,一定要看清楚是currentPage还是current-change事件里的参数,否则每次翻页都还在请求第一页。
另外Vite开发时的跨域问题,我用server.proxy解决,不需要在后端开启CORS放行所有来源:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这样前端请求/api/poem/list,在开发环境会被代理转发到后端8080端口。生产环境再由Nginx做同样的路径转发,前后端代码里都不出现跨域问题。
5. 部署发布与文档配套
5.1 后端打包与启动
后端部署非常固定,先跳过测试打包:
mvn clean package -DskipTeststarget目录下会生成poem-platform.jar。在服务器上用java -jar启动就行。我推荐用systemd托管进程,这样服务器重启后服务能自动拉起,日志也能用journalctl查看。如果你是临时演示,简单点:
nohup java -jar poem-platform.jar --spring.profiles.active=prod > app.log 2>&1 &生产环境的数据库连接串、密码这些信息,我用application-prod.yml单独维护,和本地的application.yml分开。这样不会出现测试密码带到生产环境的情况。
5.2 前端构建与Nginx反向代理
前端打包:
npm run build生成dist目录,把dist里的文件上传到Nginx的站点目录。Nginx配置除了托管静态文件,还要把/api/路径反向到后端:
server { listen 80; server_name your-domain.com; root /www/poem-web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这个配置里的核心是try_files。前端是history模式路由,用户直接访问/poem/123时,Nginx找不到这个物理文件,必须回退到index.html,由Vue Router接管后才渲染对应页面。漏掉这一行,刷新后就是404,这是全栈项目部署最常见的坑。
5.3 数据库脚本初始化与文档组织
交付的数据库脚本我分成三个文件:01_schema.sql负责建库建表,02_data.sql导入初始诗词和诗人数据,03_admin.sql单独存放管理员账号初始化。导入时一定要指定字符集:
mysql -uroot -p --default-character-set=utf8mb4 poem < 01_schema.sql不然中文注释和内容很容易变成乱码。文档方面,我坚持三件套:README写在最前面,说明项目背景、技术栈、启动方式;操作手册带着截图走一遍用户端和管理端;接口文档用Knife4j自动生成,在线可调试。数据库设计文档单独用Markdown写清楚每张表的字段意义和关联关系,这样接手的人不会对着几千行SQL发呆。
实操心得:文档要比代码更早更新。我吃过一次亏,改了一个字段名但忘了同步数据库设计文档,后来隔了一个月回看项目,自己都对不上哪个是旧的哪个是新的。现在我把写文档纳入联调的收尾环节,接口调完顺手刷新Knife4j导出,数据库结构变更时马上更新设计文档,积压到后面只会更痛苦。
6. 常见问题与排错实录
6.1 Spring Boot版本太高导致的依赖连锁反应
这是这个项目里我踩得最狠的坑。Spring Boot 3.x之后,javax.servlet全改成jakarta.servlet,很多老项目的依赖坐标全部要换。MyBatis-Plus就是一个典型,3.5.3版本以后才提供了spring-boot3-starter,如果你还是用mybatis-plus-boot-starter,启动时八成会报ClassNotFoundException。解决方案两个:要么把整个项目统一切到Spring Boot 3,并换成新坐标;要么像我一样锁死在Spring Boot 2.7.x,省心稳定。对学生项目来说,我强烈建议锁2.7.x,因为学校老师给的参考资料、网上的博客教程,绝大多数都基于这个版本。
6.2 Vue环境配置和Node版本的问题
很多人拿到Vue项目源码,第一反应是npm install,然后报node-sass编译失败。这个问题的根源是Node.js版本升级后,node-sass的二进制包不匹配。现在的Sass官方推荐包名是sass,也就是dart-sass版本,兼容性更好。如果你拿到的老项目里写的是node-sass,直接卸载换成sass即可。
另一个常见报错是“Could not find a declaration file for module '@vue/tsconfig'”,这个通常是tsconfig.json里的extends路径不对,或者依赖没装全。最简单的方法是用官方create vue模板重新生成一份基础配置,然后把src目录替换掉,不要自己手搓tsconfig,省得踩坑。
6.3 MySQL中文乱码与全文索引失效
中文乱码八成都是字符集不统一。我统一的规矩是:数据库字符集用utf8mb4,连接串里加characterEncoding=utf8,导入SQL前先执行set names utf8mb4。只要这三处统一,基本不会乱码。如果还有问题,检查一下表字段的COLLATE是不是utf8mb4_general_ci或utf8mb4_unicode_ci。
全文索引搜不到中文,优先确认两件事:MySQL版本是否在5.7.6以上,建表时FULLTEXT KEY有没有加WITH PARSER ngram。少一个都搜不到。排查SQL到底走没走索引,用EXPLAIN看一眼执行计划最靠谱,不要靠猜。
6.4 跨域、401循环和部署遗漏
前端开发环境跨域,优先用Vite的proxy,而不是在后端写CorsConfiguration。生产环境用Nginx反向代理,这样前后端都相对路径,浏览器根本认为在同一个域里,不存在跨域。如果你生产环境还出现跨域报错,多半是Nginx配置没生效,检查location /api/的proxy_pass后面有没有多余路径。
401循环跳登录的问题,解决思路就是我在axios响应拦截器里加的那个跳转锁。多个请求同时返回401,只在第一次触发跳转,后面的请求直接走reject逻辑。另外,部署后刷新页面404,先看Nginx的try_files配置。如果接口能通但页面404,也先看location /这段是不是在最下面,避免接口路径被前面的location规则吃掉。
这个项目从数据库设计到部署上线,整个过程走下来,最大的收获不是那几张表或者几个接口,而是理解了全栈项目里“约定”的重要性。前后端接口约定、用户角色约定、时间格式约定、返回结构约定,只要有一处没对齐,联调阶段就会反复返工。源码、数据库、文档这三样东西,每一个都不是摆设,数据库决定数据存得稳不稳,源码决定功能跑得通不通,文档决定你离开三个月之后还认不认得出这个项目。以后我再做类似平台,会优先把全文检索和用户行为分析做深一层,但这一套前后端分离的骨架短期内不会变。