跳到主要内容

22 篇博文 含有标签「Egg.js」

查看所有标签

一、硬件介绍

二、云打印机的使用方法

还是以上文提到的飞蛾云打印机为例,进行介绍:

  1. 给打印机连接电源。
  2. 给打印机装上打印纸。
  3. 配置打印机WiFi密码。
  4. 通过打印机的二维码来配置设备,并进行测试打印。

三、开发者接入云打印的方法

  1. 通过飞蛾云打印的开放平台。
  2. 通过文档注册账户。
  3. 下载对应的SDK。
  4. 配置SDK,并进行测试打印。

云打印机的SDK本质就是一个实现打印的Demo,针对不同的开发语言。

四、格式化日期

在Vue2中,我们可以使用过滤器对日期进行格式化,但是Vue3中去除了这个过滤器,官方建议使用计算属性或者自定义方法来对日期进行格式化。

下面主要介绍如何通过第三方模块moment,进行日期格式化。

  1. 在项目中安装模块
 npm i moment --save
  1. 在vue的指定页面中引入moment
import moment from 'moment'
  1. 定义一个方法

首先定义一个模式串,然后通过moment将毫秒变为秒,然后利用我们的模式串进行转换。

getTime(time: any) {
const pattern = "YYYY-MM-DD hh:mm:ss";
return moment(time * 1000).format(pattern)
}
  1. 在模板中调用我们上文定义好的方法
<div class="order_time">下单时间:{{getTime(list.addTime)}}</div>
  1. 此时毫秒的数据将变为年月日的时间格式。

格式化日期模式串的参数含义

image.png

参考文献

总结

我们常见的扫码点餐实现云打印小票的实现原理就是服务器通过SDK下达打印的指令,然后通过WIFI传递给打印机,从而实现云打印。


JustinEgg.js阅读需 2 分钟

将时间戳格式化为时分秒

  1. 在app文件夹下创建文件夹extend,然后在extend文件夹下创建helper.js
const sd = require('silly-datetime');

module.exports = {
formatTime(unix) {
return sd.format(new Date(unix * 1000),'YYYY-MM-DD HH:mm');
}
}
  1. 将时间戳修改为时分秒
helper.formatTime(list[i].addTime)

JustinEgg.js阅读需 1 分钟

服务端配置JWT的方法

  1. 安装jsonwebtoken第三方工具包
npm i jsonwebtoken
  1. 引入jwt
var jwt = require('jsonwebtoken');
  1. 在控制器中生成token,并进行返回
router.get('/login', function (req, res, next) {
var token = jwt.sign({ uid: '1', username: "zhangsan" }, 'this is sign', {
expiresIn: 60*60*24
});
res.send({
"token":token
});
});

注意:sign函数接收的第一个参数表示的是要传递的信息,第二个参数相当于是签名,第三个参数接收的是一个配置对象,对象中可以设置token的过期时间。

  1. 安装basic-auth
npm install basic-auth
  1. 引入basic-auth
var auth = require('basic-auth')
  1. 获取到用户传来的token
const token = auth(req);

注意:通过postman传递token,可以通过下面的这种方式来进行传递。

image.png

  1. 验证token是否合法

下面的验证是在控制器中实现的,如果想要实现复用可以在中间件中实现。

router.get('/address', function (req, res, next) {

const token = auth(req);
if (token) {
try {
const decoded = jwt.verify(token.name, 'this is sign');
if (decoded) {
res.send({
success: true,
msg: "验证成功"
})
} else {
res.send({
success: false,
msg: "token错误"
})
}
} catch (error) {
res.send({
success: false,
msg: error
})
}
} else {
res.send({
success: false,
msg: "token错误"
})
}
})
  1. 通过中间件复用验证token的逻辑
//权限判断的中间件
var authMiddleWare = function (req, res, next) {

var result = auth(req);
if (!result) {
res.send({
success: false,
msg: "token错误"
});
return;
}
try {
var decoded = jwt.verify(result.name, 'this is sign');
console.log(decoded);
next();
} catch (error) {
res.send({
success: false,
msg: error
})
}
}
  1. 在路由匹配时,通过第二个参数来加入中间件判断
router.get('/address',authMiddleWare, function (req, res, next) {
res.send({
success: true,
result:[
{"name":"张三","address":"北京市"},
{"name":"李四","address":"北京市"},
{"name":"王五","address":"北京市"}
]
});
});

前端携带token进行请求的方法

  1. 首先将axios挂载到Vue的原型对象上。
Vue.prototype.$http = axios;
  1. 前端获取token的方法

下面的这个方法不仅获取到了token,而且将token保存到了localstorage上。

  this.$http
.get("http://localhost:3000/api/login")
.then(function(response) {
console.log(response.data.token);

//保存用户信息 和 token
localStorage.setItem('token',response.data.token);
})
.catch(function(error) {
console.log(error);
});
  1. 前端携带token发送请求
getAddress() {
var token=localStorage.getItem('token');
this.$http
.get("http://localhost:3000/api/address?uid=1&address_id=345", {
auth: {
username: token,
password: 'sign'
}
})
.then(function(response) {
console.log(response);
})
.catch(function(error) {
console.log(error);
});
}

Egg.js中使用egg-jwt实现接口权限验证

  1. 安装egg-jwt
cnpm i egg-jwt --save
  1. 在config下的plugin.js中进行配置
jwt: {
enable: true,
package: 'egg-jwt'
}
  1. 在config.default.js下配置密钥
  // 配置JWT的密钥
config.jwt = {
secret: "123456xxx"
}
  1. 通过控制器请求指定路由返回token
  async login() {
const token = this.app.jwt.sign({foo: 'bar'},this.app.config.jwt.secret,{
expiresIn: 60*60*2
});
this.ctx.body = {
"success": true,
"token": token
}
}
  1. 配置指定路由需要进行token验证

只需在路由的第二个参数上进行配置即可。

router.get(`/api/v1`,app.jwt, controller.api.v1.index);

注意在egg.js中使用的不是basic auth,而是bearer token,如果想要通过测试工具进行测试可以使用VSCode中的插件Postcode。

image.png

  1. 由于egg.js中使用的不是basic auth,因此前端携带token的方式也要进行改变。
getIndex() {
var api = "http://localhost:7001/api/v1";
this.Axios.get(api, {
headers: {
Authorization: "Bearer " + this.token,
},
})
.then((response) => {
console.log(response.data);
})
.catch((err) => {
console.log(err);
});
}

上文演示的都是get请求携带token的方法,下面我们演示下post请求如何携带token。

addPeopleInfo() {
var api = "http://localhost:7001/api/v1/addPeopleInfo";
this.Axios.post(api, {
tableId: 12,
pNum: 4,
pMark: "不要辣椒",
},{
headers: {
Authorization: "Bearer " + this.token,
},
})
.then((response) => {
console.log(response.data);
})
.catch((err) => {
console.log(err);
});
}

JustinEgg.js阅读需 3 分钟

借助qr-image实现生成二维码

  1. 引入qr-image包
const qr = require('qr-image');
  1. 在service中定义获取二维码的函数
  async getQrImage(qrText) {
return new Promise((resolve,reject) => {
try {
const qrImage = qr.image(qrText,{type: 'png'});
resolve(qrImage);
} catch (error) {
reject(false);
}
})
}

在控制器中调用第二步的函数即可获取到一个二维码对象。

借助canvas将二维码图片和背景图片合成在一起

  1. 引入canvas
const { createCanvas,Image } = require('canvas');
  1. 在service中定义异步函数将两张图片合成在一起
async getCanvasImage(text, bgDir, codeDir) {
return new Promise((reslove, reject) => {
try {
const canvas = createCanvas(502, 448)
const ctx = canvas.getContext('2d');
//绘制背景图片
const img1 = new Image();
img1.onload = () => {
ctx.drawImage(img1, 0, 0);
//填充文字 注意字体
ctx.font = '30px "Microsoft YaHei"'
ctx.fillStyle = "#ffffff";
ctx.fillText(text, 195, 185);

const img2 = new Image();
img2.onload = () => {
ctx.drawImage(img2, 170, 210);
reslove(canvas.createPNGStream());
}
img2.onerror = err => {
reject(err);
}
//需要注意顺序
img2.src = codeDir;
}
img1.onerror = err => { reject(err); }
//需要注意顺序
img1.src = bgDir;
} catch (error) {
reject(false);
}
})
}
  1. 在控制器中传入相关参数进行合并
async showCode() {
let id = this.ctx.request.query.id;
let table = await this.ctx.model.Table.findByPk(id);
let qrImage = await this.ctx.service.tools.getQrImage("http://xxx");
let qrImageObj = await this.ctx.service.tools.uploadCos("code_1.jpg",qrImage);

let canvasStream = await this.ctx.service.tools.getCanvasImage(
table.title,
'app/public/admin/images/bg.png',
"http://" + qrImageObj.Location
);
let canvasImageObj = await this.ctx.service.tools.uploadCos("code_image_1.png",canvasStream);
// this.ctx.body = canvasImageObj;
await this.ctx.render("admin/table/code",{
imgUrl: "http://" + canvasImageObj.Location
})
}

使用Html5 Canvas加水印合成图片二维码

在已经有node canvas的情况下,还介绍html5 canvas是因为node canvas需要系统安装指定的程序,这对系统环境要求较高,而HTML5 canvas则不需要安装。

  1. 静态静态页面中定义好canvas函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#canvas {
margin: 0 auto;
display: block;
width: 502px;
height: 448px;
}
</style>

</head>
<body>
<canvas id="canvas" width="505" height="448"></canvas>
</body>
<script>
// 1.获取canvas这个DOM节点
var canvas = document.querySelector('#canvas');
//2.定义2d画布
var ctx = canvas.getContext('2d');
const img1 = new Image();
img1.onload = () => {
ctx.drawImage(img1, 0, 0);
//填充文字 注意字体
ctx.font = '30px "Microsoft YaHei"'
ctx.fillStyle = "#ffffff";
ctx.fillText("<%=text%>", 195, 180);

const img2 = new Image();
img2.onload = () => {
ctx.drawImage(img2, 170, 205);
}
img2.onerror = err => {
// throw err
console.log(err);
}
//需要注意顺序
img2.src = "<%=codeSrc%>";
}
img1.onerror = err => { console.log(err); }
//需要注意顺序
img1.src = "<%=bgSrc%>";
</script>
</html>
  1. 在控制器中渲染数据即可
  async showCode() {
let id = this.ctx.request.query.id;
let table = await this.ctx.model.Table.findByPk(id);
let qrImage = await this.ctx.service.tools.getQrImage("http://jpy.wiki");
let qrImageObj = await this.ctx.service.tools.uploadCos("code_1.jpg",qrImage);

await this.ctx.render("admin/table/code",{
text: table.title,
bgSrc: '/public/admin/images/bg.png',
codeSrc: "http://" + qrImageObj.Location
})
}

JustinEgg.js阅读需 3 分钟

一、跨域配置

egg.js中实现跨域主要是通过egg-cors这个插件,更多信息可以通过npm官网去查看这个插件的用法。

  1. 安装插件
cnpm i egg-cors --save
  1. 在plugin.js中配置
  cors: {
enable: true,
package: 'egg-cors'
}
  1. 在config.default.js中配置
config.cors = {
origin: '*',
allowMethods: 'GET,PUT,POST,DELETE'
}

config.security = {
csrf: {
ignore: ctx => {
if (ctx.request.url === `/${config.adminPath}/product/doUpload`) {
return true;
} else {
return false;
}
}
},
domainWhiteList: ['http://localhost:8081']
}

二、设置前端API路由POST数据无需进行CSRF验证

只需在config.default.js中的csrf属性配置中进行配置即可。

  config.security = {
csrf: {
ignore: ctx => {
if (ctx.request.url === `/${config.adminPath}/product/doUpload`) {
return true;
} else if (ctx.request.url.indexOf("/api") != -1) {
return true
} else {
return false;
}
}
},
domainWhiteList: ['http://localhost:8081']
}

三、获取数据库中指定字段的数据

主要是通过attributes这个字段来进行获取。

let result = await this.ctx.model.ProductCate.findAll({
include: {
model: this.ctx.model.Product,
attributes: ['id','cid','title','price','imgUrl','sort']
}
});

如果想要将获取到的数据按照某种顺序进行排列,可以通过order属性进行配置。

let result = await this.ctx.model.ProductCate.findAll({
include: {
model: this.ctx.model.Product,
attributes: ['id','cid','title','price','imgUrl','sort']
},
order: [
['sort','DESC'],
[this.ctx.model.Product,'sort','DESC']
]
});

JustinEgg.js阅读需 1 分钟

实现点击页面上的符号即修改数据库中的数据并进行显示

静态页面设置

<td class="text-center chStatus" data-adminPath="<%=adminPath%>" data-id="<%=list[i].id%>" data-model="Product" data-field="status">
<%if(list[i].status == 1){%>
<img src="/public/admin/images/yes.gif" alt="">
<%}else{%>
<img src="/public/admin/images/no.gif" alt="">
<%}%>
</td>

通过Jquery进行逻辑控制

  1. 首先获取静态页面的数据。
  2. 然后将数据通过ajax请求发送到指定的路由。
  3. 根据控制器返回的数据,进一步控制页面的显示。
$(function () {
app.init();
})

var app = {
init: function () {
this.changeStatus();
},
changeStatus: function() {
$(".chStatus").click(function() {
var adminPath = $(this).attr("data-adminPath");
var id = $(this).attr("data-id");
var model = $(this).attr("data-model");
var field = $(this).attr("data-field");
var el = $(this).find("img");
$.get("/" + adminPath + "/changeStatus",{"adminPath": adminPath,"id" :id,"model": model,"field": field},function (response) {
if (response.success) {
if (el.attr("src").indexOf("yes") != -1) {
el.attr("src","/public/admin/images/no.gif");
} else {
el.attr("src","/public/admin/images/yes.gif");
}
}
})
})
}
}

控制器动态控制数据的值

控制器主要是获取传过来的数据,然后根据model和field进行动态的显示,这是一种复用的方法,指的我们学习和借鉴。

async changeStatus() {
let model = this.ctx.request.query.model;
let field = this.ctx.request.query.field;
let id = this.ctx.request.query.id;

let modelObj = await this.ctx.model[model].findByPk(id);
let json = {};
if (!modelObj) {
this.ctx.body = {"success": false,"msg": "参数错误"};
return;
} else {
if (modelObj[field] == 1) {
json = {
[field]: 0
}
} else {
json={
[field]: 1
}
}
}
await modelObj.update(json);
this.ctx.body = {
"success": true,"msg": "更新数据成功"
}
}

分页功能的实现

后端通过egg-sequelize实现

下面的代码中重点通过limit和offset实现,offset设置为(page - 1) * pageSize是一个分页公式。

async index() {
let page = this.ctx.request.query.page ? this.ctx.request.query.page : 1;
let pageSize = 5;
let result = await this.ctx.model.Product.findAll({
include: { model: this.ctx.model.ProductCate },
limit: pageSize,
offset: (page - 1) * pageSize
});
await this.ctx.render('admin/product/index', {
list: result
})
}

前端通过引入jqPaginator.js实现

  1. 引入文件
<script type="text/javascript" src="/public/admin/js/jqPaginator.js"></script>
  1. 定义容器
<div id="pagination" class="pagination" style="display: flex; justify-content: center" >

</div>
  1. 定义跳转逻辑

注意:此时要注意避免出现死循环的情况。

<script>
$('#pagination').jqPaginator({
totalPages: <%=totalPages%>,
visiblePages: 5,
currentPage: <%=page%>,
onPageChange: function(num,type) {
if (type == "change") {
location.href="/<%=adminPath%>/product?page=" + num;
}
}
});
</script>

JustinEgg.js阅读需 2 分钟

通过wysiwyg-editor实现

wysiwyg-editor的主要参考文档包括下面两个:

方式一:通过CDN引入

在需要使用富文本编辑器的页面,通过下面的cdn进行引入。

<link href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css" rel="stylesheet" type="text/css" />

<textarea></textarea>

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js"></script>

<script>
new FroalaEditor('textarea');
</script>

给富文本编辑器指定高度

  <script>
new FroalaEditor('#content',{
height: 200
});
</script>

给富文本编辑器指定语言。

<link href="/public/wysiwyg/css/froala_editor.pkgd.min.css" rel="stylesheet" type="text/css" /> 
<script type="text/javascript" src="/public/wysiwyg/js/froala_editor.pkgd.min.js"></script>
<script type="text/javascript" src="/public/wysiwyg/js/languages/zh_cn.js"></script>
<script>
new FroalaEditor('#content',{
height: 200,
language: "zh_cn"
});
</script>

去掉富文本编辑器的版权。

  <script>
new FroalaEditor('#content',{
height: 200,
language: "zh_cn",
attribution: false
});
</script>

方式二:通过Github将源码下载下来后手动引入。

富文本编辑器实现图片的上传

Egg.js中配置针对一些地址关闭CSRF安全验证

只需在config.default.js中进行下面的配置即可,返回true的地址是不会进行CSRF校验的。

  config.security = {
csrf: {
ignore: ctx => {
if (ctx.request.url === `/${config.adminPath}/product/doUpload`) {
return true;
} else {
return false;
}
}
}
}

实现上传到富文本编辑器即上传到COS上

  1. 在静态文件上添加imageUploadURL属性
    new FroalaEditor('#content',{
height: 200,
language: "zh_cn",
attribution: false,
imageUploadURL: `/<%=adminPath%>/product/doUpload`
});
  1. 关闭CSRF验证。

  2. 控制器中实现如下逻辑,需要注意的是需要给编辑器返回指定类型的json对象。

async doUpload() {

const { ctx } = this;
const body = ctx.request.body;
const file = ctx.request.files[0];
if (file) {
var source = fs.createReadStream(file.filepath);
var filename = this.ctx.service.tools.getCosUploadFile(file.filename);

await this.ctx.service.tools.uploadCos(filename,source);
}

ctx.body = {link: this.config.cosUrl + "/" + filename};

}

总结

通过上文介绍的插件来实现富文本编辑器,可以让我们站在巨人的肩膀上继续前进,本文只是给出了基本的使用方法,更加详细的配置文件和配置信息请查看上文的两个在线地址。


JustinEgg.js阅读需 2 分钟

上传到COS上的参考文档可以参考官方的文档:https://cloud.tencent.com/document/product/436/8629#.E4.B8.8A.E4.BC.A0.E5.AF.B9.E8.B1.A1

上传图片到腾讯云的COS上

  1. 控制器中写法
async doAdd() {

const { ctx } = this;
const body = ctx.request.body;
const file = ctx.request.files[0];
const source = fs.createReadStream(file.filepath);
if (file) {
await this.ctx.service.tools.uploadCos(file.filename,source);
}
ctx.body = {
body, file
}
}

  1. 在service中定义好要上传的函数
async uploadCos(filename, body) {
var cos = new COS({
SecretId: 'xxx',
SecretKey: 'xxx'
});
return new Promise((resolve,reject) => {
cos.putObject({
Bucket: 'eggshop-1301559367', /* 必须 */
Region: 'ap-beijing', /* 必须 */
Key: filename, /* 必须 */
StorageClass: 'STANDARD',
Body: body, // 上传文件对象
onProgress: function (progressData) {
console.log(JSON.stringify(progressData));
}
}, function (err, data) {
// console.log(err || data);
if (!err) {
resolve(data);
} else {
reject(err);
}
});
})
}

上传文件根据时间进行文件夹分类

  1. 在service中定义好上传到COS上的文件路径
  // 上传到COS上的路径格式
getCosUploadFile(filename) {
// 获取当前的日期
let dir = sd.format(new Date(), 'YYYYMMDD');
// 生成文件名称
let unix = this.getUnixTime();
let saveDir = dir + "/" + unix + path.extname(filename)
return saveDir;
}
  1. 在service中定义好上传到COS上的函数
  async uploadCos(filename, body) {
var cos = new COS({
SecretId: 'xxx',
SecretKey: 'xxx'
});
return new Promise((resolve,reject) => {
cos.putObject({
Bucket: 'eggshop-1301559367', /* 必须 */
Region: 'ap-beijing', /* 必须 */
Key: filename, /* 必须 */
StorageClass: 'STANDARD',
Body: body, // 上传文件对象
onProgress: function (progressData) {
console.log(JSON.stringify(progressData));
}
}, function (err, data) {
// console.log(err || data);
if (!err) {
resolve(data);
} else {
reject(err);
}
});
})
}
  1. 在控制器中的写法
  async doAdd() {

const { ctx } = this;
const body = ctx.request.body;
const file = ctx.request.files[0];
if (file) {
const source = fs.createReadStream(file.filepath);
let filename = this.ctx.service.tools.getCosUploadFile(file.filename);

await this.ctx.service.tools.uploadCos(filename,source);
}
ctx.body = {
body, file
}
}

注意:如果上传到COS上的图片路径中包含小数点,是因为获取时间戳的时间需要向上进行取整。

getUnixTime() {
let obj = new Date();
return Math.ceil(obj.getTime() / 1000);
}

将本地数据库上传到云上

  1. 将数据库的结构和数据存储到本地

image.png

  1. 建立一个云数据库并通过Navicat进行连接后,通过运行SQL文件导入我们第一步创建的SQL文件。

image.png

  1. 修改数据库配置
  config.sequelize = {
dialect: 'mysql',
host: 'rm-2zenx6363vhj8129ryo.mysql.rds.aliyuncs.com',
port: 3306,
username: "xxx",
password: "xxx",
database: 'eggshop',
};

JustinEgg.js阅读需 2 分钟

上传功能的实现

  1. 将上传表单的类型置为file.

注意:name属性不可缺少。

<li>菜品图片:<input type="file" name="picUrl" /></li>
  1. 配置文件上传的模式
  // 配置文件上传的模式
config.multipart = {
mode: 'file'
};
  1. 配置csrf属性

enctype属性不可缺少。

 <form action="/<%=adminPath%>/product/doAdd?_csrf=<%=csrf%>" method="post" enctype="multipart/form-data">
  1. 将路由设置为post
router.post(`/${config.adminPath}/product/doAdd`,controller.admin.product.doAdd);
  1. 控制器中读取file
  async doAdd() {
const {ctx} = this;
const body = ctx.request.body;
const file = ctx.request.files[0];
ctx.body = {
body: body,
file: file
}
}

将上传的文件保存在指定的位置

  1. 安装工具包
npm i mz mz-modules --save
  1. 在控制器中引入相关工具包
const path = require('path');
const fs = require('mz/fs');
const pump = require('mz-modules/pump');
  1. 创建上传到哪个文件夹,这个文件夹要提前创建好(public/upload)

image.png

  1. 控制器读写逻辑的实现
async doAdd() {
const {ctx} = this;
const body = ctx.request.body;
const file = ctx.request.files[0];
// 获取文件名称
const filename = file.filename;
const targetPath = path.join('app/public/upload',filename);
// 读取文件
const source = fs.createReadStream(file.filepath);
// 创建写入流
const target = fs.createWriteStream(targetPath);
try {
await pump(source,target);
} finally {
await ctx.cleanupRequestFiles();
}
ctx.body = "写入成功"
}

实现多文件上传

  1. 静态页面设置如下所示:

image.png

  1. 控制器的核心实现逻辑
  // 实现多文件上传
async doAdd() {
const { ctx } = this;
const body = ctx.request.body;
const files = ctx.request.files;
try {
for (let file of files) {
const filename = file.filename;
const targetPath = path.join('app/public/upload', filename);
// 读取文件
const source = fs.createReadStream(file.filepath);
// 创建写入流
const target = fs.createWriteStream(targetPath);
await pump(source,target);
}
} finally {
await ctx.cleanupRequestFiles();
}
ctx.body = {
body, files
}
}

以日期文件夹进行分类上传图片

下面是实现的效果:

image.png

  1. 在service中引入相关工具包
const sd = require('silly-datetime');
const path = require('path');
const mkdirp = require('mz-modules/mkdirp');
  1. 在配置文件中定义上传的路径
  // 配置上传图片时的拼接路径
config.uploadDir = "app/public/upload";
  1. 在service中定义创建路径和文件夹的函数
  async getUploadFile(filename) {
// 获取当前的日期
let day = sd.format(new Date(),'YYYYMMDD');
// 创建图片的保存路径
let dir = path.join(this.config.uploadDir,day);
await mkdirp(dir);

// 生成文件名称
let unix = this.getUnixTime();
let saveDir = path.join(dir,unix + path.extname(filename));
return saveDir;
}
  1. 在控制器中异步调用文件路径
const targetPath = await this.ctx.service.tools.getUploadFile(filename);

下面附录下全部的控制器代码

async doAdd() {
const { ctx } = this;
const body = ctx.request.body;
const files = ctx.request.files;
try {
for (let file of files) {
const filename = file.filename;
// const targetPath = path.join('app/public/upload', filename);
const targetPath = await this.ctx.service.tools.getUploadFile(filename);
// 读取文件
const source = fs.createReadStream(file.filepath);
// 创建写入流
const target = fs.createWriteStream(targetPath);
await pump(source,target);
}
} finally {
await ctx.cleanupRequestFiles();
}
ctx.body = {
body, files
}
}

JustinEgg.js阅读需 3 分钟

什么是RBAC?

RBAC是基于角色的权限访问控制,在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限,也就是说权限是和角色绑定在一起的。

RBAC权限管理树形图

image.png

角色管理

增加角色

  • 静态页面中通过post进行提交的时候,需要配置csrf
<input type="hidden" name="_csrf" value="<%=csrf%>" />
  • 在增加角色的控制器中首先获取到请求的角色名,如果角色名为空,则渲染基类控制器中的错误提示,如果不为空,则通过sequelize中的model来操作数据库,并将指定内容添加到数据库中。
  async doAdd() {
const title = this.ctx.request.body.title;
if (title != '') {
await this.ctx.model.Role.create({
title,
description: this.ctx.request.body.description,
status: 1,
addTime: this.service.tools.getUnixTime()
})
await this.success("增加角色成功", `/${this.config.adminPath}/role`)
} else {
await this.error("角色名不能为空", `/${this.config.adminPath}/role/add`)
}
}

编辑角色

跳转到编辑页面

为了防止前端请求到错误的id,需要通过异常处理的方式,让错误的请求跳转到基类控制器中的错误提示页面,如果请求正常,则查询请求id,然后渲染到编辑的页面。

  // 跳转编辑页面
async edit() {
try {
const id = this.ctx.request.query.id;
let result = await this.ctx.model.Role.findAll({
where: {
id
}
})
console.log(result);
await this.ctx.render('admin/role/edit', {
list: result[0]
});
} catch (error) {
await this.error("非法请求", `/${this.config.adminPath}/role`)
}
}

执行编辑功能

首先获取到post请求的id,然后根据这个id到数据库中去查询,然后判断是否查询到,如果没有查询到则报错,查询到了则更新数据。

  // 执行编辑功能
async doEdit() {
let id = this.ctx.request.body.id;
let role = await this.ctx.model.Role.findByPk(id);
if (!role) {
await this.error("非法请求", `/${this.config.adminPath}/role/edit?id=${id}`)
return
}
await role.update(this.ctx.request.body);
await this.success("修改数据成功", `/${this.config.adminPath}/role`);
this.ctx.body = "修改已被执行"
}

注意:在编辑角色的时候,静态页面传递id的时候可以通过隐藏表单的形式来进行传递。

<input type="hidden" name="id" value="<%=list.id%>">

删除角色

首先获取到要删除的id,然后根据主键去查询这个角色,如果没有查到则报错,查到的话则删除。

  // 删除角色功能的实现
async delete() {
let id = this.ctx.request.query.id;
let role = await this.ctx.model.Role.findByPk(id);
if (!role) {
await this.error("非法请求", `/${this.config.adminPath}/role`);
return;
}
await role.destroy();
await this.success("删除数据成功", `/${this.config.adminPath}/role`);
}

管理员数据表与角色表进行关联

首先,我们要明确管理员数据表和角色表是通过哪一个字段进行关联的,是通过角色id进行关联的,所以,我们首先在admin的model中通过belongsTo进行关联。

  • model下的admin.js
  Admin.associate = function() {
app.model.Admin.belongsTo(app.model.Role,{foreignKey: 'roleId'})
}
  • 在控制器中进行关联查询的方式
let result = await this.ctx.model.Admin.findAll({
include: {model: this.ctx.model.Role}
});

权限管理

权限表的自关联

之所以要进行自关联是因为,一个菜单或者模块如果属于一个顶级模块的话,顶级模块的id和其子项的module_id是一致的,这一点可以从下面的数据表中可以看出。

image.png

在access.js中实现下面的功能。

  // 进行数据表的自关联
Access.associate = function() {
app.model.Access.hasMany(app.model.Access,{foreignKey: 'moduleId'});
}

修改权限

  async edit() {
// 修改权限
let id = this.ctx.request.query.id;
// console.log(id);
let accessResult = await this.ctx.model.Access.findAll({
where: {
id
}
});
// console.log(accessResult[0]);
// 获取顶级模块
let accessList = await this.ctx.model.Access.findAll({
where: {moduleId: 0}
});

await this.ctx.render("admin/access/edit",{
access: accessResult[0],
accessList
})
}

角色与权限进行关联

角色与权限进行关联主要是通过一个中间数据表来进行关联,下面是这个数据表的结构。

image.png

进入角色授权界面,显示该角色已经拥有的权限

进入显示授权页面的控制器。

  1. 获取要授权的角色ID。
  2. 获得所有的权限列表。
  3. 定义一个临时数组并找到第一步角色id对应的权限,并将其权限id添加到临时数组中。
  4. 将所有权限数组通过转换为字符串后再转换为JSON,然后通过两层循环将其添加标记后再进行渲染。
  // 授权
async auth() {
// 获取要给哪个id的角色进行授权
let roleId = this.ctx.request.query.id;
let allAuthResult = await this.ctx.model.Access.findAll({
where: {moduleId: 0},
include: {model: this.ctx.model.Access}
});
let tempArr = [];
let roleAuthResult = await this.ctx.model.RoleAccess.findAll({where: {roleId}});

for (let v of roleAuthResult) {
tempArr.push(v.accessId);
}

allAuthResult = JSON.parse(JSON.stringify(allAuthResult));

for (let i = 0; i < allAuthResult.length; i++) {
if (tempArr.indexOf(allAuthResult[i].id) != -1) {
allAuthResult[i].checked = true;
}
for (let j = 0; j < allAuthResult[i].accesses.length; j++) {
if (tempArr.indexOf(allAuthResult[i].accesses[j].id) != -1) {
allAuthResult[i].accesses[j].checked = true;
}
}
}

// this.ctx.body = allAuthResult;

await this.ctx.render('admin/role/auth',{
authList: allAuthResult,
roleId
});
}

用户权限判断

判断当前登录用户的权限,防止用户访问没有授权的页面。

  1. 在service中定义函数来判断用户请求的URL是否有权限访问。
  2. 定义一个可以忽略的URL数组,在这个数组中的请求都是直接允许所有用户访问的,比如退出登录,如果是超级管理员或者请求URL在上述的数组中,直接返回true。
  3. 获取角色id对应的所有权限,然后去权限表中查询当前请求URL对应的id,如果在上述的数组中,则返回true,反之返回true。
class AdminService extends Service {
async checkAuth() {
let roleId = this.ctx.session.userinfo.roleId;
let isSuper = this.ctx.session.userinfo.isSuper;
let adminPath = this.config.adminPath;
let pathname = this.ctx.request.url;
pathname = pathname.split("?")[0];

// 忽略权限判断的地址

if (this.config.ignoreUrl.indexOf(pathname) != -1 || isSuper === 1) {
return true;
}
let roleAccessArr = [];
let roleAuthResult = await this.ctx.model.RoleAccess.findAll({
where: {roleId}
});
for (let i = 0; i < roleAuthResult.length; i++) {
roleAccessArr.push(roleAuthResult[i].accessId);
}

// 获取当前访问的URL,对应的权限ID
let accessUrl = pathname.replace(`/${adminPath}/`,'');
let accessUrlResult = await this.ctx.model.Access.findAll({
where: {url: accessUrl}
});
if (accessUrlResult.length) {
if (roleAccessArr.indexOf(accessUrlResult[0].id) != -1) {
return true;
}
return false;
}
return false;

}
}

JustinEgg.js阅读需 5 分钟