diff --git a/.codebeatignore b/.codebeatignore deleted file mode 100644 index 5fae8584f..000000000 --- a/.codebeatignore +++ /dev/null @@ -1,2 +0,0 @@ -/src/test/** -/src/main/webapp/assets/lib/** diff --git a/.codecov.yml b/.codecov.yml index 168510d95..377a9ed46 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -16,3 +16,4 @@ ignore: - "src/test/.*" - "src/main/webapp/.*" - "src/main/java/com/rebuild/web/.*" + - ".*/.*Exception.java" diff --git a/.eslintrc.json b/.eslintrc.json index 14079c468..d547f67f4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,4 @@ +// Use ESLint in locally : `npm install eslint babel-eslint eslint-plugin-react --save-dev` { "env": { "browser": true, @@ -21,6 +22,7 @@ }, "settings": { "react": { + "pragma": "React", "version": "16.10.2" } }, @@ -93,6 +95,7 @@ }, "rules": { "strict": 0, + "no-redeclare": 0, "indent": [2, 2], "linebreak-style": [0, "unix"], "quotes": [2, "single"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..ae5554d84 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @getrebuild @devezhao \ No newline at end of file diff --git a/.gitignore b/.gitignore index f96389c70..bc61075a5 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ rebuild.iml .DS_Store +node_modules +package-lock.json diff --git a/.setup/db-init.sql b/.setup/db-init.sql deleted file mode 100644 index 2edf6ada6..000000000 --- a/.setup/db-init.sql +++ /dev/null @@ -1 +0,0 @@ --- The file has been moved to `src/main/resources/scripts/db-init.sql` \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f7140c092..69af1139b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,7 +14,9 @@ "editor.fontSize": 12, "editor.tabSize": 2, "editor.formatOnSave": true, - "eslint.autoFixOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, "eslint.options": { "configFile": "./.eslintrc.json" }, @@ -26,6 +28,4 @@ "prettier.eslintIntegration": true, "workbench.editor.enablePreview": false, "java.configuration.updateBuildConfiguration": "disabled" -} -// node and eslint(-g) -// Plugins: Beautify and ESLint \ No newline at end of file +} \ No newline at end of file diff --git a/README.md b/README.md index 802810b37..dcc0d2d97 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![codecov](https://codecov.io/gh/getrebuild/rebuild/branch/master/graph/badge.svg)](https://codecov.io/gh/getrebuild/rebuild) [![Build Status](https://travis-ci.org/getrebuild/rebuild.svg?branch=master)](https://travis-ci.org/getrebuild/rebuild) [![Crowdin](https://badges.crowdin.net/rebuild/localized.svg)](https://crowdin.com/project/rebuild) -[![License GPLv3](https://img.shields.io/github/license/getrebuild/rebuild.svg)](https://raw.githubusercontent.com/getrebuild/rebuild/master/LICENSE) -[![License COMMERCIAL](https://img.shields.io/badge/license-COMMERCIAL-orange.svg)](https://raw.githubusercontent.com/getrebuild/rebuild/master/COMMERCIAL) +[![License GPLv3](https://img.shields.io/github/license/getrebuild/rebuild.svg)](https://getrebuild.com/license/LICENSE.txt) +[![License COMMERCIAL](https://img.shields.io/badge/license-COMMERCIAL-orange.svg)](https://getrebuild.com/license/COMMERCIAL.txt) ## 快速开始 @@ -29,6 +29,6 @@ REBUILD is a true production-grade project that fully considers security, robust ## 注意 NOTICE -REBUILD 使用 [开源 GPL-3.0](https://raw.githubusercontent.com/getrebuild/rebuild/master/LICENSE) 和 [商用](https://raw.githubusercontent.com/getrebuild/rebuild/master/COMMERCIAL) 双重授权许可,您应当认真阅读许可内容。使用 REBUILD 即表示您完全同意许可内容/条款。感谢支持! +REBUILD 使用 [开源 GPL-3.0](https://getrebuild.com/license/LICENSE.txt) 和 [商用](https://getrebuild.com/license/COMMERCIAL.txt) 双重授权许可,您应当认真阅读许可内容。使用 REBUILD 即表示您完全同意许可内容/条款。感谢支持! -REBUILD uses the [open source GPL-3.0](https://raw.githubusercontent.com/getrebuild/rebuild/master/LICENSE) and [commercial](https://raw.githubusercontent.com/getrebuild/rebuild/master/COMMERCIAL) dual license agreements, and you should read the contents of the agreement carefully. By using REBUILD, you fully agree to the Licensed Content/Terms. Thanks for the support! \ No newline at end of file +REBUILD uses the [open source GPL-3.0](https://getrebuild.com/license/LICENSE.txt) and [commercial](https://getrebuild.com/license/COMMERCIAL.txt) dual license agreements, and you should read the contents of the agreement carefully. By using REBUILD, you fully agree to the Licensed Content/Terms. Thanks for the support! \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3069f8620..55d0145bb 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.rebuild rebuild war - 1.7.2 + 1.8.0 rebuild Building your business-systems free! https://getrebuild.com/ @@ -127,7 +127,7 @@ com.github.devezhao persist4j - 1.3.4 + 1.3.5 junit @@ -290,6 +290,12 @@ com.alibaba easyexcel 2.1.3 + + + ehcache + org.ehcache + + org.jxls @@ -316,5 +322,10 @@ h2 1.4.200 + + com.googlecode.aviator + aviator + 4.2.9 + diff --git a/src/main/java/com/rebuild/api/ApiGateway.java b/src/main/java/com/rebuild/api/ApiGateway.java index 4c5ec3edc..4d22ffa1d 100644 --- a/src/main/java/com/rebuild/api/ApiGateway.java +++ b/src/main/java/com/rebuild/api/ApiGateway.java @@ -21,6 +21,7 @@ import cn.devezhao.commons.CalendarUtils; import cn.devezhao.commons.EncryptUtils; import cn.devezhao.commons.ObjectUtils; +import cn.devezhao.commons.ThreadPool; import cn.devezhao.commons.web.ServletUtils; import cn.devezhao.persist4j.Record; import cn.devezhao.persist4j.engine.ID; @@ -32,6 +33,7 @@ import com.rebuild.server.service.DataSpecificationException; import com.rebuild.server.service.bizz.UserService; import com.rebuild.utils.AppUtils; +import com.rebuild.utils.CommonsUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -81,6 +83,7 @@ public void api(@PathVariable String apiName, JSON ok = formatSuccess(data); ServletUtils.writeJson(response, ok.toJSONString()); logRequestAsync(reuqestTime, remoteIp, apiName, context, ok); + return; } catch (ApiInvokeException ex) { @@ -201,17 +204,19 @@ protected void logRequestAsync(Date requestTime, String remoteIp, String apiName return; } - Record record = EntityHelper.forNew(EntityHelper.RebuildApiRequest, UserService.SYSTEM_USER); - record.setString("appId", context.getAppId()); - record.setString("remoteIp", remoteIp); - record.setString("requestUrl", apiName + " " + context.getParameterMap()); - if (context.getPostData() != null) { - record.setString("requestBody", context.getPostData().toJSONString()); - } - record.setString("responseBody", result.toJSONString()); - record.setDate("requestTime", requestTime); - record.setDate("responseTime", CalendarUtils.now()); - Application.getCommonService().create(record); + ThreadPool.exec(() -> { + Record record = EntityHelper.forNew(EntityHelper.RebuildApiRequest, UserService.SYSTEM_USER); + record.setString("appId", context.getAppId()); + record.setString("remoteIp", remoteIp); + record.setString("requestUrl", CommonsUtils.maxstr(apiName + "?" + context.getParameterMap(),300)); + if (context.getPostData() != null) { + record.setString("requestBody", CommonsUtils.maxstr(context.getPostData().toJSONString(), 10000)); + } + record.setString("responseBody", CommonsUtils.maxstr(result.toJSONString(), 10000)); + record.setDate("requestTime", requestTime); + record.setDate("responseTime", CalendarUtils.now()); + Application.getCommonService().create(record, false); + }); } // -- 注册 API diff --git a/src/main/java/com/rebuild/api/LoginToken.java b/src/main/java/com/rebuild/api/LoginToken.java index fff49651e..a475e5a06 100644 --- a/src/main/java/com/rebuild/api/LoginToken.java +++ b/src/main/java/com/rebuild/api/LoginToken.java @@ -92,7 +92,7 @@ public static ID verifyToken(String loginToken) { * @return */ public static String checkUser(String user, String password) { - if (!Application.getUserStore().exists(user)) { + if (!Application.getUserStore().existsUser(user)) { return Languages.lang("InputWrong", "UsernameOrPassword"); } diff --git a/src/main/java/com/rebuild/server/Application.java b/src/main/java/com/rebuild/server/Application.java index 94ef596f4..1b44d4fe2 100644 --- a/src/main/java/com/rebuild/server/Application.java +++ b/src/main/java/com/rebuild/server/Application.java @@ -71,7 +71,7 @@ public final class Application { /** Rebuild Version */ - public static final String VER = "1.7.2"; + public static final String VER = "1.8.0"; /** Logging for Global */ diff --git a/src/main/java/com/rebuild/server/ServerListener.java b/src/main/java/com/rebuild/server/ServerListener.java index cf332a0ba..976637ebe 100644 --- a/src/main/java/com/rebuild/server/ServerListener.java +++ b/src/main/java/com/rebuild/server/ServerListener.java @@ -21,7 +21,7 @@ import cn.devezhao.commons.CalendarUtils; import com.rebuild.server.helper.ConfigurableItem; import com.rebuild.server.helper.SysConfiguration; -import com.rebuild.server.helper.setup.InstallAfter; +import com.rebuild.server.helper.setup.InstallState; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -38,7 +38,7 @@ * @author devezhao * @since 10/13/2018 */ -public class ServerListener extends ContextCleanupListener implements InstallAfter { +public class ServerListener extends ContextCleanupListener implements InstallState { private static final Log LOG = LogFactory.getLog(ServerListener.class); diff --git a/src/main/java/com/rebuild/server/ServerStatus.java b/src/main/java/com/rebuild/server/ServerStatus.java index 794e1e4ef..8b8c56714 100644 --- a/src/main/java/com/rebuild/server/ServerStatus.java +++ b/src/main/java/com/rebuild/server/ServerStatus.java @@ -127,7 +127,7 @@ protected static Status checkCreateFile() { if (!test.exists()) { return Status.error(name, "Couldn't create file in temp Directory"); } else { - test.delete(); + FileUtils.deleteQuietly(test); } } catch (Exception ex) { diff --git a/src/main/java/com/rebuild/server/business/approval/ApprovalProcessor.java b/src/main/java/com/rebuild/server/business/approval/ApprovalProcessor.java index abcf21fe3..973f96ad9 100644 --- a/src/main/java/com/rebuild/server/business/approval/ApprovalProcessor.java +++ b/src/main/java/com/rebuild/server/business/approval/ApprovalProcessor.java @@ -124,7 +124,25 @@ public boolean submit(JSONObject selectNextUsers) throws ApprovalException { * @throws ApprovalException */ public void approve(ID approver, ApprovalState state, String remark, JSONObject selectNextUsers) throws ApprovalException { - Integer currentState = (Integer) Application.getQueryFactory().unique(this.record, EntityHelper.ApprovalState)[0]; + approve(approver, state, remark, selectNextUsers, null); + } + + /** + * 审批 + * + * @param approver + * @param state + * @param remark + * @param selectNextUsers + * @param addedData + * @throws ApprovalException + */ + public void approve(ID approver, ApprovalState state, String remark, JSONObject selectNextUsers, Record addedData) throws ApprovalException { + Object[] o = Application.getQueryFactory().unique(this.record, EntityHelper.ApprovalState); + if (o == null) { + throw new NoRecordFoundException("审批记录不存在或你无权查看"); + } + Integer currentState = (Integer) o[0]; if (currentState != ApprovalState.PROCESSING.getState()) { throw new ApprovalException("当前记录已经" + (currentState == ApprovalState.APPROVED.getState() ? "审批完成" : "驳回审批")); } @@ -152,28 +170,28 @@ public void approve(ID approver, ApprovalState state, String remark, JSONObject Set ccs = nextNodes.getCcUsers(this.getUser(), this.record, selectNextUsers); Set nextApprovers = null; String nextNode = null; - if (!nextNodes.isLastStep()) { + + if (state == ApprovalState.APPROVED && !nextNodes.isLastStep()) { nextApprovers = nextNodes.getApproveUsers(this.getUser(), this.record, selectNextUsers); if (nextApprovers.isEmpty()) { throw new ApprovalException("无下一步审批人可用,请联系管理员配置"); } - + FlowNode nextApprovalNode = nextNodes.getApprovalNode(); nextNode = nextApprovalNode != null ? nextApprovalNode.getNodeId() : null; } - + FlowNode currentNode = getFlowParser().getNode((String) stepApprover[2]); Application.getBean(ApprovalStepService.class) - .txApprove(approvedStep, currentNode.getSignMode(), ccs, nextApprovers, nextNode); + .txApprove(approvedStep, currentNode.getSignMode(), ccs, nextApprovers, nextNode, addedData); } /** * 撤销 * - * @param remark * @throws ApprovalException */ - public void cancel(String remark) throws ApprovalException { + public void cancel() throws ApprovalException { Object[] state = Application.getQueryFactory().unique(this.record, EntityHelper.ApprovalState, EntityHelper.ApprovalId); Integer currentState = (Integer) state[0]; if ((Integer) state[0] != ApprovalState.PROCESSING.getState()) { @@ -183,6 +201,13 @@ public void cancel(String remark) throws ApprovalException { Application.getBean(ApprovalStepService.class).txCancel(this.record, (ID) state[1], getCurrentNodeId()); } + /** + * @return + */ + public FlowNode getCurrentNode() { + return getFlowParser().getNode(getCurrentNodeId()); + } + /** * @return * @see #getNextNode(String) diff --git a/src/main/java/com/rebuild/server/business/approval/FlowNode.java b/src/main/java/com/rebuild/server/business/approval/FlowNode.java index 7785ef92f..6e7fd8294 100644 --- a/src/main/java/com/rebuild/server/business/approval/FlowNode.java +++ b/src/main/java/com/rebuild/server/business/approval/FlowNode.java @@ -136,7 +136,7 @@ public Set getSpecUsers(ID operator, ID record) { if (userDefs == null || userDefs.isEmpty()) { return Collections.emptySet(); } - + String userType = userDefs.getString(0); if (USER_SELF.equalsIgnoreCase(userType)) { Set users = new HashSet<>(); @@ -171,7 +171,27 @@ public int hashCode() { @Override public boolean equals(Object obj) { - return obj.hashCode() == this.hashCode(); + if (obj == null) return false; + return obj instanceof FlowNode && obj.hashCode() == this.hashCode(); + } + + /** + * 节点可编辑字段 + * + * @return + */ + public JSONArray getEditableFields() { + JSONArray editableFields = dataMap == null ? null : dataMap.getJSONArray("editableFields"); + if (editableFields == null) { + return null; + } + + editableFields = (JSONArray) JSONUtils.clone(editableFields); + for (Object o : editableFields) { + JSONObject field = (JSONObject) o; + field.put("nullable", !((Boolean) field.remove("notNull"))); + } + return editableFields; } // -- diff --git a/src/main/java/com/rebuild/server/business/approval/FlowNodeGroup.java b/src/main/java/com/rebuild/server/business/approval/FlowNodeGroup.java index 234cf1002..0d9122e5f 100644 --- a/src/main/java/com/rebuild/server/business/approval/FlowNodeGroup.java +++ b/src/main/java/com/rebuild/server/business/approval/FlowNodeGroup.java @@ -21,25 +21,31 @@ import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSONObject; import com.rebuild.server.service.bizz.UserHelper; +import org.springframework.util.Assert; import java.util.HashSet; import java.util.Set; /** + * 1个审批节点+N个抄送节点 + * * @author devezhao zhaofang123@gmail.com * @since 2019/07/11 + * @see FlowNode */ public class FlowNodeGroup { private Set nodes = new HashSet<>(); protected FlowNodeGroup() { + super(); } /** * @param node */ public void addNode(FlowNode node) { + Assert.isNull(getApprovalNode(), "Cannot add multiple approved nodes"); nodes.add(node); } @@ -59,12 +65,8 @@ public boolean allowSelfSelectingCc() { * @return */ public boolean allowSelfSelectingApprover() { - for (FlowNode node : nodes) { - if (node.getType().equals(FlowNode.TYPE_APPROVER) && node.allowSelfSelecting()) { - return true; - } - } - return false; + FlowNode node = getApprovalNode(); + return node != null && node.allowSelfSelecting(); } /** @@ -95,12 +97,12 @@ public Set getCcUsers(ID operator, ID recordId, JSONObject selectUsers) { */ public Set getApproveUsers(ID operator, ID recordId, JSONObject selectUsers) { Set users = new HashSet<>(); - for (FlowNode node : nodes) { - if (FlowNode.TYPE_APPROVER.equals(node.getType())) { - users.addAll(node.getSpecUsers(operator, recordId)); - } + + FlowNode node = getApprovalNode(); + if (node != null) { + users.addAll(node.getSpecUsers(operator, recordId)); } - + if (selectUsers != null) { users.addAll(UserHelper.parseUsers(selectUsers.getJSONArray("selectApprovers"), recordId)); } @@ -114,12 +116,7 @@ public Set getApproveUsers(ID operator, ID recordId, JSONObject selectUsers) */ public boolean isLastStep() { // TODO 对审批最后一步加强判断 - for (FlowNode node : nodes) { - if (node.getType().equals(FlowNode.TYPE_APPROVER)) { - return false; - } - } - return true; + return getApprovalNode() == null; } /** @@ -130,6 +127,8 @@ public boolean isValid() { } /** + * 获取审批节点 + * * @return */ public FlowNode getApprovalNode() { diff --git a/src/main/java/com/rebuild/server/business/approval/FormBuilder.java b/src/main/java/com/rebuild/server/business/approval/FormBuilder.java new file mode 100644 index 000000000..07255796d --- /dev/null +++ b/src/main/java/com/rebuild/server/business/approval/FormBuilder.java @@ -0,0 +1,45 @@ +/* +rebuild - Building your business-systems freely. +Copyright (C) 2020 devezhao + +rebuild is dual-licensed under commercial and open source licenses (GPLv3). +For more information, please see +*/ + +package com.rebuild.server.business.approval; + +import cn.devezhao.persist4j.Record; +import cn.devezhao.persist4j.engine.ID; +import com.alibaba.fastjson.JSONArray; +import com.rebuild.server.configuration.portals.FormsBuilder; + +/** + * 审批可修改字段表单 + * + * @author devezhao + * @since 2020/2/5 + */ +public class FormBuilder { + + final private ID record; + final private ID user; + + /** + * @param record + * @param user + */ + public FormBuilder(ID record, ID user) { + this.record = record; + this.user = user; + } + + /** + * @param elements + * @return + */ + public JSONArray build(JSONArray elements) { + Record data = FormsBuilder.instance.findRecord(record, user, elements); + FormsBuilder.instance.buildModelElements(elements, data.getEntity(), data, user); + return elements; + } +} diff --git a/src/main/java/com/rebuild/server/business/charts/TreeBuilder.java b/src/main/java/com/rebuild/server/business/charts/TreeBuilder.java index b561c6bce..433747f15 100644 --- a/src/main/java/com/rebuild/server/business/charts/TreeBuilder.java +++ b/src/main/java/com/rebuild/server/business/charts/TreeBuilder.java @@ -66,6 +66,10 @@ public JSON toJSON() { for (Object[] o : rows) { double value = (double) o[lastIndex]; + // 排除0,因为0在树图中本就不显示 + if (value <= 0d) { + continue; + } String name = (String) o[0]; Item L1 = thereAll.get(name); @@ -85,7 +89,7 @@ public JSON toJSON() { } } - Item L3 = null; + Item L3; if (lastIndex > 2) { name = name + NAME_SPEA + o[2]; L3 = thereAll.get(name); @@ -102,7 +106,8 @@ public JSON toJSON() { } return treeJson; } - + + // 单项 private class Item { private Item parent; private List children = new ArrayList<>(); @@ -123,9 +128,8 @@ private Item(String name, double value, Item parent) { protected String getName() { return name; -// String[] names = name.split(NAME_SPEA); -// return names[names.length - 1]; } + protected double getValue() { if (this.children.isEmpty()) { return value; diff --git a/src/main/java/com/rebuild/server/business/charts/builtin/ApprovalList.java b/src/main/java/com/rebuild/server/business/charts/builtin/ApprovalList.java index 938e75c60..d55f84a68 100644 --- a/src/main/java/com/rebuild/server/business/charts/builtin/ApprovalList.java +++ b/src/main/java/com/rebuild/server/business/charts/builtin/ApprovalList.java @@ -20,6 +20,7 @@ import cn.devezhao.commons.ObjectUtils; import cn.devezhao.momentjava.Moment; +import cn.devezhao.persist4j.Entity; import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; @@ -30,6 +31,7 @@ import com.rebuild.server.configuration.portals.FieldValueWrapper; import com.rebuild.server.helper.cache.NoRecordFoundException; import com.rebuild.server.metadata.MetadataHelper; +import com.rebuild.server.metadata.entity.EasyMeta; import com.rebuild.server.service.bizz.UserHelper; import com.rebuild.utils.JSONUtils; @@ -73,7 +75,8 @@ public JSONObject getChartConfig() { @Override public JSON build() { final int viewState = ObjectUtils.toInt(getExtraParams().get("state"), ApprovalState.DRAFT.getState()); - final String baseWhere = "where isCanceled = 'F' and isWaiting = 'F' and approver = ? and approvalId <> '' and approvalId is not null and "; + final String baseWhere = "where isCanceled = 'F' and isWaiting = 'F' and approver = ?" + + " and approvalId <> '' and recordId <> '' and "; Object[][] array = Application.createQueryNoFilter( "select createdBy,modifiedOn,recordId,approvalId from RobotApprovalStep " + @@ -101,6 +104,7 @@ public JSON build() { continue; } + Entity entity = MetadataHelper.getEntity(recordId.getEntityCode()); ID s = ApprovalHelper.getSubmitter(recordId, (ID) o[3]); rearray.add(new Object[] { s, @@ -109,7 +113,8 @@ public JSON build() { o[2], label, o[3], - MetadataHelper.getEntityLabel(recordId) + EasyMeta.getLabel(entity), + entity.getName() }); } diff --git a/src/main/java/com/rebuild/server/business/dataimport/DataFileParser.java b/src/main/java/com/rebuild/server/business/dataimport/DataFileParser.java index 54116297c..6f029741e 100644 --- a/src/main/java/com/rebuild/server/business/dataimport/DataFileParser.java +++ b/src/main/java/com/rebuild/server/business/dataimport/DataFileParser.java @@ -41,8 +41,7 @@ public class DataFileParser { * @param sourceFile */ public DataFileParser(File sourceFile) { - this.sourceFile = sourceFile; - this.encoding = "GBK"; + this(sourceFile, "utf-8"); } /** diff --git a/src/main/java/com/rebuild/server/business/datareport/ReportGenerator.java b/src/main/java/com/rebuild/server/business/datareport/ReportGenerator.java index 8f0ba2a5a..c4b390e7c 100644 --- a/src/main/java/com/rebuild/server/business/datareport/ReportGenerator.java +++ b/src/main/java/com/rebuild/server/business/datareport/ReportGenerator.java @@ -64,7 +64,8 @@ public class ReportGenerator extends SetUser { * @param record */ public ReportGenerator(ID reportId, ID record) { - this(DataReportManager.instance.getTemplateFile(MetadataHelper.getEntity(record.getEntityCode()), reportId), record); + this(DataReportManager.instance.getTemplateFile(MetadataHelper.getEntity(record.getEntityCode()), + reportId), record); } /** diff --git a/src/main/java/com/rebuild/server/business/datareport/TemplateExtractor.java b/src/main/java/com/rebuild/server/business/datareport/TemplateExtractor.java index 7eed90316..d3d0aa924 100644 --- a/src/main/java/com/rebuild/server/business/datareport/TemplateExtractor.java +++ b/src/main/java/com/rebuild/server/business/datareport/TemplateExtractor.java @@ -63,10 +63,10 @@ public TemplateExtractor(File template) { public Set extractVars(boolean matchsAny) { List rows = CommonsUtils.readExcel(this.template); - String regex = "\\$\\{[0-9a-zA-Z\\.]+\\}"; + String regex = "\\$\\{[0-9a-zA-Z_.]+}"; // 能够匹配中文 if (matchsAny) { - regex = "\\$\\{.+\\}"; + regex = "\\$\\{.+}"; } // jxls 不支持中文变量 @@ -131,7 +131,7 @@ protected String getRealField(Entity entity, String fieldPath) { String[] paths = fieldPath.split("\\."); List realPaths = new ArrayList<>(); - Field lastField = null; + Field lastField; Entity father = entity; for (String field : paths) { if (father == null) { diff --git a/src/main/java/com/rebuild/server/business/feeds/FeedsHelper.java b/src/main/java/com/rebuild/server/business/feeds/FeedsHelper.java index b02d9189f..44cf8a419 100644 --- a/src/main/java/com/rebuild/server/business/feeds/FeedsHelper.java +++ b/src/main/java/com/rebuild/server/business/feeds/FeedsHelper.java @@ -19,17 +19,22 @@ package com.rebuild.server.business.feeds; import cn.devezhao.bizz.security.member.Team; +import cn.devezhao.commons.CodecUtils; import cn.devezhao.commons.ObjectUtils; import cn.devezhao.persist4j.engine.ID; import com.rebuild.server.Application; import com.rebuild.server.metadata.EntityHelper; import com.rebuild.server.service.bizz.UserHelper; +import com.rebuild.server.service.notification.MessageBuilder; +import com.rebuild.utils.AppUtils; import org.apache.commons.lang.StringUtils; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * @author devezhao @@ -163,4 +168,37 @@ public static boolean checkReadable(ID feedsOrComment, ID user) { } return false; } + + /** + * URL 提取 + */ + public static final Pattern URL_PATTERN = Pattern.compile("((www|https?:\\/\\/)[-a-zA-Z0-9+&@#/%?=~_|!:,.;]{5,300})"); + + /** + * 格式化动态内容 + * + * @param content + * @return + */ + public static String formatContent(String content) { + Matcher urlMatcher = URL_PATTERN.matcher(content); + while (urlMatcher.find()) { + String url = urlMatcher.group(); + String safeUrl = AppUtils.getContextPath() + "/commons/url-safe?url=" + CodecUtils.urlEncode(url); + content = content.replace(url, + String.format("%s", safeUrl, url)); + } + + Matcher atMatcher = MessageBuilder.AT_PATTERN.matcher(content); + while (atMatcher.find()) { + String at = atMatcher.group(); + ID user = ID.valueOf(at.substring(1)); + if (user.getEntityCode() == EntityHelper.User && Application.getUserStore().existsUser(user)) { + String fullName = Application.getUserStore().getUser(user).getFullName(); + content = content.replace(at, String.format("@%s", user, fullName)); + } + } + + return content; + } } diff --git a/src/main/java/com/rebuild/server/business/feeds/FeedsType.java b/src/main/java/com/rebuild/server/business/feeds/FeedsType.java index d2141a4d0..54b8da6b3 100644 --- a/src/main/java/com/rebuild/server/business/feeds/FeedsType.java +++ b/src/main/java/com/rebuild/server/business/feeds/FeedsType.java @@ -28,6 +28,7 @@ public enum FeedsType { ACTIVITY(1, "动态"), FOLLOWUP(2, "跟进"), + ANNOUNCEMENT(3, "公告"), ; diff --git a/src/main/java/com/rebuild/server/business/recyclebin/RecycleBinCleanerJob.java b/src/main/java/com/rebuild/server/business/recyclebin/RecycleBinCleanerJob.java index c4a5d3d4e..ec825d9fb 100644 --- a/src/main/java/com/rebuild/server/business/recyclebin/RecycleBinCleanerJob.java +++ b/src/main/java/com/rebuild/server/business/recyclebin/RecycleBinCleanerJob.java @@ -46,8 +46,9 @@ public class RecycleBinCleanerJob extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { - int keepingDays = SysConfiguration.getInt(ConfigurableItem.RecycleBinKeepingDays); + final int keepingDays = getKeepingDays(); LOG.info("RecycleBinCleanerJob running ... " + keepingDays); + // Keep forever if (keepingDays > 9999) { return; } @@ -61,5 +62,16 @@ protected void executeInternal(JobExecutionContext context) throws JobExecutionE CalendarUtils.getUTCDateFormat().format(before)); int del = Application.getSQLExecutor().execute(delSql, 120); LOG.warn("RecycleBin cleaned : " + del); + + // TODO 相关引用也在此时一并删除,因为记录已经彻底删除了 + } + + /** + * 回收站保留天数。小于等于 0 表示未开启回收站,大于等于 9999 表示永远保留 + * + * @return + */ + public static int getKeepingDays() { + return SysConfiguration.getInt(ConfigurableItem.RecycleBinKeepingDays); } } diff --git a/src/main/java/com/rebuild/server/business/recyclebin/RecycleRestore.java b/src/main/java/com/rebuild/server/business/recyclebin/RecycleRestore.java index 06606d198..e50480ae0 100644 --- a/src/main/java/com/rebuild/server/business/recyclebin/RecycleRestore.java +++ b/src/main/java/com/rebuild/server/business/recyclebin/RecycleRestore.java @@ -27,7 +27,9 @@ import com.alibaba.fastjson.JSONObject; import com.rebuild.server.Application; import com.rebuild.server.RebuildException; +import com.rebuild.server.metadata.EntityHelper; import com.rebuild.server.metadata.MetadataHelper; +import com.rebuild.server.service.OperatingContext; import com.rebuild.server.service.TransactionManual; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -68,7 +70,7 @@ public int restore() { /** * 恢复数据 * - * @param cascade 级联恢复 + * @param cascade 恢复关联删除的数据 * @return */ public int restore(boolean cascade) { @@ -84,9 +86,9 @@ public int restore(boolean cascade) { final List recycleIds = new ArrayList<>(); - final List willRestores = new ArrayList<>(toRecord(JSON.parseObject((String) main[0]), (ID) main[1])); + final List willRestores = new ArrayList<>(conver2Record(JSON.parseObject((String) main[0]), (ID) main[1])); if (willRestores.isEmpty()) { - throw new RebuildException("记录的所属实体不存在"); + throw new RebuildException("记录实体已经不存在"); } recycleIds.add((ID) main[2]); @@ -96,7 +98,7 @@ public int restore(boolean cascade) { .setParameter(1, main[1]) .array(); for (Object[] o : array) { - List records = toRecord(JSON.parseObject((String) o[0]), (ID) o[1]); + List records = conver2Record(JSON.parseObject((String) o[0]), (ID) o[1]); if (!records.isEmpty()) { willRestores.addAll(records); recycleIds.add((ID) o[2]); @@ -114,13 +116,12 @@ public int restore(boolean cascade) { String primaryName = r.getEntity().getPrimaryField().getName(); ID primaryId = (ID) r.removeValue(primaryName); PM.saveInternal(r, primaryId); + restoreAttachment(PM, primaryId); - // 非明细才计数 - if (r.getEntity().getMasterEntity() == null) { - restored++; - } + restored++; } + // 从回收站删除 PM.delete(recycleIds.toArray(new ID[0])); TransactionManual.commit(status); @@ -133,11 +134,13 @@ public int restore(boolean cascade) { } /** + * 转换成 Record 对象,返回多条是可能存在明细 + * * @param content * @param recordId * @return */ - private List toRecord(JSONObject content, ID recordId) { + private List conver2Record(JSONObject content, ID recordId) { if (!MetadataHelper.containsEntity(recordId.getEntityCode())) { return Collections.emptyList(); } @@ -162,4 +165,21 @@ private List toRecord(JSONObject content, ID recordId) { } return records; } + + /** + * @param PM + * @param recordId + * @see com.rebuild.server.service.base.AttachmentAwareObserver#onDelete(OperatingContext) + */ + private void restoreAttachment(PersistManagerImpl PM, ID recordId) { + Object[][] array = Application.createQueryNoFilter( + "select attachmentId from Attachment where relatedRecord = ?") + .setParameter(1, recordId) + .array(); + for (Object[] o : array) { + Record u = EntityHelper.forUpdate((ID) o[0], null, false); + u.setBoolean(EntityHelper.IsDeleted, false); + PM.update(u); + } + } } diff --git a/src/main/java/com/rebuild/server/business/trigger/ActionType.java b/src/main/java/com/rebuild/server/business/trigger/ActionType.java index b90fb0624..7b6f7673e 100644 --- a/src/main/java/com/rebuild/server/business/trigger/ActionType.java +++ b/src/main/java/com/rebuild/server/business/trigger/ActionType.java @@ -21,6 +21,7 @@ import com.rebuild.server.business.trigger.impl.AutoAssign; import com.rebuild.server.business.trigger.impl.AutoShare; import com.rebuild.server.business.trigger.impl.FieldAggregation; +import com.rebuild.server.business.trigger.impl.FieldWriteback; import com.rebuild.server.business.trigger.impl.SendNotification; import org.springframework.cglib.core.ReflectUtils; @@ -35,8 +36,8 @@ public enum ActionType { FIELDAGGREGATION("数据聚合", FieldAggregation.class), - SENDNOTIFICATION("发送通知 (内部消息)", SendNotification.class), - + FIELDWRITEBACK("数据回写", FieldWriteback.class), + SENDNOTIFICATION("发送通知", SendNotification.class), AUTOSHARE("自动共享", AutoShare.class), AUTOASSIGN("自动分派", AutoAssign.class), @@ -70,7 +71,7 @@ public Class getActionClazz() { * @throws NoSuchMethodException */ public TriggerAction newInstance(ActionContext context) throws NoSuchMethodException { - Constructor c = getActionClazz().getConstructor(ActionContext.class); + Constructor c = getActionClazz().getConstructor(ActionContext.class); return (TriggerAction) ReflectUtils.newInstance(c, new Object[] { context }); } } diff --git a/src/main/java/com/rebuild/server/business/trigger/RobotTriggerObserver.java b/src/main/java/com/rebuild/server/business/trigger/RobotTriggerObserver.java index 317ac0297..6e00437cd 100644 --- a/src/main/java/com/rebuild/server/business/trigger/RobotTriggerObserver.java +++ b/src/main/java/com/rebuild/server/business/trigger/RobotTriggerObserver.java @@ -78,6 +78,10 @@ protected void execAction(OperatingContext context, TriggerWhen when) { if (cleanSource) { setTriggerSource(context); } + // 自己触发自己,避免无限执行 + else if (getTriggerSource().getAnyRecord().getPrimary().equals(context.getAnyRecord().getPrimary())) { + return; + } final ID currentUser = Application.getCurrentUser(); try { diff --git a/src/main/java/com/rebuild/server/business/trigger/impl/AggregationEvaluator.java b/src/main/java/com/rebuild/server/business/trigger/impl/AggregationEvaluator.java new file mode 100644 index 000000000..8250a8e9b --- /dev/null +++ b/src/main/java/com/rebuild/server/business/trigger/impl/AggregationEvaluator.java @@ -0,0 +1,162 @@ +/* +rebuild - Building your business-systems freely. +Copyright (C) 2018-2020 devezhao + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package com.rebuild.server.business.trigger.impl; + +import cn.devezhao.persist4j.Entity; +import cn.devezhao.persist4j.engine.ID; +import com.alibaba.fastjson.JSONObject; +import com.googlecode.aviator.AviatorEvaluator; +import com.googlecode.aviator.AviatorEvaluatorInstance; +import com.googlecode.aviator.Options; +import com.googlecode.aviator.exception.ExpressionSyntaxErrorException; +import com.rebuild.server.Application; +import com.rebuild.server.metadata.MetadataHelper; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 归集计算 + * + * @author devezhao + * @since 2020/1/16 + */ +public class AggregationEvaluator { + + private static final Log LOG = LogFactory.getLog(FieldAggregation.class); + + private static final Pattern FIELD_PATT = Pattern.compile("\\{(.*?)}"); + + private static AviatorEvaluatorInstance AVIATOR = AviatorEvaluator.newInstance(); + static { + // 强制使用 BigDecimal/BigInteger 运算 + AVIATOR.setOption(Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL, true); + } + + final private Entity sourceEntity; + final private JSONObject item; + final private String followSourceField; + final private String filterSql; + + /** + * @param item + * @param sourceEntity + * @param followSourceField + * @param filterSql + */ + protected AggregationEvaluator(JSONObject item, Entity sourceEntity, String followSourceField, String filterSql) { + this.sourceEntity = sourceEntity; + this.item = item; + this.followSourceField = followSourceField; + this.filterSql = filterSql; + } + + /** + * @param triggerRecord + * @return + */ + public Object eval(ID triggerRecord) { + String calcMode = item.getString("calcMode"); + if ("FORMULA".equalsIgnoreCase(calcMode)) { + return evalFormula(triggerRecord); + } + + String sourceField = item.getString("sourceField"); + if (MetadataHelper.getLastJoinField(sourceEntity, sourceField) == null) { + return null; + } + + String funcAndField = String.format("%s(%s)", calcMode, sourceField); + String sql = String.format("select %s from %s where %s = ?", + funcAndField, sourceEntity.getName(), followSourceField); + if (filterSql != null) { + sql += " and " + filterSql; + } + + Object[] o = Application.createQueryNoFilter(sql) + .setParameter(1, triggerRecord) + .unique(); + return o == null || o[0] == null ? 0 : o[0]; + } + + /** + * @param triggerRecord + * @return + */ + private Object evalFormula(ID triggerRecord) { + String formula = item.getString("sourceFormula"); + Matcher m = FIELD_PATT.matcher(formula); + + final List fields = new ArrayList<>(); + while (m.find()) { + String[] fieldAndFunc = m.group(1).split("\\$\\$\\$\\$"); + if (MetadataHelper.getLastJoinField(sourceEntity, fieldAndFunc[0]) != null) { + fields.add(fieldAndFunc); + } + } + if (fields.isEmpty()) { + return null; + } + + StringBuilder sql = new StringBuilder("select "); + for (String[] field : fields) { + if (field.length == 2) { + sql.append(String.format("%s(%s)", field[1], field[0])); + } else { + sql.append(field[0]); + } + sql.append(','); + } + sql.deleteCharAt(sql.length() - 1) + .append(" from ").append(sourceEntity.getName()) + .append(" where ").append(followSourceField).append(" = ?"); + if (filterSql != null) { + sql.append(" and ").append(filterSql); + } + + Object[] o = Application.createQueryNoFilter(sql.toString()) + .setParameter(1, triggerRecord) + .unique(); + if (o == null) { + return null; + } + + String newFormual = formula.toUpperCase() + .replace("×", "*") + .replace("÷", "/"); + for (int i = 0; i < fields.size(); i++) { + String[] field = fields.get(i); + Object v = o[i] == null ? "0" : o[i]; + String replace = "{" + StringUtils.join(field, "$$$$") + "}"; + newFormual = newFormual.replace(replace.toUpperCase(), v.toString()); + } + + try { + return AVIATOR.execute(newFormual); + } catch (ExpressionSyntaxErrorException ex) { + LOG.error("Bad formula : " + formula + " > " + newFormual, ex); + return null; + } + } +} diff --git a/src/main/java/com/rebuild/server/business/trigger/impl/CompatibleValueConversion.java b/src/main/java/com/rebuild/server/business/trigger/impl/CompatibleValueConversion.java new file mode 100644 index 000000000..0c44186e1 --- /dev/null +++ b/src/main/java/com/rebuild/server/business/trigger/impl/CompatibleValueConversion.java @@ -0,0 +1,134 @@ +/* +rebuild - Building your business-systems freely. +Copyright (C) 2020 devezhao + +rebuild is dual-licensed under commercial and open source licenses (GPLv3). +For more information, please see +*/ + +package com.rebuild.server.business.trigger.impl; + +import cn.devezhao.commons.CalendarUtils; +import cn.devezhao.persist4j.Field; +import cn.devezhao.persist4j.engine.ID; +import cn.devezhao.persist4j.engine.NullValue; +import com.rebuild.server.configuration.portals.FieldValueWrapper; +import com.rebuild.server.configuration.portals.PickListManager; +import com.rebuild.server.metadata.entity.DisplayType; +import com.rebuild.server.metadata.entity.EasyMeta; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * 字段值兼容转换 + * + * @author ZHAO + * @since 2020/2/8 + */ +public class CompatibleValueConversion { + + private static final Log LOG = LogFactory.getLog(CompatibleValueConversion.class); + + final private Field source; + final private Field target; + + /** + * @param sourceField + * @param targetField + */ + public CompatibleValueConversion(Field sourceField, Field targetField) { + this.source = sourceField; + this.target = targetField; + } + + /** + * @param sourceValue + * @return + */ + public Object conversion(Object sourceValue) { + return conversion(sourceValue, false); + } + + /** + * @param sourceValue + * @param mixValue + * @return + */ + public Object conversion(Object sourceValue, boolean mixValue) { + if (sourceValue == null || NullValue.is(sourceValue)) { + return null; + } + + final EasyMeta sourceField = EasyMeta.valueOf(source); + final DisplayType sourceType = sourceField.getDisplayType(); + final DisplayType targetType = EasyMeta.getDisplayType(target); + final boolean is2Text = targetType == DisplayType.TEXT || targetType == DisplayType.NTEXT; + + Object compatibleValue = sourceValue; + if (sourceType == DisplayType.ID) { + if (is2Text) { + compatibleValue = sourceValue.toString().toUpperCase(); + } + } + else if (sourceType == DisplayType.REFERENCE) { + if (is2Text) { + compatibleValue = FieldValueWrapper.getLabelNotry((ID) sourceValue); + } else if (mixValue) { + String text = FieldValueWrapper.getLabelNotry((ID) sourceValue); + compatibleValue = FieldValueWrapper.wrapMixValue((ID) sourceValue, text); + } + } + else if (sourceType == DisplayType.CLASSIFICATION) { + if (is2Text) { + compatibleValue = FieldValueWrapper.instance.wrapFieldValue(sourceValue, sourceField, true); + } else if (mixValue) { + compatibleValue = FieldValueWrapper.instance.wrapFieldValue(sourceValue, sourceField, false); + } + } + else if (sourceType == DisplayType.PICKLIST) { + String text = FieldValueWrapper.instance.wrapPickList(sourceValue, sourceField); + if (is2Text) { + compatibleValue = text; + } else { + // 转换 PickList ID + compatibleValue = PickListManager.instance.findItemByLabel(text, target); + if (compatibleValue == null) { + LOG.warn("Cannot find value of PickList : " + text + " << " + target); + } + } + } + else if (sourceType == DisplayType.STATE) { + if (is2Text) { + compatibleValue = FieldValueWrapper.instance.wrapState(sourceValue, sourceField); + } + } + else if (sourceType == DisplayType.DATETIME && targetType == DisplayType.DATE) { + String datetime = FieldValueWrapper.instance.wrapDatetime(sourceValue, sourceField); + compatibleValue = datetime.split(" ")[0]; + if (!(is2Text || mixValue)) { + compatibleValue = CalendarUtils.parse((String) compatibleValue); + } + } + else if (sourceType == DisplayType.DATE && targetType == DisplayType.DATETIME) { + String date = FieldValueWrapper.instance.wrapDate(sourceValue, sourceField); + if (date.length() == 4) { // YYYY + compatibleValue = date + "01-01 00:00:00"; + } else if (date.length() == 7) { // YYYY-MM + compatibleValue = date + "-01 00:00:00"; + } else { + compatibleValue = date + " 00:00:00"; + } + + if (!(is2Text || mixValue)) { + compatibleValue = CalendarUtils.parse((String) compatibleValue); + } + } + else if (is2Text) { + compatibleValue = FieldValueWrapper.instance.wrapFieldValue(sourceValue, sourceField); + } + // 整数/浮点数无需转换,因为持久层框架已有兼容处理 + + return compatibleValue; + } + +} diff --git a/src/main/java/com/rebuild/server/business/trigger/impl/FieldAggregation.java b/src/main/java/com/rebuild/server/business/trigger/impl/FieldAggregation.java index ae3ad0e2f..08c5cda1d 100644 --- a/src/main/java/com/rebuild/server/business/trigger/impl/FieldAggregation.java +++ b/src/main/java/com/rebuild/server/business/trigger/impl/FieldAggregation.java @@ -51,53 +51,69 @@ * @see com.rebuild.server.business.trigger.RobotTriggerObserver */ public class FieldAggregation implements TriggerAction { - - private static final Log LOG = LogFactory.getLog(FieldAggregation.class); - - // 此触发器可能产生连锁反应 - // 如触发器 A 调用 B,而 B 又调用了 C ... 以此类推。此处记录其深度 - private static final ThreadLocal CALL_CHAIN_DEPTH = new ThreadLocal<>(); - // 最大调用深度 - private static final int MAX_DEPTH = 5; - - final private ActionContext context; - - // 允许无权限更新 - private boolean allowNoPermissionUpdate; - - private Entity sourceEntity; - private Entity targetEntity; - - private String followSourceField; - private ID targetRecordId; - - public FieldAggregation(ActionContext context) { - this(context, Boolean.TRUE); - } - public FieldAggregation(ActionContext context, boolean allowNoPermissionUpdate) { - this.context = context; - this.allowNoPermissionUpdate = allowNoPermissionUpdate; + private static final Log LOG = LogFactory.getLog(FieldAggregation.class); + + /** + * 归集到自己 + */ + public static final String SOURCE_SELF = "$PRIMARY$"; + + final protected ActionContext context; + // 允许无权限更新 + final private boolean allowNoPermissionUpdate; + // 最大触发链深度 + final private int maxTriggerDepth; + + // 此触发器可能产生连锁反应 + // 如触发器 A 调用 B,而 B 又调用了 C ... 以此类推。此处记录其深度 + private static final ThreadLocal TRIGGER_CHAIN_DEPTH = new ThreadLocal<>(); + + // 源实体 + protected Entity sourceEntity; + // 目标实体 + protected Entity targetEntity; + // 关联字段 + protected String followSourceField; + // 触发记录 + protected ID targetRecordId; + + /** + * @param context + */ + public FieldAggregation(ActionContext context) { + this(context, Boolean.TRUE, 5); } - + + /** + * @param context + * @param allowNoPermissionUpdate + * @param maxTriggerDepth + */ + protected FieldAggregation(ActionContext context, boolean allowNoPermissionUpdate, int maxTriggerDepth) { + this.context = context; + this.allowNoPermissionUpdate = allowNoPermissionUpdate; + this.maxTriggerDepth = maxTriggerDepth; + } + @Override public ActionType getType() { return ActionType.FIELDAGGREGATION; } - - @Override - public boolean isUsableSourceEntity(int entityCode) { - return true; - } - - @Override + + @Override + public boolean isUsableSourceEntity(int entityCode) { + return true; + } + + @Override public void execute(OperatingContext operatingContext) throws TriggerException { - Integer depth = CALL_CHAIN_DEPTH.get(); + Integer depth = TRIGGER_CHAIN_DEPTH.get(); if (depth == null) { depth = 1; } - if (depth > MAX_DEPTH) { - throw new TriggerException("Too many call-chain with triggers : " + depth); + if (depth > maxTriggerDepth) { + throw new TriggerException("Too many trigger-chain with triggers : " + depth); } this.prepare(operatingContext); @@ -116,56 +132,54 @@ public void execute(OperatingContext operatingContext) throws TriggerException { // 聚合数据过滤 JSONObject dataFilter = ((JSONObject) context.getActionContent()).getJSONObject("dataFilter"); - String dataFilterWhere = null; + String dataFilterSql = null; if (dataFilter != null && !dataFilter.isEmpty()) { - dataFilterWhere = new AdvFilterParser(dataFilter).toSqlWhere(); + dataFilterSql = new AdvFilterParser(dataFilter).toSqlWhere(); } - // 更新目标 - Record targetRecord = EntityHelper.forUpdate(targetRecordId, UserService.SYSTEM_USER, false); - - JSONArray items = ((JSONObject) context.getActionContent()).getJSONArray("items"); - for (Object o : items) { - JSONObject item = (JSONObject) o; - String sourceField = item.getString("sourceField"); - String targetField = item.getString("targetField"); - if (!MetadataHelper.checkAndWarnField(sourceEntity, sourceField) - || !MetadataHelper.checkAndWarnField(targetEntity, targetField)) { - continue; - } - - // 直接利用 SQL 函数计算结果 - String calcMode = item.getString("calcMode"); - String calcField = "COUNT".equalsIgnoreCase(calcMode) ? sourceEntity.getPrimaryField().getName() : sourceField; - - String sql = String.format("select %s(%s) from %s where %s = ?", - calcMode, calcField, sourceEntity.getName(), followSourceField); - if (dataFilterWhere != null) { - sql += " and " + dataFilterWhere; - } + Record targetRecord = EntityHelper.forUpdate(targetRecordId, UserService.SYSTEM_USER, false); + buildTargetRecord(targetRecord, dataFilterSql); - Object[] result = Application.createQueryNoFilter(sql).setParameter(1, targetRecordId).unique(); - double calcValue = result == null || result[0] == null ? 0d : ObjectUtils.toDouble(result[0]); - - DisplayType dt = EasyMeta.getDisplayType(targetEntity.getField(targetField)); - if (dt == DisplayType.NUMBER) { - targetRecord.setInt(targetField, (int) calcValue); - } else if (dt == DisplayType.DECIMAL) { - targetRecord.setDouble(targetField, calcValue); - } - } - - if (targetRecord.getAvailableFieldIterator().hasNext()) { + // 不含 ID + if (targetRecord.getAvailableFields().size() > 1) { if (allowNoPermissionUpdate) { PrivilegesGuardInterceptor.setNoPermissionPassOnce(targetRecordId); } // 会关联触发下一触发器(如有) - CALL_CHAIN_DEPTH.set(depth + 1); + TRIGGER_CHAIN_DEPTH.set(depth + 1); Application.getEntityService(targetEntity.getEntityCode()).update(targetRecord); } } + /** + * @param record + * @param dataFilterSql + */ + protected void buildTargetRecord(Record record, String dataFilterSql) { + JSONArray items = ((JSONObject) context.getActionContent()).getJSONArray("items"); + for (Object o : items) { + JSONObject item = (JSONObject) o; + String targetField = item.getString("targetField"); + if (!MetadataHelper.checkAndWarnField(targetEntity, targetField)) { + continue; + } + + Object evalValue = new AggregationEvaluator(item, sourceEntity, followSourceField, dataFilterSql) + .eval(targetRecordId); + if (evalValue == null) { + continue; + } + + DisplayType dt = EasyMeta.getDisplayType(targetEntity.getField(targetField)); + if (dt == DisplayType.NUMBER) { + record.setLong(targetField, ObjectUtils.toLong(evalValue)); + } else if (dt == DisplayType.DECIMAL) { + record.setDouble(targetField, ObjectUtils.toDouble(evalValue)); + } + } + } + @Override public void prepare(OperatingContext operatingContext) throws TriggerException { if (sourceEntity != null) { // 已经初始化 @@ -180,22 +194,29 @@ public void prepare(OperatingContext operatingContext) throws TriggerException { this.sourceEntity = context.getSourceEntity(); this.targetEntity = MetadataHelper.getEntity(targetFieldEntity[1]); - this.followSourceField = targetFieldEntity[0]; - if (!sourceEntity.containsField(followSourceField)) { - return; - } - // 找到主记录 - Object[] o = Application.getQueryFactory().uniqueNoFilter( - context.getSourceRecord(), followSourceField, followSourceField + "." + EntityHelper.OwningUser); - // o[1] 为空说明记录不存在 - if (o != null && o[0] != null && o[1] != null) { - this.targetRecordId = (ID) o[0]; + // 自己 + if (SOURCE_SELF.equalsIgnoreCase(targetFieldEntity[0])) { + this.followSourceField = sourceEntity.getPrimaryField().getName(); + this.targetRecordId = context.getSourceRecord(); + } else { + this.followSourceField = targetFieldEntity[0]; + if (!sourceEntity.containsField(followSourceField)) { + return; + } + + // 找到主记录 + Object[] o = Application.getQueryFactory().uniqueNoFilter( + context.getSourceRecord(), followSourceField, followSourceField + "." + EntityHelper.OwningUser); + // o[1] 为空说明记录不存在 + if (o != null && o[0] != null && o[1] != null) { + this.targetRecordId = (ID) o[0]; + } } } @Override public void clean() { - CALL_CHAIN_DEPTH.remove(); + TRIGGER_CHAIN_DEPTH.remove(); } } diff --git a/src/main/java/com/rebuild/server/business/trigger/impl/FieldWriteback.java b/src/main/java/com/rebuild/server/business/trigger/impl/FieldWriteback.java new file mode 100644 index 000000000..dd7ef9f0c --- /dev/null +++ b/src/main/java/com/rebuild/server/business/trigger/impl/FieldWriteback.java @@ -0,0 +1,90 @@ +/* +rebuild - Building your business-systems freely. +Copyright (C) 2020 devezhao + +rebuild is dual-licensed under commercial and open source licenses (GPLv3). +For more information, please see +*/ + +package com.rebuild.server.business.trigger.impl; + +import cn.devezhao.persist4j.Field; +import cn.devezhao.persist4j.Record; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.rebuild.server.Application; +import com.rebuild.server.business.trigger.ActionContext; +import com.rebuild.server.business.trigger.ActionType; +import com.rebuild.server.metadata.MetadataHelper; +import org.apache.commons.lang.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * 数据回填 + * + * @author devezhao + * @since 2020/2/7 + * + * @see com.rebuild.server.configuration.AutoFillinManager + */ +public class FieldWriteback extends FieldAggregation { + + /** + * @param context + */ + public FieldWriteback(ActionContext context) { + super(context, Boolean.TRUE, 5); + } + + @Override + public ActionType getType() { + return ActionType.FIELDWRITEBACK; + } + + @Override + protected void buildTargetRecord(Record record, String dataFilterSql) { + JSONArray items = ((JSONObject) context.getActionContent()).getJSONArray("items"); + Map t2sMap = new HashMap<>(); + for (Object o : items) { + JSONObject item = (JSONObject) o; + String targetField = item.getString("targetField"); + String sourceField = item.getString("sourceField"); + if (!MetadataHelper.checkAndWarnField(targetEntity, targetField) + || MetadataHelper.getLastJoinField(sourceEntity, sourceField) == null) { + continue; + } + t2sMap.put(targetField, sourceField); + } + + if (t2sMap.isEmpty()) { + return; + } + + String sql = String.format("select %s from %s where %s = ?", + StringUtils.join(t2sMap.values(), ","), sourceEntity.getName(), followSourceField); + + final Record o = Application.createQueryNoFilter(sql) + .setParameter(1, targetRecordId) + .record(); + if (o == null) { + return; + } + + for (Map.Entry e : t2sMap.entrySet()) { + Object value = o.getObjectValue(e.getValue()); + // NOTE 忽略空值 + if (value == null) { + continue; + } + + Field sourceField = MetadataHelper.getLastJoinField(sourceEntity, e.getValue()); + Field targetField = targetEntity.getField(e.getKey()); + Object newValue = new CompatibleValueConversion(sourceField, targetField).conversion(value); + if (newValue != null) { + record.setObjectValue(targetField.getName(), newValue); + } + } + } +} diff --git a/src/main/java/com/rebuild/server/business/trigger/impl/SendNotification.java b/src/main/java/com/rebuild/server/business/trigger/impl/SendNotification.java index f46f8767f..7d16cd949 100644 --- a/src/main/java/com/rebuild/server/business/trigger/impl/SendNotification.java +++ b/src/main/java/com/rebuild/server/business/trigger/impl/SendNotification.java @@ -18,6 +18,8 @@ package com.rebuild.server.business.trigger.impl; +import cn.devezhao.persist4j.Entity; +import cn.devezhao.persist4j.Record; import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; @@ -26,14 +28,22 @@ import com.rebuild.server.business.trigger.ActionType; import com.rebuild.server.business.trigger.TriggerAction; import com.rebuild.server.business.trigger.TriggerException; +import com.rebuild.server.configuration.portals.FieldValueWrapper; +import com.rebuild.server.helper.SMSender; +import com.rebuild.server.metadata.MetadataHelper; import com.rebuild.server.service.OperatingContext; import com.rebuild.server.service.bizz.UserHelper; import com.rebuild.server.service.notification.Message; import com.rebuild.server.service.notification.MessageBuilder; +import org.apache.commons.lang.StringUtils; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * @author devezhao zhaofang123@gmail.com @@ -41,6 +51,14 @@ */ public class SendNotification implements TriggerAction { + // 内部消息 + @SuppressWarnings("unused") + private static final int TYPE_NOTIFICATION = 1; + // 邮件 + private static final int TYPE_MAIL = 2; + // 短信 + private static final int TYPE_SMS = 3; + final private ActionContext context; public SendNotification(ActionContext context) { @@ -70,28 +88,84 @@ public void execute(OperatingContext operatingContext) { if (toUsers.isEmpty()) { return; } - + String message = content.getString("content"); message = formatMessage(message, context.getSourceRecord()); + + final int type = content.getIntValue("type"); + final String title = StringUtils.defaultIfBlank(content.getString("title"), "你有一条新通知"); + for (ID user : toUsers) { - Message m = MessageBuilder.createMessage(user, message, context.getSourceRecord()); - Application.getNotifications().send(m); + if (type == TYPE_MAIL) { + if (!SMSender.availableMail()) break; + + String emailAddr = Application.getUserStore().getUser(user).getEmail(); + if (emailAddr != null) { + SMSender.sendMail(emailAddr, title, message); + } + + } else if (type == TYPE_SMS) { + // TODO 发送短信(暂无手机字段) + + } else { + Message m = MessageBuilder.createMessage(user, message, context.getSourceRecord()); + Application.getNotifications().send(m); + } } } @Override public void prepare(OperatingContext operatingContext) throws TriggerException { - // Nothings + // NOOP } + private static final Pattern PATT_FIELD = Pattern.compile("\\{([0-9a-zA-Z._]+)}"); /** * @param message * @param recordId * @return */ - private String formatMessage(String message, ID recordId) { - // TODO 处理变量 -// return message + " @" + recordId; + protected String formatMessage(String message, ID recordId) { + Map vars = null; + if (recordId != null) { + Entity entity = MetadataHelper.getEntity(recordId.getEntityCode()); + vars = new HashMap<>(); + + Matcher m = PATT_FIELD.matcher(message); + while (m.find()) { + String field = m.group(1); + if (MetadataHelper.getLastJoinField(entity, field) == null) { + continue; + } + vars.put(field, null); + } + + if (!vars.isEmpty()) { + String sql = String.format("select %s from %s where %s = ?", + StringUtils.join(vars.keySet(), ","), entity.getName(), entity.getPrimaryField().getName()); + + Record o = Application.createQueryNoFilter(sql) + .setParameter(1, recordId) + .record(); + if (o != null) { + for (String field : vars.keySet()) { + Object value = o.getObjectValue(field); + value = FieldValueWrapper.instance.wrapFieldValue( + value, MetadataHelper.getLastJoinField(entity, field), true); + if (value != null) { + vars.put(field, value.toString()); + } + } + } + } + } + + if (vars != null) { + for (Map.Entry e : vars.entrySet()) { + message = message.replaceAll( + "\\{" + e.getKey() + "}", StringUtils.defaultIfBlank(e.getValue(), StringUtils.EMPTY)); + } + } return message; } diff --git a/src/main/java/com/rebuild/server/configuration/AutoFillinManager.java b/src/main/java/com/rebuild/server/configuration/AutoFillinManager.java index 5822336bf..52a8207a4 100644 --- a/src/main/java/com/rebuild/server/configuration/AutoFillinManager.java +++ b/src/main/java/com/rebuild/server/configuration/AutoFillinManager.java @@ -22,13 +22,13 @@ import cn.devezhao.persist4j.Field; import cn.devezhao.persist4j.Record; import cn.devezhao.persist4j.engine.ID; +import cn.devezhao.persist4j.engine.NullValue; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.rebuild.server.Application; -import com.rebuild.server.configuration.portals.FieldValueWrapper; +import com.rebuild.server.business.trigger.impl.CompatibleValueConversion; import com.rebuild.server.metadata.MetadataHelper; -import com.rebuild.server.metadata.entity.DisplayType; import com.rebuild.server.metadata.entity.EasyMeta; import com.rebuild.utils.JSONUtils; import org.apache.commons.lang.StringUtils; @@ -57,6 +57,11 @@ private AutoFillinManager() { } * @return */ public JSONArray getFillinValue(Field field, ID source) { + // @see field-edit.jsp 内建字段无配置 + if (EasyMeta.valueOf(field).isBuiltin()) { + return JSONUtils.EMPTY_ARRAY; + } + final List config = getConfig(field); if (config.isEmpty()) { return JSONUtils.EMPTY_ARRAY; @@ -101,7 +106,7 @@ public JSONArray getFillinValue(Field field, ID source) { } // NOTE 忽略空值 - if (value == null || StringUtils.isBlank(value.toString())) { + if (value == null || NullValue.is(value) || StringUtils.isBlank(value.toString())) { continue; } @@ -119,40 +124,10 @@ public JSONArray getFillinValue(Field field, ID source) { * @param target * @param value * @return + * @see CompatibleValueConversion */ protected Object conversionCompatibleValue(Field source, Field target, Object value) { - DisplayType sourceType = EasyMeta.getDisplayType(source); - DisplayType targetType = EasyMeta.getDisplayType(target); - boolean is2Text = targetType == DisplayType.TEXT || targetType == DisplayType.NTEXT; - EasyMeta sourceField = EasyMeta.valueOf(source); - - Object compatibleValue = null; - if (sourceType == DisplayType.REFERENCE) { - String idLabel = FieldValueWrapper.getLabelNotry((ID) value); - if (is2Text) { - compatibleValue = idLabel; - } else { - compatibleValue = FieldValueWrapper.wrapMixValue((ID) value, idLabel); - } - } else if (sourceType == DisplayType.CLASSIFICATION) { - compatibleValue = FieldValueWrapper.instance.wrapFieldValue(value, sourceField, is2Text); - } else if (sourceType == DisplayType.PICKLIST || sourceType == DisplayType.STATE) { - if (is2Text) { - compatibleValue = FieldValueWrapper.instance.wrapFieldValue(value, sourceField); - } else { - compatibleValue = value; - } - } else if (sourceType == DisplayType.DATETIME && targetType == DisplayType.DATE) { - String datetime = FieldValueWrapper.instance.wrapDatetime(value, sourceField); - compatibleValue = datetime.split(" ")[0]; - } else if (sourceType == DisplayType.DATE && targetType == DisplayType.DATETIME) { - String date = FieldValueWrapper.instance.wrapDate(value, sourceField); - compatibleValue = date + " 00:00:00"; - } else { - compatibleValue = FieldValueWrapper.instance.wrapFieldValue(value, sourceField); - } - - return compatibleValue; + return new CompatibleValueConversion(source, target).conversion(value, true); } /** diff --git a/src/main/java/com/rebuild/server/configuration/RebuildApiManager.java b/src/main/java/com/rebuild/server/configuration/RebuildApiManager.java index 1dd4ce2d8..50a8f2c48 100644 --- a/src/main/java/com/rebuild/server/configuration/RebuildApiManager.java +++ b/src/main/java/com/rebuild/server/configuration/RebuildApiManager.java @@ -60,8 +60,8 @@ public ConfigEntry getApp(String appid) { } @Override - public void clean(String cacheKey) { - final String cKey = "RebuildApiManager-" + cacheKey; + public void clean(String appid) { + final String cKey = "RebuildApiManager-" + appid; Application.getCommonCache().evict(cKey); } } diff --git a/src/main/java/com/rebuild/server/configuration/RobotTriggerManager.java b/src/main/java/com/rebuild/server/configuration/RobotTriggerManager.java index a7e817cc6..30e573762 100644 --- a/src/main/java/com/rebuild/server/configuration/RobotTriggerManager.java +++ b/src/main/java/com/rebuild/server/configuration/RobotTriggerManager.java @@ -119,7 +119,7 @@ private boolean allowedWhen(ConfigEntry entry, TriggerWhen... when) { * @param record * @return */ - public boolean isFiltered(JSONObject whenFilter, ID record) { + private boolean isFiltered(JSONObject whenFilter, ID record) { if (whenFilter == null || whenFilter.isEmpty()) { return false; } @@ -195,8 +195,9 @@ public Set getAutoReadonlyFields(String entity) { */ private Map> initAutoReadonlyFields() { Object[][] array = Application.createQueryNoFilter( - "select actionContent from RobotTriggerConfig where actionType = ? and isDisabled = 'F'") + "select actionContent from RobotTriggerConfig where (actionType = ? or actionType = ?) and isDisabled = 'F'") .setParameter(1, ActionType.FIELDAGGREGATION.name()) + .setParameter(2, ActionType.FIELDWRITEBACK.name()) .array(); CaseInsensitiveMap> fieldsMap = new CaseInsensitiveMap<>(); diff --git a/src/main/java/com/rebuild/server/configuration/portals/AdvFilterManager.java b/src/main/java/com/rebuild/server/configuration/portals/AdvFilterManager.java index 5d9c83992..a3d9f0795 100644 --- a/src/main/java/com/rebuild/server/configuration/portals/AdvFilterManager.java +++ b/src/main/java/com/rebuild/server/configuration/portals/AdvFilterManager.java @@ -46,8 +46,8 @@ protected String getConfigEntity() { } @Override - protected String getFieldsForConfig() { - return super.getFieldsForConfig() + ",filterName"; + protected String getConfigFields() { + return super.getConfigFields() + ",filterName"; } /** diff --git a/src/main/java/com/rebuild/server/configuration/portals/BaseLayoutManager.java b/src/main/java/com/rebuild/server/configuration/portals/BaseLayoutManager.java index 52d4ba68a..72ff4f70b 100644 --- a/src/main/java/com/rebuild/server/configuration/portals/BaseLayoutManager.java +++ b/src/main/java/com/rebuild/server/configuration/portals/BaseLayoutManager.java @@ -73,11 +73,21 @@ public ConfigEntry getLayoutOfDatalist(ID user, String entity) { } /** + * @param user + * @return + */ + public ConfigEntry getLayoutOfNav(ID user) { + return getLayout(user, null, TYPE_NAV); + } + + /** + * 列表页 SIDE 图表 + * * @param user * @param entity * @return */ - public ConfigEntry getWidgetOfCharts(ID user, String entity) { + public ConfigEntry getWidgetCharts(ID user, String entity) { ConfigEntry e = getLayout(user, entity, TYPE_WCHARTS); if (e == null) { return null; @@ -90,14 +100,6 @@ public ConfigEntry getWidgetOfCharts(ID user, String entity) { .set("shareTo", null); } - /** - * @param user - * @return - */ - public ConfigEntry getLayoutOfNav(ID user) { - return getLayout(user, null, TYPE_NAV); - } - /** * @param user * @param belongEntity @@ -132,19 +134,18 @@ public ConfigEntry getLayoutById(ID cfgid) { } /** - * @param cached + * @param uses * @param cfgid * @return */ - private ConfigEntry findEntry(Object[][] cached, ID cfgid) { - for (Object[] c : cached) { - if (!c[0].equals(cfgid)) { - continue; + protected ConfigEntry findEntry(Object[][] uses, ID cfgid) { + for (Object[] c : uses) { + if (c[0].equals(cfgid)) { + return new ConfigEntry() + .set("id", c[0]) + .set("shareTo", c[1]) + .set("config", JSON.parse((String) c[3])); } - return new ConfigEntry() - .set("id", c[0]) - .set("shareTo", c[1]) - .set("config", JSON.parse((String) c[3])); } return null; } diff --git a/src/main/java/com/rebuild/server/configuration/portals/DashboardManager.java b/src/main/java/com/rebuild/server/configuration/portals/DashboardManager.java index 80cda0b68..88c107d50 100644 --- a/src/main/java/com/rebuild/server/configuration/portals/DashboardManager.java +++ b/src/main/java/com/rebuild/server/configuration/portals/DashboardManager.java @@ -28,6 +28,9 @@ import com.rebuild.server.service.configuration.DashboardConfigService; import com.rebuild.utils.JSONUtils; +import java.util.Arrays; +import java.util.Comparator; + /** * 首页仪表盘 * @@ -45,8 +48,8 @@ protected String getConfigEntity() { } @Override - protected String getFieldsForConfig() { - return super.getFieldsForConfig() + ",title"; + protected String getConfigFields() { + return super.getConfigFields() + ",title"; } /** @@ -75,7 +78,7 @@ public JSON getDashList(ID user) { canUses[i][2] = isSelf(user, (ID) canUses[i][2]); } - sort(canUses, 4); + Arrays.sort(canUses, Comparator.comparing(o -> o[4].toString())); return (JSON) JSON.toJSON(canUses); } diff --git a/src/main/java/com/rebuild/server/configuration/portals/DataListManager.java b/src/main/java/com/rebuild/server/configuration/portals/DataListManager.java index 1aa6d73f9..0dfc7cc47 100644 --- a/src/main/java/com/rebuild/server/configuration/portals/DataListManager.java +++ b/src/main/java/com/rebuild/server/configuration/portals/DataListManager.java @@ -153,4 +153,20 @@ public Map formatField(Field field, Field parent) { new String[] { "field", "label", "type" }, new Object[] { parentField + easyField.getName(), parentLabel + easyField.getLabel(), easyField.getDisplayType(false) }); } + + /** + * 获取可用列显示ID + * + * @param entity + * @param user + * @return + */ + public ID[] getUsesDataListId(String entity, ID user) { + Object[][] uses = getUsesConfig(user, entity, TYPE_DATALIST); + List array = new ArrayList<>(); + for (Object[] c : uses) { + array.add((ID) c[0]); + } + return array.toArray(new ID[0]); + } } diff --git a/src/main/java/com/rebuild/server/configuration/portals/FieldValueWrapper.java b/src/main/java/com/rebuild/server/configuration/portals/FieldValueWrapper.java index fd31c8c0e..e075fb6d7 100644 --- a/src/main/java/com/rebuild/server/configuration/portals/FieldValueWrapper.java +++ b/src/main/java/com/rebuild/server/configuration/portals/FieldValueWrapper.java @@ -86,7 +86,7 @@ public Object wrapFieldValue(Object value, EasyMeta field, boolean unpackMix) { value = wrapFieldValue(value, field); if (unpackMix && value != null) { DisplayType dt = field.getDisplayType(); - if (dt == DisplayType.CLASSIFICATION || dt == DisplayType.REFERENCE) { + if (value instanceof JSON && (dt == DisplayType.CLASSIFICATION || dt == DisplayType.REFERENCE)) { return ((JSONObject) value).getString("text"); } else if (dt == DisplayType.FILE || dt == DisplayType.IMAGE) { return value.toString(); @@ -195,11 +195,14 @@ public String wrapDecimal(Object value, EasyMeta field) { * @see #wrapMixValue(ID, String) */ public JSON wrapReference(Object value, EasyMeta field) { - String text = ((ID) value).getLabel(); + Object text = ((ID) value).getLabelRaw(); if (text == null) { - text = getLabelNotry((ID) value); + text = getLabelNotry((ID) value); + } else { + Field nameField = ((Field) field.getBaseMeta()).getReferenceEntity().getNameField(); + text = instance.wrapFieldValue(text, nameField, true); } - return wrapMixValue((ID) value, text); + return wrapMixValue((ID) value, text == null ? null : text.toString()); } /** @@ -320,9 +323,27 @@ protected Object wrapSpecialField(Object value, EasyMeta field) { * @throws NoRecordFoundException If no record found */ public static String getLabel(ID id, String defaultValue) throws NoRecordFoundException { + if (id == null) { + throw new NoRecordFoundException("[id] must not be null"); + } + Entity entity = MetadataHelper.getEntity(id.getEntityCode()); - Field nameField = MetadataHelper.getNameField(entity); + if (id.getEntityCode() == EntityHelper.ClassificationData) { + String hasValue = ClassificationManager.instance.getFullName(id); + if (hasValue == null) { + throw new NoRecordFoundException("No ClassificationData found by ID : " + id); + } + return hasValue; + } else if (id.getEntityCode() == EntityHelper.PickList) { + String hasValue = PickListManager.instance.getLabel(id); + if (hasValue == null) { + throw new NoRecordFoundException("No PickList found by ID : " + id); + } + return hasValue; + } + + Field nameField = MetadataHelper.getNameField(entity); Object[] nameValue = Application.getQueryFactory().uniqueNoFilter(id, nameField.getName()); if (nameValue == null) { throw new NoRecordFoundException("No record found by ID : " + id); @@ -354,7 +375,7 @@ public static String getLabel(ID id) throws NoRecordFoundException { */ public static String getLabelNotry(ID id) { try { - return FieldValueWrapper.getLabel(id); + return getLabel(id); } catch (MetadataException | NoRecordFoundException ex) { return MISS_REF_PLACE; } diff --git a/src/main/java/com/rebuild/server/configuration/portals/FormsBuilder.java b/src/main/java/com/rebuild/server/configuration/portals/FormsBuilder.java index 461808037..96bf8f65c 100644 --- a/src/main/java/com/rebuild/server/configuration/portals/FormsBuilder.java +++ b/src/main/java/com/rebuild/server/configuration/portals/FormsBuilder.java @@ -114,14 +114,12 @@ public JSON buildView(String entity, ID user, ID record) { * @param viewMode 视图模式 * @return */ - protected JSON buildModel(String entity, ID user, ID record, boolean viewMode) { + private JSON buildModel(String entity, ID user, ID record, boolean viewMode) { Assert.notNull(entity, "[entity] not be null"); Assert.notNull(user, "[user] not be null"); final Entity entityMeta = MetadataHelper.getEntity(entity); - final User currentUser = Application.getUserStore().getUser(user); - final Date now = CalendarUtils.now(); - + // 明细实体 final Entity masterEntity = entityMeta.getMasterEntity(); // 审批流程(状态) @@ -197,39 +195,124 @@ else if (viewMode) { } } - // 自动只读字段 - final Set autoReadonlyByTriggers = RobotTriggerManager.instance.getAutoReadonlyFields(entity); + // 触发器自动只读 + Set roViaTriggers = RobotTriggerManager.instance.getAutoReadonlyFields(entity); + for (Object o : elements) { + JSONObject field = (JSONObject) o; + if (roViaTriggers.contains(field.getString("field"))) { + field.put("readonly", true); + } + } + + buildModelElements(elements, entityMeta, data, user); + + if (elements.isEmpty()) { + return formatModelError("此表单布局尚未配置,请配置后使用"); + } + + // 主/明细实体处理 + if (entityMeta.getMasterEntity() != null) { + model.set("isSlave", true); + } else if (entityMeta.getSlaveEntity() != null) { + model.set("isMaster", true); + model.set("slaveMeta", EasyMeta.getEntityShow(entityMeta.getSlaveEntity())); + } + if (data != null && data.hasValue(EntityHelper.ModifiedOn)) { + model.set("lastModified", data.getDate(EntityHelper.ModifiedOn).getTime()); + } + + if (approvalState != null) { + model.set("hadApproval", approvalState.getState()); + } + + model.set("id", null); // Clean form's ID of config + return model.toJSON(); + } + + /** + * @param error + * @return + */ + private JSONObject formatModelError(String error) { + JSONObject cfg = new JSONObject(); + cfg.put("error", error); + return cfg; + } + + /** + * @param entity + * @param recordId + * @return + * + * @see RobotApprovalManager#hadApproval(Entity, ID) + */ + private ApprovalState getHadApproval(Entity entity, ID recordId) { + Entity masterEntity = entity.getMasterEntity(); + if (masterEntity == null) { + return RobotApprovalManager.instance.hadApproval(entity, recordId); + } + + ID masterRecordId = MASTERID4NEWSLAVE.get(); + if (masterRecordId == null) { + Field stm = MetadataHelper.getSlaveToMasterField(entity); + String sql = String.format("select %s from %s where %s = ?", + Objects.requireNonNull(stm).getName(), entity.getName(), entity.getPrimaryField().getName()); + Object[] o = Application.createQueryNoFilter(sql).setParameter(1, recordId).unique(); + if (o == null) { + return null; + } + masterRecordId = (ID) o[0]; + } + return RobotApprovalManager.instance.hadApproval(masterEntity, masterRecordId); + } + + /** + * 构建表单元素 + * + * @param elements + * @param entity + * @param data + * @param user + */ + public void buildModelElements(JSONArray elements, Entity entity, Record data, ID user) { + final User currentUser = Application.getUserStore().getUser(user); + final Date now = CalendarUtils.now(); + // Check and clean for (Iterator iter = elements.iterator(); iter.hasNext(); ) { JSONObject el = (JSONObject) iter.next(); String fieldName = el.getString("field"); - if (DIVIDER_LINE.equalsIgnoreCase(fieldName)) { - // 分割线表单页暂不支持 - if (!viewMode) { - iter.remove(); - } continue; } // 已删除字段 - if (!MetadataHelper.checkAndWarnField(entityMeta, fieldName)) { + if (!MetadataHelper.checkAndWarnField(entity, fieldName)) { iter.remove(); continue; } - Field fieldMeta = entityMeta.getField(fieldName); + Field fieldMeta = entity.getField(fieldName); EasyMeta easyField = new EasyMeta(fieldMeta); final DisplayType dt = easyField.getDisplayType(); el.put("label", easyField.getLabel()); el.put("type", dt.name()); - el.put("nullable", fieldMeta.isNullable()); - el.put("readonly", false); - boolean triggersReadonly = autoReadonlyByTriggers.contains(fieldName); + // 触发器自动只读 + final boolean roViaTriggers = el.getBooleanValue("readonly"); // 不可更新字段 - if ((data != null && !fieldMeta.isUpdatable()) || triggersReadonly) { + if ((data != null && !fieldMeta.isUpdatable()) || roViaTriggers) { el.put("readonly", true); + } else { + el.put("readonly", false); + } + + // 优先使用指定值 + final Boolean nullable = el.getBoolean("nullable"); + if (nullable != null) { + el.put("nullable", nullable); + } else { + el.put("nullable", fieldMeta.isNullable()); } // 字段扩展配置 FieldExtConfigProps @@ -237,11 +320,11 @@ else if (viewMode) { for (Map.Entry e : fieldExt.entrySet()) { el.put(e.getKey(), e.getValue()); } - + // 不同字段类型的处理 - + int dateLength = -1; - + if (dt == DisplayType.PICKLIST) { JSONArray options = PickListManager.instance.getPickList(fieldMeta); el.put("options", options); @@ -282,36 +365,36 @@ else if (dt == DisplayType.CLASSIFICATION) { else { if (!fieldMeta.isCreatable()) { el.put("readonly", true); - switch (fieldName) { - case EntityHelper.CreatedOn: - case EntityHelper.ModifiedOn: - el.put("value", CalendarUtils.getUTCDateTimeFormat().format(now)); - break; - case EntityHelper.CreatedBy: - case EntityHelper.ModifiedBy: - case EntityHelper.OwningUser: - el.put("value", FieldValueWrapper.wrapMixValue(currentUser.getId(), currentUser.getFullName())); - break; - case EntityHelper.OwningDept: - Department dept = currentUser.getOwningDept(); - Assert.notNull(dept, "Department of user is unset : " + currentUser.getId()); - el.put("value", FieldValueWrapper.wrapMixValue((ID) dept.getIdentity(), dept.getName())); - break; + switch (fieldName) { + case EntityHelper.CreatedOn: + case EntityHelper.ModifiedOn: + el.put("value", CalendarUtils.getUTCDateTimeFormat().format(now)); + break; + case EntityHelper.CreatedBy: + case EntityHelper.ModifiedBy: + case EntityHelper.OwningUser: + el.put("value", FieldValueWrapper.wrapMixValue(currentUser.getId(), currentUser.getFullName())); + break; + case EntityHelper.OwningDept: + Department dept = currentUser.getOwningDept(); + Assert.notNull(dept, "Department of user is unset : " + currentUser.getId()); + el.put("value", FieldValueWrapper.wrapMixValue((ID) dept.getIdentity(), dept.getName())); + break; case EntityHelper.ApprovalId: el.put("value", FieldValueWrapper.wrapMixValue(null,"自动值 (审批流程)")); break; case EntityHelper.ApprovalState: el.put("value", ApprovalState.DRAFT.getState()); break; - } + default: + break; + } } if (dt == DisplayType.SERIES) { el.put("value", "自动值 (自动编号)"); } else if (dt == DisplayType.BOOL) { el.put("value", BoolEditor.FALSE); - } else if (triggersReadonly) { - el.put("value", "自动值 (触发器)"); } else { String defVal = DefaultValueHelper.exprDefaultValueToString(fieldMeta); if (defVal != null) { @@ -321,41 +404,19 @@ else if (dt == DisplayType.CLASSIFICATION) { el.put("value", defVal); } } - } - } - - if (elements.isEmpty()) { - return formatModelError("此表单布局尚未配置,请配置后使用"); - } - - // 主/明细实体处理 - if (entityMeta.getMasterEntity() != null) { - model.set("isSlave", true); - } else if (entityMeta.getSlaveEntity() != null) { - model.set("isMaster", true); - model.set("slaveMeta", EasyMeta.getEntityShow(entityMeta.getSlaveEntity())); - } - - if (data != null && data.hasValue(EntityHelper.ModifiedOn)) { - model.set("lastModified", data.getDate(EntityHelper.ModifiedOn).getTime()); - } - if (approvalState != null) { - model.set("hadApproval", approvalState.getState()); + if (roViaTriggers && el.get("value") == null) { + if (dt == DisplayType.REFERENCE || dt == DisplayType.CLASSIFICATION) { + el.put("value", FieldValueWrapper.wrapMixValue(null,"自动值 (触发器)")); + } else if (dt == DisplayType.TEXT || dt == DisplayType.NTEXT + || dt == DisplayType.EMAIL || dt == DisplayType.URL || dt == DisplayType.PHONE + || dt == DisplayType.NUMBER || dt == DisplayType.DECIMAL + || dt == DisplayType.DATETIME || dt == DisplayType.DATE) { + el.put("value", "自动值 (触发器)"); + } + } + } } - - model.set("id", null); // Clean form's ID of config - return model.toJSON(); - } - - /** - * @param error - * @return - */ - private JSONObject formatModelError(String error) { - JSONObject cfg = new JSONObject(); - cfg.put("error", error); - return cfg; } /** @@ -364,11 +425,11 @@ private JSONObject formatModelError(String error) { * @param elements * @return */ - private Record findRecord(ID id, ID user, JSONArray elements) { + public Record findRecord(ID id, ID user, JSONArray elements) { if (elements.isEmpty()) { return null; } - + Entity entity = MetadataHelper.getEntity(id.getEntityCode()); StringBuilder ajql = new StringBuilder("select "); for (Object element : elements) { @@ -398,30 +459,6 @@ private Record findRecord(ID id, ID user, JSONArray elements) { .append(" = ?"); return Application.getQueryFactory().createQuery(ajql.toString(), user).setParameter(1, id).record(); } - - /** - * @param entity - * @param recordId - * @return - * - * @see RobotApprovalManager#hadApproval(Entity, ID) - */ - private ApprovalState getHadApproval(Entity entity, ID recordId) { - Entity masterEntity = entity.getMasterEntity(); - if (masterEntity == null) { - return RobotApprovalManager.instance.hadApproval(entity, recordId); - } - - ID masterRecordId = MASTERID4NEWSLAVE.get(); - if (masterRecordId == null) { - Field stm = MetadataHelper.getSlaveToMasterField(entity); - String sql = String.format("select %s from %s where %s = ?", - Objects.requireNonNull(stm).getName(), entity.getName(), entity.getPrimaryField().getName()); - Object[] o = Application.createQueryNoFilter(sql).setParameter(1, recordId).unique(); - masterRecordId = (ID) o[0]; - } - return RobotApprovalManager.instance.hadApproval(masterEntity, masterRecordId); - } /** * 封装表单/布局所用的字段值 diff --git a/src/main/java/com/rebuild/server/configuration/portals/NavManager.java b/src/main/java/com/rebuild/server/configuration/portals/NavManager.java index a93090732..56955c081 100644 --- a/src/main/java/com/rebuild/server/configuration/portals/NavManager.java +++ b/src/main/java/com/rebuild/server/configuration/portals/NavManager.java @@ -35,7 +35,9 @@ import org.jsoup.nodes.Element; import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; /** * 导航菜单 @@ -73,6 +75,21 @@ public JSON getNavLayoutById(ID cfgid) { return config == null ? null : config.toJSON(); } + /** + * 获取可用导航ID + * + * @param user + * @return + */ + public ID[] getUsesNavId(ID user) { + Object[][] uses = getUsesConfig(user, null, TYPE_NAV); + List array = new ArrayList<>(); + for (Object[] c : uses) { + array.add((ID) c[0]); + } + return array.toArray(new ID[0]); + } + // ---- /** @@ -182,16 +199,17 @@ public String renderNavItem(JSONObject item, String activeNav) { } StringBuilder navHtml = new StringBuilder() - .append(String.format("
  • %s", + .append(String.format("
  • %s", navName + (subNavs == null ? StringUtils.EMPTY : " parent"), subNavs == null ? navUrl : "###", - isUrlType ? " target='_blank' rel='noopener noreferrer'" : StringUtils.EMPTY, - navIcon, navText)); + isUrlType ? "_blank" : "_self", + navIcon, + navText)); if (subNavs != null) { StringBuilder subHtml = new StringBuilder() - .append("