js项目中计算引擎

292 阅读4分钟

1.计算引擎调用##

以分部分项中综合单价分析为例:

rateDetail.vue 存放数据使用tableGrid2表

<v-table-grid ref="table" :getEditStyle="getEditStyle" :getReadOnly="getReadOnly" :treeCol=1 :fixedColLeft=5 @after-update="afterUpdate"
    @current-change="currentChange" @editButton-click="editButtonclick" :getSelectData="getSelectData" @before-change="beforechange"
    @after-change="afterchange" @after-selectChange="afterSelectChange" class="fbfx-table2" :modifyData="modifyData"
    :beforePaste="beforePaste" :afterPaste="afterPaste">
    </v-table-grid>

其中的afterupdate方法会调用db().save方法,此方法中完成数据的计算。

此方法如下:

function save(onSuccess, onError) {
  if (inUpdate) {
    return;
  }
  if (curProject) {
    setCostRatiosWay();

    calcLyhfQuantity(curProject.data); // 根据业态计算清单的工程量
    sumLmmDetail(curProject.data);
    csUtil().calcCSAllCostDetail(curProject.data, function() {
      rule().getDefault(function (rules) {
        resetRateDictExper(curProject.data);
        rule().addDynamicRules(curProject.data, rules);
        ruleEngine().calcModel(curProject.data, rules, precisionUtils().getNewValueOfPrecisionRule, rule().getMacroRule());

        calcFYHZ(curProject.data);
        let twoLevelRules = rule().getTwoLevel();
        ruleEngine().calcModel(curProject.data, twoLevelRules, precisionUtils().getNewValueOfPrecisionRule, rule().getMacroRule());

        // 工程总造价
        let summaryRootRec = curProject.data.summary.find((item) => (item.id === 'summaryTaxRoot'));
        if (summaryRootRec) {
          curProject.total = summaryRootRec.total;
        } else {
          curProject.total = 0;
        }

        // 设置修改版本号加1
        curProject['version'] = curProject['version'] ? curProject['version'] + 1 : 1;
        saveHistoryUserIDs();
        // 删除无效数据
        clearData();
        // 保存到indexDB
        saveProjectToIndexDB(onSuccess, onError);
      });
    });
  } else {
    onError('无工程数据');
  }
}

setCostRatiosWay(); 获得专业的业态费率

calcLyhfQuantity(curProject.data); // 根据业态计算清单的工程量

sumLmmDetail(curProject.data);//计算lmm表中的工程量以及单价和总价,其方法内部调用了

calLmmDetailFY方法,这个方法内部计算了综合单价分析表中的费用,并未调用计算引擎。

save方法中的如下代码调用了计算引擎

rule().getDefault(function (rules) {
        resetRateDictExper(curProject.data);
        rule().addDynamicRules(curProject.data, rules);
        ruleEngine().calcModel(curProject.data, rules, precisionUtils().getNewValueOfPrecisionRule, rule().getMacroRule());

        calcFYHZ(curProject.data);
        let twoLevelRules = rule().getTwoLevel();
        ruleEngine().calcModel(curProject.data, twoLevelRules, precisionUtils().getNewValueOfPrecisionRule, rule().getMacroRule());

        // 工程总造价
        let summaryRootRec = curProject.data.summary.find((item) => (item.id === 'summaryTaxRoot'));
        if (summaryRootRec) {
          curProject.total = summaryRootRec.total;
        } else {
          curProject.total = 0;
        }

rule().getDefault方法获得计算规则集,其中一条规则如下:

每条规则包括表名,字段名,以及计算的表达式。

rule().addDynamicRules(curProject.data, rules); 添加动态规则,动态计算规则主要是根据不同费用代码用来添加综合单价组成中各项费用的计算规则,如人工费、主材费等。

2.计算引擎真正实现##

拿到计算规则后就要开始真正的计算

ruleEngine().calcModel(curProject.data, rules, precisionUtils().getNewValueOfPrecisionRule, rule().getMacroRule());

calcModel方法中定义了计算规则中会用到的函数,如上图中的filterTable函数

calcModel方法最重要的是对产品中每张表的每个字段应用计算规则表达式从而计算得出最新数据。对应代码如下

// 通过设置getter函数添加计算字段,this._@fieldName_=null是为了消除循环引用
  var sCodeFmt = "db.@tableName.forEach(function(v){Object.defineProperty(v, '@fieldName', { get: function(){if(this._@fieldName_==undefined){this._@fieldName_=null;this._@fieldName_= getNewValueOfPrecisionRule('@tableName', @expr, '@fieldName');};return this._@fieldName_;}})});";
  
  rules.forEach(function(rule) {
    var sCode = sCodeFmt.split('@tableName').join(rule.tableName).replace('@expr', rule.expr).split('@fieldName').join(rule.fieldName);
    try {
      eval(sCode);
    } catch (e) {//发生错误时打印详细日志,方便问题分析addByCaijbOn20180914
      console.error('计算引擎发生错误:'  + rule.tableName + '_' + rule.fieldName + '_' + rule.expr + '\r\n' + sCode)
    }
  });

  // 写回原来的数据
  rules.forEach(function(rule) {
    // 不需要这个上面发生异常走不到这里
    // if (!modelData[rule.tableName]) { // 发生错误时打印详细日志,方便问题分析addByCaijbOn20180914
    //   console.error('计算引擎计算时发生错误,数据为空或未被初始化。' + rule.tableName);
    // }
    modelData[rule.tableName].forEach(function(record, idx) {
      try {
        record[rule.fieldName] = db[rule.tableName][idx][rule.fieldName];
      } catch (e) {
        record[rule.fieldName] = 0;
      }
    });
  });

rules.forEach将每条计算规则转换成sCodeFmt变量的格式也就是sCode,然后调用eval函数。

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。

sCode中向引用的表中对应字段下设置getter函数向其加入计算字段,这样每次当使用getter函数给字段赋值时就会自动调用计算字段实现更新。

将sCode格式化如下:

db.@tableName.forEach(function(v){

     Object.defineProperty(v, '@fieldName', {

          get: function(){

              if(this._@fieldName_==undefined){

                  this._@fieldName_=null;

                 this._@fieldName_= getNewValueOfPrecisionRule('@tableName', @expr, '@fieldName');

            };

        return this._@fieldName_;}})

     });

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。 Object.defineProperty(obj, prop, descriptor)​

obj 要在其上定义属性的对象。 prop 要定义或修改的属性的名称。 descriptor 将被定义或修改的属性描述符。

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

存取描述符同时具有以下可选键值:

get 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。 默认为 undefined。 set 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

当你给一个属性定义setter或者getter,或者两者都有时,这个属性会被定义为“访问描述符”。也就是上面提到的存取描述符 对于访问描述符来说,Javascript会忽略他们的value和writable特性。取而代之的是set和get函数。

在添加过程中会判断是否给对应的字段添加过getter如果添加过则不再重复添加。

添加后,将会将所有数据遍历,就会使用getter函数赋值,然后调用计算规则中的表达式计算后重新赋值。 计算引擎就实现了所有计算结果的自动更新。