前言
基于同学们在上一章(第三章)对响应式网站后台的学习,以及我们第二章egg.js基础的学习,从本章节开始,我们将继续对网站后台进行扩展开发。
我们在上一章已经完成了网站后台的管理员登录、管理员板块、留言板板块的开发。那么作为普通的企业网站后台,还应该有发布新闻信息、发布产品信息、管理网站导航栏等等功能。由于接下来的这些栏目,各个栏目板块数据库表之间的关联关系,相比较于我们前面开发的管理员列表、留言板板块,会相对复杂一些,因此我们将它们放在第四章进行讲解。
由于考虑到我们会在后面的课程:关于小程序、APP的开发中涉及到直播功能、即时通讯功能、商城功能、高德百度腾讯地图功能、视频功能、网盘功能等等,以直播功能为例,直播功能依然需要后台对直播功能中的:用户、礼物、直播间等等板块做后台开发,因此我们借本章开发后台其它管理板块,一起给大家讲解一下关于直播的后台功能目的就是:让大家更深入的理解我们egg.js项目中的模型关联关系。
# 一、用户管理板块(以直播功能中的用户表liveuser表为例)
由于一般企业网站不存在用户管理,因此我们这里以大家感兴趣的‘直播’功能为例,讲一下用户管理板块
具体查看:直播功能中的用户表liveuser
# 二、礼物管理(以直播功能中的礼物表livegift表为例)
具体查看:直播功能中的礼物表livegift
# 三、订单管理(以直播功能中的订单表liveorder表为例)
# 初步理解关联关系
前面给大家已经讲了管理员表、留言表、用户表、礼物表,这四张表有个共同特点:就是它们每张表都是独立的,不跟其他表产生关联关系。
那么不经有同学要问了:什么叫做关联关系?
学习过我们上一季度(第二季):章节7.Node.js基础,章节9.Vue.js基础的同学并不陌生,特别是最后我们举了一个案例:node.js+vue.js 渲染企业网站,如何来处理不同数据间的关联关系。
回顾了一下我们之前讲的不同json文件数据间的关联关系之后,同学们很自然的想到,既然我们的mysql就是关系型数据库,那么它应该也可以处理表之前的关联关系吧。
答案是:对的。我们的mysql数据库也可以处理表之间的关联关系。
为了方便大家理解,我们接下来将由浅入深,逐步理解表之间的关联关系。因此,我们用直播中的订单表为例,方便大家理解。因为我们每个同学都会网购,网购购买商品都会产生订单,因此理解起来会容易一些。具体查看:直播功能中的订单表liveorder
# 四、直播间管理(以直播功能中的直播间表live表为例)
# 多模型关联查询实践
我们在上节课开发后台订单列表功能中,大家已经初步感受了数据库表之间的关联关系,模型关联查询的功能。那么为了让大家深入理解模型关联查询,我们再讲解一个直播功能中的直播间功能,让大家更深入理解。
具体查看:直播功能中的直播间表live
# 五、mysql语句进一步理解模型关联关系(Mysql进阶)
我们在前面几节课分别完成了直播间订单管理、直播间管理中的观看记录、刷礼物记录、评论记录等功能,用到了模型关联关系
belongsTo(反向一对多)
关于模型关联关系,更多的模型关联具体查看: egg.js重要知识详细文档--关联操作
涉及的关联关系有:
- 一对一 hasOne (比如:一个用户对应一个用户资料)
- 一对多 hasMany (比如:一个用户可以发布很多文章)
- 反向一对多 belongsTo (比如:直播间观看记录关联用户模型,一个用户可以对应多个直播间观看记录,因为他可以进入多个直播间观看,用户对于观看记录就是一对多的关系,反过来观看记录属于用户belongsTo,就是反向一对多)
- 多对多 belongsToMany (比如:一个用户可以关注很多用户,一个用户也可以被很多用户关注) 在这些模型关联中,反向一对多
belongsTo用得最多,因此我们接下来以 模型关联中的belongsTo为例, 通过mysql语句来剖析这些关联关系,让大家对模型关联有个更深入的理解。
# 六、企业网站栏目管理、内容管理
有了我们上节课的通过sql语句剖析模型关联关系的讲解,那么完成我们企业网站后台管理中的栏目管理、内容管理就是一件非常轻松的事情了。
如果要在后台管理(网站导航栏,栏目分类)的数据
具体查看:企业网站后台栏目管理
如果要在后台管理网站的新闻、产品、内容等等信息,我们统称为:内容管理
具体查看:企业网站后台内容管理
# 七、网站配置管理
网站配置管理我们可以跟前面一样创建网站配置表,然后进行新增修改删除等,当然,由于网站配置比较简单,因此我们可以将配置放在json文件里面,进行操作即可。
以下是一个配置示例样本,大家可根据自己的想法设计网站配置的json文件:
{
"data": [
{
"id": 1,
"corporate_name": "睿晨电网建设工程有限公司",
"corporate_short": "睿晨电网",
"address": "北京CBD帝国大厦999层-1001号",
"tel": "010-8888-8888",
"mobile": "13858588888",
"serviceUser": "张经理",
"QQ": "1582758589",
"weixin": "company_51yrc",
"weixinImg": "",
"email": "51yrc@126.com",
"pagetitle": "睿晨电网建设工程有限公司",
"description": "睿晨电网建设工程有限公司成立由2021年,是一家集...",
"keywords": "睿晨电网,电网建设,睿晨电网建设工程有限公司",
"domain": "www.xxxx.com",
"copyright": "Copyright © 2024 睿晨电网建设工程有限公司 版权所有",
"icpNumber": "ICP 备案号 123456",
"cnzz": ""
}
],
"total": 1,
"currentId": 1
}
控制器
app/controller/admin/config.js
'use strict';
const { log } = require('node:console');
const fs = require('node:fs');
const path = require('node:path');
//配置写入json文件
let paths = {
dir: './data',
file: './data/siteConfig.json'
// dir:'./Appdata',
// file:'./Appdata/data.json'
};
const Controller = require('egg').Controller;
class ConfigController extends Controller {
// 配置列表
async index() {
//读json文件
const { ctx, app } = this;
let data = await this.getsiteConfigJson();
// console.log(typeof data);return;
if (typeof data == 'object') {
// console.log('配置数据', JSON.stringify(data.data));
//渲染公共模版
await ctx.renderTemplate({
title: '配置管理',//现在网页title,面包屑导航title,页面标题
data: data.data,
tempType: 'table', //模板类型:table表格模板 ,form表单模板
table: {
//表格上方按钮,没有不要填buttons
buttons: [
{
url: '/admin/config/create',//新增路径
desc: '创建配置',//新增 //按钮名称
// icon: 'fa fa-plus fa-lg',//按钮图标
}
],
//表头
columns: [
{
title: '配置信息',
// key: 'corporate_name',
class: 'text-left',//可选
// width: '30%',
render(item) {
let weixinImg = item.weixinImg;
if (weixinImg.endsWith('.jpg') || weixinImg.endsWith('.jpeg') ||
weixinImg.endsWith('.png') || weixinImg.endsWith('.gif')) {
// <li>微信二维码:${item.weixinImg}</li>
weixinImg = `<li>微信二维码:<img src="${weixinImg}" style="width: 100px; height: 100px;" /></li>`;
}
return `
<h5>公司基本信息</h5>
<ul>
<li>公司名称:${item.corporate_name}</li>
<li>名称简写:${item.corporate_short}</li>
<li>地址:${item.address}</li>
<li>免费咨询电话:${item.tel}</li>
<li>手机号:${item.mobile}</li>
<li>联系人:${item.serviceUser}</li>
<li>企业QQ:${item.QQ}</li>
<li>联系微信号:${item.weixin}</li>
${weixinImg}
<li>联系邮箱:${item.email}</li>
</ul>
<h5>seo信息</h5>
<ul>
<li>网站主页标题:${item.pagetitle}</li>
<li>网页描述:${item.description}</li>
<li>搜素关键字:${item.keywords}</li>
</ul>
<h5>其它信息</h5>
<ul>
<li>官方网址:${item.domain}</li>
<li>版权信息:${item.copyright}</li>
<li>ICP 备案:${item.icpNumber}</li>
</ul>
`;
}
},
{
title: '操作',
class: 'text-right',//可选
width: '10%',
action: {
//修改
edit: function (id) {
return `/admin/config/edit/${id}`;
},
//删除
delete: function (id) {
return `/admin/config/delete/${id}`;
}
}
},
],
},
});
}
}
//获取配置数据
async getsiteConfigJson() {
//先判断是否存在这个json文件或者文件夹
if (!fs.existsSync(paths.file)) {
this.ctx.redirect('/admin/config/create');
return '';
} else {
// console.log(__dirname);//D:\【第二学期第3季】课程代码\myegg\app\controller
//D:\【第二学期第3季】课程代码\myegg\data\message.json
// console.log(path.resolve(__dirname,'../../','data/message.json'));
// let data = fs.readFileSync(path.resolve(__dirname, '../../../', 'data/siteConfig.json'), {
// encoding: 'utf-8'
// });
let data = fs.readFileSync(paths.file, {
encoding: 'utf-8'
});
// console.log(JSON.parse(data).data);
return JSON.parse(data)
}
}
// 修改配置
async edit() {
const { ctx, app } = this;
const id = ctx.params.id;
let data = await this.getsiteConfigJson();
data = data.data.find(item => item.id == id);
if (!data) {
return ctx.apiFail('该内容不存在');
}
console.log(data);
await ctx.renderTemplate({
id,
title: '修改配置',//现在网页title,面包屑导航title,页面标题
tempType: 'form', //模板类型:table表格模板 ,form表单模板
form: {
//修改配置提交地址
action: '/admin/config/update/' + id,
// 字段
fields: [
{
label: '公司名称',
type: 'text',
name: 'corporate_name',
placeholder: '请输入公司名称',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '名称简写',
type: 'text',
name: 'corporate_short',
placeholder: '请输入名称简写',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '地址',
type: 'text',
name: 'address',
placeholder: '请输入公司地址',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '免费咨询电话',
type: 'text',
name: 'tel',
placeholder: '请输入免费咨询电话',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '手机号',
type: 'text',
name: 'mobile',
placeholder: '请输入手机号',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '联系人',
type: 'text',
name: 'serviceUser',
placeholder: '请输入联系人',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '企业QQ',
type: 'text',
name: 'QQ',
placeholder: '请输入企业QQ',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '联系微信号',
type: 'text',
name: 'weixin',
placeholder: '请输入联系微信号',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '微信二维码',
type: 'file',
name: 'weixinImg',
//placeholder: '',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '联系邮箱',
type: 'text',
name: 'email',
placeholder: '请输入联系邮箱',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '网站主页标题',
type: 'text',
name: 'pagetitle',
placeholder: '网站主页标题',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '网页描述',
type: 'textarea',
name: 'description',
placeholder: '请输入网页描述',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '搜素关键字',
type: 'text',
name: 'keywords',
placeholder: '请输入搜素关键字,逗号隔开',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '官方网址',
type: 'text',
name: 'domain',
placeholder: '请输入官方网址',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '版权信息',
type: 'text',
name: 'copyright',
placeholder: '请输入版权信息',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: 'ICP 备案',
type: 'text',
name: 'icpNumber',
placeholder: '请输入ICP 备案号',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: 'cnzz统计代码',
type: 'textarea',
name: 'cnzz',
placeholder: '请输入cnzz统计代码',
// default:'默认值测试', //新增时候默认值,可选
},
],
//修改内容默认值
data: data,
},
//修改成功之后跳转到哪个页面
successUrl: '/admin/config/index',
});
}
// 修改配置写入json
async update() {
const { ctx, app } = this;
//1.参数验证
this.ctx.validate({
id: {
type: 'int',
required: true,
desc: '配置id'
},
corporate_name: {
type: 'string', //参数类型
required: true, //是否必须
// defValue: '',
desc: '公司名称' //字段含义
},
corporate_short: {
type: 'string',
required: false,
defValue: '',
desc: '名称简写'
},
address: {
type: 'string',
required: false,
defValue: '',
desc: '地址'
},
tel: {
type: 'string',
required: false,
defValue: '',
desc: '免费咨询电话'
},
mobile: {
type: 'string',
required: false,
defValue: '',
desc: '手机号'
},
serviceUser: {
type: 'string',
required: false,
defValue: '',
desc: '联系人'
},
QQ: {
type: 'string',
required: false,
defValue: '',
desc: '企业QQ'
},
weixin: {
type: 'string',
required: false,
defValue: '',
desc: '联系微信号'
},
weixinImg: {
type: 'string',
required: false,
defValue: '',
desc: '微信二维码'
},
email: {
type: 'string',
required: false,
defValue: '',
desc: '联系邮箱'
},
pagetitle: {
type: 'string',
required: false,
defValue: '',
desc: '网站主页标题'
},
description: {
type: 'string',
required: false,
defValue: '',
desc: '网页描述'
},
keywords: {
type: 'string',
required: false,
defValue: '',
desc: '搜素关键字'
},
domain: {
type: 'string',
required: false,
defValue: '',
desc: '官方网址'
},
copyright: {
type: 'string',
required: false,
defValue: '',
desc: '版权信息'
},
icpNumber: {
type: 'string',
required: false,
defValue: '',
desc: 'ICP 备案'
},
cnzz: {
type: 'string',
required: false,
defValue: '',
desc: 'cnzz统计代码'
},
});
// 参数
const id = ctx.params.id;
let data = await this.getsiteConfigJson();
let index = data.data.findIndex(item => item.id == id);
if (index == -1) {
return ctx.apiFail('该内容不存在');
}
let params = this.ctx.request.body;
params.id = id;
console.log('修改的数据', params);
console.log('修改json文件的数组元素索引', index);
console.log('原json文件数据', data);
data.data[index] = params;
console.log('写入json文件的最终所有数据', data);
fs.writeFile(paths.file, JSON.stringify(data), (err) => {
if (err) throw err;
console.log('写入成功')
});
}
// 创建配置界面
async create() {
const { ctx, app } = this;
//渲染公共模版
await ctx.renderTemplate({
title: '创建配置',//现在网页title,面包屑导航title,页面标题
tempType: 'form', //模板类型:table表格模板 ,form表单模板
form: {
//提交地址
action: "/admin/config/save",
// 字段
fields: [
{
label: '公司名称',
type: 'text',
name: 'corporate_name',
placeholder: '请输入公司名称',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '名称简写',
type: 'text',
name: 'corporate_short',
placeholder: '请输入名称简写',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '地址',
type: 'text',
name: 'address',
placeholder: '请输入公司地址',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '免费咨询电话',
type: 'text',
name: 'tel',
placeholder: '请输入免费咨询电话',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '手机号',
type: 'text',
name: 'mobile',
placeholder: '请输入手机号',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '联系人',
type: 'text',
name: 'serviceUser',
placeholder: '请输入联系人',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '企业QQ',
type: 'text',
name: 'QQ',
placeholder: '请输入企业QQ',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '联系微信号',
type: 'text',
name: 'weixin',
placeholder: '请输入联系微信号',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '微信二维码',
type: 'file',
name: 'weixinImg',
//placeholder: '',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '联系邮箱',
type: 'text',
name: 'email',
placeholder: '请输入联系邮箱',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '网站主页标题',
type: 'text',
name: 'pagetitle',
placeholder: '网站主页标题',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '网页描述',
type: 'textarea',
name: 'description',
placeholder: '请输入网页描述',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '搜素关键字',
type: 'text',
name: 'keywords',
placeholder: '请输入搜素关键字,逗号隔开',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '官方网址',
type: 'text',
name: 'domain',
placeholder: '请输入官方网址',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '版权信息',
type: 'text',
name: 'copyright',
placeholder: '请输入版权信息',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: 'ICP 备案',
type: 'text',
name: 'icpNumber',
placeholder: '请输入ICP 备案号',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: 'cnzz统计代码',
type: 'textarea',
name: 'cnzz',
placeholder: '请输入cnzz统计代码',
// default:'默认值测试', //新增时候默认值,可选
},
],
},
//新增成功之后跳转到哪个页面
successUrl: '/admin/config/index',
});
}
// 创建配置提交数据
async save() {
//参数验证
this.ctx.validate({
corporate_name: {
type: 'string', //参数类型
required: true, //是否必须
// defValue: '',
desc: '公司名称' //字段含义
},
corporate_short: {
type: 'string',
required: false,
defValue: '',
desc: '名称简写'
},
address: {
type: 'string',
required: false,
defValue: '',
desc: '地址'
},
tel: {
type: 'string',
required: false,
defValue: '',
desc: '免费咨询电话'
},
mobile: {
type: 'string',
required: false,
defValue: '',
desc: '手机号'
},
serviceUser: {
type: 'string',
required: false,
defValue: '',
desc: '联系人'
},
QQ: {
type: 'string',
required: false,
defValue: '',
desc: '企业QQ'
},
weixin: {
type: 'string',
required: false,
defValue: '',
desc: '联系微信号'
},
weixinImg: {
type: 'string',
required: false,
defValue: '',
desc: '微信二维码'
},
email: {
type: 'string',
required: false,
defValue: '',
desc: '联系邮箱'
},
pagetitle: {
type: 'string',
required: false,
defValue: '',
desc: '网站主页标题'
},
description: {
type: 'string',
required: false,
defValue: '',
desc: '网页描述'
},
keywords: {
type: 'string',
required: false,
defValue: '',
desc: '搜素关键字'
},
domain: {
type: 'string',
required: false,
defValue: '',
desc: '官方网址'
},
copyright: {
type: 'string',
required: false,
defValue: '',
desc: '版权信息'
},
icpNumber: {
type: 'string',
required: false,
defValue: '',
desc: 'ICP 备案'
},
cnzz: {
type: 'string',
required: false,
defValue: '',
desc: 'cnzz统计代码'
},
});
//写入数据
let data = this.ctx.request.body;
await this.addconfig(data, paths);
}
//写入json文件前的处理
async addconfig(data, paths) {
//创建一个文件夹data
if (!fs.existsSync(paths.dir)) {
fs.mkdirSync(paths.dir);
};
//判断json文件是否存在,存在说明之前写入过了,先读一下
let flag = fs.existsSync(paths.file);
if (flag) {
//存在先读取一下
await this.readconfig(paths.file, data)
} else {
//不存在,首次直接写
let ms = data;
ms.id = 1;
//加入时间,所在地等等
// ms.timestamp = new Date().getTime();
// console.log(ms);
let o = {};
o.data = [];
o.data.push(ms);
o.total = 1;
o.currentId = 1;
// console.log(o);
// console.log(JSON.stringify(o));
//写入内容,同步异步promise,以及可写流
await this.writeconfig(paths.file, o);
}
}
//存在先读取一下
async readconfig(path, data) {
//读取内容,同步异步promise,以及可写流
fs.readFile(path, {
flag: 'r',
encoding: 'utf-8',
}, (err, oldmessage) => {
if (err) throw err;
oldmessage = JSON.parse(oldmessage);
console.log(oldmessage)
console.log(data);
//处理数据
data.id = oldmessage.currentId + 1;
//加入时间,所在地等等
// data.timestamp = new Date().getTime();
//大对象
oldmessage.data.push(data);
oldmessage.total = oldmessage.data.length;
oldmessage.currentId = data.id;
console.log(oldmessage);
//写入内容,同步异步promise,以及可写流
this.writeconfig(path, oldmessage);
})
}
//写入json
async writeconfig(path, data) {
fs.writeFile(path, JSON.stringify(data), (err) => {
if (err) throw err;
console.log('写入成功')
});
}
// 删除配置
async delete() {
//读json文件
const { ctx, app } = this;
let data = await this.getsiteConfigJson();
console.log('配置数据', JSON.stringify(data));
const id = ctx.params.id;
let index = data.data.findIndex(item => item.id == id);
if (index == -1) {
return ctx.apiFail('该内容不存在');
}
console.log(index);
data.data.splice(index, 1);
data.total = data.data.length;
console.log('删除之后即将重新写入json的数据', data);
await this.writeconfig(paths.file, data);
//提示
ctx.toast('删除配置成功', 'success');
//跳转
ctx.redirect('/admin/config/index');
}
}
module.exports = ConfigController;
路由
// 配置列表
router.get('/admin/config/index', controller.admin.config.index);
//修改配置
router.get('/admin/config/edit/:id', controller.admin.config.edit);
//修改配置写入json
router.post('/admin/config/update/:id', controller.admin.config.update);
//创建配置
router.get('/admin/config/create', controller.admin.config.create);
// 创建配置提交数据
router.post('/admin/config/save', controller.admin.config.save);
//删除配置
router.get('/admin/config/delete/:id', controller.admin.config.delete);
# 八、后台初始化管理员登录逻辑
后台上线初期,数据库和json文件都是空的,没有任何数据(除非将开发环境的数据库和json文件拷贝到线上服务器),那么就意味着管理员表也是空的,无法进行登录,因此一般情况下,后台上线初期,需要手动添加一个超级管理员账号。
给manager表新增两个字段:status(状态:1:启用,0:禁用)和 super(是否超级管理员 1是,0:否)
迁移文件database/migrations/-init-manager.jsstatus: { type: INTEGER(1), allowNull: true, defaultValue: 1, comment: '状态:1:启用,0:禁用' }, super: { type: INTEGER(1), allowNull: true, defaultValue: 1, comment: '是否超级管理员 1是,0:否' },模型文件
app/model/manager.js同上,加上这两个字段
注:迁移文件的修改不会影响到本项目(会在以后的项目中才会在manager表生效这两个字段),如果本项目要生效,需直接修改数据库manager表字段,新增这2个字段即可
(或者数据库重新初始化)
// 升级数据库-创建数据表 npx sequelize db:migrate // 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 npx sequelize db:migrate:undo // 可以通过 `db:migrate:undo:all` 回退到初始状态 npx sequelize db:migrate:undo:all
# 后台初始化管理员登录逻辑
这个时候需要考虑,当项目刚初始化的时候,由于数据库的所有表都没有内容,当然也包括管理员表没有管理员,此时可在管理员登录界面调整一下,初始化创建一个管理员,并且该管理员角色是:超级管理员
- 控制器
app/controller/admin/home.js
//管理员登录页面---后台登录页面
async login() {
const { ctx,app } = this;
let toast = this.ctx.cookies.get('toast',{
encrypt:true
});
toast = toast ? JSON.parse(toast) : null;
// 新增初始化逻辑,当没有管理员的时候新增一个超级管理员
let manager = await app.model.Manager.findAndCountAll();
let pageobj = {};
if(manager.count == 0){
// console.log('应该创建首个超级管理员');
pageobj.type = 'createSuperManager';
pageobj.title = '创建后台首个超级管理员账户';
pageobj.h1 = '创建超级管理员';
pageobj.p = '创建后台首个超级管理员账户';
}else{
// console.log('正常登录');
pageobj.type = 'login';
pageobj.title = '登录';
pageobj.h1 = '登 录';
pageobj.p = '后台管理中心欢迎您';
}
await this.ctx.render('admin/home/login.html',{
toast,
pageobj
});
}
// 创建超级管理员账号
async createSuperManager(){
const { ctx,app } = this;
//超级管理员是数据库没有管理员的时候才创建的
let manager = await app.model.Manager.findAndCountAll();
if(manager.count != 0){
return ctx.apiFail('非法操作,系统拒绝您的请求');
}
//1.参数验证
ctx.validate({
username: {
type: 'string', //参数类型
required: true, //是否必须
// defValue: '',
desc: '管理员账号' //字段含义
},
password: {
type: 'string',
required: true,
// defValue: '',
desc: '管理员密码'
},
});
// 拿参数
const { username, password } = ctx.request.body;
await app.model.Manager.create({
username,
password,
status:1,
super:'1',//超级管理员
});
ctx.apiSuccess('ok');
}
- 路由
app/router/admin/admin.js
// 创建超级管理员
router.post('/admin/createSuperManager', controller.admin.home.createSuperManager);
- 中间件加上创建超级管理员免登录
// 对中间件adminAuth进一步配置
config.adminAuth = {
ignore: [
...
"/admin/createSuperManager",
...
],
};
- 模版
app/view/admin/home/login.html
<div class="login-right">
<div class="login-right-wrap">
<h1>{{pageobj.h1}}</h1>
<p class="account-subtitle">{{pageobj.p}}</p>
<!-- Form -->
<form>
<div class="form-group">
<input class="form-control" type="text"
placeholder="输入管理员账号..."
v-model="form.username">
</div>
<div class="form-group">
<input class="form-control" type="password"
placeholder="输入管理员密码..."
v-model="form.password">
</div>
<div class="form-group">
<button
class="btn btn-primary btn-block"
type="submit"
@click.stop.prevent="submit">{{ '登 录' if pageobj.type == 'login' else '创建超级管理员' }}</button>
</div>
</form>
</div>
</div>
...
methods: {
submit(){
//console.log('提交登录',this.form);
const type = "{{pageobj.type}}";
// console.log('类型',type);
const url = type == 'createSuperManager' ?
'/admin/createSuperManager?_csrf={{ctx.csrf|safe}}' :
"/admin/loginevent?_csrf={{ctx.csrf|safe}}";
// console.log('提交地址',url);
// return;
$.ajax({
type: 'post',
url:url,
contentType:'application/json;charset=UTF-8;',
data:JSON.stringify(this.form),
success: function (response, stutas, xhr) {
console.log(response)
const msg = type == 'createSuperManager' ? "创建超级管理员成功" : "登录成功";
Vueapp.$refs.toast.show({
msg,
type:'success',
delay:1000,
success:function(){
// 跳转到某个页面
const href = type == 'createSuperManager' ? '/admin/login' : "/admin"
window.location.href = href;
}
});
},
error:function(e){
// console.log(e)
Vueapp.$refs.toast.show({
msg:e.responseJSON.data,
type:'danger',
delay:3000
});
}
});
}
}
# 九、简单权限管理
# 1. 管理员栏目:超级管理员可以查看所有管理员,普通管理员只能看自己
控制器 app/controller/admin/manager.js
//创建管理员列表页面
async index() {
...
//超级管理员的特殊权限
console.log(ctx.session.auth);
let buttons = null;
let columns = [];
if(ctx.session.auth.super == 1){
buttons = {
buttons: [
{
url: '/admin/manager/create',//新增路径
desc: '新增管理员',//新增 //按钮名称
// icon: 'fa fa-plus fa-lg',//按钮图标
}
],
}
//禁用功能
columns = [
{
title: '可用状态',
key: 'status',
width: 200,//可选
class: 'text-center',//可选
hidekeyData: true,//是否隐藏key对应的数据
render(item) {
console.log('可用状态里面每个item', item);
let arr = [
{ value: 1, name: '可用' },
{ value: 0, name: '禁用' },
];
let str = `<div class="btn-group btn-group-${item.id}">`;
for (let i = 0; i < arr.length; i++) {
str += `<button type="button" class="btn btn-light" data="${item.status}"
value="${arr[i].value}"
@click="changeBtnStatus('status','btn-group-${item.id}',${arr[i].value},${i},${item.id},'manager','Manager')">${arr[i].name}</button>`;
}
str += `</div>`;
return str;
}
}
];
}else{
data = await ctx.page('Manager',{
id: ctx.session.auth.id
});
}
//渲染公共模版
await ctx.renderTemplate({
title: '管理员列表',//现在网页title,面包屑导航title,页面标题
data,
tempType: 'table', //模板类型:table表格模板 ,form表单模板
table: {
//表格上方按钮,没有不要填buttons
// buttons: [
// {
// url: '/admin/manager/create',//新增路径
// desc: '新增管理员',//新增 //按钮名称
// // icon: 'fa fa-plus fa-lg',//按钮图标
// }
// ],
...buttons,
//表头
columns: [
...
...columns,
...
},
],
},
});
}
//创建管理员提交数据
async save() {
//超级管理员的特殊权限
console.log(this.ctx.session.auth);
if(this.ctx.session.auth.super != 1){
return this.ctx.apiFail('您无权操作此项功能');
}
...
}
//删除管理员功能
async delete() {
const { ctx, app } = this;
const id = ctx.params.id;
//超级管理员的特殊权限,普通管理员只能删除自己
console.log(this.ctx.session.auth);
if(this.ctx.session.auth.super == 1 || (this.ctx.session.auth.id == id && this.ctx.session.auth.super == 0)){
await app.model.Manager.destroy({
where: {
id
}
});
//提示
ctx.toast('管理员删除成功', 'success');
//跳转
ctx.redirect('/admin/manager/index');
}else{
return this.ctx.apiFail('您无权操作此项功能');
}
}
//修改管理员数据功能
async update() {
if(this.ctx.session.auth.super == 0 && this.ctx.session.auth.id != this.ctx.params.id){
return this.ctx.apiFail('您无权操作此项功能');
}
...
}
# 2. 权限分配
data/root.json
图标查找:https://fontawesome.dashgame.com/ (opens new window)
//初步
[
{ "name": "主面板", "icon": "fe fe-home", "url": "/admin" },
{ "name": "管理员", "icon": "fe fe-user-plus", "url": "/admin/manager/index" },
{ "name": "留言板", "icon": "fe fe-document", "url": "/admin/message/index" },
{ "name": "直播用户", "icon": "fa fa-user-o", "url": "/admin/liveuser/index" },
{ "name": "直播礼物管理", "icon": "fa fa-user-o", "url": "/admin/livegift/index" },
{ "name": "直播订单管理", "icon": "fa fa-user-o", "url": "/admin/liveorder/index" },
{ "name": "直播间管理", "icon": "fa fa-user-o", "url": "/admin/live-/index" },
{ "name": "分类管理", "icon": "fa fa-user-o", "url": "/admin/category/index" },
{ "name": "新闻内容管理", "icon": "fa fa-user-o", "url": "/admin/news/index" },
{ "name": "配置管理", "icon": "fa fa-user-o", "url": "/admin/config/index" }
]
//转成树形结构
[
{"id":1,"pid":0, "name": "网站", "icon": "fa fa-chrome", "url": ""},
{"id":2,"pid":1, "name": "主面板", "icon": "fa fa-pie-chart", "url": "/admin" },
{"id":3,"pid":1, "name": "管理员", "icon": "fe fe-user-plus", "url": "/admin/manager/index" },
{"id":4,"pid":1, "name": "留言板", "icon": "fe fe-document", "url": "/admin/message/index" },
{"id":5,"pid":1, "name": "分类管理", "icon": "fa fa-list", "url": "/admin/category/index" },
{"id":6,"pid":1, "name": "新闻内容管理", "icon": "fa fa-file-text-o", "url": "/admin/news/index" },
{"id":7,"pid":1, "name": "配置管理", "icon": "fa fa-wrench", "url": "/admin/config/index" },
{"id":8,"pid":0, "name": "直播","icon": "fa fa-video-camera", "url": ""},
{"id":9,"pid":8, "name": "直播用户", "icon": "fa fa-venus-mars", "url": "/admin/liveuser/index" },
{"id":10,"pid":8, "name": "直播礼物管理", "icon": "fa fa-gift", "url": "/admin/livegift/index" },
{"id":11,"pid":8, "name": "直播订单管理", "icon": "fa fa-credit-card", "url": "/admin/liveorder/index" },
{"id":12,"pid":8, "name": "直播间管理", "icon": "fa fa-tv", "url": "/admin/live-/index" }
]
中间件菜单 app/middleware/admin_auth.js
module.exports = (option, app) => {
return async function adminMenu(ctx, next) {
// let menus = [
// { name: '主面板', icon: 'fe fe-home', url: '/admin' },
// { name: '管理员', icon: 'fe fe-user-plus', url: '/admin/manager/index' },
// { name: '留言板', icon: 'fe fe-document', url: '/admin/message/index' },
// { name: '直播用户', icon: 'fa fa-user-o', url: '/admin/liveuser/index' },
// { name: '直播礼物管理', icon: 'fa fa-user-o', url: '/admin/livegift/index' },
// { name: '直播订单管理', icon: 'fa fa-user-o', url: '/admin/liveorder/index' },
// { name: '直播间管理', icon: 'fa fa-user-o', url: '/admin/live-/index' },
// { name: '分类管理', icon: 'fa fa-user-o', url: '/admin/category/index' },
// { name: '新闻内容管理', icon: 'fa fa-user-o', url: '/admin/news/index' },
// { name: '配置管理', icon: 'fa fa-user-o', url: '/admin/config/index' },
// ];
let menus = [
{"id":1,"pid":0, "name": "网站", "icon": "fa fa-chrome", "url": ""},
{"id":2,"pid":1, "name": "主面板", "icon": "fa fa-pie-chart", "url": "/admin" },
{"id":3,"pid":1, "name": "管理员", "icon": "fe fe-user-plus", "url": "/admin/manager/index" },
{"id":4,"pid":1, "name": "留言板", "icon": "fe fe-document", "url": "/admin/message/index" },
{"id":5,"pid":1, "name": "分类管理", "icon": "fa fa-list", "url": "/admin/category/index" },
{"id":6,"pid":1, "name": "新闻内容管理", "icon": "fa fa-file-text-o", "url": "/admin/news/index" },
{"id":7,"pid":1, "name": "配置管理", "icon": "fa fa-wrench", "url": "/admin/config/index" },
{"id":8,"pid":0, "name": "直播","icon": "fa fa-video-camera", "url": ""},
{"id":9,"pid":8, "name": "直播用户", "icon": "fa fa-venus-mars", "url": "/admin/liveuser/index" },
{"id":10,"pid":8, "name": "直播礼物管理", "icon": "fa fa-gift", "url": "/admin/livegift/index" },
{"id":11,"pid":8, "name": "直播订单管理", "icon": "fa fa-credit-card", "url": "/admin/liveorder/index" },
{"id":12,"pid":8, "name": "直播间管理", "icon": "fa fa-tv", "url": "/admin/live-/index" }
];
// 和我们分页模版类似,通过ctx.locals对象挂载代码
ctx.locals.menus = menus.map(item => {
// console.log('当前请求地址', ctx.request.url)
// console.log('遍历项地址', item.url)
let baseURL = item.url.replace('/index', '');
// console.log('处理之后遍历项地址', baseURL);
// console.log('判断',ctx.request.url.startsWith(baseURL));
if ((ctx.request.url == '/admin' && item.url == '/admin') ||
(ctx.request.url != '/admin' && item.url != '/admin' && ctx.request.url.startsWith(baseURL) && item.url)
) {
item.active = 'active';
}
if(item.url){
item.style = 'text-indent:2em';
}else{
item.style = 'cursor:default';
}
return item;
});
// console.log(ctx.locals.menus);
await next();
}
}
菜单模版 app/view/admin/layout/_slider.html
{% for item in ctx.locals.menus %}
<li class="{{item.active}}" >
<a href="{{item.url}}" style="{{item.style}}"><i class="{{item.icon}}" style="font-size: 18px;"></i> <span>{{item.name}}</span></a>
</li>
{% endfor %}
# 3. 网站后台管理员简单权限分配功能实现
(由于内容较多,单独放在新页面讲解) 具体查看:网站后台管理员简单权限分配功能实现
# 【第二学期第3季课程】其它章节
# 章节1.课程介绍
# 章节2.Egg.js基础
# 一、关于Egg.js
# ① 安装Egg.js项目
# ② 写一个api接口进行测试
# ③ 说明一下,关于调试课件代码的问题
# ④ 自定义创建一个控制器
# 二、eggjs中的get请求post请求处理
# ① get方式路由传参:带?获取参数 ctx.query.参数名,不带?获取参数 ctx.params.参数名
# ② 设置响应状态码: ctx.status
# ③ post请求参数处理
# 一、安装get/post等请求的调试工具:postman
# 1. 下载postman
# 2. 安装postman
# 二、post请求获取参数:ctx.request.body
# 1. 关闭csrf功能开启跨域请求
# 2. eggjs中post请求:ctx.request.body
# 三、案例:eggjs + postman测试工具完成留言数据写入json文件
# 四、mysql(MySQL)数据库基础
# 五、eggjs项目中sequelize模型创建mysql数据库
# 1.安装egg-sequelize 插件
# 2. 在config/plugin.js中引入 egg-sequelize 插件
# 3. 在config/config.default.js中配置数据库连接
# 4. 安装 sequelize-cli插件
# 5. 数据库 Migrations 迁移文件相关的内容都放在database目录下
# 6. 初始化 Migrations 配置文件和目录
# 7. 在生成的database/config.json 修改一下配置内容
# 8. 创建数据库
# 9. 创建数据库迁移文件
# 10. 执行 migrate 进行数据库变更创建表
# 六、egg.js项目中sequelize模型新增数据到数据库
# 1. 创建模型文件
# 2. 编写模型文件
# 3. 插入一条数据到数据库:create方法
# 3.1 定义路由
# 3.2 定义控制器
# 4. 批量插入数据到数据库:bulkCreate方法
# 5. 修改器set()方法:数据插入到数据库前可自动修改成指定要求的数据
# 七、egg.js项目查询数据
# 1. 查询数据库中的单个数据:主键查询方法:findByPk(主键字段)、如果需要多个条件,可以使用findOne方法
# 2. 查询数据库中的多个数据:查询多个findAll(),查询多个并统计条数findAndCountAll()
# 3. 获取器get()方法:查询数据后可自动修改成指定要求的数据
# 八、egg.js项目sequelize模型where操作符
# 示例
# 1. where操作符
# 2. where操作符范围选项
# 九、egg.js项目sequelize模型查询结果指定字段、排序、分页
# 1. attributes属性指定返回的字段,exclude属性指定排除的字段
# 2. 排序:order
# 3. 分页:limit指定每页返回多少条数据、offset指定偏移量
# 示例
# 十、egg.js项目sequelize模型更新数据
# 1. 更新数据:save方法,指定修改字段fields属性
# 2. 如果觉得save方法更新字段非常麻烦,可以使用update方法批量修改字段,第二个参数可指定修改字段
# 十一、egg.js项目sequelize模删除、批量删除数据:destroy方法
# 十二、错误和异常统一处理
# 十三、中间件配置
# 十四、参数验证
# 1. 安装插件
# 2. 配置插件 config/plugin.js
# 3. 配置 config/config.default.js
# 4. 控制器举例
# 5. 参数验证的错误提示在中间件中设置一下
# 6.ValParams API 说明
# 十五、路由分组
# 1. 新建 app/router目录,在该目录下新建对应控制器名文件
# 2. 在app/router/message.js中写路由
# 3.在app/router.js中按控制器指定分组
# 十六、模版引擎
# 1. 安装模版渲染插件
# 2. 在config/plugin.js中配置插件
# 3. 在config/config.default.js中配置模版引擎
# 4. vscode安装一下nunjucks模版引擎扩展,方便代码提示
# 5. 新建 app/view目录,以后所有模版放这个目录里面
# 6. 控制器 app/controller/home.js 写一个方法
# 7. 重启项目,访问路由即可看到网页内容
# egg.js基础课程总结
# 章节3.响应式网页布局
# 一、响应式网页布局是什么
# 二、响应式网页布局的实现方法
# 三、简单的响应式页面案例
# 四、Bootstrap框架
# 五、响应式后台管理系统(egg.js + Bootstrap)
# ① 搭建界面引入模版html文件
# ② 创建管理员(页面、创建数据库表及提交数据)
# ③ 管理员列表
# ④ 管理员列表分页功能
# ⑤ 公共模板开发
# ⑥ 后台管理员登录
# ⑦ 后台用户留言板管理板块
# ⑧ 优化公共模版表格能够显示头像
# ⑨ 后台左侧菜单栏
# ⑩ 上传文件
# ⑪ 上传或修改管理员头像
# 章节4.Egg.js和Mysql数据库进阶进一步开发网站后台
# 一、用户管理板块(以直播功能中的用户表liveuser表为例)
# ① 用户管理板块说明
# ② 具体实现过程
# 二、礼物管理(以直播功能中的礼物表livegift表为例)
# ① 礼物管理板块说明
# ② 具体实现过程
# 三、订单管理(以直播功能中的订单表liveorder表为例)
# ① 初步理解关联关系
# ② 具体实现过程
# 四、直播间管理(以直播功能中的直播间表live表为例)
# ① 多模型关联查询实践
# ② 具体实现过程
# 五、mysql语句进一步理解模型关联关系(Mysql进阶)
# ① 关联关系进一步说明
# ② mysql数据库基础-三、mysql子查询和连表查询(文档搜索:mysql子查询和连表查询)
# 六、企业网站栏目管理、内容管理
# ① 企业网站栏目管理、内容管理说明
# ② 企业网站后台栏目管理具体实现
# ③ 企业网站后台内容管理具体实现
# 七、网站配置管理
# ① 网站配置管理代码实现
# 八、后台初始化管理员登录逻辑
# ① 登录逻辑代码实现
# 九、简单权限管理
# ① 管理员栏目:超级管理员可以查看所有管理员,普通管理员只能看自己
# ② 权限分配
# ③ 网站后台管理员简单权限分配功能实现
# 章节5.企业网站前端部分
# 其它学期课程
# 第一学期(学习顺序:01)
第一学期课程专为零基础的学员定制录制的,纯html+css做企业网站的网页,主讲html和css的相关基础知识,flex布局相关知识,封装css基础样式库,引入字体图标及网页开发基础布局思维,完成企业网站网页的开发过程。
[第一学期学习视频]
# 第二学期【第1季】(学习顺序:02)
# 第二学期【第2季】(学习顺序:03)
JavaScript中的面向对象,类,ajax,封装js库过渡到jQuery, vue.js基础配置网站页面,建议所有学员观看。
[第2季学习文档] [第2季学习视频]
# 第二学期【第3季】(学习顺序:04)
egg.js基础,响应式网页布局,Bootstrap框架,响应式后台系统管理,完整企业网站前后台开发,建议所有学员观看。
[第3季学习文档] [第3季学习视频]
# 第二学期【第4季】(学习顺序:05)
主要对第三季,同学们开发的企业网站,进行一个完整的上线运维流程的一个讲解,同学们将网站开发完成之后,如何进行上线运维,将项目交付给客户。
[第4季学习文档] [第4季学习视频]