From b6530c79384fc7890b5a0d7b12d71d469c22a6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?REBUILD=20=E4=BC=81=E4=B8=9A=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= <42044143+getrebuild@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:33:30 +0800 Subject: [PATCH] feat: Type cast (#702) * bcc * feat: field-type-cast * feat: aviator DateCompare * tf.nullable --- @rbv | 2 +- .../core/metadata/impl/Field2Schema.java | 50 +++++++++ .../service/trigger/aviator/AviatorUtils.java | 6 ++ .../trigger/aviator/OverOperatorType.java | 101 ++++++++++++++++++ .../core/support/ConfigurationItem.java | 2 +- .../core/support/RebuildConfiguration.java | 4 +- .../core/support/integration/SMSender.java | 16 +-- .../web/admin/ConfigurationController.java | 3 +- .../web/admin/integration/submail.html | 10 +- .../web/admin/metadata/field-edit.html | 8 +- .../web/assets/js/charts/dashboard.js | 9 +- .../web/assets/js/metadata/field-edit.js | 96 ++++++++++++++++- .../resources/web/assets/js/rb-components.js | 6 +- .../js/trigger/trigger.FIELDWRITEBACK.js | 5 +- .../trigger/aviator/AviatorUtilsTest.java | 13 +++ 15 files changed, 305 insertions(+), 26 deletions(-) diff --git a/@rbv b/@rbv index 791d6f665..031039036 160000 --- a/@rbv +++ b/@rbv @@ -1 +1 @@ -Subproject commit 791d6f665c9e937b4503459cc68958f4c266b4cd +Subproject commit 031039036ba44f47b61a2b81bcaa70922ef3656a diff --git a/src/main/java/com/rebuild/core/metadata/impl/Field2Schema.java b/src/main/java/com/rebuild/core/metadata/impl/Field2Schema.java index 0f48762ee..dac34d559 100644 --- a/src/main/java/com/rebuild/core/metadata/impl/Field2Schema.java +++ b/src/main/java/com/rebuild/core/metadata/impl/Field2Schema.java @@ -411,4 +411,54 @@ public class Field2Schema extends SetUser { return identifier; } + + /** + * 类型转换 + * + * @param field + * @param toType + * @param force + * @return + */ + public boolean castType(Field field, DisplayType toType, boolean force) { + EasyField easyMeta = EasyMetaFactory.valueOf(field); + ID metaRecordId = easyMeta.getMetaId(); + if (easyMeta.isBuiltin() || metaRecordId == null) { + throw new MetadataModificationException(Language.L("系统内置,不允许转换")); + } + + if (!force) { + long count; + if ((count = checkRecordCount(field.getOwnEntity())) > 100000) { + throw new MetadataModificationException(Language.L("实体记录过多 (%d),转换字段可能导致表损坏", count)); + } + } + + Record meta = EntityHelper.forUpdate(metaRecordId, getUser(), false); + meta.setString("displayType", toType.name()); + Application.getCommonsService().update(meta, false); + + Dialect dialect = Application.getPersistManagerFactory().getDialect(); + final Table table = new Table(field.getOwnEntity(), dialect); + StringBuilder ddl = new StringBuilder(); + table.generateFieldDDL(field, ddl); + + String alterSql = String.format("alter table `%s` change column `%s` ", + field.getOwnEntity().getPhysicalName(), field.getPhysicalName()); + alterSql += ddl.toString().trim(); + + try { + Application.getSqlExecutor().executeBatch(new String[]{alterSql}, DDL_TIMEOUT); + } catch (Throwable ex) { + // 还原 + meta.setString("displayType", EasyMetaFactory.getDisplayType(field).name()); + Application.getCommonsService().update(meta, false); + + log.error("DDL ERROR : \n" + alterSql, ex); + throw new MetadataModificationException(ThrowableUtils.getRootCause(ex).getLocalizedMessage()); + } + + MetadataHelper.getMetadataFactory().refresh(); + return true; + } } diff --git a/src/main/java/com/rebuild/core/service/trigger/aviator/AviatorUtils.java b/src/main/java/com/rebuild/core/service/trigger/aviator/AviatorUtils.java index b1eda93e0..b68e66b17 100644 --- a/src/main/java/com/rebuild/core/service/trigger/aviator/AviatorUtils.java +++ b/src/main/java/com/rebuild/core/service/trigger/aviator/AviatorUtils.java @@ -46,6 +46,12 @@ public class AviatorUtils { AVIATOR.addOpFunction(OperatorType.ADD, new OverOperatorType.DateAdd()); AVIATOR.addOpFunction(OperatorType.SUB, new OverOperatorType.DateSub()); + AVIATOR.addOpFunction(OperatorType.LE, new OverOperatorType.DateCompareLE()); + AVIATOR.addOpFunction(OperatorType.LT, new OverOperatorType.DateCompareLT()); + AVIATOR.addOpFunction(OperatorType.GE, new OverOperatorType.DateCompareGE()); + AVIATOR.addOpFunction(OperatorType.GT, new OverOperatorType.DateCompareGT()); + AVIATOR.addOpFunction(OperatorType.EQ, new OverOperatorType.DateCompareEQ()); + AVIATOR.addOpFunction(OperatorType.NEQ, new OverOperatorType.DateCompareNEQ()); addCustomFunction(new DateDiffFunction()); addCustomFunction(new DateAddFunction()); diff --git a/src/main/java/com/rebuild/core/service/trigger/aviator/OverOperatorType.java b/src/main/java/com/rebuild/core/service/trigger/aviator/OverOperatorType.java index d2ab5cda5..dec9614eb 100644 --- a/src/main/java/com/rebuild/core/service/trigger/aviator/OverOperatorType.java +++ b/src/main/java/com/rebuild/core/service/trigger/aviator/OverOperatorType.java @@ -10,6 +10,7 @@ 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.AviatorBoolean; import com.googlecode.aviator.runtime.type.AviatorLong; import com.googlecode.aviator.runtime.type.AviatorObject; @@ -26,6 +27,8 @@ public class OverOperatorType { private OverOperatorType() {} + // -- 计算 + /** * 日期加 */ @@ -83,4 +86,102 @@ public class OverOperatorType { Date d = CalendarUtils.addDay(date, num); return new AviatorDate(d); } + + // -- 比较 + + /** + * 日期比较 + */ + static abstract class DateCompare extends AbstractFunction { + private static final long serialVersionUID = -4160230503581309424L; + @Override + public AviatorObject call(Map env, AviatorObject arg1, AviatorObject arg2) { + Object $argv1 = arg1.getValue(env); + Object $argv2 = arg2.getValue(env); + + if ($argv1 instanceof Date && $argv2 instanceof Date) { + long v1 = ((Date) $argv1).getTime(); + long v2 = ((Date) $argv2).getTime(); + boolean b = compare(v1, v2); + return AviatorBoolean.valueOf(b); + } else { + return arg1.add(arg2, env); // Use default + } + } + + abstract protected boolean compare(long v1, long v2); + } + + // LE `<=` + static class DateCompareLE extends DateCompare { + private static final long serialVersionUID = 1321662048697121893L; + @Override + public String getName() { + return OperatorType.LE.getToken(); + } + @Override + protected boolean compare(long v1, long v2) { + return v1 <= v2; + } + } + // LT `<` + static class DateCompareLT extends DateCompare { + private static final long serialVersionUID = 8197857653882782806L; + @Override + public String getName() { + return OperatorType.LT.getToken(); + } + @Override + protected boolean compare(long v1, long v2) { + return v1 < v2; + } + } + // GE `>=` + static class DateCompareGE extends DateCompare { + private static final long serialVersionUID = -7966630104916265372L; + @Override + public String getName() { + return OperatorType.GE.getToken(); + } + @Override + protected boolean compare(long v1, long v2) { + return v1 >= v2; + } + } + // GT `>` + static class DateCompareGT extends DateCompare { + private static final long serialVersionUID = 5214573679573440753L; + @Override + public String getName() { + return OperatorType.GT.getToken(); + } + @Override + protected boolean compare(long v1, long v2) { + return v1 > v2; + } + } + // EQ `==` + static class DateCompareEQ extends DateCompare { + private static final long serialVersionUID = -6142749075506832977L; + @Override + public String getName() { + return OperatorType.EQ.getToken(); + } + @Override + protected boolean compare(long v1, long v2) { + return v1 == v2; + } + } + // NEQ `!=` + static class DateCompareNEQ extends DateCompare { + private static final long serialVersionUID = -838391653977975466L; + @Override + public String getName() { + return OperatorType.NEQ.getToken(); + } + @Override + protected boolean compare(long v1, long v2) { + return v1 != v2; + } + } } diff --git a/src/main/java/com/rebuild/core/support/ConfigurationItem.java b/src/main/java/com/rebuild/core/support/ConfigurationItem.java index da9818e7e..61f43fa22 100644 --- a/src/main/java/com/rebuild/core/support/ConfigurationItem.java +++ b/src/main/java/com/rebuild/core/support/ConfigurationItem.java @@ -34,7 +34,7 @@ public enum ConfigurationItem { StorageURL, StorageApiKey, StorageApiSecret, StorageBucket, // 邮件 - MailUser, MailPassword, MailAddr, MailName(AppName), MailCc, + MailUser, MailPassword, MailAddr, MailName(AppName), MailCc, MailBcc, MailSmtpServer, // 短信 diff --git a/src/main/java/com/rebuild/core/support/RebuildConfiguration.java b/src/main/java/com/rebuild/core/support/RebuildConfiguration.java index c2463e7b1..cf44cc807 100644 --- a/src/main/java/com/rebuild/core/support/RebuildConfiguration.java +++ b/src/main/java/com/rebuild/core/support/RebuildConfiguration.java @@ -115,7 +115,7 @@ public class RebuildConfiguration extends KVStorage { /** * 邮件账号 * - * @return returns [MailUser, MailPassword, MailAddr, MailName, MailCc, MailSmtpServer] + * @return returns [MailUser, MailPassword, MailAddr, MailName, MailCc, MailBcc, MailSmtpServer] */ public static String[] getMailAccount() { String[] set = getsNoUnset(false, @@ -123,11 +123,13 @@ public class RebuildConfiguration extends KVStorage { if (set == null) return null; String cc = get(ConfigurationItem.MailCc); + String bcc = get(ConfigurationItem.MailBcc); String smtpServer = get(ConfigurationItem.MailSmtpServer); return new String[] { set[0], set[1], set[2], set[3], StringUtils.defaultIfBlank(cc, null), + StringUtils.defaultIfBlank(bcc, null), StringUtils.defaultIfBlank(smtpServer, null) }; } diff --git a/src/main/java/com/rebuild/core/support/integration/SMSender.java b/src/main/java/com/rebuild/core/support/integration/SMSender.java index 091d39252..b604b2bb6 100644 --- a/src/main/java/com/rebuild/core/support/integration/SMSender.java +++ b/src/main/java/com/rebuild/core/support/integration/SMSender.java @@ -89,16 +89,16 @@ public class SMSender { * @throws ConfigurationException If mail-account unset */ public static String sendMail(String to, String subject, String content, boolean useTemplate, String[] specAccount) throws ConfigurationException { - if (specAccount == null || specAccount.length < 5 + if (specAccount == null || specAccount.length < 6 || StringUtils.isBlank(specAccount[0]) || StringUtils.isBlank(specAccount[1]) || StringUtils.isBlank(specAccount[2]) || StringUtils.isBlank(specAccount[3])) { throw new ConfigurationException(Language.L("邮件账户未配置或配置错误")); } - if (Application.devMode()) { - log.info("[dev] Fake send email to : {} / {} / {}", to, subject, content); - return null; - } +// if (Application.devMode()) { +// log.info("[dev] Fake send email to : {} / {} / {}", to, subject, content); +// return null; +// } // 使用邮件模板 if (useTemplate) { @@ -133,7 +133,7 @@ public class SMSender { final String logContent = "【" + subject + "】" + content; // Use SMTP - if (specAccount.length >= 6 && StringUtils.isNotBlank(specAccount[5])) { + if (specAccount.length >= 7 && StringUtils.isNotBlank(specAccount[6])) { try { String emailId = sendMailViaSmtp(to, subject, content, specAccount); createLog(to, logContent, TYPE_EMAIL, emailId, null); @@ -150,6 +150,7 @@ public class SMSender { params.put("signature", specAccount[1]); params.put("to", to); if (StringUtils.isNotBlank(specAccount[4])) params.put("cc", specAccount[4]); + if (StringUtils.isNotBlank(specAccount[5])) params.put("bcc", specAccount[5]); params.put("from", specAccount[2]); params.put("from_name", specAccount[3]); params.put("subject", subject); @@ -196,6 +197,7 @@ public class SMSender { HtmlEmail email = new HtmlEmail(); email.addTo(to); if (StringUtils.isNotBlank(specAccount[4])) email.addCc(specAccount[4]); + if (StringUtils.isNotBlank(specAccount[5])) email.addBcc(specAccount[5]); email.setSubject(subject); email.setHtmlMsg(htmlContent); @@ -203,7 +205,7 @@ public class SMSender { email.setAuthentication(specAccount[0], specAccount[1]); // HOST[:PORT:SSL|TLS] - String[] hostPortSsl = specAccount[5].split(":"); + String[] hostPortSsl = specAccount[6].split(":"); email.setHostName(hostPortSsl[0]); if (hostPortSsl.length > 1) email.setSmtpPort(Integer.parseInt(hostPortSsl[1])); if (hostPortSsl.length > 2) { diff --git a/src/main/java/com/rebuild/web/admin/ConfigurationController.java b/src/main/java/com/rebuild/web/admin/ConfigurationController.java index a23a35d80..4168602a7 100644 --- a/src/main/java/com/rebuild/web/admin/ConfigurationController.java +++ b/src/main/java/com/rebuild/web/admin/ConfigurationController.java @@ -245,6 +245,7 @@ public class ConfigurationController extends BaseController { data.getString("MailUser"), data.getString("MailPassword"), data.getString("MailAddr"), data.getString("MailName"), data.getString("MailCc"), + data.getString("MailBcc"), data.getString("MailSmtpServer") }; if (specAccount[1].contains("*")) { @@ -277,7 +278,7 @@ public class ConfigurationController extends BaseController { Object[] smsCount = Application.createQueryNoFilter(sqlCount) .setParameter(1, 1) - .setParameter(1, xday) + .setParameter(2, xday) .unique(); Object[][] email = Application.createQueryNoFilter(sql) diff --git a/src/main/resources/web/admin/integration/submail.html b/src/main/resources/web/admin/integration/submail.html index 162a4cadb..d67872db9 100644 --- a/src/main/resources/web/admin/integration/submail.html +++ b/src/main/resources/web/admin/integration/submail.html @@ -43,8 +43,8 @@ [[${bundle.L('SMTP 服务器地址')}]] - - [[${mailAccount == null ? bundle.L('未设置') : mailAccount[5]}]] + + [[${mailAccount == null ? bundle.L('未设置') : mailAccount[6]}]] @@ -78,6 +78,12 @@ [[${mailAccount == null || mailAccount[4] == null ? bundle.L('无') : mailAccount[4]}]] + + [[${bundle.L('密送地址')}]] + + [[${mailAccount == null || mailAccount[5] == null ? bundle.L('无') : mailAccount[5]}]] + + diff --git a/src/main/resources/web/admin/metadata/field-edit.html b/src/main/resources/web/admin/metadata/field-edit.html index c95cb97eb..8bdb64e98 100644 --- a/src/main/resources/web/admin/metadata/field-edit.html +++ b/src/main/resources/web/admin/metadata/field-edit.html @@ -66,7 +66,12 @@
- +
+ +
+ +
+
@@ -531,6 +536,7 @@ + diff --git a/src/main/resources/web/assets/js/charts/dashboard.js b/src/main/resources/web/assets/js/charts/dashboard.js index aa7089434..481bb45d1 100644 --- a/src/main/resources/web/assets/js/charts/dashboard.js +++ b/src/main/resources/web/assets/js/charts/dashboard.js @@ -321,6 +321,9 @@ class DlgAddChart extends RbFormHandler { + @@ -340,12 +343,6 @@ class DlgAddChart extends RbFormHandler { this.__select2 = $(this._$entity).select2({ allowClear: false, placeholder: $L('选择数据来源'), - // templateResult: function (res) { - // const $span = $('').attr('title', res.text).text(res.text) - // const found = _data.find((x) => x.entity === res.id) - // if (found) $(``).appendTo($span) - // return $span - // }, }) }) } diff --git a/src/main/resources/web/assets/js/metadata/field-edit.js b/src/main/resources/web/assets/js/metadata/field-edit.js index bce4b43c6..3d0a3918e 100644 --- a/src/main/resources/web/assets/js/metadata/field-edit.js +++ b/src/main/resources/web/assets/js/metadata/field-edit.js @@ -4,7 +4,7 @@ Copyright (c) REBUILD and/or its owners. All rights re rebuild is dual-licensed under commercial and open source licenses (GPLv3). See LICENSE and COMMERCIAL in the project root for license information. */ -/* global FormulaDate, FormulaCalc */ +/* global FormulaDate, FormulaCalc, FIELD_TYPES */ const wpc = window.__PageConfig const __gExtConfig = {} @@ -25,6 +25,8 @@ $(document).ready(function () { if (wpc.fieldBuildin) { $('.J_fieldAttrs, .J_advOpt, .J_for-STATE').remove() $('#referenceCascadingField, #referenceQuickNew, #referenceDataFilter').parents('.form-group').remove() + } else { + $('.J_cast-type').parent().removeClass('hide') } // 显示重复值选项 if (SHOW_REPEATABLE.includes(dt) && wpc.fieldName !== 'approvalId') { @@ -299,6 +301,14 @@ $(document).ready(function () { }, }) }) + + $('.J_cast-type').on('click', () => { + if (rb.commercial < 10) { + RbHighbar.error(WrapHtml($L('免费版不支持此功能 [(查看详情)](https://getrebuild.com/docs/rbv-features)'))) + return + } + renderRbcomp() + }) }) // Check incorrect? @@ -732,3 +742,87 @@ const _handleAnyReference = function (es) { if (es) $s2.val(es.split(',')).trigger('change') }) } + +// 字段类型转换 +const __TYPE2TYPE = { + 'NUMBER': ['DECIMAL'], + 'DECIMAL': ['NUMBER'], + 'DATE': ['DATETIME'], + 'DATETIME': ['DATE'], + 'TEXT': ['NTEXT', 'PHONE', 'EMAIL', 'URL'], + 'PHONE': ['TEXT'], + 'EMAIL': ['TEXT'], + 'URL': ['TEXT'], + 'NTEXT': ['TEXT'], + 'IMAGE': ['FILE'], + 'FILE': ['IMAGE'], +} +class FieldTypeCast extends RbFormHandler { + render() { + const toTypes = __TYPE2TYPE[this.props.fromType] || [] + + return ( + (this._dlg = c)} disposeOnHide> +
+
+ +
+
{FIELD_TYPES[this.props.fromType][0]}
+
+
+
+ +
+ +

{$L('转换可能导致一定的精度损失,请谨慎进行')}

+
+
+
(this._$btns = c)}> +
+ + +
+
+
+
+ ) + } + + componentDidMount() { + $(this._$toType).select2({ + placeholder: $L('不可转换'), + templateResult: function (res) { + const $span = $('').attr('title', res.text).text(res.text) + $(``).appendTo($span) + return $span + }, + }) + } + + post() { + const toType = $(this._$toType).val() + if (!toType) return RbHighbar.create($L('不可转换')) + + const $btn = $(this._$btns).find('.btn').button('loading') + $.post(`/admin/entity/field-type-cast?entity=${this.props.entity}&field=${this.props.field}&toType=${toType}`, (res) => { + if (res.error_code === 0) { + location.reload() + } else { + $btn.button('reset') + RbHighbar.error(res.error_msg) + } + }) + } +} diff --git a/src/main/resources/web/assets/js/rb-components.js b/src/main/resources/web/assets/js/rb-components.js index 3df7a32b5..03744ec3e 100644 --- a/src/main/resources/web/assets/js/rb-components.js +++ b/src/main/resources/web/assets/js/rb-components.js @@ -181,12 +181,12 @@ class RbModalHandler extends React.Component { this.state = { ...props } } - show = (state, call) => { + show = (state, cb) => { const callback = () => { // eslint-disable-next-line react/no-string-refs const dlg = this._dlg || this.refs['dlg'] if (dlg) dlg.show() - typeof call === 'function' && call(this) + typeof cb === 'function' && cb(this) } if (state && $.type(state) === 'object') this.setState(state, callback) else callback() @@ -1243,7 +1243,7 @@ const renderRbcomp = function (jsx, target, callback) { target = target[0] } - // ReactDOM.render({jsx}, target, call) + // ReactDOM.render({jsx}, target, callback) ReactDOM.render(jsx, target, callback) return target } diff --git a/src/main/resources/web/assets/js/trigger/trigger.FIELDWRITEBACK.js b/src/main/resources/web/assets/js/trigger/trigger.FIELDWRITEBACK.js index 7249c1bbd..93ee65a6e 100644 --- a/src/main/resources/web/assets/js/trigger/trigger.FIELDWRITEBACK.js +++ b/src/main/resources/web/assets/js/trigger/trigger.FIELDWRITEBACK.js @@ -339,8 +339,9 @@ class ContentFieldWriteback extends ActionContentSpec { sourceField = this._$sourceValue.val() if (!sourceField) return } else if (mode === 'VNULL') { - const tf = this.state.targetFields.find((x) => x.name === targetField) - if (!tf.nullable) return RbHighbar.create($L('目标字段 %s 不能为空', `[ ${tf.label} ]`)) + // v3.6 不校验 + // const tf = this.state.targetFields.find((x) => x.name === targetField) + // if (!tf.nullable) return RbHighbar.create($L('目标字段 %s 不能为空', tf.label)) } const items = this.state.items || [] diff --git a/src/test/java/com/rebuild/core/service/trigger/aviator/AviatorUtilsTest.java b/src/test/java/com/rebuild/core/service/trigger/aviator/AviatorUtilsTest.java index d7676131c..2a19c37cb 100644 --- a/src/test/java/com/rebuild/core/service/trigger/aviator/AviatorUtilsTest.java +++ b/src/test/java/com/rebuild/core/service/trigger/aviator/AviatorUtilsTest.java @@ -92,4 +92,17 @@ class AviatorUtilsTest { Assertions.assertThrows(ExpressionRuntimeException.class, () -> AviatorUtils.eval("date1 + date1", env, false)); } + + @Test + void testDateCompare() { + Map env = new HashMap<>(); + env.put("date1", CalendarUtils.now()); + env.put("date2", CalendarUtils.addDay(1)); + AviatorUtils.eval("p(date1 == date1)", env, true); + AviatorUtils.eval("p(date1 != date2)", env, true); + AviatorUtils.eval("p(date1 > date2)", env, true); + AviatorUtils.eval("p(date1 < date2)", env, true); + AviatorUtils.eval("p(date1 <= date1)", env, true); + AviatorUtils.eval("p(date2 <= date2)", env, true); + } } \ No newline at end of file