硅谷课堂-智慧星球 Day 5~Day 8——尚硅谷项目笔记 2022 年
文章目录
- 硅谷课堂-智慧星球 Day 5~Day 8——尚硅谷项目笔记 2022 年
- Day 5-教师管理模块前端
- Day 6-整合腾讯云对象存储和课程分类管理
- Day 7-点播管理模块(一)
- Day 8-点播管理模块(二)
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 文件夹下创建以下文件夹和文件。
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)进入管理控制台。
1.2、创建 Bucket
进入管理控制台,找到存储桶列表, 创建存储桶。
输入桶名称,选择:公有读取,其他默认。
点击桶名称,进入详情页,可测试上传文件。
1.3、创建 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。
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。
(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 页面。
(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
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 页面
<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
<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);
});
},
}