资讯动态

基于Spring Boot的红色知识学习平台开发实战

发布时间:2026/10/20 12:57:51 来源:尧图企业网站定制
前一阵我们团队接到一个需求要做一套红色知识学习平台。刚听到这个题目时我第一反应是这不就是一个内容管理系统吗无非是文章列表、用户登录、后台发文章。等真正把需求聊透之后才发现这里面的坑比想象中多得多。基于Spring Boot的红色知识学习平台表面上是一个“文章答题打卡”的组合拳实际上牵扯到内容审核、学习行为量化、防作弊、多角色权限等一系列问题。本文记录我从0到1搭建这套平台的全过程包括需求梳理、技术选型、数据库设计、核心模块实现、权限防作弊以及上线前的性能调优。如果你正准备做类似主题的系统设计、毕业设计或者要给单位搭一套内部学习平台这篇文章应该能帮你少踩不少坑。1. 需求边界与目标用户它不是简单的CMS站点1.1 需求方到底在痛什么需求方是一家单位的党建学习口以前学红色知识主要靠纸质材料、微信群转发文章学习效果完全没法衡量。他们提了几个很具体的诉求内容要成体系不能再靠零散转发要有专题、有分类、有来源。学习行为要留痕谁学了、学了多久、学了几篇管理员要能看得到。要能组织线上答题活动形成比学赶超的氛围。管理员要能自己录内容、发内容不能每次改个字都找开发。把这些诉求翻译成技术语言就是三个关键词内容管理、学习行为采集、答题活动闭环。但“可量化”这三个字才是整个项目的灵魂。平台上所有功能设计最终都要回答一个问题今天这个用户到底学了没有学了多少。1.2 用户角色和使用场景这个平台不像电商、社交产品有那么复杂的用户分层但角色边界必须清晰。我梳理了三类角色和对应场景角色主要行为核心功能诉求普通用户浏览文章、观看图文专题、参与答题、每日打卡学习日历、积分、排行榜、错题回顾内容管理员录入编辑内容、管理分类、审核发布富文本编辑、内容置顶、数据统计超级管理员账号管理、角色分配、系统参数配置权限分配、操作日志、全局设置使用场景也分几种。日常自主学习用户登录后按分类浏览内容阅读一篇文章并完成打卡单位组织的专题学习周管理员批量发布专题内容用户集中学习并参加答题竞赛管理员查看报表按部门、按人查看学习时长和答题成绩作为学习效果评估依据。1.3 和普通CMS平台的本质差异如果照搬一套博客系统改一改肯定会翻车。红色知识学习平台和普通CMS的差异至少有四点第一内容权威性要求高。不是用户UGC而是组织方整理的内容每一篇都可能有来源出处发出去之前必须审核审核动作要留痕。第二学习行为形成闭环。浏览、阅读、计时、打卡、答题、积分、排行这些都是串联的。普通CMS只需要“发文章—看文章”这里却要求每一步都有数据回流。第三权限模型更立体。除了前台用户、后台管理员还有“审核员”这种中间角色。内容发布要走“编辑提交—审核通过—发布上线”的状态机。第四防作弊是刚需。刷时长、刷积分、重复刷题这些在内部学习平台里是必然会出现的问题不提前设计好方案上线一周数据就会失真。想清楚这四点项目才敢开工。接下来就是技术选型和架构设计。2. 技术选型与分层架构Spring Boot 如何支撑整个平台2.1 为什么继续用 Spring Boot 而不是微服务这个项目规模撑死几千个用户同时在线属于典型的中小型业务系统上微服务纯粹是给自己找麻烦。Spring Boot 2.7.x 是当前最稳妥的选择资料多、生态成熟和 JDK 8 配合非常顺畅出了问题网上随便一搜就有答案。后端技术栈我最终定下来是Spring Boot 2.7 MyBatis-Plus MySQL 8 Redis JWT。管理端用 Vue3 Element Plus前台页面也是 Vue3前后端完全分离。有些同学可能会问为什么不用 Spring Data JPA我的理由很简单这个系统里有大量按条件查询和多表关联统计MyBatis-Plus 的 QueryWrapper 写起来最直接而且团队里大家对这个更熟悉少一点学习成本。2.2 项目骨架怎么搭创建一个 Spring Boot 工程没什么好说的关键在于依赖怎么引入。我直接贴出核心的 pom.xml 依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version3.19.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependencyJWT 用 java-jwt 这个库比手写一套 token 生成校验靠谱得多。Redis 走 Spring Data Redis后续做缓存和防刷都要用。校验组件也建议加上后面写参数校验时能省不少事。2.3 分层结构设计代码分包我按下面这种方式组织逻辑清晰团队协作不容易乱com.example.redstudy ├── common // 统一返回、全局异常、常量 ├── config // Redis、拦截器、WebMvc配置 ├── controller // 接口层只做参数接收和结果返回 ├── service // 业务层核心逻辑都在这 ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体 ├── dto // 接收前端参数的模型 └── vo // 返回给前端的数据模型一个容易被忽略的细节是 dto 和 vo 的拆分。很多新手直接拿 entity 去接收前端参数导致后面前端表单字段一变化实体类就得跟着改数据库表也可能被污染。我把请求参数和响应数据单独建模虽然多写几个类但后期维护省心太多。2.4 统一返回体和全局异常处理前后端分离项目接口返回格式必须统一。我定义了一个 Result 类Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }配合全局异常处理器把业务异常、参数校验异常、系统异常分开捕获返回给前端的结构永远是同一套前端拿到 code 不等于 200 就弹 message。这样做的好处是联调阶段基本不用撕接口格式。3. 数据模型设计六张表与“学习记录”粒度之争3.1 用户表别把角色做成死字段用户表是系统的基础但设计时容易踩一个坑把角色写死成一个字段admin、user 这样一填了事。如果以后增加一个“审核员”角色就得改代码加枚举。我的做法是保留 role_type 字段的同时预留了 user_role 关联表的空间。数据量不大时直接用字段判断等权限模型复杂了再拆表迁移成本可控。用户表核心字段如下CREATE TABLE t_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 加盐后的密码, salt varchar(32) DEFAULT NULL, nickname varchar(50) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, role_type varchar(20) NOT NULL DEFAULT USER, department_id bigint DEFAULT NULL COMMENT 所属部门, status tinyint NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码必须加盐这是基本觉悟。salt 用 UUID 去掉横线生成数据库里存的是 md5(password salt) 或者更保险的 bcrypt。建议直接用 bcrypt不要自己造轮子。3.2 内容域分类、文章、标签分离内容模块包含分类表、文章表、标签表。分类和文章是一对多标签和文章是多对多。为了不引入多余关联表我加了 content_tag 字段用逗号分隔存储标签 ID查询时由前端按需解析。小系统没必要为了范式完美搞五张表适度冗余能大幅减少查询复杂度。文章表需要注意的字段content用longtext富文本内容体积比纯文本大很多。source记录内容来源权威性要求决定这个字段不能省。status记录审核状态audit_user_id、audit_time记录审核痕迹。view_count用 int 做冗余计数避免每次统计都 count 一次。3.3 学习记录粒度之争一张表还是两张表这是整个数据库设计里我纠结最久的地方。学习记录到底存什么粒度方案 A每人每天一条记录字段是 user_id、learn_date、total_duration、checkin_time。好处是打卡、连续天数统计非常流畅一条记录一个用户一天。坏处是想知道“这个人读过哪几篇文章”时完全没数据。方案 B每篇文章一条学习记录字段是 user_id、article_id、duration、finish_flag。好处是能统计已读文章数、掌握度。坏处是打卡场景要按日期聚合连续天数计算要 group by数据量大时有点吃力。最终我两个方案都用了拆成study_daily和study_detail两张表。前者管打卡和连续天数后者管学习明细。业务上一个管“今天学了多久”一个管“这篇学了多久”本来就是两个诉求硬塞一张表会让两个方向的查询都别扭。核心建表语句如下CREATE TABLE study_daily ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, learn_date date NOT NULL, total_duration int DEFAULT 0 COMMENT 学习时长单位秒, checkin_time datetime DEFAULT NULL COMMENT 首次打卡时间, PRIMARY KEY (id), UNIQUE KEY uk_user_date (user_id, learn_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE study_detail ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, article_id bigint NOT NULL, duration int DEFAULT 0, finish_flag tinyint DEFAULT 0, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_article (user_id, article_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;study_daily上加唯一索引(user_id, learn_date)天然防止同一天重复插入。study_detail上建(user_id, article_id)索引查询某人对某篇文章的学习情况直接走索引。3.4 题库与成绩表答题模块需要三张表题目表、答题记录表、答题明细表。题目表设计question_type单选、多选、判断。optionsJSON 字符串存选项如[A. 遵义, B. 延安, ...]。answer正确答案单选存 A/B/C/D多选存逗号分隔。analysis答案解析答题后展示给用户看。difficulty1-3 难度等级抽题时按难度比例出。答题记录表记录一次考试的总体情况答题明细表记录每一道题的对错和用户答案。明细表用text存用户答案因为多选题答案是一串字母。3.5 索引策略与冗余字段心得小系统的索引设计原则是“够用就行”。我在所有外键字段上都建了索引状态字段和分类字段这种低选择性的字段建立联合索引比如(category_id, status)。像content这种大文本字段坚决不建索引。view_count这类统计字段不实时查库而是通过 Redis 异步累加后定期回写避免每次页面刷新都触发一次 update。4. 核心功能拆解学习打卡、答题抽题与内容检索4.1 学习计时与打卡的落地细节学习计时如果只靠前端计时用户关掉页面就停了或者开两个页面刷时长数据根本不可信。我的方案是前端每 30 秒上报一次心跳后端把耗时累计到 RedisService public class StudyHeartbeatService { Autowired private StringRedisTemplate redisTemplate; private static final String HEARTBEAT_KEY study:heartbeat:%s:%s; public void heartbeat(Long userId, Long articleId) { String date LocalDate.now().toString(); String key String.format(HEARTBEAT_KEY, userId, date); // 每次心跳累加30秒设置2小时过期用户学习过程中会持续续期 redisTemplate.opsForValue().increment(key, 30); redisTemplate.expire(key, 2, TimeUnit.HOURS); // 更新文章维度的学习时长 updateArticleDuration(userId, articleId, 30); } }打卡接口独立于心跳用户手动点击“今日打卡”时触发。这里用 SetNX 做并发控制防止用户手动连点导致连续天数加两次public ResultBoolean checkIn(Long userId) { String date LocalDate.now().toString(); String lockKey study:checkin: userId : date; Boolean success redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 24, TimeUnit.HOURS); if (Boolean.FALSE.equals(success)) { return Result.error(4001, 今天已经打过卡了); } // 判断昨天是否有记录有则连续天数1否则重置为1 int streak calcContinuousDays(userId); StudyDaily daily new StudyDaily(); daily.setUserId(userId); daily.setLearnDate(LocalDate.now()); daily.setCheckinTime(LocalDateTime.now()); // 写库、更新连续天数 studyDailyService.saveOrUpdate(daily); return Result.success(true); }连续天数的核心逻辑在calcContinuousDays查昨天的记录存在就在其基础上加 1不存在就重置为 1。注意这里要考虑到用户跨天还在学习的情况凌晨打卡时以“自然日”为准不搞复杂的“最近24小时滚动窗口”需求方也认可这种统计口径。4.2 答题模块随机抽题并不是纯粹的随机答题模块一开始也想得很简单从题库里随机出 10 道题。后来测试发现用户连续做几套卷子会反复抽到同样的题体验很差。原因是简单随机在题量只有一百多道时重复率确实高。我的做法是加一个“近期已出题”缓存每次抽题时排除最近 20 道已经出过的题public ListQuestion pickQuestions(Long categoryId, int count) { String cacheKey study:recent: userId : categoryId; SetString recentIds redisTemplate.opsForZSet().range(cacheKey, 0, -1); ListQuestion pool questionMapper.selectList( new LambdaQueryWrapperQuestion() .eq(Question::getCategoryId, categoryId) .notIn(JsonUtil.isNotEmpty(recentIds), Question::getId, recentIds) .orderByAsc(Question::getId) ); // 按难度比例抽题简单4道、中等4道、困难2道 MapInteger, ListQuestion groupByDifficulty pool.stream() .collect(Collectors.groupingBy(Question::getDifficulty)); // 抽取后把题目ID存入ZSet并设置过期时间为3天 recentIds.forEach(id - redisTemplate.opsForZSet().add(cacheKey, id, System.currentTimeMillis())); redisTemplate.expire(cacheKey, 3, TimeUnit.DAYS); return selectedQuestions; }按难度比例抽题这步值得展开说一下。如果不控制难度抽出来全是简单题用户会觉得没挑战全是难题学习积极性又被打击。我按 4:4:2 的比例抽简单、中等、困难三类每套卷子 10 道难度分布比较合理。答题提交后的判分逻辑要放到后端做前端只负责把 user_answer 传回来后端和 answer 字段比对绝不能信任前端传的 isCorrect 字段。4.3 内容检索不引 Elasticsearch 的全量方案学习平台的内容数据量短期很难超过十万条上 Elasticsearch 有点杀鸡用牛刀。我用 MyBatis-Plus 的 QueryWrapper 做多字段模糊匹配先满足核心搜索需求public PageArticleVO search(String keyword, Integer categoryId, int page, int size) { LambdaQueryWrapperArticle wrapper new LambdaQueryWrapper(); wrapper.and(kw - kw .like(Article::getTitle, keyword) .or() .like(Article::getSummary, keyword) .or() .like(Article::getContent, keyword)); if (categoryId ! null) { wrapper.eq(Article::getCategoryId, categoryId); } wrapper.eq(Article::getStatus, PUBLISHED) .orderByDesc(Article::getPublishTime); PageArticle result articleMapper.selectPage(new Page(page, size), wrapper); // 转成VO返回隐藏content字段避免传输超大文本 return convertToVO(result); }搜索功能有两个细节要注意。第一是 or 条件必须嵌套在 and 里否则会和 categoryId、status 的条件产生逻辑混乱。第二是返回给前端时不要把整篇 content 带出去列表接口只返回标题、摘要、封面图这些元数据详情接口单独再返回 content这对接口性能影响非常大。如果未来内容量真的大到 LIKE 扛不住再迁移到 Elasticsearch 也不迟业务层接口已经做了隔离查询逻辑都在 Service 层换引擎只影响一个实现类。4.4 管理端富文本内容的安全处理内容管理员往富文本编辑器里粘贴内容时通常会带出一堆标签和行内样式这时候直接入库就是给自己埋雷。我用 jsoup 做标签白名单过滤只保留安全的标签public String cleanHtml(String dirtyHtml) { Whitelist whitelist Whitelist.relaxed(); whitelist.addTags(p, br, span, img, h1, h2, h3, blockquote); whitelist.addAttributes(img, src, alt, width, height); whitelist.addAttributes(a, href, title, target); whitelist.removeAttributes(a, javascript, onclick); return Jsoup.clean(dirtyHtml, whitelist); }凡是用户可控输入然后会展示给其他人的内容都必须过这一道清洗。不光是防 XSS也是防止样式错乱、布局崩坏。评论、昵称这些字段同理入库前统一走过滤器。5. 权限体系与防作弊三层防线怎么落地5.1 RBAC 权限模型的轻量实现完整版 RBAC 需要用户表、角色表、权限表、用户角色关联表、角色权限关联表五张表。但本项目实际角色只有三种操作权限按角色划分足够。我采用了折中方案用户表保留 role_type 字段再配合自定义注解和拦截器控制接口访问。自定义注解加在 Controller 方法上Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); }拦截器里解析 JWT 拿到当前用户角色判断是否有对应权限public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole null) { return true; } // 从ThreadLocal获取当前登录用户 UserContext user userContextHolder.get(); if (user null) { response.setStatus(401); return false; } ListString allowedRoles Arrays.asList(requireRole.value()); if (!allowedRoles.contains(user.getRoleType())) { response.setStatus(403); return false; } } return true; }这种轻量实现的好处是角色少时直观、开发快。如果后续权限点细化到“某分类只有某些管理员能管”再迁移到完整 RBAC 模型改动也能控在一个 Service 类之内。5.2 内容审核状态机内容审核必须有状态流转我用一个 status 字段表示四个状态DRAFT草稿、PENDING待审核、PUBLISHED已发布、REJECTED已驳回。状态流转规则编辑创建时是 DRAFT提交审核后变 PENDING审核通过变 PUBLISHED不通过变 REJECTED 并填写驳回原因。已发布的内容如果编辑再次修改状态要打回 PENDING 重新走审核不能直接覆盖线上内容。审核记录单独存一张 audit_log 表每次审核操作都插入一条记录包含操作人、操作时间、操作类型、备注。一张表解决了两个问题追溯某篇文章的审核历史以及责任到人。5.3 防刷三件套每日积分上限、答题频率限制、IP限流内部学习平台一旦和积分、排行榜挂钩就必然有人想刷。我的防线分三层第一层每日积分上限。用户每天从学习、答题、打卡获得的积分设一个总上限超过后积分不再增长。这个直接用 Redis 计数器实现public boolean canAddScore(Long userId, int score) { String key study:score: userId : LocalDate.now().toString(); Long current redisTemplate.opsForValue().increment(key, score); int maxDailyScore 100; if (current maxDailyScore) { // 超出部分扣回去返回false redisTemplate.opsForValue().decrement(key, score); return false; } return true; }第二层答题频率限制。同一用户同一分类一分钟内最多作答 5 次用 Redis 加过期时间String key study:exam:rate: userId : categoryId; Long count redisTemplate.opsForValue().increment(key); if (count 1) { redisTemplate.expire(key, 60, TimeUnit.SECONDS); } if (count 5) { return Result.error(429, 答题过于频繁请稍后再试); }第三层IP 限流。针对同一 IP 短时间内的请求总量做限制防止有人写脚本批量刷接口。这个放在拦截器里执行每个请求进来先检查 IP 维度的计数器。上线后观察日志平时的正常请求量远低于限流阈值所以对正常用户几乎没有感知。5.4 敏感词过滤与操作日志评论、昵称这些用户输入内容发布前必须经过敏感词过滤。我用了基于 DFA 算法的敏感词工具初始化时加载一次词库到内存匹配时时间复杂度稳定。词库支持热更新运营发现新词后通过管理端追加不用重启服务。管理员的所有敏感操作发布内容、审核通过、修改用户状态、调整角色都记录操作日志。日志内容包含操作人、请求路径、请求参数、IP、时间。审计日志不做实时分析每天由脚本归档一次出问题时能根据日志还原完整操作链路。6. 上线前的打磨慢查询、缓存与部署排错6.1 通过慢查询日志揪出性能短板开发环境数据量小很多 SQL 性能问题根本测不出来。上线前我导了一批模拟数据然后开启 MySQL 慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;跑了几轮测试后发现study_detail表按用户查询的时候有慢 SQL。原因是表里数据量上来后虽然建了(user_id, article_id)索引但查询条件里还有update_time的排序导致 MySQL 需要回表。解决方法是把排序字段加入索引建立(user_id, article_id, update_time)联合索引或者改用覆盖索引把duration、finish_flag也带进去。这里给新手一个建议不要相信直觉先用 EXPLAIN 看执行计划。EXPLAIN SELECT ...一行输出就能告诉你有没有走索引、扫描了多少行、有没有 Using filesort。慢 SQL 优化是有据可循的不是拍脑袋瞎改。6.2 Redis 缓存策略文章详情和分类列表文章详情是读多写少的典型场景一天可能被访问几千次。我用 Cache Aside 模式public ArticleVO getArticleDetail(Long id) { String key article:detail: id; Object cache redisTemplate.opsForValue().get(key); if (cache ! null) { return JSON.parseObject(cache.toString(), ArticleVO.class); } ArticleVO vo queryFromDb(id); if (vo ! null) { redisTemplate.opsForValue().set(key, JSON.toJSONString(vo), 30, TimeUnit.MINUTES); } return vo; }更新文章时主动删除缓存而不是等缓存过期。这样能保证编辑发布新内容后用户刷新就能看到最多只存在一个极短的缓存不一致窗口。分类列表和排行榜数据也有类似处理。排行榜的缓存时间设短一点比如 5 分钟因为实时性要求相对高。缓存 key 的设计要带版本号比如article:detail:v2:{id}改结构时直接换版本号避免新旧数据格式混在一起导致的解析异常。6.3 部署阶段踩过的典型坑把项目从本地搬到服务器遇到的第一个问题是MySQL 时区错乱。本地连数据库一切正常服务器上所有create_time都比实际时间早 8 个小时最后检查发现连接串里少配了jdbc:mysql://localhost:3306/redstudy?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8mb4第二个坑是user 表名和保留字冲突。虽然user在 MySQL 里不是严格的保留字但某些环境下容易出问题。我的习惯是表名前统一加t_前缀t_user、t_article既避免了保留字问题也方便和其他业务库区分。第三个坑是文件上传路径。本地开发用绝对路径没问题打包成 jar 后路径就失效了。我的方案是把上传文件放在服务器固定目录/data/redstudy/upload然后由 Nginx 做静态资源映射应用代码里不保存完整路径只保存相对路径。第四个坑是HikariCP 连接池默认值太小。默认 maximum-pool-size 是 10模拟答题并发 100 人时直接报连接不够。调大连接池确实能缓解但更重要的是优化 SQL 和事务范围连接池上限调到 40 后稳定运行没有再出现连接耗尽的情况。6.4 部署清单参考整个项目部署我用的是最经典的方案一台云服务器装 MySQL、Redis、Nginx后端打 jar 包用 systemd 托管前端打包后的 dist 目录交给 Nginx 托管。# 后端构建打包 mvn clean package -DskipTests # 启动jar包 nohup java -jar redstudy-server.jar --spring.profiles.activeprod # Nginx关键配置 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /upload/ { alias /data/redstudy/upload/; }生产环境一定要用独立的环境配置文件application-prod.yml数据库连接、Redis 地址、日志级别都分开。开发环境和生产环境的配置混在一起是后期运维事故的高发源头。这个项目做下来我最深的体会是Spring Boot 本身只是工具真正决定项目成败的是对业务模型的理解。把“学习闭环”四个字想透了数据库设计和模块划分都会变得清晰不想透写了再多代码也只是在做一个不好用的 CMS。这套架构后续如果想继续扩展可以往视频学习、积分商城、基于行为数据的个性化推荐这几个方向走核心骨架基本不用动。希望这篇偏实战的记录能给你一些灵感少走点弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑