SSM单体项目:拉勾教育后台管理系统(下)
任务一:SSM项目前端开发
一、Vue回顾
1.项目结构说明
- 我们使用脚手架快速构建Vue项目,项目结构如下图
|--- edu-boss 项目名称
|--- node_modules 存放依赖包的目录
|--- public 静态资源管理目录
|--- src 组件源码目录(我们写的代码)
|--- assets 存放静态图片资源(CSS也可以放在这里)
|--- components 存放基础组件,可复用
|--- router 存放了项目路由文件
|--- services 存放请求后台的 JS文件,
|--- store 保存组件之间的共享数据
|--- utils 管理公用的JS文件
|--- views 放置的为公共组件(各个主要页面)
|--- App.vue app.vue可以当做是网站首页,是一个vue项目的主组件,页面入口文件
|--- main.js 打包运行的入口文件,引入了vue模块和app.vue组件以及路由route
|--- babel.config.js babel配置文件, 对源代码进行转码(把es6=>es5)
|--- package.json 项目及工具的依赖配置文件
|--- paxkage-lock.json 依赖配置文件
|--- README.md 项目说明
|--- vue.config.js 自定义配置文件
2. Views 目录说明
CourseManage: 课程管理
AdvertiseManage: 广告管理
PermissionManage: 权限管理
CommentManage:公共
Users.vue: 用户管理
Login.vue: 登录
3.vue组件化开发
每一个*.vue 文件都可以看做是一个组件.
组件的组成部分
- template : 组件的HTML部分
- script: 组件的JS脚本 (使用ES6语法编写)
- style: 组件的CSS样式
测试页面...
二、课程模块回顾
1.课程数据展示
在开始编写前端代码之前, 导入最新的数据库脚本文件
1.1 功能分析
- Course.vue 组件,完成课程数据的展示和条件查询
- 使用ElementUI 表格进行数据展示
https://element.eleme.cn/#/zh-CN/component/table
1.2 JS代码编写
- 定义数据部分
//数据部分
data() {
//查询条件
const filter = {
courseName: "",
status: ""
};
return {
filter,
courses: [],
loading: false
};
},
//钩子函数
created() {
this.loadCourses();
},
- 根据接口文档,编写查询课程数据方法
//方法一: 加载课程数据
loadCourses() {
this.loading = true;
const data = {};
//查询条件
if (this.filter.courseName) data.courseName = this.filter.courseName;
if (this.filter.status) data.status = this.filter.status;
console.log(data);
//发送请求
return axios
.post("/course/findAllCourse", data)
.then(resp => {
console.log(resp.data.content);
this.courses = resp.data.content;
this.loading = false;
})
.catch(error => {
this.$message.error("数据获取失败! ! !");
});
},
- 条件查询( 访问的是同一个接口 )
查询
//条件查询
handleFilter() {
this.loadCourses();
},
2. 新建课程
2.1 功能分析
- 点击新建,由路由导航到 CourseItem.vue
新建课程
//新建课程 路由跳转
handleAdd() {
this.$router.push({ name: "CourseItem", params: { courseId: "new" } });
},
- router.js
{
path: "/courses/:courseId",
name: "CourseItem",
meta: { requireAuth: true, title: "课程详情" },
component: () =>
import(
/* webpackChunkName: 'courses' */
"../views/CourseManage/CourseItem.vue"
)
},
- CourseItem组件使用ElementUI中的表单来提交课程数据
https://element.eleme.cn/#/zh-CN/component/form
2.2 JS代码编写
保存
//保存课程信息
handleSave() {
this.$refs.form.validate(valid => {
if (!valid) return false;
axios
.post("/course/saveOrUpdateCourse", this.course)
.then(res => {
this.$router.back();
})
.catch(error => {
this.$message.error("保存课程信息失败! ! !");
});
});
},
3.课程图片上传
3.1 功能分析
- 在SSM前端项目中,图片上传功能使用的是公共的通用组件 UploadImage.vue.
- 在CourseItem.vue,引入了该组件
import UploadImage from "@/components/UploadImage.vue";
3.2 案例演示
为了让同学们更好的理解 图片上传组件的使用,我们创建一个Vue项目来演示图片上传组件的使用方式.
- 导入准备好的Vue 基础项目
- 在components目录下创建一个 UploadImage.vue组件
-
查看ElementUI文档,复制代码到 UploadImage.vue
https://element.eleme.cn/#/zh-CN/component/upload
- 配置路由
//布局路由
{
path: "/index",
name: "index",
component: Index,
//添加子路由,使用 children属性 来表示子路由
children: [
//图片上传子路由
{
path: "/upload",
name: "upload",
component: UploadImage,
},
],
},
- 在Index.vue 导航菜单位置添加一个图片上传选项
图片上传
- 访问页面进行测试
3.3 属性说明
3.4 组件的引入
怎么将一个组件引入另一个组件 ? 接下来我们来演示一下 引入图片组件.
- 创建一个TestUplopad.vue组件
3.5 组件的传参
UploadImage.vue
/*
组件传参
uploadUrl:图片上传路径,
getUrl: 函数
*/
props: ["uploadUrl", "getUrl"],
data() {
return {
uploadAction: this.uploadUrl
};
},
//上传成功后的回调函数
uploadSuccess(res, file) {
this.getUrl(file);
}
TestUpload.vue
methods: {
show(file) {
console.log(file.name);
}
}
3.6 课程模块图片上传
CourseItem.vue
引入图片上传组件,并使用
import UploadImage from "@/components/UploadImage.vue";
export default {
name: "CourseItem",
title: "营销信息",
components: { Editor, UploadImage },
}
4. 修改课程
- 点击编辑携带当前数据的id,导航到CourseItem.vue
编辑
//课程编辑&内容管理路由
handleNavigate(name, id) {
this.$router.push({ name, params: { courseId: id } });
},
- 在CourseItem组件的钩子函数中,会进行判断,如果是修改会先获取对应课程数据,进行回显
//钩子函数
created() {
//获取课程id
const id = this.$route.params.courseId;
if (!id) return this.redirectToError();
//判断是新建还是修改
if (id === "new") {
this.pathTitle = "新增课程";
this.$breadcrumbs = [
{ name: "Courses", text: "课程管理" },
{ text: "新增课程" }
];
} else {
this.$breadcrumbs = [
{ name: "Courses", text: "课程管理" },
{ text: "营销信息" }
];
this.loadCourse(id);
}
},
//回显课程信息
loadCourse(id) {
this.loading = true;
return axios
.get("/course/findCourseById?id=" + id)
.then(resp => {
console.log(resp);
this.pathTitle = resp.data.content.courseName;
this.course = Object.assign(this.course, resp.data.content);
this.course.id = id;
this.loading = false;
})
.catch(error => {
this.$message.error("回显数据失败! !");
});
},
- 修改课程与添加课程走的都是同一个后台接口,区别是修改操作必须要携带ID
5. 课程状态管理
点击上架或者下架完成课程状态的切换.
下架
上架
//切换课程状态
handleToggleStatus(item) {
//设置最新状态
const toggledStatus = 1 - item.status;
//请求后台接口
axios
.get("/course/updateCourseStatus", {
params: {
status: toggledStatus,
id: item.id
}
})
.then(res => {
debugger;
//设置最新的值
item.status = toggledStatus;
console.log(item);
//重新加载页面
window.location.reload;
})
.catch(error => {
this.$message.error("状态修改失败! ! !");
});
},
6. 课程内容管理
6.1 获取课程内容数据
课程内容数据包括章节与课时信息, 根据课程ID 查询课程包含的章节与课时信息
内容管理
created() {
//1.显示当前页面在网站中的位置
this.$breadcrumbs = [
{ name: "Courses", text: "课程管理" },
{ text: "课程结构" }
];
//2.从路由中获取传递的参数 课程id
const id = this.$route.params.courseId;
if (!id) return this.redirectToError();
this.loading = true;
//3.加载课程信息
this.loadCourse(id);
//4.加载课程内容
this.loadSections(id);
},
//加载课程信息
loadCourse(id) {
axios
.get("/courseContent/findCourseByCourseId?courseId=" + id)
.then(res => {
const course = res.data.content;
//将数据保存到章节表单对象中
this.addSectionForm.courseId = course.id;
this.addSectionForm.courseName = course.courseName;
//将数据保存到课时表单对象中
this.addLessonForm.courseId = course.id;
this.addLessonForm.courseName = course.courseName;
})
.catch(error => {
this.$message.error("数据获取失败! ! !");
});
},
//加载课程内容(树形结构)
loadSections(courseId) {
this.loading = true;
axios
.get("/courseContent/findSectionAndLesson?courseId=" + courseId)
.then(res => {
this.sections = res.data.content;
console.log(res.data.content);
this.loading = false;
})
.catch(error => {
this.$message.error("数据获取失败! ! !");
});
},
6.2 章节管理
- 新建章节
添加章节
新增章节,需要回显章节对应的课程名称
//显示新增章节表单
handleShowAddSection() {
this.addSectionForm = {
courseId: this.addSectionForm.courseId,
courseName: this.addSectionForm.courseName
};
this.showAddSection = true;
},
- 修改章节
编辑
//编辑章节(回显)
handleEditSection(section) {
this.addSectionForm = Object.assign(this.addSectionForm, section);
this.showAddSection = true;
},
- 添加与修改章节访问的都是同一个接口
//添加&修改章节
handleAddSection() {
axios
.post("/courseContent/saveOrUpdateSection", this.addSectionForm)
.then(res => {
this.showAddSection = false;
//重新加载列表
return this.loadSections(this.addSectionForm.courseId);
})
.then(() => {
//重置表单内容
this.addSectionForm.sectionName = "";
this.addSectionForm.description = "";
this.addSectionForm.orderNum = 0;
this.reload();
})
.catch(error => {
this.showAddSection = false;
this.$message.error("操作执行失败! ! !");
});
},
- 章节状态
章节状态有3种
//状态信息
const statusMapping = {
0: "已隐藏",
1: "待更新",
2: "已更新"
};
选择状态,点击确定修改状态
确 定
//修改章节状态
handleToggleStatus() {
//判断要修改的状态
if (this.toggleStatusForm.data.sectionName) {
//修改章节状态
axios
.get("/courseContent/updateSectionStatus", {
params: {
id: this.toggleStatusForm.id,
status: this.toggleStatusForm.status
}
})
.then(resp => {
this.toggleStatusForm.data.status = this.toggleStatusForm.status;
this.toggleStatusForm = {};
this.showStatusForm = false;
this.reload();
})
.catch(error => {
this.showStatusForm = false;
this.$message.error("修改状态失败! ! !");
});
} else {
//修改课时状态
}
},
6.3 课时管理
课时管理 包括 课时新增、课时修改、课时状态管理. 与章节管理基本相同.
三、广告模块
1 广告位管理
1.1 广告位展示
- AdvertiseSpaces.vue 组件,为广告位页面
JS部分
data() {
return {
list: null,
listLoading: false
};
},
created() {
//加载广告位数据
this.loadPromotionSpace();
},
//方法1: 加载广告位信息
loadPromotionSpace() {
this.listLoading = true;
axios
.get("/PromotionSpace/findAllPromotionSpace")
.then(res => {
this.list = res.data.content;
this.listLoading = false;
})
.catch(err => {
this.$message("加载数据失败! ! !");
});
},
1.2 添加广告位
1)点击 按钮,通过路由导航到指定组件
添加广告位
/添加广告位跳转
handleAdd() {
this.$router.push({ path: "/addAdvertiseSpace" });
},
2) 查看路由 router.js, 跳转到的是 AddAdvertiseSpace.vue
{
path: "addAdvertiseSpace",
name: "AddAdvertiseSpace",
component: () => import("../views/AdvertiseManage/AddAdvertiseSpace"),
meta: { requireAuth: true, title: "添加广告位" }
},
3) 查看AddAdvertiseSpace.vue
//显示组件,并传递了参数 isEdit="false" , 表示是新增操作
4)真正显示的组件是 AdvertiseSpaceDetail.vue
- 首先判断要进行 新增还是修改操作, 根据isEdit ,true 为修改,false为新增
//钩子函数
created() {
//判断是添加还是修改操作
if (this.isEdit) {
//修改
const id = this.$route.query.id;
this.loadPromotionSpace(id);
} else {
//新增
this.homeAdvertise = {};
}
},
新增
//方法1: 保存广告位信息
handleSave() {
this.$refs.form.validate(valid => {
if (!valid) return false;
//请求后台
axios
.post(
"/PromotionSpace/saveOrUpdatePromotionSpace",
this.homeAdvertise
)
.then(res => {
//返回上个页面
this.$router.back();
})
.catch(err => {
this.$message("数据处理失败! !");
});
});
},
1.3 修改广告位
需要请求后台接口,进行广告位信息回显
//方法2: 回显广告位信息
loadPromotionSpace(id) {
return axios
.get("/PromotionSpace/findPromotionSpaceById?id=" + id)
.then(res => {
Object.assign(this.homeAdvertise, res.data.content);
this.homeAdvertise.id = id;
})
.catch(err => {
this.$message("数据处理失败! !");
});
}
2 广告管理
2.1 ElementUI 分页组件
- Advertises.vue 组件,为广告列表页面
- 广告列表的展示,使用到了分页组件, 接下来通过一个案例演示一下分页插件的使用.
https://element.eleme.cn/#/zh-CN/component/pagination
2.1.1快速使用
- 在测试项目中,创建一个PageList.vue ,复制代码如下
完整功能
- 属性介绍
分析:
- page-size 与 current-page 是需要前端传给后端的数据
- total 和 列表数据 是需要后端返回给前端的.
- 事件介绍
- 案例演示
a.复制下面代码到 PageList
{{scope.row.id}}
{{scope.row.name}}
b.编写JS部分代码
2.2 广告列表展示
1.需求分析
我们已经解决了分页问题,接下来再看一下广告页面要展示哪些内容:
- 广告列表的展示数据来源于两张表:
- promotion_ad 广告表
- promotion_space 广告位表
2.功能实现
a.数据部分
//数据部分
data() {
return {
typeMap: {}, //保存广告位对象信息
total: 0, //总条数
size: 5, //每页显示条数
page: 1, //当前页
list: [], //广告数据
listLoading: false
};
},
b. 钩子函数
created() {
//获取广告列表数据
this.loadPromotionAd();
//获取广告位置数据
this.loadPromotionSpace();
},
c.函数部分
//方法1; 获取广告列表数据
loadPromotionAd() {
this.listLoading = true;
return axios
.get("/PromotionAd/findAllPromotionAd", {
params: {
currentPage: this.page,
pageSize: this.size
}
})
.then(res => {
this.list = res.data.content.list;
this.total = res.data.content.total;
this.listLoading = false;
})
.catch(err => {});
},
//方法2: 获取广告位置数据
loadPromotionSpace() {
this.listLoading = true;
return axios.get("/PromotionSpace/findAllPromotionSpace").then(res => {
//使用map进行遍历
res.data.content.map(item => {
//将数据保存到 typeMap key就是id,value就是 广告位对象
this.typeMap[item.id] = item;
});
this.listLoading = false;
});
},
//方法3: 获取广告位置名称
getSpaceName(spaceId) {
if (!spaceId) {
return "";
}
return this.typeMap[spaceId] && this.typeMap[spaceId].name;
},
2.3 广告状态修改
- 需求分析: 点击按钮实现 状态修改, 0 下线,1 上线
- 功能实现
页面部分,使用的是 el-switch 组件
active-value: switch:打开时的值
inactive-value : switch 关闭时的值
//方法4: 修改状态
handleUpdateStatus(row) {
this.$confirm("是否要修改上线/下线状态?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
//请求后台
axios
.get("/PromotionAd/updatePromotionAdStatus", {
params: {
id: row.id,
status: row.status
}
})
.then(res => {
this.loadPromotionAd();
})
.catch(err => {
this.$message("修改状态失败! ! !");
});
});
},
2.4 广告新增&修改
- 需求分析
a.点击添加广告,触发事件
添加广告
b.路由导航到指定组件
//跳转到新增
handleAdd() {
this.$router.push({ path: "/addAdvertise" });
},
c.查看路由信息,跳转到的是 AddAdvertise.vue组件
{
path: "addAdvertise",
name: "AddAdvertise",
component: () => import("../views/AdvertiseManage/AddAdvertise"),
meta: { requireAuth: true, title: "添加广告" }
},
d. AddAdvertise.vue组件
在AddAdvertise组件中,引入了 AdvertiseDetail组件,真正的操作是在这个组件中完成的
:isEdit="false" : false表示是新增操作
e.AdvertiseDetail.vue 组件
该组件是进行 新增和修改广告的页面.
2.功能实现
数据部分
data() {
return {
homeAdvertise, //广告表单对象
typeOptions: [] //广告位下拉列表
};
},
钩子函数
created() {
//判断是新增还是修改
if (this.isEdit) {
//修改
const id = this.$route.query.id;
this.loadPromotion(id);
} else {
//新增
this.homeAdvertise = {};
}
this.loadPromotionSpace();
},
方法
//方法1: 获取广告位置数据
loadPromotionSpace() {
return axios.get("/PromotionSpace/findAllPromotionSpace").then(res => {
//使用map函数进行遍历,获取广告位id 与 name,保存到typeOptions
this.typeOptions = res.data.content.map(item => {
return { label: item.name, value: item.id };
});
});
},
//方法2: 保存广告信息
handleSave() {
this.$refs.form.validate(valid => {
if (!valid) return false;
axios
.post("/PromotionAd/saveOrUpdatePromotionAd", this.homeAdvertise)
.then(res => {
//返回上个页面 并刷新
this.$router.back();
})
.catch(err => {});
});
},
//方法3: 修改回显广告信息
loadPromotion(id) {
return axios
.get("/PromotionAd/findPromotionAdById?id=" + id)
.then(res => {
Object.assign(this.homeAdvertise, res.data.content);
this.homeAdvertise.id = id;
})
.catch(err => {});
},
四、用户管理
1 分页&条件查询用户数据
查询条件:
1. 用户手机号
2. 注册时间,包含开始日期和结束日期
1.1 日期选择器组件
在查询条件中使用了 ElementUI中的日期选择器,我们一起来简单学习一下日期选择器的使用.
https://element.eleme.cn/#/zh-CN/component/date-picker#mo-ren-xian-shi-ri-qi
- 在测试项目中,创建一个 TestDate.vue组件,复制代码到页面
带快捷选项
查询
1.2 功能实现
数据部分
//数据部分
return {
pickerOptions,//日期选择器选项设置
total: 0, //总条数
size: 10, //每页显示条数
page: 1, //当前页
filter,
users: [],
loading: false,
allocAdminId: "",
allocDialogVisible: false,
allocRoleIds: [],
allRoleList: []
};
JS部分
created() {
//初始化用户数据
this.loadUsers();
}
//方法1: 加载用户数据
loadUsers() {
this.loading = true;
//设置参数
const params = { currentPage: this.page, pageSize: this.size };
//过滤条件
if (this.filter.username) params.username = this.filter.username;
//设置日期参数
if (this.filter.resTime) {
params.startCreateTime = this.filter.resTime[0];
params.startCreateTime.setHours(0);
params.startCreateTime.setMinutes(0);
params.startCreateTime.setSeconds(0);
params.endCreateTime = this.filter.resTime[1];
params.endCreateTime.setHours(23);
params.endCreateTime.setMinutes(59);
params.endCreateTime.setSeconds(59);
}
//请求后台接口
return axios
.post("/user/findAllUserByPage", params)
.then(res => {
this.users = res.data.content.list; //用户数据
this.total = res.data.content.total;
this.loading = false;
})
.catch(err => {
this.$message("获取数据失败! ! !");
});
},
2 用户状态设置
状态按钮
{{ scope.row.status == "ENABLE" ? "禁用" : "启用" }}
JS部分
//修改用户状态
handleToggleStatus(item) {
return axios
.get("/user/updateUserStatus", {
params: {
id: item.id,
status: item.status
}
})
.then(res => {
debugger;
console.log(res.data.content);
item.status = res.data.content;
})
.catch(err => {
this.$message.error("状态修改失败! ! !");
});
},
五、权限管理
1 角色管理
1.1 展示&查询角色列表
- 角色组件是 Roles.vue ,在该组件中对角色信息进行管理
- 需求分析
- 功能实现
数据部分
data() {
return {
listQuery: { name: "" },
list: null,
listLoading: false,
dialogVisible: false,
role: Object.assign({}, defaultRole),
isEdit: false
};
},
钩子函数,调用loadRoles,获取角色数据
created() {
//获取角色列表
this.loadRoles();
},
//获取角色数据
loadRoles() {
return axios
.post("/role/findAllRole", this.listQuery)
.then(res => {
this.list = res.data.content;
this.listLoading = false;
})
.catch(err => {});
},
请求携带的参数是: listQuery
//条件查询
handleSearchList() {
this.loadRoles();
},
1.2 添加&修改角色
- 页面部分
添加角色
- 打开添加角色窗口的方法
//添加角色弹窗
handleAdd() {
this.dialogVisible = true; //打开对话框
this.isEdit = false; //false 修改操作
this.role = Object.assign({}, defaultRole);
},
3.添加角色对话框,使用v-model 进行双向数据绑定.
- 添加角色方法
//添加&修改角色
handleSave() {
axios
.post("/role/saveOrUpdateRole", this.role)
.then(res => {
this.dialogVisible = false;
this.loadRoles();
})
.catch(error => {
this.$message.error("保存课程信息失败! ! !");
});
},
- 修改角色的方法
修改按钮,点击传递当前行数据对象
编辑
显示对话框,回显数据
//修改角色弹窗
handleUpdate( row) {
this.dialogVisible = true;
this.isEdit = true;
//回显数据
this.role = Object.assign({}, row);
},
修改角色,还是调用的handleSave 方法
1.3 删除角色
删除
这里使用到了ElementIUI中的 MessageBox 弹框
https://element.eleme.cn/#/zh-CN/component/message-box#options
handleDelete(row) {
this.$confirm("是否要删除该角色?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
axios("/role/deleteRole?id=" + row.id)
.then(res => {
this.loadRoles();
})
.catch(err => {
this.$message.error("操作失败! ! !");
});
});
},
1.4 为角色分配菜单
1.需求分析
-
为角色分配菜单,一个角色可以拥有多个菜单权限
-
一个菜单权限也可以被多个角色拥有
- 角色与菜单之间的关系 是多对多
- 点击分配菜单,页面展示效果
- 前端要实现的效果
- 第一步: 获取到所有的菜单数据,在树形控件中进行展示
- 第二步: 将当前角色拥有的菜单权限,勾选上
- 菜单展示功能实现
- 分配菜单按钮,点击传递当前行数据
分配菜单
- 路由导航到 allocMenu
//为角色分配菜单
handleSelectMenu(row) {
this.$router.push({ path: "/allocMenu", query: { roleId: row.id } });
},
- routes.js
{
path: "allocMenu",
name: "AllocMenu",
component: () =>
import(
/* webpackChunkName: 'allocMenu' */ "../views/PermissionManage/AllocMenu"
),
meta: { requireAuth: true, title: "角色菜单管理" }
},
- 在AllocMenu.vue组件中完成 为角色分配菜单操作
- 数据部分
data() {
return {
menuTreeList: [], //菜单数据
checkedMenuId: [], //被选中的菜单
//树形结构子节点设置
defaultProps: {
children: "subMenuList",
label: "name"
},
roleId: null
};
},
- 钩子函数
//钩子函数
created() {
//获取路由携带的id
this.roleId = this.$route.query.roleId;
//获取菜单列表
this.treeList();
//获取角色所拥有的菜单信息
this.getRoleMenu(this.roleId);
},
//方法1: 获取菜单列表,使用树形控件展示
treeList() {
axios.get("/role/findAllMenu").then(res => {
console.log(res.data.content);
//获取树形控件所需数据
this.menuTreeList = res.data.content.parentMenuList;
});
},
//方法2: 获取当前角色所拥有菜单列表id
getRoleMenu(roleId) {
axios.get("/role/findMenuByRoleId?roleId=" + roleId).then(res => {
console.log(res.data.content);
//将已有菜单权限设置为选中
this.$refs.tree.setCheckedKeys(res.data.content);
});
},
3.分配菜单功能实现
分配菜单按钮
保存
清空
方法
//方法3: 修改角色所拥有的菜单列表
handleSave() {
//debugger;
//获取所有被选中的节点
const checkedNodes = this.$refs.tree.getCheckedNodes();
//定义常量 保存被选中的菜单id
const checkedMenuIds = [];
if (checkedNodes != null && checkedNodes.length > 0) {
//遍历获取节点对象
for (let i = 0; i < checkedNodes.length; i++) {
const checkedNode = checkedNodes[i];
//保存菜单列表id
checkedMenuIds.push(checkedNode.id);
//判断: 当前节点为子节点 && 其父ID在数组没有出现过,就保存这个父Id
if (
checkedNode.parentId !== -1 &&
checkedMenuIds.filter(item =>checkedNode.parentId).length === 0
) {
checkedMenuIds.push(checkedNode.parentId);
}
}
}
this.$confirm("是否分配菜单?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
//准备参数
const params = {
roleId: this.roleId, //角色ID
menuIdList: checkedMenuIds //当前角色拥有的菜单权限ID
};
//请求后台
axios
.post("/role/RoleContextMenu", params)
.then(res => {
this.$router.back();
})
.catch(err => {
this.$message.error("权限分配失败! ! !");
});
});
},
2 菜单管理
菜单组件是 Menus.vue ,在该组件中对菜单信息进行管理
2.1 展示菜单列表
需求分析: 菜单列表的展示是带有分页的.
- 功能实现
- 数据部分
data() {
return {
total: 0, //总条数
size: 10, //每页显示条数
page: 1, //当前页
list: [], //广告数据
listLoading: true,
parentId: 0 //菜单父id
};
},
- 钩子函数
created() {
//获取菜单列表
this.loadMenuList();
},
//方法1: 加载菜单列表数据
loadMenuList() {
this.listLoading = true;
return axios
.get("/menu/findAllMenu", {
params: {
currentPage: this.page,
pageSize: this.size
}
})
.then(res => {
this.list = res.data.content.list;
this.total = res.data.content.total;
this.listLoading = false;
})
.catch(error => {
this.$message.error("数据获取失败! ! !");
});
},
2.2 新增&修改菜单
- 路由跳转流程
a.新增按钮, 点击跳转
添加菜单
//新增菜单跳转
handleAddMenu() {
this.$router.push("/addMenu");
},
b.AddMenu.vue 组件中引入了MenuDetail
c.MenuDetail.vue 中完成菜单的新增与修改操作
2.需求分析
在打开新增菜单页面后, 需要展示一个下拉框,下拉框中的数据是所有的顶级父菜单.
3.功能实现
- 数据部分
data() {
return {
menu, //菜单对象
selectMenuList: [], //下拉列表数据
rules
};
},
- 钩子函数
在钩子函数中会进行判断,如果是修改操作,就根据ID 查询当前菜单信息,以及父菜单信息
如果是新增操作,则只查询父类菜单信息即可
created() {
if (this.isEdit) {
//修改,回显菜单信息
const id = this.$route.query.id;
//获取当前菜单和父菜单信息
this.findMenuInfoById(id);
} else {
//新增
this.menu = {};
//获取父类菜单信息
this.findMenuInfoById(-1);
}
},
//方法1: 添加或修改 下拉父菜单回显
findMenuInfoById(id) {
axios
.get("/menu/findMenuInfoById?id=" + id)
.then(res => {
debugger;
console.log(res.data);
//判断不为null,修改操作需要回显
if (res.data.content.menuInfo != null) {
this.menu = res.data.content.menuInfo;
}
//获取到父菜单信息,保存到selectMenuList
this.selectMenuList = res.data.content.parentMenuList.map(item => {
return { id: item.id, title: item.name };
});
//-1 显示 无上级菜单 (unshift向数组的开头添加一个元素)
this.selectMenuList.unshift({ id: -1, title: "无上级菜单" });
})
.catch(err => {
this.$message.error("数据获取失败! ! !");
});
},
- 点击保存
提交
//保存菜单
handleSave() {
this.$refs.form.validate(valid => {
if (!valid) return false;
axios
.post("/menu/saveOrUpdateMenu", this.menu)
.then(res => {
this.$router.back();
})
.catch(error => {
this.$message.error("保存课程信息失败! ! !");
});
});
}
3 资源管理
资源组件是 Resources.vue ,在该组件中对资源信息进行管理.
3.1 展示&查询资源列表
- 展示资源数据 带有分页
- 查询资源数据,查询条件有三个
- 资源名称
- 资源路径
- 资源分类信息: 下拉列表
- 数据部分
//查询条件
const listQuery = {
currentPage: 1,
pageSize: 5,
name: null,
url: null,
categoryId: null
};
//资源对象
const defaultResource = {
id: null,
name: null,
url: null,
categoryId: null,
description: ""
};
data() {
return {
listQuery,//查询条件
total: 0,
list: [], //资源数据
cateList: [], //资源分类数据
listLoading: false,
dialogVisible: false,
resource: Object.assign({}, defaultResource),
isEdit: false,
categoryOptions: [],
defaultCategoryId: null
};
},
- 钩子函数
- 在钩子函数中,需要获取资源 ,以及资源分类的数据
//钩子函数
created() {
//获取资源数据
this.getResourceList();
//获取资源分类数据
this.getResourceCateList();
},
- getResourceList() 方法获取的是资源信息
//方法1: 获取资源数据
getResourceList() {
this.listLoading = true;
axios
.post("/resource/findAllResource", this.listQuery)
.then(res => {
this.list = res.data.content.list;
this.total = res.data.content.total;
this.listLoading = false;
})
.catch(err => {
this.$message.error("数据获取失败! ! !");
});
},
- getResourceCateList() 方法获取的是资源分类信息,在下拉框中展示
//方法2: 获取资源分类数据
getResourceCateList() {
axios
.get("/ResourceCategory/findAllResourceCategory")
.then(res => {
this.cateList = res.data.content;
//遍历获取资源分类
for (let i = 0; i < this.cateList.length; i++) {
const cate = this.cateList[i];
//将资源分类名与id保存到 categoryOptions中,供下拉列表展示
this.categoryOptions.push({ label: cate.name, value: cate.id });
}
this.defaultCategoryId = this.cateList[0].id;
})
.catch(err => {
this.$message.error("数据获取失败! ! !");
});
},
- 查询
查询搜索
//查询条件对象
const listQuery = {
currentPage: 1,
pageSize: 5,
name: null,
url: null,
categoryId: null
};
//查询按钮
handleSearchList() {
this.getResourceList();
},
3.2 新增&修改资源
- 添加按钮
添加
- 显示添加资源表单的对话框
//添加资源回显
handleAdd() {
this.dialogVisible = true; //显示表单
this.isEdit = false; //新增为false
this.resource = Object.assign({}, defaultResource); //资源对象
this.resource.categoryId = this.defaultCategoryId; //保存默认分类id
},
- 资源分类信息使用下拉菜单进行展示:
v-model的值为当前被选中的el-option的 value 属性值
- 点击保存
确 定
//添加&修改资源
handleSave() {
axios
.post("/resource/saveOrUpdateResource", this.resource)
.then(res => {
this.dialogVisible = false;
this.getResourceList();
})
.catch(error => {
this.$message.error("操作失败! ! !");
});
},
- 修改操作, 参数是当前行数据
编辑
- 回显操作
//编辑资源 回显
handleUpdate(row) {
debugger;
this.dialogVisible = true;
this.isEdit = true;
this.resource = Object.assign({}, row);
},
六、用户权限控制
1 用户登录
1.1 流程分析
- 用户登录界面,需要输入手机号密码
- 登录组件 login.vue
- 登录按钮
{{ loading ? 'Loading...' : '登录' }}
- 提交表的方法
//提交登录表单
submit(ref) {
//校验
this.$refs[ref].validate(valid => {
if (!valid) return false;
this.error = null;
this.loading = true;
//发送登录请求
this.$store.dispatch("createToken", this.model)
.then(res => {
if (res.state !== 1) {
this.error = {
title: "Error occurred",
message: "Abnormal, please try again later!"
};
}
this.$router.replace({ path: this.$route.query.redirect || "/" });
this.loading = false;
})
.catch(err => {
this.loading = false;
});
});
}
}
- this.$store.dispatch("createToken", this.model)
- 这段代码的意思是调用 store仓库的actions.js中的createToken方法
- 发送登录请求,进行登录的代码
/**
* 创建新的客户端令牌
*/
createToken: async ({ commit }, { username, password }) => {
//请求后台登录接口
const res = await TokenService.userLogin({
phone: username.trim(),
password: password.trim()
});
console.log(res);
//判断结果不等于1,登录失败
if (res.state !== 1) {
return Promise.resolve(res);
}
//获取到content
const result = res.content;
//将token保存
commit(CHANGE_SESSION, {
accessToken: result.access_token
});
return res;
},
- TokenService
import { TokenService, UserService } from "../services";
//登录请求 async ES6语法, 作用: 发送异步请求
export const userLogin = async (data) => {
//await 表示等待接收返回的数据
return await PostRequest(`${process.env.VUE_APP_API_FAKE}/user/login${Serialize(data)}`)
}
2 动态获取用户菜单
2.1 流程分析
- 在我们登录成功后, 会立即发送第二个请求, 来获取用户的菜单权限列表
- 在actions.js 中完成请求后台接口 获取数据的操作
/**
* 获取当前登录用户权限
*/
getUserPermissions: async ({ commit }) => {
//1.请求后台 获取当前用户的权限
const res = await UserService.getUserPermissions();
//2.判断
if (!res.success) {
//获取失败直接返回 false
return res.success;
}
//3.获取数据成功,取出菜单 与 资源列表
const { menuList, resourceList } = res.content;
//4.下面的代码 就是在生成树形结构的菜单
let menus = [];
const formatMenu = treeData => {
if (treeData.length > 0) {
return treeData.map(item => formatMenu(item));
}
const result = {};
//shown等于表示可以显示,将内容保存
if (treeData.shown == 1) {
result.id = treeData.id;
result.text = treeData.name;
result.label = treeData.name;
result.name = treeData.href;
result.icon = treeData.icon;
result.shown = treeData.shown;
} else {
return "";
}
//获取子节点
if (treeData.subMenuList) {
result.children = [];
treeData.subMenuList.forEach(item => {
formatMenu(item) && result.children.push(formatMenu(item));
});
if (result.children.length === 0) {
delete result.children;
}
}
return result;
};
const memusMap = {};
const splapMenu = treeData => {
if (treeData.length > 0) {
return treeData.map(item => splapMenu(item));
}
const result = {};
result.id = treeData.id;
result.text = treeData.name;
result.label = treeData.name;
result.name = treeData.href;
result.icon = treeData.icon;
result.shown = treeData.shown;
result.name && (memusMap[result.name] = result);
if (treeData.subMenuList) {
result.children = [];
treeData.subMenuList.forEach(item => {
result.children.push(splapMenu(item));
});
}
return result;
};
splapMenu(menuList);
menus = formatMenu(menuList);
commit(CHANGE_SIDERBAR_MENU, menus);
return { menus, resourceList, menuList, memusMap };
},
3 验证Token
3.1 导航守卫
- 在执行路由之前先执行的一些钩子函数,比如验证用户是否有权限之类的操作,就需要使用.
- authorize.js 中配置了导航守卫,来对用户的登录进行限制
// 导航守卫 to要访问的url, from从哪个路径跳转过来, next() 放行
router.beforeHooks.unshift((to, from, next) => {
//不需要验证直接放行
if (!to.meta.requireAuth) return next();
//需要验证token,调用 store中的checkToken方法
store.dispatch("checkToken").then(valid => {
//判断是否存在token
if (valid) {
//发送请求到后台,在后台再次判断token是否存在
store.dispatch("getUserPermissions").then(res => {
if (!res) {
//失效 清除token
store.dispatch("deleteToken");
//跳转到登录页面
return next({ name: "ToLogin" });
}
//token正确, 导航到对应的页面
const { memusMap } = res;
if (memusMap.Courses && to.name === "Home") {
return next();
} else if (memusMap[to.name]) {
return next();
} else if (Object.keys(memusMap).length > 0) {
return next({ name: memusMap[Object.keys(memusMap)[0]].name });
} else {
next({ name: "PermissionDenied" });
}
});
return next();
}
// unauthorized
console.log("Unauthorized");
//用户没有登录 跳转到登录页面
next({ name: "Login", query: { redirect: to.fullPath } });
});
});
- 在actions.js 中检查token是否可用
checkToken: async ({ commit, getters }) => {
//取出token
const token = getters.session.accessToken;
if (!token) {
//不可用
return Promise.resolve(false);
}
return Promise.resolve(true);
},
4 用户角色分配
4.1 流程分析
- 点击分配角色按钮
分配角色
- 分配角色对话框
4.2 代码部分
- 显示对话框
//分配角色
handleSelectRole(row) {
//保存用户ID
this.allocAdminId = row.id;
//获取角色列表
this.getRoleList();
//获取当前用户拥有的角色
this.getUserRoleById(row.id);
//打开对话框
this.allocDialogVisible = true;
},
- 获取角色列表,在下拉菜单中展示
getRoleList(id) {
return axios
.post("/role/findAllRole", this.listQuery)
.then(res => {
this.allRoleList = res.data.content.map(item => {
return { id: item.id, name: item.name };
});
})
.catch(err => {});
},
- 获取当前用户拥有的角色,回显默认选中
getUserRoleById(id) {
axios.get("/user/findUserRoleById?id=" + id).then(res => {
const allocRoleList = res.data.content;
this.allocRoleIds = [];
if (allocRoleList != null && allocRoleList.length > 0) {
for (let i = 0; i < allocRoleList.length; i++) {
this.allocRoleIds.push(allocRoleList[i].id);
}
}
});
},
- 为用户分配角色
handleAllocRole() {
const params = {
userId: this.allocAdminId,
roleIdList: this.allocRoleIds
};
axios.post("/user/userContextRole", params).then(res => {
this.allocDialogVisible = false;
});
},
任务二: nginx
1 什么是nginx?
? Nginx(发音同 engine x)是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。由俄罗斯的程序设计师Igor Sysoev(伊戈尔·西索夫)所开发,供俄国大型的入口网站及搜索引擎Rambler(漫步者)(俄文:Рамблер)使用。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:新浪、网易、 腾讯等。
优点:
- 占用内存少,并发能力强
- Nginx专为性能优化而开发, 在高连接并发的情况下,能够支持高达 50,000 个并发连接数的响应.
- Nginx支持热部署, 可以在不间断服务的情况下,对软件版本进行升级.
2 应用场景
-
http服务器: Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。
-
虚拟主机: 可以实现在一台服务器虚拟出多个网站。例如个人网站使用的虚拟主机。
-
反向代理,负载均衡 : 当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会因为某台服务器负载高宕机而某台服务器闲置的情况。
3 Nginx安装
下载nginx, 官方网站:http://nginx.org/
我们使用的版本是1.17.8版本。
Nginx在Linux下安装,只提供了源代码,所以我们需要进行编译.
3.1 安装环境配置
- 因为Nginx是C语言编写的,所以需要配置C语言编译环境 (一定要在联网状态下安装)
需要安装gcc的环境。执行命令:
yum install gcc-c++
注意: 如果执行命令出现这样的提示:
解决办法:
问题是 yum在锁定状态中,强制关掉yum进程即可
rm -f /var/run/yum.pid
- 第三方的开发包, 在编译之前需要安装这些第三方包。
-
PCRE
- nginx的http模块使用pcre来解析正则表达式,所以需要在linux上安装pcre库
安装命令: yum install -y pcre pcre-devel
-
zlib
- nginx使用zlib对http包的内容进行gzip,所以需要在linux上安装zlib库。
安装命令: yum install -y zlib zlib-devel
-
openssl
- OpenSSL 是一个强大的安全套接字层密码库,nginx不仅支持http协议,还支持https,所以需要在linux安装openssl库。
安装命令: yum install -y openssl openssl-devel
3.2 安装Nginx 步骤
-
将Nginx的源码包上传到 Linux
-
解压Nginx
tar -xvf nginx-1.17.8.tar
- 进入到解压之后的目录 nginx-1.17.8
- 执行命令 configure,生成 Mikefile 文件
./configure \
--prefix=/usr/local/nginx \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_gzip_static_module \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi
执行命令后, 生成了MakeFile文件
- 创建临时文件目录
mkdir /var/temp/nginx/client -p
- 执行make命令,进行编译
make
- 安装
make install
2.3.3 启动并访问 Nginx
- 进入到nginx 安装目录
cd /usr/local/nginx/
- 进入到 sbin目录,执行
nginx
命令
./nginx 启动
./nginx -s stop 关闭
ps aux | grep nginx 查看进程
- 通过浏览器进行访问 ,默认端口 80 (注意:是否关闭防火墙。)
4 配置虚拟主机
虚拟主机指的是,在一台服务器中,我们使用Nginx,来配置多个网站.
如何区分不同的网站:
- 端口不同
- 域名不同
4.1 通过端口区分不同的虚拟主机
Nginx配置文件
- Nginx配置文件的位置
cd /usr/local/nginx/conf
nginx.conf 就是Nginx的配置文件
- Nginx核心配置文件说明
worker_processes 1; #work的进程数,默认为1
#配置 影响nginx服务器与用户的网络连接
events {
worker_connections 1024; #单个work 最大并发连接数
}
# http块是配置最频繁的部分 可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能
http {
# 引入mime类型定义文件
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65; # 超时时间
#server 配置虚拟主机的相关参数 可以有多个,一个server就是一个虚拟主机
server {
# 监听的端口
listen 80;
#监听地址
server_name localhost;
# 默认请求配置
location / {
root html; # 默认网站根目录
index index.html index.htm; # 欢迎页
}
# 错误提示页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
使用Notpad,连接Linux
? 使用notepad++来连接linux,好处是使用notepad++来编辑linux中文件的批量文字,会比直接在linux中操作方便快捷很多.
- Notepad 插件中安装NppFTP
- 打开NppFTP
- 选择设置
- 配置连接信息
- 连接
配置nginx.conf
- 使用Notpad 在nginx.conf 中添加一个 新的server
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
}
# 配置新的server
server {
listen 81; # 修改端口
server_name localhost;
location / {
root html81; # 重新制定一个目录
index index.html index.htm;
}
}
}
- 复制一份 html目录
cp -r html html81
- 重新加载配置文件
sbin/nginx -s reload
- 访问
http://192.168.52.100 访问第一个server
http://192.168.52.100:81/ 访问第二个server
4.2 通过域名区分不同的虚拟主机
什么是域名
网址就是域名,是一个网站的地址, 由域名提供商提供,一般需要购买.
www.baidu.com
www.taobao.com
www.jd.com
域名级别
- 一级域名
- 比如 .com .org .cn
- 二级域名
- 二级域名是在一级域名前加一级
- 二级域名: baidu.com , zhihu.com
- 三级域名
- www.baidu.com
- image.baidu.com
域名绑定
- 一个域名对应一个ip地址,一个ip地址可以被多个域名绑定。
- 通过 DNS服务器去解析域名
配置域名映射
- 本地测试可以修改hosts文件。修改window的hosts文件:(C:\Windows\System32\drivers\etc)
- 可以配置域名和ip的映射关系,如果hosts文件中配置了域名和ip的对应关系,不需要走dns服务器。
配置一下nginx的映射
192.168.52.100 www.ng.com
- 使用SwitchHosts,修改hosts
- 解压
- 右键 以管理员身份运行
- 配置IP与域名的映射
配置nginx.conf
#通过域名区分虚拟主机
server {
listen 80;
server_name www.t1.com;
location / {
root html-t1;
index index.html index.htm;
}
}
server {
listen 80;
server_name www.t2.com;
location / {
root html-t2;
index index.html index.htm;
}
}
- 创建 html-t1和 html-t2 目录
cp -r html html-t1
cp -r html html-t2
- 修改一下index.html 中,刷新
sbin/nginx -s reload
- 访问
虽然只有一台服务器,但是这台服务器上运行着多个网站,访问不同的域名 就可访问到不同的网站内容
5 反向代理
5.1 什么是代理
? 代理其实就是一个中介,A和B本来可以直连,中间插入一个C,C就是中介。刚开始的时候,代理多数是帮助内网client
访问外网server
用的.
客户机在发送请求时,不会直接发送给目的主机,而是先发送给代理服务器,代理服务接受客户机请求之后,再向主机发出,并接收目的主机返回的数据再发送给客户机。
5.2 正向代理
? 比如我们国内访问谷歌,直接访问访问不到,我们可以通过一个正向代理服务器,先将请求发送到到代理服,代理服务器能够访问谷歌,这样由代理去谷歌取到返回数据,再返回给我们,这样我们就能访问谷歌了
正向代理代理的是客户端, 服务端不知道实际发起请求的客户端.
5.3 反向代理
反向代理和正向代理的区别就是:正向代理代理客户端,反向代理代理服务器。
? 反向代理是指用代理服务器接收客户端的请求,然后将请求转发给网站内部应用服务器,并将从服务器上得到的结果返回给客户端.
5.4 Nginx实现反向代理
Nginx作为反向代理服务器安装在服务端,Nginx的功能就是把请求转发给后面的应用服务器.
-
配置步骤
- 第一步:简单的使用2个tomcat实例模拟两台http服务器,分别将tomcat的端口改为8080和8081
- 第二步:启动两个tomcat。
./bin/startup.sh 访问两个tomcat http://192.168.52.100:8080/ http://192.168.52.100:8081/
- 第三步:反向代理服务器的配置
#反向代理配置 #upstream中的server是真正处理请求的应用服务器地址 upstream lagou1{ #用server定义HTTP地址 server 192.168.52.100:8080; } server { listen 80; server_name www.lagou1.com; location / { # 利用 proxy_ pass可以将请求代理到upstream命名的HTTP服务 proxy_pass http://lagou1; #转发到的地址 index index.html index.htm; } } upstream lagou2{ #用server定义HTTP地址 server 192.168.52.100:8081; } server { listen 80; server_name www.lagou2.com; location / { proxy_pass http://lagou2; index index.html index.htm; } }
- 第四步:nginx重新加载配置文件
nginx -s reload
-
第五步:配置域名, 在hosts文件中添加域名和ip的映射关系
192.168.52.100 www.lagou1.com 192.168.52.100 www.lagou2.com
通过浏览器输入域名, 访问Nginx代理服务器, Nginx根据域名将请求转发给对应的目标服务器,作为用户我们看到的是服务器的响应结果页面,在整个过程中目标服务器相对于客户端是不可见的,服务端向外暴露的就是Nginx的地址.
6 负载均衡
6.1 什么是负载均衡
? 当一个请求发送过来的时候,Nginx作为反向代理服务器,会根据请求找到后面的目标服务器去处理请求,这就是反向代理. 那么, 如果目标服务器有多台的话,找哪一个服务器去处理当前请求呢 ? 这个合理分配请求到服务器的过程就叫做负载均衡.
6.2 为什么用负载均衡
? 当系统面临大量用户访问,负载过高的时候,通常会使用增加服务器数量来进行横向扩展, 负载均衡主要是为了分担访问量,将请求合理分发给不同的服务器, 避免临时的网络堵塞
6.3 负载均衡策略
6.3.1 轮询
-
默认策略, 每个请求按照时间顺序逐一分配到不同的服务器,如果某一个服务器下线,能自动剔除
-
配置方式
负载均衡
upstream lagouServer{
用server定义 HTTP地址
server 192.168.52.100:8081;
server 192.168.52.100:8082;
}server {
listen 80;
server_name www.lagouNB.com;location / { # 利用 proxy_ pass可以将请求代理到upstream命名的HTTP服务 proxy_pass http://lagouServer; index index.html index.htm; }
负载均衡
upstream lagouServer{
用server定义 HTTP地址
server 192.168.52.100:8081;
server 192.168.52.100:8082;
}server {
listen 80;
server_name www.lagouNB.com;location / { # 利用 proxy_ pass可以将请求代理到upstream命名的HTTP服务 proxy_pass http://lagouServer; index index.html index.htm; }
6.3.2 weight
-
可以根据服务器的实际情况调整服务器权重。权重越高分配的请求越多,权重越低,请求越少。默认是都是1.
负载均衡
upstream lagouServer{
# 用server定义 HTTP地址
server 192.168.52.100:8081 weight=1;
server 192.168.52.100:8082 weight=10;
}
任务三:项目部署与发布
1 后台项目部署
1.1 Linux环境准备
-
需要安装的软件
-
关闭防火墙
-
使用SQLYog连接Linux上的MySQL, 导入SQL脚本 创建项目所需的数据库
1.2 项目打包发布
? 在平常开发的过程中,不同的环境中项目的相关配置也会有相关的不同,我们在不同的环境中部署就要手动修改为对应环境的配置,这样太麻烦了以及这样也会很容易出错。
接下来我们就通过maven的相关配置来在打包时指定各个环境对应配置文件
1. 修改ssm_dao 子模块
- 修改后的目录结构
2. 第一步: 创建配置文件
在项目的src/main/resources 下面创建filter目录, 再创建 development.properties , product.properties 两个文件
- development是开发配置内容。
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///ssm_lagou_edu?characterEncoding=UTF-8
jdbc.username=root
jdbc.password=123456
- product是正式配置内容
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.52.100:3306/ssm_lagou_edu?characterEncoding=UTF-8
jdbc.username=JiuYuan
jdbc.password=JiuYuan@123
3. 第二步:配置jdbc.properties 文件
jdbc.properties中的内容不再写死,而是从上面两个文件中获取
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=${jdbc.url}
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}
注意:${jdbc.url} 直接对应上面配置的development.properties或product.properties文件中的名称。
4. 第三步: 配置dao模块的的 pom.xml文件
添加如下配置
dev
development
true
prod
product
web
src/main/resources/filter/${env}.properties
src/main/resources
filter/*.properties
true
5. profile说明
- profile可以让我们定义一系列的配置信息,然后指定其激活条件。这样我们就可以定义多个profile,然后每个profile对应不同的激活条件和配置信息,从而达到不同环境使用不同配置信息的效果
- 默认启用的是dev环境配置:
dev
development
true
prod
product
- 指定数据库配置文件路径,此路径可以自定义:
src/main/resources/filter/${env}.properties
- 指定资源目录路径
src/main/resources
filter/*.properties
true
6.第四步: 打包
- 命令打包
打本地包 mvn -Pdev install 或者mvn install(因为本例activeByDefault配的为true)
打产品包 mvn -Pprod install
结果:src/main/resources/config/jdbc.properties根据 mvn -P 参数决定值
- 使用idea打包
7.打包后的文件
- 使用生产环境的配置文件,进行打包
- 打开这个war包,我们会发现 其他子模块都已经被打成jar包,放到了lib文件夹下
8. 发布
- 修改一下项目名称
- 上传到tomcat中,启动测试
- 在部署tomcat的 webapps目录下创建一个 upload文件夹,保存图片
mkdir upload
- 访问
http://192.168.52.100:8080/ssm-web/user/login?phone=18211111111&password=123456
- 获取到响应的JSON, 发布成功
2 前端项目部署
2.1 修改配置文件
- 生产环境配置文件,配置后台URL
VUE_APP_NAME = Edu Boss
VUE_APP_TITLE = Lagou Edu Boss (Dev)
VUE_APP_STORAGE_PREFIX = lagou_edu_boss_dev
#VUE_APP_API_FAKE = /front
VUE_APP_API_FAKE = http://192.168.52.100:8080/ssm-web
#VUE_APP_API_BASE = /boss
VUE_APP_API_BASE = http://192.168.52.100:8080/ssm-web
- 自定义配置文件,配置打包相关信息
将下面内容拷贝到 vue.config.js
module.exports = {
publicPath: process.env.NODE_ENV === "production" ? "/edu-boss/" : "/",
indexPath: "index.html",
assetsDir: "static",
lintOnSave: process.env.NODE_ENV !== "production",
productionSourceMap: false,
devServer: {
open: true,
port: 8081
}
};
2.2 打包测试操作
- 打包命令
npm run build
- 在项目下会生成一个 dist 目录
- 在本地tomcat的webapps目录下,创建一个edu-boss文件夹,将dist目录中的文件拷贝到里面
- 测试: 启动本地tomcat ,访问前端项目 路径为:
http://localhost:8081/edu-boss/
2.3 发布前端项目
- 解压一个新的tomcat, 修改端口号
#解压
tar xvf apache-tomcat-8.5.50.tar
#改名
mv apache-tomcat-8.5.50 ui-tomcat
#修改端口号
cd ui-tomcat/conf/
vim server.xml
- 上传前端项目到 webapps
//上传 edu-boss.zip ,并解压
unzip edu-boss.zip
//删除edu-boss.zip
rm -rf edu-boss.zip
- 运行前端项目,并访问
./bin/startup.sh
//动态查看日志
tail -f logs/catalina.out
//访问
http://192.168.52.100:8081/edu-boss/
3 修改tomcat默认访问项目
- 使用notpad打开前端tomcat的配置文件 server.xml, 找到
Host
标签
- 在Host标签内加入
- 重新启动 并访问前端项目,这个时候只需要直接访问 8081即可
http://192.168.52.100:8081/
4 配置反向代理
- 使用notpad打开nginx的配置文件 nginx.conf
- 配置反向代理
#配置ssm项目 反向代理
upstream lagouedu{
server 192.168.52.100:8081;
}
server {
listen 80;
server_name www.edu-boss.com;
location / {
proxy_pass http://lagouedu; #转发的地址
index index.html index.htm;
}
}
- 修改本地hosts, 配置域名映射
- 访问 www.edu-boss.com