主题
Egg在开发中一些常用功能
一. 文件上传
Egg文件上传,官方提供了两方式:file
和stream
;开发中可以根据自生需要,选择合适的方式进行文件上传。文件上传官方示例。 但是,更推荐stream
的方式,在nodejs中,stream才是主流,相比file的方式,stream占用的内存空间会更小,速度也会更快。
比如:上传一个非常大的文件(2GB),file会将2GB文件先上传到内存中,再对文件进行操作(读写等),处理完后再释放内存,此时服务器压力会非常大,若是服务器内存很吃紧的话,会导致服务器宕机,相比之下,stream的方式就不存在这种请情况
1、file模式与stream共存的配置
一个项目中,可能存在多种文件上传的方式,file模式和stream模式,虽然推荐stream模式,但是有的场景下file处理起来会更方便,因此同时支持stream和file模式配置很有必要
js
// config.default.js
config.multipart = {
mode: 'stream',
// 这些接口使用stream模式,其余的上传文件接口使用file模式
fileModeMatch: /^((?!((\/api\/uploadImg)|(\/api\/uploadExcel))).)*$/,
tmpdir: path.join(__dirname, '../tmp/', 'egg-multipart-tmp'),
fileExtensions: ['.wav', '.PDF', '.BMP', '.DOC', '.DOCX', '.GIF', '.JPG', '.PNG', '.PPT', '.PPTX', '.RAR', '.TXT', '.XLS', '.XLSX', '.ZIP', '.csv', '.ftl', '.html'],
2、创建文件夹封装
js
const fs = require("fs");
function mkdirsSync(dirname) {
if (fs.existsSync(dirname)) {
return true;
} else {
if (mkdirsSync(path.dirname(dirname))) {
fs.mkdirSync(dirname);
return true;
}
}
}
单文件上传
1. file模式
js
// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('mz/fs');
module.exports = class extends Controller {
async upload() {
const { ctx } = this;
const file = ctx.request.files[0];
const name = 'egg-multipart-test/' + path.basename(file.filename);
let result;
try {
// 处理文件,比如上传到云端
result = await ctx.oss.put(name, file.filepath);
} finally {
// 需要删除临时文件
await fs.unlink(file.filepath);
}
ctx.body = {
url: result.url,
// 获取所有的字段值
requestBody: ctx.request.body,
};
}
};
2. stream模式
egg服务端 controller
js
/*所需要依赖*/
//故名思意 异步二进制 写入流
const awaitWriteStream = require("await-stream-ready").write;
//管道读入一个虫洞。
const sendToWormhole = require("stream-wormhole");
const dayjs = require("dayjs");
/*核心代码*/
/**
* @api {post} /api/uploadFile 文件上传
* @apiName uploadFile
* @apiGroup 公共模块
* @apiParam {File} file 文件
* @apiSuccess {String} data 文件相对路径
*
* @apiHeader {String} authorization 用户授权token
*/
async uploadFileAtLocal() {
const { ctx, app, config } = this;
const stream = await ctx.getFileStream();
console.log("-----------获取表单中其它数据 start--------------");
console.log(stream.fields);
console.log("-----------获取表单中其它数据 end--------------");
// 基础的目录
const uplaodBasePath = "app/public/uploads";
// 生成文件名
const filename = `${Date.now()}${Number.parseInt(
Math.random() * 1000
)}${path.extname(stream.filename).toLocaleLowerCase()}`;
// 生成文件夹
const dirname = dayjs(Date.now()).format("YYYY/MM/DD");
mkdirsSync(path.join(/* app.root, */ uplaodBasePath, dirname));
// 生成写入路径
const target = path.join(
/* app.root, */ uplaodBasePath,
dirname,
filename
);
// 写入流
const writeStream = fs.createWriteStream(target);
let filePath = path.join("/public/uploads", dirname, filename);
this.success(filePath);
try {
//异步把文件流 写入
await awaitWriteStream(stream.pipe(writeStream));
let prePath = path.join("/uploads", dirname, filename);
this.success(prePath.replace(/\\/g, "/"));
} catch (err) {
//如果出现错误,关闭管道
await sendToWormhole(stream);
this.fail("文件上传失败");
前端 前端以formData的方式发情请求
将Content-Type: multipart/form-data;
js
let formData = new FormData()
formData.append('file',file)
// 字段也这样添加进去
formData.append('publicStatus',true)
最后将这个formData放到body里面即可
多文件上传
多文件上传处理方式同单文件上传处理方式一致,参见egg获取上传的文件
egg文件上传总结
针对node Stream 上传文件的文章 https://blog.csdn.net/u014695532/article/details/51150704https://blog.csdn.net/qq_34832846/article/details/88125645https://blog.csdn.net/xj5210224/article/details/103024171
直接从流中获取数据的插件: https://www.npmjs.com/package/concat-stream
若是上传excel文件,或导出excel文件,有excel.js插件, 非常方便
egg用stream文件上传,不存文件到本地,直接获取文件内数据案例
javascript
// controller
// 批量上传txt
async batchPosId() {
const { ctx, service } = this;
let stream;
try {
stream = await ctx.getFileStream();
let str = await service.common.getStreamData(stream, 'string')
let idsArr = str.split('\n') // txt内所有数据的数组
await sendToWormhole(stream);
ctx.body = { code: 0, msg: "操作成功", data: idsArr }
} catch (err) {
console.log('==========文件上传失败========', err);
await sendToWormhole(stream);
ctx.body = { code: 1, msg: '文件上传或解析失败' };
return;
}
}
// service
const concat = require('concat-stream')
/**
*
* @param {File} stream 文件流对象
* @param {*} encoding 要返回的编码 encoding取值如下
* string - get a string (默认)
* buffer - get back a Buffer
* array - get an array of byte integers
* uint8array, u8, uint8 - get back a Uint8Array
* object, get back an array of Objects
*/
async getStreamData(stream, encoding = "string") {
return new Promise((resolve, reject) => {
stream.pipe(concat({ encoding }, data => {
resolve(data)
}))
stream.on('error', err => {
reject(err)
})
})
}
二. 处理 Excel,(exceljs)
Excel可以存放多少条数据? 根据不同的版本有不同的存储条数: 1、Excel 07-2003 一个工作表最多可有65536行,行用数字1—65536表示,最多可有256列。 单个sheet可以保存:行(65536)*列(256)=16777216个数据。 2、Excel 2007及以后版本,一个工作表最多可有1048576行,16384列; 单个sheet可以保存:行(1048576)*列(16384)=17179869184个数据。
exceljs文档:https://github.com/exceljs/exceljs/blob/master/README_zh.md 公共service
js
const Service = require('egg').Service;
const Excel = require('exceljs');
class ExcelService extends Service {
/**
*负责解析excel,以头部为键,生成数组
* @param {stream} stream excel流
* @param {Object} headNameMap 头部名称和键的索引
* 例如:
* const headNameMap = {
name: '姓名',
age: '年龄',
sex: '性别',
};
*/
async parseExcel(stream, headNameMap) {
const workbook = new Excel.Workbook();
await workbook.xlsx.read(stream);
const worksheet = workbook.getWorksheet(1);
const data = [];
const columnCount = worksheet.columnCount;
for (let c = 1; c <= columnCount; c++) {
const col = worksheet.getColumn(c);
Object.entries(headNameMap).forEach(([key, value]) => {
if (!col.values[1] || typeof col.values[1] !== 'string') return;
if (col.values[1].trim() === value) {
const colValues = col.values.slice(2);
colValues.forEach((val, index) => {
let item = {};
if (data[index]) {
item = data[index];
} else {
data[index] = item;
}
item[key] = val;
});
}
});
}
return data;
}
/**
* 获取excel workbook对象,具体用法见controller/sys_manage/log_manage.js > function export
* exceljs文档:https://github.com/exceljs/exceljs/blob/master/README_zh.md
*/
getWorkbook({ sheetName = '', columns = [], rows = [] }) {
let workbook = new Excel.Workbook();
workbook.creator = 'Mr.Li';
workbook.created = new Date(Date.now());
workbook.modified = new Date(Date.now());
let sheet = workbook.addWorksheet(sheetName);
sheet.columns = columns.map(item => {
return {
...item,
style: item.style || {
alignment: {
vertical: 'center',
horizontal: 'center'
}
}
};
});
rows.forEach((row, index) => {
let rowData = {
...row,
index: index + 1
};
sheet.addRow(rowData);
});
return workbook;
}
}
module.exports = ExcelService;
1、上传并解析excel
js
async uploadExcel() {
const { ctx, service, logger } = this;
let stream;
try {
stream = await ctx.getFileStream();
let fileExtname = path.extname(stream.filename).toLowerCase();
if (fileExtname !== ".xls" && fileExtname !== ".xlsx") {
this.fail("文件类型错误,请上传Excel文件");
await streamWormhole(stream);
return;
}
const headNameMap = {
name: "姓名",
age: "年龄",
sex: "性别"
};
// 获取Excel内数据
let exceldata = await service.common.excel.parseExcelFile(
stream,
headNameMap
);
if (exceldata.length <= 0) {
this.fail("Excel文件内容为空,操作失败");
await streamWormhole(stream);
return;
}
// exceldata就是上传的excel中根据headNameMap表头解析出来的数据,这里可以对这些数据去重,检查有效性,最后根据需求做相应的处理
ctx.body = exceldata;
await streamWormhole(stream);
} catch (error) {
logger.error(error);
ctx.body = "文件上传或解析失败";
await streamWormhole(stream);
return;
}
}
2、导出(下载)Excel
导出excel的方式有多种, 若是有固定的excel模板,
- ① 可将模板放到前端,前端自己创建a标签下载即可,
- ② 也可以将模板放到node,node通过读取excel文件后,将模板传给前端达到下载的目的,
- ③ 若是模板里面的字段是动态的,那么就需要node根据动态数据来生成excel
- 但是不管是那种方式,表头名称和字段的对应关系需要和解析excel里面的一致,否则再次上传后,无法解析出数据
js
const urlencode = require("urlencode");
async createExcelTpl() {
const { ctx, service } = this;
// 先获取所有表头列(根据需求来取到数据)
const colunmMap = await service.xxxx.getAllCustomColumnItem();
const columnConfig = [];
for (const key in colunmMap) {
if (Object.hasOwnProperty.call(colunmMap, key)) {
const header = colunmMap[key];
columnConfig.push({
key, // 表头对应的字段
header, // 表头的中文
width: 15, // 每一列宽度
style: { // 每一列表头样式
alignment: {
wrapText: true,
vertical: "left",
horizontal: "top"
}
}
});
}
}
const workbook = await service.common.excel.getWorkbook({
sheetName: "测试sheet",
columns: columnConfig,
rows: [] // 数据
});
// filename可以根据时间戳写成动态的,避免缓存
let filename = urlencode("测试excel导出.xlsx");
ctx.set("content-disposition",`attachment; filename* = UTF-8''${filename}`);
ctx.status = 200;
await workbook.xlsx.write(ctx.res);
ctx.res.end();
}
3. 用stream的形式导出Excel
上面记录的文件 I/O 需要在内存中建立整个工作簿,然后才能写入文件。虽然方便,但是由于所需的内存量,它可能会限制文档的大小。
流写入器(或读取器)在生成工作簿或工作表数据时对其进行处理,然后将其转换为文件形式。通常,这在内存上效率要高得多,因为最终的内存占用量,甚至中间的内存占用量都比文档版本要紧凑得多,尤其是当您考虑到行和单元格对象一旦提交就被销毁时,尤其如此。
也就是说,当需要导出或者读取的Excel数据量非常大的时候,流式I/O的性能会非常好,它可以让每一行数据写入excel中后立即销毁这一行数据,以减小内存的使用量。
下面是在express中导出Excel的示例;
js
const Excel = require("exceljs");
const urlencode = require("urlencode");
router.get("/export", async function (req, res) {
// 使用样式和共享字符串构造流式 XLSX 工作簿编写器
// const options = {
// stream:fs.createWriteStream('./excel.xlsx') // 指定要写入 XLSX 工作簿的可写流。
// filename: "./streamed-workbook.xlsx", // 如果未指定流,则此字段指定要写入 XLSX 工作簿的文件的路径。
// useStyles: true, // 指定是否将样式信息添加到工作簿。样式会增加一些性能开销。默认为 false
// useSharedStrings: true, // 指定是否在工作簿中使用共享字符串。默认为 false
// zip:undefined // ExcelJS 内部传递给 Archiver 的 Zip选项。默认值为 undefined
// };
// const workbook = new Excel.stream.xlsx.WorkbookWriter(options);
/*
如果在选项中未指定 stream 或 filename,则工作簿编写器将创建一个 StreamBuf 对象,该对象将 XLSX 工作簿的内容存储在内存中。
可以通过属性 workbook.stream 访问此 StreamBuf 对象,该对象可用于通过 stream.read() 直接访问字节,或将内容通过管道传输到另一个流。
*/
const workbook = new Excel.stream.xlsx.WorkbookWriter({
useStyles: true
}); // 创建工作簿对象
let worksheet = workbook.addWorksheet("My Sheet"); // 添加一个工作表
// 添加列
worksheet.columns = [
{ header: "Id", key: "id", width: 10, },
{ header: "Name", key: "name", width: 32 },
{ header: "D.O.B.", key: "DOB", width: 10, outlineLevel: 1 },
];
// 添加行
for (let i = 0; i < 1000000; i++) {
worksheet
.addRow({
id: i,
name: "name" + i,
DOB: "someOtherDetail" + i,
})
.commit(); // 添加完一行数据后,调用commit方法,将会释放这一行数据(销毁),以提高性能
}
// 完成添加数据。 提交工作表
await workbook.commit();
let filename = urlencode(`测试文件-${Date.now()}.xlsx`)
res.set("content-disposition", `attachment; filename* = UTF-8''${filename}`);
res.status = 200;
workbook.stream.pipe(res); // 利用管道流将创建的工作簿交给res以响应客户端
res.end();
})
用stream的方式读写Excel,能减少内存使用量,加快处理速度,在Exceljs里面,处理流的API与非流的API基本类似,具体说明见官方文档(流式I/O)
若是数据量很大的话,不建议直接将Excel响应前端,可以通过服务端生成Excel文件,然后上传到对象存储服务等位置,给到前端一个URL链接,让前端慢慢下载,并且设置一个定时任务,去删除该文件
4. stream示例
controller 公共方法
js
const moment = require('moment');
const urlencode = require('urlencode');
/**
* 响应excel下载
* @param {String} [options.filename] 文件名(支持带后缀和不带后缀)不传默认使用当前时间戳
* @param {String} [options.sheetName] 工作表名称 不传默认sheet1
* @param {String} [options.columns ] 列配置;
* @param {String} [options.rows] 行数据;
*
* 使用示例
* ```js
* await this.resExcelStream({
* sheetName: 'xxxxx',
* columns: [
* { header: '序号', key: 'index', width: 10 },
* { header: '内容', key: 'previousContent', width: 50 },
* { header: '操作结果', key: 'result', width: 50 },
* { header: '操作原因', key: 'reason', width: 30 },
* { header: '操作时间', key: 'insertTime', width: 30 }
* ],
* rows, // 数据行组成的数组
* filename: `日志.xlsx`
* });
* ```
*
*/
async resExcelStream({ filename = '', sheetName = '', columns = [], rows = [] }) {
const { ctx, service } = this;
try {
const workbook = await service.common.excel.getWorkBookStream({ sheetName, columns, rows });
if (filename) {
filename = filename.split('.xlsx')[0] + moment(Date.now()).format('YYYYMMDD-HHmmss') + '.xlsx';
} else {
filename = `${moment().format('YYYYMMDDHHmmss')}.xlsx`;
}
filename = urlencode(filename);
ctx.set('content-disposition', `attachment; filename* = UTF-8''${filename}`);
ctx.status = 200;
await workbook.stream.pipe(ctx.res);
ctx.res.end();
return;
} catch (error) {
ctx.status = 500;
ctx.message = '服务器错误';
this.fail('服务器错误');
ctx.logger.error(error);
return;
}
service
js
/**
* 用流的方式获取excel workbook对象,性能优于getWorkbook, 处理数据量较大excel导出更适合
* exceljs文档:https://github.com/exceljs/exceljs/blob/master/README_zh.md#%E6%B5%81%E5%BC%8F-io
*/
async getWorkBookStream({ sheetName = '', columns = [], rows = [] }) {
console.time('组装Excel所用时间');
let workbook = new Excel.stream.xlsx.WorkbookWriter({ useStyles: true }); //创建一个流式写入器 工作簿对象
workbook.creator = 'create';
workbook.created = new Date(Date.now());
workbook.modified = new Date(Date.now());
let sheet = workbook.addWorksheet(sheetName, { views: [{ state: 'frozen', xSplit: 0, ySplit: 1 }] }); // 冻结表头
sheet.columns = columns.map(item => {
return {
...item,
style: item.style || {
alignment: {
vertical: 'middle',
horizontal: 'center'
}
}
};
});
rows.forEach((row, index) => {
let rowData = {
...row,
index: index + 1
};
let newRow = sheet.addRow(rowData, 'i');
newRow.commit();
});
// 完成 workbook.
await workbook.commit();
// ... 流已被写入
console.timeEnd('组装Excel所用时间');
return workbook;
}
三. 处理 TXT 文本
1、上传解析txt,得到内容
同文件上传,获取内容可以通过插件concat-stream
js
const concat = require('concat-stream');
/**
*
* @param {File} stream 文件流对象
* @param {*} encoding 要返回的编码 encoding取值如下
* string - get a string (默认)
* buffer - get back a Buffer
* array - get an array of byte integers
* uint8array, u8, uint8 - get back a Uint8Array
* object, get back an array of Objects
*/
async getStreamData(stream, encoding = "string") {
return new Promise((resolve, reject) => {
stream.pipe(concat({ encoding }, data => {
resolve(data)
}))
stream.on('error', err => {
reject(err)
})
})
}
获取到的内容是一个String,根据特定的分隔符解析就能得到内容
2、下载txt
js
const moment = require('moment');
const urlencode = require('urlencode');
async downloadTxtTpl() {
try {
let filePath = path.resolve('.').replace(/\\/g, '/') + '/app/public/txt/demo.txt';
let txtContent = fs.createReadStream(filePath);
let filename = urlencode('xxx文本文件-' + moment(Date.now()).format('YYYYMMDD-HHmmss') + '.txt');
this.ctx.set('content-disposition', `attachment; filename* = UTF-8''${filename}`);
this.ctx.set('content-type', 'application/octet-stream');
this.ctx.status = 200;
this.ctx.body = txtContent;
} catch (e) {
console.log(e);
this.ctx.logger.error(e)
this.fail('系统错误')
}
}
3、nodeJS将buffer转换成为stream
js
const stream = require('stream);
let bufferStream = new stream.PassThrough();
bufferStream.end(new Buffer(['hello word']))
// bufferStream就是buffer转换成的stream,可以直接使用,例如
bufferStream.pipe(process.stdout)
4、egg解析csv文件
这里是直接解析的, github上也有一些线程的库,比如exceljs等
js
/**
* 解析简单的csv文件
* @param {Stream|String} stream csv流
* @param {Object} [options] 配置对象
* @param {String} [options.delimiter] 每行内数据分隔符,默认英文逗号",""
* @param {Number} [options.columnMaxCount] 列数
* @param {Array} [options.headerArr] 若有表头,则表头的列字段
* @param {Boolean} [options.trim] 默认值false,每个字段是否去除两边空格
* @returns {Object} {total, effectCsvArrData, invalidData}
*/
async parseCsvFile(stream, { delimiter = ",", columnMaxCount, headerArr, trim = false }) {
let csvStr = stream;
if (typeof stream !== 'string') {
csvStr = await this.service.common.excel.getStreamData(stream)
}
csvStr = csvStr.split(/\r?\n/)
let effectCsvArrData = [] // 最终有效的数据
let invalidData = [] // 每行字段不够的数据(无效)
let total = 0 // 总行数(不包括空行)
// 对csv文件进行有效性过滤
csvStr.forEach(item => {
total++
let itemArr = item.split(delimiter)
// 若规定了一行有几个字段的情况
if (columnMaxCount && itemArr.length < columnMaxCount) {
invalidData.push(item)
} else {
itemArr = itemArr.map(filed => {
if (filed) {
return filed.trim() || ""
} else {
return ""
}
})
effectCsvArrData.push(itemArr)
}
})
if (Array.isArray(headerArr)) {
effectCsvArrData = effectCsvArrData.map(item => {
return item.reduce((pre, cur, index) => {
let columnName = headerArr[index] || 'column-' + index
pre[columnName] = cur
return pre
}, {})
})
}
return {
total,
effectCsvArrData,
invalidData
}
}
5、stream转化成为string
js
const concat = require('concat-stream');
/**
*
* @param {File} stream 文件流对象
* @param {*} encoding 要返回的编码 encoding取值如下
* string - get a string (默认)
* buffer - get back a Buffer
* array - get an array of byte integers
* uint8array, u8, uint8 - get back a Uint8Array
* object, get back an array of Objects
*/
async getStreamData(stream, encoding = "string") {
return new Promise((resolve, reject) => {
stream.pipe(concat({ encoding }, data => {
resolve(data)
}))
stream.on('error', err => {
reject(err)
})
})
}
四、egg中同时使用mysql和mongodb遇到的问题
问题描述: 项目同时使用了mysql数据库和mongoDB数据库,mysql用的egg-sequelize来操纵的数据库,导致启动时候一直报错
查文档发现是由于 egg-sequelize 导致的,而部分业务接口之前配置了使用 MySQL 数据库。 egg-sequelize 和 egg-mongoose 默认都是加载 app/model 下的文件,所以导致加载失败
解决方案: 看是否可以修改插件的加载目录。egg-mongoose 是不能配置的,源码里面写死了指定的 model 路径, egg-sequelize 是可以的。
js
// 配置mysql
// delegate 表示将 orm 挂载的位置,之前默认是 model,修改完之后不要忘记业务代码中的已有代码(如this.ctx.model.xxx => this.ctx.myModel.xxx)。
sequelize: {
baseDir:"my_model", // 修改mysql的model目录,会与mongooDB冲突
delegate: "myModel", // 使用this.ctx.myModel.xxx来回去mysql模型
dialect: "mysql", // support: mysql, mariadb, postgres, mssql
database: "app_test",
host: "localhost",
port: 3306,
username: "root",
password: '123456',
},
五. egg封装基类
base Controller
js
"use strict";
const Controller = require("egg").Controller;
class BaseController extends Controller {
/**
* 成功响应
* @param {any}} data 响应数据
* @param {String} msg 成功提示
*/
success(data, msg = "操作成功") {
const { config } = this.app;
this.ctx.body = {
code: config.code.SUCCESS_CODE,
data,
msg
};
}
/**
* 失败响应
* @param {String} msg 失败原因
* @param {Number} code 失败状态码
*/
fail(msg = "操作失败", code) {
const { config } = this.app;
if (msg instanceof Error) {
msg = msg.message;
}
this.ctx.body = {
code: code || config.code.ERROR_CODE,
msg
};
}
/**
* 返回客户端无分页数据
* @param {Object} data 返回客户端的数据
*/
resNoPage(data, msg) {
let { app } = this;
if (data && data instanceof Error) {
this.fail(data.message || app.config.tip.SERVICE_ERROR);
return;
} else {
this.success(data, msg);
}
}
/**
* 返回客户端错误信息
* @param {Object} res 数据库得来的信息(可能是错误也可能是数据)
* @param {String} msg 当res不是错误信息时候,给客户端的错误提示信息
*/
resSalfError(res, msg) {
const { config } = this.app;
if (res instanceof Error) {
console.log("++++++++++++++++++++++++", res);
let errMsg = res.message;
this.fail(errMsg);
return;
}
this.fail(msg || config.tip.SERVICE_ERROR);
}
/**
* 返回分页数据
* @param {Object} data 返回分页数据
* @param {Object} page 默认的分页
* @param {*} other 其他字段
*/
resPageData(data, page, other) {
const { app } = this;
const { rows, count } = data;
const { pageSize, currentPage } = page;
const resData = {
records: rows,
total: count,
totalPage: Math.ceil(count / pageSize),
pageSize: pageSize,
currentPage: currentPage
};
if (other) {
resData.other = other;
}
if (data && data instanceof Error) {
this.fail(app.config.tip.SERVICE_ERROR);
return;
} else {
this.success(resData);
}
}
/**
* 校验参数,失败直接响应,通过返回true
* @param {Object} rule 校验规则对象
* @param {Object} params 客户端传过来的参数
* @returns Boolean
*/
validate(rule, params) {
try {
this.ctx.validate(rule, params);
return true;
} catch (error) {
let msg = this.ctx.helper.tip(error.errors[0], "PARAMS_ERROR");
this.fail(msg);
return false;
}
}
/**
* 为where条件加入模糊查询
* @param {Array} fields 要模糊查询的字段名组成的数组
* @param {Object} params 前端纯过来的有效参数
* @param {Boolean} throuthZero value为0时候是否添加模糊查询,默认不添加(用于判断类型为int的字段)
* @returns {Object} where
*/
addSequelizeOpLike(fields = [], params, throuthZero = false) {
const { Op } = this.app.Sequelize;
let obj = {};
fields.forEach(item => {
if (params[item] || (throuthZero && params[item] === 0)) {
obj[item] = { [Op.like]: `%${params[item]}%` };
}
});
return obj;
}
async hasDate(filter, model) {
try {
let result = await model.findOne(filter);
if (result instanceof Error) {
this.fail(this.config.tip.SERVICE_ERROR);
return true;
}
if (!result) {
return false;
}
if (result) {
return result;
}
} catch (error) {
this.fail(this.config.tip.SERVICE_ERROR);
return true;
}
}
}
module.exports = Base_controllerController;
base Service
js
"use strict";
const Service = require("egg").Service;
class BaseService extends Service {
/**
* 封装的Mysql处理数据库统一处理请求以及错误
* @param {...any} args (method params)
* @returns
*/
async safeSevice(...args) {
console.log("============MySQL============\n", args);
let [method, ...param] = args;
console.log(
"----------Mysql传入参数--------\n",
JSON.stringify(param, null, 2)
);
try {
let result = await this.myModel[method](...param);
// console.log("----------Mysql输出结果--------\n", result);
return result;
} catch (error) {
this.ctx.logger.error(error);
console.log("-------Mysql错误-------\n", error);
if (this.app.config.env === "prod") {
throw new Error("服务器错误");
}
throw error;
}
}
/**
* 封装的MongoDB查询数据库同统一处理错误
* @param {参数1} 数据库方法
* @param {参数2...} 传参
* @returns
*/
async salfeMongDB(...args) {
console.log("===========MongoDb=============\n", args);
let [method, ...param] = args;
console.log(
"----------MongoDb传入参数--------\n",
JSON.stringify(param, null, 2)
);
// console.log("------------", ...param);
try {
return await this.MongModel[method](...param);
} catch (error) {
console.log("-------MongDb错误-------\n", error);
throw new Error("服务器错误");
}
}
/**
* 插入数据Mysql
* @param {Object} params 要添加的数据
* @returns
*/
async cerate(params) {
return this.safeSevice("create", params);
}
/**
* 根据条件查询某个数据Mysql
* @param {Object} params 查询条件
* @returns
*/
async findOne(params) {
return this.safeSevice("findOne", params);
}
/**
* cha查询mysql所有数据
* @param {Object} params 查询条件
* @returns
*/
async findAll(params) {
return this.safeSevice("findAll", params);
}
/**
* 查询分页Mysql
* @param {Object} params 查询条件
*/
async findAndCountAll(params) {
return this.safeSevice("findAndCountAll", params);
}
/**
* 修改Mysql数据
* @param {Array} params 要更新的数据
* @param {Object} filter 查询条件
* @returns
*/
async update(params, filter) {
return this.safeSevice("update", params, filter);
}
/**
* 根据条件删除Mysql数据
* @param {Object} params 删除的查询条件
* @returns
*/
async destroy(params) {
return this.safeSevice("destroy", params);
}
/**
* Mysql批量更新
* 文档: https://itbilu.com/nodejs/npm/V1PExztfb.html#api-bulkCreate
* @param {Object} params 要更新的数据
* @param {Object} options 配置
* @returns
*/
async bulkCreate(params, options) {
return this.safeSevice("bulkCreate", params, options);
}
/**
* 根据主键查找Mysql
* @param {String} pk 主键值
* @returns
*/
async findByPk(pk) {
return this.safeSevice("findByPk", pk);
}
/**
* 添加mongoDb数据
* @param {Object} params 要添加的数据
* @returns
*/
async createMongodb(params) {
return this.salfeMongDB("create", params);
}
/**
* 查找mongodb里面的某个内容 findOne
* @param {Object} filter 查询条件
* @returns
*/
async findMongdbOne(filter) {
return this.salfeMongDB("findOne", filter);
}
/**
* 编辑mongoDb里的文章信息 update
* @param {Object} filter 查询条件{_id:"q232323x"}
* @param {Object} params 编辑的内容 {xxx: 'sfddssd'}
*/
async editMongodb(filter, params) {
return this.salfeMongDB("update", filter, params);
}
/**
* 根据条件删除mongodb数据
* @param {Object} params 查询参数
* @returns
*/
async delMongodb(params) {
return this.salfeMongDB("remove", params);
}
}
module.exports = Base_serviceService;
六. 封装一个导出Excel的service
js
const Service = require('egg').Service;
const Excel = require('exceljs');
const concat = require('concat-stream');
function getcellVal(obj) {
if (Object.prototype.toString.call(obj) === '[object Object]' && Array.isArray(obj.richText)) {
return obj.richText.reduce((pre, cur) => {
pre += cur.text;
return pre;
}, '').trim().replace('/\\/g', '\\\\');
} else if (Object.prototype.toString.call(obj) === '[object Object]' && 'text' in obj && 'hyperlink' in obj) {
return obj.text.trim().replace('/\\/g', '\\\\');
} else if (typeof obj === 'string' && obj) {
return obj.trim().replace('/\\/g', '\\\\');
} else {
return obj;
}
}
class ExcelService extends Service {
/**
*负责解析excel,以头部为键,生成数组
* @param {stream} stream excel流
* @param {Object} headNameMap 头部名称和键的索引
* 例如:
* const headNameMap = {
adId: '广告ID',
ownerId: '广告主ID',
targetId: '定向包ID',
};
*/
async parseExcel(stream, headNameMap) {
const workbook = new Excel.Workbook();
await workbook.xlsx.read(stream);
const worksheet = workbook.getWorksheet(1);
const data = [];
const columnCount = worksheet.columnCount;
for (let c = 1; c <= columnCount; c++) {
const col = worksheet.getColumn(c);
Object.entries(headNameMap).forEach(([key, value]) => {
let colHeader = getcellVal(col.values[1]);
if (!colHeader && typeof colHeader !== 'string') return;
if (colHeader === value) {
const colValues = col.values.slice(2);
colValues.forEach((val, index) => {
let item = {};
if (data[index]) {
item = data[index];
} else {
data[index] = item;
}
item[key] = getcellVal(val);
});
}
});
}
return data;
}
/**
* 获取excel workbook对象,具体用法见controller/sys_manage/log_manage.js > function export
* exceljs文档:https://github.com/exceljs/exceljs/blob/master/README_zh.md
*/
getWorkbook({ sheetName = '', columns = [], rows = [] }) {
let workbook = new Excel.Workbook();
workbook.creator = 'createUser';
workbook.created = new Date(Date.now());
workbook.modified = new Date(Date.now());
let sheet = workbook.addWorksheet(sheetName);
sheet.columns = columns.map(item => {
return {
...item,
style: item.style || {
alignment: {
vertical: 'middle',
horizontal: 'center'
}
}
};
});
rows.forEach((row, index) => {
let rowData = {
...row,
index: index + 1
};
sheet.addRow(rowData);
});
return workbook;
}
/**
* 用流的方式获取excel workbook对象,性能优于getWorkbook, 处理数据量较大excel导出更适合
* exceljs文档:https://github.com/exceljs/exceljs/blob/master/README_zh.md#%E6%B5%81%E5%BC%8F-io
*/
async getWorkBookStream({ sheetName = '', columns = [], rows = [] }) {
console.time('组装Excel所用时间');
let workbook = new Excel.stream.xlsx.WorkbookWriter({
useStyles: true
});//创建一个流式写入器
workbook.creator = 'CreateUser';
workbook.created = new Date(Date.now());
workbook.modified = new Date(Date.now());
let sheet = workbook.addWorksheet(sheetName, { views: [{ state: 'frozen', xSplit: 0, ySplit: 1 }] }); // 表头固定
sheet.columns = columns.map(item => {
return {
...item,
style: item.style || {
alignment: {
vertical: 'middle',
horizontal: 'center'
}
}
};
});
rows.forEach((row, index) => {
let rowData = {
...row,
index: index + 1
};
let newRow = sheet.addRow(rowData, 'i');
newRow.commit();
});
// 完成 workbook.
await workbook.commit();
// ... 流已被写入
console.timeEnd('组装Excel所用时间');
return workbook;
}
/**
*
* @param {File} stream 文件流对象
* @param {*} encoding 要返回的编码 encoding取值如下
* string - get a string (默认)
* buffer - get back a Buffer
* array - get an array of byte integers
* uint8array, u8, uint8 - get back a Uint8Array
* object, get back an array of Objects
*/
async getStreamData(stream, encoding = 'string') {
return new Promise((resolve, reject) => {
stream.pipe(concat({ encoding }, data => {
resolve(data);
}));
stream.on('error', err => {
reject(err);
});
});
}
// 流式解析Excel数据
async parseExcelStream(stream, headNameMap, option) {
// sheel为第几个sheet
let { sheet = 1 } = option || {};
const workbook = new Excel.stream.xlsx.WorkbookReader(stream);
let data = [];
let headSource = Object.values(headNameMap);
let start = 1;
for await (const worksheetReader of workbook) {
if (sheet !== start) {
start++;
continue;
}
let index = 0;
let headKeys = [];
for await (const row of worksheetReader) {
let rowArr = row.values.slice(1);
if (index === 0) {
rowArr.forEach(item => {
if (headSource.includes(item)) {
Object.entries(headNameMap).forEach(([key, h]) => {
if (getcellVal(item) === h) {
headKeys.push(key);
}
});
} else {
headKeys.push(null);
}
});
index += 1;
} else {
let itemObj = {};
headKeys.forEach((item, idx) => {
if (item !== null) {
const val = rowArr[idx];
itemObj[item] = val === undefined ? null : getcellVal(val);
}
});
data.push(itemObj);
index += 1;
}
}
start++;
}
return data;
}
/**
* 将excel解析得到的 45438.5 日期时间转化为 正确的时间格式, 例如: yyyy-mm-dd hh:mm:ss
* @param {Number} excelDateNum excel解析得到的时间值, 一般为小数或整数
* @param {*} option 格式化配置 可选
* @param {String} option.type 可选值'string', 'timeStamp' 默认 'string'
* @param {String} option.format moment中的format所需参数, 默认YYYY-MM-DD HH:mm:ss type=== 'string'时生效
* @param {Boolean} option.isMillisecond 时间戳是否以毫秒为单位, 默认false type === 'timeStamp'是生效
* @returns
*/
excelDateToString(excelDateNum, option) {
try {
const { type = 'string', format = 'YYYY-MM-DD HH:mm:ss', isMillisecond = false } = option || {};
if (!['string', 'timeStamp'].includes(type)) throw new Error('excelDateToString 参数 option.type错误, 仅支持 "string" 或 "timeStamp"');
// Excel 中的基准日期是 1899 年 12 月 30 日(注意 Excel 认为 1900 年是闰年)
const excelEpoch = new Date(Date.UTC(1899, 11, 30)); // 注意:月份是从 0 开始的,所以 11 表示 12 月
const jsMilliseconds = excelDateNum * 86400000 + excelEpoch.getTime(); // 减 2 是因为 Excel 的日期从 1 开始,而 JavaScript 从 0 开始
const jsDate = new Date(jsMilliseconds);
const offset = 8 * 60 * 60 * 1000; // UTC+8 小时的时区偏移量
const time = jsDate - offset;
if (type === 'timeStamp') {
if (isMillisecond) return time;
return parseInt(time / 1000, 10);
}
return moment(time).format(format);
} catch (error) {
throw error;
}
}
}
module.exports = ExcelService;