浏览代码

指标模板,上传Excel文件,解析得到指标节点和指标

MaiXinRong 7 年之前
父节点
当前提交
11a8c3b5e8

+ 32 - 1
app/controller/template_controller.js

@@ -8,6 +8,9 @@
  * @version
  */
 
+// excel解析
+const excel = require('node-xlsx');
+const sendToWormhole = require('stream-wormhole');
 module.exports = app => {
     class TemplateController extends app.BaseController {
         /**
@@ -17,13 +20,41 @@ module.exports = app => {
          * @return {void}
          */
         async index (ctx) {
-            const node = await ctx.service.templateNode.getAllDataByCondition({template_id: 1});;
+            const node = await ctx.service.templateNode.getAllDataByCondition({template_id: 1});
             const treeNode = ctx.helper.convertData(node, true, 'node_id', 'node_pid');
             const renderData = {
                 nodes: JSON.stringify(treeNode),
             }
             await this.layout('template/index.ejs', renderData, 'template/modal.ejs');
         }
+
+        async uploadExcel(ctx) {
+            let stream;
+            try {
+                stream = await ctx.getFileStream();
+                const fileName = this.app.baseDir + '/app/public/template/uploads/' + stream.filename;
+                // 保存文件
+                await ctx.helper.saveStreamFile(stream, fileName);
+                // 读取文件
+                const sheet = excel.parse(fileName);
+                if (!sheet || sheet.length === 0 || sheet[0] === undefined || sheet[0].data === undefined) {
+                    throw 'excel没有对应数据';
+                }
+                const result = await ctx.service.templateNode.importData(sheet);
+                if (!result) {
+                    throw '导入数据失败';
+                }
+                ctx.redirect('/template');
+            } catch (err) {
+                console.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                //this.setMessage(err.toString(), this.messageType.ERROR);
+                ctx.redirect('/template');
+            }
+        }
     }
 
     return TemplateController;

+ 71 - 3
app/extend/helper.js

@@ -1,3 +1,16 @@
+'use strict';
+
+/**
+ * 指标模板控制器
+ *
+ * @author Mai
+ * @data 2018/4/19
+ * @version
+ */
+
+const fs = require('fs');
+const streamToArray = require('stream-to-array');
+
 module.exports = {
     /**
      * 转换数据
@@ -31,7 +44,6 @@ module.exports = {
 
         return tree ? rootData : listData;
     },
-
     /**
      * 递归组织数组结构数据
      *
@@ -56,7 +68,6 @@ module.exports = {
             }
         }
     },
-
     /**
      * 递归组织树结构数据
      *
@@ -77,10 +88,67 @@ module.exports = {
                 // 已用的子项删除
                 delete childData[parent[index][id]];
 
-                this._recursionTreeData(tmpParent, childData);
+                this._recursionTreeData(tmpParent, childData, id);
                 // 递归完赋值回原数据
                 parent[index].children = tmpParent;
             }
         }
+    },
+
+    /**
+     * 将文件流的数据保存至本地文件
+     * @param stream
+     * @param fileName
+     * @returns {Promise<void>}
+     */
+    async saveStreamFile(stream, fileName) {
+        // 读取字节流
+        const parts = await streamToArray(stream);
+        // 转化为buffer
+        const buffer = Buffer.concat(parts);
+        // 写入文件
+        await fs.writeFileSync(fileName, buffer);
+    },
+
+    /**
+     * 检查code是否是指标模板数据
+     * @param {String} code
+     * @returns {boolean}
+     */
+    ValidTemplateCode(code) {
+        const reg1 = /(^[a-z]+)([a-z0-9\-]*)/i;
+        const reg2 = /([a-z0-9]+$)/i;
+        return reg1.test(code) && reg2.test(code);
+    },
+    /**
+     * 检查code 是否是 指标节点编号
+     * @param {String} code
+     * @returns {boolean}
+     */
+    ValidTemplateNodeCode(code) {
+        const reg1 = /(^[a-z]+)([a-z0-9\-]*)/i;
+        const reg2 = /([a-z0-9]+$)/i;
+        const reg3 = /([\-][0-9]+$)/i;
+        return reg1.test(code) && reg2.test(code) && (!reg3.test(code));
+    },
+    /**
+     * 检查code 是否是 指标节点编号
+     * @param {String} code
+     * @returns {boolean}
+     */
+    ValidTemplateIndexCode(code) {
+        const reg1 = /(^[a-z]+)([a-z0-9\-]*)/i;
+        const reg2 = /([0-9]+$)/i;
+        return reg1.test(code) && reg2.test(code);
+    },
+
+    findObj(arr, field, value) {
+        if (arr.length === 0) { return undefined; }
+        for (const a of arr) {
+            if (a[field] && a[field] === value) {
+                return a;
+            }
+        }
+        return undefined;
     }
 }

+ 1 - 0
app/router.js

@@ -19,6 +19,7 @@ module.exports = app => {
 
     // 指标模板
     app.get('/template', sessionAuth, 'templateController.index');
+    app.post('/template/uploadExcel', sessionAuth, 'templateController.uploadExcel');
 
     // 指标对比
     app.get('/compare', sessionAuth, 'compareController.index');

+ 82 - 1
app/service/template_node.js

@@ -1,7 +1,7 @@
 'use strict';
 
 /**
- * 用户管理业务类
+ * 指标节点业务类
  *
  * @author Mai
  * @date 2018/4/19
@@ -22,6 +22,87 @@ module.exports = app => {
             this.tableName = 'template_node';
         }
 
+        _findParentId(code, nodes) {
+            if (nodes.length === 0) { return -1; }
+            const codeList = code.split('-');
+            if (codeList.length > 1) {
+                codeList.splice(codeList.length - 1);
+                const parentCode = codeList.join('-');
+                for (const node of nodes) {
+                    if (parentCode === node.code) {
+                        return node.node_id;
+                    }
+                }
+            } else {
+                for (const node of nodes) {
+                    if (code.search(node.code) === 0) {
+                        return node.node_id;
+                    }
+                }
+            }
+        }
+        _parseSheetData(excelSheet, nodes, indexes) {
+            for (const row of excelSheet.data) {
+                if (!row[0]) { continue; }
+                if (this.ctx.helper.ValidTemplateNodeCode(row[0])) {
+                    if (!this.ctx.helper.findObj(nodes, 'code', row[0])) {
+                        const node = {
+                            template_id: 1,
+                            node_id: nodes.length + 1,
+                            node_pid: this._findParentId(row[0], nodes),
+                            code: row[0],
+                            name: row[1],
+                        };
+                        nodes.push(node);
+                    }
+                } else if (this.ctx.helper.ValidTemplateIndexCode(row[0])) {
+                    const index = {
+                        code: row[0],
+                        name: row[1],
+                        unit1: row[2],
+                        unit2: row[3],
+                        node_id: nodes.length,
+                    };
+                    if (row[5] === '√') {
+                        index.indexType = 1;
+                    } else if (row[6] === '√') {
+                        index.indexType = 2;
+                    } else if (row[7] === '√') {
+                        index.indexType = 3;
+                    } else if (row[8] === '√') {
+                        index.indexType = 4;
+                    }
+                    indexes.push(index);
+                }
+            }
+        }
+
+        async importData(excelSheets) {
+            let result = false;
+            const limit = 30000;
+            const transaction = await this.db.beginTransaction();
+            try {
+                const nodes = [], indexes = [];
+                for (const sheet of excelSheets) {
+                    this._parseSheetData(sheet, nodes, indexes);
+                }
+
+                if (nodes.length > 0) {
+                    await transaction.delete(this.tableName, {template_id: 1});
+                    const insertResult = await transaction.insert(this.tableName, nodes);
+                    result = insertResult.affectedRows === nodes.length;
+                } else {
+                    throw 'Excel文件中无标准的指标数据';
+                }
+
+                await transaction.commit();
+            } catch(err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return result;
+        }
+
     }
 
     return TemplateNode;

+ 18 - 4
app/view/template/index.ejs

@@ -7,7 +7,7 @@
     </div>
     <div class="scrollbar-auto">
         <div class="nav-box">
-            <ul id="treeDemo" class="ztree"></ul>
+            <ul id="templateNode" class="ztree"></ul>
         </div>
     </div>
 </div>
@@ -44,12 +44,26 @@
 </div>
 <script type="text/javascript">
     const treeSetting = {
-        view: {showIcon: false}
+        view: {showIcon: false},
+        data: {
+            key: {
+                name: 'text',
+            }
+        }
     };
     const treeNode = '<%- nodes %>';
-    const treeNodeData = treeNode !== '' ? JSON.parse(treeNode) : [];
 
     $(document).ready(function(){
-        $.fn.zTree.init($("#treeDemo"), treeSetting, JSON.parse(treeNode));
+        const loadText = function (arr) {
+            for (const a of arr) {
+                a.text = a.code + ' ' + a.name;
+                if (a.children && a.children.length > 0) {
+                    loadText(a.children);
+                }
+            }
+        }
+        const treeNodeData = treeNode !== '' ? JSON.parse(treeNode) : [];
+        loadText(treeNodeData);
+        $.fn.zTree.init($("#templateNode"), treeSetting, treeNodeData);
     });
 </script>

+ 6 - 4
app/view/template/modal.ejs

@@ -40,7 +40,7 @@
 </div>
 <!-- 导入项目节 -->
 <div id="upload" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
-    <div class="modal-dialog">
+    <form class="modal-dialog" action="/template/uploadExcel?_csrf=<%= ctx.csrf %>" method="post" enctype="multipart/form-data">
         <div class="modal-content">
             <div class="modal-header">
                 <h5 class="modal-title">导入项目节</h5>
@@ -49,13 +49,15 @@
             <div class="modal-body">
                 <div class="form-group">
                     <label for="exampleFormControlFile1">上传项目节文件</label>
-                    <div class="form-control"><input class="form-control-file" id="exampleFormControlFile1" type="file"></div>
+                    <div class="form-control">
+                        <input class="form-control-file" name="file" type="file">
+                    </div>
                 </div>
             </div>
             <div class="modal-footer">
-                <button class="btn btn-primary">确认导入</button>
+                <button type="submit" class="btn btn-primary">确认导入</button>
                 <button class="btn btn-secondary" data-dismiss="modal" aria-hidden="true">关闭</button>
             </div>
         </div>
-    </div>
+    </form>
 </div>

+ 6 - 0
config/config.default.js

@@ -50,5 +50,11 @@ module.exports = appInfo => {
     // add your config here
     config.middleware = ['urlParse'];
 
+    // 上传设置
+    config.multipart = {
+        whitelist: ['.xls', '.xlsx'],
+        fileSize: '10mb',
+    };
+
     return config;
 };

+ 6 - 0
config/config.local.js

@@ -50,5 +50,11 @@ module.exports = appInfo => {
     // add your config here
     config.middleware = ['urlParse'];
 
+    // 上传设置
+    config.multipart = {
+        whitelist: ['.xls', '.xlsx'],
+        fileSize: '10mb',
+    };
+
     return config;
 };

+ 6 - 0
config/config.qa.js

@@ -50,5 +50,11 @@ module.exports = appInfo => {
     // add your config here
     config.middleware = ['urlParse'];
 
+    // 上传设置
+    config.multipart = {
+        whitelist: ['.xls', '.xlsx'],
+        fileSize: '10mb',
+    };
+
     return config;
 };

+ 6 - 4
package.json

@@ -10,7 +10,9 @@
     "egg-scripts": "^2.5.0",
     "egg-view": "^2.1.0",
     "egg-view-ejs": "^2.0.0",
-    "node-xlsx": "^0.12.0"
+    "node-xlsx": "^0.12.0",
+    "stream-to-array": "^2.3.0",
+    "stream-wormhole": "^1.0.3"
   },
   "devDependencies": {
     "autod": "^3.0.1",
@@ -28,9 +30,9 @@
   "scripts": {
     "start": "egg-scripts start --daemon --title=egg-server-index_sys",
     "stop": "egg-scripts stop --title=egg-server-index_sys",
-    "dev": "egg-bin dev",
-    "dev-qa": "set EGG_SERVER_ENV=qa && egg-bin dev",
-    "dev-local": "set EGG_SERVER_ENV=local && egg-bin dev",
+    "dev": "egg-bin dev --port 7003",
+    "dev-qa": "set EGG_SERVER_ENV=qa && egg-bin dev --port 7003",
+    "dev-local": "set EGG_SERVER_ENV=local && egg-bin dev --port 7003",
     "debug": "egg-bin debug",
     "test": "npm run lint -- --fix && npm run test-local",
     "test-local": "egg-bin test",

+ 26 - 0
test/app/extend/helper.test.js

@@ -0,0 +1,26 @@
+'use strict';
+
+/**
+ * 辅助方法扩展 单元测试
+ *
+ * @author Mai
+ * @data 2018/4/20
+ * @version
+ */
+
+const { app, assert } = require('egg-mock/bootstrap');
+
+describe('test/app/extend/helper.test.js', () => {
+    it('ValidTemplateCode test', function () {
+        const ctx = app.mockContext();
+        assert(ctx.helper.ValidTemplateCode('z') === true);
+        assert(ctx.helper.ValidTemplateCode('Z') === true);
+        assert(ctx.helper.ValidTemplateCode('-') === false);
+        assert(ctx.helper.ValidTemplateCode('1') === false);
+        assert(ctx.helper.ValidTemplateCode('z1') === true);
+        assert(ctx.helper.ValidTemplateCode('z1-') === false);
+        assert(ctx.helper.ValidTemplateCode('zav') === true);
+        assert(ctx.helper.ValidTemplateCode('z1-e') === true);
+        assert(ctx.helper.ValidTemplateCode('z1-e-1') === true);
+    });
+});

+ 27 - 0
test/app/service/template_node.test.js

@@ -0,0 +1,27 @@
+'use strict';
+
+/**
+ * 指标节点业务逻辑 单元测试
+ *
+ * @author Mai
+ * @data 2018/4/20
+ * @version
+ */
+
+const { app, assert } = require('egg-mock/bootstrap');
+// excel解析
+const excel = require('node-xlsx');
+
+describe('test/app/service/template_node.test.js', () => {
+    it('_parseSheetData test', function () {
+        const ctx = app.mockContext();
+        const fileName = app.baseDir + '/test/app/service/test.xls';
+        const sheets = excel.parse(fileName);
+        const nodes = [], indexes = [];
+        for (const sheet of sheets) {
+            ctx.service.templateNode._parseSheetData(sheet, nodes, indexes);
+        }
+        assert(nodes.length === 5);
+        assert(indexes.length === 42);
+    });
+});