创建项目&&环境
更改镜像源
sudo npm config set registry https://registry.npm.taobao.org
脚手架生成项目
mkdir api_disk && cd api_disk
npm init egg&&npm i
配置eslint
安装并初始化
npm init @eslint/config
将.eslintrc.js后缀名改为.eslintrc
修改拓展
"extends": "eslint-config-egg",
添加规则(忽略数组里空格问题)
"array-bracket-spacing": "off"
修改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配置
git init
npm install husky
npx husky install
npm install -D @commitlint/config-conventional @commitlint/cli
创建commitlint.config.js文件
module.exports = { extends: ['@commitlint/config-conventional'], };
npx husky add .husky/pre-commit "npm test"
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'
修改./husky/pre-commit文件,把npm test修改为
npx lint-staged
一定要记得给与权限
chmod +x .husky/commit-msg .husky/pre-commit
配置git
git remote add origin https://github.com/W433567423/onlineDiskApi.git
git add .
git branch -m main
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:不属于以上类型的其他类型(日常事务
配置项目
配置跨域
安装插件npm install egg-cors -S
配置插件
//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数据库
安装npm包
npm i sequelize mysql2 -S
引入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'使用数据库语言特定的列标识符
设计接口
用户相关
数据库迁移(用户表)
安装
npm install -D sequelize-cli
创建并配置.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'), };
初始化Migrations配置文件和目录
npx sequelize init:config
和npx sequelize init:migrations
和//npx sequelize init:models
修改生成的database/config.json文件和database/migrations目录
"development": { "username": "tutu", "password": "8848MySQL*", "database": "onlineDisk", "host": "127.0.0.1", "dialect": "mysql" },
创建数据库npx sequelize db:create
数据表设计和迁移
创建数据迁移表
npx sequelize migration:generate --name=user
配置
//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'); },
迁移
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;
};
参数验证
安装egg-valparams
npm install egg-valparams -S
配置
//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鉴权加密
安装egg-jwt包
npm i egg-jwt -S
配置
//app/config/plugin.js jwt: { enable: true, package: 'egg-jwt' } // config/config.default.js config.jwt = { secret: 'tutu@secretPassword' };
Redis缓存
安装egg-redis
npm install egg-redis -S
配置
//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 } };
封装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;
文件相关
对象存储
安装egg-oss
npm i egg-oss -S
配置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' ] };
封装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;
数据库迁移(文件表)
创建数据迁移表
npx sequelize migration:generate --name=file
配置迁移文件
//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'); } };
迁移
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;
分享相关
数据库迁移(分享表)
创建数据迁移表
npx sequelize migration:generate --name=share
配置迁移文件
//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'); } };
执行数据库迁移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;
评论区