硅谷课堂-智慧星球 Day 5~Day 8——尚硅谷项目笔记 2022 年

硅谷课堂-智慧星球 Day 5~Day 8——尚硅谷项目笔记 2022 年

文章目录

Day 5-教师管理模块前端

一、设置路由定义

1、修改路由

修改 src/router/index.js 文件,重新定义 constantRouterMap。

注意: 每个路由的 name 不能相同。

import Vue from "vue";
import Router from "vue-router";

Vue.use(Router);

/* Layout */
import Layout from "@/layout";

/**
 * Note: sub-menu only appear when route children.length >= 1
 * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
 *
 * hidden: true                   if set true, item will not show in the sidebar(default is false)
 * alwaysShow: true               if set true, will always show the root menu
 *                                if not set alwaysShow, when item has more than one children route,
 *                                it will becomes nested mode, otherwise not show the root menu
 * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb
 * name:'router-name'             the name is used by <keep-alive> (must set!!!)
 * meta : {
    roles: ['admin','editor']    control the page roles (you can set multiple roles)
    title: 'title'               the name show in sidebar and breadcrumb (recommend set)
    icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true)
    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set
  }
 */

/**
 * constantRoutes
 * a base page that does not have permission requirements
 * all roles can be accessed
 */
export const constantRoutes = [
  {
    path: "/login",
    component: () => import("@/views/login/index"),
    hidden: true,
  },

  {
    path: "/404",
    component: () => import("@/views/404"),
    hidden: true,
  },

  // 首页
  {
    path: "/",
    component: Layout,
    redirect: "/dashboard",
    children: [
      {
        path: "dashboard",
        name: "Dashboard",
        component: () => import("@/views/dashboard/index"),
        // meta: { title: "Dashboard", icon: "dashboard" },
        meta: { title: "智慧星球后台管理系统", icon: "dashboard" },
      },
    ],
  },

  // 教师管理
  {
    path: "/vod",
    component: Layout,
    redirect: "/vod/teacher/list",
    name: "vod",
    meta: { title: "教师管理", icon: "el-icon-s-help" },
    alwaysShow: true,
    children: [
      {
        path: "teacher/list",
        name: "TeacherList",
        component: () => import("@/views/vod/teacher/list"),
        meta: { title: "教师列表", icon: "table" },
      },
      {
        path: "teacher/create",
        name: "teacherCreate",
        component: () => import("@/views/vod/teacher/form"),
        meta: { title: "添加教师", icon: "tree" },
      },
      {
        path: "teacher/edit/:id",
        name: "TeacherEdit",
        component: () => import("@/views/vod/teacher/form"),
        meta: { title: "编辑教师" },
        hidden: true,
      },
    ],
  },

  {
    path: "external-link",
    component: Layout,
    children: [
      {
        path: "https://github.com/MYXHcode",
        meta: { title: "联系作者", icon: "link" },
      },
    ],
  },

  /*
  {
    path: "/example",
    component: Layout,
    redirect: "/example/table",
    name: "Example",
    meta: { title: "Example", icon: "el-icon-s-help" },
    children: [
      {
        path: "table",
        name: "Table",
        component: () => import("@/views/table/index"),
        meta: { title: "Table", icon: "table" },
      },
      {
        path: "tree",
        name: "Tree",
        component: () => import("@/views/tree/index"),
        meta: { title: "Tree", icon: "tree" },
      },
    ],
  },
   */

  /*
  {
    path: "/form",
    component: Layout,
    children: [
      {
        path: "index",
        name: "Form",
        component: () => import("@/views/form/index"),
        meta: { title: "Form", icon: "form" },
      },
    ],
  },
   */

  /*
  {
    path: "/nested",
    component: Layout,
    redirect: "/nested/menu1",
    name: "Nested",
    meta: {
      title: "Nested",
      icon: "nested",
    },
    children: [
      {
        path: "menu1",
        component: () => import("@/views/nested/menu1/index"), // Parent router-view
        name: "Menu1",
        meta: { title: "Menu1" },
        children: [
          {
            path: "menu1-1",
            component: () => import("@/views/nested/menu1/menu1-1"),
            name: "Menu1-1",
            meta: { title: "Menu1-1" },
          },
          {
            path: "menu1-2",
            component: () => import("@/views/nested/menu1/menu1-2"),
            name: "Menu1-2",
            meta: { title: "Menu1-2" },
            children: [
              {
                path: "menu1-2-1",
                component: () =>
                  import("@/views/nested/menu1/menu1-2/menu1-2-1"),
                name: "Menu1-2-1",
                meta: { title: "Menu1-2-1" },
              },
              {
                path: "menu1-2-2",
                component: () =>
                  import("@/views/nested/menu1/menu1-2/menu1-2-2"),
                name: "Menu1-2-2",
                meta: { title: "Menu1-2-2" },
              },
            ],
          },
          {
            path: "menu1-3",
            component: () => import("@/views/nested/menu1/menu1-3"),
            name: "Menu1-3",
            meta: { title: "Menu1-3" },
          },
        ],
      },
      {
        path: "menu2",
        component: () => import("@/views/nested/menu2/index"),
        name: "Menu2",
        meta: { title: "menu2" },
      },
    ],
  },
   */

  /*
  {
    path: "external-link",
    component: Layout,
    children: [
      {
        path: "https://panjiachen.github.io/vue-element-admin-site/#/",
        meta: { title: "External Link", icon: "link" },
      },
    ],
  },
   */

  // 404 page must be placed at the end !!!
  { path: "*", redirect: "/404", hidden: true },
];

const createRouter = () =>
  new Router({
    // mode: 'history', // require service support
    scrollBehavior: () => ({ y: 0 }),
    routes: constantRoutes,
  });

const router = createRouter();

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter();
  router.matcher = newRouter.matcher; // reset router
}

export default router;
2、创建 vue 组件

在 src/views 文件夹下创建以下文件夹和文件。

创建 vue 组件

3、form.vue
<template>
  <div class="app-container">
    <h1>教师表单</h1>
  </div>
</template>
4、list.vue
<template>
  <div class="app-container">
    <h1>教师列表</h1>
  </div>
</template>

二、教师分页列表

1、定义 api

创建文件 src/api/vod/teacher.js。

import request from "@/utils/request";

const TEACHER_API = "/admin/vod/teacher";

export default {
  /**
   * 条件查询教师分页
   *
   * @param {number} current   - 当前页码
   * @param {number} limit     - 每页记录数
   * @param {Object} searchObj - 查询对象
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  teacherListPage(current, limit, searchObj) {
    return request({
      url: `${TEACHER_API}/find/query/page/${current}/${limit}`,
      method: "post",

      /*
      使用参数格式传递,写法是 params:searchObj
      使用 json 格式传递,写法是 data:searchObj
       */
      data: searchObj,
    });
  },
};
2、初始化 vue 组件

src/views/vod/teacher/list.vue

<template>
  <div class="app-container">
    <h1>教师列表</h1>
  </div>
</template>

<script>
  // 引入定义接口的 js 文件
  import teacherAPI from "@/api/vod/teacher";

  export default {
    // 初始值
    data() {
      return {};
    },

    //页面渲染之前
    created() {
      this.fetchData();
    },

    // 具体方法
    methods: {
      fetchData() {},
    },
  };
</script>
3、定义 data
// 初始值
data() {
  return {
    // 教师列表
    list: [],
    // 总记录数
    total: 0,
    // 当前页码
    page: 1,
    // 每页记录数
    limit: 10,
    // 查询对象
    searchObj: {},
    // 批量删除选中的记录列表
    multipleSelection: [],
  };
},
4、定义 methods
methods: {
  fetchData() {
    // 验证开始时间和结束时间的合法性
    if (!this.validateDateRange()) {
      return;
    }

    // 调用 API,进行 ajax 请求
    teacherAPI
    .teacherListPage(this.page, this.limit, this.searchObj)
    .then((response) => {
      this.list = response.data.records;
      this.total = response.data.total;
    });
  },
}
5、表格渲染
<!-- 表格 -->
<el-table :data="list" border stripe @selection-change="handleSelectionChange">
  <el-table-column type="selection" />
  <el-table-column label="序号" width="50">
    <template slot-scope="scope">
      {{ (page - 1) * limit + scope.$index + 1 }}
    </template>
  </el-table-column>
  <el-table-column prop="name" label="名称" width="80" />
  <el-table-column label="头衔" width="90">
    <template slot-scope="scope">
      <el-tag v-if="scope.row.level === 1" type="success" size="mini"
        >高级教师</el-tag
      >
      <el-tag v-if="scope.row.level === 0" size="mini">首席教师</el-tag>
    </template>
  </el-table-column>
  <el-table-column prop="intro" label="简介" />
  <el-table-column prop="sort" label="排序" width="60" />
  <el-table-column prop="joinDate" label="入驻时间" width="160" />
  <el-table-column label="操作" width="200" align="center">
    <template slot-scope="scope">
      <el-button type="text" size="mini" @click="removeById(scope.row.id)"
        >删除</el-button
      >
      <router-link :to="'/vod/teacher/edit/' + scope.row.id">
        <el-button type="text" size="mini">修改</el-button>
      </router-link>
    </template>
  </el-table-column>
</el-table>
6、分页组件
<!-- 分页组件 -->
<el-pagination
  :current-page="page"
  :total="total"
  :page-size="limit"
  :page-sizes="[5, 10, 20, 30, 40, 50, 100]"
  style="padding: 30px 0; text-align: center"
  layout="total, sizes, prev, pager, next, jumper"
  @size-change="changePageSize"
  @current-change="changeCurrentPage"
/>
7、顶部查询表单
<!--查询表单-->
<el-card class="operate-container" shadow="never">
  <el-form :inline="true" class="demo-form-inline">
    <el-form-item label="名称">
      <el-input v-model="searchObj.name" placeholder="教师名称" />
    </el-form-item>

    <el-form-item label="头衔">
      <el-select v-model="searchObj.level" clearable placeholder="头衔">
        <el-option value="1" label="高级教师" />
        <el-option value="0" label="首席教师" />
      </el-select>
    </el-form-item>

    <el-form-item label="入驻时间">
      <el-date-picker
        v-model="searchObj.joinDateBegin"
        placeholder="开始时间"
        value-format="yyyy-MM-dd"
      />
    </el-form-item>
    <el-form-item label="-">
      <el-date-picker
        v-model="searchObj.joinDateEnd"
        placeholder="结束时间"
        value-format="yyyy-MM-dd"
      />
    </el-form-item>

    <el-button type="primary" icon="el-icon-search" @click="fetchData()"
      >查询</el-button
    >
    <el-button type="default" @click="resetData()">清空</el-button>
  </el-form>
</el-card>

验证开始时间和结束时间的合法性的方法。

// 验证开始时间和结束时间的合法性
validateDateRange() {
  if (
    this.searchObj.joinDateBegin &&
    this.searchObj.joinDateEnd &&
    this.searchObj.joinDateBegin > this.searchObj.joinDateEnd
  ) {
    this.$message.error("开始时间不能晚于结束时间");
    return false;
  }
  return true;
},

清空和分页方法。

// 清空表单
resetData() {
  this.searchObj = {};
  this.fetchData();
},

// 改变每页显示的记录数,size:回调参数,表示当前选中的“每页条数”
changePageSize(size) {
  this.limit = size;
  this.fetchData();
},

// 改变页码数,page:回调参数,表示当前选中的“页码”
changeCurrentPage(page) {
  this.page = page;
  this.fetchData();
},

三、教师删除

1、定义 api

src/api/vod/teacher.js

import request from "@/utils/request";

const TEACHER_API = "/admin/vod/teacher";

export default {
  /**
   * 逻辑删除教师
   *
   * @param {number} id - id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  removeTeacherById(id) {
    return request({
      url: `${TEACHER_API}/remove/${id}`,
      method: "delete",
    });
  },
};
2、定义 methods

src/views/vod/teacher/list.vue

使用 MessageBox 弹框组件。

// 逻辑删除教师
removeById(id) {
  this.$confirm("此操作将删除该教师信息, 是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    // 调用接口删除
    teacherAPI.removeTeacherById(id).then((response) => {
      // 提示信息
      this.$message({
        type: "success",
        message: "删除成功!",
      });
      // 刷新页面
      this.fetchData();
    });
  });
},

四、教师新增

1、定义 api

src/api/vod/teacher.js

import request from "@/utils/request";

const TEACHER_API = "/admin/vod/teacher";

export default {
  /**
   * 添加教师
   *
   * @param {Object} teacher - 教师数据
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  saveTeacher(teacher) {
    return request({
      url: `${TEACHER_API}/save`,
      method: "post",
      data: teacher,
    });
  },
};
2、初始化组件

src/views/vod/teacher/form.vue

<template>
  <div class="app-container">
    <h1>教师表单</h1>

    <!-- 输入表单 -->
    <el-form label-width="120px">
      <el-form-item label="教师名称">
        <el-input v-model="teacher.name" />
      </el-form-item>
      <el-form-item label="入驻时间">
        <el-date-picker v-model="teacher.joinDate" value-format="yyyy-MM-dd" />
      </el-form-item>
      <el-form-item label="教师排序">
        <el-input-number v-model="teacher.sort" :min="0" />
      </el-form-item>
      <el-form-item label="教师头衔">
        <el-select v-model="teacher.level">
          <!--
            数据类型一定要和取出的 json 中的一致,否则没法回填
            因此,这里 value 使用动态绑定的值,保证其数据类型是 number
            -->
          <el-option :value="1" label="高级教师" />
          <el-option :value="0" label="首席教师" />
        </el-select>
      </el-form-item>
      <el-form-item label="教师简介">
        <el-input v-model="teacher.intro" />
      </el-form-item>
      <el-form-item label="教师资历">
        <el-input v-model="teacher.career" :rows="10" type="textarea" />
      </el-form-item>

      <!-- 教师头像 -->
      <el-form-item label="教师头像"> </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="saveOrUpdate()">保存</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>
3、实现新增功能
<script>
import teacherAPI from "@/api/vod/teacher";

export default {
  data() {
    return {
      teacher: {
        // 初始化教师默认数据
        sort: 0,
        level: 1,
      },

      // 保存按钮是否禁用,防止表单重复提交
      saveBtnDisabled: false,
    };
  },

  created() {},

  methods: {
    // 添加教师
    save() {
      teacherAPI.saveTeacher(this.teacher).then((response) => {
        // 提示信息
        this.$message({
          type: "success",
          message: "添加成功!",
        });

        // 跳转列表页面
        this.$router.push({ path: "/vod/teacher/list" });
      });
    },

    // 修改教师
    update() {},

    // 添加或修改教师
    saveOrUpdate() {
      // 禁用保存按钮
      this.saveBtnDisabled = true;

      if (!this.teacher.id) {
        // 教师数据中没有 id,添加
        if (!this.teacher.name) {
          this.$message.error("请输入教师名称");
          this.saveBtnDisabled = false;
          return;
        }

        this.save();
      } else {
        // 教师数据中有 id,修改
        this.update();
      }
    },
  },
};
</script>

五、教师修改-数据回显

1、定义 api

src/api/vod/teacher.js

import request from "@/utils/request";

const TEACHER_API = "/admin/vod/teacher";

export default {
  /**
   * 根据 id 查询教师
   *
   * @param {number} id - id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getTeacherById(id) {
    return request({
      url: `${TEACHER_API}/get/${id}`,
      method: "get",
    });
  },
};
2、组件中调用 api

methods 中定义 fetchDataById。

// 根据 id 查询教师
fetchDataById(id) {
    teacherAPI.getTeacherById(id).then((response) => {
    this.teacher = response.data;
  });
},
3、页面渲染前调用 fetchDataById
created() {
  // 获取路径中的 id 值,根据 id 查询得到数据,进行回显
  if (this.$route.params.id) {
    const id = this.$route.params.id;
    this.fetchDataById(id);
  }
},

六、教师修改-更新

1、定义 api
import request from "@/utils/request";

const TEACHER_API = "/admin/vod/teacher";

export default {
  /**
   * 修改教师
   *
   * @param {Object} teacher - 教师数据
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  updateTeacher(teacher) {
    return request({
      url: `${TEACHER_API}/update`,
      method: "post",
      data: teacher,
    });
  },
};
2、组件中调用 api

methods 中定义 updateData。

// 修改教师
update() {
  teacherAPI.updateTeacher(this.teacher).then((response) => {
  // 提示信息
  this.$message({
      type: "success",
      message: "修改成功!",
  });

  // 跳转列表页面
  this.$router.push({ path: "/vod/teacher/list" });
  });
},
3、完善 saveOrUpdate 方法
// 添加或修改教师
saveOrUpdate() {
  // 禁用保存按钮
  this.saveBtnDisabled = true;

  if (!this.teacher.id) {
    // 教师数据中没有 id,添加
    if (!this.teacher.name) {
      this.$message.error("请输入教师名称");
      this.saveBtnDisabled = false;
      return;
    }

    this.save();
  } else {
    // 教师数据中有 id,修改
    this.update();
  }
},

七、教师批量删除

1、定义 api

src/api/vod/teacher.js

import request from "@/utils/request";

const TEACHER_API = "/admin/vod/teacher";

export default {
  /**
   * 批量删除教师
   *
   * @param {Array}idList id 数组,Json 数组 [1,2,3,...]
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  removeBatchTeacher(idList) {
    return request({
      url: `${TEACHER_API}/remove/batch`,
      method: `delete`,
      data: idList,
    });
  },
};
2、初始化组件

src/views/vod/teacher/list.vue

在 table 组件上添加 批量删除按钮。

<!-- 工具按钮 -->
<el-card class="operate-container" shadow="never">
  <i class="el-icon-tickets" style="margin-top: 5px"></i>
  <span style="margin-top: 5px">数据列表</span>
  <el-button class="btn-add" @click="add()" style="margin-left: 10px"
    >添加</el-button
  >
  <el-button class="btn-add" @click="batchRemove()">批量删除</el-button>
</el-card>

在 table 组件上添加复选框

<!-- 表格 -->
<el-table :data="list" border stripe @selection-change="handleSelectionChange">
  <el-table-column type="selection" />
  <el-table-column label="序号" width="50">
    <template slot-scope="scope">
      {{ (page - 1) * limit + scope.$index + 1 }}
    </template>
  </el-table-column>
  <el-table-column prop="name" label="名称" width="80" />
  <el-table-column label="头衔" width="90">
    <template slot-scope="scope">
      <el-tag v-if="scope.row.level === 1" type="success" size="mini"
        >高级教师</el-tag
      >
      <el-tag v-if="scope.row.level === 0" size="mini">首席教师</el-tag>
    </template>
  </el-table-column>
  <el-table-column prop="intro" label="简介" />
  <el-table-column prop="sort" label="排序" width="60" />
  <el-table-column prop="joinDate" label="入驻时间" width="160" />
  <el-table-column label="操作" width="200" align="center">
    <template slot-scope="scope">
      <el-button type="text" size="mini" @click="removeById(scope.row.id)"
        >删除</el-button
      >
      <router-link :to="'/vod/teacher/edit/' + scope.row.id">
        <el-button type="text" size="mini">修改</el-button>
      </router-link>
    </template>
  </el-table-column>
</el-table>
3、实现功能

data 定义数据

// 初始值
data() {
  return {
    // 教师列表
    list: [],
    // 总记录数
    total: 0,
    // 当前页码
    page: 1,
    // 每页记录数
    limit: 10,
    // 查询对象
    searchObj: {},
    // 批量删除选中的记录列表
    multipleSelection: [],
  };
},

完善方法。

// 跳转到添加表单页面
add() {
  this.$router.push({ path: "/vod/teacher/create" });
},

// 复选框发生变化,调用方法,选中复选框行的内容传递
handleSelectionChange(selection) {
  this.multipleSelection = selection;
  // console.log(this.multipleSelection);
},

// 批量删除教师
batchRemove() {
  // 判断非空
  if (this.multipleSelection.length === 0) {
    this.$message.warning("请选择要删除的记录!");
    return;
  }

  this.$confirm("此操作将删除该教师信息, 是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    let idList = [];

    // 遍历数组 multipleSelection
    this.multipleSelection.forEach((item) => {
      // 放到数组 idList
      idList.push(item.id);
    });

    // 调用接口批量删除
    teacherAPI.removeBatchTeacher(idList).then((response) => {
      // 提示信息
      this.$message({
        type: "success",
        message: "删除成功!",
      });
      // 刷新页面
      this.fetchData();
    });
  });
},

Day 6-整合腾讯云对象存储和课程分类管理

一、教师管理模块整合腾讯云对象存储

1、腾讯云对象存储介绍

腾讯云对象存储介绍

1.1、开通“对象存储 COS”服务

(1)申请腾讯云账号:https://cloud.tencent.com/

(2)实名认证。

(3)开通“对象存储 COS”服务。

(4)进入管理控制台。

开通“对象存储 COS”服务

1.2、创建 Bucket

进入管理控制台,找到存储桶列表, 创建存储桶。

创建 Bucket

输入桶名称,选择:公有读取,其他默认。

创建 Bucket

点击桶名称,进入详情页,可测试上传文件。

创建 Bucket

1.3、创建 API 秘钥

进入 API 秘钥管理。

创建 API 秘钥

新建秘钥。

创建 API 秘钥

1.4、快速入门

参考文档:https://cloud.tencent.com/document/product/436/10199

引入依赖。

<!--腾讯云对象存储(Cloud Object Storage,COS) -->
<dependency>
    <groupId>com.qcloud</groupId>
    <artifactId>cos_api</artifactId>
    <version>5.6.173</version>
</dependency>

测试上传。

package com.myxh.smart.planet;

import com.alibaba.fastjson.JSON;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.http.HttpProtocol;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.PutObjectResult;
import com.qcloud.cos.region.Region;
import org.apache.ibatis.javassist.LoaderClassPath;

import java.io.File;
import java.net.URI;

/**
 * @author MYXH
 * @date 2023/10/5
 */
public class TestCOS
{
    public static void main(String[] args)
    {
        // 1、初始化用户身份信息(secretId, secretKey)
        // SECRETID 和 SECRETKEY 请登录访问管理控制台 https://console.cloud.tencent.com/cam/capi 进行查看和管理
        // 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
        String secretId = "AKIDeOxIPH0VlnaBYgAQUKvIfqmUyFI7kAPS";

        // 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
        String secretKey = "wfFqEQPDpEQPDpEQYWHpBMNBmCRUKxVr";
        COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);

        // 2、设置 bucket 的地域, COS 地域的简称请参见 https://cloud.tencent.com/document/product/436/6224
        // clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分
        Region region = new Region("ap-beijing");
        ClientConfig clientConfig = new ClientConfig(region);

        // 这里建议设置使用 https 协议
        // 从 5.6.54 版本开始,默认使用了 https
        clientConfig.setHttpProtocol(HttpProtocol.https);

        // 3、生成 cos 客户端
        COSClient cosClient = new COSClient(cred, clientConfig);

        // 4、测试上传
        try
        {
            // 指定要上传的文件
            URI uri = LoaderClassPath.class.getResource("/image/大户爱.png").toURI();
            File localFile = new File(uri);

            // 指定文件将要存放的存储桶
            String bucketName = "smart-planet-1315007088";

            // 指定文件上传到 COS 上的路径,即对象键。例如对象键为 folder/picture.jpg,则表示将文件 picture.jpg 上传到 folder 路径下
            String key = "TestCOS/image/大户爱.png";
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, localFile);
            PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);

            System.out.println(JSON.toJSONString(putObjectResult));
        } catch (Exception clientException)
        {
            clientException.printStackTrace();
        }
    }
}
2、整合腾讯云对象存储
2.1、service-vod 模块引入依赖
<!--腾讯云对象存储(Cloud Object Storage,COS) -->
<dependency>
    <groupId>com.qcloud</groupId>
    <artifactId>cos_api</artifactId>
    <version>5.6.173</version>
</dependency>

<!-- 日期时间工具 -->
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
</dependency>
2.2、配置 application.properties

添加如下内容:

# 设置上传文件的大小
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB

# 设置初始化用户身份信息
tencent.cos.file.secretid=AKIDeOxIPH0VlnaBYgAQUKvIfqmUyFI7kAPS
tencent.cos.file.secretkey=wfFqEQPDpEQPDpEQYWHpBMNBmCRUKxVr

# 设置 bucket 的地域
tencent.cos.file.region=ap-beijing

# 设置指定文件将要存放的存储桶
tencent.cos.file.bucketname=smart-planet-1315007088
3.3、创建工具类
package com.myxh.smart.planet.vod.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @author MYXH
 * @date 2023/10/5
 * @description 常量类,读取配置文件 application.properties 中的配置
 */
@Component
public class ConstantPropertiesUtil implements InitializingBean
{
    @Value("${tencent.cos.file.secretid}")
    private String secretId;

    @Value("${tencent.cos.file.secretkey}")
    private String secretKey;

    @Value("${tencent.cos.file.region}")
    private String region;

    @Value("${tencent.cos.file.bucketname}")
    private String bucketName;

    public static String ACCESS_KEY_ID;
    public static String ACCESS_KEY_SECRET;
    public static String END_POINT;
    public static String BUCKET_NAME;

    @Override
    public void afterPropertiesSet() throws Exception
    {
        ACCESS_KEY_ID = secretId;
        END_POINT = region;
        ACCESS_KEY_SECRET = secretKey;
        BUCKET_NAME = bucketName;
    }
}
3.4、创建 Service

创建 Interface:FileService.java。

package com.myxh.smart.planet.vod.service;

import org.springframework.web.multipart.MultipartFile;

/**
 * @author MYXH
 * @date 2023/10/5
 */
public interface FileService
{
    /**
     * 上传文件
     *
     * @param file 文件
     * @return url 文件地址
     */
    String upload(MultipartFile file);
}

实现:FileServiceImpl.java。

创建 Service

package com.myxh.smart.planet.vod.service.impl;

import com.myxh.smart.planet.vod.service.FileService;
import com.myxh.smart.planet.vod.utils.ConstantPropertiesUtil;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.http.HttpProtocol;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.PutObjectResult;
import com.qcloud.cos.region.Region;
import org.joda.time.DateTime;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

/**
 * @author MYXH
 * @date 2023/10/5
 */
@Service
public class FileServiceImpl implements FileService
{
    /**
     * 上传文件
     *
     * @param file 文件
     * @return url 文件地址
     */
    @Override
    public String upload(MultipartFile file)
    {
        // 1、初始化用户身份信息(secretId, secretKey)
        // SECRETID 和 SECRETKEY 请登录访问管理控制台 https://console.cloud.tencent.com/cam/capi 进行查看和管理
        // 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
        String secretId = ConstantPropertiesUtil.ACCESS_KEY_ID;

        // 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
        String secretKey = ConstantPropertiesUtil.ACCESS_KEY_SECRET;
        COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);

        // 2、设置 bucket 的地域, COS 地域的简称请参见 https://cloud.tencent.com/document/product/436/6224
        // clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分
        Region region = new Region(ConstantPropertiesUtil.END_POINT);
        ClientConfig clientConfig = new ClientConfig(region);

        // 这里建议设置使用 https 协议
        // 从 5.6.54 版本开始,默认使用了 https
        clientConfig.setHttpProtocol(HttpProtocol.https);

        // 3、生成 cos 客户端
        COSClient cosClient = new COSClient(cred, clientConfig);

        try
        {
            // 4、上传文件
            // 存储桶的命名格式为 BucketName-APPID,此处填写的存储桶名称必须为此格式
            String bucketName = ConstantPropertiesUtil.BUCKET_NAME;

            // 对象键(Key)是对象在存储桶中的唯一标识
            // 在文件名称前面添加 uuid 值
            String key = UUID.randomUUID().toString().replaceAll("-", "")
                    + file.getOriginalFilename();

            // 在文件名称前面添加日期时间格式文件夹
            String dateTime = new DateTime().toString("yyyy/MM/dd");
            key = "ProductionCOS/image/" + dateTime + "/" + key;

            System.out.println("key = " + key);

            // 获取上传文件的输入流
            InputStream inputStream = file.getInputStream();

            ObjectMetadata objectMetadata = new ObjectMetadata();

            // 指定文件上传到 COS 上的路径,即对象键
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream, objectMetadata);

            // 执行文件上传
            PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);

            // 返回上传文件路径
            String url = "https://" + bucketName + "." + "cos" + "." + ConstantPropertiesUtil.END_POINT + ".myqcloud.com" + "/" + key;

            return url;

        } catch (CosClientException e)
        {
            e.printStackTrace();
        } catch (IOException e)
        {
            throw new RuntimeException(e);
        }

        return null;
    }
}
3.5、创建 Controller

FileUploadController.java

package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vod.service.FileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 * @author MYXH
 * @date 2023/10/5
 */
@Tag(name = "文件接口", description = "文件上传接口")
@RestController
@RequestMapping("/admin/vod/file")
@CrossOrigin
public class FileUploadController
{
    @Autowired
    private FileService fileService;

    /**
     * 上传文件
     *
     * @param file 文件
     * @return url 文件地址
     */
    @Operation(summary = "上传", description = "上传文件")
    @PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Result<String> uploadFile(@RequestParam("file") MultipartFile file)
    {
        String url = fileService.upload(file);

        return Result.ok(url).message("上传文件成功");
    }
}
3、添加教师前端完善
3.1、添加上传组件

操作 teacher 目录下的 form.vue 页面。

<!-- 教师头像 -->
<el-form-item label="教师头像">
  <el-upload
    :show-file-list="false"
    :on-success="handleAvatarSuccess"
    :before-upload="beforeAvatarUpload"
    :on-error="handleAvatarError"
    :action="BASE_API + '/admin/vod/file/upload'"
    class="avatar-uploader"
  >
    <img
      v-if="teacher.avatar"
      :src="teacher.avatar"
      style="width: 200px; height: auto"
    />
    <i v-else class="el-icon-plus avatar-uploader-icon" />
  </el-upload>
</el-form-item>
3.2、添加上传方法

初始化访问路径。

data() {
  return {
    teacher: {
      // 初始化教师默认数据
      sort: 0,
      level: 1,
    },

    BASE_API: "http://localhost:8301",
  };
},

添加上传操作方法。

// 上传成功回调
handleAvatarSuccess(response, file) {
  if (response.code == 20000) {
    // console.log(response);
    this.teacher.avatar = response.data;
    // 强制重新渲染
    this.$forceUpdate();
  } else {
    this.$message.error("上传失败");
  }
},

// 上传校验
beforeAvatarUpload(file) {
  const isJPG = file.type === "image/jpeg";
  const isPNG = file.type === "image/png";
  const isLt2M = file.size / 1024 / 1024 < 2;

  if (!isJPG && !isPNG) {
    this.$message.error("上传头像图片只能是 JPG 或 PNG 格式!");
  }
  if (!isLt2M) {
    this.$message.error("上传头像图片大小不能超过 2MB!");
  }

  return (isJPG || isPNG) && isLt2M;
},

// 错误处理
handleAvatarError() {
  console.log("error");
  this.$message.error("上传失败(http 失败)");
},
3.3 添加显示头像组件

操作 teacher 目录下的 list.vue 页面。

<el-table-column label="头像" width="80">
  <template slot-scope="scope">
    <img
      v-if="scope.row.avatar"
      :src="scope.row.avatar"
      style="
              width: 50px;
              height: 50px;
              border-radius: 50%;
              object-fit: cover;
            "
    />
  </template>
</el-table-column>
3.4 设置 referrer,正常显示图片

在 teacher 目录下的 list.vue 和 form.vue 页面,设置 referrer 为 no-referrer,用于绕过防盗链限制,从而正常显示部分使用了外链存储的图片,达到节省 Bucket 存储桶服务器流量的目的。

<head>
  <!-- 设置 referrer 为 no-referrer,用于绕过防盗链限制,从而正常显示图片 -->
  <meta name="referrer" content="no-referrer" />
</head>

二、后台管理系统-课程分类管理模块

1、课程分类管理模块需求

(1)课程分类列表功能。

课程分类列表功能

(2)课程分类导入功能。

课程分类导入功能

(3)课程分类导出功能。

课程分类导出功能

2、课程分类数据库设计

(1)创建课程分类表 subject。

创建课程分类表 subject

(2)课程分类表结构分析

课程分类表结构分析

3、功能实现-课程分类列表
3.1、接口实现分析

课程分类采用树形展示,使用“树形数据与懒加载”的方式展现数据列表,因此需要提供的接口如下:根据上级 id 获取下级数据,参考 element-ui 文档:https://element.eleme.cn/#/zh-CN/component/table ,页面搜索:树形数据与懒加载。

接口实现分析

接口实现分析

3.2、编写 SubjectController
package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.model.vod.Subject;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vod.service.SubjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/6
 *
 * <p>
 * 课程科目 前端控制器
 * </p>
 */
@Tag(name = "课程分类接口", description = "课程分类管理接口")
@RestController
@RequestMapping(value = "/admin/vod/subject")
@CrossOrigin
public class SubjectController
{
    @Autowired
    private SubjectService subjectService;

    /**
     * 查询下一层的课程分类列表
     * 根据 parent_id,懒加载,每次查询一层数据
     *
     * @param id id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "查询课程分类", description = "查询下一层的课程分类")
    @GetMapping("get/child/subject/{id}")
    public Result<List<Subject>> getChildSubject(@Parameter(name = "id", description = "ID", required = true)
                                                 @PathVariable("id") Long id)
    {
        List<Subject> list = subjectService.selectSubjectList(id);

        return Result.ok(list);
    }
}
3.3、编写 SubjectService
package com.myxh.smart.planet.vod.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.vod.Subject;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/6
 *
 * <p>
 * 课程科目 服务类
 * </p>
 */
public interface SubjectService extends IService<Subject>
{
    /**
     * 查询下一层的课程分类列表
     * 根据 parent_id,懒加载,每次查询一层数据
     *
     * @param id id
     * @return subjectList 下一层的课程分类列表
     */
    List<Subject> selectSubjectList(Long id);
}
3.4、编写 SubjectServiceImpl
package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Subject;
import com.myxh.smart.planet.vod.mapper.SubjectMapper;
import com.myxh.smart.planet.vod.service.SubjectService;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/6
 *
 * <p>
 * 课程科目 服务实现类
 * </p>
 */
@Service
public class SubjectServiceImpl extends ServiceImpl<SubjectMapper, Subject> implements SubjectService
{
    /**
     * 查询下一层的课程分类列表
     * 根据 parent_id,懒加载,每次查询一层数据
     *
     * @param id id
     * @return subjectList 下一层的课程分类列表
     */
    @Override
    public List<Subject> selectSubjectList(Long id)
    {
        QueryWrapper<Subject> wrapper = new QueryWrapper<>();
        wrapper.eq("parent_id", id);
        List<Subject> subjectList = baseMapper.selectList(wrapper);

        // 遍历 subjectList,得到每个 Subject 对象,判断是否有下一层数据,如果有,则向 subjectList 集合每个 Subject 对象中设置 hasChildren
        for (Subject subject : subjectList)
        {
            // 获取 subject 的 id 值
            Long subjectId = subject.getId();

            // 查询
            boolean isChild = this.isChildren(subjectId);

            // 封装到对象里面
            subject.setHasChildren(isChild);
        }

        return subjectList;
    }

    /**
     * 判断 id 下面是否有子节点
     *
     * @param subjectId 课程 id
     * @return isChild 是否有子节点
     */
    private boolean isChildren(Long subjectId)
    {
        QueryWrapper<Subject> wrapper = new QueryWrapper<>();
        wrapper.eq("parent_id", subjectId);
        Long count = baseMapper.selectCount(wrapper);

        return count > 0;
    }
}
3.5、开发课程分类列表前端

(1)添加数据字典路由。

修改 router/index.js 文件。

// 课程分类管理
{
  path: "/subject",
  component: Layout,
  redirect: "/subject/list",
  name: "课程分类管理",
  meta: { title: "课程分类管理", icon: "example" },
  alwaysShow: true,
  children: [
    {
      path: "list",
      name: "课程分类列表",
      component: () => import("@/views/vod/subject/list"),
      meta: { title: "课程分类列表", icon: "table" },
    },
  ],
},

(2)定义数据字典列表接口。

创建文件 src/api/vod/subject.js。

import request from "@/utils/request";

const SUBJECT_API = "/admin/vod/subject";

export default {
  /**
   * 课程分类列表
   *
   * @param {number} id id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getChildList(id) {
    return request({
      url: `${SUBJECT_API}/get/child/subject/${id}`,
      method: "get",
    });
  },
};

(3)编写 subject/list.vue。

<template>
  <div class="app-container">
    <el-table
      :data="list"
      style="width: 100%"
      row-key="id"
      border
      lazy
      :load="load"
      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
    >
      <el-table-column prop="title" label="名称" width="150"></el-table-column>
      <el-table-column prop="createTime" label="创建时间"></el-table-column>
    </el-table>
  </div>
</template>

<script>
  import subjectAPI from "@/api/vod/subject";

  export default {
    data() {
      return {
        // 课程分类列表数组
        list: [],
      };
    },

    created() {
      this.getSubList(0);
    },

    methods: {
      // 课程分类列表
      getSubList(id) {
        subjectAPI.getChildList(id).then((response) => {
          this.list = response.data;
        });
      },

      // 下一层的课程分类列表
      load(tree, treeNode, resolve) {
        subjectAPI.getChildList(tree.id).then((response) => {
          resolve(response.data);
        });
      },
    },
  };
</script>
4、技术点-EasyExcel
4.1、EasyExcel 介绍

EasyExcel 是阿里巴巴开源的一个 excel 处理框架,以使用简单、节省内存著称。EasyExcel 能大大减少占用内存的主要原因是在解析 Excel 时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。

4.2、EasyExcel 特点
  • Java 领域解析、生成 Excel 比较有名的框架有 Apache poi、jxl 等。但他们都存在一个严重的问题就是非常的耗内存。如果系统并发量不大的话可能还行,但是一旦并发上来后一定会 OOM 或者 JVM 频繁的 Full GC。

  • EasyExcel 采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。

  • EasyExcel 是一个基于 Java 的简单、省内存的读写 Excel 的开源项目。在尽可能节约内存的情况下支持读写百 MB 的 Excel。

4.3、EasyExcel 写操作

(1)pom 中引入 xml 相关依赖。

<!-- easyexcel -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
</dependency>

(2)创建实体类。

设置表头和添加的数据字段。

package com.myxh.smart.planet.excel.entity;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * @author MYXH
 * @date 2023/10/6
 */
@Data
public class User
{
    // 设置表头名称
    @ExcelProperty("用户编号")
    private Integer id;

    // 设置表头名称
    @ExcelProperty("用户姓名")
    private String name;
}

(3)实现写操作。

创建方法循环设置要添加到 Excel 的数据。

package com.myxh.smart.planet.excel;

import com.myxh.smart.planet.excel.entity.User;

import java.util.ArrayList;
import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/6
 */
public class TestWrite
{
    /**
     * 循环设置要添加的数据,最终封装到list集合中
     *
     * @return list 用户表格
     */
    private static List<User> data()
    {
        List<User> list = new ArrayList<User>();

        for (int i = 1; i <= 10; i++)
        {
            User data = new User();
            data.setId(i);
            data.setName("MYXH" + i);
            list.add(data);
        }

        return list;
    }
}

实现最终的添加操作。

package com.myxh.smart.planet.excel;

import com.alibaba.excel.EasyExcel;
import com.myxh.smart.planet.excel.entity.User;

import java.util.ArrayList;
import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/6
 */
public class TestWrite
{
    public static void main(String[] args)
    {
        // 设置文件名称和路径
        String fileName = "service\\service-vod\\src\\test\\resources\\excel\\用户.xlsx";

        // 调用方法
        EasyExcel.write(fileName, User.class)
                .sheet("写操作")
                .doWrite(data());
    }

    /**
     * 循环设置要添加的数据,最终封装到list集合中
     *
     * @return list 用户表格
     */
    private static List<User> data()
    {
        List<User> list = new ArrayList<User>();

        for (int i = 1; i <= 10; i++)
        {
            User data = new User();
            data.setId(i);
            data.setName("MYXH" + i);
            list.add(data);
        }

        return list;
    }
}
4.4、EasyExcel 读操作

(1)创建实体类。

package com.myxh.smart.planet.excel.entity;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * @author MYXH
 * @date 2023/10/6
 */
@Data
public class User
{
    // 设置表头名称
    @ExcelProperty(value = "用户编号", index = 0)
    private Integer id;

    // 设置表头名称
    @ExcelProperty(value = "用户姓名", index = 1)
    private String name;
}

(2)创建读取操作的监听器。

package com.myxh.smart.planet.excel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.myxh.smart.planet.excel.entity.User;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/6
 */
public class ExcelListener extends AnalysisEventListener<User>
{


    /**
     * 一行一行读取 excel 内容,把每行内容封装到 User 对象
     * 从 excel 第二行开始读取
     *
     * @param data            one row value. It is same as {@link AnalysisContext#readRowHolder()} 一行值。与 {@link AnalysisContext#readRowHolder()} 相同
     * @param analysisContext analysis context 分析上下文
     */
    @Override
    public void invoke(User data, AnalysisContext analysisContext)
    {
        System.out.println(data);
    }

    /**
     * 读取表头内容
     *
     * @param headMap 表头信息
     * @param context 上下文
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context)
    {
        System.out.println("表头信息:" + headMap);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext)
    {

    }
}

(3)调用实现最终的读取。

package com.myxh.smart.planet.excel;

import com.alibaba.excel.EasyExcel;
import com.myxh.smart.planet.excel.entity.User;

/**
 * @author MYXH
 * @date 2023/10/6
 */
public class TestRead
{
    public static void main(String[] args)
    {
        // 设置文件名称和路径
        String fileName = "service\\service-vod\\src\\test\\resources\\excel\\用户.xlsx";

        // 调用方法进行读操作
        EasyExcel.read(fileName, User.class, new ExcelListener()).sheet().doRead();
    }
}
5、功能实现-课程分类导出
5.1、查看 model 实体类

在 model 模块查看实体:com.myxh.smart.planet.vo.vod.SubjectEeVo。

package com.myxh.smart.planet.vo.vod;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * @author MYXH
 * @date 2023/9/27
 */
@Data
public class SubjectEeVo
{
    @ExcelProperty(value = "ID", index = 0)
    private Long id;

    @ExcelProperty(value = "课程分类名称", index = 1)
    private String title;

    @ExcelProperty(value = "上级ID", index = 2)
    private Long parentId;

    @ExcelProperty(value = "排序", index = 3)
    private Integer sort;
}
5.2、编写 SubjectService 和实现

SubjectService

package com.myxh.smart.planet.vod.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.vod.Subject;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;

/**
 * @author MYXH
 * @date 2023/10/6
 *
 * <p>
 * 课程科目 服务类
 * </p>
 */
public interface SubjectService extends IService<Subject>
{
    /**
     * 课程分类导出为 Excel
     *
     * @param response 响应
     */
    void exportData(HttpServletResponse response);

    /**
     * 从 Excel 导入课程分类
     *
     * @param file 文件
     */
    void importData(MultipartFile file);
}

SubjectServiceImpl

package com.myxh.smart.planet.vod.service.impl;

import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.model.vod.Subject;
import com.myxh.smart.planet.vo.vod.SubjectEeVo;
import com.myxh.smart.planet.vod.mapper.SubjectMapper;
import com.myxh.smart.planet.vod.service.SubjectService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/6
 *
 * <p>
 * 课程科目 服务实现类
 * </p>
 */
@Service
public class SubjectServiceImpl extends ServiceImpl<SubjectMapper, Subject> implements SubjectService
{
    /**
     * 课程分类导出为 Excel
     *
     * @param response 响应
     */
    @Override
    public void exportData(HttpServletResponse response)
    {

        try
        {
            // 设置下载信息
            response.setContentType("application/vnd.ms-excel");
            response.setCharacterEncoding("utf-8");

            // 这里 URLEncoder.encode 可以防止中文乱码,当然和 easyexcel 没有关系
            String fileName = URLEncoder.encode("课程分类", StandardCharsets.UTF_8);

            response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");

            // 查询课程分类表所有数据
            List<Subject> subjectList = baseMapper.selectList(null);

            // List<Subject> 转换为 List<SubjectEeVo>
            List<SubjectEeVo> subjectEeVoList = new ArrayList<>();

            for (Subject subject : subjectList)
            {
                SubjectEeVo subjectEeVo = new SubjectEeVo();
                // subjectEeVo.setId(subject.getId());
                // subjectEeVo.setParentId(subject.getParentId());
                BeanUtils.copyProperties(subject, subjectEeVo);
                subjectEeVoList.add(subjectEeVo);
            }

            // EasyExcel 写操作
            EasyExcel.write(response.getOutputStream(), SubjectEeVo.class)
                    .sheet("课程分类")
                    .doWrite(subjectEeVoList);
        }
        catch (IOException e)
        {
            throw new SmartPlanetException(20001, "导出失败");
        }
    }
}
5.3、添加 Controller 方法
package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.vod.service.SubjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/6
 *
 * <p>
 * 课程科目 前端控制器
 * </p>
 */
@Tag(name = "课程分类接口", description = "课程分类管理接口")
@RestController
@RequestMapping(value = "/admin/vod/subject")
@CrossOrigin
public class SubjectController
{
    @Autowired
    private SubjectService subjectService;

    /**
     * 课程分类导出为 Excel
     *
     * @param response 响应
     */
    @Operation(summary = "课程分类导出", description = "课程分类导出为 Excel")
    @GetMapping("export/data")
    public void exportData(HttpServletResponse response)
    {
        subjectService.exportData(response);
    }
}
5.4、数据字典导出前端

(1)list.vue 页面添加导出按钮。

<div
  class="el-toolbar"
  style="display: flex; justify-content: center; align-items: center"
>
  <div
    class="el-toolbar-body"
    style="justify-content: flex-start; margin-top: 20px"
  >
    <el-button type="primary" @click="exportData">
      <i class="fa fa-plus" /> 导出
    </el-button>
  </div>
</div>

(2)编写调用方法。

data() {
  return {
    // 课程分类列表数组
    list: [],

    BASE_API: "http://localhost:8301",

    SUBJECT_API: "admin/vod/subject",
  };
},

methods: {
  // 课程分类导出为 Excel
  exportData() {
    window.open(`${this.BASE_API}/${this.SUBJECT_API}/export/data`);
  },
},
6、功能实现-课程分类导入
6.1、创建读取监听器
package com.myxh.smart.planet.vod.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.myxh.smart.planet.vo.vod.SubjectEeVo;
import com.myxh.smart.planet.vod.mapper.SubjectMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.myxh.smart.planet.model.vod.Subject;

/**
 * @author MYXH
 * @date 2023/10/6
 */
@Component
public class SubjectListener extends AnalysisEventListener<SubjectEeVo>
{
    // 注入 subjectMapper
    @Autowired
    private SubjectMapper subjectMapper;

    /**
     * 一行一行读取 excel 内容,把每行内容封装到 User 对象
     * 从 excel 第二行开始读取
     *
     * @param subjectEeVo     one row value. It is same as {@link AnalysisContext#readRowHolder()} 一行值。与 {@link AnalysisContext#readRowHolder()} 相同
     * @param analysisContext analysis context 分析上下文
     */
    @Override
    public void invoke(SubjectEeVo subjectEeVo, AnalysisContext analysisContext)
    {
        //  SubjectEeVo 转换为 Subject
        Subject subject = new Subject();
        BeanUtils.copyProperties(subjectEeVo, subject);

        // 添加 subject
        subjectMapper.insert(subject);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext)
    {

    }
}
6.2、添加 controller 方法
package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vod.service.SubjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 * @author MYXH
 * @date 2023/10/6
 *
 * <p>
 * 课程科目 前端控制器
 * </p>
 */
@Tag(name = "课程分类接口", description = "课程分类管理接口")
@RestController
@RequestMapping(value = "/admin/vod/subject")
@CrossOrigin
public class SubjectController
{
    @Autowired
    private SubjectService subjectService;

    /**
     * 从 Excel 导入课程分类
     *
     * @param file 文件
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "课程分类导入", description = "从 Excel 导入课程分类")
    @PostMapping(value = "import/data", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Result<Void> importData(@RequestParam("file") MultipartFile file)
    {
        subjectService.importData(file);

        return Result.ok(null);
    }
}
6.3、添加 service 方法
package com.myxh.smart.planet.vod.service.impl;

import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.model.vod.Subject;
import com.myxh.smart.planet.vo.vod.SubjectEeVo;
import com.myxh.smart.planet.vod.listener.SubjectListener;
import com.myxh.smart.planet.vod.mapper.SubjectMapper;
import com.myxh.smart.planet.vod.service.SubjectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
 * @author MYXH
 * @date 2023/10/6
 *
 * <p>
 * 课程科目 服务实现类
 * </p>
 */
@Service
public class SubjectServiceImpl extends ServiceImpl<SubjectMapper, Subject> implements SubjectService
{
    @Autowired
    private SubjectListener subjectListener;

    /**
     * 从 Excel 导入课程分类
     *
     * @param file 文件
     */
    @Override
    public void importData(MultipartFile file)
    {
        try
        {
            EasyExcel.read(file.getInputStream(),
                    SubjectEeVo.class,
                    subjectListener).sheet().doRead();
        }
        catch (IOException e)
        {
            throw new SmartPlanetException(20001, "导入失败");
        }
    }
}
6.4、数据字典导入前端

(1)在 list.vue 页面添加导入按钮。

<div
  class="el-toolbar"
  style="display: flex; justify-content: center; align-items: center"
>
  <div
    class="el-toolbar-body"
    style="justify-content: flex-start; margin-top: 20px"
  >
    <el-button type="primary" @click="exportData">
      <i class="fa fa-plus" /> 导出
    </el-button>
    <el-button type="primary" @click="importData">
      <i class="fa fa-plus" /> 导入
    </el-button>
  </div>
</div>

(2)添加导入弹出层

<el-dialog title="导入" :visible.sync="dialogImportVisible" width="480px">
  <el-form label-position="right" label-width="170px">
    <el-form-item label="文件">
      <el-upload
        :multiple="false"
        :on-success="onUploadSuccess"
        :action="BASE_API + '/' + SUBJECT_API + '/import/data'"
        class="upload-demo"
      >
        <el-button size="small" type="primary">点击上传</el-button>
        <div slot="tip" class="el-upload__tip">
          只能上传 xlsx 文件,且不超过 500KB
        </div>
      </el-upload>
    </el-form-item>
  </el-form>
  <div slot="footer" class="dialog-footer">
    <el-button @click="dialogImportVisible = false">取消</el-button>
  </div>
</el-dialog>

(3)添加导入弹出层属性

data() {
  return {
    // 课程分类列表数组
    list: [],

    dialogImportVisible: false,

    BASE_API: "http://localhost:8301",

    SUBJECT_API: "admin/vod/subject",
  };
},

(4)添加导入方法

methods: {
  // 从 Excel 导入课程分类
  importData() {
    this.dialogImportVisible = true;
  },

  onUploadSuccess(response, file) {
    this.$message.info("上传成功");
    this.dialogImportVisible = false;
    this.getSubList(0);
  },
},

Day 7-点播管理模块(一)

一、后台管理系统-点播管理模块

1、点播管理模块需求

添加点播课程,包含课程基本信息,课程章节,课程小结和最终发布。

点播管理模块需求

1.1、创建课程相关表

创建课程相关表

2、环境搭建
2.1、生成相关代码

生成相关代码

3、功能实现-课程列表

实现分页条件查询点播课程功能。

功能实现-课程列表

3.1、开发课程列表接口

编写 CourseController。

package com.myxh.smart.planet.vod.controller;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.vod.CourseQueryVo;
import com.myxh.smart.planet.vod.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 前端控制器
 * </p>
 */
@Tag(name = "课程接口", description = "课程管理接口")
@RestController
@RequestMapping("/admin/vod/course")
@CrossOrigin
public class CourseController
{
    @Autowired
    private CourseService courseService;

    /**
     * 点播课程列表
     *
     * @param current       当前页码
     * @param limit         每页记录数
     * @param courseQueryVo 查询对象
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "获取点播课程", description = "获取点播课程列表")
    @GetMapping("find/query/page/{current}/{limit}")
    public Result<Map<String, Object>> courseList(@Parameter(name = "current", description = "当前页码", required = true) @PathVariable("current") Long current,
                                                  @Parameter(name = "limit", description = "每页记录数", required = true) @PathVariable("limit") Long limit,
                                                  @Parameter(name = "courseQueryVo", description = "查询对象") CourseQueryVo courseQueryVo)
    {
        Page<Course> coursePageParam = new Page<>(current, limit);
        Map<String, Object> coursePage = courseService.findPage(coursePageParam, courseQueryVo);

        return Result.ok(coursePage);
    }
}

编写 CourseService。

package com.myxh.smart.planet.vod.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vo.vod.CourseQueryVo;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务类
 * </p>
 */
public interface CourseService extends IService<Course>
{

    /**
     * 点播课程列表
     *
     * @param coursePageParam 课程页面参数
     * @param courseQueryVo   查询对象
     * @return coursePage 课程页面
     */
    Map<String, Object> findPage(Page<Course> coursePageParam, CourseQueryVo courseQueryVo);
}

编写 CourseServiceImpl。

package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.model.vod.Subject;
import com.myxh.smart.planet.model.vod.Teacher;
import com.myxh.smart.planet.vo.vod.CourseQueryVo;
import com.myxh.smart.planet.vod.mapper.CourseMapper;
import com.myxh.smart.planet.vod.service.CourseService;
import com.myxh.smart.planet.vod.service.SubjectService;
import com.myxh.smart.planet.vod.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务实现类
 * </p>
 */
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService
{
    @Autowired
    private SubjectService subjectService;

    @Autowired
    private TeacherService teacherService;

    /**
     * 点播课程列表
     *
     * @param coursePageParam 课程页面参数
     * @param courseQueryVo   查询对象
     * @return coursePage 课程页面
     */
    @Override
    public Map<String, Object> findPage(Page<Course> coursePageParam, CourseQueryVo courseQueryVo)
    {
        // 获取条件值
        // 名称
        String title = courseQueryVo.getTitle();

        // 二级分类
        Long subjectId = courseQueryVo.getSubjectId();

        // 一级分类
        Long subjectParentId = courseQueryVo.getSubjectParentId();

        // 教师
        Long teacherId = courseQueryVo.getTeacherId();

        // 封装条件
        QueryWrapper<Course> wrapper = new QueryWrapper<>();

        if (StringUtils.hasLength(title))
        {
            wrapper.like("title", title);
        }
        if (!ObjectUtils.isEmpty(subjectId))
        {
            wrapper.eq("subject_id", subjectId);
        }
        if (!ObjectUtils.isEmpty(subjectParentId))
        {
            wrapper.eq("subject_parent_id", subjectParentId);
        }
        if (!ObjectUtils.isEmpty(teacherId))
        {
            wrapper.eq("teacher_id", teacherId);
        }

        // 调用方法实现条件查询分页
        Page<Course> coursePage = baseMapper.selectPage(coursePageParam, wrapper);

        // 总记录数
        Long totalCount = coursePage.getTotal();

        // 总页数
        Long totalPage = coursePage.getPages();

        // 每页数据集合
        List<Course> coursePageRecords = coursePage.getRecords();

        // 遍历封装教师和分类名称,获取 id 对应名称,进行封装,最终显示
        coursePageRecords.stream().forEach(this::getTeacherOrSubjectName);

        // 封装返回数据
        Map<String, Object> coursePageMap = new HashMap<>();
        coursePageMap.put("totalCount", totalCount);
        coursePageMap.put("totalPage", totalPage);
        coursePageMap.put("records", coursePageRecords);

        return coursePageMap;
    }

    /**
     * 获取教师和分类名称
     *
     * @param course 课程数据
     * @return course 课程数据
     */
    private Course getTeacherOrSubjectName(Course course)
    {
        // 根据教师 id 获取教师名称
        Teacher teacher = teacherService.getById(course.getTeacherId());

        if (teacher != null)
        {
            course.getParam().put("teacherName", teacher.getName());
        }

        // 根据课程分类 id 获取课程分类名称
        Subject subjectOne = subjectService.getById(course.getSubjectParentId());

        if (subjectOne != null)
        {
            course.getParam().put("subjectParentTitle", subjectOne.getTitle());
        }

        Subject subjectTwo = subjectService.getById(course.getSubjectId());

        if (subjectTwo != null)
        {
            course.getParam().put("subjectTitle", subjectTwo.getTitle());
        }

        return course;
    }
}
3.2、开发课程列表前端

(1)src 目录下 index.js 文件添加路由。

// 课程管理
{
  path: "/vod/course",
  component: Layout,
  redirect: "/vod/course/list",
  name: "vodCourse",
  meta: {
    title: "点播管理",
    icon: "el-icon-bank-card",
  },
  alwaysShow: true,
  children: [
    {
      path: "course/list",
      name: "CourseList",
      component: () => import("@/views/vod/course/list"),
      meta: { title: "课程列表", icon: "table" },
    },
    {
      path: "course/info",
      name: "CourseInfo",
      component: () => import("@/views/vod/course/form"),
      meta: { title: "发布课程", icon: "table" },
    },
    {
      path: "course/info/:id",
      name: "CourseInfoEdit",
      component: () => import("@/views/vod/course/form"),
      meta: { title: "编辑课程", icon: "table" },
      hidden: true,
    },
    {
      path: "course/chapter/:id",
      name: "CourseChapterEdit",
      component: () => import("@/views/vod/course/form"),
      meta: { title: "编辑大纲", icon: "table" },
      hidden: true,
    },
    {
      path: "course/chart/:id",
      name: "CourseChart",
      component: () => import("@/views/vod/course/chart"),
      meta: { title: "课程统计", icon: "table" },
      hidden: true,
    },
  ],
},

(2)创建 vue 页面。

创建 vue 页面

(3)在 api 目录创建 course.js 文件。

import request from "@/utils/request";

const COURSE_API = "/admin/vod/course";

export default {
  /**
   * 点播课程列表
   *
   * @param {number} current 当前页码
   * @param {number} limit 每页记录数
   * @param {Object} courseQueryVo 查询对象
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getPageList(current, limit, courseQueryVo) {
    return request({
      url: `${COURSE_API}/find/query/page/${current}/${limit}`,
      method: "get",
      params: courseQueryVo,
    });
  },
};

(4)在 api 目录 teacher.js 文件定义接口。

import request from "@/utils/request";

const TEACHER_API = "/admin/vod/teacher";

export default {
  /**
   * 查询所有教师
   *
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  list() {
    return request({
      url: `${TEACHER_API}/find/all`,
      method: `get`,
    });
  },
};

(5)编写 list.vue 页面。

<template>
  <div class="app-container">
    <head>
      <!-- 设置 referrer 为 no-referrer,用于绕过防盗链限制,从而正常显示图片 -->
      <meta name="referrer" content="no-referrer" />
    </head>

    <!-- 查询表单 -->
    <el-card class="operate-container" shadow="never">
      <el-form :inline="true" class="demo-form-inline">
        <!-- 所属分类:级联下拉列表 -->
        <!-- 一级分类 -->
        <el-form-item label="课程类别">
          <el-select
            v-model="searchObj.subjectParentId"
            placeholder="请选择"
            @change="subjectLevelOneChanged"
          >
            <el-option
              v-for="subject in subjectList"
              :key="subject.id"
              :label="subject.title"
              :value="subject.id"
            />
          </el-select>

          <!-- 二级分类 -->
          <el-select v-model="searchObj.subjectId" placeholder="请选择">
            <el-option
              v-for="subject in subjectLevelTwoList"
              :key="subject.id"
              :label="subject.title"
              :value="subject.id"
            />
          </el-select>
        </el-form-item>

        <!-- 标题 -->
        <el-form-item label="标题">
          <el-input v-model="searchObj.title" placeholder="课程标题" />
        </el-form-item>

        <!-- 教师 -->
        <el-form-item label="教师">
          <el-select v-model="searchObj.teacherId" placeholder="请选择教师">
            <el-option
              v-for="teacher in teacherList"
              :key="teacher.id"
              :label="teacher.name"
              :value="teacher.id"
            />
          </el-select>
        </el-form-item>

        <el-button type="primary" icon="el-icon-search" @click="fetchData()"
          >查询</el-button
        >
        <el-button type="default" @click="resetData()">清空</el-button>
      </el-form>
    </el-card>

    <!-- 工具按钮 -->
    <el-card class="operate-container" shadow="never">
      <i class="el-icon-tickets" style="margin-top: 5px"></i>
      <span style="margin-top: 5px">数据列表</span>
      <el-button class="btn-add" @click="add()">添加</el-button>
    </el-card>

    <!-- 表格 -->
    <el-table :data="list" border stripe>
      <el-table-column label="序号" width="50">
        <template slot-scope="scope">
          {{ (page - 1) * limit + scope.$index + 1 }}
        </template>
      </el-table-column>

      <el-table-column label="封面" width="200" align="center">
        <template slot-scope="scope">
          <img :src="scope.row.cover" alt="scope.row.title" width="100%" />
        </template>
      </el-table-column>
      <el-table-column label="课程信息">
        <template slot-scope="scope">
          <a href="">{{ scope.row.title }}</a>
          <p>
            分类:{{ scope.row.param.subjectParentTitle }} {{
            scope.row.param.subjectTitle }}
          </p>
          <p>
            课时:{{ scope.row.lessonNum }} / 浏览:{{ scope.row.viewCount }} /
            付费学员:{{ scope.row.buyCount }}
          </p>
        </template>
      </el-table-column>
      <el-table-column label="教师" width="100" align="center">
        <template slot-scope="scope">
          {{ scope.row.param.teacherName }}
        </template>
      </el-table-column>
      <el-table-column label="价格(元)" width="100" align="center">
        <template slot-scope="scope">
          <el-tag v-if="Number(scope.row.price) === 0" type="success"
            >免费</el-tag
          >
          <!-- 前端解决保留两位小数的问题 -->
          <!-- <el-tag v-else>{{ Number(scope.row.price).toFixed(2) }}</el-tag> -->

          <!-- 后端解决保留两位小数的问题,前端不用处理 -->
          <el-tag v-else>{{ scope.row.price }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column
        prop="status"
        label="课程状态"
        width="100"
        align="center"
      >
        <template slot-scope="scope">
          <el-tag :type="scope.row.status === 0 ? 'warning' : 'success'"
            >{{ scope.row.status === 0 ? "未发布" : "已发布" }}</el-tag
          >
        </template>
      </el-table-column>
      <el-table-column label="发布时间" width="140" align="center">
        <template slot-scope="scope">
          {{ scope.row.createTime ? scope.row.createTime.substr(0, 16) : "" }}
        </template>
      </el-table-column>

      <el-table-column label="操作" width="210" align="center">
        <template slot-scope="scope">
          <router-link :to="'/vod/course/course/info/' + scope.row.id">
            <el-button type="text" icon="el-icon-edit">修改</el-button>
          </router-link>
          <router-link :to="'/vod/course/course/chapter/' + scope.row.id">
            <el-button type="text" icon="el-icon-edit">编辑大纲</el-button>
          </router-link>
          <router-link :to="'/vod/course/course/chart/' + scope.row.id">
            <el-button type="text" icon="el-icon-edit">课程统计</el-button>
          </router-link>
          <el-button
            type="text"
            icon="el-icon-delete"
            @click="removeById(scope.row.id)"
            >删除</el-button
          >
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页组件 -->
    <el-pagination
      :current-page="page"
      :total="total"
      :page-size="limit"
      :page-sizes="[5, 10, 20, 30, 40, 50, 100]"
      style="padding: 30px 0; text-align: center"
      layout="total, sizes, prev, pager, next, jumper"
      @size-change="changePageSize"
      @current-change="changeCurrentPage"
    />
  </div>
</template>

<script>
  import teacherAPI from "@/api/vod/teacher";
  import subjectAPI from "@/api/vod/subject";
  import courseAPI from "@/api/vod/course";

  export default {
    data() {
      return {
        // 课程列表
        list: [],
        // 总记录数
        total: 0,
        // 页码
        page: 1,
        // 每页记录数
        limit: 10,

        // 查询条件
        searchObj: {
          // 解决查询表单无法选中二级类别
          subjectId: "",
        },

        // 教师列表
        teacherList: [],
        // 一级分类列表
        subjectList: [],
        // 二级分类列表,
        subjectLevelTwoList: [],
      };
    },

    created() {
      this.fetchData();

      // 初始化分类列表
      this.initSubjectList();

      // 获取教师列表
      this.initTeacherList();
    },

    methods: {
      fetchData() {
        courseAPI
          .getPageList(this.page, this.limit, this.searchObj)
          .then((response) => {
            this.list = response.data.records;
            this.total = response.data.totalCount;
          });
      },

      initTeacherList() {
        teacherAPI.list().then((response) => {
          this.teacherList = response.data;
        });
      },

      initSubjectList() {
        subjectAPI.getChildList(0).then((response) => {
          this.subjectList = response.data;
        });
      },

      subjectLevelOneChanged(value) {
        subjectAPI.getChildList(value).then((response) => {
          this.subjectLevelTwoList = response.data;
          this.searchObj.subjectId = "";
        });
      },

      add() {
        this.$router.push({ path: "/vod/course/course/info" });
      },

      // 每页记录数改变,size:回调参数,表示当前选中的“每页条数”
      changePageSize(size) {
        this.limit = size;
        this.fetchData();
      },

      // 改变页码,page:回调参数,表示当前选中的“页码”
      changeCurrentPage(page) {
        this.page = page;
        this.fetchData();
      },

      // 重置表单
      resetData() {
        this.searchObj = {};

        // 二级分类列表
        this.subjectLevelTwoList = [];
        this.fetchData();
      },
    },
  };
</script>

二、发布课程-填写课程基本信息

1、界面效果

界面效果

2、添加课程基本信息接口
2.1、创建课程描述的 service 和 mapper

创建课程描述的 service 和 mapper

2.2、创建添加课程基本信息接口

(1)controller

package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.vod.CourseFormVo;
import com.myxh.smart.planet.vod.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 前端控制器
 * </p>
 */
@Tag(name = "课程接口", description = "课程管理接口")
@RestController
@RequestMapping("/admin/vod/course")
@CrossOrigin
public class CourseController
{
    @Autowired
    private CourseService courseService;

    /**
     * 添加课程基本信息
     *
     * @param courseFormVo 课程基本信息
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "添加课程基本信息", description = "添加课程基本信息")
    @PostMapping("save")
    public Result<Long> saveCourseInfo(@RequestBody CourseFormVo courseFormVo)
    {
        Long courseId = courseService.saveCourseInfo(courseFormVo);

        return Result.ok(courseId);
    }
}

(2)service

package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.model.vod.CourseDescription;
import com.myxh.smart.planet.vo.vod.CourseFormVo;
import com.myxh.smart.planet.vod.mapper.CourseMapper;
import com.myxh.smart.planet.vod.service.CourseDescriptionService;
import com.myxh.smart.planet.vod.service.CourseService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务实现类
 * </p>
 */
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService
{
    @Autowired
    private CourseDescriptionService courseDescriptionService;

    /**
     * 添加课程基本信息
     *
     * @param courseFormVo 课程基本信息
     * @return courseId 课程 id
     */
    @Override
    public Long saveCourseInfo(CourseFormVo courseFormVo)
    {
        // 添加课程基本信息,操作 course 表
        Course course = new Course();
        BeanUtils.copyProperties(courseFormVo, course);
        baseMapper.insert(course);

        // 添加课程详情信息,操作 course_description 表
        CourseDescription courseDescription = new CourseDescription();
        courseDescription.setDescription(courseFormVo.getDescription());

        // 设置课程 id
        courseDescription.setCourseId(course.getId());
        courseDescriptionService.save(courseDescription);

        // 返回课程 id
        return course.getId();
    }
}
3、添加课程基本信息前端
3.1、课程列表 list.vue 添加方法
add() {
  this.$router.push({ path: "/vod/course/course/info" });
},
3.2、course.js 定义接口
import request from "@/utils/request";

const COURSE_API = "/admin/vod/course";

export default {
  /**
   * 添加课程基本信息
   * @param {Object} courseInfo 课程基本信息
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  saveCourseInfo(courseInfo) {
    return request({
      url: `${COURSE_API}/save`,
      method: "post",
      data: courseInfo,
    });
  },
};
3.3、创建课程基本信息添加页面

创建课程基本信息添加页面

(1)form.vue

<template>
  <div class="app-container">
    <head>
      <!-- 设置 referrer 为 no-referrer,用于绕过防盗链限制,从而正常显示图片 -->
      <meta name="referrer" content="no-referrer" />
    </head>

    <h2 style="text-align: center">发布新课程</h2>
    <el-steps
      :active="active"
      finish-status="success"
      simple
      style="margin-bottom: 40px"
    >
      <el-step title="填写课程基本信息" />
      <el-step title="创建课程大纲" />
      <el-step title="发布课程" />
    </el-steps>

    <!-- 填写课程基本信息 -->
    <info v-if="active === 0" />

    <!-- 创建课程大纲 -->
    <chapter v-if="active === 1" />

    <!-- 发布课程 -->
    <publish v-if="active === 2 || active === 3" />
  </div>
</template>

<script>
  // 引入子组件
  import info from "@/views/vod/course/components/info";
  import chapter from "@/views/vod/course/components/chapter";
  import publish from "@/views/vod/course/components/publish";

  export default {
    // 注册子组件
    components: { info, chapter, publish },

    data() {
      return {
        active: 0,
        courseId: null,
      };
    },

    created() {
      // 获取路由id
      if (this.$route.params.id) {
        this.courseId = this.$route.params.id;
      }
      if (this.$route.name === "CourseInfoEdit") {
        this.active = 0;
      }
      if (this.$route.name === "CourseChapterEdit") {
        this.active = 1;
      }
    },
  };
</script>

(2)Info.vue

<template>
  <div class="app-container">
    <head>
      <!-- 设置 referrer 为 no-referrer,用于绕过防盗链限制,从而正常显示图片 -->
      <meta name="referrer" content="no-referrer" />
    </head>

    <!-- 课程信息表单 -->
    <el-form label-width="120px">
      <el-form-item label="课程标题">
        <el-input
          v-model="courseInfo.title"
          placeholder=" 示例:机器学习项目课:从基础到搭建项目视频课程。专业名称注意大小写"
        />
      </el-form-item>

      <!-- 课程教师 -->
      <el-form-item label="课程教师">
        <el-select v-model="courseInfo.teacherId" placeholder="请选择">
          <el-option
            v-for="teacher in teacherList"
            :key="teacher.id"
            :label="teacher.name"
            :value="teacher.id"
          />
        </el-select>
      </el-form-item>

      <!-- 所属分类:级联下拉列表 -->
      <el-form-item label="课程类别">
        <!-- 一级分类 -->
        <el-select
          v-model="courseInfo.subjectParentId"
          placeholder="请选择"
          @change="subjectChanged"
        >
          <el-option
            v-for="subject in subjectList"
            :key="subject.id"
            :label="subject.title"
            :value="subject.id"
          />
        </el-select>

        <!-- 二级分类 -->
        <el-select v-model="courseInfo.subjectId" placeholder="请选择">
          <el-option
            v-for="subject in subjectLevelTwoList"
            :key="subject.id"
            :label="subject.title"
            :value="subject.id"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="总课时">
        <el-input-number
          :min="0"
          v-model="courseInfo.lessonNum"
          controls-position="right"
          placeholder="请填写课程的总课时数"
        />
      </el-form-item>

      <!-- 课程简介-->
      <el-form-item label="课程简介">
        <el-input v-model="courseInfo.description" type="textarea" rows="5" />
      </el-form-item>

      <!-- 课程封面 -->
      <el-form-item label="课程封面">
        <el-upload
          :show-file-list="false"
          :on-success="handleCoverSuccess"
          :before-upload="beforeCoverUpload"
          :on-error="handleCoverError"
          :action="BASE_API + '/admin/vod/file/upload'"
          class="cover-uploader"
        >
          <img
            v-if="courseInfo.cover"
            :src="courseInfo.cover"
            style="width: 200px; height: auto"
          />
          <i v-else class="el-icon-plus avatar-uploader-icon" />
        </el-upload>
      </el-form-item>
      <el-form-item label="课程价格">
        <el-input-number
          :min="0"
          v-model="courseInfo.price"
          controls-position="right"
          placeholder="免费课程请设置为0元"
        /></el-form-item>
    </el-form>

    <div style="text-align: center">
      <el-button
        :disabled="saveBtnDisabled"
        type="primary"
        @click="saveAndNext()"
        >保存并下一步</el-button
      >
    </div>
  </div>
</template>

<script>
  import courseAPI from "@/api/vod/course";
  import teacherAPI from "@/api/vod/teacher";
  import subjectAPI from "@/api/vod/subject";

  export default {
    data() {
      return {
        BASE_API: "http://localhost:8301",
        // 按钮是否禁用
        saveBtnDisabled: false,

        courseId: 0,

        courseInfo: {
          // 表单数据
          price: 0,
          lessonNum: 0,
          // 以下解决表单数据不全时 insert 语句非空校验
          teacherId: "",
          subjectId: "",
          subjectParentId: "",
          cover: "",
          description: "",
        },

        // 教师列表
        teacherList: [],
        // 一级分类列表
        subjectList: [],
        // 二级分类列表
        subjectLevelTwoList: [],
      };
    },

    created() {
      // 初始化分类列表
      this.initSubjectList();

      // 获取教师列表
      this.initTeacherList();
    },

    methods: {
      // 获取教师列表
      initTeacherList() {
        teacherAPI.list().then((response) => {
          this.teacherList = response.data;
        });
      },

      // 初始化分类列表
      initSubjectList() {
        subjectAPI.getChildList(0).then((response) => {
          this.subjectList = response.data;
        });
      },

      // 选择一级分类,切换二级分类
      subjectChanged(value) {
        subjectAPI.getChildList(value).then((response) => {
          this.courseInfo.subjectId = "";
          this.subjectLevelTwoList = response.data;
        });
      },

      // 上传成功回调
      handleCoverSuccess(response, file) {
        this.courseInfo.cover = response.data;
      },

      // 上传校验
      beforeCoverUpload(file) {
        const isJPG = file.type === "image/jpeg";
        const isPNG = file.type === "image/png";
        const isLt2M = file.size / 1024 / 1024 < 2;

        if (!isJPG && !isPNG) {
          this.$message.error("上传课程封面只能是 JPG 或 PNG 格式!");
        }
        if (!isLt2M) {
          this.$message.error("上传课程封面大小不能超过 2MB!");
        }

        return (isJPG || isPNG) && isLt2M;
      },

      // 错误处理
      handleCoverError() {
        console.log("error");
        this.$message.error("上传失败");
      },

      // 保存并下一步
      saveAndNext() {
        this.saveBtnDisabled = true;

        if (!this.courseInfo.title) {
          this.$message.error("请输入课程标题");
          this.saveBtnDisabled = false;

          return;
        }

        if (!this.courseInfo.description) {
          this.$message.error("请输入课程简介");
          this.saveBtnDisabled = false;

          return;
        }

        if (!this.$parent.courseId) {
          this.saveData();
        } else {
          this.updateData();
        }
      },

      // 保存
      saveData() {
        courseAPI.saveCourseInfo(this.courseInfo).then((response) => {
          this.$message.success(response.message);

          // 获取 courseId
          this.$parent.courseId = response.data;

          // 下一步
          this.$parent.active = 1;
        });
      },

      // 修改
      updateData() {},
    },
  };
</script>

三、发布课程-修改课程基本信息

发布课程-修改课程基本信息

1、修改课程基本信息接口
1.1、CourseService 定义方法
package com.myxh.smart.planet.vod.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vo.vod.CourseFormVo;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务类
 * </p>
 */
public interface CourseService extends IService<Course>
{
    /**
     * 根据 id 获取课程信息
     *
     * @param id 课程 id
     * @return CourseFormVo 课程基本信息
     */
    CourseFormVo getCourseInfoById(Long id);

    /**
     * 根据 id 修改课程信息
     *
     * @param courseFormVo 课程基本信息
     * @return courseId 课程 id
     */
    Long updateCourseById(CourseFormVo courseFormVo);
}
1.2、CourseServiceImpl 实现方法
package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.model.vod.CourseDescription;
import com.myxh.smart.planet.vo.vod.CourseFormVo;
import com.myxh.smart.planet.vod.mapper.CourseMapper;
import com.myxh.smart.planet.vod.service.CourseDescriptionService;
import com.myxh.smart.planet.vod.service.CourseService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务实现类
 * </p>
 */
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService
{
    @Autowired
    private CourseDescriptionService courseDescriptionService;

    /**
     * 根据 id 获取课程信息
     *
     * @param id 课程 id
     * @return CourseFormVo 课程基本信息
     */
    @Override
    public CourseFormVo getCourseInfoById(Long id)
    {
        // 从 course 表中获取课程基本信息
        Course course = baseMapper.selectById(id);

        if (course == null)
        {
            return null;
        }

        //从 course_description 表中获取课程描述信息
        QueryWrapper<CourseDescription> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", id);
        CourseDescription courseDescription = courseDescriptionService.getOne(wrapper);

        // 封装描述信息,创建 CourseFormVo 对象
        CourseFormVo courseFormVo = new CourseFormVo();
        BeanUtils.copyProperties(course, courseFormVo);

        if (courseDescription != null)
        {
            courseFormVo.setDescription(courseDescription.getDescription());
        }

        return courseFormVo;
    }

    /**
     * 根据 id 修改课程信息
     *
     * @param courseFormVo 课程基本信息
     * @return courseId 课程 id
     */
    @Override
    public void updateCourseById(CourseFormVo courseFormVo)
    {
        // 修改课程基本信息
        Course course = new Course();
        BeanUtils.copyProperties(courseFormVo, course);
        baseMapper.updateById(course);

        // 修改课程详情信息
        QueryWrapper<CourseDescription> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", course.getId());
        CourseDescription courseDescription = courseDescriptionService.getOne(wrapper);
        courseDescription.setDescription(courseFormVo.getDescription());

        // 设置课程描述 id
        courseDescription.setCourseId(course.getId());
        courseDescriptionService.updateById(courseDescription);

        // 返回课程 id
        return course.getId();
    }
}
1.3、CourseController 实现方法
package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.vod.CourseFormVo;
import com.myxh.smart.planet.vod.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 前端控制器
 * </p>
 */
@Tag(name = "课程接口", description = "课程管理接口")
@RestController
@RequestMapping("/admin/vod/course")
@CrossOrigin
public class CourseController
{
    @Autowired
    private CourseService courseService;

    /**
     * 根据 id 获取课程信息
     *
     * @param id 课程 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "根据 id 获取课程信息", description = "根据 id 获取课程信息")
    @GetMapping("get/{id}")
    public Result<CourseFormVo> get(@PathVariable("id") Long id)
    {
        CourseFormVo courseFormVo = courseService.getCourseInfoById(id);

        return Result.ok(courseFormVo);
    }

    /**
     * 根据 id 修改课程信息
     *
     * @param courseFormVo 课程基本信息
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "根据 id 修改课程信息", description = "根据 id 修改课程信息")
    @PostMapping("update")
    public Result<Void> updateCourse(@RequestBody CourseFormVo courseFormVo)
    {
        Long courseId = courseService.updateCourseById(courseFormVo);

        return Result.ok(courseId);
    }
}
2、修改课程基本信息前端
2.1、course.js 定义方法
import request from "@/utils/request";

const COURSE_API = "/admin/vod/course";

export default {
  /**
   * 根据 id 获取课程信息
   *
   * @param {number} id 课程 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getCourseInfoById(id) {
    return request({
      url: `${COURSE_API}/get/${id}`,
      method: "get",
    });
  },

  /**
   * 根据 id 修改课程信息
   *
   * @param {Object} courseInfo 课程基本信息
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  updateCourseInfoById(courseInfo) {
    return request({
      url: `${COURSE_API}/update`,
      method: "post",
      data: courseInfo,
    });
  },
};
2.2、修改 Info.vue 页面
<template>
  <div class="app-container">
    <head>
      <!-- 设置 referrer 为 no-referrer,用于绕过防盗链限制,从而正常显示图片 -->
      <meta name="referrer" content="no-referrer" />
    </head>

    <!-- 课程信息表单 -->
    <el-form label-width="120px">
      <el-form-item label="课程标题">
        <el-input
          v-model="courseInfo.title"
          placeholder=" 示例:机器学习项目课:从基础到搭建项目视频课程。专业名称注意大小写"
        />
      </el-form-item>

      <!-- 课程教师 -->
      <el-form-item label="课程教师">
        <el-select v-model="courseInfo.teacherId" placeholder="请选择">
          <el-option
            v-for="teacher in teacherList"
            :key="teacher.id"
            :label="teacher.name"
            :value="teacher.id"
          />
        </el-select>
      </el-form-item>

      <!-- 所属分类:级联下拉列表 -->
      <el-form-item label="课程类别">
        <!-- 一级分类 -->
        <el-select
          v-model="courseInfo.subjectParentId"
          placeholder="请选择"
          @change="subjectChanged"
        >
          <el-option
            v-for="subject in subjectList"
            :key="subject.id"
            :label="subject.title"
            :value="subject.id"
          />
        </el-select>

        <!-- 二级分类 -->
        <el-select v-model="courseInfo.subjectId" placeholder="请选择">
          <el-option
            v-for="subject in subjectLevelTwoList"
            :key="subject.id"
            :label="subject.title"
            :value="subject.id"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="总课时">
        <el-input-number
          :min="0"
          v-model="courseInfo.lessonNum"
          controls-position="right"
          placeholder="请填写课程的总课时数"
        />
      </el-form-item>

      <!-- 课程简介-->
      <el-form-item label="课程简介">
        <el-input v-model="courseInfo.description" type="textarea" rows="5" />
      </el-form-item>

      <!-- 课程封面 -->
      <el-form-item label="课程封面">
        <el-upload
          :show-file-list="false"
          :on-success="handleCoverSuccess"
          :before-upload="beforeCoverUpload"
          :on-error="handleCoverError"
          :action="BASE_API + '/admin/vod/file/upload'"
          class="cover-uploader"
        >
          <img
            v-if="courseInfo.cover"
            :src="courseInfo.cover"
            style="width: 200px; height: auto"
          />
          <i v-else class="el-icon-plus avatar-uploader-icon" />
        </el-upload>
      </el-form-item>
      <el-form-item label="课程价格">
        <el-input-number
          :min="0"
          v-model="courseInfo.price"
          controls-position="right"
          placeholder="免费课程请设置为0元"
        /></el-form-item>
    </el-form>

    <div style="text-align: center">
      <el-button
        :disabled="saveBtnDisabled"
        type="primary"
        @click="saveAndNext()"
        >保存并下一步</el-button
      >
    </div>
  </div>
</template>

<script>
  import courseAPI from "@/api/vod/course";
  import teacherAPI from "@/api/vod/teacher";
  import subjectAPI from "@/api/vod/subject";

  export default {
    data() {
      return {
        BASE_API: "http://localhost:8301",
        // 按钮是否禁用
        saveBtnDisabled: false,

        courseId: 0,

        courseInfo: {
          // 表单数据
          price: 0,
          lessonNum: 0,
          // 以下解决表单数据不全时 insert 语句非空校验
          teacherId: "",
          subjectId: "",
          subjectParentId: "",
          cover: "",
          description: "",
        },

        // 教师列表
        teacherList: [],
        // 一级分类列表
        subjectList: [],
        // 二级分类列表
        subjectLevelTwoList: [],
      };
    },

    created() {
      if (this.$parent.courseId) {
        // 回显数据
        this.fetchCourseInfoById(this.$parent.courseId);
      } else {
        // 新增数据
        // 初始化分类列表
        this.initSubjectList();
      }

      // 获取教师列表
      this.initTeacherList();
    },

    methods: {
      // 获取教师列表
      initTeacherList() {
        teacherAPI.list().then((response) => {
          this.teacherList = response.data;
        });
      },

      // 初始化分类列表
      initSubjectList() {
        subjectAPI.getChildList(0).then((response) => {
          this.subjectList = response.data;
        });
      },

      // 选择一级分类,切换二级分类
      subjectChanged(value) {
        subjectAPI.getChildList(value).then((response) => {
          this.courseInfo.subjectId = "";
          this.subjectLevelTwoList = response.data;
        });
      },

      // 上传成功回调
      handleCoverSuccess(response, file) {
        this.courseInfo.cover = response.data;
      },

      // 上传校验
      beforeCoverUpload(file) {
        const isJPG = file.type === "image/jpeg";
        const isPNG = file.type === "image/png";
        const isLt2M = file.size / 1024 / 1024 < 2;

        if (!isJPG && !isPNG) {
          this.$message.error("上传课程封面只能是 JPG 或 PNG 格式!");
        }
        if (!isLt2M) {
          this.$message.error("上传课程封面大小不能超过 2MB!");
        }

        return (isJPG || isPNG) && isLt2M;
      },

      // 错误处理
      handleCoverError() {
        console.log("error");
        this.$message.error("上传失败");
      },

      // 保存并下一步
      saveAndNext() {
        this.saveBtnDisabled = true;

        if (!this.courseInfo.title) {
          this.$message.error("请输入课程标题");
          this.saveBtnDisabled = false;

          return;
        }

        if (!this.courseInfo.description) {
          this.$message.error("请输入课程简介");
          this.saveBtnDisabled = false;

          return;
        }

        if (!this.$parent.courseId) {
          this.saveData();
        } else {
          this.updateData();
        }
      },

      // 保存
      saveData() {
        courseAPI.saveCourseInfo(this.courseInfo).then((response) => {
          this.$message.success(response.message);

          // 获取 courseId
          this.$parent.courseId = response.data;

          // 下一步
          this.$parent.active = 1;
        });
      },

      // 获取课程信息
      fetchCourseInfoById(id) {
        courseAPI.getCourseInfoById(id).then((response) => {
          this.courseInfo = response.data;

          // 初始化分类列表
          subjectAPI.getChildList(0).then((response) => {
            this.subjectList = response.data;

            // 填充二级菜单,遍历一级菜单列表
            this.subjectList.forEach((subject) => {
              // 找到和 courseInfo.subjectParentId 一致的父类别记录
              if (subject.id === this.courseInfo.subjectParentId) {
                // 拿到当前类别下的子类别列表,将子类别列表填入二级下拉菜单列表
                subjectAPI.getChildList(subject.id).then((response) => {
                  this.subjectLevelTwoList = response.data;
                });
              }
            });
          });
        });
      },

      // 修改
      updateData() {
        courseAPI.updateCourseInfoById(this.courseInfo).then((response) => {
          this.$message.success(response.message);

          // 获取 courseId
          this.$parent.courseId = response.data;

          // 下一步
          this.$parent.active = 1;
        });
      },
    },
  };
</script>
2.3、创建 chapter/index.vue 页面

创建 chapter/index.vue 页面

<template>
  <div class="app-container">
    <div style="text-align: center">
      <el-button type="primary" @click="prev()">上一步</el-button>
      <el-button type="primary" @click="next()">下一步</el-button>
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {};
    },

    created() {},

    methods: {
      // 上一步
      prev() {
        this.$parent.active = 0;
      },

      // 下一步
      next() {
        this.$parent.active = 2;
      },
    },
  };
</script>

Day 8-点播管理模块(二)

一、发布课程-创建课程大纲

发布课程-创建课程大纲

1、课程章节接口

实现课程章节的列表、添加、修改和删除功能。

1.1、编写章节 Controller
package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.model.vod.Chapter;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.vod.ChapterVo;
import com.myxh.smart.planet.vod.service.ChapterService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 章节 前端控制器
 * </p>
 */
@Tag(name = "章节接口", description = "章节管理接口")
@RestController
@RequestMapping("/admin/vod/chapter")
@CrossOrigin
public class ChapterController
{
    @Autowired
    private ChapterService chapterService;


    /**
     * 大纲列表,获取章节和小节列表
     *
     * @param courseId 课程 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "大纲列表", description = "获取获取章节和小节列表")
    @GetMapping("get/nested/tree/list/{courseId}")
    public Result<List<ChapterVo>> getNestedTreeList(@Parameter(name = "courseId", description = "课程ID", required = true)
                                                     @PathVariable("courseId") Long courseId)
    {
        List<ChapterVo> chapterVoList = chapterService.getNestedTreeList(courseId);

        return Result.ok(chapterVoList);
    }

    /**
     * 添加章节
     *
     * @param chapter 章节数据
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "添加章节", description = "添加章节")
    @PostMapping("save")
    public Result<Void> saveChapter(@RequestBody Chapter chapter)
    {
        chapterService.save(chapter);

        return Result.ok(null);
    }

    /**
     * 根据 id 查询章节
     *
     * @param id 章节 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "查询章节", description = "根据 id 查询章节")
    @GetMapping("get/{id}")
    public Result<Chapter> getChapter(@Parameter(name = "id", description = "章节ID", required = true)
                                      @PathVariable("id") Long id)
    {
        Chapter chapter = chapterService.getById(id);

        return Result.ok(chapter);
    }

    /**
     * 修改章节
     *
     * @param chapter 章节数据
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "修改章节", description = "修改章节")
    @PostMapping("update")
    public Result<Void> updateChapter(@RequestBody Chapter chapter)
    {
        chapterService.updateById(chapter);

        return Result.ok(null);
    }

    /**
     * 根据 id 删除章节
     *
     * @param id 章节 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "删除章节", description = "根据 id 删除章节")
    @DeleteMapping("remove/{id}")
    public Result<Void> removeChapter(@Parameter(name = "id", description = "章节ID", required = true)
                                      @PathVariable("id") Long id)
    {
        chapterService.removeById(id);

        return Result.ok(null);
    }
}
1.2、编写章节 Service

(1)ChapterService

package com.myxh.smart.planet.vod.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.vod.Chapter;
import com.myxh.smart.planet.vo.vod.ChapterVo;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 章节 服务类
 * </p>
 */
public interface ChapterService extends IService<Chapter>
{
    /**
     * 大纲列表,获取章节和小节列表
     *
     * @param courseId 课程 id
     * @return chapterVoList 章节和小节列表
     */
    List<ChapterVo> getNestedTreeList(Long courseId);

    /**
     * 根据课程 id 删除章节
     *
     * @param id 课程 id
     */
    void removeChapterByCourseId(Long id);
}

(2)ChapterServiceImpl

package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Chapter;
import com.myxh.smart.planet.model.vod.Video;
import com.myxh.smart.planet.vo.vod.ChapterVo;
import com.myxh.smart.planet.vo.vod.VideoVo;
import com.myxh.smart.planet.vod.mapper.ChapterMapper;
import com.myxh.smart.planet.vod.service.ChapterService;
import com.myxh.smart.planet.vod.service.VideoService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 章节 服务实现类
 * </p>
 */
@Service
public class ChapterServiceImpl extends ServiceImpl<ChapterMapper, Chapter> implements ChapterService
{
    @Autowired
    private VideoService videoService;

    /**
     * 大纲列表,获取章节和小节列表
     *
     * @param courseId 课程 id
     * @return chapterVoList 章节和小节列表
     */
    @Override
    public List<ChapterVo> getNestedTreeList(Long courseId)
    {
        // 定义章节和小节列表 List 集合
        List<ChapterVo> chapterVoList = new ArrayList<>();

        // 根据 courseId 获取课程里面所有章节
        QueryWrapper<Chapter> chapterQueryWrapper = new QueryWrapper<>();
        chapterQueryWrapper.eq("course_id", courseId);
        List<Chapter> chapterList = baseMapper.selectList(chapterQueryWrapper);

        // 根据 courseId 获取课程里面所有小节
        LambdaQueryWrapper<Video> videoLambdaQueryWrapper = new LambdaQueryWrapper<>();
        videoLambdaQueryWrapper.eq(Video::getCourseId, courseId);
        videoLambdaQueryWrapper.orderByAsc(Video::getSort, Video::getId);
        List<Video> videoList = videoService.list(videoLambdaQueryWrapper);

        // 封装章节
        // 遍历所有的章节
        for (Chapter chapter : chapterList)
        {
            // 创建 ChapterVo 对象
            ChapterVo chapterVo = new ChapterVo();
            BeanUtils.copyProperties(chapter, chapterVo);
            chapterVoList.add(chapterVo);

            // 封装章节里面的小节
            // 创建 List 集合用来封装章节所有小节
            List<VideoVo> videoVoList = new ArrayList<>();

            // 遍历小节 List
            for (Video video : videoList)
            {
                // 判断小节是哪个章节下面
                if (chapter.getId().equals(video.getChapterId()))
                {
                    VideoVo videoVo = new VideoVo();
                    BeanUtils.copyProperties(video, videoVo);
                    videoVoList.add(videoVo);
                }
            }

            // 把章节里面所有小节集合放到每个章节里面
            chapterVo.setChildren(videoVoList);
        }

        return chapterVoList;
    }

    /**
     * 根据课程 id 删除章节
     *
     * @param id 课程 id
     */
    @Override
    public void removeChapterByCourseId(Long id)
    {
        QueryWrapper<Chapter> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", id);
        baseMapper.delete(wrapper);
    }
}
2、课程小节接口
2.1、编写 VideoController
package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.model.vod.Video;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vod.service.VideoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程视频 前端控制器
 * </p>
 */
@Tag(name = "课程视频小节接口", description = "课程视频小节管理接口")
@RestController
@RequestMapping("/admin/vod/video")
@CrossOrigin
public class VideoController
{
    @Autowired
    private VideoService videoService;

    /**
     * 获取课程视频小节
     *
     * @param id 课程视频小节 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "获取课程视频小节", description = "获取课程视频小节")
    @GetMapping("get/{id}")
    public Result<Video> get(@Parameter(name = "id", description = "课程视频小节ID", required = true)
                             @PathVariable("id") Long id)
    {
        Video video = videoService.getById(id);

        return Result.ok(video);
    }

    /**
     * 新增课程视频小节
     *
     * @param video 课程视频小节数据
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "新增课程视频小节", description = "新增课程视频小节")
    @PostMapping("save")
    public Result<Void> save(@RequestBody Video video)
    {
        videoService.save(video);

        return Result.ok(null);
    }

    /**
     * 修改课程视频小节
     *
     * @param video 课程视频小节数据
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "修改课程视频小节", description = "修改课程视频小节")
    @PostMapping("update")
    public Result<Void> update(@RequestBody Video video)
    {
        videoService.updateById(video);

        return Result.ok(null);
    }

    /**
     * 删除课程视频小节
     *
     * @param id 课程视频小节 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "删除课程视频小节", description = "删除课程视频小节")
    @DeleteMapping("remove/{id}")
    public Result<Void> remove(@Parameter(name = "id", description = "课程视频小节ID", required = true)
                               @PathVariable("id") Long id)
    {
        videoService.removeById(id);

        return Result.ok(null);
    }
}
3、课程大纲前端
3.1、定义接口

定义接口

(1)chapter.js

import request from "@/utils/request";

const CHAPTER_API = "/admin/vod/chapter";

export default {
  /**
   * 大纲列表,获取章节和小节列表
   *
   * @param {number} courseId 课程 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getNestedTreeList(courseId) {
    return request({
      url: `${CHAPTER_API}/get/nested/tree/list/${courseId}`,
      method: "get",
    });
  },

  /**
   * 添加章节
   *
   * @param {Object} chapter 章节数据
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  save(chapter) {
    return request({
      url: `${CHAPTER_API}/save`,
      method: "post",
      data: chapter,
    });
  },

  /**
   * 根据 id 查询章节
   *
   * @param {number} id 章节 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getById(id) {
    return request({
      url: `${CHAPTER_API}/get/${id}`,
      method: "get",
    });
  },

  /**
   * 修改章节
   *
   * @param {Object} chapter 章节数据
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  updateById(chapter) {
    return request({
      url: `${CHAPTER_API}/update`,
      method: "post",
      data: chapter,
    });
  },

  /**
   * 根据 id 删除章节
   *
   * @param {number} id 章节 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  removeById(id) {
    return request({
      url: `${CHAPTER_API}/remove/${id}`,
      method: "delete",
    });
  },
};

(2)创建 video.js。

import request from "@/utils/request";

const VIDEO_API = "/admin/vod/video";

export default {
  /**
   * 获取课程视频小节
   *
   * @param {number} id 课程视频小节 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getById(id) {
    return request({
      url: `${VIDEO_API}/get/${id}`,
      method: "get",
    });
  },

  /**
   * 新增课程视频小节
   *
   * @param {Object}video 课程视频小节数据
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  save(video) {
    return request({
      url: `${VIDEO_API}/save`,
      method: "post",
      data: video,
    });
  },

  /**
   * 修改课程视频小节
   *
   * @param {Object}video 课程视频小节数据
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  updateById(video) {
    return request({
      url: `${VIDEO_API}/update`,
      method: "post",
      data: video,
    });
  },

  /**
   * 删除课程视频小节
   *
   * @param {number} id 课程视频小节 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  removeById(id) {
    return request({
      url: `${VIDEO_API}/remove/${id}`,
      method: "delete",
    });
  },
};
3.2、编写章节页面

编写章节页面

(1)chapter/index.vue

<template>
  <div class="app-container">
    <!-- 添加章节按钮 -->
    <div>
      <el-button type="primary" @click="addChapter()">添加章节</el-button>
    </div>

    <!-- 章节列表 -->
    <ul class="chapterList">
      <li v-for="chapter in chapterList" :key="chapter.id">
        <p>
          {{ chapter.title }}
          <span class="acts">
            <el-button type="text" @click="addVideo(chapter.id)"
              >添加课时</el-button
            >
            <el-button type="text" @click="editChapter(chapter.id)"
              >编辑</el-button
            >
            <el-button type="text" @click="removeChapterById(chapter.id)"
              >删除</el-button
            >
          </span>
        </p>

        <!-- 视频 -->
        <ul class="chapterList videoList">
          <li v-for="video in chapter.children" :key="video.id">
            <p>
              {{ video.title }}
              <el-tag v-if="!video.videoSourceId" size="mini" type="danger">
                {{ "尚未上传视频" }}
              </el-tag>
              <span class="acts">
                <el-tag v-if="video.isFree" size="mini" type="success"
                  >{{ "免费观看" }}</el-tag
                >
                <el-button type="text" @click="editVideo(chapter.id, video.id)"
                  >编辑</el-button
                >
                <el-button type="text" @click="removeVideoById(video.id)"
                  >删除</el-button
                >
              </span>
            </p>
          </li>
        </ul>
      </li>
    </ul>

    <!-- 章节表单对话框 -->
    <chapter-form ref="chapterForm" />

    <!-- 课时表单对话框 -->
    <video-form ref="videoForm" />

    <div style="text-align: center">
      <el-button type="primary" @click="prev()">上一步</el-button>
      <el-button type="primary" @click="next()">下一步</el-button>
    </div>
  </div>
</template>

<script>
  import chapterAPI from "@/api/vod/chapter";
  import videoAPI from "@/api/vod/video";

  // 引入组件
  import chapterForm from "@/views/vod/course/components/chapter/form";
  import videoForm from "@/views/vod/course/components/video/form";

  export default {
    // 注册组件
    components: { chapterForm, videoForm },

    data() {
      return {
        // 章节嵌套列表
        chapterList: [],
      };
    },

    created() {
      this.fetchNodeList();
    },

    methods: {
      // 获取章节小节数据
      fetchNodeList() {
        chapterAPI.getNestedTreeList(this.$parent.courseId).then((response) => {
          this.chapterList = response.data;
        });
      },

      // 添加章节
      addChapter() {
        this.$refs.chapterForm.open();
      },

      // 编辑章节
      editChapter(chapterId) {
        this.$refs.chapterForm.open(chapterId);
      },

      // 删除章节
      removeChapterById(chapterId) {
        this.$confirm("此操作将永久删除该章节,是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            return chapterAPI.removeById(chapterId);
          })
          .then((response) => {
            this.fetchNodeList();
            this.$message.success(response.message);
          })
          .catch((response) => {
            if (response === "cancel") {
              this.$message.info("取消删除");
            }
          });
      },

      // 添加课时
      addVideo(chapterId) {
        this.$refs.videoForm.open(chapterId);
      },

      // 编辑课时
      editVideo(chapterId, videoId) {
        this.$refs.videoForm.open(chapterId, videoId);
      },

      // 删除课时
      removeVideoById(videoId) {
        this.$confirm("此操作将永久删除该课时, 是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            return videoAPI.removeById(videoId);
          })
          .then((response) => {
            this.fetchNodeList();
            this.$message.success(response.message);
          })
          .catch((response) => {
            if (response === "cancel") {
              this.$message.info("取消删除");
            }
          });
      },

      // 上一步
      prev() {
        this.$parent.active = 0;
      },

      // 下一步
      next() {
        this.$parent.active = 2;
      },
    },
  };
</script>

(2)chapter/form.vue

<template>
  <!-- 添加和修改章节表单 -->
  <el-dialog :visible="dialogVisible" title="添加章节" @close="close()">
    <el-form :model="chapter" label-width="120px">
      <el-form-item label="章节标题">
        <el-input v-model="chapter.title" />
      </el-form-item>
      <el-form-item label="章节排序">
        <el-input-number v-model="chapter.sort" :min="0" />
      </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button @click="close()">取 消</el-button>
      <el-button type="primary" @click="saveOrUpdate()">确 定</el-button>
    </div>
  </el-dialog>
</template>

<script>
  import chapterAPI from "@/api/vod/chapter";

  export default {
    data() {
      return {
        dialogVisible: false,

        chapter: {
          sort: 0,
        },
      };
    },

    methods: {
      open(chapterId) {
        this.dialogVisible = true;

        if (chapterId) {
          chapterAPI.getById(chapterId).then((response) => {
            this.chapter = response.data;
          });
        }
      },

      close() {
        this.dialogVisible = false;
        // 重置表单
        this.resetForm();
      },

      resetForm() {
        this.chapter = {
          sort: 0,
        };
      },

      saveOrUpdate() {
        this.dialogVisible = true;

        if (!this.chapter.title) {
          this.$message.error("请输入章节标题");
          this.dialogVisible = false;

          return;
        }

        if (!this.chapter.id) {
          this.save();
        } else {
          this.update();
        }
      },

      save() {
        this.chapter.courseId = this.$parent.$parent.courseId;
        chapterAPI.save(this.chapter).then((response) => {
          this.$message.success(response.message);
          // 关闭组件
          this.close();

          // 刷新列表
          this.$parent.fetchNodeList();
        });
      },

      update() {
        chapterAPI.updateById(this.chapter).then((response) => {
          this.$message.success(response.message);
          // 关闭组件
          this.close();

          // 刷新列表
          this.$parent.fetchNodeList();
        });
      },
    },
  };
</script>
3.3、编写小节(课时)页面

编写小节(课时)页面

(1)video/form.vue

<template>
  <!-- 添加和修改课时表单 -->
  <el-dialog :visible="dialogVisible" title="添加课时" @close="close()">
    <el-form :model="video" label-width="120px">
      <el-form-item label="课时标题">
        <el-input v-model="video.title" />
      </el-form-item>
      <el-form-item label="课时排序">
        <el-input-number v-model="video.sort" :min="0" />
      </el-form-item>
      <el-form-item label="是否免费">
        <el-radio-group v-model="video.isFree">
          <el-radio :label="0">收费</el-radio>
          <el-radio :label="1">免费</el-radio>
        </el-radio-group>
      </el-form-item>

      <!-- 上传视频 -->
      <el-form-item label="上传视频">
        <el-upload
          ref="upload"
          :auto-upload="false"
          :on-success="handleUploadSuccess"
          :on-error="handleUploadError"
          :on-exceed="handleUploadExceed"
          :file-list="fileList"
          :limit="1"
          :before-remove="handleBeforeRemove"
          :on-remove="handleOnRemove"
          :action="BASE_API + '/admin/vod/upload'"
        >
          <el-button slot="trigger" size="small" type="primary"
            >选择视频</el-button
          >
          <el-button
            :disabled="uploadBtnDisabled"
            style="margin-left: 10px"
            size="small"
            type="success"
            @click="submitUpload()"
            >上传</el-button
          >
        </el-upload>
      </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button @click="close()">取 消</el-button>
      <el-button type="primary" @click="saveOrUpdate()">确 定</el-button>
    </div>
  </el-dialog>
</template>

<script>
  import videoAPI from "@/api/vod/video";

  export default {
    data() {
      return {
        BASE_API: "http://localhost:8301",
        dialogVisible: false,

        video: {
          sort: 0,
          free: false,
        },

        // 上传文件列表
        fileList: [],
        uploadBtnDisabled: false,
      };
    },

    methods: {
      open(chapterId, videoId) {
        this.dialogVisible = true;
        this.video.chapterId = chapterId;

        if (videoId) {
          videoAPI.getById(videoId).then((response) => {
            this.video = response.data;
            // 回显
            if (this.video.videoOriginalName) {
              this.fileList = [{ name: this.video.videoOriginalName }];
            }
          });
        }
      },

      close() {
        this.dialogVisible = false;

        // 重置表单
        this.resetForm();
      },

      resetForm() {
        this.video = {
          sort: 0,
          free: false,
        };

        // 重置视频上传列表
        this.fileList = [];
      },

      saveOrUpdate() {
        this.dialogVisible = true;

        if (!this.video.title) {
          this.$message.error("请输入课时标题");
          this.dialogVisible = false;

          return;
        }

        if (!this.video.id) {
          this.save();
        } else {
          this.update();
        }
      },

      save() {
        this.video.courseId = this.$parent.$parent.courseId;
        videoAPI.save(this.video).then((response) => {
          this.$message.success(response.message);
          // 关闭组件
          this.close();

          // 刷新列表
          this.$parent.fetchNodeList();
        });
      },

      update() {
        videoAPI.updateById(this.video).then((response) => {
          this.$message.success(response.message);
          // 关闭组件
          this.close();

          // 刷新列表
          this.$parent.fetchNodeList();
        });
      },

      // 处理上传超出一个视频
      handleUploadExceed(files, fileList) {
        this.$message.warning("想要重新上传视频,请先删除已上传的视频");
      },

      // 上传
      submitUpload() {
        this.uploadBtnDisabled = true;

        // 提交上传请求
        this.$refs.upload.submit();
      },

      // 视频上传成功的回调
      handleUploadSuccess(response, file, fileList) {
        this.uploadBtnDisabled = false;
        this.video.videoSourceId = response.data;
        this.video.videoOriginalName = file.name;
      },

      // 失败回调
      handleUploadError() {
        this.uploadBtnDisabled = false;
        this.$message.error("上传失败");
      },

      // 删除视频文件确认
      handleBeforeRemove(file, fileList) {
        return this.$confirm(`确定移除 ${file.name}`);
      },

      // 执行视频文件的删除
      handleOnRemove(file, fileList) {
        if (!this.video.videoSourceId) {
          return;
        }
      },
    },
  };
</script>

二、发布课程-课程最终发布

1、课程最终发布接口
1.1、编写 CourseController

添加方法。

package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.vod.CoursePublishVo;
import com.myxh.smart.planet.vod.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 前端控制器
 * </p>
 */
@Tag(name = "课程接口", description = "课程管理接口")
@RestController
@RequestMapping("/admin/vod/course")
@CrossOrigin
public class CourseController
{
    @Autowired
    private CourseService courseService;

    /**
     * 根据课程 id 查询课程发布信息
     *
     * @param id 课程 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "根据课程 id 查询课程发布信息", description = "根据课程 id 查询课程发布信息")
    @GetMapping("get/course/publish/vo/{id}")
    public Result<CoursePublishVo> getCoursePublishVoById(@Parameter(name = "id", description = "课程ID", required = true)
                                                          @PathVariable Long id)
    {
        CoursePublishVo coursePublishVo = courseService.getCoursePublishVo(id);

        return Result.ok(coursePublishVo);
    }

    /**
     * 根据课程 id 最终发布课程
     *
     * @param id 课程 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "根据课程 id 最终发布课程", description = "根据课程 id 最终发布课程")
    @PutMapping("publish/course/{id}")
    public Result<Void> publishCourseById(@Parameter(name = "id", description = "课程ID", required = true)
                                          @PathVariable Long id)
    {
        courseService.publishCourse(id);

        return Result.ok(null);
    }
}
1.2、编写 CourseService
package com.myxh.smart.planet.vod.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vo.vod.CoursePublishVo;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务类
 * </p>
 */
public interface CourseService extends IService<Course>
{
    /**
     * 根据课程 id 查询课程发布信息
     *
     * @param id 课程 id
     * @return coursePublishVo 课程发布信息
     */
    CoursePublishVo getCoursePublishVo(Long id);

    /**
     * 根据课程 id 最终发布课程
     *
     * @param id 课程 id
     */
    void publishCourse(Long id);
}
1.3、编写 CourseServiceImpl
package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vo.vod.CoursePublishVo;
import com.myxh.smart.planet.vod.mapper.CourseMapper;
import com.myxh.smart.planet.vod.service.CourseService;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务实现类
 * </p>
 */
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService
{
    /**
     * 根据课程 id 查询课程发布信息
     *
     * @param id 课程 id
     * @return coursePublishVo 课程发布信息
     */
    @Override
    public CoursePublishVo getCoursePublishVo(Long id)
    {
        return baseMapper.selectCoursePublishVoById(id);
    }

    /**
     * 根据课程 id 最终发布课程
     *
     * @param id 课程 id
     */
    @Override
    public void publishCourse(Long id)
    {
        Course course = baseMapper.selectById(id);

        // 已经发布课程
        course.setStatus(1);
        course.setPublishTime(new Date());
        baseMapper.updateById(course);
    }
}
1.4、编写 CourseMapper
package com.myxh.smart.planet.vod.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vo.vod.CoursePublishVo;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 Mapper 接口
 * </p>
 */
public interface CourseMapper extends BaseMapper<Course>
{
    /**
     * 根据课程 id 查询课程发布信息
     *
     * @param id 课程 id
     * @return coursePublishVo 课程发布信息
     */
    CoursePublishVo selectCoursePublishVoById(Long id);
}
1.5、编写 CourseMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myxh.smart.planet.vod.mapper.CourseMapper">
    <select id="selectCoursePublishVoById" resultType="com.myxh.smart.planet.vo.vod.CoursePublishVo">
        SELECT c.id,
               c.title,
               c.cover,
               c.lesson_num AS lessonNum,
               c.price,
               t.name       AS teacherName,
               s1.title     AS subjectParentTitle,
               s2.title     AS subjectTitle
        FROM `course` AS c
                 LEFT OUTER JOIN `teacher` t ON c.teacher_id = t.id
                 LEFT OUTER JOIN `subject` s1 ON c.subject_parent_id = s1.id
                 LEFT OUTER JOIN `subject` s2 ON c.subject_id = s2.id
        WHERE c.id = #{id}
    </select>
</mapper>
1.6、添加配置

(1)application.properties 添加。

# 设置 mapper.xml 的路径
mybatis-plus.mapper-locations=classpath:com/myxh/smart/planet/vod/mapper/xml/*.xml

(2)service 模块 pom.xml 添加。

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>

    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.yml</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>

        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.yml</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>
2、课程最终发布前端
2.1、course.js 定义接口
import request from "@/utils/request";

const COURSE_API = "/admin/vod/course";

export default {
  /**
   * 根据课程 id 查询课程发布信息
   *
   * @param {number} id 课程 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getCoursePublishById(id) {
    return request({
      url: `${COURSE_API}/get/course/publish/vo/${id}`,
      method: "get",
    });
  },

  /**
   * 根据课程 id 最终发布课程
   *
   * @param {number} id 课程 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  publishCourseById(id) {
    return request({
      url: `${COURSE_API}/publish/course/${id}`,
      method: "put",
    });
  },
};
2.2、编写 publish.vue

编写 publish.vue

<template>
  <div class="app-container">
    <head>
      <!-- 设置 referrer 为 no-referrer,用于绕过防盗链限制,从而正常显示图片 -->
      <meta name="referrer" content="no-referrer" />
    </head>

    <!-- 课程预览 -->
    <div class="ccInfo">
      <img :src="coursePublish.cover" style="width: 200px; height: auto" />
      <div class="main">
        <h2>{{ coursePublish.title }}</h2>
        <p class="gray">
          <span>共{{ coursePublish.lessonNum }}课时</span>
        </p>
        <p>
          <span
            >所属分类:{{ coursePublish.subjectParentTitle }} — {{
            coursePublish.subjectTitle }}</span
          >
        </p>
        <p>课程教师:{{ coursePublish.teacherName }}</p>
        <h3 class="red">¥{{ coursePublish.price }}</h3>
      </div>
    </div>
    <div style="text-align: center">
      <el-button type="primary" @click="prev()">上一步</el-button>
      <el-button
        :disabled="publishBtnDisabled"
        type="primary"
        @click="publish()"
        >发布课程</el-button
      >
    </div>
  </div>
</template>

<script>
  import courseAPI from "@/api/vod/course";

  export default {
    data() {
      return {
        // 按钮是否禁用
        publishBtnDisabled: false,
        coursePublish: {},
      };
    },

    created() {
      if (this.$parent.courseId) {
        this.fetchCoursePublishById(this.$parent.courseId);
      }
    },

    methods: {
      // 获取课程发布信息
      fetchCoursePublishById(id) {
        courseAPI.getCoursePublishById(id).then((response) => {
          this.coursePublish = response.data;
        });
      },

      // 上一步
      prev() {
        this.$parent.active = 1;
      },

      // 下一步
      publish() {
        this.publishBtnDisabled = true;
        courseAPI.publishCourseById(this.$parent.courseId).then((response) => {
          this.$parent.active = 3;
          this.$message.success(response.message);
          this.$router.push({ path: "/vod/course/course/list" });
        });
      },
    },
  };
</script>

三、功能实现-课程删除

1、课程删除接口
1.1、编写课程 Controller
package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vod.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 前端控制器
 * </p>
 */
@Tag(name = "课程接口", description = "课程管理接口")
@RestController
@RequestMapping("/admin/vod/course")
@CrossOrigin
public class CourseController
{
    @Autowired
    private CourseService courseService;

    /**
     * 删除课程
     *
     * @param id 课程 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "删除课程", description = "删除课程")
    @DeleteMapping("remove/{id}")
    public Result<Void> remove(@Parameter(name = "id", description = "课程ID", required = true)
                               @PathVariable Long id)
    {
        courseService.removeCourseById(id);

        return Result.ok(null);
    }
}
1.2、编写课程 Service
package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vod.mapper.CourseMapper;
import com.myxh.smart.planet.vod.service.ChapterService;
import com.myxh.smart.planet.vod.service.CourseDescriptionService;
import com.myxh.smart.planet.vod.service.CourseService;
import com.myxh.smart.planet.vod.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务实现类
 * </p>
 */
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService
{
    @Autowired
    private CourseDescriptionService courseDescriptionService;

    @Autowired
    private ChapterService chapterService;

    @Autowired
    private VideoService videoService;

    /**
     * 删除课程
     *
     * @param id 课程 id
     */
    @Override
    public void removeCourseById(Long id)
    {
        // 根据课程 id 删除小节
        videoService.removeVideoByCourseId(id);

        // 根据课程 id 删除章节
        chapterService.removeChapterByCourseId(id);

        // 根据课程 id 删除描述
        courseDescriptionService.removeByCourseId(id);

        // 根据课程 id 删除课程
        baseMapper.deleteById(id);
    }
}
1.3、编写 VideoService
package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Video;
import com.myxh.smart.planet.vod.mapper.VideoMapper;
import com.myxh.smart.planet.vod.service.VideoService;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程视频 服务实现类
 * </p>
 */
@Service
public class VideoServiceImpl extends ServiceImpl<VideoMapper, Video> implements VideoService
{
    /**
     * 根据课程 id 删除课程视频小节
     *
     * @param id 课程 id
     */
    @Override
    public void removeVideoByCourseId(Long id)
    {
        QueryWrapper<Video> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", id);
        baseMapper.delete(wrapper);
    }
}
1.4、编写 ChapterService
package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Chapter;
import com.myxh.smart.planet.vod.mapper.ChapterMapper;
import com.myxh.smart.planet.vod.service.ChapterService;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 章节 服务实现类
 * </p>
 */
@Service
public class ChapterServiceImpl extends ServiceImpl<ChapterMapper, Chapter> implements ChapterService
{
    /**
     * 根据课程 id 删除章节
     *
     * @param id 课程 id
     */
    @Override
    public void removeChapterByCourseId(Long id)
    {
        QueryWrapper<Chapter> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", id);
        baseMapper.delete(wrapper);
    }
}
1.5 编写 CourseDescriptionService
package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.CourseDescription;
import com.myxh.smart.planet.vod.mapper.CourseDescriptionMapper;
import com.myxh.smart.planet.vod.service.CourseDescriptionService;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程简介 服务实现类
 * </p>
 */
@Service
public class CourseDescriptionServiceImpl extends ServiceImpl<CourseDescriptionMapper, CourseDescription> implements CourseDescriptionService
{
    /**
     * 根据课程 id 删除描述
     *
     * @param id 课程 id
     */
    @Override
    public void removeByCourseId(Long id)
    {
        QueryWrapper<CourseDescription> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", id);
        baseMapper.delete(wrapper);
    }
}
2、课程删除前端
2.1、course.js 定义接口
import request from "@/utils/request";

const COURSE_API = "/admin/vod/course";

export default {
  /**
   * 删除课程
   *
   * @param {number} id 课程 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  removeById(id) {
    return request({
      url: `${COURSE_API}/remove/${id}`,
      method: "delete",
    });
  },
};
2.2、course/list.vue 添加方法
methods: {
  // 根据 id 删除数据
  removeById(id) {
    this.$confirm(
      "此操作将永久删除该课程,以及该课程下的章节和视频,是否继续?",
      "提示",
      {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }
    )
      .then(() => {
        return courseAPI.removeById(id);
      })
      .then((response) => {
        this.fetchData();
        this.$message.success(response.message);
      });
  },
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

末影小黑xh

感谢朋友们对我的支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值