瀏覽代碼

导入导出合同计量相关

MaiXinRong 7 月之前
父節點
當前提交
92c2788725

+ 8 - 1
app/const/audit.js

@@ -24,6 +24,13 @@ const auditType = (function () {
     return { types, key, info };
 })();
 
+const auditMasterType = {
+    stage: 'stage',
+    material: 'material',
+    ledger: 'ledger',
+    revise: 'revise',
+    pay: 'pay', // 独立合同支付
+};
 
 // 期审批流程
 const common = (function() {
@@ -1098,7 +1105,6 @@ const changePlan = (function() {
 
 // 推送类型
 const pushType = {
-    common,
     material: 1,
     stage: 2,
     change: 3,
@@ -1112,6 +1118,7 @@ const pushType = {
 };
 
 module.exports = {
+    common,
     auditType,
     ledger,
     stage,

+ 90 - 0
app/controller/stage_controller.js

@@ -2291,6 +2291,96 @@ module.exports = app => {
                 ctx.ajaxErrorBody(err, '导入计量台账数据错误');
             }
         }
+
+        async exportStageData(ctx) {
+            try {
+                const ledgerData = await ctx.service.ledger.getAllDataByCondition({
+                    columns: ['id', 'ledger_id', 'ledger_pid', 'level', 'order', 'full_path', 'is_leaf', 'code', 'b_code', 'name', 'unit', 'unit_price', 'node_type'],
+                    where: { tender_id: ctx.tender.id }
+                });
+                const extraLedgerData = await ctx.service.ledgerExtra.getData(ctx.tender.id, this.ledgerExtraColumn);
+                const dgnData = await ctx.service.stageBillsDgn.getDgnData(ctx.tender.id);
+                const curStageLedgerData = ctx.stage.readOnly
+                    ? await ctx.service.stageBills.getAuditorStageData2(ctx.tender.id, ctx.stage.id, ctx.stage.curTimes, ctx.stage.curOrder)
+                    : await ctx.service.stageBills.getLastestStageData2(ctx.tender.id, ctx.stage.id);
+                this.ctx.helper.assignRelaData(ledgerData, [
+                    { data: dgnData, fields: ['deal_dgn_qty1', 'deal_dgn_qty2', 'c_dgn_qty1', 'c_dgn_qty2'], prefix: '', relaId: 'id' },
+                    { data: extraLedgerData, fields: ['is_tp'], prefix: '', relaId: 'id' },
+                    { data: curStageLedgerData, fields: ['contract_qty', 'contract_tp', 'postil'], prefix: '', relaId: 'lid' },
+                ]);
+
+                const posData = await ctx.service.pos.getAllDataByCondition({
+                    columns: ['id', 'lid', 'name', 'position', 'porder'],
+                    where: { tid: ctx.tender.id }
+                });
+                // 根据当前人,或指定对象查询数据
+                const curStagePosData = ctx.stage.readOnly
+                    ? await ctx.service.stagePos.getAuditorStageData2(ctx.tender.id, ctx.stage.id, ctx.stage.curTimes, ctx.stage.curOrder)
+                    : await ctx.service.stagePos.getLastestStageData2(ctx.tender.id, ctx.stage.id);
+                this.ctx.helper.assignRelaData(posData, [
+                    { data: curStagePosData, fields: ['contract_qty', 'postil'], prefix: '', relaId: 'pid' },
+                ]);
+
+                const Cpd = require('../lib/crypt').cpd;
+                const cpd = new Cpd();
+                const filename = `第${ctx.stage.order}期计量数据.cpd`;
+                const filepath = path.join(this.ctx.app.baseDir, 'temp', `第${ctx.stage.order}期计量数据.cpd`);
+                await cpd.encrypt({ ledger: ledgerData, pos: posData }, filepath);
+                const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();
+                let disposition = '';
+                if (userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
+                    disposition = 'attachment; filename=' + encodeURIComponent(filename);
+                } else if (userAgent.indexOf('firefox') >= 0) {
+                    disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(filename) + '"';
+                } else {
+                    /* safari等其他非主流浏览器只能自求多福了 */
+                    disposition = 'attachment; filename=' + new Buffer(filename).toString('binary');
+                }
+                ctx.response.set({
+                    'Content-Type': 'application/octet-stream',
+                    'Content-Disposition': disposition,
+                });
+                ctx.body = await fs.readFileSync(filepath);
+            } catch(err) {
+                ctx.log(err);
+                ctx.postError(err, '导出计量法数据失败');
+            }
+        }
+
+        async importStageData(ctx) {
+            const create_time = Date.parse(new Date()) / 1000;
+            const filepath = path.join(this.ctx.app.baseDir, 'temp', `计量数据${create_time}.cpd`);
+            let stream, index = 0;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+                stream = await parts();
+                while (stream !== undefined) {
+                    if (!stream.filename) throw '未发现上传文件!';
+                    // 保存文件
+                    await ctx.helper.saveStreamFile(stream, filepath);
+                    await sendToWormhole(stream);
+                    ++index;
+                    if (Array.isArray(parts.field.size) && index < parts.field.size.length) {
+                        stream = await parts();
+                    } else {
+                        stream = undefined;
+                    }
+                }
+                const Cpd = require('../lib/crypt').cpd;
+                const cpd = new Cpd();
+                const data = await cpd.decrypt(filepath);
+                await ctx.service.stageStash.loadStageDealData(ctx.stage, data);
+                await ctx.service.stage.updateCheckCalcFlag(ctx.stage, true);
+                await ctx.service.stage.updateCacheTime(ctx.stage.id);
+                ctx.body = { err: 0, mgs: '', data: '' };
+            } catch (err) {
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) await sendToWormhole(stream);
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '导入文件失败');
+            }
+        }
+
     }
 
     return StageController;

+ 121 - 0
app/lib/crypt.js

@@ -0,0 +1,121 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const crypto = require('crypto-js');
+const fs = require('fs');
+const path = require('path');
+const encryptSpr = '|----|';
+const baseKey = crypto.enc.Utf8.parse('Sc3850?Calc888Zh'); // 十六位十六进制数作为密钥
+const baseIv = crypto.enc.Utf8.parse('Zh888!Sc3850Calc'); // 十六位十六进制数作为密钥偏移量
+
+const baseUtils = {
+    aesDecrypt: function(str, key, iv) {
+        const encryptedHexStr = crypto.enc.Hex.parse(str);
+        const src = crypto.enc.Base64.stringify(encryptedHexStr);
+        const decrypt = crypto.AES.decrypt(src, key, {
+            iv,
+            mode: crypto.mode.CBC,
+            padding: crypto.pad.Pkcs7,
+        });
+        const decryptedStr = decrypt.toString(crypto.enc.Utf8);
+        return decryptedStr.toString();
+    },
+    aesEncrypt: function(str, key, iv) {
+        const src = crypto.enc.Utf8.parse(str);
+        const encrypted = crypto.AES.encrypt(src, key, {
+            iv,
+            mode: crypto.mode.CBC,
+            padding: crypto.pad.Pkcs7,
+        });
+        return encrypted.ciphertext.toString().toUpperCase();
+    },
+    md5: function(str) {
+        return crypto.MD5(str).toString();
+    },
+    decryptBuffer: function(buffer) {
+        try {
+            const result = [];
+            const arr = buffer.split(encryptSpr);
+            const header = JSON.parse(this.aesDecrypt(arr[0], baseKey, baseIv));
+            header.key = crypto.enc.Hex.parse(header.key);
+            header.iv = crypto.enc.Hex.parse(header.iv);
+            arr.shift();
+            const check = this.md5(arr.join(encryptSpr));
+            if (check !== header.check) throw '文件被篡改';
+            for (const a of arr) {
+                result.push(JSON.parse(this.aesDecrypt(a, header.key, header.iv)));
+            }
+            return header.data === 'array' ? result : result[0];
+        } catch(err) {
+            throw err;
+        }
+    },
+    getRamdonHeader: function () {
+        return {
+            key: crypto.lib.WordArray.random(16),
+            iv: crypto.lib.WordArray.random(16),
+        };
+    },
+    encryptBuffer: function(data) {
+        const datas = data instanceof Array ? data : [data];
+        const header = this.getRamdonHeader();
+        const arr = [];
+        for (const d of datas) {
+            arr.push(this.aesEncrypt(JSON.stringify(d), header.key, header.iv ));
+        }
+        header.key = header.key.toString();
+        header.iv = header.iv.toString();
+        header.check = this.md5(arr.join(encryptSpr));
+        header.data = data instanceof Array ? 'array' : 'object';
+        return this.aesEncrypt(JSON.stringify(header), baseKey, baseIv) + encryptSpr + arr.join(encryptSpr);
+    },
+};
+
+class baseFile {
+    doDecrypt(str) {
+
+    }
+    async decrypt(file) {
+        const str = await fs.readFileSync(file, 'utf-8');
+        return this.doDecrypt(str);
+    }
+    async recursiveMkdirSync(pathName) {
+        if (!fs.existsSync(pathName)) {
+            const upperPath = path.dirname(pathName);
+            if (!fs.existsSync(upperPath)) {
+                await this.recursiveMkdirSync(upperPath);
+            }
+            await fs.mkdirSync(pathName);
+        }
+    }
+    doEncrypt(data) {
+
+    }
+    async encrypt(data, file) {
+        const buffer = this.doEncrypt(data);
+        // 检查文件夹是否存在,不存在则直接创建文件夹
+        const pathName = path.dirname(file);
+        if (!fs.existsSync(pathName)) await this.recursiveMkdirSync(pathName);
+        await fs.writeFileSync(file, buffer);
+    }
+}
+
+class cpd extends baseFile {
+    doEncrypt(data) {
+        return baseUtils.encryptBuffer(data);
+    }
+    doDecrypt(str) {
+        return baseUtils.decryptBuffer(str);
+    }
+}
+
+module.exports = {
+    cpd
+};

+ 1 - 0
app/public/js/shares/batch_import.js

@@ -96,6 +96,7 @@ const BatchImportStageGcl = function (setting) {
                     {title: '编号', field: 'code', hAlign: 0, width: 180, formatter: '@', cellType: 'tree', readOnly: true },
                     {title: '名称/引用标段', field: 'name', hAlign: 0, width: 180, formatter: '@', readOnly: true },
                     {title: '可选期', field: 'stage', hAlign: 1, width: 60, formatter: '@'},
+                    {title: '指定项目节', field: 'match_code', hAlign: 1, width: 70, formatter: '@', readOnly: true},
                     {title: '状态', field: 'status', hAlign: 1, width: 60, formatter: '@', readOnly: true},
                     {title: '错误信息', field: 'error', hAlign: 1, width: 60, formatter: '@', readOnly: true},
                 ],

+ 23 - 1
app/public/js/stage.js

@@ -1878,7 +1878,29 @@ $(document).ready(() => {
                     return contractExpr;
                 }
             },
-            importSpr: '---',
+            dealLoadSpr: '---',
+            exportDealData: {
+                name: '导出本期合同计量',
+                callback: function (key, opt, menu, e) {
+                    window.open(window.location.pathname + '/cpd');
+                },
+            },
+            importDealData: {
+                name: '导入本期合同计量',
+                callback: function (key, opt, menu, e) {
+                    BaseImportFile.show({
+                        validList: ['.cpd'],
+                        url: window.location.pathname + '/cpd/load',
+                        afterImport: function() {
+                            window.location.reload();
+                        }
+                    });
+                },
+                disabled: function(key, opt) {
+                    return readOnly;
+                },
+            },
+            sumLoadSpr: '---',
             importStageGcl: {
                 name: '导入(其他标段)工程量清单计量数据',
                 icon: 'fa-link',

+ 2 - 0
app/router.js

@@ -345,6 +345,8 @@ module.exports = app => {
     app.post('/tender/:id/measure/stage/:order/im-file/upload', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.uploadImFile');
     app.post('/tender/:id/measure/stage/:order/im-file/del', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.deleteImFile');
     app.get('/tender/:id/measure/stage/:order/im-file/download', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.downloadImFile');
+    app.get('/tender/:id/measure/stage/:order/cpd', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.exportStageData');
+    app.post('/tender/:id/measure/stage/:order/cpd/load', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.importStageData');
 
     // 暂存计量
     app.post('/tender/:id/measure/stage/:order/stash/list', sessionAuth, tenderCheck, stageCheck, 'stageController.stashList');

+ 33 - 0
app/service/extra_pay.js

@@ -0,0 +1,33 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const payConst = require('../const/deal_pay');
+const auditConst = require('../const/audit');
+
+module.exports = app => {
+    class ExtraPay extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'extra_pay';
+        }
+
+        async AddExtraPay() {
+
+        }
+    }
+
+    return ExtraPay;
+};

+ 86 - 3
app/service/stage_stash.js

@@ -8,6 +8,8 @@
  * @version
  */
 
+const Ledger = require('../lib/ledger');
+
 class loadStageExcelTree {
     constructor(ctx) {
         this.ctx = ctx;
@@ -48,7 +50,7 @@ class loadStageExcelTree {
             if (node.b_code) {
                 if (x.has_pos === undefined) {
                     const relaPos = self.pos.getLedgerPos(x.id);
-                    x.has_pos = !!relaPos && relaPos.length > 0;
+                    x.has_pos = !!(relaPos && relaPos.length > 0);
                 }
                 return node.b_code === _.trimEnd(x.b_code) && node.name === _.trimEnd(x.name) && node.unit === _.trimEnd(x.unit)
                     && node.unit_price === x.unit_price && node.has_pos === x.has_pos;
@@ -122,7 +124,7 @@ class loadStageExcelTree {
     }
     loadNode(node, parent) {
         node.is_leaf = !node.children || node.children.length === 0 ? 1 : 0;
-        node.has_pos = node.pos && node.pos.length > 0;
+        node.has_pos = !!(node.pos && node.pos.length > 0);
         const cur = this.findNode(node, parent);
         if (!cur || cur.settle_status === this.settleStatus.finish) return;
 
@@ -144,8 +146,31 @@ class loadStageExcelTree {
             this.loadNode(node, null);
         }
     }
-}
 
+    loadDealNode(node, parent, pos) {
+        node.pos = pos.getLedgerPos(node.id) || [];
+        node.has_pos = node.pos.length > 0;
+        const cur = this.findNode(node, parent);
+        if (!cur || cur.settle_status === this.settleStatus.finish) return;
+
+        if (cur) {
+            if (!node.b_code) this.loadDgn(node, cur);
+            if (node.is_leaf) {
+                this.loadLeaf(node, cur);
+            } else {
+                for (const c of node.children) {
+                    this.loadDealNode(c, cur, pos);
+                }
+            }
+        }
+    }
+    loadDeal(dealTree, dealPos, source) {
+        this.init(source);
+        for (const node of dealTree.children) {
+            this.loadDealNode(node, null, dealPos);
+        }
+    }
+}
 
 module.exports = app => {
     class StageStash extends app.BaseService {
@@ -423,6 +448,64 @@ module.exports = app => {
                 throw '解析Excel错误';
             }
         }
+
+        async loadStageDealData(stage, dealData) {
+            try {
+                const dealTree = new Ledger.billsTree(this.ctx, {
+                    id: 'ledger_id',
+                    pid: 'ledger_pid',
+                    order: 'order',
+                    level: 'level',
+                    rootId: -1,
+                });
+                const dealPos = new Ledger.pos({ id: 'id', ledgerId: 'lid' });
+                dealTree.loadDatas(dealData.ledger);
+                dealPos.loadDatas(dealData.pos);
+
+                const ledgerData = await this.ctx.service.ledger.getAllDataByCondition({
+                    columns: ['id', 'ledger_id', 'ledger_pid', 'level', 'order', 'full_path', 'is_leaf', 'code', 'b_code', 'name', 'unit', 'unit_price'],
+                    where: { tender_id: stage.tid},
+                });
+                const extraData = await this.ctx.service.ledgerExtra.getData(this.ctx.tender.id, ['is_tp']);
+                const settleStatusBills = stage.readySettle ? await this.ctx.service.settleBills.getAllDataByCondition({ where: { settle_id: stage.readySettle.id }}) : [];
+                this.ctx.helper.assignRelaData(ledgerData, [
+                    { data: extraData, fields: ['is_tp'], prefix: '', relaId: 'id' },
+                    { data: settleStatusBills, fields: ['settle_status'], prefix: '', relaId: 'lid' },
+                ]);
+                const posData = await this.ctx.service.pos.getAllDataByCondition({
+                    columns: ['id', 'lid', 'name', 'porder'],
+                    where: { tid: stage.tid },
+                });
+                const settleStatusPos = stage.readySettle ? await this.ctx.service.settlePos.getAllDataByCondition({ where: { settle_id: stage.readySettle.id }}) : [];
+                this.ctx.helper.assignRelaData(posData, [
+                    { data: settleStatusPos, fields: ['settle_status'], prefix: '', relaId: 'pid' },
+                ]);
+                const stageBills = await this.ctx.service.stageBills.getAllDataByCondition({ where: { sid: stage.id } });
+                const stagePos = await this.ctx.service.stagePos.getAllDataByCondition({ where: { sid: stage.id } });
+                const stageBillsDgn = await this.ctx.service.stageBillsDgn.getAllDataByCondition({ where: { tid: stage.tid } });
+
+                const loadModal = new loadStageExcelTree(this.ctx);
+                loadModal.loadDeal(dealTree, dealPos, { ledgerData, posData, stageBills, stagePos, stageBillsDgn, default: { tid: stage.tid, sid: stage.id, said: this.ctx.session.sessionUser.accountId } });
+
+                const conn = await this.db.beginTransaction();
+                try {
+                    if (loadModal.insertBills.length > 0) conn.insert(this.ctx.service.stageBills.tableName, loadModal.insertBills);
+                    if (loadModal.updateBills.length > 0) conn.updateRows(this.ctx.service.stageBills.tableName, loadModal.updateBills);
+                    if (loadModal.insertPos.length > 0) conn.insert(this.ctx.service.stagePos.tableName, loadModal.insertPos);
+                    if (loadModal.updatePos.length > 0) conn.updateRows(this.ctx.service.stagePos.tableName, loadModal.updatePos);
+                    if (loadModal.insertDgn.length > 0) conn.insert(this.ctx.service.stageBillsDgn.tableName, loadModal.insertDgn);
+                    if (loadModal.updateDgn.length > 0) conn.updateRows(this.ctx.service.stageBillsDgn.tableName, loadModal.updateDgn);
+                    await conn.commit();
+                } catch (err) {
+                    await conn.rollback();
+                    this.ctx.log(err);
+                    throw '保存导入数据失败';
+                }
+            } catch (err) {
+                this.ctx.log(err);
+                throw '导入本期合同计量';
+            }
+        }
     }
 
     return StageStash;

+ 66 - 0
app/view/shares/import_file_modal.ejs

@@ -0,0 +1,66 @@
+<div class="modal fade" id="base-import-file" data-backdrop="static" enctype="multipart/form-data">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">导入</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label >选择文件</label>
+                    <input type="file" class="form-control-file" id="bsf-file" accept="*.cpd">
+                </div>
+            </div>
+            <div class="modal-footer d-flex justify-content-between">
+                <div class="ml-auto">
+                    <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                    <button type="button" class="btn btn-primary btn-sm" id="bsf-ok">确认</button>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const BaseImportFile = (function () {
+        let importSetting;
+        // 清除上一次数据
+        $('#base-import-file').bind('hidden.bs.modal', function () {
+            $('#bsf-file').val('');
+        });
+
+        // 上传excel内容,并导入
+        $('#bsf-ok').click(function () {
+            const files = $('#bsf-file')[0].files;
+            const formData = new FormData();
+
+            for (const file of files) {
+                if (file === undefined) {
+                    toast('未选择上传文件!', 'error');
+                    return false;
+                }
+                const filesize = file.size;
+                if (filesize > 30 * 1024 * 1024) {
+                    toast('存在上传文件大小过大!', 'error');
+                    return false;
+                }
+                const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
+                if (importSetting.validList.indexOf(fileext) === -1) {
+                    toast('只能上传指定格式的附件!', 'error');
+                    return false;
+                }
+                formData.append('size', filesize);
+                formData.append('file[]', file);
+            }
+
+            postDataWithFile(importSetting.url, formData, function (data) {
+                if (importSetting.afterImport) importSetting.afterImport(data);
+                $('#base-import-file').modal('hide');
+            });
+        });
+
+        const show = function (setting) {
+            importSetting = setting;
+            $('#base-import-file').modal('show');
+        }
+        return { show };
+    })();
+</script>

+ 2 - 1
app/view/stage/modal.ejs

@@ -658,4 +658,5 @@
 <% include ../shares/import_excel_modal.ejs%>
 <% include ../shares/stage_stash_modal.ejs%>
 <% include ../shares/delete_hint_modal.ejs%>
-<% include ../shares/batch_import_modal.ejs%>
+<% include ../shares/batch_import_modal.ejs%>
+<% include ../shares/import_file_modal.ejs%>

+ 2 - 1
config/config.default.js

@@ -124,7 +124,8 @@ module.exports = appInfo => {
 
     // 上传设置
     config.multipart = {
-        whitelist: ['.json', '.txt',
+        whitelist: ['.cpd',
+            '.json', '.txt',
             '.xls', '.xlsx',
             '.doc', '.docx',
             '.pdf',

+ 0 - 13
config/config.local.js

@@ -58,19 +58,6 @@ module.exports = appInfo => {
         app: true,
     };
 
-    // 上传设置
-    config.multipart = {
-        whitelist: ['.json', '.txt',
-            '.xls', '.xlsx',
-            '.doc', '.docx',
-            '.pdf',
-            '.ppt', '.pptx',
-            '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.cad', '.dwg',
-            '.zip', '.rar', '.7z', ''],
-        fileSize: '100mb',
-        fields: '15',
-    };
-
     // session配置
     config.session = {
         key: 'ZHC_SESS',

+ 0 - 11
config/config.remoteqa.js

@@ -55,17 +55,6 @@ module.exports = appInfo => {
         app: true,
     };
 
-    // 上传设置
-    config.multipart = {
-        whitelist: ['.json', '.txt',
-            '.xls', '.xlsx',
-            '.doc', '.docx',
-            '.pdf',
-            '.png', '.jpg', '.jpeg', '.gif', '.bmp',
-            '.zip', '.rar', '.7z'],
-        fileSize: '30mb',
-    };
-
     // session配置
     config.session = {
         key: 'ZHC_SESS',

+ 1 - 0
package.json

@@ -11,6 +11,7 @@
         "atob": "^2.1.2",
         "axios": "^1.3.4",
         "bignumber.js": "^8.1.1",
+        "crypto-js": "^4.2.0",
         "decimal.js": "^10.2.0",
         "egg": "^1.13.0",
         "egg-etag": "^1.1.0",