vue-admin-template模板
谷粒学苑项目框架
框架入口
index.html
和 src/main.js
框架作用
对 Vue 和 Element-ui 的封装
配置目录
index.js 文件
'use strict'
const path = require('path')
module.exports = {
dev: {
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
host: 'localhost',
port: 9528,
autoOpenBrowser: true,
errorOverlay: true,
notifyOnErrors: false,
poll: false,
// 这里修改为false, 不需要使用这么严格的Eslint检查
useEslint: false,
showEslintErrorsInOverlay: false,
devtool: 'cheap-source-map',
cssSourceMap: false
},
build: {
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
productionSourceMap: false,
devtool: 'source-map',
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
bundleAnalyzerReport: process.env.npm_config_report || false,
generateAnalyzerReport: process.env.npm_config_generate_report || false
}
}
dev.env.js 开发环境配置
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
// BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
// 修改BASE_API, 使得可以登录成功
BASE_API: '"http://localhost:8001"',
})
工具目录
request.js 文件中通过将 axios 封装成
import axios from 'axios'
import {Message, MessageBox} from 'element-ui'
import store from '../store'
import {getToken} from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
response => {
/**
* code为非20000是抛错 可结合自己业务进行修改
*/
const res = response.data
if (res.code !== 20000) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000
})
// 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
MessageBox.confirm(
'你已被登出,可以取消继续留在该页面,或者重新登录',
'确定登出',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
} else {
return response.data
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
vue.config.js
配置文件, 修改访问端口地址
src目录
api目录
自定义方法
asset目录
静态资源
components目录
存放组件
icons目录
存放图标
router目录
路由
style目录
样式文件
utils目录
request文件
views目录
项目中的具体页面
@RestController表示交给Spring管理, 并返回数据
操作流程
- 修改vue.config.js中的端口号为8001
添加路由
- 在router/index.js中添加
- title属性决定页面的标题
- component表示点击路由跳转到指定页面
讲师列表的前端实现
添加路由
在
router/index.js
中模仿样例改造一级路由讲师管理
和二级路由创建路由对应的页面并修改
router/index.js
中对应的路由映射路径- 讲师列表
views/edu/teacher/list.vue
- 添加讲师
views/edu/teacher/save.vue
- 讲师列表
vue
页面的基本写法添加讲师在
api
文件夹中创建teacher.js
, 在其中定义接口的访问地址// 引入utils/request, request中封装了axios import request from '@/utils/request' export function getList(params) { return request({ url: '/vue-admin-template/table/list', method: 'get', params }) }
vue-admin-template 项目改造
后端请求路径改造
vue-admin-template 默认使用 https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin/user/login 地址进行请求,需要修改 dev.env.js 中的 BASE_API
属性
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"http://localhost:8001"',
})
页面改造
讲师管理模块页面改造
路由设置
// src/router/index.js
// 侧边栏, 主体框架
{
path: '/teacher',
component: Layout,
// 重定向到子路由的路径上, 相当于这一个分支的默认展示页面
redirect: '/teacher/table',
name: '讲师管理',
meta: {
// title 的修改会反映到对应页面上
title: '讲师管理',
// icon 是 src/icons/svg 目录下存储的矢量图片
icon: 'user'
},
children: [
{
// 子路由的路径: /teacher/table
path: 'table',
name: '讲师列表',
// '@/views/edu/teacher/table' 这些页面从现有的模板中进行拷贝, 或者自己写页面
component: () => import('@/views/edu/teacher/table'),
meta: {
title: '讲师列表',
icon: 'table'
}
},
{
// 子路由的路径: /teacher/tree
path: 'tree',
name: '讲师层次',
component: () => import('@/views/edu/teacher/tree'),
meta: {
title: '讲师层次',
icon: 'tree'
}
},
{
path: '/add',
name: '添加讲师',
component: () => import('@/views/edu/teacher/add'),
meta: {
title: '添加讲师',
icon: 'form'
}
},
]
},
页面设置

效果展示
后台的前端项目开发
login 登录业务
前端接口修改(src/api/login.js)
import request from '@/utils/request'
export function login(username, password) {
return request({
url: '/edu/user/login',
method: 'post',
data: {
username,
password
}
})
}
export function getInfo(token) {
return request({
url: '/edu/user/info',
method: 'get',
params: {token}
})
}
export function logout() {
return request({
url: '/edu/user/logout',
method: 'post'
})
}
后端接口修改
package com.xiong.controller;
import com.xiong.Result;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.*;
/**
* RestController注解: 表示该类交给Spring管理并返回数据
* <p>
* CrossOrigin注解: 用来解决跨域问题
* <p>
* 这里的逻辑纯粹是为了迎合前端vue-admin-template模板中的要求
*/
@RestController
@RequestMapping("/edu/user")
@Api(description = "登录管理")
@CrossOrigin
public class EduLoginController {
@PostMapping("login")
public Result login() {
// "token"是由于vue-admin-template模板登录功能store/user中使用的token
return Result.ok()
.setData("token", "admin");
}
@GetMapping("info")
public Result info() {
// 这里的name和avatar应该和前端关键字相同吧
return Result.ok()
.setData("roles", "[admin]")
.setData("name", "admin")
.setData("avatar", "https://gw.alicdn.com/i4/710600684/O1CN01bNcLnV1GvJd3wK1k2_!!710600684.jpg_Q75.jpg_.webp");
}
}
分页查询业务
后端接口
@ApiOperation(value = "带条件的分页查询")
@PostMapping("pageTeacherCondition/{current}/{limit}")
public Result pageTeacherCondition(@ApiParam(name = "current", value = "当前页", required = true) @PathVariable long current,
@ApiParam(name = "limit", value = "每页最多个数", required = true) @PathVariable long limit,
@RequestBody(required = false) TeacherQuery teacherQuery) {
QueryWrapper<Teacher> wrapper = null;
if (teacherQuery != null) {
// 构建查询条件
wrapper = new QueryWrapper<Teacher>();
// 多条件组合查询, 相当于mybatis中的动态sql
String name = teacherQuery.getName();
Integer level = teacherQuery.getLevel();
String begin = teacherQuery.getBegin();
String end = teacherQuery.getEnd();
// 拼接上下面的查询条件
if (name != null) {
wrapper.like("name", name);
}
if (level != null) {
wrapper.eq("level", level);
}
if (begin != null) {
wrapper.ge("gmt_create", begin);
}
if (end != null) {
wrapper.le("gmt_modified", end);
}
// 对数据进行排序, 按照创建时间倒序排序
wrapper.orderByDesc("gmt_create");
}
//current和limit的参数类型是根据代码内部逻辑确定的, 而不是固定的String类型
Page<Teacher> teacherPage = new Page<>(current, limit);
//将数据封装到teacherPage对象中
teacherService.page(teacherPage, wrapper);
// 获取返回数据的总条数
long total = teacherPage.getTotal();
// 获取每一条返回数据
List<Teacher> records = teacherPage.getRecords();
return Result.ok().setData("total", total).setData("rows", records);
}
前端接口
import request from '@/utils/request'
// 导出为一个对象(例如为teacher), 可以通过teacher.pageQuery来调用方法, 不需要import {pageQuery, add, logout} from '@/api/edu/teacher'
export default {
pageQuery(current, pageSize, queryObject) {
return request({
// 通过路径进行传递值
url: `/edu/teacher/pageTeacherCondition/${current}/${pageSize}`,
method: 'post',
data: {
// data 的作用是将对象转为json
// 通过 post 请求体进行传递值, 对应后端的 @RequestBody (将json数据变为对象)
queryObject
}
})
},
/**
* 添加一位教师
* @param teacherObject 待添加的teacher对象
*/
add(teacherObject) {
return request({
url: '/edu/teacher/add',
method: 'post',
data: {
teacherObject
}
})
},
logout() {
return request({
url: '/edu/user/logout',
method: 'post'
})
}
}
前端页面调用接口
前端页面内容
表单组件
姓名: {{ scope.row.name }}
头衔: {{ scope.row.level === 1 ? '高级' : '首席' }}
{{ scope.row.name }}
{{ scope.row.level === 1 ? '高级讲师' : '首席讲师' }}
编辑
删除
分页组件
条件查询组件
删除业务
存在的问题
删除无法自动刷新,执行删除操作后需要手动进行一次刷新或者执行两次删除操作,才能看到删除效果
修改业务
上传图片业务
图片上传到阿里云OSS进行存储
如果想保存到自己本地的服务器该如何操作呢?这里只是图片上传功能,那通用的文件上传功能是如何实现的呢?
课程分类业务(难点)
分类任务建模
数据库中通过id和parentId进行实现
一级分类的parentId为默认的0
二级分类的parentId为一级分类的id
三级分类的parentId为二级分类的id
依此类推
id | parentId |
---|---|
10(后端) | 0(技术) |
11(前端) | 0(技术) |
100(Java) | 10 |
150(JavaScript) | 11 |
EsayExcel 实现写操作
EsayExcel 实现读操作
文件上传功能
多级别的分类
多级选择框的联动问题:例如省市县
可以返回一个map,其中key对应id(准确说是包含id,可能在前端还需要显示该对象的其他属性,往往显示的不是id属性),而value是一个子类别的列表。
这样在前端进行一级分类指定id后,可以获取该一级分类下的所有二级分类;同样在指定二级分类id后,可以获取三级分类列表,以此类推
对于省市县这种固定的分类和不固定的分类是否需要分别考虑,固定的分类直接写死避免查询数据库吗?还是有什么其他的优化?存疑
课程管理业务
课程添加过程需求分析
Vo 和 Po 问题
- 课程添加页面中包含各种信息,而这些信息分布在多张表中(edu_course 和 edu_course_description),因此需要创建一个 XXXVo 类来接收前端传入的对象,而 XXXPo 类则是和数据库表对应。(XXXPo 和 XXXVo 在简单的场景下可能是相同的,此时就没有必要额外创建一个 XXXVo 类)
- 同样由于 XXXVo 类和 XXXPo 类不完全相同,因此接收到的 XXXVo 需要将信息拆分添加到多张表中。
- 把 所属讲师 和 所属分类 在前端限制成下拉列表的形式,限制域的取值范围。所属分类是多级分类,需要实现多级联动效果(省市县)。
添加课程业务中存在的问题
添加课程在前端是三个阶段的过程:编辑基础信息、添加课程大纲、最终发布。但是目前在编辑基础信息的时候就将课程数据保存到数据库中,如果用户在执行到第二个阶段或第三个阶段希望取消,那该如何撤销之前保留的数据?
三个阶段之间的数据不能够回显,如何做到数据回显的效果?
解决思路:第一个阶段执行完成后,保存到数据库,会自动生成课程id,此时需要获取该课程的id,传递给第二、三阶段进行使用。数据回显也可以基于课程id来实现。
XXXVo 对象中不保存id,因此需要一个额外的属性来记录id值,该id值在第一次添加时由数据库生成并返回给前端进行记录,在进行修改时,前端需要通过该值来对数据库进行访问。类似cookie的作用。
视频点播能力(阿里云)
上传、自动化转码(普通视频转高清,怎么实现的呢?)、媒体资源管理、分发加速
这些功能如果需要存储在本地该如何实现呢?即老板愿意买磁盘,但是不愿意买阿里云的服务来存储视频。难道必须存储在阿里云,还是应该学点别的。
上传视频
数据库中不存储视频的地址,因为对于加密视频而言,不能通过视频地址直接播放,所以数据库中存储视频id。通过视频id来获取视频播放地址和凭证,再通过凭证来判断是否有播放(访问)权限。
删除视频
存放在阿里云的这种视频,如何保证事务一致性呢?会不会出现本地数据库中视频id被删除,但是实际阿里云存储的视频没有删除成功(先删除id,后删除视频会产生这个问题)。但先删除视频,后删除视频id,如果阿里云那边成功删除视频,但是本地删除数据库中的视频id时发生错误。这样导致本地数据库回滚,而阿里云中无法回滚,那该怎么办呢?
批量删除
在Controller中通过List来接收多个视频id,在阿里云存储视频的方式中,需要将List中的值拼接成一个以逗号分隔的字符串(因此用别人的接口,按照别人的要求)。这个需求可以使用Spring提供的工具类 StringUtils 中的 join()
方法
删除多个视频的时候,阿里云是如何保证事务的呢?会出现成功删除一部分,另外一部分删除失败吗?或者某个视频删除了一半?感觉删除还是先逻辑删除,确保业务操作简单,之后再在某个空闲的时候统一删除会更好。
播放视频
SpringCloud 微服务框架
模块和模块之间是相互独立的,一个模块引入另一个模块不属于微服务架构。因此微服务架构需要考虑微服务和微服务之间的相互调用问题(RPC)
SpringCloud 在接口调用上,会经过几个组件的配合:
Feign 接口化请求调用
将restTemplate直接硬编码的请求地址给转化成接口的形式,Feign 会根据指定的服务名去服务注册中心中查找服务地址,然后向那个微服务发送调用请求
@Component @FeignClient("${nacos-service.vod}") public interface VodClient { @GetMapping("/edu/vod/test/{id}") String getVodServiceTest(@PathVariable("id") String id); }
Hystrix 服务熔断
通过在当前微服务模块中简单实现一个 VodClient 接口的实现类,作为VodClient 接口远程调用失败时的服务降级方案。对上面的接口进行一些简单修改,在@FeignClient注解中添加上
fallback = VodClientImpl.class
即可@Component @FeignClient(value = "${nacos-service.vod}", fallback = VodClientImpl.class) public interface VodClient { @GetMapping("/edu/vod/test/{id}") String getVodServiceTest(@PathVariable("id") String id); }
@Component @ResponseBody public class VodClientImpl implements VodClient { // Hystrix 服务降级 // todo: 为什么这里会经过视图解析器, 不是添加了@ResponseBody吗? 这是IDEA的小bug @Override public String getVodServiceTest(String id) { return "服务熔断"; } }
Ribbon 负载均衡
如果被调用的微服务有多个,那么会将请求通过负载均衡策略分配到该微服务的实例
Http Client / OkHttp 进行http请求调用
前台的前端项目开发
NUXT框架
AJAX 请求:缺点是不利于SEO,不利于爬虫(百度、谷歌)的排名
服务端渲染技术
注意需要通过 nvm use 14
来切换成 node 14.x 版本,再通过 npm run dev
来启动nuxt项目
目录介绍
- assets:一般存放静态资源,例如css、js、img等
- component:存放项目相关组件,例如上传功能组件
- layouts
整合前台系统页面
首页显示banner数据(轮播图或幻灯片)
首页显示热门课程和名师
首页数据使用 Redis 进行缓存
Maven 加载机制
mapper.xml 文件应该存放在resource目录下的mapper包中(这些名称都是默认指定的,不要修改为别的名称,否则容易产生mapper文件找不到的问题)