架构:eggjs + egg-jwt + egg-sequelize + egg-validate
数据库:mysql
实现功能:登录、验证码、权限菜单、鉴权、角色、用户、上传、下载、错误统一处理
api格式: restful + json
项目目录
app->controller->base_controller.js
const { Controller } = require('egg');
class BaseController extends Controller {
// get user() {
// return this.ctx.session.user;
// }
success(msg = '', status = 200, data = {}) {
const { ctx } = this;
ctx.body = {
msg,
status,
data
};
}
error(msg = '', status = 500, data = {}) {
const { ctx } = this;
ctx.body = {
msg,
status,
data
};
}
// notFound(msg) {
// msg = msg || 'not found';
// this.ctx.throw(404, msg);
// }
}
module.exports = BaseController;
app->controller->home.js
'use strict';
const Controller = require('./base_controller');
class HomeController extends Controller {
async index() {
this.success('hi, egg!', 200);
}
}
module.exports = HomeController;
app->controller->menu.js
'use strict';
const Controller = require('./base_controller');
class menuController extends Controller {
async index() {
const { ctx } = this;
ctx.validate({ isMenu: 'boolean' });
const { isMenu } = ctx.request.body;
this.success('查询成功', 200, {
list: await ctx.service.menu.getMenu(isMenu)
});
}
async update() {
const { ctx } = this;
ctx.validate({ name: 'string', pid: 'number' });
ctx.validate({ id: 'string' }, ctx.params);
const { name, pid } = ctx.request.body;
await ctx.service.menu.editMenu(ctx.params.id, name, pid);
if (ctx.status === 400) {
this.error('编辑失败', 400);
} else {
this.success('编辑成功', 200);
}
}
}
module.exports = menuController;
app->controller->role.js
'use strict';
const Controller = require('./base_controller');
class RoleController extends Controller {
async index() {
const { ctx } = this;
this.success('查询成功', 200, {
list: await ctx.service.role.getRole()
});
}
async show() {
const { ctx } = this;
ctx.validate({ id: 'string' }, ctx.params);
this.success('查询成功', 200, {
list: await ctx.service.role.getRole(ctx.params.id)
});
}
// 插入角色
async create() {
const { ctx } = this;
ctx.validate({ name: 'string', pid: 'number'});
const { name, pid } = ctx.request.body;
await ctx.service.role.addRole(name, pid);
this.success('新增成功', 200);
}
// 更新角色
async update() {
const { ctx } = this;
ctx.validate({ name: 'string' });
ctx.validate({ pid: 'string' }, ctx.params);
const { name, pid } = ctx.request.body;
await ctx.service.role.editRole(ctx.params.id, name, pid);
if (ctx.status === 400) {
this.error('修改失败', 400);
} else {
this.success('修改成功', 200);
}
}
// 移除角色
async remove() {
const { ctx } = this;
ctx.validate({ id: 'string' }, ctx.params);
await ctx.service.role.removeRole(ctx.params.id);
if (ctx.status === 400) {
this.error('删除失败', 400);
} else {
this.success('删除成功', 200);
}
}
}
module.exports = RoleController;
app->controller->upload.js
const Controller = require('./base_controller');
const path = require('path');
const fs = require("fs");
const dayjs = require('dayjs');
function mkdirsSync(dirname) {
if (fs.existsSync(dirname)) {
return true;
} else {
if (mkdirsSync(path.dirname(dirname))) {
fs.mkdirSync(dirname);
return true;
}
}
}
module.exports = class extends Controller {
// 上传单个文件
async uploadOne() {
const { ctx } = this;
const file = ctx.request.files[0]
console.log('-----------获取数据 start--------------');
console.log(file);
console.log('-----------获取数据 end--------------');
// 基础的目录
const uplaodBasePath = '../public/uploads';
// 生成文件名
const filename = `${Date.now()}${Number.parseInt(
Math.random() * 1000,
)}${path.extname(file.filename).toLocaleLowerCase()}`;
// 生成文件夹
const dirname = dayjs(Date.now()).format('YYYY/MM/DD');
mkdirsSync(path.join(uplaodBasePath, dirname));
// 生成写入路径
const url = `/public/uploads/${dirname}/${filename}`
try {
//异步把文件流 写入
fs.writeFileSync(`app/${url}`, fs.readFileSync(file.filepath));
} catch (err) {
console.error(err)
}
await ctx.service.upload.addUploadFile(filename, file.filename, url);
this.success('上传成功', 200, {
name: filename,
org_name: file.filename,
url
})
}
// 上传多个文件
async uploadMulti() {
const { ctx } = this;
const files = ctx.request.files
console.log('-----------获取数据 start--------------');
console.log(files);
console.log('-----------获取数据 end--------------');
// 基础的目录
const uplaodBasePath = '../public/uploads';
let uplist = [];
for (let i = 0; i < files.length; i++) {
let element = files[i];
// 生成文件名
const filename = `${Date.now()}${Number.parseInt(
Math.random() * 1000,
)}${path.extname(element.filename).toLocaleLowerCase()}`;
// 生成文件夹
const dirname = dayjs(Date.now()).format('YYYY/MM/DD');
mkdirsSync(path.join(uplaodBasePath, dirname));
// 生成写入路径
const url = `/public/uploads/${dirname}/${filename}`
try {
//异步把文件流 写入
fs.writeFileSync(`app/${url}`, fs.readFileSync(element.filepath));
} catch (err) {
console.error(err)
}
uplist.push({
name: filename,
org_name: element.filename,
url
})
await ctx.service.upload.addUploadFile(filename, element.filename, url);
}
this.success('上传成功', 200, {
list: uplist
})
}
// 通过id获取文件信息
async show() {
const { ctx } = this;
ctx.validate({ id: 'string'}, ctx.params)
const data = await ctx.service.upload.getUpload(ctx.params.id);
this.success('查询成功', 200, data);
}
};
app->controller->user.js
'use strict';
const Controller = require('./base_controller');
const svgCaptcha = require('svg-captcha');
class UserController extends Controller {
// 登录
async login() {
const { ctx, app } = this;
const data = ctx.request.body;
ctx.validate({ captcha: 'string', username: 'string', password: 'string'})
// 判断验证码是否可用
if(ctx.session.captcha === undefined){
this.error('验证码失效,请重新获取', 400)
return;
}
if(ctx.session.captcha!==data.captcha){
this.error('请正确输入验证码', 400)
return;
}
// 判断该用户是否存在并且密码是否正确
const getUser = await ctx.service.user.validUser(data.username, data.password);
if (getUser) {
const token = app.jwt.sign({ username: data.username }, app.config.jwt.secret);
this.success('登录成功', 200, {
token,
roleid: getUser.roleid
});
} else {
this.error('用户名或密码错误', 400)
}
}
// 获取验证码
async captcha() {
const { ctx } = this;
const captcha = svgCaptcha.create({
size: 4, //图片验证码的字符数
fontSize: 50,
ignoreChars: 'Ooli', //忽略的一些字符
width: 100,
height: 40,
noise: 2,
color: true,
background: '#ffffff',
});
ctx.session.maxAge = 60 * 1000;
ctx.session.captcha = captcha.text; //text及data都是函数返回的string属性的对象 将图片验证码中的text传到session里边
ctx.response.type = 'image/svg+xml'; //返回的类型
this.success('查询成功', 200, {
svg: captcha.data,
captcha: captcha.text
});
}
// 获取所有用户
async index() {
const { ctx } = this;
const data = await ctx.service.user.getUser();
this.success('查询成功', 200, data);
}
// 通过id获取用户
async show() {
const { ctx } = this;
ctx.validate({ id: 'string'}, ctx.params)
const data = await ctx.service.user.getUser(ctx.params.id);
this.success('查询成功', 200, data);
}
// 创建用户
async create() {
const { ctx } = this;
ctx.validate({ username: 'string', roleid: 'number' });
const { username, roleid } = ctx.request.body;
await ctx.service.user.addUser(username, roleid);
this.success('新增成功', 200);
}
// 修改密码
async updatePwd() {
const { ctx } = this;
ctx.validate({ password: 'string' });
ctx.validate({ password: 'string' }, ctx.params);
const { password } = ctx.request.body;
await ctx.service.user.editPwd(ctx.params.id, password);
this.success('修改成功', 200);
}
// 明文密码得到加密密码
async getMd5Data() {
const { ctx } = this;
ctx.validate({ data: 'string' }, ctx.params);
this.success('查询成功', 200, {
password: await ctx.service.user.getMd5Data(ctx.params.data)
});
}
}
module.exports = UserController;
app->middleware->error_handler.js
'use strict';
module.exports = () => {
return async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
// 所有的异常都会在app上出发一个error事件,框架会记录一条错误日志
ctx.app.emit('error', err, ctx);
const status = err.status || 500;
// 如果时生产环境的时候 500错误的详细错误内容不返回给客户端
const error = status === 500 && ctx.app.config.env === 'prod' ? '网络错误' : err.message;
ctx.body = {
msg: error,
status: status,
data: {},
};
}
};
};
app->middleware->notfound_handler.js
module.exports = () => {
return async function notFoundHandler(ctx, next) {
await next();
if (ctx.status === 404 && !ctx.body) {
ctx.body = {
msg: 'Not Found',
status: ctx.status,
data: {},
};
}
};
};
app->mode->menu.js
'use strict';
module.exports = app => {
const { INTEGER, STRING } = app.Sequelize;
const Menu = app.model.define('menu', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(50),
pid: INTEGER,
}, {
timestamps: false,
});
return Menu;
};
app->mode->role.js
'use strict';
module.exports = app => {
const { INTEGER, STRING } = app.Sequelize;
const Role = app.model.define('role', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(50),
pid: INTEGER,
}, {
timestamps: false,
});
return Role;
};
app->mode->upload.js
'use strict';
module.exports = app => {
const { STRING, INTEGER } = app.Sequelize;
const Upload = app.model.define('upload', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(50),
org_name: STRING(50),
path: STRING(100)
}, {
timestamps: false,
});
return Upload;
};
app->mode->user.js
'use strict';
module.exports = app => {
const { STRING, INTEGER } = app.Sequelize;
const User = app.model.define('user', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
username: STRING(20),
password: STRING(50),
roleid: INTEGER,
}, {
timestamps: false,
});
User.associate = function() {
app.model.User.belongsTo(app.model.Role, { foreignKey: 'roleid', targetKey: 'id', as: 'role' });
};
return User;
};
app->service->menu.js
'use strict';
const Service = require('egg').Service;
function toInt(str) {
if (typeof str === 'number') return str;
if (!str) return str;
return parseInt(str, 10) || 0;
}
class MenuService extends Service {
// 构建菜单权限树
// 如果id为空,则构建所有的数据
// id不为空,则构建以id为根结点的树
buildTree(id, data, isMenu) {
const res = [];
if (id) {
for (const item of data) {
if (toInt(item.id) === toInt(id)) {
item.children = getNode(id);
res.push(item);
}
}
} else {
for (const item of data) {
if (!item.pid) {
item.children = getNode(item.id);
res.push(item);
}
}
}
// 传入根结点id 递归查找所有子节点
function getNode(id) {
const node = [];
for (const item of data) {
if (toInt(item.pid) === toInt(id) && (isMenu === true ? item.children : true)) {
item.children = getNode(item.id);
node.push(item);
}
}
if (node.length === 0) return;
return node;
}
return res;
}
// 获取所有子节点集合
getChildrenIds(treeData) {
const res = [];
function getIds(treeData, res) {
for (const item of treeData) {
res.push(item.id);
if (item.children) { getIds(item.children, res); }
}
}
getIds(treeData, res);
return res;
}
// 查询角色并构建菜单树
async getMenu(isMenu) {
const { ctx } = this;
const query = { limit: toInt(ctx.query.limit), offset: toInt(ctx.query.offset) };
const data = await ctx.model.Menu.findAll({ query, raw: true });
return this.buildTree(null, data, isMenu);
}
// 根据id查询角色
async getMenuById(id) {
const { ctx } = this;
return await ctx.model.Menu.findByPk(toInt(id));
}
// 编辑菜单
async editMenu(id, name, pid) {
const { ctx } = this;
const menu = await this.getMenuById(toInt(id));
if (!menu) {
ctx.status = 404;
return;
}
await menu.update({ name: name || menu.name, pid: pid || menu.pid });
ctx.status = 200;
}
}
module.exports = MenuService;
app->service->role.js
'use strict';
const Service = require('egg').Service;
function toInt(str) {
if (typeof str === 'number') return str;
if (!str) return str;
return parseInt(str, 10) || 0;
}
class RoleService extends Service {
// 构建树形结构数据
// 如果id为空,则构建所有的数据
// id不为空,则构建以id为根结点的树
buildTree(id, data) {
const res = [];
if (id) {
for (const item of data) {
if (toInt(item.id) === toInt(id)) {
item.children = getNode(id);
res.push(item);
}
}
} else {
for (const item of data) {
if (!item.pid) {
item.children = getNode(item.id);
res.push(item);
}
}
}
// 传入根结点id 递归查找所有子节点
function getNode(id) {
const node = [];
for (const item of data) {
if (toInt(item.pid) === toInt(id)) {
item.children = getNode(item.id);
node.push(item);
}
}
if (node.length === 0) return;
return node;
}
return res;
}
// 获取所有子节点集合
getChildrenIds(treeData) {
const res = [];
function getIds(treeData, res) {
for (const item of treeData) {
res.push(item.id);
if (item.children) { getIds(item.children, res); }
}
}
getIds(treeData, res);
return res;
}
// 查询角色并构建角色树
async getRole(id) {
const { ctx } = this;
const query = { limit: toInt(ctx.query.limit), offset: toInt(ctx.query.offset) };
const data = await ctx.model.Role.findAll({ query, raw: true });
return this.buildTree(id, data);
}
// 根据id查询角色
async getRoleById(id) {
const { ctx } = this;
return await ctx.model.Role.findByPk(toInt(id));
}
// 插入角色
async addRole(name, pid) {
const { ctx } = this;
await ctx.model.Role.create({ name, pid });
}
// 修改角色
async editRole(id, name, pid) {
const { ctx } = this;
const role = await this.getRoleById(toInt(id));
if (!role) {
ctx.status = 404;
return;
}
await role.update({ name: name || role.name, pid: pid || role.pid });
ctx.status = 200;
}
// 删除角色
async removeRole(id) {
const { ctx } = this;
const roleTree = await this.getRole(toInt(id));
const role = await this.getRoleById(toInt(id));
if (!role) {
ctx.status = 404;
return;
}
const ids = this.getChildrenIds(roleTree);
for (const i of ids) {
const r = await this.getRoleById(toInt(i));
r.destroy();
}
ctx.status = 200;
}
}
module.exports = RoleService;
app->service->upload.js
'use strict';
const Service = require('egg').Service;
function toInt(str) {
if (typeof str === 'number') return str;
if (!str) return str;
return parseInt(str, 10) || 0;
}
class UploadService extends Service {
// 插入上传文件
async addUploadFile(name, org_name, path) {
const { ctx } = this;
await ctx.model.Upload.create({ name, org_name, path });
}
// 获取上传文件
async getUpload(id) {
const { ctx } = this;
return await ctx.model.Upload.findByPk(toInt(id));
}
}
module.exports = UploadService;
app->service->user.js
'use strict';
const Service = require('egg').Service;
const crypto = require('crypto');
// 设置默认密码
const DEFAULT_PWD = '123456';
function toInt(str) {
if (typeof str === 'number') return str;
if (!str) return str;
return parseInt(str, 10) || 0;
}
class UserService extends Service {
// 查询user表,验证密码和花名
async validUser(username, password) {
const data = await this.ctx.model.User.findAll();
const pwd = this.getMd5Data(password);
for (const item of data) {
if (item.username === username && item.password === pwd) return item;
}
return false;
}
// 获取用户,不传id则查询所有
async getUser(id) {
const { ctx } = this;
const query = { limit: toInt(ctx.query.limit), offset: toInt(ctx.query.offset) };
if (id) {
return await ctx.model.User.findByPk(toInt(id));
}
return await ctx.model.User.findAll({ query,
attributes: [ 'username' ],
include: [{
model: ctx.model.Role,
as: 'role',
attributes: [ 'name', 'pid' ],
}],
});
}
// 新增人员
async addUser(username, roleid) {
const { ctx } = this;
const password = this.getMd5Data(DEFAULT_PWD);
await ctx.model.User.create({ username, password, roleid });
}
// 修改密码
async editPwd(id, password) {
const { ctx } = this;
const user = await ctx.model.User.findByPk(id);
const newPwd = this.getMd5Data(password);
await user.update({ password: newPwd });
}
// 专门对数据进行md5加密的方法,输入明文返回密文
getMd5Data(data) {
return crypto.createHash('md5').update(data).digest('hex');
}
}
module.exports = UserService;
app->router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller, jwt } = app;
const preUrl = '/api'
router.get('/', controller.home.index);
router.post(`${preUrl}/user/login`, controller.user.login);
router.get(`${preUrl}/user/captcha`, controller.user.captcha);
// 查询
router.get(`${preUrl}/user`, controller.user.index);
router.get(`${preUrl}/user/:id`, jwt, controller.user.show);
// 生成经过md5加密后的密文
router.get(`${preUrl}/user/getMd5/:data`, controller.user.getMd5Data);
// 新增
router.post(`${preUrl}/user`, jwt, controller.user.create);
// 修改密码
router.put(`${preUrl}/user/:id`, jwt, controller.user.updatePwd);
// 获取角色
router.get(`${preUrl}/role`, controller.role.index);
router.get(`${preUrl}/role/:id`, controller.role.show);
// 插入角色
router.post(`${preUrl}/role`, jwt, controller.role.create);
// 修改角色
router.put(`${preUrl}/role/:id`, jwt, controller.role.update);
// 删除角色
router.delete(`${preUrl}/role/:id`, jwt, controller.role.remove);
// 获取菜单
router.get(`${preUrl}/menu`, controller.menu.index);
// 编辑菜单
router.put(`${preUrl}/menu/:id`, jwt, controller.menu.update);
// 上传一个文件
router.post(`${preUrl}/upload`, controller.upload.uploadOne);
// 上传多个文件
router.post(`${preUrl}/upload/multi`, controller.upload.uploadMulti);
// 获取上传文件
router.get(`${preUrl}/upload/:id`, controller.upload.show);
};
app->config->config.default.js
/* eslint valid-jsdoc: "off" */
'use strict';
/**
* @param {Egg.EggAppInfo} appInfo app info
*/
module.exports = appInfo => {
/**
* built-in config
* @type {Egg.EggAppConfig}
**/
const config = exports = {};
// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1635393215721_8048';
// add your middleware config here
config.middleware = ['errorHandler', 'notfoundHandler'];
// add your user config here
const userConfig = {
// myAppName: 'egg',
};
config.jwt = {
secret: '123456',
};
// 安全配置 (https://eggjs.org/zh-cn/core/security.html)
config.security = {
csrf: {
enable: false,
ignoreJSON: true,
},
// 允许访问接口的白名单
domainWhiteList: [ 'http://localhost:8080' ],
};
// 跨域配置
config.cors = {
origin: '*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
};
config.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
port: '3306',
user: 'root',
password: '123456',
database: 'test',
define: {
underscored: true,
freezeTableName: true,
},
};
config.errorHandler = {
enable: true, // 中间件开启配置
match: '/api', // 设置请求中间件的请求路由,只对/api开头的有效
// ignore: '', // 设置不经过这个中间件的请求路由
};
config.notfoundHandler = {
enable: true, // 中间件开启配置
match: '/api', // 设置请求中间件的请求路由,只对/api开头的有效
// ignore: '', // 设置不经过这个中间件的请求路由
};
config.captcha = {
enable: true,
package: 'svg-captcha'
}
config.validate = {
// convert: false,
// validateRoot: false,
};
config.multipart = {
fileSize: '50mb',
mode: 'file',
fileExtensions: ['.txt'],
};
return {
...config,
...userConfig,
};
};
app->config->plugin.js
'use strict';
/** @type Egg.EggPlugin */
module.exports = {
// had enabled by egg
// static: {
// enable: true,
// }
jwt: {
enable: true,
package: 'egg-jwt',
},
cors: {
enable: true,
package: 'egg-cors',
},
sequelize: {
enable: true,
package: 'egg-sequelize',
},
validate: {
enable: true,
package: 'egg-validate',
}
};