Browse Source

台账分解,从标准库添加数据
1. 简单添加 2. 按树结构添加

MaiXinRong 7 năm trước cách đây
mục cha
commit
351b5c3ccd

+ 59 - 1
app/controller/ledger_controller.js

@@ -8,6 +8,11 @@
  * @version
  */
 
+const stdDataAddType = {
+    self: 1,
+    withParent: 2
+}
+
 module.exports = app => {
 
     class LedgerController extends app.BaseController {
@@ -83,7 +88,7 @@ module.exports = app => {
                     throw '当前未打开标段';
                 }
                 const data = JSON.parse(ctx.request.body.data);
-                const id = data.id;
+                const id = data.ledger_id;
                 if (isNaN(id) || id <= 0) {
                     throw '参数错误';
                 }
@@ -230,6 +235,59 @@ module.exports = app => {
         }
 
         /**
+         * 从标准项目表添加数据
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async addFromStandardLib(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: [],
+            };
+            try {
+                const tenderId = ctx.session.sessionUser.tenderId;
+                if (!tenderId) {
+                    throw '当前未打开标段';
+                }
+                const data = JSON.parse(ctx.request.body.data);
+                if ((isNaN(data.id) || data.id <= 0) || !data.stdType || !data.stdNode) {
+                    throw '参数错误';
+                }
+                //todo 校验项目是否使用该库的权限
+
+                let stdLib;
+                switch (data.stdType) {
+                    case 'chapter':
+                        stdLib = ctx.service.stdChapter;
+                        break;
+                    case 'bills':
+                        stdLib = ctx.service.stdBills;
+                        break;
+                    default:
+                        throw '未知标准库';
+                }
+                const stdData = await stdLib.getDataByDataId(data.stdLibId, data.stdNode);
+                const addType = stdDataAddType.withParent;
+                switch (addType) {
+                    case stdDataAddType.self:
+                        responseData.data = await ctx.service.ledger.addStdNode(tenderId, data.id, stdData);
+                        break;
+                    case stdDataAddType.withParent:
+                        responseData.data = await ctx.service.ledger.addStdNodeWithParent(tenderId, stdData, stdLib);
+                        break;
+                    default:
+                        throw '未知添加方法';
+                }
+            } catch (err) {
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+
+            ctx.body = responseData;
+        }
+
+        /**
          * 台账变更页面
          *
          * @param {object} ctx - egg全局变量

+ 72 - 0
app/extend/helper.js

@@ -223,6 +223,78 @@ module.exports = {
     },
 
     /**
+     * 比较编码
+     * @param str1
+     * @param str2
+     * @param symbol
+     * @returns {number}
+     */
+    compareCode(str1, str2, symbol = '-') {
+        if (!str1) {
+            return -1;
+        } else if (!str2) {
+            return 1;
+        }
+
+        const path1 = str1.split(symbol);
+        const path2 = str2.split(symbol);
+        for (let i = 0, iLen = Math.min(path1.length, path2.length); i < iLen; i++) {
+            if (path1 < path2) {
+                return -1;
+            } else if (path1 > path2) {
+                return 1;
+            }
+        }
+        return path1.length - path2.length;
+    },
+
+    /**
+     * 树结构节点排序,要求最顶层节点须在同一父节点下
+     * @param treeNodes
+     * @param idField
+     * @param pidField
+     */
+    sortTreeNodes (treeNodes, idField, pidField) {
+        const result = [];
+        const getFirstLevel = function (nodes) {
+            let result;
+            for (const node of nodes) {
+                if (!result || result > node.level) {
+                    result = node.level;
+                }
+            }
+            return result;
+        }
+        const getLevelNodes = function (nodes, level) {
+            const children = nodes.filter(function (a) {
+                return a.level = level;
+            });
+            children.sort(function (a, b) {
+                return a.order - b.order;
+            })
+            return children;
+        }
+        const getChildren = function (nodes, node) {
+            const children = nodes.filter(function (a) {
+                return a[pidField] = node[idField];
+            });
+            children.sort(function (a, b) {
+                return a.order - b.order;
+            })
+            return children;
+        }
+        const addSortNodes = function (nodes) {
+            for (let i = 0; i< nodes.length; i++) {
+                result.push(nodes[i]);
+                addSortNodes(getChildren(nodes[i]));
+            }
+        }
+
+        const firstLevel = getFirstLevel(treeNodes);
+        addSortNodes(getLevelNodes(treeNodes, firstLevel));
+    },
+
+    /**
      * 判断当前用户是否有指定权限
      *
      * @param {Number|Array} permission - 权限id

+ 82 - 56
app/public/js/ledger.js

@@ -62,6 +62,50 @@ $(document).ready(function() {
             setObjEnable($('#up-level'), node && tree.getParent(node));
             setObjEnable($('#down-level'), node && node.order > 1);
         },
+        refreshTree: function (sheet, data) {
+            SpreadJsObj.massOperationSheet(sheet, function () {
+                const tree = sheet.zh_tree;
+                // 处理删除
+                if (data.delete) {
+                    for (const d of data.delete) {
+                        sheet.deleteRows(tree.nodes.indexOf(d), 1);
+                    }
+                }
+                // 处理新增
+                if (data.create) {
+                    const newNodes = data.create;
+                    if (newNodes) {
+                        newNodes.sort(function (a, b) {
+                            const aIndex = tree.nodes.indexOf(a);
+                            const bIndex = tree.nodes.indexOf(b);
+                            return aIndex - bIndex;
+                        });
+                        for (const node of newNodes) {
+                            const index = tree.nodes.indexOf(node);
+                            sheet.addRows(index, 1);
+                            SpreadJsObj.reLoadRowData(sheet, index, 1);
+                        }
+                    }
+                }
+                // 处理更新
+                if (data.update) {
+                    const rows = [];
+                    for (const u of data.update) {
+                        rows.push(tree.nodes.indexOf(u));
+                    }
+                    SpreadJsObj.reLoadRowsData(sheet, rows);
+                }
+                // 处理展开
+                if (data.expand) {
+                    for (const e of data.expand) {
+                        const children = tree.getChildren(e);
+                        for (const child of children) {
+                            sheet.setRowVisible(tree.nodes.indexOf(child), child.visible);
+                        }
+                    }
+                }
+            });
+        },
         /**
          * 新增节点
          * @param spread
@@ -79,18 +123,7 @@ $(document).ready(function() {
 
             SpreadJsObj.massOperationSheet(sheet, function () {
                 tree.baseOperation('base-operation', node, 'add', function (result) {
-                    const newNodes = result.create;
-                    if (newNodes) {
-                        newNodes.sort(function (a, b) {
-                            const aIndex = tree.nodes.indexOf(a);
-                            const bIndex = tree.nodes.indexOf(b);
-                            return aIndex - bIndex;
-                        });
-                        for (const node of newNodes) {
-                            const index = tree.nodes.indexOf(node);
-                            sheet.addRows(index, 1);
-                        }
-                    }
+                    self.refreshTree(sheet, result);
                     self.refreshOperationValid(sheet, sheet.getSelections());
                 });
             });
@@ -186,11 +219,7 @@ $(document).ready(function() {
             if (!node) { return; }
 
             tree.baseOperation('base-operation', node, 'up-level', function (result) {
-                const rows = [];
-                for (const u of result.update) {
-                    rows.push(tree.nodes.indexOf(u));
-                }
-                SpreadJsObj.reLoadRowsData(sheet, rows);
+                self.refreshTree(sheet, result);
                 self.refreshOperationValid(sheet, sheet.getSelections());
             });
 
@@ -211,11 +240,7 @@ $(document).ready(function() {
             if (!node) { return; }
 
             tree.baseOperation('base-operation', node, 'down-level', function (result) {
-                const rows = [];
-                for (const u of result.update) {
-                    rows.push(tree.nodes.indexOf(u));
-                }
-                SpreadJsObj.reLoadRowsData(sheet, rows);
+                self.refreshTree(sheet, result);
                 self.refreshOperationValid(sheet, sheet.getSelections());
             });
         },
@@ -324,29 +349,8 @@ $(document).ready(function() {
             if (!node) { return; }
 
             tree.pasteBlock('paste-block', node, block, function (result) {
-                SpreadJsObj.massOperationSheet(sheet, function () {
-                    // 新增
-                    const newNodes = result.create;
-                    if (newNodes) {
-                        newNodes.sort(function (a, b) {
-                            const aIndex = tree.nodes.indexOf(a);
-                            const bIndex = tree.nodes.indexOf(b);
-                            return aIndex - bIndex;
-                        });
-                        for (const node of newNodes) {
-                            const index = tree.nodes.indexOf(node);
-                            sheet.addRows(index, 1);
-                            SpreadJsObj.reLoadRowData(sheet, index, 1);
-                        }
-                    }
-                    // 更新
-                    const rows = [];
-                    for (const data of result.update) {
-                        rows.push(tree.nodes.indexOf(data));
-                    }
-                    SpreadJsObj.reLoadRowsData(sheet, rows);
-                    self.refreshOperationValid(sheet, sheet.getSelections());
-                });
+                self.refreshTree(sheet, result);
+                self.refreshOperationValid(sheet, sheet.getSelections());
             });
         }
     };
@@ -492,7 +496,7 @@ $(document).ready(function() {
             $('.tab-content .tab-pane').hide();
             tabPanel.show();
             if (tab.attr('content') === '#std-chapter' && !stdChapter) {
-                stdChapter = new stdLib($('#std-chapter-spread')[0], '/std/chapter', {
+                stdChapter = new stdLib($('#std-chapter-spread')[0], 'chapter', {
                     id: 'chapter_id',
                     pid: 'pid',
                     order: 'order',
@@ -501,16 +505,16 @@ $(document).ready(function() {
                     keys: ['id', 'list_id', 'chapter_id'],
                 }, {
                     cols: [
-                        {title: '项目节编号', field: 'code', width: 120, cellType: 'tree'},
-                        {title: '名称', field: 'name', width: 230},
-                        {title: '单位', field: 'unit', width: 50}
+                        {title: '项目节编号', field: 'code', width: 120, readOnly: true, cellType: 'tree'},
+                        {title: '名称', field: 'name', width: 230, readOnly: true},
+                        {title: '单位', field: 'unit', width: 50, readOnly: true}
                     ],
                     treeCol: 0,
                     emptyRows: 0
                 });
                 stdChapter.loadLib(1);
             } else if (tab.attr('content') === '#std-bills' && !stdBills) {
-                stdBills = new stdLib($('#std-bills-spread')[0], '/std/bills', {
+                stdBills = new stdLib($('#std-bills-spread')[0], 'bills', {
                     id: 'bill_id',
                     pid: 'pid',
                     order: 'order',
@@ -519,9 +523,9 @@ $(document).ready(function() {
                     keys: ['id', 'list_id', 'bill_id']
                 }, {
                     cols: [
-                        {title: '清单编号', field: 'code', width: 120, cellType: 'tree'},
-                        {title: '名称', field: 'name', width: 230},
-                        {title: '单位', field: 'unit', width: 50}
+                        {title: '清单编号', field: 'code', width: 120, readOnly: true, cellType: 'tree'},
+                        {title: '名称', field: 'name', width: 230, readOnly: true},
+                        {title: '单位', field: 'unit', width: 50, readOnly: true}
                     ],
                     treeCol: 0,
                     emptyRows: 0
@@ -537,14 +541,36 @@ $(document).ready(function() {
     });
 
     class stdLib {
-        constructor(obj, url, treeSetting, spreadSetting) {
+        constructor(obj, stdType, treeSetting, spreadSetting) {
             this.obj = obj;
-            this.url = url;
+            this.url = '/std/' + stdType;
             this.treeSetting = treeSetting;
-            treeSetting.preUrl = url;
+            treeSetting.preUrl = this.url;
             this.spreadSetting = spreadSetting;
             this.spread = SpreadJsObj.createNewSpread(this.obj);
             SpreadJsObj.initSheet(this.spread.getActiveSheet(), this.spreadSetting);
+            this.spread.getActiveSheet().bind(GC.Spread.Sheets.Events.CellDoubleClick, function (e, info) {
+                const stdSheet = info.sheet;
+                const mainSheet = ledgerSpread.getActiveSheet();
+                if (!stdSheet.zh_setting || !stdSheet.zh_tree || !mainSheet.zh_tree) { return; }
+
+                const stdTree = stdSheet.zh_tree;
+                const stdNode = stdTree.nodes[info.row];
+                const mainTree = mainSheet.zh_tree;
+                const sel = mainSheet.getSelections()[0];
+                const mainNode = mainTree.nodes[sel.row];
+                if (!stdNode) { return; }
+
+                mainTree.postData('add-by-std', mainNode, {
+                    tender_id: mainNode.tender_id,
+                    stdType: stdType,
+                    stdLibId: stdNode.list_id,
+                    stdNode: stdTree.getNodeKey(stdNode)
+                }, function (result) {
+                    treeOperationObj.refreshTree(mainSheet, result);
+                    treeOperationObj.refreshOperationValid(mainSheet, mainSheet.getSelections());
+                });
+            });
             this.pathTree = createNewPathTree(this.treeSetting);
         }
         loadLib (listId) {

+ 99 - 2
app/public/js/path_tree.js

@@ -1,4 +1,9 @@
-
+/**
+ * 构建pathTree
+ * 可动态加载子节点,要求子节点获取接口按/xxx/get-children定义
+ * @param {Object} setting - 设置
+ * @returns {PathTree}
+ */
 const createNewPathTree = function (setting) {
     const treeSetting = JSON.parse(JSON.stringify(setting));
     const itemsPre = 'id_';
@@ -12,6 +17,7 @@ const createNewPathTree = function (setting) {
         this.nodes = [];
     };
     const proto = PathTree.prototype;
+
     /**
      * 树结构根据显示排序
      */
@@ -26,6 +32,7 @@ const createNewPathTree = function (setting) {
         self.nodes = [];
         addSortNodes(this.getChildren(null));
     };
+
     /**
      * 加载数据(初始化), 并给数据添加部分树结构必须数据
      * @param datas
@@ -66,6 +73,10 @@ const createNewPathTree = function (setting) {
                 loadedData.push(node);
             }
         }
+        for (const node of loadedData) {
+            const children = this.getChildren(node);
+            node.expanded = children.length > 0 && children[0].visible;
+        }
         this.sortTreeNode();
         return loadedData;
     };
@@ -97,6 +108,13 @@ const createNewPathTree = function (setting) {
             }
         }
         this.sortTreeNode();
+        for (const node of loadedData) {
+            const children = this.getChildren(node);
+            if (!node.expanded && children.length > 0) {
+                node.expaned = true;
+                this._refreshChildrenVisible(node);
+            }
+        }
         return loadedData;
     };
     /**
@@ -119,6 +137,47 @@ const createNewPathTree = function (setting) {
         }
     };
     /**
+     * 加载需展开的数据
+     * @param {Array} datas
+     * @returns {Array}
+     * @private
+     */
+    proto._loadExpandData = function (datas) {
+        const loadedData = [], existData = [], expandData = [];
+        for (const data of datas) {
+            let node = this.getItems(data[treeSetting.id]);
+            if (node) {
+                existData.push(node);
+            } else {
+                const keyName = itemsPre + data[treeSetting.id];
+                const node = JSON.parse(JSON.stringify(data));
+                this.items[keyName] = node;
+                this.datas.push(node);
+                node.expanded = false;
+                node.visible = true;
+                loadedData.push(node);
+            }
+        }
+        this.sortTreeNode();
+        for (const node of loadedData) {
+            const children = this.getChildren(node);
+            if (!node.expanded && children.length > 0) {
+                node.expaned = true;
+                this._refreshChildrenVisible(node);
+            }
+        }
+        for (const node of existData) {
+            const children = this.getChildren(node);
+            if (!node.expanded && children.length > 0) {
+                node.expanded = children.length > 0;
+                this._refreshChildrenVisible(node);
+                expandData.push(node);
+            }
+        }
+        return [loadedData, expandData];
+    };
+
+    /**
      * 根据id获取树结构节点数据
      * @param {Number} id
      * @returns {Object}
@@ -167,7 +226,7 @@ const createNewPathTree = function (setting) {
      */
     proto.isLastSibling = function (node) {
         const siblings = this.getChildren(this.getParent(node));
-        return node.order === siblings.length;
+        return node.order === siblings[siblings.length - 1].order;
     };
     /**
      * 刷新子节点是否可见
@@ -203,6 +262,14 @@ const createNewPathTree = function (setting) {
         }
         return data;
     }
+    /**
+     * 得到树结构构成id
+     * @param node
+     * @returns {*}
+     */
+    proto.getNodeKey = function (node) {
+        return node[treeSetting.id];
+    }
 
     /**
      * 以下方法需等待响应, 通过callback刷新界面
@@ -215,6 +282,7 @@ const createNewPathTree = function (setting) {
     proto.loadChildren = function (node, callback) {
         const self = this;
         const url = treeSetting.preUrl ? treeSetting.preUrl + '/get-children' : 'get-children';
+        console.log(url);
         postData(url, this.getNodeKeyData(node), function (data) {
             self._loadData(data);
             callback();
@@ -297,6 +365,35 @@ const createNewPathTree = function (setting) {
             callback(result);
         });
     };
+    /**
+     * 提交数据
+     * @param {String} url - 请求地址
+     * @param {Object} node - 当前选中节点
+     * @param {Object} data - 提交的数据
+     * @param {function} callback - 界面刷新
+     */
+    proto.postData = function (url, node, data, callback) {
+        const self = this;
+        data.id = node[treeSetting.id];
+        postData(url, data, function (datas) {
+            const result = {};
+            if (datas.update) {
+                result.update = self._updateData(datas.update);
+            }
+            if (datas.create) {
+                result.create = self._loadData(datas.create);
+            }
+            if (datas.delete) {
+                result.delete = self._freeData(datas.delete);
+            }
+            if (datas.expand) {
+                const [create, update] = self._loadExpandData(datas.expand);
+                result.create = result.create.concat(create);
+                result.expand = update;
+            }
+            callback(result);
+        });
+    }
 
     return new PathTree();
 }

+ 1 - 0
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -228,6 +228,7 @@ const SpreadJsObj = {
             // 单元格重新写入数据
             for (let i = row; i < row + count; i++) {
                 const data = sortData[i];
+                if (!data) { continue; }
                 sheet.zh_setting.cols.forEach(function (col, j) {
                     const cell = sheet.getCell(i, j);
                     if (col.field !== '' && data[col.field]) {

+ 1 - 0
app/router.js

@@ -38,6 +38,7 @@ module.exports = app => {
     app.post('/ledger/update', sessionAuth, 'ledgerController.update');
     app.post('/ledger/update-info', sessionAuth, 'ledgerController.updateInfo');
     app.post('/ledger/paste-block', sessionAuth, 'ledgerController.pasteBlock');
+    app.post('/ledger/add-by-std', sessionAuth, 'ledgerController.addFromStandardLib');
     app.get('/ledger/change', sessionAuth, 'ledgerController.change');
     app.get('/ledger/index', sessionAuth, 'ledgerController.index');
 

+ 257 - 26
app/service/ledger.js

@@ -24,6 +24,7 @@ const keyFields = {
 const readOnlyFields = ['id', 'tender_id', 'ledger_id', 'ledger_pid', 'order', 'level', 'full_path', 'is_leaf'];
 const calcFields = ['quantity', 'unit_price', 'total_price'];
 const zeroRange = 0.0000000001;
+const rootId = -1;
 
 module.exports = app => {
 
@@ -40,15 +41,24 @@ module.exports = app => {
             this.tableName = 'ledger';
         }
 
+        /**
+         * 新增数据(供内部或其他service类调用, controller不可直接使用)
+         * @param {Array|Object} data - 新增数据
+         * @param {Number} tenderId - 标段id
+         * @param {Object} transaction - 新增事务
+         * @returns {Promise<boolean>} - {Promise<是否正确新增成功>}
+         */
         async innerAdd(data, tenderId, transaction) {
             const datas = data instanceof Array ? data : [data];
             if (tenderId <= 0) {
                 throw '标段id错误';
             }
-            // 数组则为批量插入
             if (datas.length <= 0) {
                 throw '插入数据为空';
             }
+            if (!transaction) {
+                throw '内部错误';
+            }
             // 整理数据
             const insertData = [];
             for (const tmp of datas) {
@@ -62,7 +72,6 @@ module.exports = app => {
             const operate = await transaction.insert(this.tableName, insertData);
             return operate.affectedRows === datas.length;
         }
-
         /**
          * 新增数据
          *
@@ -190,6 +199,29 @@ module.exports = app => {
         }
 
         /**
+         * 根据标准清单源检索
+         * @param tenderId
+         * @param source
+         * @returns {Promise<*>}
+         */
+        async getDataBySource(tenderId, source) {
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('tender_id', {
+                value: tenderId,
+                operate: '=',
+            });
+            this.sqlBuilder.setAndWhere('source', {
+                value: source,
+                operate: '=',
+            });
+
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+            const data = await this.db.query(sql, sqlParam);
+
+            return data;
+        }
+
+        /**
          * 获取最末的子节点
          * @param {Number} tenderId - 标段id
          * @param {Number} pid - 父节点id
@@ -262,9 +294,13 @@ module.exports = app => {
          * @return {Promise<*>}
          */
         async getChildrenByParentId(tenderId, nodeId) {
-            if ((nodeId <= 0) || (tenderId <= 0)) {
+            if (tenderId <= 0 || !nodeId) {
                 return undefined;
             }
+            const nodeIds = nodeId instanceof Array ? nodeId : [nodeId];
+            if (nodeIds.length === 0) {
+                return [];
+            }
 
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
@@ -272,9 +308,10 @@ module.exports = app => {
                 operate: '=',
             });
             this.sqlBuilder.setAndWhere('ledger_pid', {
-                value: nodeId,
-                operate: '=',
+                value: nodeIds,
+                operate: 'in',
             });
+            this.sqlBuilder.orderBy = [['order', 'ASC']];
 
             const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
             const data = await this.db.query(sql, sqlParam);
@@ -289,7 +326,7 @@ module.exports = app => {
          * @return {Array}
          */
         async getNextsData(tenderId, pid, order) {
-            if ((tenderId <= 0) || (pid <= 0) || (order < 0)) {
+            if ((tenderId <= 0) || (order < 0)) {
                 return undefined;
             }
 
@@ -383,37 +420,48 @@ module.exports = app => {
         }
 
         /**
-         *  select的全部后兄弟节点,Order自增
-         *
-         * @param {Object} select - 选中的节点
-         * @param {Number} incre - 自增值
-         * @return {Array} - 自增后的数据
+         * 更新order
+         * @param {Number} tenderId - 标段id
+         * @param {Number} parentId - 父节点id
+         * @param {Number} order - 自增起始order(含)
+         * @param {Number} incre - 自增量
+         * @returns {Promise<*>}
          * @private
          */
-        async _updateSelectNextsOrder(select, incre = 1) {
+        async _updateChildrenOrderAfter(tenderId, parentId, order, incre = 1) {
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
-                value: select.tender_id,
-                operate: '=',
+                value: tenderId,
+                operate: '='
             });
             this.sqlBuilder.setAndWhere('order', {
-                value: select.order + 1,
+                value: order,
                 operate: '>=',
             });
             this.sqlBuilder.setAndWhere('ledger_pid', {
-                value: select.ledger_pid,
+                value: parentId,
                 operate: '=',
             });
             this.sqlBuilder.setUpdateData('order', {
                 value: Math.abs(incre),
                 selfOperate: incre > 0 ? '+' : '-',
             });
-            // sql = update this.tableName set order = order + 1 where (tender_id = select.tender_id) && (pid = select.pid) && (order >= select.order+1)
             const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
             const data = await this.transaction.query(sql, sqlParam);
 
             return data;
         }
+        /**
+         *  select的全部后兄弟节点,Order自增
+         *
+         * @param {Object} select - 选中的节点
+         * @param {Number} incre - 自增值
+         * @return {Array} - 自增后的数据
+         * @private
+         */
+        async _updateSelectNextsOrder(select, incre = 1) {
+            return await this._updateChildrenOrderAfter(select.tender_id, select.ledger_pid, select.order + 1, incre);
+        }
 
         /**
          * 从数据库获取标段的最大节点id
@@ -430,7 +478,7 @@ module.exports = app => {
         }
 
         /**
-         * 根据selectData, data 新增数据
+         * 根据selectData, data 新增数据(新增为selectData的后项,该方法不可单独使用)
          *
          * @param {Number} tenderId - 标段id
          * @param {Object} selectData - 选中节点的数据
@@ -457,7 +505,7 @@ module.exports = app => {
             data.ledger_pid = selectData.ledger_pid;
             data.level = selectData.level;
             data.order = selectData.order + 1;
-            data.full_path = selectData.full_path.replace(selectData.ledger_id, data.ledger_id);
+            data.full_path = selectData.full_path.replace('.' + selectData.ledger_id, '.' + data.ledger_id);
             data.is_leaf = true;
             const result = await this.transaction.insert(this.tableName, data);
 
@@ -465,6 +513,79 @@ module.exports = app => {
 
             return result;
         }
+        /**
+         * 根据parentData, data新增数据(新增为parentData的最后一个子项)
+         * @param {Number} tenderId - 标段id
+         * @param {Object} parentData - 父项数据
+         * @param {Object} data - 新增节点,初始数据
+         * @returns {Promise<*>} - 新增结果
+         * @private
+         */
+        async _addChildNodeData(tenderId, parentData, data) {
+            if (tenderId <= 0) {
+                return undefined;
+            }
+            if (!data) {
+                data = {};
+            }
+            const pid = parentData ? parentData.ledger_id : rootId;
+
+            const cacheKey = 'tender_node_maxId: ' + tenderId;
+            let maxId = parseInt(await this.cache.get(cacheKey));
+            if (!maxId) {
+                maxId = await this._getMaxNodeId(tenderId);
+                this.cache.set(cacheKey, maxId, 'EX', this.ctx.app.config.cacheTime);
+            }
+
+            data.tender_id = tenderId;
+            data.ledger_id = maxId + 1;
+            data.ledger_pid = pid;
+            if (data.order === undefined) {
+                data.order = 1;
+            }
+            data.level = parentData ? parentData.level + 1 : 1;
+            data.full_path = parentData ? parentData.full_path + '.' + data.ledger_id : '' + data.ledger_id;
+            if (data.is_leaf === undefined) {
+                data.is_leaf = true;
+            }
+            const result = await this.transaction.insert(this.tableName, data);
+
+            this.cache.set(cacheKey, maxId + 1, 'EX', this.ctx.app.config.cacheTime);
+            return [result, data];
+        }
+
+        /**
+         * 根据parentData, data新增数据(自动排序)
+         * @param tenderId
+         * @param parentData
+         * @param data
+         * @returns {Promise<void>}
+         * @private
+         */
+        async _addChildAutoOrder(tenderId, parentData, data) {
+            const self = this;
+            const findPreData = function (list, a) {
+                if (!list || list.length === 0) { return null; }
+                for (let i = 0, iLen = list.length; i < iLen; i++) {
+                    if (self.ctx.helper.compareCode(list[i].code, a.code) > 0) {
+                        return i > 0 ? list[i-1] : null;
+                    }
+                }
+                return list[list.length -1];
+            }
+
+            const pid = parentData ? parentData.ledger_id : rootId;
+            const children = await this.getChildrenByParentId(tenderId, pid);
+            const preData = findPreData(children, data);
+            let parent = null;
+            if (!preData || children.indexOf(preData) < children.length - 1) {
+                await this._updateChildrenOrderAfter(tenderId, pid, preData ? preData.order + 1 : 1);
+            }
+            data.order = preData ? preData.order + 1 : 1;
+            const [addResult, node] = await this._addChildNodeData(tenderId, parentData, data);
+
+            return [addResult, node];
+        }
 
         /**
          * tenderId标段中, 在selectId后新增一个节点
@@ -482,19 +603,23 @@ module.exports = app => {
             if (!selectData) {
                 throw '新增节点数据错误';
             }
-            this.transaction = await this.db.beginTransaction();
-
+            if (!this.transaction) {
+                this.transaction = await this.db.beginTransaction();
+            }
 
             try {
                 // 选中节点的所有后兄弟节点,order+1
                 await this._updateSelectNextsOrder(selectData);
                 // 数据库创建新增节点数据
-                await this._addNodeData(tenderId, selectData, data);
+                const newNode = await this._addNodeData(tenderId, selectData, data);
+                if (!newNode) { throw '新增节点数据错误'; }
                 await this.transaction.commit();
             } catch (err) {
                 await this.transaction.rollback();
+                this.transaction = null;
                 throw err;
             }
+            this.transaction = null;
 
             // 查询应返回的结果
             const createData = await this.getDataByParentAndOrder(selectData.tender_id, selectData.ledger_pid, [selectData.order + 1]);
@@ -502,6 +627,109 @@ module.exports = app => {
             return { create: createData, update: updateData };
         }
 
+        /**
+         * 从标准数据中提取有效数据
+         * @param {Object} stdData - 从标准库中查询所得
+         * @returns {name, unit, source, code, b_code}
+         * @private
+         */
+        _filterStdData(stdData) {
+            const result = {
+                name: stdData.name,
+                unit: stdData.unit,
+                source: stdData.source
+            }
+            result.code = stdData.code ? stdData.code : '';
+            result.b_code = stdData.b_code ? stdData.b_code : '';
+            return result;
+        }
+
+        /**
+         * 添加节点(来自标准清单)
+         * @param {Number} tenderId
+         * @param {Number} selectId
+         * @param {Object} stdData
+         * @returns {Promise<*>}
+         */
+        async addStdNode(tenderId, selectId, stdData) {
+            const newData = this.filterStdData(stdData);
+            const result = await this.addNode(tenderId, selectId, newData);
+            return result;
+        }
+
+        /**
+         * 添加节点,并同步添加父节点
+         * @param {Number} tenderId - 标段id
+         * @param {Number} selectId - 选中节点id
+         * @param {Object} stdData - 节点数据
+         * @param {StandardLib} stdLib - 标准库
+         * @returns {Promise<void>}
+         */
+        async addStdNodeWithParent(tenderId, stdData, stdLib) {
+            const fullLevel = await stdLib.getFullLevelDataByFullPath(stdData.list_id, stdData.full_path);
+            fullLevel.sort(function (x, y) {
+                return x.level - y.level
+            });
+            let isNew = false, node, firstNew, updateParent, addResult;
+            const expandIds = [];
+            this.transaction = await this.db.beginTransaction();
+            try {
+                for (let i = 0, len = fullLevel.length; i < len; i++) {
+                    const stdNode = fullLevel[i];
+
+                    if (isNew) {
+                        const newData = this._filterStdData(stdNode);
+                        newData.is_leaf = (i === len - 1);
+                        [addResult, node] = await this._addChildNodeData(tenderId, node, newData);
+                    } else {
+                        const parent = node;
+                        node = await this.getDataByCondition({
+                            tender_id: tenderId,
+                            ledger_pid: parent ? parent.ledger_id : rootId,
+                            code: stdNode.code,
+                            name: stdNode.name
+                        });
+                        if (!node) {
+                            isNew = true;
+                            const newData = this._filterStdData(stdNode);
+                            newData.is_leaf = (i === len - 1);
+                            [addResult, node] = await this._addChildAutoOrder(tenderId, parent, newData);
+                            if (parent && parent.is_leaf) {
+                                await this.transaction.update(this.tableName, {id: parent.id, is_leaf: false} );
+                                updateParent = parent;
+                            }
+                            firstNew = node;
+                        } else {
+                            expandIds.push(node.ledger_id);
+                        }
+                    }
+                }
+                this.transaction.commit();
+            } catch (err) {
+                await this.transaction.rollback();
+                throw err;
+            }
+
+            // 查询应返回的结果
+            let createData = [], updateData = [];
+            if (firstNew) {
+                createData = await this.getDataByFullPath(tenderId, firstNew.full_path + '%');
+                updateData = await this.getNextsData(tenderId, firstNew.ledger_pid, firstNew.order);
+                if (updateParent) {
+                    updateData.push(await this.getDataByCondition({id: updateParent.id}));
+                }
+            }
+            const expandData = await this.getChildrenByParentId(tenderId, expandIds);
+            return { create: createData, update: updateData, expand: expandData };
+        }
+
+        /**
+         * 删除节点
+         * @param {Number} tenderId - 标段id
+         * @param {Object} deleteData - 删除节点数据
+         * @returns {Promise<*>}
+         * @private
+         */
         async _deleteNodeData(tenderId, deleteData) {
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
@@ -546,10 +774,7 @@ module.exports = app => {
                 if (parentData) {
                     const count = this.db.count(this.tableName, { ledger_pid: selectData.ledger_pid });
                     if (count === 1) {
-                        await this.transaction.update({
-                            id: parentData.id,
-                            is_leaf: true,
-                        });
+                        await this.transaction.update(this.tableName, { id: parentData.id, is_leaf: true });
                     }
                 }
                 // 选中节点--全部后节点 order--
@@ -1170,6 +1395,12 @@ module.exports = app => {
             }
         }
 
+        /**
+         * 提交数据 - 响应计算(增量方式计算)
+         * @param {Number} tenderId
+         * @param {Object} data
+         * @returns {Promise<*>}
+         */
         async updateCalc(tenderId, data) {
             const findData = function (id, datas) {
                 for (const d of datas) {

+ 86 - 0
app/service/standard_lib.js

@@ -21,8 +21,11 @@ class StandardLib extends BaseService {
     constructor(ctx, tableName) {
         super(ctx);
         this.tableName = tableName;
+        this.dataId = '';
+        this.stdType = '';
     }
 
+
     /**
      * 获取数据
      *
@@ -46,6 +49,89 @@ class StandardLib extends BaseService {
 
         return list;
     }
+
+
+    /**
+     * 实例中具体的dataId使用字段不相同,统一赋值到source下
+     * @param {Object|Array} data
+     */
+    setSourceData(data) {
+        if (!data) { return; }
+        const datas = data instanceof Array ? data : [data];
+        for (const d of datas) {
+            if (d) {
+                d.source = this.stdType + '-' + d.list_id + '-' + d[this.dataId];
+            }
+        }
+    }
+    /**
+     * 根据dataId和listId获取指定标准节点
+     * @param {Number} listId - 标准库id
+     * @param {Number} dataId - dataId(chapter_id|bill_id)
+     * @returns {Promise<*>}
+     */
+    async getDataByDataId(listId, dataId) {
+        this.initSqlBuilder();
+        this.sqlBuilder.setAndWhere('list_id', {
+            value: listId,
+            operate: '='
+        });
+        this.sqlBuilder.setAndWhere(this.dataId, {
+            value: dataId,
+            operate: '='
+        });
+        const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+        const node = await this.db.queryOne(sql, sqlParam);
+        this.setSourceData(node);
+
+        return node;
+    }
+    /**
+     * 根据full_path获取数据 full_path Like ‘1.2.3%’(传参full_path = '1.2.3%')
+     * @param {Number} listId - 标准库id
+     * @param {String} full_path - 路径
+     * @return {Promise<void>}
+     */
+    async getDataByFullPath(listId, full_path) {
+        this.initSqlBuilder();
+        this.sqlBuilder.setAndWhere('list_id', {
+            value: listId,
+            operate: '=',
+        });
+        this.sqlBuilder.setAndWhere('full_path', {
+            value: this.db.escape(full_path),
+            operate: 'Like',
+        });
+        const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+        const resultData = await this.db.query(sql, sqlParam);
+        this.setSourceData(resultData);
+        return resultData;
+    }
+    /**
+     * 根据full_path检索自己及所有父项
+     * @param {Number} listId - 标准库id
+     * @param {Array|String} fullPath - 节点完整路径
+     * @returns {Promise<*>}
+     * @private
+     */
+    async getFullLevelDataByFullPath(listId, fullPath) {
+        const explodePath = this.ctx.helper.explodePath(fullPath);
+
+        this.initSqlBuilder();
+        this.sqlBuilder.setAndWhere('list_id', {
+            value: listId,
+            operate: '=',
+        });
+        this.sqlBuilder.setAndWhere('full_path', {
+            value: explodePath,
+            operate: 'in'
+        });
+
+        const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+        const data = await this.db.query(sql, sqlParam);
+        this.setSourceData(data);
+        return data;
+    };
 }
 
 module.exports = StandardLib;

+ 1 - 0
app/service/std_bills.js

@@ -22,6 +22,7 @@ module.exports = app => {
         constructor(ctx) {
             super(ctx, 'bill');
             this.dataId = 'bill_id';
+            this.stdType = 'bill';
         }
 
     }

+ 1 - 0
app/service/std_chapter.js

@@ -22,6 +22,7 @@ module.exports = app => {
         constructor(ctx) {
             super(ctx, 'project_chapter');
             this.dataId = 'chapter_id';
+            this.stdType = 'chapter';
         }
 
     }

+ 1 - 1
package.json

@@ -37,7 +37,7 @@
     "dev": "egg-bin dev --port 7002",
     "dev-local": "set EGG_SERVER_ENV=qa&&egg-bin dev --port 7002",
     "dev-qa": "set EGG_SERVER_ENV=qa&&egg-bin dev --port 7002",
-    "test": "npm run lint -- --fix && npm run test-local",
+    "test": "npm run lint -- --fix&&npm run test-local",
     "test-local": "set EGG_SERVER_ENV=local&& egg-bin test",
     "test-qa": "set EGG_SERVER_ENV=qa&&egg-bin test",
     "cov": "egg-bin cov",

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

@@ -89,4 +89,35 @@ describe('test/app/extend/helper.test.js', () => {
         result = ctx.helper.hasPermission([permission.permission.CREATE_TENDER, permission.permission.VIEW_ALL_TENDER]);
         assert(result);
     });
+
+    it('test compareCode', function* () {
+        const ctx = app.mockContext();
+        // 测试项目节
+        assert(ctx.helper.compareCode('1-1-1', '1-1-2') < 0);
+        assert(ctx.helper.compareCode('1-2-2', '1-3-1') < 0);
+        assert(ctx.helper.compareCode('1-2-2', '1-2-2-1') < 0);
+        // 测试广东清单编号
+        assert(ctx.helper.compareCode('404-1-1', '404-1-2') < 0);
+        assert(ctx.helper.compareCode('404-1-2', '404-2-1') < 0);
+        assert(ctx.helper.compareCode('404-1', '404-2-a') < 0);
+        assert(ctx.helper.compareCode('404', '404-1') < 0);
+        assert(ctx.helper.compareCode('404-1-b', '清单编号') < 0);
+        // 测试全国清单编号
+        assert(ctx.helper.compareCode('404-1-a', '404-1-b') < 0);
+        assert(ctx.helper.compareCode('404-1-1', '404-1-a') < 0);
+        assert(ctx.helper.compareCode('404-1-b', '404-2-a') < 0);
+        assert(ctx.helper.compareCode('404-1-b', '404-a-a') < 0);
+        // 测试日期
+        assert(ctx.helper.compareCode('2018-03-04', '2018-03-05') < 0);
+        assert(ctx.helper.compareCode('2018-02-09', '2018-03-06') < 0);
+        assert(ctx.helper.compareCode('2017-05-15', '2018-03-05') < 0);
+        // 测试时间
+        assert(ctx.helper.compareCode('03:05:20', '03:05:24') < 0);
+        assert(ctx.helper.compareCode('04:04:33', '04:05:33') < 0);
+        assert(ctx.helper.compareCode('07:20:50', '08:20:50') < 0);
+        // 测试层次编号
+        assert(ctx.helper.compareCode('1.1.1', '1.1.2', '.') < 0);
+        assert(ctx.helper.compareCode('1.1.3', '1.2.2', '.') < 0);
+        assert(ctx.helper.compareCode('2.4.3', '3.1.2', '.') < 0);
+    });
 });

+ 94 - 1
test/app/service/ledger.test.js

@@ -2,7 +2,7 @@
  * 标段--台账 模型 单元测试
  *
  * @author Mai
- * @date 2017/12/1
+ * @date 2017/2/1
  * @version
  */
 'use strict';
@@ -845,6 +845,99 @@ describe('test/app/service/ledger.test.js', () => {
         │       └── 1-3-1
         └── 1-4
      */
+    // 从标准库添加数据
+    // 检查添加,直接添加为选中节点子节点
+    it('test addStdNode', function* () {
+        const ctx = app.mockContext();
+
+        // 选中1-1-4
+        const selectData = yield ctx.service.ledger.getDataByCondition({ tender_id: testTenderId, code: '1-1-4' });
+        assert(selectData);
+        // 从标准库中添加1-1-5
+        const condition1 = { list_id: 1, code: '1-1-5' };
+        const libData1 = yield ctx.service.stdChapter.getDataByCondition(condition1);
+        assert(libData1);
+        const stdData1 = yield ctx.service.stdChapter.getDataByDataId(1, libData1.chapter_id);
+        assert(stdData1);
+        assert(stdData1.id === libData1.id);
+        const result1 = yield ctx.service.ledger.addStdNode(testTenderId, selectData.ledger_id, stdData1);
+        assert(result1);
+        assert(result1.create.length === 1);
+        assert(result1.update.length === 1);
+        // 从标准库中添加101-1
+    });
+    /* 期望运行结果:
+        1                                                   122.00040759
+        ├── 1-1                                             90.00009719
+        │   ├── 1-1-1                                       30.00003573
+        │   │   └── 202-2                                   30.00003573
+        │   │       ├── 202-2-c     4.00000025  6.0000083   24.0000347
+        │   │       └── 202-2-e     2.00000001  3.0000005   6.00000103
+        │   ├── 1-1-4
+        │   ├── 1-1-5
+        │   └── 202-2                                       60.00007146
+        │       ├── 202-2-c         4.00000025  6.0000083   24.0000347
+        │       ├── 202-2-e         2.00000001  3.0000005   6.00000103
+        │       └── 202-2                                   30.00003573
+        │           ├── 202-2-c     4.00000025  6.0000083   24.0000347
+        │           └── 202-2-e     2.00000001  3.0000005   6.00000103
+        ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2                                             32.0003004
+        │   ├── 1-2-1
+        │   ├── 1-2-2                                       32.0003004
+        │   │   └── 202-2                                   32.0003004
+        │   │       └── 202-2-e     8.0000579   4.0000086   32.0003004
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    it('test addStdNodeWithParent', function* () {
+        const ctx = app.mockContext();
+
+        // 从标准库添加1-4-2-1
+        const condition1 = { list_id: 1, code: '1-4-2-1'}
+        const libData1 = yield ctx.service.stdChapter.getDataByCondition(condition1);
+        assert(libData1);
+        const stdData1 = yield ctx.service.stdChapter.getDataByDataId(1, libData1.chapter_id);
+        assert(stdData1);
+        assert(stdData1.id === libData1.id);
+        const result1 = yield ctx.service.ledger.addStdNodeWithParent(testTenderId, stdData1, ctx.service.stdChapter);
+        assert(result1);
+        assert(result1.create.length === 4);
+        assert(!result1.update || result1.update.length === 0);
+        assert(!result1.expand || result1.expand.length === 0);
+        // 从标准库添加1-4-2-1-2
+        const condition2 = { list_id: 1, code: '1-4-2-1-2'}
+        const libData2 = yield ctx.service.stdChapter.getDataByCondition(condition2);
+        assert(libData2);
+        const stdData2 = yield ctx.service.stdChapter.getDataByDataId(1, libData2.chapter_id);
+        assert(stdData2);
+        assert(stdData2.id === libData2.id);
+        const result2 = yield ctx.service.ledger.addStdNodeWithParent(testTenderId, stdData2, ctx.service.stdChapter);
+        assert(result2);
+        assert(result2.create.length === 1);
+        assert(result2.update.length === 1);
+        assert(result2.update[0].code = '1-4-2-1');
+        assert(!result2.update[0].is_leaf);
+        assert(result2.expand.length === 4);
+        // 从标准库添加1-4-2-1-1
+        const condition3 = { list_id: 1, code: '1-4-2-1-1'}
+        const libData3 = yield ctx.service.stdChapter.getDataByCondition(condition3);
+        assert(libData3);
+        const stdData3 = yield ctx.service.stdChapter.getDataByDataId(1, libData3.chapter_id);
+        assert(stdData3);
+        assert(stdData3.id === libData3.id);
+        const result3 = yield ctx.service.ledger.addStdNodeWithParent(testTenderId, stdData3, ctx.service.stdChapter);
+        assert(result3);
+        assert(result3.create.length === 1);
+        assert(result3.create[0].order === 1);
+        assert(result3.update.length === 1);
+        assert(result3.update[0].code === '1-4-2-1-2');
+        assert(result3.update[0].order === 2);
+        assert(result3.expand.length === 5);
+    });
 
     // 测试统计类方法
     it('test addUpChildren', function* () {