侧边栏壁纸
博主头像
tutuの博客博主等级

day day up !

  • 累计撰写 17 篇文章
  • 累计创建 8 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Api-disk开发

tutu
2022-08-11 / 0 评论 / 0 点赞 / 9 阅读 / 46943 字

创建项目&&环境

更改镜像源
  • sudo npm config set registry https://registry.npm.taobao.org

脚手架生成项目
  1. mkdir api_disk && cd api_disk

  2. npm init egg&&npm i

配置eslint
  1. 安装并初始化npm init @eslint/config

  2. 将.eslintrc.js后缀名改为.eslintrc

  3. 修改拓展"extends": "eslint-config-egg",

  4. 添加规则(忽略数组里空格问题)"array-bracket-spacing": "off"

  5. 修改pakage.json

    {
      "name": "api_disk",
      "version": "1.0.0",
      "description": "API for onlineDisk",
      "private": true,
      "egg": {
        "declarations": true
      },
      "dependencies": {
        "egg": "^3",
        "egg-scripts": "^2",
        "husky": "^8.0.3"
      },
      "devDependencies": {
        "@commitlint/cli": "^17.4.4",
        "@commitlint/config-conventional": "^17.4.4",
        "egg-bin": "^5",
        "egg-ci": "^2",
        "egg-mock": "^5",
        "eslint": "^8",
        "eslint-config-egg": "^12"
      },
      "engines": {
        "node": ">=16.0.0"
      },
      "scripts": {
        "start": "egg-scripts start --daemon --title=egg-server-diskAPI",
        "stop": "egg-scripts stop --title=egg-server-diskAPI",
        "dev": "egg-bin dev",
        "debug": "egg-bin debug",
        "test": "npm run lint -- --fix && npm run test-local",
        "test-local": "egg-bin test",
        "cov": "egg-bin cov",
        "lint": "eslint . --fix",
        "ci": "npm run lint && npm run cov"
      },
      "ci": {
        "version": "16, 18",
        "type": "github"
      },
      "repository": {
        "type": "git",
        "url": ""
      },
      "author": "tutu",
      "license": "MIT",
      "lint-staged": {
        "src/**/*.{js,jsx,ts,tsx,vue,less,sass,scss,css.json}": [
          "eslint --fix",
          "git add"
        ]
      }
    }
husky和commitlint配置
  1. git init

  2. npm install husky

  3. npx husky install

  4. npm install -D @commitlint/config-conventional @commitlint/cli

  5. 创建commitlint.config.js文件

    module.exports = { extends: ['@commitlint/config-conventional'], };
  6. npx husky add .husky/pre-commit "npm test"

  7. npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'

  8. 修改./husky/pre-commit文件,把npm test修改为npx lint-staged

    一定要记得给与权限chmod +x .husky/commit-msg .husky/pre-commit

配置git
  1. git remote add origin https://github.com/W433567423/onlineDiskApi.git

  2. git add .

  3. git branch -m main

  4. git push -u main

    • 提交规范

    • build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交

    • ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交

    • docs:文档更新

    • feat:新增功能

    • fix:bug 修复

    • perf:性能优化

    • refactor:重构代码(既没有新增功能,也没有修复 bug)

    • style:不影响程序逻辑的代码修改(修改空白字符,补全缺失的分号等)

    • test:新增测试用例或是更新现有测试

    • revert:回滚某个更早之前的提交

    • chore:不属于以上类型的其他类型(日常事务

配置项目

配置跨域

  1. 安装插件npm install egg-cors -S

  2. 配置插件

    //app/config/plugin.js
    cors:{
      enable:true,
      package:'egg-cors'
    }
    ​
    //app/config/config.default.js
    config.security = {
        // 关闭csrf
        csrf: { enable: true },
        // 跨域白名单
        domainWhiteList: ['http://localhost:5173'],
      };
      // 允许跨域的方法
      config.cors = {
        origin: '*',
        allowMethods: 'GET,POST,PUT,DELETE,PATCH',
      };

封装api返回格式全局

//app/extend/context.js
module.exports = {
  apiSuccess(data = '', msg = 'ok', code = 200) {
    this.body = { msg, data };
    this.status = code;
  },
  apiFail(data = '', msg = 'fail', code = 400) {
    this.body = { msg, data };
    this.status = code;
  },
};

全局抛出异常

//app/middleware/error_handle.js
module.exports = (option, app) => {
  return async function error_handle(ctx, next) {
    try {
      await next();
      // 404错误处理
      if (ctx.status === 404 && !ctx.body) {
        ctx.body = {
          msg: 'fail',
          data: '404错误'
        };
      }
    } catch (err) {
      app.emit('error', err, ctx);
      const status = err.status || 500;
      // 默认错误
      let error =
        status === 500 && app.config.env === 'prod'
          ? 'Internal Sever Error'
          : err.message;
      ctx.body = {
        msg: 'fail',
        data: err
      };
      // 参数验证错误
      if (status === 422 && err.message === 'Validation Failed') {
        if (err.errors && Array.isArray(err.errors)) {
          error = err.errors[0].err[0]
            ? err.errors[0].err[0]
            : err.errors[0].err[1];
        }
        ctx.body = {
          msg: 'fail',
          data: error
        };
      }
      ctx.status = status;
    }
  };
};
​
​
//app/config/config.default.js
config.middleware = ['errorHandle'];

配置sequelize数据库

  1. 安装npm包npm i sequelize mysql2 -S

  2. 引入egg-sequelize插件

    //config/plugin.js
      sequelize: {
        enable: true,
        package: 'egg-sequelize',
      },

配置sequelize

//config/config.default.js
  config.sequelize = {
    dialect: 'mysql',
    host: '127.0.0.1',
    username: 'tutu',
    password: '8848MySQL*',
    port: 3306,
    database: 'onlineDisk',
    timezone: '+08:00',
    define: {
      freezeTableName: true,
      timestamps: true,
      // paranoid: true,
      createdAt: 'created_time',
      updatedAt: 'updated_time',
      underscored: true,
    },
  };

数据库迁移指令

  • 迁移数据库:npx sequelize db:migrate

  • 回滚数据库:npx sequelize db:migrate:undo

加上:all可以回滚到初始状态

Op操作符

[Op.and]:{a:5}  //且a=5
[Op.or]:{a:5},{a:6}  //a=5或a=6
[Op.gt]:6 //a>6
[Op.gte]:6 //a>=6
[Op.lt]:6 //a<6
[Op.lte]:6 //a<=6
[Op.ne]:20  //a≠5
[Op.eq]:{a:5}  //且a=5
[Op.not]:true  //非真(not True)
[Op.between]:[6,10]  //6到10之间
[Op.notBetween]:[11,15]  //不11到15之间
[Op.in]:[6,10]  //在[6,10]中
[Op.notIn]:[6,10]  //不在[6,10]中
[Op.like]:'%hat' //包含%hat
[Op.like]:'%hat' //包含%hat
[Op.notLike]:'%hat' //不包含%hat
[Op.iLike]:'%hat' //包含%hat(不区分大小写)【仅限PG】
[Op.notILike]:'%hat' //不包含%hat(不区分大小写)【仅限PG】
[Op.startWith]:'hat'  //类似hat%
[Op.endWith]:'hat'  //类似%hat
[Op.subString]:'hat'  //类似%hat%
[Op.regexp]:'^[h|a|t]'  //正则【仅限MySQL/PG】
[Op.regexp]:'^[h|a|t]'  //反正则【仅限MySQL/PG】
[Op.iRegexp]:'^[h|a|t]'  //~*'^[h|a|t]'【仅限PG】
[Op.notIRegexp]:'^[h|a|t]'  //!~*'^[h|a|t]'【仅限PG】
[Op.like]:{[Op.any]:['cat','hat']}  //包含任何数组['cat','hat']同样适用于iLike和notLike
[Op.oberlap]:[1, 2]  //&&[1,2](PG数组重叠运算符)
[Op.contains]:[1, 2]  //@>[1,2](PG数组重叠运算符)
[Op.contained]:[1, 2]  //<@[1,2](PG数组重叠运算符)
[Op.any]:[2, 3]  //任何[1,2]::INTEGER(PG数组重叠运算符)
​
[Op.col]:'user.organization_id'  //='user'.'organization_id'使用数据库语言特定的列标识符

设计接口

用户相关

数据库迁移(用户表)
  1. 安装npm install -D sequelize-cli

  2. 创建并配置.sequelizerc文件

    'use strict';
    const path = require('path');
    module.exports = {
      config: path.join(__dirname, 'database/config.json'),
      'migrations-path': path.join(__dirname, 'database/migrations'),
      'seeders-path': path.join(__dirname, 'database/seeders'),
      'models-path': path.join(__dirname, 'database/model'),
    };
  3. 初始化Migrations配置文件和目录npx sequelize init:confignpx sequelize init:migrations//npx sequelize init:models

  4. 修改生成的database/config.json文件和database/migrations目录

    "development": {
        "username": "tutu",
        "password": "8848MySQL*",
        "database": "onlineDisk",
        "host": "127.0.0.1",
        "dialect": "mysql"
      },
  5. 创建数据库npx sequelize db:create

  6. 数据表设计和迁移

    1. 创建数据迁移表npx sequelize migration:generate --name=user

    2. 配置

      //database/migrations
      async up(queryInterface, Sequelize) {
          /**
           * Add altering commands here.
           *
           * Example:
           * await queryInterface.createTable('users', { id: Sequelize.INTEGER });
           */
          const { INTEGER, STRING, DATE, ENUM, TEXT } = Sequelize;
          return queryInterface.createTable('user', {
            id: {
              type: INTEGER(20),
              primaryKey: true,
              autoIncrement: true,
            },
            username: {
              type: STRING(30),
              allowNull: false,
              defaultValue: '',
              comment: '用户名',
              unique: true,
            },
            nickname: {
              type: STRING(30),
              allowNull: false,
              defaultValue: '',
              comment: '昵称',
            },
            email: {
              type: STRING(160),
              allowNull: false,
              defaultValue: '',
              comment: '邮箱',
            },
            password: {
              type: STRING,
              allowNull: false,
              defaultValue: '',
              comment: '密码',
            },
            avatar: {
              type: STRING,
              allowNull: true,
              defaultValue: '',
              comment: '头像',
            },
            phone: {
              type: STRING(11),
              allowNull: false,
              defaultValue: '',
              comment: '手机',
            },
            sex: {
              type: ENUM,
              values: ['男', '女', '保密'],
              allowNull: false,
              defaultValue: '保密',
              comment: '性别',
            },
            desc: {
              type: TEXT,
              allowNull: false,
              defaultValue: '',
              comment: '个性签名',
            },
            create_time: DATE,
            update_time: DATE,
          });
        },
       async down(queryInterface, Sequelize) {
          /**
           * Add reverting commands here.
           *
           * Example:
           * await queryInterface.dropTable('users');
           */
          return queryInterface.dropTable('user');
        },
    3. 迁移npx sequelize db:migrate

user模型创建
//app/model/user.js
'use strict';
module.exports = app => {
  const { INTEGER, STRING, DATE, ENUM, TEXT } = app.Sequelize;
  const User = app.model.define('user', {
    id: {
      type: INTEGER(20),
      primaryKey: true,
      autoIncrement: true,
    },
    username: {
      type: STRING(30),
      allowNull: false,
      defaultValue: '',
      comment: '用户名',
      unique: true,
    },
    nickname: {
      type: STRING(30),
      allowNull: false,
      defaultValue: '',
      comment: '昵称',
    },
    email: {
      type: STRING(160),
      allowNull: false,
      defaultValue: '',
      comment: '邮箱',
    },
    password: {
      type: STRING,
      allowNull: false,
      defaultValue: '',
      comment: '密码',
    },
    avatar: {
      type: STRING,
      allowNull: true,
      defaultValue: '',
      comment: '头像',
    },
    phone: {
      type: STRING(11),
      allowNull: false,
      defaultValue: '',
      comment: '手机',
    },
    sex: {
      type: ENUM,
      values: ['男', '女', '保密'],
      allowNull: false,
      defaultValue: '保密',
      comment: '性别',
    },
    desc: {
      type: TEXT,
      allowNull: false,
      defaultValue: '',
      comment: '个性签名',
    },
    created_time: DATE,
    updated_time: DATE,
  });
  return User;
};
参数验证
  1. 安装egg-valparams npm install egg-valparams -S

  2. 配置

    //config/plugin.js
    valparams: {
        enable: true,
        package: 'egg-valparams'
      }

//config/config.default.js config.valparams = { locale: 'zh-cn', throwError: true };


##### crypto数据加密

1. 安装crypto`npm install crypto -S`

2. 配置

   ```javascript
   //config/config.default.js
   // 配置数据加密
     config.crypto = {
       secret: 'tutu@secretPassword'
     };

   //在模型里的修改器配置
jwt鉴权加密
  1. 安装egg-jwt包npm i egg-jwt -S

  2. 配置

    //app/config/plugin.js
      jwt: {
        enable: true,
        package: 'egg-jwt'
      }
    
    // config/config.default.js
      config.jwt = {
        secret: 'tutu@secretPassword'
      };

Redis缓存
  1. 安装egg-redisnpm install egg-redis -S

  2. 配置

    //app/config/plugin.js
    redis: {
        enable: true,
        package: 'egg-redis'
      }
    // config/config.default.js
      config.redis = {
        client: {
          port: 6379,
          host: '127.0.0.1',
          password: '',
          db: 2
        }
      };
  3. 封装chahe.js

    //app/service/cache.js
    'use strict';
    const Service = require('egg').Service;
    
    class CacheService extends Service {
      /**
       * 获取列表
       * @param {string} key 键
       * @param {boolean} isChildObject 元素是否为对象
       * @return { array } 返回数组
       */
      async getList(key, isChildObject = false) {
        const { redis } = this.app;
        let data = await redis.lrange(key, 0, -1);
        if (isChildObject) {
          data = data.map(item => {
            return JSON.parse(item);
          });
        }
        return data;
      }
      /**
       * 设置列表
       * @param {string} key 键
       * @param {object|string} value 值
       * @param {string} type 类型:push和unshift
       * @param {Number} expir 过期时间 单位秒
       * @return { Number } 返回索引
       */
      async setList(key, value, type = 'push', expir = 0) {
        const { redis } = this.app;
        if (expir > 0) {
          await redis.expire(key, expir);
        }
        if (typeof value === 'object') {
          value = JSON.stringify(value);
        }
        if (type === 'push') {
          return await redis.rpush(key, value);
        }
        return await redis.lpush(key, value);
      }
    
      /**
       * 设置 redis 缓存
       * @param { String } key 键
       * @param {String | Object | array} value 值
       * @param { Number } expir 过期时间 单位秒
       * @return { String } 返回成功字符串OK
       */
      async set(key, value, expir = 604800) {
        const { redis } = this.app;
        if (expir === 0) {
          return await redis.set(key, JSON.stringify(value));
        }
        return await redis.set(key, JSON.stringify(value), 'EX', expir);
      }
    
      /**
       * 获取 redis 缓存
       * @param { String } key 键
       * @return { String | array | Object } 返回获取的数据
       */
      async get(key) {
        const { redis } = this.app;
        const result = await redis.get(key);
        return JSON.parse(result);
      }
    
      /**
       * redis 自增
       * @param { String } key 键
       * @param { Number } value 自增的值
       * @return { Number } 返回递增值
       */
      async incr(key, number = 1) {
        const { redis } = this.app;
        if (number === 1) {
          return await redis.incr(key);
        }
        return await redis.incrby(key, number);
      }
    
      /**
       * 查询长度
       * @param { String } key
       * @return { Number } 返回数据长度
       */
      async strlen(key) {
        const { redis } = this.app;
        return await redis.strlen(key);
      }
    
      /**
       * 删除指定key
       * @param {String} key
       */
      async remove(key) {
        const { redis } = this.app;
        return await redis.del(key);
      }
    
      /**
       * 清空缓存
       */
      async clear() {
        return await this.app.redis.flushall();
      }
    }
    
    module.exports = CacheService;
全局权限验证中间件
//app/middleware/auth.js
module.exports = (option, app) => {
  return async (ctx, next) => {
    // 1.从header获得头部中的token
    const { token } = ctx.header;
    if (!token) {
      ctx.throw(400, '你没有权限访问接口');
    }
    // 2.根据token解密,换取用户信息
    let user = {};
    try {
      user = app.jwt.verify(token, app.config.jwt.secret);
    } catch (error) {
      const fail =
        error.name === 'TokenExpiredError'
          ? 'token已过期,请重新登录'
          : 'token不合法';
      return ctx.throw(400, fail);
    }
    // 3.判断当前用户是否登录
    const t = await ctx.service.cache.get('user_' + user.id);
    if (!t || t !== token) {
      ctx.throw(400, 'token不合法');
    }
    // 4.获取当前用户(可加入禁用选项)
    user = JSON.parse(JSON.stringify(await app.model.User.findByPk(user.id)));
    if (!user) {
      ctx.throw(400, '用户不存在');
    }
    ctx.authUser = user;
    // console.log(user);
    await next();
  };
};



//app/config/config.default.js
// 权限验证的路由
  config.auth = {
    // ignore:['/reg','/login']
    match: ['/logout']
  };

user接口
//app/controller/user.js
'use strict';

const crypto = require('crypto');
const Controller = require('egg').Controller;

class UserController extends Controller {
  // 注册接口
  async reg() {
    const { ctx, app } = this;
    // 参数验证
    ctx.validate(
      {
        username: {
          type: 'string',
          required: true,
          range: {
            min: 5,
            max: 20
          },
          desc: '用户名'
        },
        password: {
          type: 'string',
          required: true,
          desc: '密码'
        },
        repassword: {
          type: 'string',
          required: true,
          desc: '确认密码'
        }
      },
      {
        equals: [['password', 'repassword']]
      }
    );
    const { username, password } = ctx.request.body;
    if (await app.model.User.findOne({ where: { username } })) {
      ctx.throw(400, '用户已存在');
    }
    let user = await app.model.User.create({
      username,
      password
    });
    if (!user) {
      ctx.throw(400, '创建用户失败');
    }
    user = JSON.parse(JSON.stringify(user));
    delete user.password;
    return ctx.apiSuccess(user);
  }
  // 登录接口
  async login() {
    const { ctx, app } = this;
    // 参数验证
    ctx.validate({
      username: {
        type: 'string',
        required: true,
        range: {
          min: 5,
          max: 20
        },
        desc: '用户名'
      },
      password: {
        type: 'string',
        required: true,
        desc: '密码'
      }
    });
    const { username, password } = ctx.request.body;
    // 验证用户是否存在|是否被禁用
    let user = await app.model.User.findOne({
      where: { username }
    });
    if (!user) {
      throw (400, '用户不存在或被禁用');
    }
    // 验证密码
    await this.checkPassword(password, user.password);
    user = JSON.parse(JSON.stringify(user));
    // 生成token
    const token = ctx.getToken(user);
    user.token = token;
    delete user.password;
    // 加入缓存
    if (!(await this.service.cache.set('user_' + user.id, token))) {
      ctx.throw(400, '登录失败');
    }
    // 返回用户信息和token
    return ctx.apiSuccess(user);
  }
  // 退出登录
  async logout() {
    const { ctx, service } = this;
    const current_user_id = ctx.authUser.id;
    // 移除redis的用户token
    if (!(await service.cache.remove('user_' + current_user_id))) {
      ctx.throw(400, '退出登录失败');
    }
    return this.ctx.apiSuccess(`${this.ctx.authUser.username},下次再来玩啊~`);
  }

  // 剩余容量
  async getSize() {
    const { ctx } = this;
    return ctx.apiSuccess({
      total_size: ctx.authUser.total_size,
      used_size: ctx.authUser.used_size
    });
  }
  // 验证密码
  async checkPassword(password, hash_password) {
    // 先对密码进行加密
    const cPassword = crypto.createHash(
      'sha256',
      this.app.config.crypto.secret
    );
    cPassword.update(password);
    password = cPassword.digest('hex');
    const res = password === hash_password;
    if (!res) {
      this.ctx.throw(400, '密码错误');
    }
    return true;
  }
}

module.exports = UserController;

文件相关

对象存储
  1. 安装egg-ossnpm i egg-oss -S

  2. 配置oss

    //app/config/plugin.js
    oss: {
        enable: true,
        package: 'egg-oss'
      }
    
    //app/config/config.default.js
    // 对象存储
      config.oss = {
        client: {
          accessKeyId: '*',
          accessKeySecret: '*',
          bucket: '*',
          endpoint: '*',
          timeout: '60s'
        }
      };
    
      // 上传文件的限制
      config.multipart = {
        // 50mb
        fileSize: 1048576000,
        mode: 'file',
        fileExtensions: [
          // images
          '.jpg',
          '.png',
          '.gif',
          '.bmp',
          '.wbmp',
          '.webp',
          '.tif',
          '.psd',
          '.svg',
          // text
          '.js',
          '.jsx',
          '.json',
          '.css',
          '.less',
          '.scss',
          '.html',
          '.htm',
          '.xml',
          // tar
          '.zip',
          '.gz',
          '.tgz',
          '.gzip',
          '.tar',
          // '.tar.gz',
          // video
          '.mp3',
          '.mp4',
          '.avi'
        ]
      };
  3. 封装cos服务模块

    //app/service/cos.js
    'use strict';
    const fs = require('fs');
    
    const Service = require('egg').Service;
    const COS = require('cos-nodejs-sdk-v5');
    class CosService extends Service {
      async putObject(filename, filepath) {
        const { client } = this.app.config.oss;
        const cos = new COS({
          SecretId: client.accessKeyId,
          SecretKey: client.accessKeySecret
        });
        return new Promise((resolve, reject) => {
          console.log(client.bucket);
          cos.putObject(
            {
              Bucket: client.bucket,
              Region: client.region,
              Key: filename,
              StorageClass: 'STANDARD',
              Body: fs.createReadStream(filepath),
              ContentLength: fs.statSync(filepath).size,
              onProgress: progressData => {
                console.log(JSON.stringify(progressData));
              }
            },
            (err, data) => {
              if (!err) resolve(data);
              reject(err);
            }
          );
        });
      }
    }
    
    module.exports = CosService;
数据库迁移(文件表)
  1. 创建数据迁移表npx sequelize migration:generate --name=file

  2. 配置迁移文件

    //database/migrations/xxx-file.js
    'use strict';
    
    /** @type {import('sequelize-cli').Migration} */
    module.exports = {
      async up(queryInterface, Sequelize) {
        /**
         * Add altering commands here.
         *
         * Example:
         * await queryInterface.createTable('users', { id: Sequelize.INTEGER });
         */
        const { INTEGER, STRING, DATE } = Sequelize;
        return queryInterface.createTable('file', {
          id: {
            type: INTEGER(20),
            primaryKey: true,
            autoIncrement: true
          },
          name: {
            type: STRING(20),
            allowNull: false,
            defaultValue: '',
            comment: '文件名'
          },
          ext: {
            type: STRING(50),
            allowNull: true,
            defaultValue: '',
            comment: '文件拓展名'
          },
          md: {
            type: STRING,
            allowNull: true,
            defaultValue: '',
            comment: '文件MD5'
          },
          file_id: {
            type: INTEGER,
            allowNull: false,
            defaultValue: 0,
            comment: '目录id'
          },
          user_id: {
            type: INTEGER,
            allowNull: false,
            defaultValue: 0,
            comment: '用户id',
            references: {
              model: 'user',
              key: 'id'
            },
            onDelete: 'cascade',
            onUpdate: 'restrict' // 更新时的操作
          },
          size: {
            type: INTEGER,
            allowNull: false,
            defaultValue: 0,
            comment: '文件大小kb'
          },
          url: {
            type: STRING,
            allowNull: true,
            defaultValue: '',
            comment: '图片真实url'
          },
          isdir: {
            type: INTEGER,
            allowNull: false,
            defaultValue: 0,
            comment: '是否为文件夹'
          },
          create_time: DATE,
          update_time: DATE
        });
      },
    
      async down(queryInterface) {
        /**
         * Add reverting commands here.
         *
         * Example:
         * await queryInterface.dropTable('users');
         */
        return queryInterface.dropTable('file');
      }
    };
  3. 迁移npx sequelize db:migrate

file模型创建
//app/model/file.js
module.exports = app => {
  const { INTEGER, STRING, DATE } = app.Sequelize;
  const file = app.model.define('file', {
    id: {
      type: INTEGER(20),
      primaryKey: true,
      autoIncrement: true
    },
    name: {
      type: STRING(20),
      allowNull: false,
      defaultValue: '',
      comment: '文件名'
    },
    ext: {
      type: STRING(50),
      allowNull: true,
      defaultValue: '',
      comment: '文件拓展名'
    },
    md: {
      type: STRING,
      allowNull: true,
      defaultValue: '',
      comment: '文件MD5'
    },
    file_id: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 0,
      comment: '目录id'
    },
    user_id: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 0,
      comment: '用户id',
      references: {
        model: 'user',
        key: 'id'
      },
      onDelete: 'cascade',
      onUpdate: 'restrict' // 更新时的操作
    },
    size: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 0,
      comment: '文件大小kb'
    },
    url: {
      type: STRING,
      allowNull: true,
      defaultValue: '',
      comment: '图片真实url'
    },
    isdir: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 0,
      comment: '是否为文件夹'
    },
    create_time: DATE,
    update_time: DATE
  });
  // 删除后
  file.afterBulkDestory(async data => {
    console.log('删除后', data.where);
    const files = await app.model.file.findAll({
      where: {
        file_id: data.where.id,
        user_id: data.where.user_id,
        isdir: 1
      }
    });
    const ids = files.map(item => item.id);
    if (ids.lenght > 0) {
      app.model.file.destroy({
        where: {
          id: ids,
          user_id: data.where.user_id
        }
      });
    }
  });
  return file;
};
file接口
  • 封装file服务

    //app/service/file.js
    'use strict';
    
    const Service = require('egg').Service;
    
    class FileService extends Service {
      async isDirExist(id) {
        const f = await this.app.model.File.findOne({
          where: { id, user_id: this.ctx.authUser.id, isdir: 1 }
        });
        if (!f) {
          return this.ctx.throw(404, '目录不存在');
        }
      }
      async isExist(id) {
        const f = await this.app.model.File.findOne({
          where: { id, user_id: this.ctx.authUser.id }
        });
        if (!f) {
          return this.ctx.throw(404, '文件不存在');
        }
        return f;
      }
    }
    
    module.exports = FileService;
  • context拓展增加getToken方法和genID方法

    //app/extend/context.js
    module.exports = {
      // 成功的返回
      apiSuccess(data = '', msg = 'ok', code = 200) {
        this.body = { msg, data };
        this.status = code;
      },
      // 失败的返回
      apiFail(data = '', msg = 'fail', code = 400) {
        this.body = { msg, data };
        this.status = code;
      },
      // 生成token
      getToken(value) {
        return this.app.jwt.sign(value, this.app.config.jwt.secret);
      },
      // 生成唯一id
      genID(length) {
        return Number(
          Math.random()
            .toString()
            .substring(3, 3 + length) + Date.now()
        ).toString(36);
      }
    };
  • file接口

    //app/controller/file.js
    'use strict';
    const fs = require('fs');
    const path = require('path');
    const Controller = require('egg').Controller;
    
    class FileController extends Controller {
      // 上传文件
      async upload() {
        const { ctx, app } = this;
        const currentUser = ctx.authUser;
        if (!ctx.request.files) {
          return ctx.apiFail('请先选择上传的文件');
        }
        ctx.validate({
          file_id: {
            type: 'int',
            required: true,
            defValue: 0,
            desc: 'file_id'
          }
        });
        const file_id = ctx.query.file_id;
        // 文件id是否存在
        if (file_id > 0) {
          await this.service.file.isDirExist(file_id);
        }
        const file = ctx.request.files[0];
        const name = 'egg-oss/' + ctx.genID(10) + path.extname(file.filename);
    
        // 验证用户剩余内存是否满足
        const s = await new Promise(resolve => {
          // eslint-disable-next-line node/prefer-promises/fs
          fs.stat(file.filepath, (err, stats) => {
            resolve((stats.size / 1024).toFixed(1));
          });
        });
        if (currentUser.total_size - currentUser.used_size < s) {
          return ctx.apiFail('你的可用内存不足');
        }
        let result;
        try {
          result = await this.service.cos.putObject(name, file.filepath);
        } catch (err) {
          console.log('err', err);
        }
        if (result) {
          // 写入数据表
          const addData = {
            name: file.filename,
            ext: file.ext,
            md: result.Location.slice(result.Location.indexOf('/') + 1),
            file_id,
            user_id: currentUser.id,
            size: parseInt(s),
            url: result.Location,
            isdir: 0
          };
          if (file_id > 0) {
            addData.file_id = file_id;
          }
          const res = await app.model.File.create(addData);
          if (!res) {
            ctx.throw(400, '创建用户失败');
          }
          // 更新网盘容量
          currentUser.used_size = currentUser.used_size + parseInt(s);
          currentUser.save();
          return ctx.apiSuccess(res);
        }
        return ctx.apiSuccess('上传失败');
      }
    
      // 文件列表
      async list() {
        const { ctx, app } = this;
        const user_id = ctx.authUser.id;
        ctx.validate({
          file_id: {
            required: true,
            type: 'int',
            defValue: 0,
            desc: '目录id'
          },
          orderby: {
            required: false,
            type: 'string',
            defValue: 'name',
            range: { in: ['name', 'created_time'] },
            desc: '排序'
          },
          type: {
            required: false,
            type: 'string',
            desc: '类型'
          }
        });
        const { file_id, orderby, type } = ctx.query;
        const where = { user_id, file_id };
        // 按文件类型索引
        if (type) {
          const Op = app.Sequelize.Op;
          where.ext = {
            [Op.like]: type + '%'
          };
        }
        const rows = await app.model.File.findAll({
          where,
          order: [
            ['isdir', 'desc'],
            [orderby, 'desc']
          ]
        });
        if (rows) return ctx.apiSuccess({ rows });
        return ctx.apiFail('获取文件列表错误');
      }
    
      // 创建文件夹
      async mkdir() {
        const { ctx, app } = this;
        const user_id = ctx.authUser.id;
        ctx.validate({
          file_id: {
            required: true,
            type: 'int',
            defValue: 0,
            desc: '目录id'
          },
          name: {
            required: true,
            type: 'string',
            desc: '文件夹名称'
          }
        });
        const { file_id, name } = ctx.request.body;
        // 验证目录是否存在
        if (file_id) {
          await this.service.file.isDirExist(file_id);
        }
        const res = await app.model.File.create({
          name,
          file_id,
          user_id,
          isdir: 1,
          size: 0
        });
        return ctx.apiSuccess(res);
      }
    
      // 重命名
      async rename() {
        const { ctx } = this;
        ctx.validate({
          id: {
            required: true,
            type: 'int',
            desc: '文件id'
          },
          file_id: {
            required: true,
            type: 'int',
            defValue: 0,
            desc: '目录id'
          },
          name: {
            required: true,
            type: 'string',
            desc: '文件名称'
          }
        });
        const { id, file_id, name } = ctx.request.body;
        if (file_id > 0) {
          await this.service.file.isDirExist(file_id);
        }
        // 文件是否存在
        const f = await this.service.file.isExist(id);
        f.name = name;
        const res = await f.save();
        return ctx.apiSuccess(res);
      }
    
      // 删除
      async delete() {
        const { ctx, app } = this;
        const user_id = ctx.authUser.id;
        ctx.validate({
          ids: {
            required: true,
            type: 'string',
            desc: '删除文件的id'
          }
        });
        let { ids } = ctx.request.body;
        ids = ids.split(',');
    
        // 计算删除文件的内存
        const files = await app.model.File.findAll({
          where: {
            id: ids,
            user_id
          }
        });
        let size = 0;
        files.forEach(item => {
          size += item.size;
        });
        const res = await app.model.File.destroy({
          where: { id: ids, user_id }
        });
        if (res) {
          size = ctx.authUser.used_size - size;
          ctx.authUser.used_size = size > 0 ? size : 0;
          ctx.authUser.save();
        }
        return ctx.apiSuccess(`删除成功${res}`);
      }
    
      // 搜索
      async search() {
        const { ctx, app } = this;
        const user_id = ctx.authUser.id;
        const Op = app.Sequelize.Op;
        ctx.validate({
          keyword: {
            required: true,
            type: 'string',
            defValue: '',
            desc: '关键字'
          }
        });
        const { keyword } = ctx.query;
        const rows = await app.model.File.findAll({
          where: {
            name: { [Op.like]: `%${keyword}%` },
            isdir: 0,
            user_id
          }
        });
        if (!rows) {
          ctx.apiFail('未找到');
        }
        return ctx.apiSuccess(rows);
      }
    }
    
    module.exports = FileController;

分享相关

数据库迁移(分享表)
  1. 创建数据迁移表npx sequelize migration:generate --name=share

  2. 配置迁移文件

    //app/database/migrations/xxx-share.js
    
    'use strict';
    
    /** @type {import('sequelize-cli').Migration} */
    module.exports = {
      async up(queryInterface, Sequelize) {
        /**
         * Add altering commands here.
         *
         * Example:
         * await queryInterface.createTable('users', { id: Sequelize.INTEGER });
         */
        const { INTEGER, STRING, DATE } = Sequelize;
        return queryInterface.createTable('share', {
          id: {
            type: INTEGER(20),
            primaryKey: true,
            autoIncrement: true
          },
          sharedurl: {
            type: STRING,
            allowNull: true,
            defaultValue: '',
            comment: '分享链接'
          },
          file_id: {
            type: INTEGER,
            allowNull: false,
            defaultValue: 0,
            comment: '文件id',
            references: {
              model: 'file',
              key: 'id'
            },
            onDelete: 'cascade',
            onUpdate: 'restrict' // 更新时的操作
          },
          iscancel: {
            type: INTEGER(1),
            allowNull: false,
            defaultValue: 0,
            comment: '是否取消分享'
          },
          user_id: {
            type: INTEGER,
            allowNull: false,
            defaultValue: 0,
            comment: '用户id',
            references: { model: 'user', key: 'id' },
            onDelete: 'cascade',
            onUpdate: 'restrict' // 更新时的操作
          },
          created_time: DATE,
          updated_time: DATE
        });
      },
    
      async down(queryInterface) {
        /**
         * Add reverting commands here.
         *
         * Example:
         * await queryInterface.dropTable('users');
         */
        return queryInterface.dropTable('share');
      }
    };
  3. 执行数据库迁移npx sequelize db:migrate

share模型创建
//app/model/share.js
'use strict';

module.exports = app => {
  const { INTEGER, STRING, DATE } = app.Sequelize;
  const Share = app.model.define('share', {
    id: {
      type: INTEGER(20),
      primaryKey: true,
      autoIncrement: true
    },
    sharedurl: {
      type: STRING,
      allowNull: true,
      defaultValue: '',
      comment: '分享链接'
    },
    file_id: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 0,
      comment: '文件id',
      references: {
        model: 'file',
        key: 'id'
      },
      onDelete: 'cascade',
      onUpdate: 'restrict' // 更新时的操作
    },
    iscancel: {
      type: INTEGER(1),
      allowNull: false,
      defaultValue: 0,
      comment: '是否取消分享'
    },
    user_id: {
      type: INTEGER,
      allowNull: false,
      defaultValue: 0,
      comment: '用户id',
      references: { model: 'user', key: 'id' },
      onDelete: 'cascade',
      onUpdate: 'restrict' // 更新时的操作
    },
    created_time: DATE,
    updated_time: DATE
  });
  Share.associate = () => {
    // 关联文件
    Share.belongsTo(app.model.File);
  };
  return Share;
};
share接口
  • 封装share服务

    //app/service/share.js
    'use strict';
    
    const Service = require('egg').Service;
    
    class ShareService extends Service {
      async isExist(sharedurl, options = {}) {
        const s = await this.app.model.Share.findOne({
          where: { sharedurl, iscancel: 0 },
          ...options
        });
        if (!s) return this.ctx.throw(404, '该分享已失效');
        return s;
      }
    }
    
    module.exports = ShareService;
  • share接口

    //app/controller/share.js
    'use strict';
    
    const Controller = require('egg').Controller;
    
    class ShareController extends Controller {
      // 创建分享
      async create() {
        const { ctx, app } = this;
        const user_id = ctx.authUser.id;
        ctx.validate({
          file_id: {
            type: 'int',
            required: true,
            desc: '文件id'
          }
        });
        const { file_id } = ctx.request.body;
        // 文件/文件夹是否存在
        // console.log('-----------------------------------');
        const f = await app.model.File.findOne({
          where: {
            id: file_id,
            user_id
          }
        });
        if (!f) {
          return this.ctx.throw(404, '文件不存在');
        }
        // 唯一id
        const sharedurl = ctx.genID(15);
        // 创建分享
        const s = await app.model.Share.create({
          sharedurl,
          file_id,
          iscancel: 0,
          user_id
        });
        if (s) {
          const url = 'http://127.0.0.1:7001/share/' + sharedurl;
          return ctx.apiSuccess('分享链接:' + url);
        }
        return ctx.apiFail('分享失败');
      }
    
      // 获取分享列表
      async list() {
        const { ctx } = this;
        const user_id = ctx.authUser.id;
        const list = await this.app.model.Share.findAndCountAll({
          where: {
            user_id
          },
          include: [{ model: this.app.model.File }]
        });
        ctx.apiSuccess(list);
      }
    
      // 查看分享
      async read() {
        const { ctx, service, app } = this;
        const sharedurl = ctx.params.sharedurl;
        if (!sharedurl) return ctx.apiFail('参数非法');
        const file_id = ctx.query.file_id;
    
        // 查看分享是否存在
        const s = await service.share.isExist(sharedurl);
        const where = { user_id: s.user_id };
        if (!file_id) where.id = s.file_id;
        else where.file_id = file_id;
        const rows = await app.model.File.findAll({
          where,
          order: [['isdir', 'desc']]
        });
        ctx.apiSuccess(rows);
      }
    
      // 保存别人的分享
      async saveToself() {
        const { ctx, app, service } = this;
        const current_user_id = ctx.authUser.id;
        ctx.validate({
          dir_id: {
            type: 'int',
            required: true,
            desc: '目录id'
          },
          sharedurl: {
            type: 'string',
            required: true,
            desc: '分享标识'
          }
        });
        const { dir_id, sharedurl } = ctx.request.body;
    
        // 分享是否存在
        const s = await service.share.isExist(sharedurl, {
          include: [{ model: app.model.File }]
        });
        if (s.user_id === current_user_id) {
          return ctx.apiFail('要乖,不可以保存自己的分享哦~');
        }
        // 文件是否存在
        if (dir_id > 0) {
          await service.file.isDirExist(dir_id);
        }
    
        // 查询该分享目录下的所有数据
        const gettAllFile = async (obj, dirId) => {
          const data = {
            name: obj.name,
            ext: obj.ext,
            md: obj.md,
            file_id: dirId,
            user_id: current_user_id,
            size: obj.size,
            isdir: obj.isdir,
            url: obj.url
          };
          // 判断当前用户剩余空间
          if (ctx.authUser.total_size - ctx.authUser.used_size < data.size) {
            return ctx.throw(400, '你的可用空间不足');
          }
          // 直接创建
          const o = await app.model.File.create(data);
    
          // 更新剩余内存
          ctx.used_size += parseInt(data.size);
          await ctx.authUser.save();
    
          // 目录
          if (obj.isdir) {
            const rows = await app.model.File.findAll({
              where: {
                user_id: obj.used_id,
                file_id: obj.id
              }
            });
            rows.forEach(element => {
              gettAllFile(element, o.id);
            });
            return;
          }
        };
        await gettAllFile(s.file, dir_id);
        ctx.apiSuccess('ok');
      }
    }
    
    module.exports = ShareController;

0

评论区