Skip to content

Egg在开发中一些常用功能

一. 文件上传

Egg文件上传,官方提供了两方式:filestream;开发中可以根据自生需要,选择合适的方式进行文件上传。文件上传官方示例。 但是,更推荐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;