// egg-restfulapi
// 没有app.js 文件
// 特别的库:await-stream-ready egg-bcrypt egg-cors egg-jwt
// egg-mongoose egg-scripts image-downloader mocha moment stream-to-array
// stream-wormhole
// 应该是个很规范的例子
// image-downloader 下载图片
// Download to a directory and save with an another filename
options = {
url: 'http://someurl.com/image2.jpg',
dest: '/path/to/dest/photo.jpg' // Save to /path/to/dest/photo.jpg
}
download.image(options)
.then(({ filename, image }) => {
console.log('File saved to', filename)
})
.catch((err) => {
console.error(err)
})
// await-stream-ready
// 以流的方式读文件
const fs = require('fs');
const awaitWriteStream = require('await-stream-ready').write;
async function write(srcStream, file) {
const stream = srcStream.pipe(fs.createWriteStream(file));
await awaitWriteStream(stream);
}
const fs = require('fs');
const awaitReadStream = require('await-stream-ready').read;
async function read(file) {
const stream = fs.createReadStream(file);
stream.on('data', buf => {
// consume buf
});
await awaitReadStream(stream);
}
// egg-bcrypt 根据字符 建立hash值 类似 md5的库
// 产生
exports.bcrypt = {
saltRounds: 10 // default 10
}
// {app_root}/config/plugin.js
exports.bcrypt = {
enable: true,
package: 'egg-bcrypt'
}
async generate() {
const hash = await this.ctx.genHash(this.ctx.request.body.plainText);
// Store hash in your password DB
}
// 比较
async compare() {
const { hash, plainText } = this.ctx.request.body;
const checked = await this.ctx.compare(plainText, hash);
this.ctx.body = { checked };
}
// egg-cors 跨域返回
// {app_root}/config/plugin.js
exports.cors = {
enable: true,
package: 'egg-cors',
};
// 配置白名单
exports.security = {
domainWhiteList: ['http://localhost:4200'],
};
// {app_root}/config/config.default.js
exports.cors = {
// {string|Function} origin: '*',
// {string|Array} allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
};
// egg-jwt
// {app_root}/config/plugin.js
exports.jwt = {
enable: true,
package: "egg-jwt"
};
// {app_root}/config/config.default.js
exports.jwt = {
secret: "123456"
};
const token = app.jwt.sign({ foo: 'bar' }, app.config.jwt.secret);
module.exports = app => {
app.get("/", app.jwt, "render.index"); // use old api app.jwt
app.get("/login", "login.index");
app.get("/success", "success.index"); // is setting in config.jwt.match
};
// restapi
// role
// router.post('/api/role', controller.role.create)
// router.delete('/api/role/:id', controller.role.destroy)
// router.put('/api/role/:id', controller.role.update)
// router.get('/api/role/:id', controller.role.show)
// router.get('/api/role', controller.role.index)
router.delete('/api/role', controller.role.removes)
router.resources('role', '/api/role', controller.role)
// default config
module.exports = appInfo => {
const config = exports = {}
// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1513779989145_1674'
// add your config here
// 加载 errorHandler 中间件
config.middleware = ['errorHandler']
// 只对 /api 前缀的 url 路径生效
// config.errorHandler = {
// match: '/api',
// }
config.security = {
csrf: {
enable: false,
},
domainWhiteList: ['http://localhost:8000'],
}
config.multipart = {
fileExtensions: ['.apk', '.pptx', '.docx', '.csv', '.doc', '.ppt', '.pdf', '.pages', '.wav', '.mov'], // 增加对 .apk 扩展名的支持
},
config.bcrypt = {
saltRounds: 10 // default 10
}
config.mongoose = {
url: 'mongodb://127.0.0.1:27017/egg_x',
options: {
useMongoClient: true,
autoReconnect: true,
reconnectTries: Number.MAX_VALUE,
bufferMaxEntries: 0,
},
}
config.jwt = {
secret: 'Great4-M',
enable: true, // default is false
match: '/jwt', // optional
}
return config
}
/******************************************************************************** */
// plugin
exports.validate = {
enable: true,
package: 'egg-validate',
}
exports.bcrypt = {
enable: true,
package: 'egg-bcrypt'
}
exports.mongoose = {
enable: true,
package: 'egg-mongoose',
}
exports.jwt = {
enable: true,
package: 'egg-jwt',
}
exports.cors = {
enable: true,
package: 'egg-cors',
}
// 错误处理 中间件
'use strict'
module.exports = (option, app) => {
return async function (ctx, next) {
try {
await next()
} catch (err) {
// 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
app.emit('error', err, this)
const status = err.status || 500
// 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
const error = status === 500 && app.config.env === 'prod' ?
'Internal Server Error' :
err.message
// 从 error 对象上读出各个属性,设置到响应中
ctx.body = {
code: status, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码
error: error
// msg: error
}
if (status === 422) {
ctx.body.detail = err.errors
// ctx.body.data = err.errors
}
ctx.status = 200
}
}
}
/******************************************************************************** */
// mongoose 的model
module.exports = app => {
const mongoose = app.mongoose
const RoleSchema = new mongoose.Schema({
name: { type: String, unique: true, required: true },
access: { type: String, required: true, default: 'user' },
extra: { type: mongoose.Schema.Types.Mixed },//混合类型
createdAt: { type: Date, default: Date.now }
})
return mongoose.model('Role', RoleSchema)
}
/******************************************************************************** */
// 处理成功响应
exports.success = ({ ctx, res = null, msg = '请求成功' }) => {
ctx.body = {
code: 0,
data: res,
msg
}
ctx.status = 200
}
// jwt 鉴权
router.get('/api/user/access/current', app.jwt, controller.userAccess.current)
/******************************************************************************** */
// 用户登入
async login() {
const { ctx, service } = this
// 校验参数
ctx.validate(this.UserLoginTransfer)
// 组装参数
const payload = ctx.request.body || {}
// 调用 Service 进行业务处理
const res = await service.userAccess.login(payload)
// 设置响应内容和响应状态码
ctx.helper.success({ ctx, res })
}
//请求头访问
// curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
// eyJmb28iOiJiYXIiLCJpYXQiOjE0OTAwMTU0MTN9.ehQ38YsRlM8hDpUMKYq1rHt-YjBPSU11dFm0NOroPEg"
// jwt 生成token
class ActionTokenService extends Service {
async apply(_id) {
const {ctx} = this
return ctx.app.jwt.sign({
data: {
_id: _id
},
// exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 7)
exp: Math.floor(Date.now() / 1000) + (60)
}, ctx.app.config.jwt.secret)
}
}
// 登录
async login(payload) {
const { ctx, service } = this
const user = await service.user.findByMobile(payload.mobile)
if (!user) {
ctx.throw(404, 'user not found')
}
let verifyPsw = await ctx.compare(payload.password, user.password)//hash比较密码 ,后者hash
if (!verifyPsw) {
ctx.throw(404, 'user password is error')
}
// 生成Token令牌
return { token: await service.actionToken.apply(user._id) }
}
// ctx.state.user 可以提取到JWT编码的data
const _id = ctx.state.user.data._id
const user = await service.user.find(_id)
if (!user) {
ctx.throw(404, 'user is not found')
}
if (!verifyPsw) {
ctx.throw(404, 'user password error')
} else {
// 重置密码
values.password = await ctx.genHash(values.password)
return service.user.findByIdAndUpdate(_id, values)
}
/******************************************************************************** */
// 数据库上有不同
return ctx.model.User.findByIdAndUpdate(_id, payload) //mongo
ctx.model.ProductModel.findOne({ where: { id } }); //seqlize
/******************************************************************************** */
// 分页排序
async index(payload) {
const { currentPage, pageSize, isPaging, search } = payload
let res = []
let count = 0
let skip = ((Number(currentPage)) - 1) * Number(pageSize || 10)
if(isPaging) {
if(search) {
res = await this.ctx.model.User.find({mobile: { $regex: search } }).populate('role').skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec()
count = res.length
} else {
res = await this.ctx.model.User.find({}).populate('role').skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec()
count = await this.ctx.model.User.count({}).exec()
}
} else {
if(search) {
res = await this.ctx.model.User.find({mobile: { $regex: search } }).populate('role').sort({ createdAt: -1 }).exec()
count = res.length
} else {
res = await this.ctx.model.User.find({}).populate('role').sort({ createdAt: -1 }).exec()
count = await this.ctx.model.User.count({}).exec()
}
}
// 整理数据源 -> Ant Design Pro
let data = res.map((e,i) => {
const jsonObject = Object.assign({}, e._doc)
jsonObject.key = i
jsonObject.password = 'Are you ok?'
jsonObject.createdAt = this.ctx.helper.formatTime(e.createdAt)
return jsonObject
})
return { count: count, list: data, pageSize: Number(pageSize), currentPage: Number(currentPage) }
// 资源上传======================================================================================================>
// 通过URL添加单个图片: 如果网络地址不合法,EGG会返回500错误
async url() {
const { ctx, service } = this
// 组装参数
const attachment = new this.ctx.model.Attachment
const { url } = ctx.request.body
const filename = path.basename(url) // 文件名称
const extname = path.extname(url).toLowerCase() // 文件扩展名称
const options = {
url: url,
dest: path.join(this.config.baseDir, 'app/public/uploads', `${attachment._id.toString()}${extname}`)
}//下载资源到静态资源文件夹 外部返回 url/uploads
let res
try {
// 写入文件 const { filename, image}
await download.image(options)
attachment.extname = extname
attachment.filename = filename
attachment.url = `/uploads/${attachment._id.toString()}${extname}`//静态资源相对根目录
res = await service.upload.create(attachment)
} catch (err) {
throw err
}
// 设置响应内容和响应状态码
ctx.helper.success({ctx, res})
}
// 上传单个文件
async create() {
const { ctx, service } = this
// 要通过 ctx.getFileStream 便捷的获取到用户上传的文件,需要满足两个条件:
// 只支持上传一个文件。
// 上传文件必须在所有其他的 fields 后面,否则在拿到文件流时可能还获取不到 fields。
const stream = await ctx.getFileStream()
// 所有表单字段都能通过 `stream.fields` 获取到
const filename = path.basename(stream.filename) // 文件名称
const extname = path.extname(stream.filename).toLowerCase() // 文件扩展名称
// 组装参数 model
const attachment = new this.ctx.model.Attachment
attachment.extname = extname
attachment.filename = filename
attachment.url = `/uploads/${attachment._id.toString()}${extname}`
// 组装参数 stream
const target = path.join(this.config.baseDir, 'app/public/uploads', `${attachment._id.toString()}${attachment.extname}`)
const writeStream = fs.createWriteStream(target)
// 文件处理,上传到云存储等等
try {
await awaitWriteStream(stream.pipe(writeStream))
} catch (err) {
// 必须将上传的文件流消费掉,要不然浏览器响应会卡死
await sendToWormhole(stream)
throw err
}
// 调用 Service 进行业务处理
const res = await service.upload.create(attachment)
// 设置响应内容和响应状态码
ctx.helper.success({ctx, res})
}
// 上传多个文件
async multiple() {
// 要获取同时上传的多个文件,不能通过 ctx.getFileStream() 来获取
const { ctx, service } = this
const parts = ctx.multipart()
const res = {}
const files = []
let part // parts() return a promise
while ((part = await parts()) != null) {
if (part.length) {
// 如果是数组的话是 filed
// console.log('field: ' + part[0])
// console.log('value: ' + part[1])
// console.log('valueTruncated: ' + part[2])
// console.log('fieldnameTruncated: ' + part[3])
} else {
if (!part.filename) {
// 这时是用户没有选择文件就点击了上传(part 是 file stream,但是 part.filename 为空)
// 需要做出处理,例如给出错误提示消息
return
}
// part 是上传的文件流
// console.log('field: ' + part.fieldname)
// console.log('filename: ' + part.filename)
// console.log('extname: ' + part.extname)
// console.log('encoding: ' + part.encoding)
// console.log('mime: ' + part.mime)
const filename = part.filename.toLowerCase() // 文件名称
const extname = path.extname(part.filename).toLowerCase() // 文件扩展名称
// 组装参数
const attachment = new ctx.model.Attachment
attachment.extname = extname
attachment.filename = filename
attachment.url = `/uploads/${attachment._id.toString()}${extname}`
// const target = path.join(this.config.baseDir, 'app/public/uploads', filename)
const target = path.join(this.config.baseDir, 'app/public/uploads', `${attachment._id.toString()}${extname}`)
const writeStream = fs.createWriteStream(target)
// 文件处理,上传到云存储等等
let res
try {
// result = await ctx.oss.put('egg-multipart-test/' + part.filename, part)
await awaitWriteStream(part.pipe(writeStream))
// 调用Service
res = await service.upload.create(attachment)
} catch (err) {
// 必须将上传的文件流消费掉,要不然浏览器响应会卡死
await sendToWormhole(part)
throw err
}
files.push(`${attachment._id}`) // console.log(result)
}
}
ctx.helper.success({ctx, res: { _ids:files }})
}
// 分页
if(isPaging) {
if(search) {
if (kind) {
res = await this.ctx.model.Attachment
.find({filename: { $regex: search }, extname: { $in: attachmentKind[`${kind}`]} })
.skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec()
async removes(payload) {
return this.ctx.model.User.remove({ _id: { $in: payload } })
}
// Commons======================================================================================================>
async findByMobile(mobile) {
return this.ctx.model.User.findOne({mobile: mobile})
}
async find(id) {
return this.ctx.model.User.findById(id)
}
async findByIdAndUpdate(id, values) {
return this.ctx.model.User.findByIdAndUpdate(id, values)
}