Form calc date 109 (#689)

* feat: eval-calc-formula

---------

Co-authored-by: devezhao <zhaofang123@gmail.com>
This commit is contained in:
REBUILD 企业管理系统 2023-12-06 11:46:58 +08:00 committed by GitHub
parent 3f7feaf90b
commit 2690f0791d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 303 additions and 101 deletions

View file

@ -49,11 +49,19 @@ public class EasyFieldConfigProps {
* 日期格式
*/
public static final String DATE_FORMAT = "dateFormat";
/**
* 表单公式
*/
public static final String DATE_CALCFORMULA = NUMBER_CALCFORMULA;
/**
* 日期格式
*/
public static final String DATETIME_FORMAT = "datetimeFormat";
/**
* 表单公式
*/
public static final String DATETIME_CALCFORMULA = NUMBER_CALCFORMULA;
/**
* 时间格式

View file

@ -11,6 +11,7 @@ import com.googlecode.aviator.AviatorEvaluator;
import com.googlecode.aviator.AviatorEvaluatorInstance;
import com.googlecode.aviator.Options;
import com.googlecode.aviator.exception.ExpressionSyntaxErrorException;
import com.googlecode.aviator.lexer.token.OperatorType;
import com.googlecode.aviator.runtime.function.system.AssertFunction;
import com.googlecode.aviator.runtime.type.AviatorFunction;
import lombok.extern.slf4j.Slf4j;
@ -20,7 +21,7 @@ import java.util.Collections;
import java.util.Map;
/**
* // https://www.yuque.com/boyan-avfmj/aviatorscript
* https://www.yuque.com/boyan-avfmj/aviatorscript
*
* @author devezhao
* @since 2021/4/12
@ -43,6 +44,9 @@ public class AviatorUtils {
} catch (Exception ignored) {
}
AVIATOR.addOpFunction(OperatorType.ADD, new OverOperatorType.DateAdd());
AVIATOR.addOpFunction(OperatorType.SUB, new OverOperatorType.DateSub());
addCustomFunction(new DateDiffFunction());
addCustomFunction(new DateAddFunction());
addCustomFunction(new DateSubFunction());
@ -55,13 +59,21 @@ public class AviatorUtils {
}
/**
* 表达式计算
*
* @param expression
* @return
* @see #eval(String, Map, boolean)
*/
public static Object evalQuietly(String expression) {
return eval(expression, null, true);
public static Object eval(String expression) {
return eval(expression, null, false);
}
/**
* @param expression
* @return
* @see #eval(String, Map, boolean)
*/
public static Object eval(String expression, Map<String, Object> env) {
return eval(expression, env, false);
}
/**
@ -74,13 +86,13 @@ public class AviatorUtils {
*/
public static Object eval(String expression, Map<String, Object> env, boolean quietly) {
try {
return AVIATOR.execute(expression, env);
return AVIATOR.execute(expression, env == null ? Collections.emptyMap() : env);
} catch (Exception ex) {
if (ex instanceof AssertFunction.AssertFailed) {
throw new AssertFailedException((AssertFunction.AssertFailed) ex);
}
log.error("Bad aviator expression : \n{}\n<< {}", expression, env, ex);
log.error("Bad aviator expression : \n>> {}\n>> {}\n>> {}", expression, env, ex.getLocalizedMessage());
if (!quietly) throw ex;
}
return null;

View file

@ -0,0 +1,84 @@
/*!
Copyright (c) REBUILD <https://getrebuild.com/> and/or its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.core.service.trigger.aviator;
import cn.devezhao.commons.CalendarUtils;
import com.googlecode.aviator.lexer.token.OperatorType;
import com.googlecode.aviator.runtime.function.AbstractFunction;
import com.googlecode.aviator.runtime.type.AviatorLong;
import com.googlecode.aviator.runtime.type.AviatorObject;
import java.util.Date;
import java.util.Map;
/**
* 操作符重载
*
* @author RB
* @since 2023/12/6
*/
public class OverOperatorType {
private OverOperatorType() {}
/**
* 日期加
*/
static class DateAdd extends AbstractFunction {
@Override
public String getName() {
return OperatorType.ADD.getToken();
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
Object $argv1 = arg1.getValue(env);
Object $argv2 = arg2.getValue(env);
if ($argv1 instanceof Date && $argv2 instanceof Number) {
return opDate((Date) $argv1, ((Number) $argv2).intValue());
} else if ($argv2 instanceof Date && $argv1 instanceof Number) {
return opDate((Date) $argv2, ((Number) $argv1).intValue());
} else {
return arg1.add(arg2, env); // Use default
}
}
}
/**
* 日期减
*/
static class DateSub extends AbstractFunction {
@Override
public String getName() {
return OperatorType.SUB.getToken();
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
Object $argv1 = arg1.getValue(env);
Object $argv2 = arg2.getValue(env);
if ($argv1 instanceof Date && $argv2 instanceof Number) {
return opDate((Date) $argv1, -((Number) $argv2).intValue());
} else if ($argv2 instanceof Date && $argv1 instanceof Number) {
return opDate((Date) $argv2, -((Number) $argv1).intValue());
} else if ($argv1 instanceof Date && $argv2 instanceof Date) {
int diff = CalendarUtils.getDayLeft((Date) $argv1, (Date) $argv2);
return AviatorLong.valueOf(diff);
} else {
return arg1.add(arg2, env); // Use default
}
}
}
static AviatorDate opDate(Date date, int num) {
Date d = CalendarUtils.addDay(date, num);
return new AviatorDate(d);
}
}

View file

@ -8,22 +8,32 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.web.general;
import cn.devezhao.bizz.privileges.impl.BizzPermission;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.commons.web.ServletUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONAware;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.api.RespBody;
import com.rebuild.core.Application;
import com.rebuild.core.configuration.general.AutoFillinManager;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.DisplayType;
import com.rebuild.core.metadata.easymeta.EasyDecimal;
import com.rebuild.core.metadata.easymeta.EasyEntity;
import com.rebuild.core.metadata.easymeta.EasyField;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.impl.EasyFieldConfigProps;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.privileges.bizz.User;
import com.rebuild.core.service.general.RepeatedRecordsException;
import com.rebuild.core.service.general.transform.RecordTransfomer;
import com.rebuild.core.service.trigger.aviator.AviatorUtils;
import com.rebuild.core.support.general.ContentWithFieldVars;
import com.rebuild.core.support.i18n.I18nUtils;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.JSONUtils;
@ -31,7 +41,9 @@ import com.rebuild.web.BaseController;
import com.rebuild.web.EntityParam;
import com.rebuild.web.IdParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -39,7 +51,9 @@ import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 表单/视图 功能扩展
@ -195,4 +209,80 @@ public class ModelExtrasController extends BaseController {
}
return allowed;
}
@PostMapping("eval-calc-formula")
public RespBody evalCalcFormula(@EntityParam Entity entity, HttpServletRequest request) {
String targetField = getParameterNotNull(request, "field");
if (!entity.containsField(targetField)) return RespBody.error();
JSONObject post = (JSONObject) ServletUtils.getRequestJson(request);
Map<String, Object> varsInFormula = post.getInnerMap();
for (Object value : varsInFormula.values()) {
if (value == null || StringUtils.isBlank(value.toString())) {
return RespBody.ok();
}
}
EasyField easyField = EasyMetaFactory.valueOf(entity.getField(targetField));
String formula = easyField.getExtraAttr(EasyFieldConfigProps.DATE_CALCFORMULA);
boolean canCalc = true;
Set<String> fieldVars = ContentWithFieldVars.matchsVars(formula);
for (String field : fieldVars) {
if (!entity.containsField(field)) {
canCalc = false;
break;
}
Object fieldValue = varsInFormula.get(field);
if (fieldValue == null) {
canCalc = false;
break;
}
String fieldValue2 = fieldValue.toString();
DisplayType dt = EasyMetaFactory.valueOf(entity.getField(field)).getDisplayType();
if (dt == DisplayType.DATE || dt == DisplayType.DATETIME) {
fieldValue = CalendarUtils.parse(fieldValue2, CalendarUtils.UTC_DATETIME_FORMAT.substring(0, fieldValue2.length()));
} else if (dt == DisplayType.NUMBER || dt == DisplayType.DECIMAL) {
fieldValue = EasyDecimal.clearFlaged(fieldValue2);
if (StringUtils.isNotBlank((String) fieldValue)) {
if (dt == DisplayType.NUMBER) fieldValue = ObjectUtils.toLong(fieldValue);
else fieldValue = ObjectUtils.toDouble(fieldValue);
} else {
fieldValue = null;
}
}
if (fieldValue == null) {
canCalc = false;
break;
}
varsInFormula.put(field, fieldValue);
}
if (!canCalc) return RespBody.ok();
formula = formula
.replace("{", "").replace("}", "")
.replace("×", "*").replace("÷", "/");
Object evalVal = AviatorUtils.eval(formula, varsInFormula, true);
if (evalVal == null) return RespBody.ok();
DisplayType dt = easyField.getDisplayType();
if (dt == DisplayType.DATE || dt == DisplayType.DATETIME) {
if (evalVal instanceof Date) {
evalVal = easyField.wrapValue(evalVal);
return RespBody.ok(evalVal);
}
} else if (dt == DisplayType.NUMBER || dt == DisplayType.DECIMAL) {
if (evalVal instanceof Number) {
evalVal = easyField.wrapValue(evalVal);
return RespBody.ok(evalVal);
}
}
log.warn("Bad eval value `{}` for field : {}", evalVal, easyField.getRawMeta());
return RespBody.ok();
}
}

View file

@ -83,7 +83,7 @@
</label>
<label class="custom-control custom-control-sm custom-radio custom-control-inline mb-0 mr-1">
<input class="custom-control-input" type="radio" name="decimalType" value="¥" />
<span class="custom-control-label">[[${bundle.L('货币')}]]</span>
<span class="custom-control-label">[[${bundle.L('符号')}]]</span>
</label>
<span>
<select class="underline-sm J_decimalTypeFlag">
@ -359,14 +359,17 @@
</div>
</div>
</div>
<div th:if="${fieldType == 'DECIMAL' or fieldType == 'NUMBER'}" class="form-group row J_for-DECIMAL J_for-NUMBER">
<div
th:if="${fieldType == 'DECIMAL' or fieldType == 'NUMBER' or fieldType == 'DATE' or fieldType == 'DATETIME'}"
class="form-group row J_for-DECIMAL J_for-NUMBER J_for-DATE J_for-DATETIME"
>
<label class="col-md-12 col-xl-3 col-lg-4 col-form-label text-lg-right">[[${bundle.L('表单计算公式')}]]</label>
<div class="col-md-12 col-xl-6 col-lg-8">
<input type="hidden" class="form-control" id="calcFormula" />
<div class="form-control-plaintext formula" id="calcFormula2" th:_title="${bundle.L('无')}">[[${calcFormula ?: calcFormula}]]</div>
<p
class="form-text"
th:utext="${bundle.L('本公式仅做前端计算,如公式中所用字段未布局/未显示,则无法进行计算。你可以通过 [触发器 (字段更新)](/admin/robot/triggers) 实现更强大的计算规则')}"
th:utext="${bundle.L('如公式中所用字段未布局/未显示,则无法进行计算。本公式适用前端简单计算,你可以通过 [触发器 (字段更新)](/admin/robot/triggers) 实现更强大的计算规则')}"
></p>
</div>
</div>

View file

@ -1165,80 +1165,8 @@ class RbFormNumber extends RbFormText {
componentDidMount() {
super.componentDidMount()
// 表单计算视图下无效
if (this.props.calcFormula && !this.props.onView) {
const calcFormula = this.props.calcFormula.replace(new RegExp('×', 'ig'), '*').replace(new RegExp('÷', 'ig'), '/')
const fixed = this.props.decimalFormat ? (this.props.decimalFormat.split('.')[1] || '').length : 0
// 等待字段初始化完毕
setTimeout(() => {
const calcFormulaValues = {}
const watchFields = calcFormula.match(/\{([a-z0-9]+)}/gi) || []
watchFields.forEach((item) => {
const name = item.substr(1, item.length - 2)
const fieldComp = this.props.$$$parent.refs[`fieldcomp-${name}`]
if (fieldComp && !$emptyNum(fieldComp.state.value)) {
calcFormulaValues[name] = this._removeComma(fieldComp.state.value)
}
})
// 表单计算
let _timer
this.props.$$$parent.onFieldValueChange((s) => {
if (!watchFields.includes(`{${s.name}}`)) {
if (rb.env === 'dev') console.log('onFieldValueChange ignored :', s, this.props.field)
return false
} else if (rb.env === 'dev') {
console.log('onFieldValueChange for calcFormula :', s, this.props.field)
}
// // fix: 3.2 字段相互使用导致死循环
// this.__fixUpdateDepth = (this.__fixUpdateDepth || 0) + 1
// if (this.__fixUpdateDepth > 9) {
// console.log(`Maximum update depth exceeded : ${this.props.field}=${this.props.calcFormula}`)
// setTimeout(() => (this.__fixUpdateDepth = 0), 100)
// return false
// }
if ($emptyNum(s.value)) {
delete calcFormulaValues[s.name]
} else {
calcFormulaValues[s.name] = this._removeComma(s.value)
}
let formula = calcFormula
for (let key in calcFormulaValues) {
formula = formula.replace(new RegExp(`{${key}}`, 'ig'), calcFormulaValues[key] || 0)
}
if (_timer) {
clearTimeout(_timer)
_timer = null
}
// v34 延迟执行
_timer = setTimeout(() => {
// 还有变量无值
if (formula.includes('{')) {
this.setValue(null)
return false
}
try {
let calcv = null
eval(`calcv = ${formula}`)
if (!isNaN(calcv)) this.setValue(calcv.toFixed(fixed))
} catch (err) {
if (rb.env === 'dev') console.log(err)
}
return true
}, 200)
// _timer end
})
}, 200) // init
}
// 表单计算
if (this.props.calcFormula && !this.props.onView) __calcFormula(this)
}
// 移除千分为位
@ -1496,6 +1424,12 @@ class RbFormDateTime extends RbFormElement {
wt = $(this._fieldValue).offset().top
return wt + 280 < wh ? 'bottom-right' : 'top-right'
}
componentDidMount() {
super.componentDidMount()
// 表单计算
if (this.props.calcFormula && !this.props.onView) __calcFormula(this)
}
}
class RbFormTime extends RbFormDateTime {
@ -3002,3 +2936,59 @@ const __addRecentlyUse = function (id) {
$.post(`/commons/search/recently-add?id=${id}`)
}
}
// 表单计算视图下无效
const __calcFormula = function (_this) {
const watchFields = _this.props.calcFormula.match(/\{([a-z0-9]+)}/gi) || []
const $$$parent = _this.props.$$$parent
const evalUrl = `/app/entity/extras/eval-calc-formula?entity=${$$$parent.props.entity}&field=${_this.props.field}`
setTimeout(() => {
const calcFormulaValues = {}
let _timer
// init
watchFields.forEach((item) => {
const name = item.substr(1, item.length - 2)
const fieldComp = $$$parent.refs[`fieldcomp-${name}`]
if (fieldComp && !$empty(fieldComp.state.value)) {
calcFormulaValues[name] = fieldComp.state.value
}
})
// onchange
$$$parent.onFieldValueChange((s) => {
if (!watchFields.includes(`{${s.name}}`)) {
if (rb.env === 'dev') console.log('onFieldValueChange ignored :', s, _this.props.field)
return false
} else if (rb.env === 'dev') {
console.log('onFieldValueChange for calcFormula :', s, _this.props.field)
}
if ($empty(s.value)) delete calcFormulaValues[s.name]
else calcFormulaValues[s.name] = s.value
if (_timer) {
clearTimeout(_timer)
_timer = null
}
// v36
_timer = setTimeout(() => {
$.post(evalUrl, JSON.stringify(calcFormulaValues), (res) => {
if (res.data) _this.setValue(res.data)
else _this.setValue(null)
})
}, 400)
return true
})
// 新建时
if (_this._isNew) {
$.post(evalUrl, JSON.stringify(calcFormulaValues), (res) => {
if (res.data) _this.setValue(res.data)
else _this.setValue(null)
})
}
}, 400) // delay for init
}

View file

@ -232,6 +232,7 @@ $(document).ready(function () {
} else if (dt === 'SERIES') {
_handleSeries()
} else if (dt === 'DATE' || dt === 'DATETIME' || dt === 'TIME') {
if (dt === 'DATE' || dt === 'DATETIME') _handleCalcFormula(extConfig.calcFormula)
_handleDatetime(dt)
} else if (dt === 'FILE' || dt === 'IMAGE') {
_handleFile(extConfig.uploadNumber)
@ -247,8 +248,7 @@ $(document).ready(function () {
} else if (dt === 'BARCODE') {
$('.J_fieldAttrs input').attr('disabled', true)
} else if (dt === 'NUMBER' || dt === 'DECIMAL') {
_handleNumber(extConfig.calcFormula)
_handleCalcFormula(extConfig.calcFormula)
if (dt === 'DECIMAL') {
if (!extConfig.decimalType || extConfig.decimalType === 0 || extConfig.decimalType === '0' || extConfig.decimalType === '%') {
// 数字百分比
@ -580,7 +580,7 @@ const _loadRefsLabel = function ($dv, $dvClear) {
}
let FIELDS_CACHE
const _handleNumber = function (calcFormula) {
const _handleCalcFormula = function (formula) {
const $el = $('#calcFormula2')
function _call(s) {
$('#calcFormula').val(s || '')
@ -591,13 +591,13 @@ const _handleNumber = function (calcFormula) {
$.get(`/commons/metadata/fields?entity=${wpc.entityName}`, (res) => {
const fs = []
res.data.forEach((item) => {
if ((item.type === 'NUMBER' || item.type === 'DECIMAL') && item.name !== wpc.fieldName) {
if (['NUMBER', 'DECIMAL', 'DATE', 'DATETIME'].includes(item.type) && !['approvalLastTime', 'modifiedOn'].includes(item.name) && item.name !== wpc.fieldName) {
fs.push(item)
}
})
FIELDS_CACHE = fs
if (calcFormula) _call(calcFormula)
if (formula) _call(formula)
})
}

View file

@ -7,11 +7,13 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.trigger.aviator;
import cn.devezhao.commons.CalendarUtils;
import com.googlecode.aviator.exception.ExpressionRuntimeException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
@ -22,7 +24,7 @@ class AviatorUtilsTest {
@Test
void eval() {
System.out.println(AviatorUtils.evalQuietly("123*123"));
System.out.println(AviatorUtils.eval("123*123"));
System.out.println(AviatorUtils.eval(
"abc12_.abc+123", Collections.singletonMap("abc12_.abc", 100), true));
@ -36,30 +38,30 @@ class AviatorUtilsTest {
@Test
void func() {
AviatorUtils.evalQuietly("p(DATEDIFF('2021-03-04 00:00:00', '2022-03-05 23:59:59', 'D'))");
AviatorUtils.evalQuietly("p(DATEDIFF('2021-03-04 00:00:00', '2022-03-05 23:59:59', 'M'))");
AviatorUtils.evalQuietly("p(DATEDIFF('2021-03-04 00:00:00', '2022-03-09 23:59:59', 'Y'))");
AviatorUtils.eval("p(DATEDIFF('2021-03-04 00:00:00', '2022-03-05 23:59:59', 'D'))");
AviatorUtils.eval("p(DATEDIFF('2021-03-04 00:00:00', '2022-03-05 23:59:59', 'M'))");
AviatorUtils.eval("p(DATEDIFF('2021-03-04 00:00:00', '2022-03-09 23:59:59', 'Y'))");
AviatorUtils.evalQuietly("p(DATEADD('2021-01-01 18:17:00', '2H'))");
AviatorUtils.eval("p(DATEADD('2021-01-01 18:17:00', '2H'))");
AviatorUtils.evalQuietly("p(DATESUB('2021-01-01 18:17:00', '1'))");
AviatorUtils.eval("p(DATESUB('2021-01-01 18:17:00', '1'))");
}
@Test
void funcComplex() {
AviatorUtils.evalQuietly("p(100 + DATEDIFF('2021-01-01 18:17:00', '2021-01-01 16:17:00', 'H'))");
AviatorUtils.evalQuietly("p(DATEADD(DATEADD('2021-01-01 18:17:00', '2H'), '1D'))");
AviatorUtils.eval("p(100 + DATEDIFF('2021-01-01 18:17:00', '2021-01-01 16:17:00', 'H'))");
AviatorUtils.eval("p(DATEADD(DATEADD('2021-01-01 18:17:00', '2H'), '1D'))");
}
@Test
void funcRequestFunction() {
AviatorUtils.evalQuietly("p(REQUEST('https://www.baidu.com/'))");
AviatorUtils.evalQuietly("p(REQUEST('https://www.google.com/', 'imdefault'))");
AviatorUtils.eval("p(REQUEST('https://www.baidu.com/'))");
AviatorUtils.eval("p(REQUEST('https://www.google.com/', 'imdefault'))");
}
@Test
void funcLocationDistanceFunction() {
AviatorUtils.evalQuietly("p(LOCATIONDISTANCE('123.456789,123.456789', '地址$$$$123.456789,123.456789'))");
AviatorUtils.eval("p(LOCATIONDISTANCE('123.456789,123.456789', '地址$$$$123.456789,123.456789'))");
}
@Test
@ -75,6 +77,19 @@ class AviatorUtilsTest {
@Test
void testJava() {
AviatorUtils.evalQuietly("p(StringUtils.upperCase('abcd'));");
AviatorUtils.eval("p(StringUtils.upperCase('abcd'));");
}
@Test
void testDateOp() {
Map<String, Object> env = new HashMap<>();
env.put("date1", CalendarUtils.now());
AviatorUtils.eval("p(date1 + 8)", env, true);
AviatorUtils.eval("p(date1 - 8)", env, true);
AviatorUtils.eval("p(date1 - date1)", env, true);
// BAD
Assertions.assertThrows(ExpressionRuntimeException.class,
() -> AviatorUtils.eval("date1 + date1", env, false));
}
}