引言
在企业级在线教育系统的建设中,技术架构的选择往往决定了系统的天花板:单体架构可能在前几百门课程时就遇到瓶颈;前端各端独立开发会导致维护成本指数级增长;缺乏实时通信能力则无法支撑学习进度追踪和考试监考等关键场景。
本文将从技术架构层面深度拆解织码在线教育系统,重点分析五个核心技术方案:微服务拆分策略、多端统一 API 设计、SSE 实时通信方案、RSA+AES 混合加密传输、以及 Docker+Nginx 容器化部署。每个方案都会给出架构思路和关键代码实现。
— 
一、微服务拆分策略
1.1 拆分原则
织码在线教育系统采用按业务域拆分的微服务策略。拆分遵循三个原则:
- 高内聚低耦合:每个服务对应一个明确的业务域,服务内部功能高度相关
- 独立部署独立扩容:高频服务(如课程服务)和低频服务(如配置服务)可以独立扩容
- 数据所有权独立:每个服务独占自己的数据库,跨服务通过 API 调用,不共享数据库
1.2 服务拆分全景
┌─────────────────────────────────────────────────────────────┐
│ API Gateway (网关) │
│ 路由 / 鉴权 / 限流 / 日志 / 请求聚合 │
└────────────────────────────┬────────────────────────────────┘
│
┌────────────┬───────────┼───────────┬────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ ┌────────┐
│用户服务│ │课程服务│ │考试服务 │ │任务服务│ │订单服务│
│User │ │Course │ │Exam │ │Task │ │Order │
│ │ │ │ │ │ │ │ │ │
│注册登录│ │课程管理│ │题库考试 │ │培训任务│ │订单支付│
│组织架构│ │章节管理│ │组卷判分 │ │学习路径│ │权限开通│
│权限RBAC│ │素材管理│ │防作弊 │ │进度追踪│ │ │
│学习档案│ │套餐管理│ │错题本 │ │问卷调研│ │ │
└────────┘ └────────┘ └──────────┘ └────────┘ └────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ ┌────────┐
│资源服务│ │直播服务│ │统计服务 │ │配置服务│ │消息服务│
│Resource│ │Live │ │Stats │ │Config │ │Message│
│ │ │ │ │ │ │ │ │ │
│素材入库│ │推拉流 │ │数据看板 │ │应用配置│ │站内信 │
│试卷归档│ │互动 │ │多维报表 │ │菜单管理│ │推送 │
│证书管理│ │ │ │订单统计 │ │微页面 │ │ │
│表单中心│ │ │ │ │ │资讯中心│ │ │
└────────┘ └────────┘ └──────────┘ └────────┘ └────────┘
1.3 服务注册与发现
使用 Nacos 作为注册中心和配置中心:
# bootstrap.yml - 各微服务统一配置
spring:
application:
name: course-service
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:nacos}:8848
namespace: ${NAMESPACE:prod}
config:
server-addr: ${NACOS_HOST:nacos}:8848
namespace: ${NAMESPACE:prod}
file-extension: yaml
shared-configs:
- data-id: common-db.yaml # 共享数据库配置
- data-id: common-redis.yaml # 共享缓存配置
- data-id: common-oss.yaml # 共享存储配置
1.4 服务间调用:OpenFeign
// 考试服务需要调用用户服务获取学员信息
@FeignClient(name = "user-service", contextId = "userClient")
public interface UserFeignClient {
@GetMapping("/api/user/internal/{id}")
UserDTO getUserById(@PathVariable("id") Long id);
@GetMapping("/api/user/internal/{id}/permissions")
List<String> getUserPermissions(@PathVariable("id") Long id);
}
// 考试服务需要调用课程服务校验课程权限
@FeignClient(name = "course-service", contextId = "courseClient")
public interface CourseFeignClient {
@GetMapping("/api/course/internal/{id}/permission/{userId}")
Boolean checkPermission(@PathVariable("id") Long courseId,
@PathVariable("userId") Long userId);
}
1.5 熔断与降级
// Feign 降级处理
@Component
public class UserFeignFallback implements UserFeignClient {
@Override
public UserDTO getUserById(Long id) {
// 降级策略:返回缓存数据或默认值
return UserDTO.builder()
.id(id)
.name("未知用户")
.build();
}
@Override
public List<String> getUserPermissions(Long id) {
// 降级策略:返回空权限列表,拒绝访问(安全优先)
return Collections.emptyList();
}
}

二、多端统一 API 设计
2.1 设计理念
织码在线教育系统的四端(Admin/Web/App/小程序)共享同一套后端 API。核心设计原则:
| 设计原则 | 实现方式 |
|---|---|
| URL 统一 | 所有端调用相同的 RESTful API |
| 响应格式统一 | 统一 Result 包装 |
| 鉴权统一 | JWT Token,四端通用 |
| 分页统一 | 游标分页,兼容小程序无限滚动 |
| 差异处理 | 通过请求头 X-Client-Type 标识客户端类型 |
2.2 统一响应格式
@Data
@Schema(description = "统一响应结构")
public class Result<T> {
private int code; // 业务状态码:0成功,非0失败
private String message; // 提示信息
private T data; // 业务数据
private long timestamp; // 时间戳
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 0;
r.message = "success";
r.data = data;
r.timestamp = System.currentTimeMillis();
return r;
}
public static <T> Result<T> error(int code, String message) {
Result<T> r = new Result<>();
r.code = code;
r.message = message;
r.timestamp = System.currentTimeMillis();
return r;
}
}
2.3 多端分页适配
// 统一分页请求参数
@Data
public class PageRequest {
private Integer page = 1;
private Integer size = 20;
private Long lastId; // 游标:上一页最后一条记录ID(小程序用)
private String sort; // 排序字段
private String order; // asc/desc
}
// 分页响应
@Data
public class PageResult<T> {
private List<T> list;
private Long total;
private Integer page;
private Integer size;
private Boolean hasMore; // 是否还有更多(小程序无限滚动用)
private Long lastId; // 当前页最后一条ID(下一页游标)
}
2.4 客户端类型处理
不同端在某些场景下有差异化需求,通过拦截器统一处理:
@Component
public class ClientTypeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String clientType = request.getHeader("X-Client-Type");
// 记录客户端类型到请求上下文
RequestContext.setClientType(clientType);
// 小程序端:限制单次请求数据量,避免大响应导致卡顿
if ("MINIAPP".equals(clientType)) {
String size = request.getParameter("size");
if (size == null || Integer.parseInt(size) > 10) {
request.getParameterMap().put("size", new String[]{"10"});
}
}
return true;
}
}
2.5 文件上传统一方案
四端文件上传统一走 OSS 直传 + 服务端签名:
上传流程(以图片上传为例):
前端(任意端) 后端签名服务 阿里云OSS
│ │ │
│ 1.请求上传签名 │ │
│───────────────────▶│ │
│ │ 2.生成STS临时凭证 │
│ │───────────────────▶│
│ │◀───────────────────│
│ 3.返回签名/上传URL │ │
│◀───────────────────│ │
│ │ │
│ 4.直传文件到OSS │ │
│────────────────────────────────────────▶│
│ │ │
│ 5.上传成功回调 │ │
│◀────────────────────────────────────────│
│ │ │
│ 6.通知后端文件已上传 │ │
│───────────────────▶│ │
│ │ 7.记录文件元数据 │
│ 8.返回文件访问URL │ │
│◀───────────────────│ │
@RestController
@RequestMapping("/api/media")
public class MediaController {
/**
* 获取 OSS 上传签名(所有端通用)
*/
@PostMapping("/upload-sign")
public Result<UploadSignVO> getUploadSign(@RequestBody UploadSignDTO dto) {
// 1. 生成唯一文件Key
String fileKey = String.format("edu/%s/%s/%s",
dto.getType(), // course/cover/question/...
LocalDate.now(), // 按日期分目录
UUID.randomUUID() + "_" + dto.getFileName()
);
// 2. 生成 POST 签名策略
String policy = ossService.generatePolicy(dto.getContentType(), 50); // 50MB限制
String signature = ossService.signPolicy(policy);
return Result.success(UploadSignVO.builder()
.host(ossConfig.getEndpoint())
.key(fileKey)
.policy(policy)
.accessKeyId(ossConfig.getAccessKeyId())
.signature(signature)
.expire(ossConfig.getExpireSeconds())
.build());
}
}
三、SSE 实时通信方案
3.1 为什么用 SSE 而不是 WebSocket
在在线教育场景中,大部分实时需求是服务端推送(学习进度更新、考试倒计时同步、直播状态推送),客户端不需要频繁向服务端发消息。SSE(Server-Sent Events)相比 WebSocket 有以下优势:
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 服务端→客户端(单向) | 双向 |
| 协议 | HTTP | 独立协议 |
| 断线重连 | 浏览器自动重连 | 需手动实现 |
| 兼容性 | 除IE外全支持 | 全支持 |
| 复杂度 | 低 | 中 |
| 适用场景 | 推送通知、进度更新 | 聊天、协同编辑 |
3.2 SSE 服务端实现
@RestController
@RequestMapping("/api/sse")
public class SSEController {
private final ConcurrentHashMap<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
/**
* 建立 SSE 连接
*/
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@AuthenticationPrincipal UserPrincipal user) {
// 超时时间设为30分钟(覆盖一节课程的学习时长)
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);
emitter.onCompletion(() -> emitters.remove(user.getId()));
emitter.onTimeout(() -> emitters.remove(user.getId()));
emitter.onError(e -> emitters.remove(user.getId()));
emitters.put(user.getId(), emitter);
// 发送连接成功事件
try {
emitter.send(SseEmitter.event()
.name("connected")
.data("{\"status\":\"connected\"}")
.id(String.valueOf(System.currentTimeMillis())));
} catch (IOException e) {
emitter.completeWithError(e);
}
return emitter;
}
/**
* 推送学习进度更新
*/
public void pushProgressUpdate(Long userId, ProgressUpdateDTO dto) {
SseEmitter emitter = emitters.get(userId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event()
.name("progress")
.data(JsonUtils.toJson(dto))
.reconnectTime(3000)); // 断线后3秒重连
} catch (IOException e) {
emitter.completeWithError(e);
emitters.remove(userId);
}
}
}
/**
* 推送考试时间提醒
*/
public void pushExamReminder(Long userId, ExamReminderDTO dto) {
SseEmitter emitter = emitters.get(userId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event()
.name("exam-reminder")
.data(JsonUtils.toJson(dto)));
} catch (IOException e) {
emitter.completeWithError(e);
emitters.remove(userId);
}
}
}
}
3.3 SSE 前端实现
// Vue 3 Composition API - SSE 连接 Hook
export function useSSE() {
const eventSource = ref(null)
const connected = ref(false)
const connect = (token) => {
// 注意:SSE 不支持自定义请求头,通过 URL 参数传递 Token
eventSource.value = new EventSource(
`/api/sse/connect?token=${token}`
)
eventSource.value.addEventListener('connected', (e) => {
connected.value = true
console.log('SSE 已连接')
})
// 学习进度推送
eventSource.value.addEventListener('progress', (e) => {
const data = JSON.parse(e.data)
// 更新学习进度
progressStore.update(data)
})
// 考试提醒推送
eventSource.value.addEventListener('exam-reminder', (e) => {
const data = JSON.parse(e.data)
ElNotification({
title: '考试提醒',
message: `「${data.examTitle}」将在${data.minutes}分钟后开始`,
type: 'warning',
duration: 0 // 不自动关闭
})
})
eventSource.value.onerror = () => {
connected.value = false
// 浏览器会自动重连,无需手动处理
}
}
const disconnect = () => {
eventSource.value?.close()
connected.value = false
}
onUnmounted(() => disconnect())
return { connected, connect, disconnect }
}
3.4 学习进度实时同步
@Service
public class ProgressSyncService {
private final SSEController sseController;
/**
* 学员观看视频时,前端定时上报学习进度
* 服务端收到后通过 SSE 推送给管理端实时看板
*/
@Async
public void syncLearnProgress(Long userId, Long courseId, Integer progress) {
// 1. 更新数据库学习进度
userCourseService.updateProgress(userId, courseId, progress);
// 2. 通过 SSE 推送实时进度
sseController.pushProgressUpdate(userId, ProgressUpdateDTO.builder()
.userId(userId)
.courseId(courseId)
.progress(progress)
.timestamp(System.currentTimeMillis())
.build());
// 3. 进度100%时触发完成事件
if (progress >= 100) {
eventPublisher.publishEvent(new CourseCompletedEvent(userId, courseId));
}
}
}
四、RSA+AES 混合加密传输
4.1 加密方案设计
系统对敏感接口(登录、支付、考试提交等)采用 RSA+AES 混合加密传输,兼顾安全性与性能:
混合加密流程:
客户端 服务端
│ │
│ 1. 请求获取 RSA 公钥 │
│────────────────────────────────────────▶│
│ 2. 返回 RSA 公钥 │
│◀────────────────────────────────────────│
│ │
│ 3. 生成随机 AES 密钥 │
│ 4. 用 RSA 公钥加密 AES 密钥 │
│ 5. 用 AES 密钥加密请求体 │
│ 6. 发送:加密的AES密钥 + 加密的请求体 │
│────────────────────────────────────────▶│
│ │ 7. 用 RSA 私钥解密获取 AES 密钥
│ │ 8. 用 AES 密钥解密请求体
│ │ 9. 业务处理
│ 10. 返回:AES 加密的响应体 │
│◀────────────────────────────────────────│
│ │
│ 11. 用 AES 密钥解密响应体 │
4.2 服务端实现
@Component
public class CryptoService {
private final KeyPair rsaKeyPair;
private final ConcurrentHashMap<String, String> aesKeyCache = new ConcurrentHashMap<>();
public CryptoService() {
// 启动时生成 RSA 密钥对
this.rsaKeyPair = RSAUtil.generateKeyPair(2048);
}
/**
* 获取 RSA 公钥(Base64 编码)
*/
public String getPublicKey() {
return Base64.getEncoder().encodeToString(
rsaKeyPair.getPublic().getEncoded()
);
}
/**
* 解密客户端发来的加密数据
*/
public String decrypt(String encryptedAesKey, String encryptedData) {
// 1. 用 RSA 私钥解密 AES 密钥
String aesKey = RSAUtil.decrypt(encryptedAesKey, rsaKeyPair.getPrivate());
// 2. 用 AES 密钥解密数据
return AESUtil.decrypt(encryptedData, aesKey);
}
/**
* 加密响应数据
*/
public String encrypt(String aesKey, String data) {
return AESUtil.encrypt(data, aesKey);
}
}
// 加密请求解密过滤器
@Component
public class DecryptFilter implements Filter {
@Autowired
private CryptoService cryptoService;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
// 检查是否是加密接口
String encrypted = req.getHeader("X-Encrypted");
if ("true".equals(encrypted)) {
String aesKey = req.getHeader("X-Aes-Key");
String body = StreamUtils.copyToString(
req.getInputStream(), StandardCharsets.UTF_8
);
// 解密
String decrypted = cryptoService.decrypt(aesKey, body);
// 包装为新的请求对象
request = new DecryptRequestWrapper(req, decrypted);
}
chain.doFilter(request, response);
}
}
4.3 前端加密实现
// 前端加密工具
import JSEncrypt from 'jsencrypt'
import CryptoJS from 'crypto-js'
// 获取 RSA 公钥并加密请求
export async function encryptedRequest(url, data) {
// 1. 获取 RSA 公钥
const { data: pubKey } = await api.get('/api/crypto/public-key')
// 2. 生成随机 AES 密钥
const aesKey = CryptoJS.lib.WordArray.random(16).toString()
// 3. RSA 加密 AES 密钥
const encryptor = new JSEncrypt()
encryptor.setPublicKey(pubKey)
const encryptedAesKey = encryptor.encrypt(aesKey)
// 4. AES 加密请求数据
const encryptedData = CryptoJS.AES.encrypt(
JSON.stringify(data),
CryptoJS.enc.Utf8.parse(aesKey),
{ mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }
).toString()
// 5. 发送加密请求
const response = await api.post(url, encryptedData, {
headers: {
'X-Encrypted': 'true',
'X-Aes-Key': encryptedAesKey,
'Content-Type': 'text/plain'
}
})
// 6. 解密响应
const decrypted = CryptoJS.AES.decrypt(
response.data,
CryptoJS.enc.Utf8.parse(aesKey),
{ mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }
).toString(CryptoJS.enc.Utf8)
return JSON.parse(decrypted)
}
五、容器化部署方案
5.1 部署架构
┌──────────────────────────────────────────────────────┐
│ 互联网用户 │
└──────────────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Nginx (反向代理 + SSL) │
│ ┌─────────┐ ┌──────────┐ ┌─────────────────────┐ │
│ │ /admin │ │ / │ │ /api/* │ │
│ │ Admin端 │ │ Web学习端│ │ Gateway网关 │ │
│ │ 静态资源│ │ 静态资源 │ │ │ │
│ └─────────┘ └──────────┘ └──────────┬──────────┘ │
└─────────────────────────────────────────┼────────────┘
│
┌─────────────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│user-svc │ │course-svc│ │exam-svc │
│Container │ │Container │ │Container │
│:8081 │ │:8082 │ │:8083 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└────────────┬───────┴──────────────┘
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ MySQL │ │ Redis │ │ Nacos │
│ │ │ │ │ │
└────────┘ └────────┘ └────────┘
5.2 Nginx 配置
# nginx.conf
upstream gateway {
server gateway:8080;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name edu.example.com;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
# Admin 管理端
location /admin {
alias /usr/share/nginx/html/admin;
try_files $uri $uri/ /admin/index.html;
}
# Web 学习端 (Nuxt SSR)
location / {
proxy_pass http://web-ssr:3000;
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;
}
# API 网关
location /api {
proxy_pass http://gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# SSE 支持
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
# WebSocket 支持(直播)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1024;
}
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name edu.example.com;
return 301 https://$server_name$request_uri;
}
5.3 Docker Compose 完整编排
version: '3.8'
services:
# ============ 基础设施 ============
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: edu_system
volumes:
- mysql-data:/var/lib/mysql
- ./sql/init:/docker-entrypoint-initdb.d
ports:
- "3306:3306"
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 512mb
volumes:
- redis-data:/data
nacos:
image: nacos/nacos-server:v2.3.0
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_DB_NAME: nacos_config
depends_on:
- mysql
# ============ 后端微服务 ============
gateway:
build: ./services/gateway
environment:
NACOS_SERVER_ADDR: nacos:8848
depends_on:
- nacos
restart: always
user-service:
build: ./services/user-service
environment:
NACOS_SERVER_ADDR: nacos:8848
MYSQL_HOST: mysql
REDIS_HOST: redis
depends_on:
- mysql
- redis
- nacos
restart: always
course-service:
build: ./services/course-service
environment:
NACOS_SERVER_ADDR: nacos:8848
MYSQL_HOST: mysql
REDIS_HOST: redis
depends_on:
- mysql
- redis
- nacos
restart: always
exam-service:
build: ./services/exam-service
environment:
NACOS_SERVER_ADDR: nacos:8848
MYSQL_HOST: mysql
REDIS_HOST: redis
depends_on:
- mysql
- redis
- nacos
restart: always
# ============ 前端服务 ============
admin-web:
build: ./frontend/admin
# 构建后为静态文件,由 Nginx 托管
web-ssr:
build: ./frontend/web
environment:
NUXT_PUBLIC_API_BASE: https://edu.example.com/api
restart: always
# ============ 反向代理 ============
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl:/etc/nginx/ssl
- admin-build:/usr/share/nginx/html/admin
depends_on:
- gateway
- web-ssr
restart: always
volumes:
mysql-data:
redis-data:
admin-build:
5.4 后端 Dockerfile 多阶段构建
# 阶段1:构建
FROM maven:3.9-openjdk-17 AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests -q
# 阶段2:运行(使用精简镜像)
FROM openjdk:17-jdk-slim
WORKDIR /app
# 安装时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY --from=builder /build/target/*.jar app.jar
# JVM 参数优化
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
EXPOSE 8082
ENTRYPOINT exec java $JAVA_OPTS -jar app.jar
六、架构总结
回顾织码在线教育系统的技术架构,五个核心方案各有侧重:
| 技术方案 | 解决的核心问题 | 关键收益 |
|---|---|---|
| Spring Cloud 微服务 | 系统复杂度高、需独立扩容 | 服务解耦、独立部署、弹性扩容 |
| 多端统一 API | 四端数据一致性、维护成本 | 一套 API 驱动四端,降低 60%+ 后端维护成本 |
| SSE 实时通信 | 学习进度推送、考试提醒 | 轻量级实时推送,浏览器自动重连 |
| RSA+AES 混合加密 | 敏感数据传输安全 | RSA 保证密钥安全,AES 保证传输性能 |
| Docker+Nginx 容器化 | 部署一致性、运维效率 | 一键部署、环境隔离、快速扩缩容 |
这套架构已在企业新员工培训、合规安全培训、技能认证考试、知识付费等多种场景中稳定运行,支撑了课程管理、在线考试、培训任务、直播培训等全流程业务。
技术架构没有银弹,但务实的选择能让团队走得更远。如果你正在规划类似的企业级教育平台,希望本文的拆解能为你提供有价值的参考。
605

被折叠的 条评论
为什么被折叠?



