浏览代码

1. 台账分解,显示当前打开标段,标段列表
2. 台账分解,切换标段
3. 台账分解,编辑单元格、复制粘贴,删除单元格数据
4. 台账分解,复制粘贴整块
5. 后台,台账分解模块,补全单元测试,测试覆盖全部public方法

MaiXinRong 7 年之前
父节点
当前提交
90e0218558

+ 44 - 9
app/controller/ledger_controller.js

@@ -60,7 +60,7 @@ module.exports = app => {
                 const ledgerData = await ctx.service.ledger.getDataByTenderId(tenderId);
                 const renderData = {
                     ledger: JSON.stringify(ledgerData),
-                    tenderList: tenderList
+                    tenderList,
                 };
                 await this.layout('ledger/explode.ejs', renderData);
             }
@@ -69,13 +69,13 @@ module.exports = app => {
         /**
          * 获取子节点
          * @param ctx
-         * @returns {Promise<void>}
+         * @return {Promise<void>}
          */
         async getChildren(ctx) {
             const responseData = {
                 err: 0,
                 msg: '',
-                data: []
+                data: [],
             };
             try {
                 const tenderId = ctx.session.sessionUser.tenderId;
@@ -95,18 +95,18 @@ module.exports = app => {
             }
 
             ctx.body = responseData;
-        };
+        }
 
         /**
          * 树结构基本操作(增、删、上下移、升降级)
          * @param {Object} ctx - egg全局变量
-         * @returns {Promise<void>}
+         * @return {Promise<void>}
          */
         async baseOperation(ctx) {
             const responseData = {
                 err: 0,
                 msg: '',
-                data: []
+                data: [],
             };
             try {
                 const tenderId = ctx.session.sessionUser.tenderId;
@@ -146,13 +146,18 @@ module.exports = app => {
             }
 
             ctx.body = responseData;
-        };
+        }
 
+        /**
+         * 提交更新数据
+         * @param ctx
+         * @return {Promise<void>}
+         */
         async updateInfo(ctx) {
             const responseData = {
                 err: 0,
                 msg: '',
-                data: []
+                data: [],
             };
             try {
                 const tenderId = ctx.session.sessionUser.tenderId;
@@ -171,7 +176,37 @@ module.exports = app => {
             }
 
             ctx.body = responseData;
-        };
+        }
+
+        /**
+         * 复制粘贴整块
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async pasteBlock(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.block || data.block.length <= 0)) {
+                    throw '参数错误';
+                }
+
+                responseData.data = await ctx.service.ledger.pasteBlock(tenderId, data.id, data.block);
+            } catch (err) {
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+
+            ctx.body = responseData;
+        }
 
         /**
          * 台账变更页面

+ 177 - 1
app/public/js/ledger.js

@@ -8,12 +8,14 @@
 $(document).ready(function() {
     autoFlashHeight();
     const ledgerSpread = SpreadJsObj.createNewSpread($('#ledger-spread')[0]);
+    SpreadJsObj.addDeleteBind(ledgerSpread);
     const ledgerTree = createNewPathTree({
         id: 'ledger_id',
         pid: 'ledger_pid',
         order: 'order',
         level: 'level',
-        rootId: -1
+        rootId: -1,
+        keys: ['id', 'tender_id', 'ledger_id']
     });
     ledgerTree.loadDatas(ledger);
     SpreadJsObj.initSheet(ledgerSpread.getActiveSheet(), {
@@ -35,6 +37,11 @@ $(document).ready(function() {
     SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), 'tree', ledgerTree);
 
     const treeOperationObj = {
+        /**
+         * 刷新顶部按钮是否可用
+         * @param sheet
+         * @param selections
+         */
         refreshOperationValid: function (sheet, selections) {
             const setObjEnable = function (obj, enable) {
                 if (enable) {
@@ -200,12 +207,129 @@ $(document).ready(function() {
                 sheet.repaint();
                 self.refreshOperationValid(sheet, sheet.getSelections());
             });
+        },
+        /**
+         * 编辑单元格响应事件
+         * @param {Object} e
+         * @param {Object} info
+         */
+        editEnded: function (e, info) {
+            if (info.sheet.zh_setting) {
+                const col = info.sheet.zh_setting.cols[info.col];
+                const sortData = info.sheet.zh_dataType === 'tree' ? info.sheet.zh_tree.nodes : info.sheet.zh_data;
+                const node = sortData[info.row];
+                const data = {
+                    id: node.id,
+                    tender_id: node.tender_id,
+                    ledger_id: node.ledger_id
+                };
+                data[col.field] = info.editingText;
+
+                info.sheet.zh_tree.updateInfo('update-info', [data], function (result) {
+                    const rows = [];
+                    for (const data of result) {
+                        rows.push(sortData.indexOf(data));
+                    }
+                    SpreadJsObj.reLoadRowsData(info.sheet, rows);
+                });
+            }
+        },
+        /**
+         * 粘贴单元格响应事件
+         * @param e
+         * @param info
+         */
+        clipboardPasted: function (e, info) {
+            if (info.sheet.zh_setting && info.sheet.zh_dataType === 'tree') {
+                const sortData = info.sheet.zh_tree.nodes;
+                const datas = [], nodes = [];
+                for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
+                    const curRow = info.cellRange.row + iRow;
+                    const node = sortData[curRow];
+                    if (node) {
+                        const data = info.sheet.zh_tree.getNodeKeyData(node);
+                        for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                            const curCol = info.cellRange.col + iCol;
+                            const colSetting = info.sheet.zh_setting.cols[curCol];
+                            data[colSetting.field] = info.sheet.getText(curRow, curCol);
+                        }
+                        datas.push(data);
+                        nodes.push(node);
+                    }
+                }
+                info.sheet.zh_tree.updateInfo('update-info', datas, function (result) {
+                    const rows = [];
+                    for (const data of result) {
+                        rows.push(sortData.indexOf(data));
+                    }
+                    SpreadJsObj.reLoadRowsData(info.sheet, rows);
+                });
+            }
+        },
+        deletePress: function (sheet) {
+            if (sheet.zh_setting && sheet.zh_dataType === 'tree') {
+                const sortData = sheet.zh_tree.nodes;
+                const datas = [], nodes = [];
+                const sel = sheet.getSelections()[0];
+                for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                    const node = sortData[iRow];
+                    if (node) {
+                        const data = sheet.zh_tree.getNodeKeyData(node);
+                        for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                            const colSetting = sheet.zh_setting.cols[iCol];
+                            data[colSetting.field] = null;
+                        }
+                        datas.push(data);
+                        nodes.push(node);
+                    }
+                }
+                sheet.zh_tree.updateInfo('update-info', datas, function (result) {
+                    const rows = [];
+                    for (const data of result) {
+                        rows.push(sortData.indexOf(data));
+                    }
+                    SpreadJsObj.reLoadRowsData(sheet, rows);
+                });
+            }
+        },
+        pasteBlock: function (spread, block) {
+            const self = this;
+            const sheet = spread.getActiveSheet();
+            const row = sheet.getSelections()[0].row;
+
+            const tree = sheet.zh_tree;
+            if (!tree) { return; }
+
+            const node = tree.nodes[row];
+            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);
+                        }
+                    }
+                    self.refreshOperationValid(sheet, sheet.getSelections());
+                });
+            });
         }
     };
 
     ledgerSpread.bind(GC.Spread.Sheets.Events.SelectionChanged, function (e, info) {
         treeOperationObj.refreshOperationValid(info.sheet, info.newSelections);
     });
+    ledgerSpread.bind(GC.Spread.Sheets.Events.EditEnded, treeOperationObj.editEnded);
+    ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardPasted, treeOperationObj.clipboardPasted);
+    SpreadJsObj.addDeleteBind(ledgerSpread, treeOperationObj.deletePress);
 
     // 绑定 删除等 顶部按钮
     $('#delete').click(function () {
@@ -260,6 +384,58 @@ $(document).ready(function() {
                     const select = ledgerTree.nodes[row];
                     return select;
                 }
+            },
+            'copyBlock': {
+                name: '复制整块',
+                icon: 'fa-files-o',
+                callback: function (key, opt) {
+                    /*ledgerSpread.commandManager().execute({
+                        cmd:"copy",
+                        sheetName:ledgerSpread.getActiveSheet().name()
+                    });*/
+                    treeOperationObj.block = [];
+                    const copyBlockList = [];
+                    const sheet = ledgerSpread.getActiveSheet();
+                    const sel = sheet.getSelections()[0];
+                    let iRow = sel.row;
+                    const pid = sheet.zh_tree.nodes[iRow].ledger_pid;
+                    while (iRow < sel.row + sel.rowCount) {
+                        const node = sheet.zh_tree.nodes[iRow];
+                        if (node.ledger_pid !== pid) {
+                            toast('error: 仅可同时选中同层节点', 'error', 'exclamation-circle');
+                            return;
+                        }
+                        copyBlockList.push(node.ledger_id);
+                        iRow += sheet.zh_tree.getPosterity(node).length + 1;
+                    }
+                    treeOperationObj.block = copyBlockList;
+                },
+                visible: function (key, opt) {
+                    const sheet = ledgerSpread.getActiveSheet();
+                    const selection = sheet.getSelections();
+                    const row = selection[0].row;
+                    const select = ledgerTree.nodes[row];
+                    return select;
+                }
+            },
+            'pasteBlock': {
+                name: '粘贴',
+                icon: 'fa-clipboard',
+                disabled: function (key, opt) {
+                    const block = treeOperationObj.block || [];
+                    return block.length <= 0;
+                },
+                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()
+                        });
+                    }*/
+                }
             }
         }
     });

+ 54 - 3
app/public/js/path_tree.js

@@ -2,7 +2,7 @@
 const createNewPathTree = function (setting) {
     const treeSetting = JSON.parse(JSON.stringify(setting));
     const itemsPre = 'id_';
-    const postData = function (url, data, successCallback) {
+    const postData = function (url, data, successCallback, errowCallBack) {
         $.ajax({
             type:"POST",
             url: url,
@@ -21,10 +21,16 @@ const createNewPathTree = function (setting) {
                     }
                 } else {
                     toast('error: ' + result.message, 'error', 'exclamation-circle');
+                    if (errowCallBack) {
+                        errowCallBack();
+                    }
                 }
             },
             error: function(jqXHR, textStatus, errorThrown){
                 toast('error ' + textStatus + " " + errorThrown, 'error', 'exclamation-circle');
+                if (errowCallBack) {
+                    errowCallBack();
+                }
             }
         });
     };
@@ -85,7 +91,7 @@ const createNewPathTree = function (setting) {
             let node = this.getItems(data[treeSetting.id]);
             if (node) {
                 for (const prop in node) {
-                    if (data[prop]) {
+                    if (data[prop] !== undefined) {
                         node[prop] = data[prop];
                     }
                 }
@@ -190,6 +196,14 @@ const createNewPathTree = function (setting) {
         this._refreshChildrenVisible(node);
     };
 
+    proto.getNodeKeyData = function (node) {
+        const data = {};
+        for (const key of treeSetting.keys) {
+            data[key] = node[key];
+        }
+        return data;
+    }
+
     /**
      * 以下方法需等待响应, 通过callback刷新界面
      */
@@ -230,7 +244,44 @@ const createNewPathTree = function (setting) {
                 result.delete = self._freeData(datas.delete);
             }
             callback(result);
-        })
+        });
+    };
+    proto.updateInfo = function (url, updateData, callback) {
+        const self = this;
+        postData(url, updateData, function (datas) {
+            const result = self._loadData(datas);
+            callback(result);
+        }, function () {
+            if (updateData instanceof Array) {
+                const result = [];
+                for (const data of updateData) {
+                    result.push(self.getItems(data[treeSetting.id]));
+                }
+                callback(result);
+            } else {
+                callback([self.getItems(updateData[treeSetting.id])]);
+            }
+        });
+    };
+    proto.pasteBlock = function (url, node, block, callback) {
+        const self = this;
+        const data = {
+            id: node[treeSetting.id],
+            block: block
+        };
+        postData(url, data, function (datas) {
+            const result = {};
+            if (datas.update) {
+                result.update = self._loadData(datas.update);
+            }
+            if (datas.create) {
+                result.create = self._loadData(datas.create);
+            }
+            if (datas.delete) {
+                result.delete = self._freeData(datas.delete);
+            }
+            callback(result);
+        });
     };
 
     return new PathTree();

+ 47 - 5
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -129,12 +129,15 @@ const SpreadJsObj = {
     },
     /**
      * sheet中 使用delete键,触发EndEdited事件
-     * @param {GC.Spreads.Sheets.Worksheet} sheet
+     * @param {GC.Spreads.Sheets.Workbook} spread
+     * @param {function} fun
      */
-    addDeleteBind: function (sheet) {
-        sheet.addKeyMap(46, false, false, false, false, function () {
-            let selections = sheet.getSelections();
+    addDeleteBind: function (spread, fun) {
+        spread.commandManager().register('deleteEvent', function () {
+            fun(spread.getActiveSheet());
         });
+        spread.commandManager().setShortcutKey('null', GC.Spread.Commands.Key.del, false, false, false, false);
+        spread.commandManager().setShortcutKey('deleteEvent', GC.Spread.Commands.Key.del, false, false, false, false);
     },
     /**
      * 根据sheet.zh_setting初始化sheet表头
@@ -213,7 +216,7 @@ const SpreadJsObj = {
     },
     /**
      * 重新加载部分数据行
-     * @param sheet
+     * @param {GC.Spread.Sheets.Worksheet} sheet
      * @param {Number} row
      * @param {Number} count
      */
@@ -253,6 +256,45 @@ const SpreadJsObj = {
         });
     },
     /**
+     * 重新加载部分行数据
+     * @param {GC.Spread.Sheets.Worksheet} sheet
+     * @param {Array} rows
+     */
+    reLoadRowsData: function (sheet, rows) {
+        this.massOperationSheet(sheet, function () {
+            const sortData = sheet.zh_dataType === 'tree' ? sheet.zh_tree.nodes : sheet.zh_data;
+            for (const row of rows) {
+                // 清空原单元格数据
+                sheet.clear(row, -1, 1, -1, GC.Spread.Sheets.SheetArea.viewport, GC.Spread.Sheets.StorageType.data);
+                const data = sortData[row];
+                // 单元格重新写入数据
+                sheet.zh_setting.cols.forEach(function (col, j) {
+                    // 设置值
+                    const cell = sheet.getCell(row, j);
+                    if (col.field !== '' && data[col.field]) {
+                        cell.value(data[col.field]).locked(col.readOnly || false);
+                    } else {
+                        cell.locked(col.readOnly || false);
+                    }
+                    // 设置单元格格式
+                    if (col.cellType) {
+                        if (col.cellType === 'tree') {
+                            if (!sheet.extendCellType.tree) {
+                                sheet.extendCellType.tree = SpreadJsExtendCellType.getTreeNodeCellType();
+                            }
+                            sheet.getRange(row, j, 1, 1).cellType(sheet.extendCellType.tree);
+                        } else if (col.cellType === 'tip') {
+                            if (!sheet.extendCellType.tip) {
+                                sheet.extendCellType.tip = SpreadJsExtendCellType.getTipCellType();
+                            }
+                            sheet.getRange(row, j, 1, 1).cellType(sheet.extendCellType.tip);
+                        }
+                    }
+                });
+            };
+        });
+    },
+    /**
      * 根据data加载sheet数据,合并了一般数据和树结构数据的加载
      * @param {GC.Spread.Sheets.Worksheet} sheet
      * @param {String} dataType - 1.'zh_data' 2.'zh_tree'

+ 2 - 0
app/router.js

@@ -35,6 +35,8 @@ 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-info', sessionAuth, 'ledgerController.updateInfo');
+    app.post('/ledger/paste-block', sessionAuth, 'ledgerController.pasteBlock');
     app.get('/ledger/change', sessionAuth, 'ledgerController.change');
     app.get('/ledger/index', sessionAuth, 'ledgerController.index');
 

+ 291 - 66
app/service/ledger.js

@@ -13,8 +13,15 @@ const needField = {
     pid: 'ledger_pid',
     order: 'order',
     level: 'level',
-    fullPath: 'full_path'
-}
+    fullPath: 'full_path',
+    isLeaf: 'is_leaf',
+};
+const keyFields = {
+    table: ['id'],
+    index: ['tender_id', 'ledger_id'],
+};
+// 以下字段仅可通过树结构操作改变,不可直接通过update方式从接口提交,发现时过滤
+const readOnlyFields = ['id', 'tender_id', 'ledger_id', 'ledger_pid', 'order', 'level', 'full_path', 'is_leaf'];
 
 module.exports = app => {
 
@@ -132,20 +139,47 @@ module.exports = app => {
         }
 
         /**
+         * 根据节点Id获取数据
+         * @param {Number} tenderId - 标段Id
+         * @param {Array} nodesIds - 节点Id
+         * @return {Array}
+         */
+        async getDataByNodeIds(tenderId, nodesIds) {
+            if (tenderId <= 0) {
+                return [];
+            }
+
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('tender_id', {
+                value: tenderId,
+                operate: '=',
+            });
+            this.sqlBuilder.setAndWhere('ledger_id', {
+                value: nodesIds,
+                operate: 'in',
+            });
+
+            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
-         * @returns {Object}
+         * @return {Object}
          */
         async getLastChildData(tenderId, pid) {
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
                 value: tenderId,
-                operate: '='
+                operate: '=',
             });
             this.sqlBuilder.setAndWhere('ledger_pid', {
                 value: pid,
-                operate: '='
+                operate: '=',
             });
             this.sqlBuilder.orderBy = [['order', 'DESC']];
             const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
@@ -160,7 +194,7 @@ module.exports = app => {
          * @param {Number} tenderId - 标段id
          * @param {Number} pid - 父节点id
          * @param {Number|Array} order - 排序
-         * @returns {Object|Array} - 查询结果
+         * @return {Object|Array} - 查询结果
          */
         async getDataByParentAndOrder(tenderId, pid, order) {
             if ((tenderId <= 0) || (pid <= 0) || (order <= 0)) {
@@ -170,7 +204,7 @@ module.exports = app => {
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
                 value: tenderId,
-                operate: '='
+                operate: '=',
             });
             this.sqlBuilder.setAndWhere('ledger_pid', {
                 value: pid,
@@ -179,12 +213,12 @@ module.exports = app => {
             if (order instanceof Array) {
                 this.sqlBuilder.setAndWhere('order', {
                     value: order,
-                    operate: 'in'
+                    operate: 'in',
                 });
             } else {
                 this.sqlBuilder.setAndWhere('order', {
                     value: order,
-                    operate: '='
+                    operate: '=',
                 });
             }
 
@@ -203,7 +237,7 @@ module.exports = app => {
          * 根据 父节点id 获取子节点
          * @param tenderId
          * @param nodeId
-         * @returns {Promise<*>}
+         * @return {Promise<*>}
          */
         async getChildrenByParentId(tenderId, nodeId) {
             if ((nodeId <= 0) || (tenderId <= 0)) {
@@ -230,19 +264,19 @@ module.exports = app => {
          * 根据full_path获取数据 full_path Like ‘1.2.3%’(传参full_path = '1.2.3%')
          * @param {Number} tenderId - 标段id
          * @param {String} full_path - 路径
-         * @returns {Promise<void>}
+         * @return {Promise<void>}
          */
         async getDataByFullPath(tenderId, full_path) {
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
                 value: tenderId,
-                operate: '='
+                operate: '=',
             });
             this.sqlBuilder.setAndWhere('full_path', {
                 value: this.db.escape(full_path),
-                operate: 'Like'
+                operate: 'Like',
             });
-            let [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
             const resultData = await this.db.query(sql, sqlParam);
             return resultData;
         }
@@ -252,7 +286,7 @@ module.exports = app => {
          * @param {Number} tenderId - 标段id
          * @param {Number} pid - 父节点id
          * @param {Number} order - 排序
-         * @returns {Array}
+         * @return {Array}
          */
         async getNextsData(tenderId, pid, order) {
             if ((tenderId <= 0) || (pid <= 0) || (order <= 0)) {
@@ -262,7 +296,7 @@ module.exports = app => {
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
                 value: tenderId,
-                operate: '='
+                operate: '=',
             });
             this.sqlBuilder.setAndWhere('ledger_pid', {
                 value: pid,
@@ -270,7 +304,7 @@ module.exports = app => {
             });
             this.sqlBuilder.setAndWhere('order', {
                 value: order,
-                operate: '>'
+                operate: '>',
             });
 
             const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
@@ -345,8 +379,8 @@ module.exports = app => {
             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);
+                maxId = await this._getMaxNodeId(tenderId);
+                this.cache.set(cacheKey, maxId, 'EX', this.ctx.app.config.cacheTime);
             }
 
             data.tender_id = tenderId;
@@ -396,7 +430,7 @@ module.exports = app => {
             // 查询应返回的结果
             const createData = await this.getDataByParentAndOrder(selectData.tender_id, selectData.ledger_pid, [selectData.order + 1]);
             const updateData = await this.getNextsData(selectData.tender_id, selectData.ledger_pid, selectData.order + 1);
-            return {create: createData, update: updateData};
+            return { create: createData, update: updateData };
         }
 
         /**
@@ -425,28 +459,28 @@ module.exports = app => {
                 this.initSqlBuilder();
                 this.sqlBuilder.setAndWhere('tender_id', {
                     value: tenderId,
-                    operate: '='
+                    operate: '=',
                 });
                 this.sqlBuilder.setAndWhere('full_path', {
                     value: this.db.escape(selectData.full_path + '%'),
-                    operate: 'Like'
+                    operate: 'Like',
                 });
                 const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'delete');
                 const operate = await this.transaction.query(sql, sqlParam);
                 // 选中节点--父节点 只有一个子节点时,应升级is_leaf
                 if (parentData) {
-                    const count = this.db.count(this.tableName, {ledger_pid: selectData.ledger_pid});
+                    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
+                            is_leaf: true,
                         });
                     }
                 }
                 // 选中节点--全部后节点 order--
                 await this._updateSelectNextsOrder(selectData, -1);
                 await this.transaction.commit();
-            } catch(err) {
+            } catch (err) {
                 deleteData = [];
                 await this.transaction.rollback();
                 throw err;
@@ -461,7 +495,7 @@ module.exports = app => {
                     updateData.push(updateData2);
                 }
             }
-            return {delete: deleteData, update: updateData};
+            return { delete: deleteData, update: updateData };
         }
 
         /**
@@ -486,8 +520,8 @@ module.exports = app => {
 
             this.transaction = await this.db.beginTransaction();
             try {
-                const sData = await this.transaction.update(this.tableName, {id: selectData.id, order: selectData.order - 1});
-                const pData = await this.transaction.update(this.tableName, {id: preData.id, order: preData.order + 1});
+                const sData = await this.transaction.update(this.tableName, { id: selectData.id, order: selectData.order - 1 });
+                const pData = await this.transaction.update(this.tableName, { id: preData.id, order: preData.order + 1 });
                 this.transaction.commit();
             } catch (err) {
                 await this.transaction.rollback();
@@ -495,7 +529,7 @@ module.exports = app => {
             }
 
             const resultData = await this.getDataByParentAndOrder(tenderId, selectData.ledger_pid, [selectData.order, preData.order]);
-            return {update: resultData};
+            return { update: resultData };
         }
 
         /**
@@ -515,13 +549,13 @@ module.exports = app => {
             }
             const nextData = await this.getDataByParentAndOrder(tenderId, selectData.ledger_pid, selectData.order + 1);
             if (!nextData) {
-                throw '节点不可下移'
+                throw '节点不可下移';
             }
 
             this.transaction = await this.db.beginTransaction();
             try {
-                const sData = await this.transaction.update(this.tableName, {id: selectData.id, order: selectData.order + 1});
-                const pData = await this.transaction.update(this.tableName, {id: nextData.id, order: nextData.order - 1});
+                const sData = await this.transaction.update(this.tableName, { id: selectData.id, order: selectData.order + 1 });
+                const pData = await this.transaction.update(this.tableName, { id: nextData.id, order: nextData.order - 1 });
                 this.transaction.commit();
             } catch (err) {
                 await this.transaction.rollback();
@@ -529,7 +563,7 @@ module.exports = app => {
             }
 
             const resultData = await this.getDataByParentAndOrder(tenderId, selectData.ledger_pid, [selectData.order, nextData.order]);
-            return {update: resultData};
+            return { update: resultData };
         }
 
         /**
@@ -542,19 +576,19 @@ module.exports = app => {
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
                 value: selectData.tender_id,
-                operate: '='
+                operate: '=',
             });
             this.sqlBuilder.setAndWhere('full_path', {
                 value: this.db.escape(selectData.full_path + '.%'),
-                operate: 'like'
+                operate: 'like',
             });
             this.sqlBuilder.setUpdateData('level', {
                 value: 1,
-                selfOperate: '-'
+                selfOperate: '-',
             });
             this.sqlBuilder.setUpdateData('full_path', {
                 value: ['`full_path`', this.db.escape(selectData.ledger_pid + '.'), this.db.escape('')],
-                literal: 'Replace'
+                literal: 'Replace',
             });
             const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
             const data = this.transaction.query(sql, sqlParam);
@@ -576,29 +610,29 @@ module.exports = app => {
                 // 修改nextsData pid, 排序
                 this.initSqlBuilder();
                 this.sqlBuilder.setUpdateData('ledger_pid', {
-                    value: selectData.ledger_id
+                    value: selectData.ledger_id,
                 });
-                const orderInc = lastChildData ? lastChildData.order - selectData.order : - selectData.order;
+                const orderInc = lastChildData ? lastChildData.order - selectData.order : -selectData.order;
                 this.sqlBuilder.setUpdateData('order', {
                     value: Math.abs(orderInc),
-                    selfOperate: orderInc > 0 ? '+' : '-'
+                    selfOperate: orderInc > 0 ? '+' : '-',
                 });
                 this.sqlBuilder.setAndWhere('ledger_pid', {
                     value: selectData.ledger_pid,
-                    operate: '='
+                    operate: '=',
                 });
                 this.sqlBuilder.setAndWhere('order', {
                     value: selectData.order,
-                    operate: '>'
+                    operate: '>',
                 });
                 const [sql1, sqlParam1] = this.sqlBuilder.build(this.tableName, 'update');
                 await this.transaction.query(sql1, sqlParam1);
 
                 // 选中节点 is_leaf应为false
                 if (selectData.is_leaf) {
-                    const updateData = {id: selectData.id,
-                        is_leaf: false
-                    }
+                    const updateData = { id: selectData.id,
+                        is_leaf: false,
+                    };
                     await this.transaction.update(this.tableName, updateData);
                 }
 
@@ -607,7 +641,7 @@ module.exports = app => {
                 const newSubStr = this.db.escape(selectData.ledger_id + '.');
                 const sqlArr = [];
                 sqlArr.push('Update ?? SET `full_path` = Replace(`full_path`,' + oldSubStr + ',' + newSubStr + ') Where');
-                sqlArr.push('(`tender_id` = ' + selectData.tender_id +')');
+                sqlArr.push('(`tender_id` = ' + selectData.tender_id + ')');
                 sqlArr.push(' And (');
                 for (const data of nextsData) {
                     sqlArr.push('`full_path` Like ' + this.db.escape(data.full_path + '%'));
@@ -639,7 +673,7 @@ module.exports = app => {
             }
             const parentData = await this.getDataByNodeId(tenderId, selectData.ledger_pid);
             if (!parentData) {
-                throw '升级节点数据错误'
+                throw '升级节点数据错误';
             }
 
             this.transaction = await this.db.beginTransaction();
@@ -649,17 +683,17 @@ module.exports = app => {
                 if (selectData.order === 1) {
                     this.transaction.update(this.tableName, {
                         id: parentData.id,
-                        is_leaf: true
-                    })
+                        is_leaf: true,
+                    });
                 }
                 // 选中节点--父节点--全部后兄弟节点 order+1
                 await this._updateSelectNextsOrder(parentData);
                 // 选中节点 修改pid, order, full_path
-                const updateData = {id: selectData.id,
+                const updateData = { id: selectData.id,
                     ledger_pid: parentData.ledger_pid,
                     order: parentData.order + 1,
                     level: selectData.level - 1,
-                    full_path: newFullPath
+                    full_path: newFullPath,
                 };
                 await this.transaction.update(this.tableName, updateData);
                 // 选中节点--全部子节点(含孙) level-1, full_path变更
@@ -679,33 +713,33 @@ module.exports = app => {
                 const preParent = await this.getDataByNodeId(tenderId, parentData.ledger_id);
                 resultData2.push(preParent);
             }
-            return {update: resultData1.concat(resultData2)};
+            return { update: resultData1.concat(resultData2) };
         }
 
         /**
          * 降级selectData, 同步修改所有子节点
          * @param {Object} selectData - 选中节点
          * @param {Object} preData - 选中节点的前一节点(降级后为父节点)
-         * @returns {Promise<*>}
+         * @return {Promise<*>}
          * @private
          */
         async _syncDownlevelChildren(selectData, preData) {
             this.initSqlBuilder();
             this.sqlBuilder.setAndWhere('tender_id', {
                 value: selectData.tender_id,
-                operate: '='
+                operate: '=',
             });
             this.sqlBuilder.setAndWhere('full_path', {
                 value: this.db.escape(selectData.full_path + '.%'),
-                operate: 'like'
+                operate: 'like',
             });
             this.sqlBuilder.setUpdateData('level', {
                 value: 1,
-                selfOperate: '+'
+                selfOperate: '+',
             });
             this.sqlBuilder.setUpdateData('full_path', {
-                value: ['`full_path`', this.db.escape(selectData.ledger_id), this.db.escape(preData.ledger_id + '.' + selectData.ledger_id)],
-                literal: 'Replace'
+                value: ['`full_path`', this.db.escape('.' + selectData.ledger_id), this.db.escape('.' + preData.ledger_id + '.' + selectData.ledger_id)],
+                literal: 'Replace',
             });
             const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
             const data = this.transaction.query(sql, sqlParam);
@@ -728,32 +762,36 @@ module.exports = app => {
             if (!selectData) {
                 throw '降级节点数据错误';
             }
-            const preData = await this.getDataByParentAndOrder(tenderId, selectData.ledger_pid, selectData.order-1);
+            const preData = await this.getDataByParentAndOrder(tenderId, selectData.ledger_pid, selectData.order - 1);
             if (!preData) {
                 throw '节点不可降级';
             }
             const preLastChildData = await this.getLastChildData(tenderId, preData.ledger_id);
 
             this.transaction = await this.db.beginTransaction();
-            const newFullPath = selectData.full_path.replace(selectData.ledger_id, preData.ledger_id + '.' + selectData.ledger_id);
+            const orgLastPath = selectData.level === 1 ? selectData.ledger_id : '.' + selectData.ledger_id;
+            const newLastPath = selectData.level === 1 ? preData.ledger_id + '.' + selectData.ledger_id : '.' + preData.ledger_id + '.' + selectData.ledger_id;
+            const newFullPath = selectData.full_path.replace(orgLastPath, newLastPath);
             try {
                 // 选中节点--全部后节点 order--
                 await this._updateSelectNextsOrder(selectData, -1);
                 // 选中节点 修改pid, level, order, full_path
-                const updateData = {id: selectData.id,
+                const updateData = {
+                    id: selectData.id,
                     ledger_pid: preData.ledger_id,
                     order: preLastChildData ? preLastChildData.order + 1 : 1,
                     level: selectData.level + 1,
-                    full_path: newFullPath
+                    full_path: newFullPath,
                 };
                 await this.transaction.update(this.tableName, updateData);
                 // 选中节点--全部子节点(含孙) level++, full_path
                 await this._syncDownlevelChildren(selectData, preData);
                 // 选中节点--前兄弟节点 is_leaf应为false
                 if (preData.is_leaf) {
-                    const updateData2 = {id: preData.id,
-                        is_leaf: false
-                    }
+                    const updateData2 = {
+                        id: preData.id,
+                        is_leaf: false,
+                    };
                     await this.transaction.update(this.tableName, updateData);
                 }
                 this.transaction.commit();
@@ -768,7 +806,194 @@ module.exports = app => {
             // 选中节点--原前兄弟节点&全部后兄弟节点
             const queryOrder = preData.is_leaf ? preData.order - 1 : preData.order;
             const resultData2 = await this.getNextsData(tenderId, preData.ledger_pid, queryOrder);
-            return {update: resultData1.concat(resultData2)};
+            return { update: resultData1.concat(resultData2) };
+        }
+
+        /**
+         * 过滤data中update方式不可提交的字段
+         * @param {Number} id - 主键key
+         * @param {Object} data
+         * @return {Object<{id: *}>}
+         * @private
+         */
+        _filterUpdateInvalidField(id, data) {
+            const result = {
+                id,
+            };
+            for (const prop in data) {
+                if (readOnlyFields.indexOf(prop) === -1) {
+                    result[prop] = data[prop];
+                }
+            }
+            return result;
+        }
+
+        /**
+         * 提交数据 - 不影响计算等未提交项
+         * @param {Number} tenderId - 标段id
+         * @param {Object} data - 提交数据
+         * @return {Object} - 提交后的数据
+         */
+        async updateInfo(tenderId, data) {
+            // 简单校验数据
+            if (tenderId <= 0) {
+                throw '标段不存在';
+            }
+            if (tenderId !== data.tender_id) {
+                throw '提交数据错误';
+            }
+
+            try {
+                // 过滤不可提交字段
+                const updateNode = await this.getDataById(data.id);
+                if (!updateNode || tenderId !== updateNode.tender_id || data.ledger_id !== updateNode.ledger_id) {
+                    throw '提交数据错误';
+                }
+                const updateData = this._filterUpdateInvalidField(updateNode.id, data);
+                await this.db.update(this.tableName, updateData);
+            } catch (err) {
+                throw err;
+            }
+
+            const result = await this.getDataByNodeId(tenderId, data.ledger_id);
+            return result;
+        }
+
+        /**
+         * 提交多条数据 - 不影响计算等未提交项
+         * @param {Number} tenderId - 标段id
+         * @param {Array} datas - 提交数据
+         * @return {Array} - 提交后的数据
+         */
+        async updateInfos(tenderId, datas) {
+            if (tenderId <= 0) {
+                throw '标段不存在';
+            }
+            for (const data of datas) {
+                if (tenderId !== data.tender_id) {
+                    throw '提交数据错误1';
+                }
+            }
+
+            this.transaction = await this.db.beginTransaction();
+            try {
+                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';
+                    }
+                    const updateData = this._filterUpdateInvalidField(updateNode.id, data);
+                    await this.transaction.update(this.tableName, updateData);
+                }
+                this.transaction.commit();
+            } catch (err) {
+                this.transaction.rollback();
+                throw err;
+            }
+
+            const filter = [];
+            for (const data of datas) {
+                filter.push(data.id);
+            }
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('id', {
+                value: filter,
+                operate: 'in',
+            });
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+            const resultData = await this.db.query(sql, sqlParam);
+            return resultData;
+        }
+
+        /**
+         * 复制粘贴整块
+         * @param {Number} tenderId - 标段Id
+         * @param {Number} selectId - 选中几点Id
+         * @param {Array} block - 复制节点Id
+         * @return {Object} - 提价后的数据(其中新增粘贴数据,只返回第一层)
+         */
+        async pasteBlock(tenderId, selectId, block) {
+            if ((tenderId <= 0) || (selectId <= 0)) {
+                return [];
+            }
+            const selectData = await this.getDataByNodeId(tenderId, selectId);
+            if (!selectData) {
+                throw '位置数据错误';
+            }
+            const newParentPath = selectData.full_path.replace(selectData.ledger_id, '');
+
+            const copyNodes = await this.getDataByNodeIds(tenderId, block);
+            if (!copyNodes || copyNodes.length <= 0) {
+                throw '复制数据错误';
+            }
+            let bSameParent = true;
+            for (const node of copyNodes) {
+                if (node.ledger_pid !== copyNodes[0].ledger_pid) {
+                    bSameParent = false;
+                    break;
+                }
+            }
+            if (!bSameParent) {
+                throw '复制数据错误:仅可操作同层节点';
+            }
+            const orgParentPath = copyNodes[0].full_path.replace(copyNodes[0].ledger_id, '');
+
+            this.transaction = await this.db.beginTransaction();
+            try {
+                // 选中节点的所有后兄弟节点,order+粘贴节点个数
+                await this._updateSelectNextsOrder(selectData, copyNodes.length);
+                // 数据库创建新增节点数据
+                for (const node of copyNodes) {
+                    const datas = await this.getDataByFullPath(tenderId, node.full_path + '%');
+
+                    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 + datas.length, 'EX', this.ctx.app.config.cacheTime);
+
+                    // 计算粘贴数据中需更新部分
+                    for (let index = 0; index < datas.length; index++) {
+                        const data = datas[index];
+                        const newId = maxId + index + 1;
+                        delete data.id;
+                        if (!data.is_leaf) {
+                            for (const children of datas) {
+                                children.full_path = children.full_path.replace('.' + data.ledger_id, '.' + newId);
+                                if (children.ledger_pid === data.ledger_id) {
+                                    children.ledger_pid = newId;
+                                }
+                            }
+                        } else {
+                            data.full_path = data.full_path.replace('.' + data.ledger_id, '.' + newId);
+                        }
+                        data.ledger_id = newId;
+                        data.full_path = data.full_path.replace(orgParentPath, newParentPath);
+                        if (data.ledger_pid === copyNodes[0].ledger_pid) {
+                            data.ledger_pid = selectData.ledger_pid;
+                            data.order = selectData.order + index + 1;
+                        }
+                        data.level = data.level + selectData.level - copyNodes[0].level;
+                    }
+
+                    // 插入粘贴数据
+                    await this.transaction.insert(this.tableName, datas);
+                }
+                await this.transaction.commit();
+            } catch (err) {
+                await this.transaction.rollback();
+                throw err;
+            }
+
+            // 查询应返回的结果
+            const order = [];
+            for (let i = 1; i <= copyNodes.length; i++) {
+                order.push(selectData.order + i);
+            }
+            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 };
         }
     }
 

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

@@ -69,7 +69,7 @@ describe('test/app/lib/sql_builder.test.js', () => {
         });
         sqlBuilder.setUpdateData('full_path', {
             value: ['`full_path`', app.mysql.escape('1.'), app.mysql.escape('2.')],
-            literal: 'Replace'
+            literal: 'Replace',
         });
         sqlBuilder.setAndWhere('group_id', {
             value: 1,
@@ -86,13 +86,13 @@ describe('test/app/lib/sql_builder.test.js', () => {
         const sqlBuilder = new SqlBuilder();
         sqlBuilder.setAndWhere('office', {
             value: 2,
-            operate: '='
+            operate: '=',
         });
         const prePath = '1.2.3';
         sqlBuilder.setAndWhere('path', {
-            value: app.mysql.escape(prePath +'%'),
-            operate: 'Like'
-        })
+            value: app.mysql.escape(prePath + '%'),
+            operate: 'Like',
+        });
         const [sql, sqlParam] = sqlBuilder.build('table', 'delete');
         const finalSql = app.mysql.format(sql, sqlParam);
 

+ 361 - 16
test/app/service/ledger.test.js

@@ -45,20 +45,20 @@ const testNodeData = [
 const testTenderId = 3;
 
 const { app, assert } = require('egg-mock/bootstrap');
-const findById = function (nodes, Id) {
-    const filters = nodes.filter(function (x) {
+const findById = function(nodes, Id) {
+    const filters = nodes.filter(function(x) {
         return x.ledger_id === Id;
     });
     return filters.length > 0 ? filters[0] : undefined;
-}
+};
 
 describe('test/app/service/ledger.test.js', () => {
+    // 准备测试数据
     it('clear history test data', function* () {
         const ctx = app.mockContext();
         const result = yield ctx.service.ledger.db.delete(ctx.service.ledger.tableName, { tender_id: testTenderId });
         assert(result.affectedRows >= 0);
     });
-
     it('add test data', function* () {
         const ctx = app.mockContext();
         for (const data of testNodeData) {
@@ -66,8 +66,139 @@ describe('test/app/service/ledger.test.js', () => {
         }
         const result = yield ctx.service.ledger.db.insert(ctx.service.ledger.tableName, testNodeData);
         assert(result.affectedRows === testNodeData.length);
+        ctx.service.ledger.cache.set('tender_node_maxId:' + testTenderId, 16, 'EX', ctx.app.config.cacheTime);
+    });
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   ├── 202-1
+        │   │   │   ├── 202-1-a
+        │   │   │   └── 202-1-b
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-1-3
+        │   ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   └── 1-2-1
+        ├── 1-3
+        │   └── 1-3-1
+        └── 1-4
+     */
+
+    // 测试R类方法
+    it('test getDataByTenderId', function* () {
+        const ctx = app.mockContext();
+
+        // 查询前4层节点
+        const result1 = yield ctx.service.ledger.getDataByTenderId(testTenderId);
+        assert(result1.length === 12);
+        // 查询前3层节点
+        const result2 = yield ctx.service.ledger.getDataByTenderId(testTenderId, 3);
+        assert(result2.length === 10);
+    });
+    it('test getDataByNodeId', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点202-1
+        const node = yield ctx.service.ledger.getDataByNodeId(testTenderId, 7);
+        assert(node);
+        assert(node.code === '202-1');
+        assert(node.full_path === '1.2.6.7');
+    });
+    it('test getDataByNodeIds', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点202-1-a与201-1-b
+        const result = yield ctx.service.ledger.getDataByNodeIds(testTenderId, [10, 9]);
+        assert(result.length === 2);
+
+        let node = findById(result, 10);
+        assert(node.code === '202-1-a');
+
+        node = findById(result, 9);
+        assert(node.full_path === '1.2.6.7.9');
+    });
+    it('test getLastChildData', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点202-1最后一个子节点
+        const result = yield ctx.service.ledger.getLastChildData(testTenderId, 7);
+        assert(result.ledger_id === 10);
+        assert(result.full_path === '1.2.6.7.10');
+    });
+    it('test getDataByParentAndOrder', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点202-1 第1子节点
+        const result1 = yield ctx.service.ledger.getDataByParentAndOrder(testTenderId, 8, 1);
+        assert(result1.ledger_id === 11);
+        assert(result1.full_path === '1.2.6.8.11');
+
+        // 查询节点1-1 第2/3子节点
+        const result2 = yield ctx.service.ledger.getDataByParentAndOrder(testTenderId, 2, [2, 3]);
+        assert(result2.length === 2);
+
+        let node = findById(result2, 13);
+        assert(node);
+        assert(node.code === '1-1-2');
+
+        node = findById(result2, 14);
+        assert(node);
+        assert(node.code === '1-1-3');
     });
+    it('test getChildrenByParentId', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点202-1最后一个子节点
+        const result = yield ctx.service.ledger.getChildrenByParentId(testTenderId, 8);
+        assert(result.length === 2);
+
+        let node = findById(result, 11);
+        assert(node);
+        assert(node.code === '202-2-c');
+
+        node = findById(result, 12);
+        assert(node);
+        assert(node.full_path === '1.2.6.8.12');
+    });
+    it('test getDataByFullPath', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点202-1最后一个子节点
+        const result = yield ctx.service.ledger.getDataByFullPath(testTenderId, '1.2.6.8%');
+        assert(result.length === 3);
+
+        let node = findById(result, 8);
+        assert(node);
+        assert(node.code === '202-2');
 
+        node = findById(result, 11);
+        assert(node);
+        assert(node.code === '202-2-c');
+
+        node = findById(result, 12);
+        assert(node);
+        assert(node.full_path === '1.2.6.8.12');
+    });
+    it('test getNextsData', 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');
+
+        node = findById(result, 14);
+        assert(node);
+        assert(node.code === '1-1-3');
+    });
+
+    // 测试CUD类方法
     it('test addNode', function* () {
         const ctx = app.mockContext();
         // 选中1-1-1,插入节点
@@ -75,8 +206,27 @@ describe('test/app/service/ledger.test.js', () => {
         assert(resultData.create.length === 1);
         assert(resultData.update.length === 2);
         assert(resultData.create[0].is_leaf === 1);
+        assert(resultData.create[0].ledger_id === 17);
     });
-
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   ├── 202-1
+        │   │   │   ├── 202-1-a
+        │   │   │   └── 202-1-b
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-1-3
+        │   ├── new
+        │   ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   └── 1-2-1
+        ├── 1-3
+        │   └── 1-3-1
+        └── 1-4
+     */
     it('test deleteNode', function* () {
         const ctx = app.mockContext();
         // 选中202-1,删除节点
@@ -84,33 +234,78 @@ describe('test/app/service/ledger.test.js', () => {
         assert(resultData.delete.length === 3);
         assert(resultData.update.length === 1);
     });
-
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   ├── new
+        │   ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   └── 1-2-1
+        ├── 1-3
+        │   └── 1-3-1
+        └── 1-4
+     */
     it('test upMoveNode', function* () {
         const ctx = app.mockContext();
         // 选中202-2-e上移
-        let resultData = yield ctx.service.ledger.upMoveNode(testTenderId, 12);
-        resultData.update.sort(function (x, y) {
+        const resultData = yield ctx.service.ledger.upMoveNode(testTenderId, 12);
+        resultData.update.sort(function(x, y) {
             return x.order - y.order;
         });
         assert(resultData.update.length === 2);
         assert(resultData.update[0].code === '202-2-e');
     });
-
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   ├── new
+        │   ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   └── 1-2-1
+        ├── 1-3
+        │   └── 1-3-1
+        └── 1-4
+     */
     it('test downMoveNode', function* () {
         const ctx = app.mockContext();
         // 选中202-2-e下移
-        let resultData = yield ctx.service.ledger.downMoveNode(testTenderId, 12);
-        resultData.update.sort(function (x, y) {
+        const resultData = yield ctx.service.ledger.downMoveNode(testTenderId, 12);
+        resultData.update.sort(function(x, y) {
             return x.order - y.order;
         });
         assert(resultData.update.length === 2);
         assert(resultData.update[0].code === '202-2-c');
     });
-
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   ├── new
+        │   ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   └── 1-2-1
+        ├── 1-3
+        │   └── 1-3-1
+        └── 1-4
+     */
     it('test upLevelNode', function* () {
         const ctx = app.mockContext();
         // 选中 1-1-2 升级
-        let resultData = yield ctx.service.ledger.upLevelNode(testTenderId, 13);
+        const resultData = yield ctx.service.ledger.upLevelNode(testTenderId, 13);
         assert(resultData);
         assert(resultData.update.length === 5);
 
@@ -132,11 +327,26 @@ describe('test/app/service/ledger.test.js', () => {
         node = findById(resultData.update, 5);
         assert(node.order === 5);
     });
-
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   └── new
+        ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   └── 1-2-1
+        ├── 1-3
+        │   └── 1-3-1
+        └── 1-4
+     */
     it('test downLevelNode', function* () {
         const ctx = app.mockContext();
         // 选中1-3 降级
-        let resultData = yield ctx.service.ledger.downLevelNode(testTenderId, 4);
+        const resultData = yield ctx.service.ledger.downLevelNode(testTenderId, 4);
         assert(resultData.update.length === 3);
 
         let node = findById(resultData.update, 4);
@@ -151,5 +361,140 @@ describe('test/app/service/ledger.test.js', () => {
 
         node = findById(resultData.update, 5);
         assert(node.order === 4);
-    })
+    });
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   └── new
+        ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   ├── 1-2-1
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    it('test pasteBlock', function* () {
+        const ctx = app.mockContext();
+        // 选中1-2-1, 粘贴1-1-1和new
+        const resultData = yield ctx.service.ledger.pasteBlock(testTenderId, 15, [6, 17]);
+
+        assert(resultData.create.length === 2);
+        assert(resultData.create[0].order = 2);
+        assert(resultData.create[0].level = 3);
+        assert(resultData.create[0].full_path = '1.3.18');
+
+        assert(resultData.create[1].order = 3);
+        assert(resultData.create[1].level = 3);
+        assert(resultData.create[1].full_path = '1.3.22');
+
+        assert(resultData.update.length === 1);
+        assert(resultData.update[0].order = 3);
+    });
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   └── new
+        ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   ├── 1-2-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   ├── new
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    it('test updateInfo', function* () {
+        const ctx = app.mockContext();
+
+        // 修改new(id=17)的code 为 1-1-4
+        const node = yield ctx.service.ledger.getDataByNodeId(testTenderId, 17);
+        assert(node);
+
+        const resultData = yield ctx.service.ledger.updateInfo(testTenderId, {
+            id: node.id,
+            tender_id: node.tender_id,
+            ledger_id: node.ledger_id,
+            code: '1-1-4',
+        });
+        assert(resultData.code === '1-1-4');
+    });
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   └── 1-1-3
+        ├── 1-1-2
+        │   └── 1-1-3
+        ├── 1-2
+        │   ├── 1-2-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   ├── new
+        │   └── 1-3
+        │       └── 1-3-1
+        └── 1-4
+     */
+    it('test updateInfos', function* () {
+        const ctx = app.mockContext();
+
+        // 修改new(id=17)的code 为 1-1-4
+        const node1 = yield ctx.service.ledger.getDataByNodeId(testTenderId, 18);
+        assert(node1);
+        const node2 = yield ctx.service.ledger.getDataByNodeId(testTenderId, 22);
+        assert(node2);
+
+        const resultData = yield ctx.service.ledger.updateInfos(testTenderId, [{
+            id: node1.id,
+            tender_id: node1.tender_id,
+            ledger_id: node1.ledger_id,
+            code: '1-2-2',
+        }, {
+            id: node2.id,
+            tender_id: node2.tender_id,
+            ledger_id: node2.ledger_id,
+            code: '1-2-3',
+        }]);
+        assert(resultData.length === 2);
+        assert(resultData[0].code === '1-2-2');
+        assert(resultData[1].code === '1-2-3');
+    });
+    /* 期望运行结果:
+        1
+        ├── 1-1
+        │   ├── 1-1-1
+        │   │   └── 202-2
+        │   │       ├── 202-2-c
+        │   │       └── 202-2-e
+        │   └── 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
+     */
 });