eggjs 项目实践

架构: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',
  }
};

猜你喜欢

转载自blog.csdn.net/bbsyi/article/details/121245064