从课程创建到上架运营——织码在线教育系统课程管理模块技术实现详解

引言

课程管理是在线教育系统的核心模块,也是运营人员使用频率最高的功能。一个设计良好的课程管理系统,不仅要让运营人员"用得顺手",还要在底层保证课程数据的一致性、多媒体素材的可扩展性以及套餐组合的灵活性。

本文将深入拆解织码在线教育系统课程管理模块的技术实现,涵盖课程全生命周期管理、WangEditor 5 富文本编辑器集成、阿里云 VOD/OSS 多媒体素材库方案、课程套餐组合逻辑以及讲师主页展示,为正在开发类似系统的技术团队提供参考。


一、课程全生命周期管理

1.1 课程状态机设计

织码在线教育系统的课程管理覆盖完整的生命周期:创建 → 编辑 → 提交审核 → 上架 → 下架。每个状态变更都有明确的流转规则和权限控制。

课程状态流转图:

    ┌────────┐     ┌────────┐     ┌────────┐     ┌────────┐
    │ 草稿   │────▶│ 待审核 │────▶│ 已上架 │────▶│ 已下架 │
    │ DRAFT  │     │ REVIEW │     │ ONLINE │     │ OFFLINE│
    └────────┘     └────────┘     └────────┘     └────────┘
         │              │              │              │
         │              │  审核驳回     │    下架操作    │
         │              ▼              │              │
         │         ┌────────┐         │              │
         └────────▶│ 驳回   │─────────┘              │
                   │REJECTED│            重新上架     │
                   └────────┘───────────────────────▶│
                                                   (回到ONLINE)

在这里插入图片描述

1.2 课程数据模型

-- 课程主表
CREATE TABLE `edu_course` (
  `id` bigint NOT NULL COMMENT '课程ID',
  `title` varchar(200) NOT NULL COMMENT '课程标题',
  `cover_url` varchar(500) COMMENT '封面图URL',
  `intro` text COMMENT '课程简介',
  `detail` longtext COMMENT '课程详情(富文本HTML)',
  `category_id` bigint COMMENT '分类ID',
  `lecturer_id` bigint COMMENT '讲师ID',
  `price` decimal(10,2) DEFAULT 0 COMMENT '价格',
  `original_price` decimal(10,2) COMMENT '原价',
  `status` tinyint DEFAULT 0 COMMENT '状态: 0草稿 1待审核 2已上架 3已下架 4驳回',
  `sort` int DEFAULT 0 COMMENT '排序权重',
  `view_count` int DEFAULT 0 COMMENT '浏览量',
  `study_count` int DEFAULT 0 COMMENT '学习人次',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_status_sort` (`status`, `sort`),
  KEY `idx_category` (`category_id`),
  KEY `idx_lecturer` (`lecturer_id`)
) ENGINE=InnoDB COMMENT='课程主表';

-- 课程章节表
CREATE TABLE `edu_course_chapter` (
  `id` bigint NOT NULL,
  `course_id` bigint NOT NULL COMMENT '所属课程ID',
  `title` varchar(200) NOT NULL COMMENT '章节标题',
  `sort` int DEFAULT 0 COMMENT '排序',
  `media_type` tinyint COMMENT '媒体类型: 1视频 2图文 3文档',
  `media_id` varchar(100) COMMENT '媒体资源ID(VOD视频ID/OSS文件Key)',
  `duration` int COMMENT '时长(秒)',
  `is_free` tinyint DEFAULT 0 COMMENT '是否试看',
  PRIMARY KEY (`id`),
  KEY `idx_course_sort` (`course_id`, `sort`)
) ENGINE=InnoDB COMMENT='课程章节表';

1.3 课程发布流程

课程发布涉及多个校验环节,确保上架课程的完整性:

@Service
public class CoursePublishService {

    @Transactional
    public void publishCourse(Long courseId) {
        Course course = courseMapper.selectById(courseId);

        // 1. 基础信息校验
        validateCourseBasic(course);

        // 2. 章节完整性校验(至少一个已发布的章节)
        List<Chapter> chapters = chapterMapper.selectByCourseId(courseId);
        if (chapters.isEmpty()) {
            throw new BizException("请至少添加一个章节");
        }

        // 3. 媒体资源校验(视频必须转码完成)
        for (Chapter ch : chapters) {
            if (ch.getMediaType() == MediaType.VIDEO) {
                String vodStatus = vodService.getVideoStatus(ch.getMediaId());
                if (!"Normal".equals(vodStatus)) {
                    throw new BizException("章节「" + ch.getTitle() + "」视频未转码完成");
                }
            }
        }

        // 4. 状态流转:草稿/驳回 → 待审核
        course.setStatus(CourseStatus.REVIEW);
        courseMapper.updateById(course);

        // 5. 记录操作日志
        auditLogService.log("COURSE_PUBLISH", courseId, "提交课程审核");
    }
}

二、WangEditor 5 富文本编辑器集成

2.1 编辑器选型与集成

课程详情页需要富文本编辑能力,系统选择 WangEditor 5 作为编辑器,理由是:开箱即用、API 设计清晰、支持自定义扩展、中文文档完善。

// Admin 端 RichTextEditor 组件封装
<template>
  <div class="rich-editor">
    <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" />
    <Editor
      v-model="modelValue"
      :defaultConfig="editorConfig"
      @onCreated="handleCreated"
      @onChange="handleChange"
    />
  </div>
</template>

<script setup>
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { onBeforeUnmount, ref, shallowRef } from 'vue'

const props = defineProps({
  modelValue: { type: String, default: '' }
})
const emit = defineEmits(['update:modelValue'])

const editorRef = shallowRef()

// 自定义图片上传
const editorConfig = {
  MENU_CONF: {
    uploadImage: {
      async customUpload(file, insertFn) {
        // 1. 获取 OSS 上传签名
        const { uploadUrl, fileUrl } = await getOSSSignature(file.name)
        // 2. 上传到 OSS
        await fetch(uploadUrl, { method: 'PUT', body: file })
        // 3. 插入编辑器
        insertFn(fileUrl, file.name, fileUrl)
      }
    },
    uploadVideo: {
      async customUpload(file, insertFn) {
        // 1. 获取 VOD 上传凭证
        const { videoId, uploadAuth, uploadAddress } = await getVodUploadAuth(file.name)
        // 2. 通过 VOD SDK 上传
        await uploadToVod(file, uploadAuth, uploadAddress)
        // 3. 获取播放地址
        const { playUrl, coverUrl } = await getVodPlayInfo(videoId)
        insertFn(playUrl, coverUrl)
      }
    }
  }
}

onBeforeUnmount(() => {
  editorRef.value?.destroy()
})
</script>

2.2 内容安全过滤

富文本内容存储前进行 XSS 过滤,防止恶意脚本注入:

@Component
public class RichTextSanitizer {

    private static final Safelist SAFELIST = Safelist.relaxed()
            .addTags("video", "source", "iframe")
            .addAttributes("video", "src", "controls", "width", "height")
            .addAttributes("source", "src", "type")
            .addAttributes("iframe", "src", "width", "height", "frameborder")
            .addProtocols("iframe", "src", "https")
            .preserveRelativeLinks(false);

    public String sanitize(String html) {
        if (StringUtils.isBlank(html)) return "";
        return Jsoup.clean(html, SAFELIST);
    }
}

三、多媒体素材库:阿里云 VOD + OSS 方案

3.1 架构设计

课程内容涉及大量多媒体资源(视频、图片、文档),系统采用阿里云 VOD 管理视频、OSS 管理图片和文档的混合方案:

多媒体素材库架构:

┌──────────────┐      上传视频       ┌──────────────────┐
│  Admin 端    │───────────────────▶│  阿里云 VOD       │
│  编辑器/素材库│                    │  转码/截图/加密   │
│              │      上传图片/文档   ├──────────────────┤
│              │───────────────────▶│  阿里云 OSS       │
└──────┬───────┘                    │  对象存储         │
       │                            └──────────────────┘
       │  获取播放凭证/签名URL
       ▼
┌──────────────┐
│  后端服务    │  ← 素材元数据入库
│  资源服务    │
└──────────────┘

在这里插入图片描述

3.2 视频上传与播放流程

上传流程(客户端 → VOD → 服务端回调):

@RestController
@RequestMapping("/api/media/video")
public class VideoController {

    /**
     * 获取视频上传凭证
     */
    @PostMapping("/upload-auth")
    public Result<UploadAuthVO> getUploadAuth(@RequestBody UploadDTO dto) {
        // 1. 调用 VOD 获取上传凭证
        CreateUploadVideoRequest request = new CreateUploadVideoRequest(
            "织码教育", dto.getFileName(), "video/mp4"
        );
        CreateUploadVideoResponse response = vodClient.createUploadVideo(request);

        // 2. 预创建素材记录(状态:上传中)
        mediaService.preCreate(Media.builder()
            .type(MediaType.VIDEO)
            .name(dto.getFileName())
            .videoId(response.getVideoId())
            .status(MediaStatus.UPLOADING)
            .build());

        return Result.success(UploadAuthVO.builder()
            .videoId(response.getVideoId())
            .uploadAuth(response.getUploadAuth())
            .uploadAddress(response.getUploadAddress())
            .build());
    }

    /**
     * VOD 转码完成回调
     */
    @PostMapping("/callback")
    public String vodCallback(@RequestBody Map<String, String> body) {
        String videoId = body.get("VideoId");
        String status = body.get("Status");

        if ("success".equals(status)) {
            // 更新素材状态为可用
            mediaService.updateStatus(videoId, MediaStatus.READY);
            // 记录转码后的视频信息
            mediaService.updateVideoInfo(videoId,
                body.get("Duration"),
                body.get("CoverURL"));
        }
        return "success";
    }

    /**
     * 获取播放凭证(学员端播放视频时调用)
     */
    @GetMapping("/play-auth/{videoId}")
    public Result<PlayAuthVO> getPlayAuth(@PathVariable String videoId) {
        // 1. 权限校验:学员是否已购买/有权观看
        coursePermissionService.checkPlayPermission(currentUserId(), videoId);
        // 2. 获取播放凭证
        GetVideoPlayAuthResponse resp = vodClient.getVideoPlayAuth(videoId);
        return Result.success(PlayAuthVO.builder()
            .playAuth(resp.getPlayAuth())
            .videoId(videoId)
            .build());
    }
}

3.3 OSS 文件直传方案

图片和文档采用客户端直传 OSS + 服务端签名方案,减轻服务端带宽压力:

// 前端获取 OSS 上传签名
async function uploadToOSS(file) {
  // 1. 向服务端请求签名
  const { data } = await api.post('/api/media/oss/sign', {
    fileName: file.name,
    contentType: file.type
  })

  // 2. 直传 OSS
  const formData = new FormData()
  formData.append('key', data.key)
  formData.append('policy', data.policy)
  formData.append('OSSAccessKeyId', data.accessKeyId)
  formData.append('signature', data.signature)
  formData.append('file', file)

  await fetch(data.host, { method: 'POST', body: formData })

  // 3. 返回可访问 URL
  return `${data.host}/${data.key}`
}

在这里插入图片描述


四、课程套餐组合逻辑

4.1 套餐数据模型

课程套餐支持将多门课程打包销售,常用于"系列课""全栈课"等运营场景:

-- 课程套餐表
CREATE TABLE `edu_course_package` (
  `id` bigint NOT NULL,
  `title` varchar(200) NOT NULL COMMENT '套餐名称',
  `cover_url` varchar(500),
  `price` decimal(10,2) NOT NULL COMMENT '套餐价',
  `original_price` decimal(10,2) COMMENT '原价(各课程原价之和)',
  `status` tinyint DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

-- 套餐-课程关联表
CREATE TABLE `edu_package_course` (
  `id` bigint NOT NULL,
  `package_id` bigint NOT NULL,
  `course_id` bigint NOT NULL,
  `sort` int DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_package_course` (`package_id`, `course_id`)
) ENGINE=InnoDB;

4.2 套餐购买后的权限处理

学员购买套餐后,系统自动为其开通套餐内所有课程的学习权限:

@Service
public class PackageOrderService {

    @Transactional
    public void handlePackageOrderPaid(Order order) {
        // 1. 查询套餐包含的课程
        List<Long> courseIds = packageCourseMapper
            .selectCourseIdsByPackageId(order.getProductId());

        // 2. 批量开通课程权限
        List<UserCourse> userCourses = courseIds.stream().map(courseId ->
            UserCourse.builder()
                .userId(order.getUserId())
                .courseId(courseId)
                .source(OrderSource.PACKAGE)
                .orderId(order.getId())
                .expireTime(order.getExpireTime())
                .build()
        ).collect(Collectors.toList());

        userCourseMapper.batchInsert(userCourses);

        // 3. 更新套餐学习人数
        packageMapper.incrementStudyCount(order.getProductId());
    }
}

五、讲师主页展示

5.1 讲师数据模型

CREATE TABLE `edu_lecturer` (
  `id` bigint NOT NULL,
  `name` varchar(50) NOT NULL COMMENT '讲师姓名',
  `avatar` varchar(500) COMMENT '头像',
  `title` varchar(100) COMMENT '职称/头衔',
  `intro` text COMMENT '个人简介',
  `expertise` varchar(500) COMMENT '擅长领域',
  `course_count` int DEFAULT 0 COMMENT '课程数',
  `student_count` int DEFAULT 0 COMMENT '学员数',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='讲师表';

5.2 讲师主页 API 设计

讲师主页聚合展示讲师信息、课程列表和学员评价,采用一个聚合接口返回:

@RestController
@RequestMapping("/api/lecturer")
public class LecturerController {

    @GetMapping("/{id}")
    public Result<LecturerHomeVO> getLecturerHome(@PathVariable Long id) {
        LecturerHomeVO vo = new LecturerHomeVO();

        // 1. 讲师基本信息
        vo.setLecturer(lecturerMapper.selectById(id));

        // 2. 讲师课程列表(已上架)
        vo.setCourses(courseMapper.selectByLecturerAndStatus(
            id, CourseStatus.ONLINE));

        // 3. 统计数据
        vo.setStats(lecturerStatsService.getStats(id));

        return Result.success(vo);
    }
}

学员端支持关注讲师功能,关注后可在个人中心接收讲师新课程动态推送。


六、前端组件设计

6.1 课程管理列表页

Admin 端课程管理列表采用 Element Plus Table + 组合式 API:

<template>
  <div class="course-list">
    <!-- 筛选区 -->
    <el-form :inline="true" :model="filters">
      <el-form-item label="课程名称">
        <el-input v-model="filters.keyword" placeholder="搜索课程" />
      </el-form-item>
      <el-form-item label="状态">
        <el-select v-model="filters.status" clearable>
          <el-option label="草稿" :value="0" />
          <el-option label="已上架" :value="2" />
          <el-option label="已下架" :value="3" />
        </el-select>
      </el-form-item>
      <el-button type="primary" @click="fetchList">查询</el-button>
    </el-form>

    <!-- 操作栏 -->
    <div class="actions">
      <el-button type="primary" @click="goCreate">新建课程</el-button>
      <el-button @click="batchPublish">批量上架</el-button>
    </div>

    <!-- 表格 -->
    <el-table :data="list" v-loading="loading" @selection-change="onSelect">
      <el-table-column type="selection" width="50" />
      <el-table-column label="封面" width="120">
        <template #default="{ row }">
          <el-image :src="row.coverUrl" class="cover-thumb" />
        </template>
      </el-table-column>
      <el-table-column prop="title" label="课程名称" min-width="200" />
      <el-table-column prop="lecturerName" label="讲师" width="120" />
      <el-table-column prop="studyCount" label="学习人次" width="100" />
      <el-table-column label="状态" width="100">
        <template #default="{ row }">
          <el-tag :type="statusTag(row.status)">
            {{ statusText(row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="200" fixed="right">
        <template #default="{ row }">
          <el-button text @click="goEdit(row.id)">编辑</el-button>
          <el-button text type="primary" @click="handlePublish(row)">
            {{ row.status === 2 ? '下架' : '上架' }}
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <el-pagination
      v-model:current-page="page.current"
      v-model:page-size="page.size"
      :total="page.total"
      @current-change="fetchList"
    />
  </div>
</template>

七、总结

织码在线教育系统的课程管理模块在技术实现上重点解决了三个问题:

  1. 生命周期管理:通过状态机模型清晰管理课程从创建到下架的全流程,每一步都有校验和日志
  2. 多媒体素材管理:VOD + OSS 混合方案兼顾视频转码和文件存储需求,客户端直传减轻服务端压力
  3. 运营灵活性:套餐组合、讲师主页等功能为运营提供了丰富的课程运营手段

后续文章我们将继续拆解题库与考试系统、微服务架构等核心模块的技术实现。如果你对某个技术点有疑问,欢迎评论区交流。

如需私有化部署报价、远程产品演示,可访问官网https://www.weavecodes.com/,私信作者领取企业落地案例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值