Sfoglia il codice sorgente

1. 编辑、复制粘贴数据,实时计算
2. 升级、降级、删除,实时计算
3. 复制整块,实时计算
4. 更新单元测试,覆盖全部计算相关

MaiXinRong 7 anni fa
parent
commit
87795a72e9

+ 21 - 0
app/controller/ledger_controller.js

@@ -178,6 +178,27 @@ module.exports = app => {
             ctx.body = responseData;
         }
 
+        async update(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);
+                responseData.data = await ctx.service.ledger.updateCalc(tenderId, data);
+            } catch (err) {
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+
+            ctx.body = responseData;
+        }
+
         /**
          * 复制粘贴整块
          * @param ctx

+ 1 - 1
app/controller/login_controller.js

@@ -73,4 +73,4 @@ module.exports = app => {
     }
 
     return LoginController;
-};
+};

+ 50 - 0
app/extend/helper.js

@@ -159,6 +159,56 @@ module.exports = {
     },
 
     /**
+     * 基于obj, 拷贝sObj中的内容
+     * obj = {a: 1, b: 2}, sObj = {a: 0, c: 3}, 返回{a: 0, b: 2, c: 3}
+     * @param obj
+     * @param sObj
+     * @returns {any}
+     */
+    updateObj(obj, sObj) {
+        if (!obj) {
+            return JSON.parse(JSON.stringify(sObj));
+        }
+        const result = JSON.parse(JSON.stringify(obj));
+        if (sObj) {
+            for (const prop in sObj) {
+                result[prop] = sObj[prop];
+            }
+        }
+        return result;
+    },
+
+    /**
+     * 在数组中查找
+     * @param {Array} arr
+     * @param name -
+     * @param value
+     * @returns {*}
+     */
+    findData(arr, name, value) {
+        if (!arr instanceof Array) {
+            throw '该方法仅用于数组查找';
+        }
+
+        if (arr.length === 0) { return undefined; }
+        for (const data of arr) {
+            if (data[name] == value) {
+                return data;
+            }
+        }
+        return undefined;
+    },
+
+    /**
+     * 检查数字是否为0
+     * @param value
+     */
+    checkZero(value) {
+        const zeroRange = 0.0000000001;
+        return value && Math.abs(value) > zeroRange;
+    },
+
+    /**
      * 判断当前用户是否有指定权限
      *
      * @param {Number|Array} permission - 权限id

+ 2 - 1
app/lib/sql_builder.js

@@ -107,11 +107,12 @@ class SqlBuilder {
                 this.sqlParam.push(set.field);
             } else {
                 const tmp = set.data.selfOperate !== undefined ?
-                    ' ?? = ?? ' + set.data.selfOperate + ' ' + set.data.value : ' ?? = ' + set.data.value;
+                    ' ?? = IF(IsNull(??), 0, ??) ' + set.data.selfOperate + ' ' + set.data.value : ' ?? = ' + set.data.value;
                 setDataArr.push(tmp);
                 // 如果是自身操作则压多一次字段进数组
                 if (set.data.selfOperate !== undefined) {
                     this.sqlParam.push(set.field);
+                    this.sqlParam.push(set.field);
                 }
                 this.sqlParam.push(set.field);
             }

+ 53 - 18
app/public/js/ledger.js

@@ -24,9 +24,9 @@ $(document).ready(function() {
             {title: '清单编号', field: 'b_code', width: 80},
             {title: '名称', field: 'name', width: 230},
             {title: '单位', field: 'unit', width: 50},
-            {title: '单价', field: 'price', width: 60},
-            {title: '数量', field: 'quantity', width: 60},
-            {title: '金额', field: 'totalPrice', width: 60},
+            {title: '单价', field: 'unit_price', width: 60, type: 'Number'},
+            {title: '数量', field: 'quantity', width: 60, type: 'Number'},
+            {title: '金额', field: 'total_price', width: 60, type: 'Number'},
             {title: '施工图原设计', field: 'design', width: 60},
             {title: '图(册)号', field: 'drawingCode', width: 80},
             {title: '备注', field: 'memo', width: 100}
@@ -113,6 +113,9 @@ $(document).ready(function() {
             const count = ledgerTree.getPosterity(node).length;
             tree.baseOperation('base-operation', node, 'delete', function (result) {
                 sheet.deleteRows(row, count + 1);
+                for (const data of result.update) {
+                    SpreadJsObj.reLoadRowData(sheet, tree.nodes.indexOf(data), tree.getPosterity(data).length + 1);
+                }
                 self.refreshOperationValid(sheet, sheet.getSelections());
             });
         },
@@ -183,7 +186,11 @@ $(document).ready(function() {
             if (!node) { return; }
 
             tree.baseOperation('base-operation', node, 'up-level', function (result) {
-                sheet.repaint();
+                const rows = [];
+                for (const u of result.update) {
+                    rows.push(tree.nodes.indexOf(u));
+                }
+                SpreadJsObj.reLoadRowsData(sheet, rows);
                 self.refreshOperationValid(sheet, sheet.getSelections());
             });
 
@@ -204,7 +211,11 @@ $(document).ready(function() {
             if (!node) { return; }
 
             tree.baseOperation('base-operation', node, 'down-level', function (result) {
-                sheet.repaint();
+                const rows = [];
+                for (const u of result.update) {
+                    rows.push(tree.nodes.indexOf(u));
+                }
+                SpreadJsObj.reLoadRowsData(sheet, rows);
                 self.refreshOperationValid(sheet, sheet.getSelections());
             });
         },
@@ -223,12 +234,12 @@ $(document).ready(function() {
                     tender_id: node.tender_id,
                     ledger_id: node.ledger_id
                 };
-                data[col.field] = info.editingText;
+                data[col.field] = col.type === 'Number' ? parseFloat(info.editingText) : info.editingText;
 
-                info.sheet.zh_tree.updateInfo('update-info', [data], function (result) {
+                info.sheet.zh_tree.update('update', data, function (result) {
                     const rows = [];
-                    for (const data of result) {
-                        rows.push(sortData.indexOf(data));
+                    for (const r of result) {
+                        rows.push(sortData.indexOf(r));
                     }
                     SpreadJsObj.reLoadRowsData(info.sheet, rows);
                 });
@@ -257,7 +268,7 @@ $(document).ready(function() {
                         nodes.push(node);
                     }
                 }
-                info.sheet.zh_tree.updateInfo('update-info', datas, function (result) {
+                info.sheet.zh_tree.update('update', datas, function (result) {
                     const rows = [];
                     for (const data of result) {
                         rows.push(sortData.indexOf(data));
@@ -266,6 +277,10 @@ $(document).ready(function() {
                 });
             }
         },
+        /**
+         * 删除按钮响应事件
+         * @param sheet
+         */
         deletePress: function (sheet) {
             if (sheet.zh_setting && sheet.zh_dataType === 'tree') {
                 const sortData = sheet.zh_tree.nodes;
@@ -283,7 +298,7 @@ $(document).ready(function() {
                         nodes.push(node);
                     }
                 }
-                sheet.zh_tree.updateInfo('update-info', datas, function (result) {
+                sheet.zh_tree.update('update-info', datas, function (result) {
                     const rows = [];
                     for (const data of result) {
                         rows.push(sortData.indexOf(data));
@@ -292,6 +307,11 @@ $(document).ready(function() {
                 });
             }
         },
+        /**
+         * 粘贴整块
+         * @param spread
+         * @param block
+         */
         pasteBlock: function (spread, block) {
             const self = this;
             const sheet = spread.getActiveSheet();
@@ -305,6 +325,7 @@ $(document).ready(function() {
 
             tree.pasteBlock('paste-block', node, block, function (result) {
                 SpreadJsObj.massOperationSheet(sheet, function () {
+                    // 新增
                     const newNodes = result.create;
                     if (newNodes) {
                         newNodes.sort(function (a, b) {
@@ -318,6 +339,12 @@ $(document).ready(function() {
                             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());
                 });
             });
@@ -330,6 +357,17 @@ $(document).ready(function() {
     ledgerSpread.bind(GC.Spread.Sheets.Events.EditEnded, treeOperationObj.editEnded);
     ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardPasted, treeOperationObj.clipboardPasted);
     SpreadJsObj.addDeleteBind(ledgerSpread, treeOperationObj.deletePress);
+    ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardChanging, function (e, info) {
+        info.copyData.html = SpreadJsObj.getFilterCopyHTML(info.sheet);
+        info.copyData.text = SpreadJsObj.getFilterCopyText(info.sheet);
+        console.log(info.copyData.text);
+    });
+    ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardChanged, function (e, info) {
+        console.log(info.copyData.text);
+    });
+    ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardPasting, function (e, info) {
+       console.log(info.pasteData.text);
+    });
 
     // 绑定 删除等 顶部按钮
     $('#delete').click(function () {
@@ -423,18 +461,15 @@ $(document).ready(function() {
                 icon: 'fa-clipboard',
                 disabled: function (key, opt) {
                     const block = treeOperationObj.block || [];
-                    return block.length <= 0;
+                    return block.length <= 0 && false;
                 },
                 callback: function (key, opt) {
                     const block = treeOperationObj.block || [];
                     if (block.length > 0) {
                         treeOperationObj.pasteBlock(ledgerSpread, block);
-                    }/* else {
-                        ledgerSpread.commandManager().execute({
-                            cmd:"paste",
-                            sheetName:ledgerSpread.getActiveSheet().name()
-                        });
-                    }*/
+                    } else {
+                        document.execCommand('paste');
+                    }
                 }
             }
         }

+ 50 - 5
app/public/js/path_tree.js

@@ -85,6 +85,28 @@ const createNewPathTree = function (setting) {
      * @return {Array} 加载到树的数据
      * @privateA
      */
+    proto._updateData = function (datas) {
+        const loadedData = [];
+        for (const data of datas) {
+            let node = this.getItems(data[treeSetting.id]);
+            if (node) {
+                for (const prop in node) {
+                    if (data[prop] !== undefined) {
+                        node[prop] = data[prop];
+                    }
+                }
+                loadedData.push(node);
+            }
+        }
+        this.sortTreeNode();
+        return loadedData;
+    };
+    /**
+     * 加载数据(动态),只加载不同部分
+     * @param {Array} datas
+     * @return {Array} 加载到树的数据
+     * @privateA
+     */
     proto._loadData = function (datas) {
         const loadedData = [];
         for (const data of datas) {
@@ -109,6 +131,11 @@ const createNewPathTree = function (setting) {
         this.sortTreeNode();
         return loadedData;
     };
+    /**
+     * 清理数据(动态)
+     * @param datas
+     * @private
+     */
     proto._freeData = function (datas) {
         const removeArrayData = function (array, data) {
             const index = array.indexOf(data);
@@ -196,6 +223,11 @@ const createNewPathTree = function (setting) {
         this._refreshChildrenVisible(node);
     };
 
+    /**
+     * 提取节点key和索引数据
+     * @param {Object} node - 节点
+     * @returns {key}
+     */
     proto.getNodeKeyData = function (node) {
         const data = {};
         for (const key of treeSetting.keys) {
@@ -235,7 +267,7 @@ const createNewPathTree = function (setting) {
         postData(url, data, function (datas) {
             const result = {};
             if (datas.update) {
-                result.update = self._loadData(datas.update);
+                result.update = self._updateData(datas.update);
             }
             if (datas.create) {
                 result.create = self._loadData(datas.create);
@@ -246,10 +278,16 @@ const createNewPathTree = function (setting) {
             callback(result);
         });
     };
-    proto.updateInfo = function (url, updateData, callback) {
+    /**
+     * 节点数据编辑
+     * @param {String} url - 请求地址
+     * @param {Array|Object} updateData - 需更新的数据
+     * @param {function} callback - 界面刷新
+     */
+    proto.update = function (url, updateData, callback) {
         const self = this;
         postData(url, updateData, function (datas) {
-            const result = self._loadData(datas);
+            const result = self._updateData(datas);
             callback(result);
         }, function () {
             if (updateData instanceof Array) {
@@ -257,12 +295,19 @@ const createNewPathTree = function (setting) {
                 for (const data of updateData) {
                     result.push(self.getItems(data[treeSetting.id]));
                 }
-                callback(result);
+                callback(result)
             } else {
                 callback([self.getItems(updateData[treeSetting.id])]);
             }
         });
     };
+    /**
+     * 复制粘贴整块(目前仅可粘贴为后项)
+     * @param {String} url - 请求地址
+     * @param {Object} node - 操作节点
+     * @param {Array} block - 被复制整块的节点列表
+     * @param {function} callback - 界面刷新
+     */
     proto.pasteBlock = function (url, node, block, callback) {
         const self = this;
         const data = {
@@ -272,7 +317,7 @@ const createNewPathTree = function (setting) {
         postData(url, data, function (datas) {
             const result = {};
             if (datas.update) {
-                result.update = self._loadData(datas.update);
+                result.update = self._updateData(datas.update);
             }
             if (datas.create) {
                 result.create = self._loadData(datas.create);

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

@@ -308,5 +308,51 @@ const SpreadJsObj = {
             sheet.zh_data = data;
         }
         this.reLoadSheetData(sheet);
+    },
+    /**
+     * 获取复制数据HTML格式(过滤不可见单元格)
+     * @param {GC.Spread.Sheets.Worksheet} sheet
+     * @returns {string}
+     */
+    getFilterCopyHTML: function (sheet) {
+        const sel = sheet.getSelections()[0];
+        const html = [];
+        html.push('<table>');
+        for (let i = sel.row, iLen = sel.row + sel.rowCount; i < iLen; i++) {
+            // 跳过隐藏行
+            if (!sheet.getCell(i, -1).visible()) { continue; }
+
+            const rowHtml = [];
+            rowHtml.push('<tr>');
+            for (let j = sel.col, jLen = sel.col + sel.colCount; j < jLen; j++) {
+                const data = sheet.getText(i, j);
+                rowHtml.push('<td>' + data + '</td>');
+            }
+            rowHtml.push('</tr>');
+            html.push(rowHtml.join(''));
+        }
+        html.push('</table>');
+        return html.join('');
+    },
+    /**
+     * 获取复制数据Text格式(过滤不可见单元格)
+     * @param {GC.Spread.Sheets.Worksheet} sheet
+     * @returns {string}
+     */
+    getFilterCopyText: function (sheet) {
+        const copyData = [];
+        const sel = sheet.getSelections()[0];
+        for(let i = sel.row, iLen = sel.row + sel.rowCount; i < iLen; i++) {
+            // 跳过隐藏行
+            if (!sheet.getCell(i, -1).visible()) { continue; }
+
+            const rowText = [];
+            for (let j = sel.col, jLen = sel.col + sel.colCount; j < jLen; j++) {
+                const data = sheet.getText(i, j);
+                rowText.push(data);
+            }
+            copyData.push(rowText.join('\t'));
+        }
+        return copyData.join('\n');
     }
 };

+ 1 - 0
app/router.js

@@ -35,6 +35,7 @@ module.exports = app => {
     app.get('/ledger/explode', sessionAuth, 'ledgerController.explode');
     app.post('/ledger/get-children', sessionAuth, 'ledgerController.getChildren');
     app.post('/ledger/base-operation', sessionAuth, 'ledgerController.baseOperation');
+    app.post('/ledger/update', sessionAuth, 'ledgerController.update');
     app.post('/ledger/update-info', sessionAuth, 'ledgerController.updateInfo');
     app.post('/ledger/paste-block', sessionAuth, 'ledgerController.pasteBlock');
     app.get('/ledger/change', sessionAuth, 'ledgerController.change');

+ 284 - 44
app/service/ledger.js

@@ -22,6 +22,8 @@ const keyFields = {
 };
 // 以下字段仅可通过树结构操作改变,不可直接通过update方式从接口提交,发现时过滤
 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;
 
 module.exports = app => {
 
@@ -137,7 +139,6 @@ module.exports = app => {
 
             return data;
         }
-
         /**
          * 根据节点Id获取数据
          * @param {Number} tenderId - 标段Id
@@ -164,6 +165,24 @@ module.exports = app => {
 
             return data;
         }
+        /**
+         * 根据主键id获取数据
+         * @param {Array|Number} id - 主键id
+         * @returns {Promise<*>}
+         */
+        async getDataByIds(id) {
+            const ids = id instanceof Array ? id : [id];
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('id', {
+                value: ids,
+                operate: 'in',
+            });
+
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+            const data = await this.db.query(sql, sqlParam);
+
+            return data;
+        }
 
         /**
          * 获取最末的子节点
@@ -187,7 +206,6 @@ module.exports = app => {
 
             return resultData;
         }
-
         /**
          * 根据 父节点id 和 节点排序order 获取数据
          *
@@ -232,7 +250,6 @@ module.exports = app => {
 
             return data;
         }
-
         /**
          * 根据 父节点id 获取子节点
          * @param tenderId
@@ -259,6 +276,37 @@ module.exports = app => {
 
             return data;
         }
+        /**
+         * 根据 父节点ID 和 节点排序order 获取全部后节点数据
+         * @param {Number} tenderId - 标段id
+         * @param {Number} pid - 父节点id
+         * @param {Number} order - 排序
+         * @return {Array}
+         */
+        async getNextsData(tenderId, pid, order) {
+            if ((tenderId <= 0) || (pid <= 0) || (order < 0)) {
+                return undefined;
+            }
+
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('tender_id', {
+                value: tenderId,
+                operate: '=',
+            });
+            this.sqlBuilder.setAndWhere('ledger_pid', {
+                value: pid,
+                operate: '=',
+            });
+            this.sqlBuilder.setAndWhere('order', {
+                value: order,
+                operate: '>',
+            });
+
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+            const data = await this.db.query(sql, sqlParam);
+
+            return data;
+        }
 
         /**
          * 根据full_path获取数据 full_path Like ‘1.2.3%’(传参full_path = '1.2.3%')
@@ -280,37 +328,53 @@ module.exports = app => {
             const resultData = await this.db.query(sql, sqlParam);
             return resultData;
         }
-
         /**
-         * 根据 父节点ID 和 节点排序order 获取全部后节点数据
+         * 根据full_path检索自己及所有父项
          * @param {Number} tenderId - 标段id
-         * @param {Number} pid - 父节点id
-         * @param {Number} order - 排序
-         * @return {Array}
+         * @param {Array|String} fullPath - 节点完整路径
+         * @returns {Promise<*>}
+         * @private
          */
-        async getNextsData(tenderId, pid, order) {
-            if ((tenderId <= 0) || (pid <= 0) || (order <= 0)) {
-                return undefined;
-            }
+        async getFullLevelDataByFullPath(tenderId, fullPath) {
+            const explodePath = this.ctx.helper.explodePath(fullPath);
 
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
                 value: tenderId,
                 operate: '=',
             });
-            this.sqlBuilder.setAndWhere('ledger_pid', {
-                value: pid,
-                operate: '=',
-            });
-            this.sqlBuilder.setAndWhere('order', {
-                value: order,
-                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);
 
             return data;
+        };
+
+        /**
+         * 统计子节点total_price
+         * @param {Number} tenderId - 标段id
+         * @param {Number} pid - 父节点id
+         * @param {Number} order - order取值
+         * @param {String} orderOperate - order比较操作符
+         * @returns {Promise<void>}
+         */
+        async addUpChildren(tenderId, pid, order, orderOperate) {
+            this.initSqlBuilder();
+            const sql = ['SELECT SUM(??) As value FROM ?? ', ' WHERE ']
+            const sqlParam = ['total_price', this.tableName];
+            sql.push(' ?? = ' + tenderId);
+            sqlParam.push('tender_id');
+            sql.push(' And ?? = ' + pid);
+            sqlParam.push('ledger_pid');
+            sql.push(' And ?? ' + orderOperate + ' ' + order);
+            sqlParam.push('order');
+            const result = await this.db.queryOne(sql.join(''), sqlParam);
+
+            return result.value;
         }
 
         /**
@@ -433,6 +497,22 @@ module.exports = app => {
             return { create: createData, update: updateData };
         }
 
+        async _deleteNodeData(tenderId, deleteData) {
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('tender_id', {
+                value: tenderId,
+                operate: '=',
+            });
+            this.sqlBuilder.setAndWhere('full_path', {
+                value: this.db.escape(deleteData.full_path + '%'),
+                operate: 'Like',
+            });
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'delete');
+            const result = await this.transaction.query(sql, sqlParam);
+
+            return result;
+        }
+
         /**
          *  tenderId标段中, 删除选中节点及其子节点
          *
@@ -456,17 +536,7 @@ module.exports = app => {
                 // 获取将要被删除的数据
                 deleteData = await this.getDataByFullPath(tenderId, selectData.full_path + '%');
                 // 删除
-                this.initSqlBuilder();
-                this.sqlBuilder.setAndWhere('tender_id', {
-                    value: tenderId,
-                    operate: '=',
-                });
-                this.sqlBuilder.setAndWhere('full_path', {
-                    value: this.db.escape(selectData.full_path + '%'),
-                    operate: 'Like',
-                });
-                const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'delete');
-                const operate = await this.transaction.query(sql, sqlParam);
+                const operate = await this._deleteNodeData(tenderId, selectData);
                 // 选中节点--父节点 只有一个子节点时,应升级is_leaf
                 if (parentData) {
                     const count = this.db.count(this.tableName, { ledger_pid: selectData.ledger_pid });
@@ -479,6 +549,13 @@ module.exports = app => {
                 }
                 // 选中节点--全部后节点 order--
                 await this._updateSelectNextsOrder(selectData, -1);
+                // 更新父项金额
+                if (selectData.total_price && Math.abs(selectData.total_price) > zeroRange) {
+                    const parentFullPath = selectData.full_path.replace('.' + selectData.ledger_id, '');
+                    const updateMap = {};
+                    updateMap[parentFullPath] = -selectData.total_price;
+                    await this._increCalcParent(tenderId, updateMap);
+                }
                 await this.transaction.commit();
             } catch (err) {
                 deleteData = [];
@@ -490,9 +567,12 @@ module.exports = app => {
             if (deleteData.length > 0) {
                 updateData = await this.getNextsData(tenderId, selectData.ledger_pid, selectData.order - 1);
                 updateData = updateData ? updateData : [];
-                const updateData2 = await this.getDataByNodeId(tenderId, selectData.ledger_pid);
-                if (updateData2.is_leaf === parentData.is_leaf) {
-                    updateData.push(updateData2);
+                const updateData1 = await this.getDataByNodeId(tenderId, selectData.ledger_pid);
+                if (selectData.total_price && Math.abs(selectData.total_price) > zeroRange) {
+                    const updateData2 = await this.getFullLevelDataByFullPath(tenderId, updateData1.full_path);
+                    updateData = updateData.concat(updateData2);
+                } else if (updateData1.is_leaf !== parentData.is_leaf) {
+                    updateData.push(updateData1);
                 }
             }
             return { delete: deleteData, update: updateData };
@@ -684,16 +764,26 @@ module.exports = app => {
                     this.transaction.update(this.tableName, {
                         id: parentData.id,
                         is_leaf: true,
+                        total_price: 0
+                    });
+                } else {
+                    this.transaction.update(this.tableName, {
+                        id: parentData.id,
+                        total_price: await this.addUpChildren(tenderId, selectData.ledger_pid, selectData.order, '<')
                     });
                 }
                 // 选中节点--父节点--全部后兄弟节点 order+1
                 await this._updateSelectNextsOrder(parentData);
                 // 选中节点 修改pid, order, full_path
+                let totalPrice = selectData.total_price ? selectData.total_price : 0;
+                const plus = await this.addUpChildren(tenderId, selectData.ledger_pid, selectData.order, '>');
+                totalPrice = plus ? totalPrice + plus : totalPrice;
                 const updateData = { id: selectData.id,
                     ledger_pid: parentData.ledger_pid,
                     order: parentData.order + 1,
                     level: selectData.level - 1,
                     full_path: newFullPath,
+                    total_price: totalPrice
                 };
                 await this.transaction.update(this.tableName, updateData);
                 // 选中节点--全部子节点(含孙) level-1, full_path变更
@@ -709,10 +799,9 @@ module.exports = app => {
             // 查询修改的数据
             const resultData1 = await this.getDataByFullPath(tenderId, newFullPath + '%');
             const resultData2 = await this.getNextsData(tenderId, parentData.ledger_pid, parentData.order + 1);
-            if (selectData.order === 1) {
-                const preParent = await this.getDataByNodeId(tenderId, parentData.ledger_id);
-                resultData2.push(preParent);
-            }
+            // 默认原Parent被刷新过,不核对total_price修改
+            const preParent = await this.getDataByNodeId(tenderId, parentData.ledger_id);
+            resultData2.push(preParent);
             return { update: resultData1.concat(resultData2) };
         }
 
@@ -787,12 +876,15 @@ module.exports = app => {
                 // 选中节点--全部子节点(含孙) level++, full_path
                 await this._syncDownlevelChildren(selectData, preData);
                 // 选中节点--前兄弟节点 is_leaf应为false
-                if (preData.is_leaf) {
+                if (preData.is_leaf || this.ctx.helper.checkZero(selectData.total_price)) {
                     const updateData2 = {
                         id: preData.id,
-                        is_leaf: false,
+                        is_leaf: false
                     };
-                    await this.transaction.update(this.tableName, updateData);
+                    if (this.ctx.helper.checkZero(selectData.total_price)) {
+                        updateData2['total_price'] = preData.total_price ? preData.total_price + selectData.total_price : selectData.total_price;
+                    }
+                    await this.transaction.update(this.tableName, updateData2);
                 }
                 this.transaction.commit();
             } catch (err) {
@@ -804,7 +896,7 @@ module.exports = app => {
             // 选中节点及子节点
             const resultData1 = await this.getDataByFullPath(tenderId, newFullPath + '%');
             // 选中节点--原前兄弟节点&全部后兄弟节点
-            const queryOrder = preData.is_leaf ? preData.order - 1 : preData.order;
+            const queryOrder = (preData.is_leaf || this.ctx.helper.checkZero(selectData.total_price)) ? preData.order - 1 : preData.order;
             const resultData2 = await this.getNextsData(tenderId, preData.ledger_pid, queryOrder);
             return { update: resultData1.concat(resultData2) };
         }
@@ -828,6 +920,39 @@ module.exports = app => {
             return result;
         }
 
+         /**
+         * newData中,以orgData为基准,过滤掉orgData中未定义或值相等的部分
+         * @param {Object} orgData
+         * @param {Object} newData
+         * @private
+         */
+        _filterChangedField(orgData, newData) {
+            const result= {};
+            let bChanged = false;
+            for (const prop in orgData) {
+                if (newData[prop] && newData[prop] !== orgData[prop]) {
+                    result[prop] = newData[prop];
+                    bChanged = true;
+                }
+            }
+            return bChanged ? result : undefined;
+        }
+
+        /**
+         * 检查data中是否含有计算字段
+         * @param {Object} data
+         * @returns {boolean}
+         * @private
+         */
+        _checkCalcField(data) {
+            for (const prop in data) {
+                if (calcFields.indexOf(prop) >= 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
         /**
          * 提交数据 - 不影响计算等未提交项
          * @param {Number} tenderId - 标段id
@@ -871,7 +996,7 @@ module.exports = app => {
             }
             for (const data of datas) {
                 if (tenderId !== data.tender_id) {
-                    throw '提交数据错误1';
+                    throw '提交数据错误';
                 }
             }
 
@@ -880,7 +1005,7 @@ module.exports = app => {
                 for (const data of datas) {
                     const updateNode = await this.getDataById(data.id);
                     if (!updateNode || tenderId !== updateNode.tender_id || data.ledger_id !== updateNode.ledger_id) {
-                        throw '提交数据错误2';
+                        throw '提交数据错误';
                     }
                     const updateData = this._filterUpdateInvalidField(updateNode.id, data);
                     await this.transaction.update(this.tableName, updateData);
@@ -938,12 +1063,14 @@ module.exports = app => {
             }
             const orgParentPath = copyNodes[0].full_path.replace(copyNodes[0].ledger_id, '');
 
+            let incre = 0;
             this.transaction = await this.db.beginTransaction();
             try {
                 // 选中节点的所有后兄弟节点,order+粘贴节点个数
                 await this._updateSelectNextsOrder(selectData, copyNodes.length);
                 // 数据库创建新增节点数据
                 for (const node of copyNodes) {
+                    incre += node.total_price ? node.total_price : 0;
                     const datas = await this.getDataByFullPath(tenderId, node.full_path + '%');
 
                     const cacheKey = 'tender_node_maxId:' + tenderId;
@@ -980,6 +1107,12 @@ module.exports = app => {
                     // 插入粘贴数据
                     await this.transaction.insert(this.tableName, datas);
                 }
+                // 更新父节点金额
+                if (Math.abs(incre) > zeroRange) {
+                    const updateMap = {};
+                    updateMap[newParentPath] = incre;
+                    await this._increCalcParent(tenderId, updateMap);
+                }
                 await this.transaction.commit();
             } catch (err) {
                 await this.transaction.rollback();
@@ -993,7 +1126,114 @@ module.exports = app => {
             }
             const createData = await this.getDataByParentAndOrder(selectData.tender_id, selectData.ledger_pid, order);
             const updateData = await this.getNextsData(selectData.tender_id, selectData.ledger_pid, selectData.order + copyNodes.length);
-            return { create: createData, update: updateData };
+            if (Math.abs(incre) > zeroRange) {
+                const updateData1 = await this.getFullLevelDataByFullPath(selectData.tender_id, newParentPath);
+                return { create: createData, update: updateData.concat(updateData1) };
+            } else {
+                return { create: createData, update: updateData };
+            }
+        }
+
+        /**
+         * 增量更新父项金额
+         * @param {Number} tenderId - 标段id
+         * @param {Object} updateMap - 增量更新数,使用更新父项的full_path为索引
+         *        e.g: {'1.2.6.8': 30, '1.2.6.10': 40}表示'1.2.6.8'增量30,'1.2.6.10'增量40(此处同步更新'1.2.6', '1.2', '1')
+         * @returns {Promise<void>}
+         * @private
+         */
+        async _increCalcParent(tenderId, updateMap) {
+            for (const prop in updateMap) {
+                this.initSqlBuilder();
+
+                this.sqlBuilder.setAndWhere('tender_id', {
+                    value: tenderId,
+                    operate: '='
+                });
+                const fullPath = this.ctx.helper.explodePath(prop);
+                this.sqlBuilder.setAndWhere('full_path', {
+                    value: this.ctx.helper.explodePath(prop),
+                    operate: 'in'
+                });
+                this.sqlBuilder.setUpdateData('total_price', {
+                    value: updateMap[prop] > 0 ? updateMap[prop] : -updateMap[prop],
+                    selfOperate: updateMap[prop] > 0 ? '+' : '-'
+                });
+
+                const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
+                await this.transaction.query(sql, sqlParam);
+            }
+        }
+
+        async updateCalc(tenderId, data) {
+            const findData = function (id, datas) {
+                for (const d of datas) {
+                    if (d.id === id) {
+                        return d;
+                    }
+                }
+                return undefined;
+            }
+            // 简单验证数据
+            if (tenderId <= 0) {
+                throw '标段不存在';
+            }
+            if (!data) {
+                throw '提交数据错误1';
+            }
+            const datas = data instanceof Array ? data : [data];
+            const ids = [];
+            for (const row of datas) {
+                if (tenderId !== row.tender_id) {
+                    throw '提交数据错误2';
+                }
+                ids.push(row.id);
+            }
+
+            const updateMap = {}, updateFullPath = [];
+            this.transaction = await this.db.beginTransaction();
+            try {
+                for (const row of datas) {
+                    const updateNode = await this.getDataById(row.id);
+                    if (!updateNode || tenderId !== updateNode.tender_id || row.ledger_id !== updateNode.ledger_id) {
+                        throw '提交数据错误3';
+                    }
+                    let updateData;
+                    if (this._checkCalcField(row)) {
+                        const calcData = this.ctx.helper.updateObj(updateNode, row);
+                        if (updateNode.is_leaf) {
+                            calcData.total_price = calcData.quantity * calcData.unit_price;
+                        }
+                        if (updateNode.total_price === undefined || Math.abs(calcData.total_price - updateNode.total_price) > zeroRange) {
+                            const pfp = updateNode.full_path.replace('.' + updateNode.ledger_id, '');
+                            if (updateMap[pfp]) {
+                                updateMap[pfp] = updateMap[pfp] + calcData.total_price - updateNode.total_price;
+                            } else {
+                                updateMap[pfp] = calcData.total_price - updateNode.total_price;
+                                updateFullPath.push(pfp);
+                            }
+                        }
+                        const data1 = this._filterChangedField(updateNode, calcData);
+                        updateData = this._filterUpdateInvalidField(updateNode.id, data1);
+                    } else {
+                        updateData = this._filterUpdateInvalidField(updateNode.id, row);
+                    }
+                    await this.transaction.update(this.tableName, updateData);
+                }
+                await this._increCalcParent(tenderId, updateMap);
+                this.transaction.commit();
+            } catch (err) {
+                this.transaction.rollback();
+                throw err;
+            }
+
+            const result1 = await this.getDataByIds(ids);
+            if (updateFullPath.length > 0) {
+                const result2 = await this.getFullLevelDataByFullPath(tenderId, updateFullPath);
+                return result1.concat(result2);
+            } else {
+                return result1;
+            }
         }
     }
 

+ 1 - 1
config/config.local.js

@@ -19,7 +19,7 @@ module.exports = appInfo => {
             // 用户名
             user: 'root',
             // 密码
-            password: 'root',
+            password: 'admin',
             // 数据库名
             database: 'calculation',
         },

+ 1 - 1
test/app/lib/sql_builder.test.js

@@ -78,7 +78,7 @@ describe('test/app/lib/sql_builder.test.js', () => {
         const [sql, sqlParam] = sqlBuilder.build('table', 'update');
         const finalSql = app.mysql.format(sql, sqlParam);
 
-        const matchSql = "UPDATE `table` SET  `create_time` = `create_time` + 1, `office` = 2, `full_path` = Replace(`full_path`,'1.','2.') WHERE  `group_id` >= 1";
+        const matchSql = "UPDATE `table` SET  `create_time` = IF(IsNull(`create_time`), 0, `create_time`) + 1, `office` = 2, `full_path` = Replace(`full_path`,'1.','2.') WHERE  `group_id` >= 1";
         assert(finalSql === matchSql);
     });
 

+ 443 - 36
test/app/service/ledger.test.js

@@ -1,4 +1,3 @@
-
 /**
  * 标段--台账 模型 单元测试
  *
@@ -8,23 +7,7 @@
  */
 'use strict';
 
-/* const testNodeData = [
-    { ledger_id: 1, ledger_pid: -1, order: 1, level: 1, full_path: '1', code: '1' },
-    { ledger_id: 2, ledger_pid: 1, order: 1, level: 2, full_path: '1.2', code: '1-1' },
-    { ledger_id: 6, ledger_pid: 2, order: 1, level: 3, full_path: '1.2.6', code: '1-1-1' },
-    { ledger_id: 7, ledger_pid: 6, order: 1, level: 4, full_path: '1.2.6.7', code: '202-1' },
-    { ledger_id: 10, ledger_pid: 7, order: 2, level: 5, full_path: '1.2.6.7.10', code: '202-1-a' },
-    { ledger_id: 9, ledger_pid: 7, order: 1, level: 5, full_path: '1.2.6.7.9', code: '202-1-b' },
-    { ledger_id: 8, ledger_pid: 6, order: 2, level: 4, full_path: '1.2.6.8', code: '202-2' },
-    { ledger_id: 11, ledger_pid: 8, order: 1, level: 5, full_path: '1.2.6.8.11', code: '202-2-c' },
-    { ledger_id: 12, ledger_pid: 8, order: 2, level: 5, full_path: '1.2.6.8.12', code: '202-2-e' },
-    { ledger_id: 13, ledger_pid: 2, order: 2, level: 3, full_path: '1.2.13', code: '1-1-2' },
-    { ledger_id: 14, ledger_pid: 2, order: 3, level: 3, full_path: '1.2.14', code: '1-1-3' },
-    { ledger_id: 3, ledger_pid: 1, order: 2, level: 2, full_path: '1.3', code: '1-2' },
-    { ledger_id: 4, ledger_pid: 1, order: 3, level: 2, full_path: '1.4', code: '1-3' },
-    { ledger_id: 5, ledger_pid: 1, order: 4, level: 2, full_path: '1.5', code: '1-4' },
-];*/
-const testNodeData = [
+/*const testNodeData = [
     { ledger_id: 1, ledger_pid: -1, order: 1, level: 1, full_path: '1', code: '1', is_leaf: false },
     { ledger_id: 2, ledger_pid: 1, order: 1, level: 2, full_path: '1.2', code: '1-1', is_leaf: false },
     { ledger_id: 6, ledger_pid: 2, order: 1, level: 3, full_path: '1.2.6', code: '1-1-1', is_leaf: false },
@@ -41,6 +24,24 @@ const testNodeData = [
     { ledger_id: 4, ledger_pid: 1, order: 3, level: 2, full_path: '1.4', code: '1-3', is_leaf: false },
     { ledger_id: 16, ledger_pid: 4, order: 1, level: 3, full_path: '1.4.16', code: '1-3-1', is_leaf: true },
     { ledger_id: 5, ledger_pid: 1, order: 4, level: 2, full_path: '1.5', code: '1-4', is_leaf: true },
+];*/
+const testNodeData = [
+    { id: 1, pid: -1, order: 1, level: 1, full_path: '1', code: '1', is_leaf: false },
+    { id: 2, pid: 1, order: 1, level: 2, full_path: '1.2', code: '1-1', is_leaf: false },
+    { id: 6, pid: 2, order: 1, level: 3, full_path: '1.2.6', code: '1-1-1', is_leaf: false },
+    { id: 7, pid: 6, order: 1, level: 4, full_path: '1.2.6.7', code: '202-1', is_leaf: false },
+    { id: 10, pid: 7, order: 2, level: 5, full_path: '1.2.6.7.10', code: '202-1-a', is_leaf: true },
+    { id: 9, pid: 7, order: 1, level: 5, full_path: '1.2.6.7.9', code: '202-1-b', is_leaf: true },
+    { id: 8, pid: 6, order: 2, level: 4, full_path: '1.2.6.8', code: '202-2', is_leaf: false },
+    { id: 11, pid: 8, order: 1, level: 5, full_path: '1.2.6.8.11', code: '202-2-c', is_leaf: true },
+    { id: 12, pid: 8, order: 2, level: 5, full_path: '1.2.6.8.12', code: '202-2-e', is_leaf: true },
+    { id: 13, pid: 2, order: 2, level: 3, full_path: '1.2.13', code: '1-1-2', is_leaf: true },
+    { id: 14, pid: 2, order: 3, level: 3, full_path: '1.2.14', code: '1-1-3', is_leaf: true },
+    { id: 3, pid: 1, order: 2, level: 2, full_path: '1.3', code: '1-2', is_leaf: false },
+    { id: 15, pid: 3, order: 1, level: 3, full_path: '1.3.15', code: '1-2-1', is_leaf: true },
+    { id: 4, pid: 1, order: 3, level: 2, full_path: '1.4', code: '1-3', is_leaf: false },
+    { id: 16, pid: 4, order: 1, level: 3, full_path: '1.4.16', code: '1-3-1', is_leaf: true },
+    { id: 5, pid: 1, order: 4, level: 2, full_path: '1.5', code: '1-4', is_leaf: true },
 ];
 const testTenderId = 3;
 
@@ -59,13 +60,15 @@ describe('test/app/service/ledger.test.js', () => {
         const result = yield ctx.service.ledger.db.delete(ctx.service.ledger.tableName, { tender_id: testTenderId });
         assert(result.affectedRows >= 0);
     });
-    it('add test data', function* () {
+    it('add test data(test add)', function* () {
         const ctx = app.mockContext();
         for (const data of testNodeData) {
             data.tender_id = testTenderId;
         }
-        const result = yield ctx.service.ledger.db.insert(ctx.service.ledger.tableName, testNodeData);
-        assert(result.affectedRows === testNodeData.length);
+        //const result = yield ctx.service.ledger.db.insert(ctx.service.ledger.tableName, testNodeData);
+        //assert(result.affectedRows === testNodeData.length);
+        const result = yield ctx.service.ledger.add(testNodeData, testTenderId);
+        assert(result);
         ctx.service.ledger.cache.set('tender_node_maxId:' + testTenderId, 16, 'EX', ctx.app.config.cacheTime);
     });
     /* 期望运行结果:
@@ -120,6 +123,16 @@ describe('test/app/service/ledger.test.js', () => {
         node = findById(result, 9);
         assert(node.full_path === '1.2.6.7.9');
     });
+    it('test getDataByIds', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点202-1
+        const node = yield ctx.service.ledger.getDataByNodeId(testTenderId, 7);
+        const result = yield ctx.service.ledger.getDataByIds([node.id]);
+        assert(result.length === 1);
+        assert(node.code === result[0].code);
+        assert(node.full_path === result[0].full_path);
+    });
     it('test getLastChildData', function* () {
         const ctx = app.mockContext();
 
@@ -163,10 +176,25 @@ describe('test/app/service/ledger.test.js', () => {
         assert(node);
         assert(node.full_path === '1.2.6.8.12');
     });
+    it('test getNextsData', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点1-1-1的全部子节点
+        const result = yield ctx.service.ledger.getNextsData(testTenderId, 2, 1);
+        assert(result.length === 2);
+
+        let node = findById(result, 13);
+        assert(node);
+        assert(node.code === '1-1-2');
+
+        node = findById(result, 14);
+        assert(node);
+        assert(node.code === '1-1-3');
+    });
     it('test getDataByFullPath', function* () {
         const ctx = app.mockContext();
 
-        // 查询节点202-1最后一个子节点
+        // 查询节点202-2及其子节点
         const result = yield ctx.service.ledger.getDataByFullPath(testTenderId, '1.2.6.8%');
         assert(result.length === 3);
 
@@ -181,24 +209,24 @@ describe('test/app/service/ledger.test.js', () => {
         node = findById(result, 12);
         assert(node);
         assert(node.full_path === '1.2.6.8.12');
+
+        // 查询1-1-1的子孙节点
+        const result1 = yield ctx.service.ledger.getDataByFullPath(testTenderId, '1.2.6.%');
+        assert(result1.length === 6);
     });
-    it('test getNextsData', function* () {
+    it('test getFullLevelDataByFullPath', function* () {
         const ctx = app.mockContext();
 
-        // 查询节点202-1最后一个子节点
-        const result = yield ctx.service.ledger.getNextsData(testTenderId, 2, 1);
-        assert(result.length === 2);
-
-        let node = findById(result, 13);
-        assert(node);
-        assert(node.code === '1-1-2');
+        // 查询202-2-c及其全部父节点
+        const result1 = yield ctx.service.ledger.getFullLevelDataByFullPath(testTenderId, '1.2.6.8.11');
+        assert(result1.length === 5);
 
-        node = findById(result, 14);
-        assert(node);
-        assert(node.code === '1-1-3');
+        const result2 = yield ctx.service.ledger.getFullLevelDataByFullPath(testTenderId, ['1.2.6.8.11', '1.2.6.7.9']);
+        assert(result2.length === 7);
     });
 
     // 测试CUD类方法
+    // 基本树结构操作
     it('test addNode', function* () {
         const ctx = app.mockContext();
         // 选中1-1-1,插入节点
@@ -307,7 +335,7 @@ describe('test/app/service/ledger.test.js', () => {
         // 选中 1-1-2 升级
         const resultData = yield ctx.service.ledger.upLevelNode(testTenderId, 13);
         assert(resultData);
-        assert(resultData.update.length === 5);
+        assert(resultData.update.length === 6);
 
         let node = findById(resultData.update, 13);
         assert(node.full_path === '1.13');
@@ -378,6 +406,7 @@ describe('test/app/service/ledger.test.js', () => {
         │       └── 1-3-1
         └── 1-4
      */
+    // 复制整块
     it('test pasteBlock', function* () {
         const ctx = app.mockContext();
         // 选中1-2-1, 粘贴1-1-1和new
@@ -416,6 +445,7 @@ describe('test/app/service/ledger.test.js', () => {
         │       └── 1-3-1
         └── 1-4
      */
+    // 增量计算
     it('test updateInfo', function* () {
         const ctx = app.mockContext();
 
@@ -438,7 +468,7 @@ describe('test/app/service/ledger.test.js', () => {
         │   │   └── 202-2
         │   │       ├── 202-2-c
         │   │       └── 202-2-e
-        │   └── 1-1-3
+        │   └── 1-1-4
         ├── 1-1-2
         │   └── 1-1-3
         ├── 1-2
@@ -455,7 +485,7 @@ describe('test/app/service/ledger.test.js', () => {
     it('test updateInfos', function* () {
         const ctx = app.mockContext();
 
-        // 修改new(id=17)的code 为 1-1-4
+        // 修改1-1-1(id=18)的code 为 1-2-2、修改new(id=22)的code 为 1-2-3
         const node1 = yield ctx.service.ledger.getDataByNodeId(testTenderId, 18);
         assert(node1);
         const node2 = yield ctx.service.ledger.getDataByNodeId(testTenderId, 22);
@@ -483,6 +513,383 @@ describe('test/app/service/ledger.test.js', () => {
         │   │   └── 202-2
         │   │       ├── 202-2-c
         │   │       └── 202-2-e
+        │   └── 1-1-4
+        ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   ├── 1-2-1
+        │   ├── 1-2-2
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    it('test updateCalc - update 1', function* () {
+        const ctx = app.mockContext();
+        // 修改202-2-e(1-1-1下)(id=12)的quantity为2.00000001, unit_price位3.0000005
+        const qty = 2.00000001;
+        const up = 3.0000005;
+        const tp = 6.00000103;
+        const node1 = yield ctx.service.ledger.getDataByNodeId(testTenderId, 12);
+        assert(node1);
+        const resultData = yield ctx.service.ledger.updateCalc(testTenderId, {
+            id: node1.id,
+            tender_id: node1.tender_id,
+            ledger_id: node1.ledger_id,
+            quantity: qty,
+            unit_price: up
+        });
+        assert(resultData.length === 5);
+        let node = findById(resultData, 12);
+        assert(node.total_price.toFixed(8) == tp);
+        node = findById(resultData, 8);
+        assert(node.total_price.toFixed(8) == tp);
+        node = findById(resultData, 6);
+        assert(node.total_price.toFixed(8) == tp);
+        node = findById(resultData, 2);
+        assert(node.total_price.toFixed(8) == tp);
+        node = findById(resultData, 1);
+        assert(node.total_price.toFixed(8) == tp);
+    });
+    /* 期望运行结果:
+        1                                                   6.00000103
+        ├── 1-1                                             6.00000103
+        │   ├── 1-1-1                                       6.00000103
+        │   │   └── 202-2                                   6.00000103
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e     2.00000001  3.0000005   6.00000103
+        │   └── 1-1-4
+        ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   ├── 1-2-1
+        │   ├── 1-2-2
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    it('test updateCalc - update N', function* () {
+        const ctx = app.mockContext();
+        // 修改202-2-c(1-1-1下)(id=11)的quantity为4.00000025, unit_price为6.0000083
+        //    202-2-c(1-2-2下)(id=20)的quantity为2.0000001, unit_price为5.000065
+        //    202-2-e(1-2-2下)(id=21)的quantity为8.0000579, unit_price为4.0000086
+        const qty = [4.00000025, 2.0000001, 8.0000579];
+        const up = [6.0000083, 5.000065, 4.0000086];
+        const tp = [24.0000347, 10.0001305, 32.0003004];
+        const sum = [30.00003573, 42.0004309, 72.00046663]
+        const node1 = yield ctx.service.ledger.getDataByNodeId(testTenderId, 11);
+        assert(node1);
+        const node2 = yield ctx.service.ledger.getDataByNodeId(testTenderId, 20);
+        assert(node2);
+        const node3 = yield ctx.service.ledger.getDataByNodeId(testTenderId, 21);
+        assert(node3);
+
+        const resultData = yield ctx.service.ledger.updateCalc(testTenderId, [{
+            id: node1.id,
+            tender_id: node1.tender_id,
+            ledger_id: node1.ledger_id,
+            quantity: qty[0],
+            unit_price: up[0]
+        }, {
+            id: node2.id,
+            tender_id: node2.tender_id,
+            ledger_id: node2.ledger_id,
+            quantity: qty[1],
+            unit_price: up[1]
+        }, {
+            id: node3.id,
+            tender_id: node3.tender_id,
+            ledger_id: node3.ledger_id,
+            quantity: qty[2],
+            unit_price: up[2]
+        }]);
+
+        assert(resultData.length === 10);
+
+        let node = findById(resultData, 11);
+        assert(node.total_price.toFixed(8) == tp[0]);
+
+        node = findById(resultData, 8);
+        assert(node.total_price.toFixed(8) == sum[0]);
+        node = findById(resultData, 6);
+        assert(node.total_price.toFixed(8) == sum[0]);
+        node = findById(resultData, 2);
+        assert(node.total_price.toFixed(8) == sum[0]);
+
+        node = findById(resultData, 20);
+        assert(node.total_price.toFixed(8) == tp[1]);
+        node = findById(resultData, 21);
+        assert(node.total_price.toFixed(8) == tp[2]);
+
+        node = findById(resultData, 19);
+        assert(node.total_price.toFixed(8) == sum[1]);
+        node = findById(resultData, 18);
+        assert(node.total_price.toFixed(8) == sum[1]);
+        node = findById(resultData, 3);
+        assert(node.total_price.toFixed(8) == sum[1]);
+
+        node = findById(resultData, 1);
+        assert(node.total_price.toFixed(8) == sum[2]);
+    });
+    /* 期望运行结果:
+        1                                                   72.00046663
+        ├── 1-1                                             30.00003573
+        │   ├── 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-2
+        │   └── 1-1-3
+        ├── 1-2                                             42.0004309
+        │   ├── 1-2-1
+        │   ├── 1-2-2                                       42.0004309
+        │   │   └── 202-2                                   42.0004309
+        │   │       ├── 202-2-c     2.0000001   5.000065    10.0001305
+        │   │       └── 202-2-e     8.0000579   4.0000086   32.0003004
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    // 复制整块+实时计算
+    it('test pasteBlock - with Increment Calculate', function* () {
+        const ctx = app.mockContext();
+        // 选中1-1-4, 粘贴202-2(1-1-1下)
+        const resultData = yield ctx.service.ledger.pasteBlock(testTenderId, 17, [8]);
+
+        assert(resultData.update.length === 2);
+
+        let node = findById(resultData.update, 2);
+        assert(node.total_price.toFixed(8) == 60.00007146);
+
+        node = findById(resultData.update, 1);
+        assert(node.total_price.toFixed(8) == 102.00050236);
+    });
+    /* 期望运行结果:
+        1                                                   102.00050236
+        ├── 1-1                                             60.00007146
+        │   ├── 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
+        │   └── 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                                             42.0004309
+        │   ├── 1-2-1
+        │   ├── 1-2-2                                       42.0004309
+        │   │   └── 202-2                                   42.0004309
+        │   │       ├── 202-2-c     2.0000001   5.000065    10.0001305
+        │   │       └── 202-2-e     8.0000579   4.0000086   32.0003004
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    // 树结构基本操作+实时计算
+    it('test downLevel - with Increment Calculate', function* () {
+        const ctx = app.mockContext();
+        // 选中202-2(1-1-4后兄弟节点) 降级
+        const resultData = yield ctx.service.ledger.downLevelNode(testTenderId, 23);
+        assert(resultData.update.length === 4);
+        const node = findById(resultData.update, 17);
+        assert(node.total_price.toFixed(8) == 30.00003573);
+    });
+    /* 期望运行结果:
+        1                                                   102.00050236
+        ├── 1-1                                             60.00007146
+        │   ├── 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                                       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-2
+        │   └── 1-1-3
+        ├── 1-2                                             42.0004309
+        │   ├── 1-2-1
+        │   ├── 1-2-2                                       42.0004309
+        │   │   └── 202-2                                   42.0004309
+        │   │       ├── 202-2-c     2.0000001   5.000065    10.0001305
+        │   │       └── 202-2-e     8.0000579   4.0000086   32.0003004
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    it('test upLevel - with Increment Calculate', function* () {
+        const ctx = app.mockContext();
+        yield ctx.service.ledger.pasteBlock(testTenderId, 23, [23]);
+        /* 期望运行结果:
+            1                                                   132.00053809
+            ├── 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                                       60.00007146
+            │       ├── 202-2                                   30.00003573
+            │       │   ├── 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                                             42.0004309
+            │   ├── 1-2-1
+            │   ├── 1-2-2                                       42.0004309
+            │   │   └── 202-2                                   42.0004309
+            │   │       ├── 202-2-c     2.0000001   5.000065    10.0001305
+            │   │       └── 202-2-e     8.0000579   4.0000086   32.0003004
+            │   ├── 1-2-3
+            │   └── 1-3
+            │       └── 1-3-1
+            └── 1-4
+         */
+        // 选中202-2-c(1-1-4下)(id=23)升级
+        const resultData = yield ctx.service.ledger.upLevelNode(testTenderId, 23);
+        assert(resultData.update.length === 7);
+
+        let node = findById(resultData.update, 23);
+        assert(node.total_price.toFixed(8) == 60.00007146);
+        node = findById(resultData.update, 17);
+        assert(node.total_price.toFixed(8) == 0);
+    });
+    /* 期望运行结果:
+        1                                                   132.00053809
+        ├── 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
+        │   └── 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                                             42.0004309
+        │   ├── 1-2-1
+        │   ├── 1-2-2                                       42.0004309
+        │   │   └── 202-2                                   42.0004309
+        │   │       ├── 202-2-c     2.0000001   5.000065    10.0001305
+        │   │       └── 202-2-e     8.0000579   4.0000086   32.0003004
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    it('test deleteNode - with Increment Calculate', function* () {
+        const ctx = app.mockContext();
+
+        // 选中202-2-c(1-2-2下)(id=20),删除节点
+        const resultData = yield ctx.service.ledger.deleteNode(testTenderId, 20);
+        assert(resultData.delete.length === 1);
+        assert(resultData.update.length === 5);
+
+        let node = findById(resultData.update, 21);
+        assert(node.order === 1);
+
+        node = findById(resultData.update, 19);
+        assert(node.total_price.toFixed(8) == 32.0003004);
+
+        node = findById(resultData.update, 18);
+        assert(node.total_price.toFixed(8) == 32.0003004);
+
+        node = findById(resultData.update, 3);
+        assert(node.total_price.toFixed(8) == 32.0003004);
+
+        node = findById(resultData.update, 1);
+        assert(node.total_price.toFixed(8) == 122.00040759);
+    });
+    /* 期望运行结果:
+        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
+        │   └── 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 addUpChildren', function* () {
+        const ctx = app.mockContext();
+
+        // 统计202-2(id=23)前两个子节点的金额
+        const result1 = yield ctx.service.ledger.addUpChildren(testTenderId, 23, 2, '<=');
+        assert(result1 && result1.toFixed(8) == 30.00003573);
+        // 统计202-2(id=23)后两个子节点的金额
+        const result2 = yield ctx.service.ledger.addUpChildren(testTenderId, 23, 2, '>=');
+        assert(result2 && result2.toFixed(8) == 36.00003676);
+        // 统计202-2(id=23)全部子节点的金额
+        const result3 = yield ctx.service.ledger.addUpChildren(testTenderId, 23, 0, '>');
+        assert(result3 && result3.toFixed(8) == 60.00007146);
+    });
+
+    // 小数位数策略示例:
+    /*  先加总再保留3位小数:
+        1                                        35.585
+        ├── 1-1                                  35.585
+        │   ├── 1-1-1                            35.585
+        │   │   └── 202-2                        35.585
+        │   │       ├── 202-2-c     4.25  6.83   29.028(29.0275)
+        │   │       └── 202-2-e     2.15  3.05   6.558(6.5575)
+        │   └── 1-1-3
+        ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   ├── 1-2-1
+        │   ├── 1-2-2
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   ├── 1-2-3
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    /*  先保留3位小数再加总:
+        1                                        35.586
+        ├── 1-1                                  35.586
+        │   ├── 1-1-1                            35.586
+        │   │   └── 202-2                        35.586
+        │   │       ├── 202-2-c     4.25  6.83   29.028(29.0275)
+        │   │       └── 202-2-e     2.15  3.05   6.558(6.5575)
         │   └── 1-1-3
         ├── 1-1-2
         │   └── 1-1-3