diff --git a/.eslintrc.js b/.eslintrc.js index d578e9682d..f24aa8fce7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -115,7 +115,9 @@ module.exports = { $isTrue: true, $fieldIsCompatible: true, $unhideDropdown: true, + RecordSelector: true, AnyRecordSelector: true, + ReferenceSearcher: true, AsideTree: true, $useMap: true, BaiduMap: true, @@ -146,5 +148,8 @@ module.exports = { $isImage: true, $dropUpload: true, $tagStyle2: true, + $clearSelection: true, + DlgTransform: true, + $select2OpenTemplateResult: true, }, } diff --git a/@rbv b/@rbv index 20b87fe662..6d1d8ad6b0 160000 --- a/@rbv +++ b/@rbv @@ -1 +1 @@ -Subproject commit 20b87fe66254338b3510bd5260bd95fbc8e80777 +Subproject commit 6d1d8ad6b0d25ce952d1de5668cfe72a7b3bdc9d diff --git a/README.md b/README.md index 81758e2a8a..054fc1d296 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,17 @@ > **福利:加入 REBUILD VIP 用户 QQ 交流群 819865721 1013051587 GET 使用技能** -## V3.8 新特性 +## V3.9 新特性 本次更新为你带来众多功能增强与优化。 -1. [新增] HTML 报表模版 -2. [新增] 多表单布局 -3. [新增] 明细支持 Excel 粘贴录入 -4. [新增] 图片/附件支持摄像头上传模式 -5. [新增] 手机版数据列表可导出报表 -6. [新增] 多个 FrontJS 函数 -7. [新增] 用户支持批量操作 +1. [新增] 导出报表触发器 +2. [新增] 字段更新、字段聚合支持自动新建记录 +3. [新增] 自定义操作(按钮) +4. [新增] 通讯录功能 +5. [新增] 图表支持选择主题颜色、显示单位 +6. [新增] 可查看记录的所有历史审批 +7. [新增] 多个 FrontJS 函数 8. [优化] 20+ 细节/BUG/安全性更新 9. ... diff --git a/pom.xml b/pom.xml index 56e7466586..7f63b35059 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.rebuild rebuild - 3.8.6 + 3.9.0-beta1 rebuild Building your business-systems freely! https://getrebuild.com/ @@ -218,7 +218,6 @@ - org.springframework.boot spring-boot-starter-tomcat diff --git a/src/main/java/com/rebuild/core/Application.java b/src/main/java/com/rebuild/core/Application.java index fc6b70c774..42bd4372aa 100644 --- a/src/main/java/com/rebuild/core/Application.java +++ b/src/main/java/com/rebuild/core/Application.java @@ -74,11 +74,11 @@ public class Application implements ApplicationListener /** * Rebuild Version */ - public static final String VER = "3.8.6"; + public static final String VER = "3.9.0-beta1"; /** * Rebuild Build [MAJOR]{1}[MINOR]{2}[PATCH]{2}[BUILD]{2} */ - public static final int BUILD = 3080611; + public static final int BUILD = 3090001; static { // Driver for DB diff --git a/src/main/java/com/rebuild/core/BootEnvironmentPostProcessor.java b/src/main/java/com/rebuild/core/BootEnvironmentPostProcessor.java index 77f0c9d89e..dcc14d6b51 100644 --- a/src/main/java/com/rebuild/core/BootEnvironmentPostProcessor.java +++ b/src/main/java/com/rebuild/core/BootEnvironmentPostProcessor.java @@ -145,7 +145,7 @@ private void aesDecrypt(Properties ps) { String newValue = AES.decryptQuietly(value); if (newValue == null) { newValue = StringUtils.EMPTY; - log.warn("Decrypting error (Use blank string) : " + name); + log.warn("Decrypting error (Use blank string) : {}", name); } ps.put(name, newValue); } diff --git a/src/main/java/com/rebuild/core/configuration/NavBuilder.java b/src/main/java/com/rebuild/core/configuration/NavBuilder.java index 1e7da16800..7775ad3acb 100644 --- a/src/main/java/com/rebuild/core/configuration/NavBuilder.java +++ b/src/main/java/com/rebuild/core/configuration/NavBuilder.java @@ -25,6 +25,7 @@ import com.rebuild.core.privileges.RoleService; import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.privileges.UserService; +import com.rebuild.core.service.dashboard.DashboardManager; import com.rebuild.core.service.project.ProjectManager; import com.rebuild.core.support.KVStorage; import com.rebuild.core.support.License; @@ -66,10 +67,11 @@ private NavBuilder() { // 默认导航 private static final JSONArray NAVS_DEFAULT = JSONUtils.toJSONObjectArray( NAV_ITEM_PROPS, - new Object[][] { - new Object[] { "chart-donut", "动态", "BUILTIN", NAV_FEEDS }, - new Object[] { "shape", "项目", "BUILTIN", NAV_PROJECT }, - new Object[] { "folder", "文件", "BUILTIN", NAV_FILEMRG } + new Object[][]{ + new Object[]{"chart-donut", "动态", "BUILTIN", NAV_FEEDS}, + new Object[]{"folder", "文件", "BUILTIN", NAV_FILEMRG}, + new Object[]{"account-box-phone", "通讯录", "BUILTIN", NAV_CONTACT}, + new Object[]{"shape", "项目", "BUILTIN", NAV_PROJECT}, }); // 新建项目 @@ -119,7 +121,7 @@ public JSONArray getUserNav(ID user, String useNav) { if (config == null) { JSONArray useDefault = replaceLang(NAVS_DEFAULT); - ((JSONObject) useDefault.get(1)).put("sub", buildAvailableProjects(user)); + ((JSONObject) useDefault.get(3)).put("sub", buildAvailableProjects(user)); return useDefault; } @@ -145,6 +147,8 @@ public JSONArray getUserNav(ID user, String useNav) { iter.remove(); } else if (NAV_PROJECT.equals(nav.getString("value"))) { nav.put("sub", buildAvailableProjects(user)); + } else if (NAV_DASHBOARD.equals(nav.getString("value"))) { + nav.put("sub", buildAvailableDashboards(user)); } } @@ -168,7 +172,8 @@ private boolean isFilterNavItem(JSONObject item, ID user) { if ("ENTITY".equalsIgnoreCase(type)) { if (NAV_PARENT.equals(value)) { return true; - } else if (NAV_FEEDS.equals(value) || NAV_FILEMRG.equals(value) || NAV_PROJECT.equals(value)) { + } else if (NAV_FEEDS.equals(value) || NAV_FILEMRG.equals(value) + || NAV_PROJECT.equals(value) || NAV_CONTACT.equals(value) || NAV_DASHBOARD.equals(value)) { return false; } else if (!MetadataHelper.containsEntity(value)) { log.warn("Unknown entity in nav : {}", value); @@ -256,6 +261,27 @@ private JSONArray buildAvailableProjects(ID user) { return navsOfProjects; } + /** + * 动态获取仪表盘菜单 + * + * @param user + * @return + */ + private JSONArray buildAvailableDashboards(ID user) { + JSONArray dashs = (JSONArray) DashboardManager.instance.getAvailable(user, false); + if (dashs == null || dashs.isEmpty()) return JSONUtils.EMPTY_ARRAY; + + JSONArray itemsOfNav = new JSONArray(); + for (Object d : dashs) { + JSONArray dash = (JSONArray) d; + JSONObject item = JSONUtils.toJSONObject( + NAV_ITEM_PROPS, + new Object[]{"--", dash.getString(4), NAV_DASHBOARD, dash.getString(0)}); + itemsOfNav.add(item); + } + return itemsOfNav; + } + /** * 首次安装添加菜单 * @@ -382,15 +408,19 @@ static String renderNavItem(JSONObject item, String activeNav) { } } else if (NAV_FEEDS.equals(navName)) { - navName = "nav_entity-FEEDS"; + navName = "nav_entity--FEEDS"; navUrl = AppUtils.getContextPath("/feeds/home"); } else if (NAV_FILEMRG.equals(navName)) { - navName = "nav_entity-ATTACHMENT"; + navName = "nav_entity--ATTACHMENT"; navUrl = AppUtils.getContextPath("/files/home"); + } else if (NAV_CONTACT.equals(navName)) { + navName = "nav_entity--CONTACTS"; + navUrl = AppUtils.getContextPath("/contacts/home"); + } else if (NAV_PROJECT.equals(navName)) { - navName = "nav_entity-PROJECT"; + navName = "nav_entity--PROJECT"; navUrl = AppUtils.getContextPath("/project/search"); } else if (NAV_PROJECT.equals(navType)) { @@ -398,9 +428,17 @@ static String renderNavItem(JSONObject item, String activeNav) { navUrl = String.format("%s/project/%s/tasks", AppUtils.getContextPath(), navUrl); } else if (navName.startsWith(NAV_PROJECT)) { - navName = "nav_project--add"; + navName = "nav_project-add"; navUrl = AppUtils.getContextPath("/admin/projects"); + } else if (NAV_DASHBOARD.equals(navType)) { + navName = "nav_dashboard-" + navName; + navUrl = String.format("%s/dashboard/home?d=%s", AppUtils.getContextPath(), navUrl); + + } else if (NAV_DASHBOARD.equals(navName)) { + navName = "nav_dashboard-DASHBOARD"; + navUrl = String.format("%s/dashboard/home", AppUtils.getContextPath()); + } else { navEntity = navName; navName = "nav_entity-" + navName; @@ -417,9 +455,6 @@ static String renderNavItem(JSONObject item, String activeNav) { JSONArray subNavs = null; if (activeNav != null) { subNavs = item.getJSONArray("sub"); - if (subNavs == null || subNavs.isEmpty()) { - subNavs = null; - } } String navItemHtml; @@ -429,14 +464,25 @@ static String renderNavItem(JSONObject item, String activeNav) { String parentClass = " parent"; if (item.getBooleanValue("open")) parentClass += " open"; - navItemHtml = String.format( - "
  • %s", - navName + (subNavs == null ? StringUtils.EMPTY : parentClass), - navEntity == null ? StringUtils.EMPTY : navEntity, - subNavs == null ? navUrl : "###", - isOutUrl ? "_blank" : "_self", - iconClazz, - navText); + // v3.9 No icon + if ("zmdi zmdi---".equals(iconClazz)) { + navItemHtml = String.format( + "
  • %s", + navName + (subNavs == null ? StringUtils.EMPTY : parentClass), + navEntity == null ? StringUtils.EMPTY : navEntity, + subNavs == null ? navUrl : "###", + isOutUrl ? "_blank" : "_self", + navText); + } else { + navItemHtml = String.format( + "
  • %s", + navName + (subNavs == null ? StringUtils.EMPTY : parentClass), + navEntity == null ? StringUtils.EMPTY : navEntity, + subNavs == null ? navUrl : "###", + isOutUrl ? "_blank" : "_self", + iconClazz, + navText); + } } StringBuilder navHtml = new StringBuilder(navItemHtml); @@ -451,6 +497,7 @@ static String renderNavItem(JSONObject item, String activeNav) { JSONObject subNav = (JSONObject) o; subHtml.append(renderNavItem(subNav, null)); } + if (subNavs.isEmpty()) subHtml.append(String.format("
  • %s
  • ", Language.L("暂无可用"))); subHtml.append(""); navHtml.append(subHtml); diff --git a/src/main/java/com/rebuild/core/configuration/NavManager.java b/src/main/java/com/rebuild/core/configuration/NavManager.java index 0fb9518bc5..77401484c2 100644 --- a/src/main/java/com/rebuild/core/configuration/NavManager.java +++ b/src/main/java/com/rebuild/core/configuration/NavManager.java @@ -30,6 +30,10 @@ public class NavManager extends BaseLayoutManager { public static final String NAV_FEEDS = "$FEEDS$"; // 项目 public static final String NAV_PROJECT = "$PROJECT$"; + // 通讯录 + public static final String NAV_CONTACT = "$CONTACT$"; + // 仪表盘 + public static final String NAV_DASHBOARD = "$DASHBOARD$"; // 分栏 public static final String NAV_DIVIDER = "$DIVIDER$"; diff --git a/src/main/java/com/rebuild/core/configuration/general/DataListManager.java b/src/main/java/com/rebuild/core/configuration/general/DataListManager.java index 4a863c42f4..78f380d048 100644 --- a/src/main/java/com/rebuild/core/configuration/general/DataListManager.java +++ b/src/main/java/com/rebuild/core/configuration/general/DataListManager.java @@ -65,7 +65,7 @@ public JSON getListFields(String entity, ID user) { /** * @param entity * @param user - * @param useSysFlag + * @param useSysFlag 优先使用系统指定的 `SYS XXX` or ID * @return */ public JSON getListFields(String entity, ID user, String useSysFlag) { diff --git a/src/main/java/com/rebuild/core/configuration/general/EasyActionManager.java b/src/main/java/com/rebuild/core/configuration/general/EasyActionManager.java index 5938b24a51..5f996e237a 100644 --- a/src/main/java/com/rebuild/core/configuration/general/EasyActionManager.java +++ b/src/main/java/com/rebuild/core/configuration/general/EasyActionManager.java @@ -11,6 +11,7 @@ import com.rebuild.core.configuration.ConfigBean; import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.privileges.UserService; +import com.rebuild.utils.JSONUtils; /** * 自定义操作按钮 @@ -22,8 +23,11 @@ public class EasyActionManager extends BaseLayoutManager { public static final EasyActionManager instance = new EasyActionManager(); - private EasyActionManager() { - } + private static final String TYPE_DATALIST = "datalist"; + private static final String TYPE_DATAROW = "datarow"; + private static final String TYPE_VIEW = "view"; + + private EasyActionManager() {} /** * @param entity @@ -34,45 +38,50 @@ public JSON getEasyAction(String entity, ID user) { ConfigBean cb = getLayout(UserService.SYSTEM_USER, entity, TYPE_EASYACTION, null); if (cb == null) return null; - JSONArray items; - try { - items = (JSONArray) cb.getJSON("config"); - } catch (Exception ignored) { - return null; - } - if (items == null || items.isEmpty()) return null; - - JSONArray items4User = new JSONArray(); - for (Object item : items) { - JSONObject itemObj = (JSONObject) item; - - JSONArray itemsL2 = itemObj.getJSONArray("items"); - boolean hasChild = itemsL2 != null && !itemsL2.isEmpty(); - if (hasChild) { - JSONArray items4UserL2 = new JSONArray(); - for (Object itemL2 : itemsL2) { - JSONObject itemL2Obj = (JSONObject) itemL2; - String shareTo = itemL2Obj.getString("shareTo"); - if (UserHelper.isAdmin(user) || isShareTo(shareTo, user)) { - items4UserL2.add(itemL2Obj); + Object config = cb.getJSON("config"); + JSONObject configJson; + if (config instanceof JSONArray) configJson = JSONUtils.toJSONObject(TYPE_DATALIST, config); + else configJson = config == null ? null : (JSONObject) config; + + if (configJson == null || configJson.isEmpty()) return null; + + JSONObject action4User = new JSONObject(); + for (String type : configJson.keySet()) { + final JSONArray items = (JSONArray) configJson.get(type); + + JSONArray items4User = new JSONArray(); + for (Object item : items) { + JSONObject itemObj = (JSONObject) item; + + JSONArray itemsL2 = itemObj.getJSONArray("items"); + boolean hasChild = itemsL2 != null && !itemsL2.isEmpty(); + if (hasChild) { + JSONArray items4UserL2 = new JSONArray(); + for (Object itemL2 : itemsL2) { + JSONObject itemL2Obj = (JSONObject) itemL2; + String shareTo = itemL2Obj.getString("shareTo"); + if (UserHelper.isAdmin(user) || isShareTo(shareTo, user)) { + items4UserL2.add(itemL2Obj); + } } - } - // 是否可见由子元素确定 - if (!items4UserL2.isEmpty()) { - itemObj.put("items", items4UserL2); - items4User.add(itemObj); - } + // 是否可见由子元素确定 + if (!items4UserL2.isEmpty()) { + itemObj.put("items", items4UserL2); + items4User.add(itemObj); + } - } else { - String shareTo = itemObj.getString("shareTo"); - if (UserHelper.isAdmin(user) || isShareTo(shareTo, user)) { - items4User.add(itemObj); + } else { + String shareTo = itemObj.getString("shareTo"); + if (UserHelper.isAdmin(user) || isShareTo(shareTo, user)) { + items4User.add(itemObj); + } } } - } - return items4User; + if (!items4User.isEmpty()) action4User.put(type, items4User); + } + return action4User; } /** diff --git a/src/main/java/com/rebuild/core/configuration/general/FormsBuilder.java b/src/main/java/com/rebuild/core/configuration/general/FormsBuilder.java index 591a27ea5d..5cf7bd43d7 100644 --- a/src/main/java/com/rebuild/core/configuration/general/FormsBuilder.java +++ b/src/main/java/com/rebuild/core/configuration/general/FormsBuilder.java @@ -19,6 +19,7 @@ import com.rebuild.core.Application; import com.rebuild.core.configuration.ConfigBean; import com.rebuild.core.metadata.EntityHelper; +import com.rebuild.core.metadata.EntityRecordCreator; import com.rebuild.core.metadata.MetadataHelper; import com.rebuild.core.metadata.MetadataSorter; import com.rebuild.core.metadata.easymeta.DisplayType; @@ -29,12 +30,15 @@ import com.rebuild.core.metadata.impl.EasyFieldConfigProps; import com.rebuild.core.privileges.FieldPrivileges; import com.rebuild.core.privileges.UserFilters; +import com.rebuild.core.privileges.UserService; import com.rebuild.core.privileges.bizz.Department; import com.rebuild.core.privileges.bizz.User; import com.rebuild.core.service.NoRecordFoundException; import com.rebuild.core.service.approval.ApprovalState; import com.rebuild.core.service.approval.RobotApprovalManager; +import com.rebuild.core.service.general.GeneralEntityService; import com.rebuild.core.service.query.QueryHelper; +import com.rebuild.core.support.general.DataListWrapper; import com.rebuild.core.support.general.FieldValueHelper; import com.rebuild.core.support.i18n.Language; import com.rebuild.core.support.state.StateManager; @@ -76,6 +80,7 @@ protected FormsBuilder() { // 引用主记录 public static final String DV_MAINID = "$MAINID$"; + public static final String DV_MAINID_FJS = "$MAINID$FJS"; // 引用记录 public static final String DV_REFERENCE_PREFIX = "&"; @@ -438,7 +443,6 @@ protected void buildModelElements(JSONArray elements, Entity entity, Record reco final DisplayType dt = easyField.getDisplayType(); el.put("label", easyField.getLabel()); el.put("type", dt.name()); - el.put("readonly", (!isNew && !fieldMeta.isUpdatable()) || roViaAuto); // 优先使用指定值 @@ -657,7 +661,7 @@ protected Record findRecord(ID id, ID user, JSONArray elements) { * @param user4Desensitized 不传则不脱敏 * @return * @see FieldValueHelper#wrapFieldValue(Object, EasyField) - * @see com.rebuild.core.support.general.DataListWrapper#wrapFieldValue(Object, Field) + * @see DataListWrapper#wrapFieldValue(Object, Field) */ public Object wrapFieldValue(Record data, EasyField field, ID user4Desensitized) { final DisplayType dt = field.getDisplayType(); @@ -717,6 +721,13 @@ public void setFormInitialValue(Entity entity, JSON formModel, JSONObject initia JSONArray elements = ((JSONObject) formModel).getJSONArray("elements"); if (elements == null || elements.isEmpty()) return; + // v3.9 可携带明细 + Object hasDetails = initialVal.remove(GeneralEntityService.HAS_DETAILS); + if (hasDetails instanceof JSONArray && !((JSONArray) hasDetails).isEmpty()) { + JSONObject ds = setFormInitialValue4Details39(entity, (JSONArray) hasDetails); + if (!ds.isEmpty()) ((JSONObject) formModel).put(GeneralEntityService.HAS_DETAILS, ds); + } + // 已布局字段。字段是否布局会影响返回值 Set inFormFields = new HashSet<>(); for (Object o : elements) { @@ -724,12 +735,18 @@ public void setFormInitialValue(Entity entity, JSON formModel, JSONObject initia } // 保持在初始值中 - // TODO 更多保持字段 Set initialValKeeps = new HashSet<>(); Map initialValReady = new HashMap<>(); for (Map.Entry e : initialVal.entrySet()) { final String field = e.getKey(); + Object v = e.getValue(); + if (!(v instanceof String)) { + if (!(EntityRecordCreator.META_FIELD.equals(field))) { + log.warn("Invalid value in `initialVal` : {}", e); + } + continue; + } final String value = (String) e.getValue(); if (StringUtils.isBlank(value)) continue; @@ -752,7 +769,17 @@ else if (field.equals(DV_MAINID)) { ? getReferenceMixValue(value) : (isNewMainId(value) ? EntityHelper.UNSAVED_ID : value); - if (mixValue != null) { + // v3.9 明细直接新建 + if (DV_MAINID_FJS.equals(value)) { + for (Object o : elements) { + JSONObject item = (JSONObject) o; + if (dtmField.getName().equalsIgnoreCase(item.getString("field"))) { + item.remove("readonly"); + item.remove("readonlyw"); + break; + } + } + } else if (mixValue != null) { initialValReady.put(dtmField.getName(), mixValue); initialValKeeps.add(dtmField.getName()); } @@ -809,6 +836,44 @@ else if (entity.containsField(field)) { } } + // 支持明细 + private JSONObject setFormInitialValue4Details39(Entity entity, JSONArray details) { + final Entity defDetailEntity = entity.getDetailEntity(); + Assert.notNull(defDetailEntity, "None detail-entity"); + + ID forceMainid = EntityHelper.UNSAVED_ID; + FormsBuilderContextHolder.setMainIdOfDetail(forceMainid); + JSONObject dsMap = new JSONObject(); + try { + for (Object o : details) { + JSONObject item = (JSONObject) o; + JSONObject metadata = item.getJSONObject(EntityRecordCreator.META_FIELD); + Entity detailEntity = null; + if (metadata != null) { + String n = metadata.getString("entity"); + if (n != null) detailEntity = MetadataHelper.getEntity(n); + } + if (detailEntity == null) detailEntity = defDetailEntity; + + // 新建明细记录时必须指定主实体 + item.put(DV_MAINID, forceMainid.toString()); + JSON model = buildForm(detailEntity.getName(), UserService.SYSTEM_USER, null); + setFormInitialValue(detailEntity, model, item); + + JSONArray ds = dsMap.getJSONArray(detailEntity.getName()); + if (ds == null) { + ds = new JSONArray(); + dsMap.put(detailEntity.getName(), ds); + } + ds.add(model); + } + + } finally { + FormsBuilderContextHolder.getMainIdOfDetail(true); + } + return dsMap; + } + /** * 引用字段值 * diff --git a/src/main/java/com/rebuild/core/configuration/general/FormsManager.java b/src/main/java/com/rebuild/core/configuration/general/FormsManager.java index 03a948c417..1a3af5c1c5 100644 --- a/src/main/java/com/rebuild/core/configuration/general/FormsManager.java +++ b/src/main/java/com/rebuild/core/configuration/general/FormsManager.java @@ -54,7 +54,7 @@ public ConfigBean getNewFormLayout(String entity) { * @return */ public ConfigBean getFormLayout(String entity, ID recordOrLayoutId, int applyType) { - final Object[][] alls = getAllConfig(entity, TYPE_FORM); + final Object[][] allConfs = getAllConfig(entity, TYPE_FORM); // TODO `applyType` 暂未用 @@ -62,7 +62,7 @@ public ConfigBean getFormLayout(String entity, ID recordOrLayoutId, int applyTyp // 1.指定布局 if (recordOrLayoutId != null && recordOrLayoutId.getEntityCode() == EntityHelper.LayoutConfig) { - use = findConfigBean(alls, recordOrLayoutId); + use = findConfigBean(allConfs, recordOrLayoutId); if (use == null) { log.warn("Spec layout not longer exists : {}", recordOrLayoutId); recordOrLayoutId = null; @@ -72,8 +72,8 @@ public ConfigBean getFormLayout(String entity, ID recordOrLayoutId, int applyTyp // 2.查找布局 if (use == null) { // 优先使用条件匹配的 - for (Object[] o : alls) { - ConfigBean cb = findConfigBean(alls, (ID) o[0]); + for (Object[] o : allConfs) { + ConfigBean cb = findConfigBean(allConfs, (ID) o[0]); ShareToAttr attr = new ShareToAttr(cb); if (recordOrLayoutId == null) { if (attr.isFallback() || attr.isForNew()) { @@ -90,14 +90,14 @@ public ConfigBean getFormLayout(String entity, ID recordOrLayoutId, int applyTyp // 默认优先级 if (recordOrLayoutId == null) { - use = findDefault(alls); + use = findDefault(allConfs); } } // 3.默认布局 if (use == null && recordOrLayoutId != null) { - for (Object[] o : alls) { - ConfigBean cb = findConfigBean(alls, (ID) o[0]); + for (Object[] o : allConfs) { + ConfigBean cb = findConfigBean(allConfs, (ID) o[0]); ShareToAttr attr = new ShareToAttr(cb); if (attr.isFallback()) { use = cb; @@ -217,7 +217,12 @@ protected ConfigBean findConfigBean(Object[][] uses, ID cfgid) { // attrs String shareTo = cb.getString("shareTo"); if (JSONUtils.wellFormat(shareTo)) { - cb.set("shareTo", JSON.parse(shareTo)); + JSONObject shareTo2 = (JSONObject) JSON.parse(shareTo); + cb.set("shareTo", shareTo2); + // v3.9 + if (shareTo2.getBooleanValue("extrasAction")) { + cb.set("extrasAction", true); + } } return cb; } diff --git a/src/main/java/com/rebuild/core/configuration/general/ShareToManager.java b/src/main/java/com/rebuild/core/configuration/general/ShareToManager.java index 7eaa6eeb05..263bfa1bf1 100644 --- a/src/main/java/com/rebuild/core/configuration/general/ShareToManager.java +++ b/src/main/java/com/rebuild/core/configuration/general/ShareToManager.java @@ -90,6 +90,13 @@ protected ID detectUseConfig(ID user, String belongEntity, String applyType, boo final Object[][] alls = getAllConfig(belongEntity, applyType); if (alls.length == 0) return null; + // 0. v3.9 指定配置 + if (ID.isId(useSysFlag)) { + for (Object[] d : alls) { + if (d[0].toString().equals(useSysFlag)) return (ID) d[0]; + } + } + // 1.优先使用自己的 if (firstUseSelf) { for (Object[] d : alls) { @@ -207,7 +214,7 @@ protected boolean isShareTo(String shareTo, ID user) { * @return */ final protected String formatCacheKey(String belongEntity, String applyType) { - return String.format("%s-%s-%s33-1", getConfigEntity(), + return String.format("%s-%s-%s39", getConfigEntity(), StringUtils.defaultIfBlank(belongEntity, "N"), StringUtils.defaultIfBlank(applyType, "N")).toUpperCase(); } diff --git a/src/main/java/com/rebuild/core/configuration/general/TransformManager.java b/src/main/java/com/rebuild/core/configuration/general/TransformManager.java index 9e8769333e..c1faf45422 100644 --- a/src/main/java/com/rebuild/core/configuration/general/TransformManager.java +++ b/src/main/java/com/rebuild/core/configuration/general/TransformManager.java @@ -79,7 +79,9 @@ public JSONArray getTransforms(String sourceEntity, ID user) { JSONObject item = EasyMetaFactory.toJSON(targetEntity); item.put("transid", cb.getID("id")); item.put("transName", cb.getString("name")); - item.put("previewMode", config.getIntValue("transformMode") == 2); + // 有主实体 + String mainEntity = item.getString("mainEntity"); + if (mainEntity != null) item.put("mainEntityLabel", EasyMetaFactory.getLabel(mainEntity)); data.add(item); } return data; diff --git a/src/main/java/com/rebuild/core/metadata/DeleteRecord.java b/src/main/java/com/rebuild/core/metadata/DeleteRecord.java index e49ea2609e..74d9c9ad58 100644 --- a/src/main/java/com/rebuild/core/metadata/DeleteRecord.java +++ b/src/main/java/com/rebuild/core/metadata/DeleteRecord.java @@ -9,6 +9,7 @@ import cn.devezhao.persist4j.engine.ID; import cn.devezhao.persist4j.engine.StandardRecord; +import lombok.Getter; /** * 删除专用 @@ -19,8 +20,16 @@ public class DeleteRecord extends StandardRecord { private static final long serialVersionUID = -7098132591224439549L; + @Getter + private boolean quietly; + public DeleteRecord(ID primaryid, ID editor) { + this(primaryid, editor, false); + } + + public DeleteRecord(ID primaryid, ID editor, boolean quietly) { super(MetadataHelper.getEntity(primaryid.getEntityCode()), editor); this.setID(getEntity().getPrimaryField().getName(), primaryid); + this.quietly = quietly; } } diff --git a/src/main/java/com/rebuild/core/metadata/EntityHelper.java b/src/main/java/com/rebuild/core/metadata/EntityHelper.java index 46de155ea4..61b6977eff 100644 --- a/src/main/java/com/rebuild/core/metadata/EntityHelper.java +++ b/src/main/java/com/rebuild/core/metadata/EntityHelper.java @@ -95,6 +95,10 @@ public static Record parse(JSONObject data, ID user, boolean safetyUrl, boolean String id = metadata.getString("id"); return new DeleteRecord(ID.valueOf(id), user); } + if (metadata.getBooleanValue("deleteQuietly")) { + String id = metadata.getString("id"); + return new DeleteRecord(ID.valueOf(id), user, true); + } Record record = new EntityRecordCreator(MetadataHelper.getEntity(entityName), data, user, safetyUrl) .create(false); diff --git a/src/main/java/com/rebuild/core/metadata/MetadataHelper.java b/src/main/java/com/rebuild/core/metadata/MetadataHelper.java index 43d50a3a18..71243fd2e9 100644 --- a/src/main/java/com/rebuild/core/metadata/MetadataHelper.java +++ b/src/main/java/com/rebuild/core/metadata/MetadataHelper.java @@ -301,6 +301,16 @@ public static boolean isBusinessEntity(Entity entity) { return hasPrivilegesField(entity) || EasyMetaFactory.valueOf(entity).isPlainEntity(); } + /** + * 是否明细实体 + * + * @param entityCode + * @return + */ + public static boolean isDetailEntity(int entityCode) { + return getEntity(entityCode).getMainEntity() != null; + } + /** * 实体是否具备权限字段(业务实体) * diff --git a/src/main/java/com/rebuild/core/metadata/easymeta/EasyMetaFactory.java b/src/main/java/com/rebuild/core/metadata/easymeta/EasyMetaFactory.java index ea53282c09..bd81b1cb1a 100644 --- a/src/main/java/com/rebuild/core/metadata/easymeta/EasyMetaFactory.java +++ b/src/main/java/com/rebuild/core/metadata/easymeta/EasyMetaFactory.java @@ -15,16 +15,19 @@ import cn.devezhao.persist4j.metadata.MetadataException; import cn.devezhao.persist4j.metadata.MissingMetaExcetion; import com.alibaba.fastjson.JSONObject; -import com.esotericsoftware.minlog.Log; import com.rebuild.core.RebuildException; import com.rebuild.core.configuration.general.AutoFillinManager; import com.rebuild.core.metadata.EntityHelper; import com.rebuild.core.metadata.MetadataHelper; import com.rebuild.core.service.trigger.RobotTriggerManager; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; import org.springframework.util.ReflectionUtils; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import static com.rebuild.core.metadata.easymeta.DisplayType.ANYREFERENCE; @@ -45,6 +48,7 @@ * @author devezhao * @since 2020/11/17 */ +@Slf4j public class EasyMetaFactory { /** @@ -161,7 +165,7 @@ public static String getLabel(BaseMeta entityOrField) { } /** - * 获取字段 Label(支持两级字段,如 owningUser.fullName) + * 获取字段 Label(如 owningUser.fullName) * * @param entity * @param fieldPath @@ -171,15 +175,20 @@ public static String getLabel(Entity entity, String fieldPath) { try { String[] fieldPathSplit = fieldPath.split("\\."); Field firstField = entity.getField(fieldPathSplit[0]); - if (fieldPathSplit.length == 1) { - return getLabel(firstField); + if (fieldPathSplit.length == 1) return getLabel(firstField); + + List labels = new ArrayList<>(); + Entity prevEntity = entity; + for (String s : fieldPathSplit) { + Field field = prevEntity.getField(s); + labels.add(getLabel(field)); + // ref + if (field.getType() == FieldType.REFERENCE) prevEntity = field.getReferenceEntity(); } + return StringUtils.join(labels, "."); - Entity refEntity = firstField.getReferenceEntity(); - Field secondField = refEntity.getField(fieldPathSplit[1]); - return String.format("%s.%s", getLabel(firstField), getLabel(secondField)); } catch (MissingMetaExcetion ex) { - Log.error(ex.getLocalizedMessage()); + log.error("{}:{}", fieldPath, ex.getLocalizedMessage()); return String.format("[%s]", fieldPath.toUpperCase()); } } diff --git a/src/main/java/com/rebuild/core/metadata/impl/CopyEntity.java b/src/main/java/com/rebuild/core/metadata/impl/CopyEntity.java index 12399eb4f9..c66b78503f 100644 --- a/src/main/java/com/rebuild/core/metadata/impl/CopyEntity.java +++ b/src/main/java/com/rebuild/core/metadata/impl/CopyEntity.java @@ -12,10 +12,8 @@ import com.alibaba.fastjson.JSONObject; import com.rebuild.core.RebuildException; import com.rebuild.core.metadata.MetadataHelper; -import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.rbstore.MetaschemaExporter; import com.rebuild.core.rbstore.MetaschemaImporter; -import com.rebuild.core.support.i18n.Language; import com.rebuild.core.support.task.TaskExecutors; import com.rebuild.utils.CommonsUtils; import com.rebuild.utils.RbAssert; @@ -44,7 +42,7 @@ public CopyEntity(Entity sourceEntity) { */ public String copy(String entityName, String detailName) { final ID user = getUser(); - RbAssert.isAllow(UserHelper.isSuperAdmin(user), Language.L("仅超级管理员可操作")); + RbAssert.isSuperAdmin(user); // 导出 diff --git a/src/main/java/com/rebuild/core/metadata/impl/EasyFieldConfigProps.java b/src/main/java/com/rebuild/core/metadata/impl/EasyFieldConfigProps.java index c07ff5a628..fdf53193d5 100644 --- a/src/main/java/com/rebuild/core/metadata/impl/EasyFieldConfigProps.java +++ b/src/main/java/com/rebuild/core/metadata/impl/EasyFieldConfigProps.java @@ -199,4 +199,8 @@ public class EasyFieldConfigProps { */ public static final String TAG_MAXSELECT = "tagMaxSelect"; + /** + * PICKLIST:显示样式 + */ + public static final String PICKLIST_SHOWSTYLE = "showStyle"; } diff --git a/src/main/java/com/rebuild/core/metadata/impl/ExcelEntity.java b/src/main/java/com/rebuild/core/metadata/impl/ExcelEntity.java index 1138f15e45..e8193f23c7 100644 --- a/src/main/java/com/rebuild/core/metadata/impl/ExcelEntity.java +++ b/src/main/java/com/rebuild/core/metadata/impl/ExcelEntity.java @@ -6,7 +6,6 @@ import com.alibaba.fastjson.JSONArray; import com.rebuild.core.metadata.MetadataHelper; import com.rebuild.core.metadata.easymeta.DisplayType; -import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.support.i18n.Language; import com.rebuild.utils.JSONUtils; import com.rebuild.utils.RbAssert; @@ -30,7 +29,7 @@ public class ExcelEntity extends Entity2Schema { */ public String imports(String entityName, JSONArray fields) { final ID user = getUser(); - RbAssert.isAllow(UserHelper.isSuperAdmin(user), Language.L("仅超级管理员可操作")); + RbAssert.isSuperAdmin(user); // 1.实体 String uniqueEntityName = createEntity(null, entityName, null, null, false, false); 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 69a9831cd2..bc1ac49ddb 100644 --- a/src/main/java/com/rebuild/core/metadata/impl/Field2Schema.java +++ b/src/main/java/com/rebuild/core/metadata/impl/Field2Schema.java @@ -29,7 +29,6 @@ import com.rebuild.core.metadata.easymeta.DisplayType; import com.rebuild.core.metadata.easymeta.EasyField; import com.rebuild.core.metadata.easymeta.EasyMetaFactory; -import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.support.SetUser; import com.rebuild.core.support.i18n.Language; import com.rebuild.core.support.setup.Installer; @@ -81,7 +80,7 @@ public Field2Schema(ID user) { @Override public ID getUser() { ID user = super.getUser(); - RbAssert.isAllow(UserHelper.isSuperAdmin(user), Language.L("仅超级管理员可操作")); + RbAssert.isSuperAdmin(user); return user; } diff --git a/src/main/java/com/rebuild/core/privileges/UserHelper.java b/src/main/java/com/rebuild/core/privileges/UserHelper.java index cbb4c924b7..57a040bb4e 100644 --- a/src/main/java/com/rebuild/core/privileges/UserHelper.java +++ b/src/main/java/com/rebuild/core/privileges/UserHelper.java @@ -116,7 +116,7 @@ public static Department getDepartment(ID userId) { User u = Application.getUserStore().getUser(userId); return u.getOwningDept(); } catch (NoMemberFoundException ex) { - log.error("No User found : " + userId); + log.error("No User found : {}", userId); } return null; } @@ -380,8 +380,8 @@ public static Member[] sortMembers(Member[] members) { return new Member[0]; } - if (members[0] instanceof User) { - Arrays.sort(members, Comparator.comparing(o -> ((User) o).getFullName())); + if (members[0] instanceof Comparable) { + Arrays.sort(members); } else { Arrays.sort(members, Comparator.comparing(Member::getName)); } diff --git a/src/main/java/com/rebuild/core/privileges/UserService.java b/src/main/java/com/rebuild/core/privileges/UserService.java index 6003266a2f..858c884b15 100644 --- a/src/main/java/com/rebuild/core/privileges/UserService.java +++ b/src/main/java/com/rebuild/core/privileges/UserService.java @@ -81,8 +81,8 @@ public Record create(Record record) { * @return */ private Record create(Record record, boolean notifyUser) { - if (!License.isRbvAttached() && Application.getUserStore().getAllUsers().length >= 100) { - throw new NeedRbvException(Language.L("用户数量超出免费版限制")); + if (Application.getUserStore().getAllUsers().length >= 50) { + if (!License.isRbvAttached()) throw new NeedRbvException(Language.L("用户数量超出免费版限制")); } checkAdminGuard(BizzPermission.CREATE, null); @@ -333,6 +333,7 @@ public void updateEnableUser(ID user, ID deptNew, ID roleNew, ID[] roleAppends, // Kill session if (enableNew != null && !enableNew) { Application.getSessionStore().killSession(user); + log.warn("FORCE DESTROY USER SESSIONS : {}", enUser.getId()); } } diff --git a/src/main/java/com/rebuild/core/privileges/UserStore.java b/src/main/java/com/rebuild/core/privileges/UserStore.java index bfe22ef60f..278fc4515a 100644 --- a/src/main/java/com/rebuild/core/privileges/UserStore.java +++ b/src/main/java/com/rebuild/core/privileges/UserStore.java @@ -255,7 +255,7 @@ public void refreshUser(ID userId) { .setParameter(1, userId) .unique(); final User newUser = new User( - userId, (String) o[1], (String) o[2], (String) o[8], (String) o[3], (String) o[4], (Boolean) o[5]); + userId, (String) o[1], (String) o[2], (String) o[8], (String) o[3], (String) o[4], (Boolean) o[5], (Integer) o[9]); final ID deptId = (ID) o[6]; final ID roleId = (ID) o[7]; @@ -398,10 +398,10 @@ public void refreshDepartment(ID deptId) { } } - Object[] o = aPMFactory.createQuery("select name,isDisabled,parentDept from Department where deptId = ?") + Object[] o = aPMFactory.createQuery("select name,isDisabled,parentDept,seq from Department where deptId = ?") .setParameter(1, deptId) .unique(); - final Department newDept = new Department(deptId, (String) o[0], (Boolean) o[1]); + final Department newDept = new Department(deptId, (String) o[0], (Boolean) o[1], (Integer) o[3]); // Members Object[][] array = aPMFactory.createQuery("select userId from User where deptId = ?") @@ -549,7 +549,7 @@ private void refreshRoleAppends(ID roleId) { } } - private static final String USER_FS = "userId,loginName,email,fullName,avatarUrl,isDisabled,deptId,roleId,workphone"; + private static final String USER_FS = "userId,loginName,email,fullName,avatarUrl,isDisabled,deptId,roleId,workphone,seq"; @Override public void init() { @@ -559,7 +559,7 @@ public void init() { for (Object[] o : array) { ID userId = (ID) o[0]; User user = new User( - userId, (String) o[1], (String) o[2], (String) o[8], (String) o[3], (String) o[4], (Boolean) o[5]); + userId, (String) o[1], (String) o[2], (String) o[8], (String) o[3], (String) o[4], (Boolean) o[5], (Integer) o[9]); store(user); } log.info("Loaded [ " + USERS.size() + " ] users."); diff --git a/src/main/java/com/rebuild/core/privileges/bizz/Department.java b/src/main/java/com/rebuild/core/privileges/bizz/Department.java index 1e275ede8e..7828cad580 100644 --- a/src/main/java/com/rebuild/core/privileges/bizz/Department.java +++ b/src/main/java/com/rebuild/core/privileges/bizz/Department.java @@ -9,6 +9,7 @@ import cn.devezhao.bizz.security.member.BusinessUnit; import cn.devezhao.persist4j.engine.ID; +import org.jetbrains.annotations.NotNull; import java.io.Serializable; import java.util.Collections; @@ -21,11 +22,14 @@ * @author devezhao * @since 10/08/2018 */ -public class Department extends BusinessUnit { +public class Department extends BusinessUnit implements Comparable { private static final long serialVersionUID = -5308455934676294159L; - public Department(Serializable identity, String name, boolean disabled) { + private int seq; + + public Department(Serializable identity, String name, boolean disabled, int seq) { super(identity, name, disabled); + this.seq = seq; } /** @@ -75,4 +79,11 @@ public Set getAllChildren() { } return Collections.unmodifiableSet(children); } + + @Override + public int compareTo(@NotNull Department o) { + int c = Integer.compare(seq, o.seq); + if (c == 0) return getName().compareTo(o.getName()); + return c; + } } \ No newline at end of file diff --git a/src/main/java/com/rebuild/core/privileges/bizz/User.java b/src/main/java/com/rebuild/core/privileges/bizz/User.java index ce57e2b3fd..07b9dea70c 100644 --- a/src/main/java/com/rebuild/core/privileges/bizz/User.java +++ b/src/main/java/com/rebuild/core/privileges/bizz/User.java @@ -12,6 +12,7 @@ import com.rebuild.core.privileges.RoleService; import com.rebuild.core.privileges.UserService; import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; /** * 用户 @@ -19,23 +20,25 @@ * @author Zixin (RB) * @since 09/16/2018 */ -public class User extends cn.devezhao.bizz.security.member.User { +public class User extends cn.devezhao.bizz.security.member.User implements Comparable { private static final long serialVersionUID = 15823574375847575L; private String email; private String workphone; private String fullName; private String avatarUrl; + private int seq; private CombinedRole combinedRole; public User(ID userId, String loginName, String email, String workphone, - String fullName, String avatarUrl, boolean disabled) { + String fullName, String avatarUrl, boolean disabled, int seq) { super(userId, loginName, disabled); this.email = email; this.workphone = workphone; this.fullName = fullName; this.avatarUrl = avatarUrl; + this.seq = seq; } /** @@ -143,4 +146,11 @@ public int hashCode() { public boolean equals(Object o) { return super.equals(o); } + + @Override + public int compareTo(@NotNull User o) { + int c = Integer.compare(seq, o.seq); + if (c == 0) return getName().compareTo(o.getName()); + return c; + } } diff --git a/src/main/java/com/rebuild/core/rbstore/RBStore.java b/src/main/java/com/rebuild/core/rbstore/RBStore.java index 1219b3a908..77882d9a05 100644 --- a/src/main/java/com/rebuild/core/rbstore/RBStore.java +++ b/src/main/java/com/rebuild/core/rbstore/RBStore.java @@ -27,7 +27,7 @@ public class RBStore { // https://github.com/getrebuild/rebuild-datas/ - private static final String DATA_REPO = BootEnvironmentPostProcessor.getProperty( + public static final String DATA_REPO = BootEnvironmentPostProcessor.getProperty( ConfigurationItem.RbStoreUrl.name(), "https://getrebuild.com/gh/getrebuild/rebuild-datas/"); /** diff --git a/src/main/java/com/rebuild/core/rbstore/RbSystemImporter.java b/src/main/java/com/rebuild/core/rbstore/RbSystemImporter.java new file mode 100644 index 0000000000..706bb4c856 --- /dev/null +++ b/src/main/java/com/rebuild/core/rbstore/RbSystemImporter.java @@ -0,0 +1,191 @@ +/*! +Copyright (c) REBUILD and/or its owners. All rights reserved. + +rebuild is dual-licensed under commercial and open source licenses (GPLv3). +See LICENSE and COMMERCIAL in the project root for license information. +*/ + +package com.rebuild.core.rbstore; + +import cn.devezhao.persist4j.PersistManagerFactory; +import cn.devezhao.persist4j.util.SqlHelper; +import cn.hutool.core.util.ZipUtil; +import com.rebuild.core.Application; +import com.rebuild.core.RebuildException; +import com.rebuild.core.metadata.impl.DynamicMetadataFactory; +import com.rebuild.core.support.ConfigurationItem; +import com.rebuild.core.support.RebuildConfiguration; +import com.rebuild.core.support.setup.Installer; +import com.rebuild.core.support.task.HeavyTask; +import com.rebuild.utils.OkHttpUtils; +import org.apache.commons.io.FileUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Field; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * 系统导入 + * + * @author devezhao + * @see com.rebuild.core.support.setup.Installer + * @since 2024/11/29 + */ +public class RbSystemImporter extends HeavyTask { + + private final String fileUrl; + private File rbspkgDir; + + public RbSystemImporter(String fileUrl) { + this.fileUrl = fileUrl; + } + + protected RbSystemImporter(File rbspkgDir) { + this.fileUrl = null; + this.rbspkgDir = rbspkgDir; + } + + @Override + protected Integer exec() throws Exception { + // #1 准备文件 + this.readyFiles(); + + // #2 保留必要参数 + final String holdSN = RebuildConfiguration.get(ConfigurationItem.SN); + final String holdAppName = RebuildConfiguration.get(ConfigurationItem.AppName); + final String holdLOGO = RebuildConfiguration.get(ConfigurationItem.LOGO); + final String holdLOGOWhite = RebuildConfiguration.get(ConfigurationItem.LOGOWhite); + final String holdHomeURL = RebuildConfiguration.get(ConfigurationItem.HomeURL); + // 2.1# _READY=false + final Field field_READY = Application.class.getDeclaredField("_READY"); + field_READY.setAccessible(true); + field_READY.set(null, false); + + // TODO 保留用戶/部門/角色 + + // #3 清空数据库 + this.forceEmptyDb(); + // #3.1 初始化数据库 + this.importDb(); + + // #4 清空缓存 + Installer.clearAllCache(); + + // #5 重载配置 + RebuildConfiguration.set(ConfigurationItem.SN, holdSN); + RebuildConfiguration.set(ConfigurationItem.AppName, holdAppName); + RebuildConfiguration.set(ConfigurationItem.LOGO, holdLOGO); + RebuildConfiguration.set(ConfigurationItem.LOGOWhite, holdLOGOWhite); + RebuildConfiguration.set(ConfigurationItem.HomeURL, holdHomeURL); + // 刷新配置缓存 + for (ConfigurationItem item : ConfigurationItem.values()) { + RebuildConfiguration.get(item, true); + } + // 加载自定义实体 + ((DynamicMetadataFactory) Application.getBean(PersistManagerFactory.class).getMetadataFactory()).refresh(); + // 字段还原 + field_READY.set(null, true); + field_READY.setAccessible(false); + + // #6 报表模版 + File REPORT_TEMPLATES = new File(rbspkgDir, "REPORT_TEMPLATES"); + if (REPORT_TEMPLATES.exists()) { + File dest = RebuildConfiguration.getFileOfData("rb/REPORT_TEMPLATES"); + if (!dest.exists()) FileUtils.forceMkdir(dest); + FileUtils.copyDirectory(REPORT_TEMPLATES, dest); + } + + return 1; + } + + /** + * 檢查 + * + * @throws RebuildException + */ + public void check() throws RebuildException { + try { + this.readyFiles(); + } catch (IOException e) { + throw new RebuildException("CANNOT FETCH RBSPKG", e); + } + + if (!new File(this.rbspkgDir, "rebuild.sql").exists()) { + throw new RebuildException("BAD RBSPKG RESOURCES"); + } + } + + private void readyFiles() throws IOException { + if (rbspkgDir != null) return; + + File rbspkgDir = RebuildConfiguration.getFileOfTemp("__RBSPKG"); + if (rbspkgDir.exists()) FileUtils.forceDelete(rbspkgDir); + FileUtils.forceMkdir(rbspkgDir); + + File rbspkg = OkHttpUtils.readBinary(RBStore.DATA_REPO + fileUrl); + if (rbspkg == null) throw new RebuildException("Cannot fetch respkg : " + RBStore.DATA_REPO + fileUrl); + + ZipUtil.unzip(rbspkg, rbspkgDir); + this.rbspkgDir = rbspkgDir; + } + + private void forceEmptyDb() { + Connection conn = Application.getSqlExecutor().getConnection(); + Statement stmt = null; + Statement stmt2 = null; + ResultSet rs = null; + + try { + stmt = conn.createStatement(); + rs = stmt.executeQuery("SHOW TABLES"); + stmt2 = conn.createStatement(); + while (rs.next()) { + String tableName = rs.getString(1); + stmt2.executeUpdate("DROP TABLE IF EXISTS " + tableName); + } + + } catch (SQLException ex) { + throw new RebuildException(ex); + } finally { + SqlHelper.close(rs); + SqlHelper.close(stmt); + SqlHelper.close(stmt2); + Application.getSqlExecutor().closeConnection(conn); + } + } + + private void importDb() { + File fileOfDb = new File(this.rbspkgDir, "rebuild.sql"); + Connection conn = Application.getSqlExecutor().getConnection(); + Statement stmt = null; + + try (BufferedReader reader = new BufferedReader(new FileReader(fileOfDb))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("--") || line.startsWith("/*")) continue; + sb.append(line); + + if (line.endsWith(";")) { + if (stmt == null) stmt = conn.createStatement(); + stmt.execute(sb.toString()); + sb = new StringBuilder(); + } else { + sb.append("\n"); + } + } + + } catch (Exception ex) { + throw new RebuildException(ex); + } finally { + SqlHelper.close(stmt); + Application.getSqlExecutor().closeConnection(conn); + } + } +} diff --git a/src/main/java/com/rebuild/core/service/BaseService.java b/src/main/java/com/rebuild/core/service/BaseService.java index 811e670fd3..80a67442e5 100644 --- a/src/main/java/com/rebuild/core/service/BaseService.java +++ b/src/main/java/com/rebuild/core/service/BaseService.java @@ -46,7 +46,7 @@ * @since 01/04/2019 */ @Slf4j -public class BaseService extends InternalPersistService { +public abstract class BaseService extends InternalPersistService { public BaseService(PersistManagerFactory aPMFactory) { super(aPMFactory); diff --git a/src/main/java/com/rebuild/core/service/CommonsService.java b/src/main/java/com/rebuild/core/service/CommonsService.java index bc12c138aa..60c7621331 100644 --- a/src/main/java/com/rebuild/core/service/CommonsService.java +++ b/src/main/java/com/rebuild/core/service/CommonsService.java @@ -15,16 +15,18 @@ import com.rebuild.core.RebuildException; import com.rebuild.core.metadata.MetadataHelper; import com.rebuild.core.privileges.PrivilegesGuardInterceptor; +import lombok.Getter; import org.springframework.stereotype.Service; import org.springframework.util.Assert; /** - * 基础 CRUD 服务,使用须知: + * 基础 CRUD 服务。使用须知: *
    - 此类有事物 *
    - 此类不经过用户权限验证 {@link PrivilegesGuardInterceptor} *
    - 此类不字段值做任何处理,如多值、附件 {@link BaseService} *
    - 此类无任何系统规则,如默认值、重复检查、自动编号等 *
    - 有权限的实体使用此类需要指定 `strictMode=false` + *
    - v3.9 如有以上(部分)需求请考虑使用 #getBaseService * * @author Zixin (RB) * @since 11/06/2019 @@ -32,8 +34,12 @@ @Service("rbCommonsService") public class CommonsService extends InternalPersistService { + @Getter + final private BaseService baseService; + protected CommonsService(PersistManagerFactory aPMFactory) { super(aPMFactory); + this.baseService = new BaseService(aPMFactory){}; } @Override diff --git a/src/main/java/com/rebuild/core/service/SqlExecutor.java b/src/main/java/com/rebuild/core/service/SqlExecutor.java index 9948b15b94..e5e123e851 100644 --- a/src/main/java/com/rebuild/core/service/SqlExecutor.java +++ b/src/main/java/com/rebuild/core/service/SqlExecutor.java @@ -11,8 +11,10 @@ import cn.devezhao.persist4j.PersistManagerFactory; import cn.devezhao.persist4j.engine.JdbcSupport; import cn.devezhao.persist4j.engine.StatementCallback; +import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.stereotype.Service; +import java.sql.Connection; import java.sql.PreparedStatement; import java.util.ArrayList; import java.util.Collection; @@ -119,4 +121,20 @@ private int executeBatchInternal(Collection sqls, int timeout) { } return affected; } + + /** + * @return + * @see #closeConnection(Connection) + */ + public Connection getConnection() { + return DataSourceUtils.getConnection(aPMFactory.getDataSource()); + } + + /** + * @param conn + */ + public void closeConnection(Connection conn) { + if (conn == null) return; + DataSourceUtils.releaseConnection(conn, aPMFactory.getDataSource()); + } } \ No newline at end of file diff --git a/src/main/java/com/rebuild/core/service/approval/ApprovalFields2Schema.java b/src/main/java/com/rebuild/core/service/approval/ApprovalFields2Schema.java index a5140d6d3e..e63aaacf07 100644 --- a/src/main/java/com/rebuild/core/service/approval/ApprovalFields2Schema.java +++ b/src/main/java/com/rebuild/core/service/approval/ApprovalFields2Schema.java @@ -123,7 +123,7 @@ private Field buildApprovalStepNodeName(Entity entity) { DisplayType.TEXT, true, false, false, true, true, null, null, null, null, null); } - private boolean schema2DatabaseInternal(Entity entity, Field... fields) { + private void schema2DatabaseInternal(Entity entity, Field... fields) { boolean schemaReady = schema2Database(entity, fields); if (!schemaReady) { @@ -132,7 +132,6 @@ private boolean schema2DatabaseInternal(Entity entity, Field... fields) { } MetadataHelper.getMetadataFactory().refresh(); - return true; } /** diff --git a/src/main/java/com/rebuild/core/service/approval/ApprovalHelper.java b/src/main/java/com/rebuild/core/service/approval/ApprovalHelper.java index 0443cf94fb..52a3e69ff9 100644 --- a/src/main/java/com/rebuild/core/service/approval/ApprovalHelper.java +++ b/src/main/java/com/rebuild/core/service/approval/ApprovalHelper.java @@ -22,6 +22,7 @@ import com.rebuild.core.service.trigger.TriggerWhen; import com.rebuild.core.support.i18n.Language; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.util.Assert; import java.util.ArrayList; @@ -166,13 +167,19 @@ public static String getNodeIdByName(String nodeName, ID approvalId) { * 根据节点编号获取名称 * * @param nodeId + * @param approvalId + * @param fallbackName * @return */ - public static String getNodeNameById(String nodeId, ID approvalId) { + public static String getNodeNameById(String nodeId, ID approvalId, boolean fallbackName) { FlowDefinition flowDefinition = RobotApprovalManager.instance.getFlowDefinition(approvalId); FlowParser flowParser = flowDefinition.createFlowParser(); for (FlowNode node : flowParser.getAllNodes()) { - if (nodeId.equals(node.getNodeId())) return node.getNodeName(); + if (nodeId.equals(node.getNodeId())) { + String name = node.getNodeName(); + if (StringUtils.isBlank(name) && fallbackName) name = "@" + nodeId; + return name; + } } return null; } diff --git a/src/main/java/com/rebuild/core/service/approval/ApprovalProcessor.java b/src/main/java/com/rebuild/core/service/approval/ApprovalProcessor.java index dce6240bbc..ed5efb90f7 100644 --- a/src/main/java/com/rebuild/core/service/approval/ApprovalProcessor.java +++ b/src/main/java/com/rebuild/core/service/approval/ApprovalProcessor.java @@ -25,6 +25,7 @@ import com.rebuild.core.service.general.GeneralEntityServiceContextHolder; import com.rebuild.core.service.notification.MessageBuilder; import com.rebuild.core.support.SetUser; +import com.rebuild.core.support.general.FieldValueHelper; import com.rebuild.core.support.i18n.Language; import com.rebuild.utils.CommonsUtils; import com.rebuild.utils.JSONUtils; @@ -513,19 +514,53 @@ public JSONArray getCurrentStep(ApprovalStatus useStatus) { /** * 获取已审批节点 * + * @param allHis 包括历史审批 * @return returns [ [S,S], [S], [SSS], [S] ] */ - public JSONArray getWorkedSteps() { + public JSONArray getWorkedSteps(final boolean allHis) { final ApprovalStatus status = ApprovalHelper.getApprovalStatus(this.recordId); this.approvalId = status.getApprovalId(); - Object[][] array = Application.createQueryNoFilter( - "select approver,state,remark,approvedTime,createdOn,createdBy,node,prevNode,nodeBatch,ccUsers,ccAccounts,attrMore from RobotApprovalStep" + - " where recordId = ? and isWaiting = 'F' and isCanceled = 'F' order by createdOn") + String sql = "select approver,state,remark,approvedTime,createdOn,createdBy,node,prevNode,nodeBatch,ccUsers,ccAccounts,attrMore,approvalId,isBacked from RobotApprovalStep" + + " where recordId = ? and isWaiting = 'F' and isCanceled = 'F' order by createdOn"; + if (allHis) sql = sql.replace("and isCanceled = 'F' ", ""); + Object[][] array = Application.createQueryNoFilter(sql) .setParameter(1, this.recordId) .array(); if (array.length == 0) return JSONUtils.EMPTY_ARRAY; + if (!allHis) return getWorkedSteps1Batch(array, status); + + JSONArray batchs = new JSONArray(); + List batch = new ArrayList<>(); + for (Object[] o : array) { + String node = (String) o[6]; + batch.add(o); + + // 撤销了:新批次 + if (FlowNode.NODE_REVOKED.equals(node) || (FlowNode.NODE_CANCELED.equals(node) && !(Boolean) o[13])) { + Object[] bLast = batch.get(batch.size() - 2); + ID bLastApprovalId = (ID) bLast[12]; + String bLastApprovalName = FieldValueHelper.getLabelNotry(bLastApprovalId); + ApprovalStatus bLastStatus = new ApprovalStatus( + bLastApprovalId, bLastApprovalName, ApprovalState.REVOKED.getState(), (String) bLast[6], this.recordId); + + this.approvalId = bLastApprovalId; + this.flowParser = null; + batchs.addAll(getWorkedSteps1Batch(batch.toArray(new Object[0][]), bLastStatus)); + batch.clear(); + } + } + // current + this.approvalId = status.getApprovalId(); + this.flowParser = null; + if (!batch.isEmpty()) { + batchs.addAll(getWorkedSteps1Batch(batch.toArray(new Object[0][]), status)); + } + + return batchs; + } + private JSONArray getWorkedSteps1Batch(Object[][] array, ApprovalStatus status) { Object[] firstStep = null; Map> stepBatchMap = new LinkedHashMap<>(); for (Object[] o : array) { diff --git a/src/main/java/com/rebuild/core/service/approval/ApprovalStatus.java b/src/main/java/com/rebuild/core/service/approval/ApprovalStatus.java index 92cf26eaeb..c82b55d7ff 100644 --- a/src/main/java/com/rebuild/core/service/approval/ApprovalStatus.java +++ b/src/main/java/com/rebuild/core/service/approval/ApprovalStatus.java @@ -9,11 +9,13 @@ import cn.devezhao.persist4j.engine.ID; import com.rebuild.core.Application; +import lombok.Getter; /** * @author devezhao zhaofang123@gmail.com * @since 2020/08/10 */ +@Getter public class ApprovalStatus { private ID approvalId; @@ -21,11 +23,10 @@ public class ApprovalStatus { private Integer currentState; private String currentStepNode; - private String lastComment; final private ID recordId; - protected ApprovalStatus(ID approvalId, String approvalName, Integer currentState, String currentStepNode, ID recordId) { + public ApprovalStatus(ID approvalId, String approvalName, Integer currentState, String currentStepNode, ID recordId) { this.approvalId = approvalId; this.approvalName = approvalName; this.currentState = currentState; @@ -33,23 +34,11 @@ protected ApprovalStatus(ID approvalId, String approvalName, Integer currentStat this.recordId = recordId; } - public ID getApprovalId() { - return approvalId; - } - - public String getApprovalName() { - return approvalName; - } - public ApprovalState getCurrentState() { return currentState == null ? ApprovalState.DRAFT : (ApprovalState) ApprovalState.valueOf(currentState); } - public String getCurrentStepNode() { - return currentStepNode; - } - /** * @return */ diff --git a/src/main/java/com/rebuild/core/service/approval/ApprovalStepService.java b/src/main/java/com/rebuild/core/service/approval/ApprovalStepService.java index 42019fba9c..52e065b753 100644 --- a/src/main/java/com/rebuild/core/service/approval/ApprovalStepService.java +++ b/src/main/java/com/rebuild/core/service/approval/ApprovalStepService.java @@ -125,7 +125,7 @@ public void txSubmit(Record recordOfMain, Set ccUsers, Set ccAccount String ckey = "ApprovalSubmitter" + recordId + approvalId; Application.getCommonsCache().evict(ckey); - execTriggersWhenSE(recordOfMain, TriggerWhen.SUBMIT); + execTriggersWhenSR(recordOfMain, TriggerWhen.SUBMIT); this.execSopSteps38(recordOfMain); } @@ -225,7 +225,7 @@ public void txApprove(Record stepRecord, String signMode, Set ccUsers, Set ccUsers, Set ccUsers, Set ccUsers, Set allowTriggers = new ArrayList<>(); @@ -805,14 +808,14 @@ private void execTriggersByNode(Record approvalRecord, ID approvalId, String cur } if (!allowTriggers.isEmpty()) { - RobotTriggerObserver.setAllowTriggersOnApproved(allowTriggers.toString()); + RobotTriggerObserver.setAllowTriggersOnNodeApproved(allowTriggers.toString()); for (ID did : QueryHelper.detailIdsNoFilter(approvalRecord.getPrimary(), de)) { Record dAfter = EntityHelper.forUpdate(did, approvalUser, false); triggerManual.onApproved( OperatingContext.create(approvalUser, InternalPermission.APPROVAL, null, dAfter)); } - RobotTriggerObserver.clearAllowTriggersOnApproved(); + RobotTriggerObserver.clearAllowTriggersOnNodeApproved(); allowTriggers.clear(); } } @@ -824,25 +827,23 @@ private void execTriggersByNode(Record approvalRecord, ID approvalId, String cur } if (!allowTriggers.isEmpty()) { - RobotTriggerObserver.setAllowTriggersOnApproved(allowTriggers.toString()); + RobotTriggerObserver.setAllowTriggersOnNodeApproved(allowTriggers.toString()); triggerManual.onApproved( OperatingContext.create(approvalUser, InternalPermission.APPROVAL, null, approvalRecord)); } } finally { - RobotTriggerObserver.clearAllowTriggersOnApproved(); + RobotTriggerObserver.clearAllowTriggersOnNodeApproved(); } } - private boolean isSpecApproveNode(TriggerAction triggerAction, String nodeName) { - JSONObject actionContent = (JSONObject) triggerAction.getActionContext().getActionContent(); - JSONArray whenApproveNodes = actionContent.getJSONArray("whenApproveNodes"); - return whenApproveNodes != null && (whenApproveNodes.contains(nodeName) || whenApproveNodes.contains("*")); + private boolean isSpecApproveNode(TriggerAction action, String nodeName) { + JSONArray whenApproveNodes = ((JSONObject) action.getActionContext().getActionContent()) + .getJSONArray("whenApproveNodes"); + if (whenApproveNodes == null || whenApproveNodes.isEmpty()) return false; + return whenApproveNodes.contains(nodeName) || whenApproveNodes.contains("*"); } - /** - * @param approvalRecord - */ private void execSopSteps38(Record approvalRecord) { try { CommonsUtils.invokeMethod("com.rebuild.rbv.sop.RobotSopObserver#onApproveManual", approvalRecord); diff --git a/src/main/java/com/rebuild/core/service/approval/FlowParser.java b/src/main/java/com/rebuild/core/service/approval/FlowParser.java index 593af78c2e..b4f5787b88 100644 --- a/src/main/java/com/rebuild/core/service/approval/FlowParser.java +++ b/src/main/java/com/rebuild/core/service/approval/FlowParser.java @@ -152,7 +152,7 @@ public boolean hasApproverNode() { /** * @return */ - protected Collection getAllNodes() { + public Collection getAllNodes() { return nodeMap.values(); } diff --git a/src/main/java/com/rebuild/core/service/approval/RobotApprovalManager.java b/src/main/java/com/rebuild/core/service/approval/RobotApprovalManager.java index cda533cf65..5d84c7792e 100644 --- a/src/main/java/com/rebuild/core/service/approval/RobotApprovalManager.java +++ b/src/main/java/com/rebuild/core/service/approval/RobotApprovalManager.java @@ -173,9 +173,7 @@ public FlowDefinition[] getFlowDefinitions(ID recordId, ID user) { public FlowDefinition[] getFlowDefinitions(Entity entity) { final String cKey = CKEY_PREFIX + entity.getName(); FlowDefinition[] defs = (FlowDefinition[]) Application.getCommonsCache().getx(cKey); - if (defs != null) { - return defs; - } + if (defs != null) return defs; Object[][] array = Application.createQueryNoFilter( "select flowDefinition,isDisabled,name,configId,modifiedOn from RobotApprovalConfig where belongEntity = ?") diff --git a/src/main/java/com/rebuild/core/service/dashboard/DashboardManager.java b/src/main/java/com/rebuild/core/service/dashboard/DashboardManager.java index da5841f5d6..d7d4d66b99 100644 --- a/src/main/java/com/rebuild/core/service/dashboard/DashboardManager.java +++ b/src/main/java/com/rebuild/core/service/dashboard/DashboardManager.java @@ -15,6 +15,7 @@ import com.rebuild.core.configuration.general.ShareToManager; import com.rebuild.core.metadata.EntityHelper; import com.rebuild.core.privileges.UserHelper; +import com.rebuild.core.privileges.bizz.ZeroEntry; import com.rebuild.core.support.i18n.Language; import com.rebuild.utils.JSONUtils; @@ -51,9 +52,23 @@ protected String getConfigFields() { * @return */ public JSON getAvailable(ID user) { + return getAvailable(user, true); + } + + /** + * 获取可用面板 + * + * @param user + * @param autoCreate + * @return + */ + public JSON getAvailable(ID user, boolean autoCreate) { ID detected = detectUseConfig(user, null, null); // 没有就初始化一个 if (detected == null) { + if (!autoCreate) return null; + if (!Application.getPrivilegesManager().allow(user, ZeroEntry.AllowCustomChart)) return null; + Record record = EntityHelper.forNew(EntityHelper.DashboardConfig, user); record.setString("config", JSONUtils.EMPTY_ARRAY_STR); record.setString("title", UserHelper.isAdmin(user) ? Language.L("默认仪表盘") : Language.L("我的仪表盘")); diff --git a/src/main/java/com/rebuild/core/service/dashboard/charts/ChartData.java b/src/main/java/com/rebuild/core/service/dashboard/charts/ChartData.java index e7f5024eb8..980abba5f4 100644 --- a/src/main/java/com/rebuild/core/service/dashboard/charts/ChartData.java +++ b/src/main/java/com/rebuild/core/service/dashboard/charts/ChartData.java @@ -11,7 +11,6 @@ import cn.devezhao.persist4j.Entity; import cn.devezhao.persist4j.Field; import cn.devezhao.persist4j.Query; -import cn.devezhao.persist4j.dialect.FieldType; import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; @@ -152,6 +151,7 @@ public Numerical[] getNumericals() { validFields[0], getFormatSort(item), getFormatCalc(item), item.getString("label"), item.getInteger("scale"), + item.getInteger("unit"), item.getJSONObject("filter"), validFields[1]); list.add(num); @@ -284,7 +284,7 @@ protected String getSortSql() { * @return */ protected String wrapAxisValue(Numerical numerical, Object value) { - return wrapAxisValue(numerical, value, Boolean.FALSE); + return wrapAxisValue(numerical, value, false); } /** @@ -294,20 +294,16 @@ protected String wrapAxisValue(Numerical numerical, Object value) { * @return */ protected String wrapAxisValue(Numerical numerical, Object value, boolean useThousands) { - if (ChartsHelper.isZero(value)) { - return ChartsHelper.VALUE_ZERO; - } + if (ChartsHelper.isZero(value)) return ChartsHelper.VALUE_ZERO; + if (ID.isId(value)) value = 1; String format = "###"; if (numerical.getScale() > 0) { format = "##0."; format = StringUtils.rightPad(format, format.length() + numerical.getScale(), "0"); } - if (useThousands) format = "#," + format; - if (ID.isId(value)) value = 1; - String n = new DecimalFormat(format).format(value); if (useThousands) n = formatAxisValue(numerical, n); return n; @@ -319,29 +315,26 @@ protected String wrapAxisValue(Numerical numerical, Object value, boolean useTho private String formatAxisValue(Numerical numerical, String value) { String type = getNumericalFlag(numerical); if (type == null) return value; + type = type.split(":")[0]; if ("%".equals(type)) value += "%"; else if (type.contains("%s")) value = String.format(type, value); - else value = type + " " + value; + else if (!"0".equals(type)) value = type + " " + value; return value; } /** + * @param numerical + * @return returns FLAG:UNIT * @see com.rebuild.core.metadata.easymeta.EasyDecimal#wrapValue(Object) */ protected String getNumericalFlag(Numerical numerical) { - if (numerical.getField().getType() != FieldType.DECIMAL) return null; - - if (!(numerical.getFormatCalc() == FormatCalc.SUM - || numerical.getFormatCalc() == FormatCalc.AVG - || numerical.getFormatCalc() == FormatCalc.MIN - || numerical.getFormatCalc() == FormatCalc.MAX)) { - return null; + String type = null; + EasyField easyField = EasyMetaFactory.valueOf(numerical.getField()); + if (easyField.getDisplayType() == DisplayType.DECIMAL) { + type = easyField.getExtraAttr(EasyFieldConfigProps.DECIMAL_TYPE); } - - String type = EasyMetaFactory.valueOf(numerical.getField()).getExtraAttr(EasyFieldConfigProps.DECIMAL_TYPE); - if (type == null || "0".equalsIgnoreCase(type)) return null; - else return type; + return StringUtils.defaultIfBlank(type, "0") + ":" + numerical.getUnit(); } /** diff --git a/src/main/java/com/rebuild/core/service/dashboard/charts/ChartsHelper.java b/src/main/java/com/rebuild/core/service/dashboard/charts/ChartsHelper.java index f2caeab28c..503f4365dc 100644 --- a/src/main/java/com/rebuild/core/service/dashboard/charts/ChartsHelper.java +++ b/src/main/java/com/rebuild/core/service/dashboard/charts/ChartsHelper.java @@ -7,6 +7,8 @@ package com.rebuild.core.service.dashboard.charts; +import java.math.BigDecimal; + /** * @author ZHAO * @since 2020/4/28 @@ -38,6 +40,8 @@ public static boolean isZero(Object value) { return (Long) value == 0L; } else if (value instanceof Integer) { return (Integer) value == 0; + } else if (value instanceof BigDecimal) { + return ((BigDecimal) value).doubleValue() == 0d; } return false; } diff --git a/src/main/java/com/rebuild/core/service/dashboard/charts/IndexChart.java b/src/main/java/com/rebuild/core/service/dashboard/charts/IndexChart.java index 5faabbd950..14827b7400 100644 --- a/src/main/java/com/rebuild/core/service/dashboard/charts/IndexChart.java +++ b/src/main/java/com/rebuild/core/service/dashboard/charts/IndexChart.java @@ -30,15 +30,16 @@ public JSON build() { Numerical num = nums[0]; Object[] dataRaw = createQuery(buildSql(num, true)).unique(); JSONObject index = JSONUtils.toJSONObject( - new String[]{"data", "label"}, - new Object[]{wrapAxisValue(num, dataRaw[0], true), num.getLabel()}); + new String[]{"data", "label", "dataFlag"}, + new Object[]{wrapAxisValue(num, dataRaw[0]), num.getLabel(), getNumericalFlag(num)}); // 对比 if (nums.length > 1) { num = nums[1]; dataRaw = createQuery(buildSql(num, true)).unique(); - index.put("data2", wrapAxisValue(num, dataRaw[0], true)); + index.put("data2", wrapAxisValue(num, dataRaw[0])); index.put("label2", num.getLabel()); + index.put("dataFlag2", getNumericalFlag(num)); } JSONObject renderOption = config.getJSONObject("option"); diff --git a/src/main/java/com/rebuild/core/service/dashboard/charts/Numerical.java b/src/main/java/com/rebuild/core/service/dashboard/charts/Numerical.java index b5c4e826b0..5cbc15247d 100644 --- a/src/main/java/com/rebuild/core/service/dashboard/charts/Numerical.java +++ b/src/main/java/com/rebuild/core/service/dashboard/charts/Numerical.java @@ -12,6 +12,7 @@ import com.rebuild.core.metadata.easymeta.DisplayType; import com.rebuild.core.metadata.easymeta.EasyMetaFactory; import com.rebuild.core.service.query.ParseHelper; +import lombok.Getter; import org.apache.commons.lang.StringUtils; /** @@ -22,8 +23,12 @@ */ public class Numerical extends Axis { + @Getter private JSONObject filter = null; + @Getter private int scale = 2; + @Getter + private int unit = 0; /** * @param field @@ -31,32 +36,18 @@ public class Numerical extends Axis { * @param calc * @param label * @param scale + * @param unit * @param filter * @param parentField */ - protected Numerical(Field field, FormatSort sort, FormatCalc calc, String label, Integer scale, + protected Numerical(Field field, FormatSort sort, FormatCalc calc, String label, Integer scale, Integer unit, JSONObject filter, Field parentField) { super(field, sort, calc, label, parentField); if (scale != null) this.scale = scale; + if (unit != null) this.unit = unit; if (ParseHelper.validAdvFilter(filter)) this.filter = filter; } - /** - * 小数位 - * @return - */ - public int getScale() { - return scale; - } - - /** - * 字段筛选条件 - * @return - */ - public JSONObject getFilter() { - return filter; - } - @Override public String getLabel() { if (FormatCalc.NONE == getFormatCalc()) { diff --git a/src/main/java/com/rebuild/core/service/dashboard/charts/TableBuilder.java b/src/main/java/com/rebuild/core/service/dashboard/charts/TableBuilder.java index 0a477f708a..56b18dac7a 100644 --- a/src/main/java/com/rebuild/core/service/dashboard/charts/TableBuilder.java +++ b/src/main/java/com/rebuild/core/service/dashboard/charts/TableBuilder.java @@ -8,7 +8,6 @@ package com.rebuild.core.service.dashboard.charts; import cn.devezhao.commons.ObjectUtils; -import com.rebuild.utils.CommonsUtils; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang.StringUtils; @@ -29,8 +28,8 @@ public class TableBuilder { */ protected static final Axis LN_REF = new Axis(null, null, null, "#", null); - private TableChart chart; - private Object[][] rows; + final private TableChart chart; + final private Object[][] rows; /** * @param chart diff --git a/src/main/java/com/rebuild/core/service/dashboard/charts/TableChart.java b/src/main/java/com/rebuild/core/service/dashboard/charts/TableChart.java index cc5c6c2219..372d48510e 100644 --- a/src/main/java/com/rebuild/core/service/dashboard/charts/TableChart.java +++ b/src/main/java/com/rebuild/core/service/dashboard/charts/TableChart.java @@ -8,14 +8,18 @@ package com.rebuild.core.service.dashboard.charts; import cn.devezhao.commons.ObjectUtils; +import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; +import com.rebuild.core.support.i18n.Language; import com.rebuild.utils.JSONUtils; import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ArrayUtils; import java.math.BigDecimal; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -29,6 +33,7 @@ public class TableChart extends ChartData { private boolean showLineNumber = false; private boolean showSums = false; private boolean mergeCell = true; + private int pageSize = 0; protected TableChart(JSONObject config) { super(config); @@ -38,6 +43,7 @@ protected TableChart(JSONObject config) { this.showLineNumber = option.getBooleanValue("showLineNumber"); this.showSums = option.getBooleanValue("showSums"); if (option.containsKey("mergeCell")) this.mergeCell = option.getBooleanValue("mergeCell"); + if (option.containsKey("pageSize")) this.pageSize = option.getIntValue("pageSize"); } } @@ -63,6 +69,11 @@ public JSON build() { dataRaw = createQuery(buildSql(dims, nums)).array(); } + // v3.9 + if (pageSize > 0 && dataRaw.length > pageSize) { + dataRaw = ArrayUtils.subarray(dataRaw, 0, pageSize); + } + // 行号 if (this.showLineNumber && dataRaw.length > 0) { for (int i = 0; i < dataRaw.length; i++) { @@ -153,4 +164,56 @@ private String buildSql(Dimension[] dims, Numerical[] nums) { return appendSqlSort(sql); } + + @Override + protected String wrapAxisValue(Numerical numerical, Object value, boolean useThousands) { + if (ChartsHelper.isZero(value)) return ChartsHelper.VALUE_ZERO; + if (ID.isId(value)) value = 1d; + + String flag = getNumericalFlag(numerical); + if (StringUtils.isBlank(flag) || "0:0".equals(flag)) return super.wrapAxisValue(numerical, value, useThousands); + + // v3.9 + String flagUnit = ""; + int unit = Integer.parseInt(flag.split(":")[1]); + if (unit > 0) { + double d = ObjectUtils.toDouble(value); + switch (unit) { + case 1000: { + d /= 1000; + flagUnit = Language.L("千"); + break; + } + case 10000: { + d /= 10000; + flagUnit = Language.L("万"); + break; + } + case 100000: { + d /= 100000; + flagUnit = Language.L("十万"); + break; + } + case 1000000: { + d /= 1000000; + flagUnit = Language.L("百万"); + break; + } + case 10000000: { + d /= 10000000; + flagUnit = Language.L("千万"); + break; + } + case 100000000: { + d /= 100000000; + flagUnit = Language.L("亿"); + break; + } + } + value = d; + } + + String n = super.wrapAxisValue(numerical, value, useThousands); + return n + flagUnit; + } } diff --git a/src/main/java/com/rebuild/core/service/dashboard/charts/TreemapChart.java b/src/main/java/com/rebuild/core/service/dashboard/charts/TreemapChart.java index 71a19417c9..0842580392 100644 --- a/src/main/java/com/rebuild/core/service/dashboard/charts/TreemapChart.java +++ b/src/main/java/com/rebuild/core/service/dashboard/charts/TreemapChart.java @@ -54,15 +54,4 @@ public JSON build() { new String[]{"data", "xLabel", "xAmount", "_renderOption"}, new Object[]{builder.toJSON(), num1.getLabel(), xAmount, renderOption}); } - - @Override - public Numerical[] getNumericals() { - Numerical[] nums = super.getNumericals(); - if (nums.length == 0) { - Numerical n = new Numerical( - getSourceEntity().getPrimaryField(), FormatSort.NONE, FormatCalc.COUNT, null, 0, null, null); - return new Numerical[]{n}; - } - return nums; - } } diff --git a/src/main/java/com/rebuild/core/service/dataimport/DataImporter.java b/src/main/java/com/rebuild/core/service/dataimport/DataImporter.java index ba29c1d997..40fdcbbae8 100644 --- a/src/main/java/com/rebuild/core/service/dataimport/DataImporter.java +++ b/src/main/java/com/rebuild/core/service/dataimport/DataImporter.java @@ -194,7 +194,7 @@ protected Record checkoutRecord(Cell[] row, ID defaultOwning) { } } - // fix: 3.9 导入也生效 + // fix: v3.9 导入也生效 AutoFillinManager.instance.fillinRecord(checkout); // Verify new record diff --git a/src/main/java/com/rebuild/core/service/general/GeneralEntityService.java b/src/main/java/com/rebuild/core/service/general/GeneralEntityService.java index f528f053b8..7377255188 100644 --- a/src/main/java/com/rebuild/core/service/general/GeneralEntityService.java +++ b/src/main/java/com/rebuild/core/service/general/GeneralEntityService.java @@ -148,8 +148,13 @@ record = record.getPrimary() == null ? create(record) : update(record); for (int i = 0; i < details.size(); i++) { Record d = details.get(i); if (d instanceof DeleteRecord) { - des.delete(d.getPrimary()); - detaileds.put(i, d.getPrimary()); + // 安静删除,不触发任何系统逻辑 + if (((DeleteRecord) d).isQuietly()) { + Application.getCommonsService().delete(d.getPrimary(), false); + } else { + des.delete(d.getPrimary()); + detaileds.put(i, d.getPrimary()); + } } } @@ -816,6 +821,7 @@ public void approve(ID recordId, ApprovalState state, ID approvalUser) { log.warn("Use '{}' do approve : {}", approvalUser, recordId); } + // after Record approvalRecord = EntityHelper.forUpdate(recordId, approvalUser, false); approvalRecord.setInt(EntityHelper.ApprovalState, state.getState()); delegateService.update(approvalRecord); diff --git a/src/main/java/com/rebuild/core/service/general/ObservableService.java b/src/main/java/com/rebuild/core/service/general/ObservableService.java index aaa5ddfc15..b40f89c0ff 100644 --- a/src/main/java/com/rebuild/core/service/general/ObservableService.java +++ b/src/main/java/com/rebuild/core/service/general/ObservableService.java @@ -42,7 +42,7 @@ public abstract class ObservableService extends SafeObservable implements Servic * @param aPMFactory */ protected ObservableService(PersistManagerFactory aPMFactory) { - this.delegateService = new BaseService(aPMFactory); + this.delegateService = new BaseService(aPMFactory){}; addObserver(new AttachmentAwareObserver()); addObserver(new RevisionHistoryObserver()); diff --git a/src/main/java/com/rebuild/core/service/general/OperatingContext.java b/src/main/java/com/rebuild/core/service/general/OperatingContext.java index 78880d088b..e8eca1a931 100644 --- a/src/main/java/com/rebuild/core/service/general/OperatingContext.java +++ b/src/main/java/com/rebuild/core/service/general/OperatingContext.java @@ -83,7 +83,7 @@ public Record getAfterRecord() { /** * NOTE!!! 请注意当共享时得到的是共享实体 Record - * 如果是为了获取源纪录 ID 推荐使用 {@link #getFixedRecordId()} + * 如果是为了获取源记录 ID 推荐使用 {@link #getFixedRecordId()} * * @return */ diff --git a/src/main/java/com/rebuild/core/service/general/recyclebin/RecycleBean.java b/src/main/java/com/rebuild/core/service/general/recyclebin/RecycleBean.java index 44c697da72..86150b3103 100644 --- a/src/main/java/com/rebuild/core/service/general/recyclebin/RecycleBean.java +++ b/src/main/java/com/rebuild/core/service/general/recyclebin/RecycleBean.java @@ -15,6 +15,7 @@ import com.alibaba.fastjson.JSONObject; import com.rebuild.core.Application; import com.rebuild.core.metadata.MetadataHelper; +import com.rebuild.core.service.general.GeneralEntityService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; @@ -32,6 +33,7 @@ public class RecycleBean implements Serializable { private static final long serialVersionUID = -1058552856844427594L; public static final String NAME_DETAILLIST = "$SLAVELIST$"; + public static final String NAME_DETAILLIST39 = GeneralEntityService.HAS_DETAILS; final private ID recordId; @@ -79,7 +81,7 @@ public JSON serialize() { detailList.add(item); } } - s.put(NAME_DETAILLIST, detailList); + s.put(NAME_DETAILLIST39, detailList); return s; } diff --git a/src/main/java/com/rebuild/core/service/general/recyclebin/RecycleRestore.java b/src/main/java/com/rebuild/core/service/general/recyclebin/RecycleRestore.java index dcb260edf8..d746911da8 100644 --- a/src/main/java/com/rebuild/core/service/general/recyclebin/RecycleRestore.java +++ b/src/main/java/com/rebuild/core/service/general/recyclebin/RecycleRestore.java @@ -44,7 +44,7 @@ @Slf4j public class RecycleRestore { - private ID recycleId; + final private ID recycleId; /** * @param recycleId @@ -178,9 +178,11 @@ private List conver2Record(JSONObject content, ID recordId) { } } - JSONArray detailList = content.getJSONArray(RecycleBean.NAME_DETAILLIST); - if (detailList != null) { + JSONArray detailsList = content.getJSONArray(RecycleBean.NAME_DETAILLIST); + if (detailsList == null) detailsList = content.getJSONArray(RecycleBean.NAME_DETAILLIST39); + if (detailsList != null) { content.remove(RecycleBean.NAME_DETAILLIST); + content.remove(RecycleBean.NAME_DETAILLIST39); } List records = new ArrayList<>(); @@ -189,8 +191,8 @@ private List conver2Record(JSONObject content, ID recordId) { // v36 多明细 Entity detailEntity = entity.getDetailEntity(); - if (detailList != null && detailEntity != null) { - for (Object o : detailList) { + if (detailsList != null && detailEntity != null) { + for (Object o : detailsList) { JSONObject item = (JSONObject) o; Entity de = detailEntity; if (item.containsKey(RestoreRecordCreator.META_FIELD)) { diff --git a/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer.java b/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer.java index ce018d7db7..7d55f18691 100644 --- a/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer.java +++ b/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer.java @@ -17,6 +17,7 @@ import com.rebuild.core.Application; import com.rebuild.core.configuration.ConfigBean; import com.rebuild.core.configuration.ConfigurationException; +import com.rebuild.core.configuration.general.AutoFillinManager; import com.rebuild.core.configuration.general.TransformManager; import com.rebuild.core.metadata.EntityHelper; import com.rebuild.core.metadata.EntityRecordCreator; @@ -172,6 +173,11 @@ public ID transform(ID sourceRecordId, ID specMainId) { return theNewId; } + /** + * @param record + * @param detailsList + * @return + */ protected ID saveRecord(Record record, List detailsList) { if (this.skipGuard) GeneralEntityServiceContextHolder.setSkipGuard(EntityHelper.UNSAVED_ID); @@ -242,22 +248,22 @@ protected Record transformRecord( // v3.7 clean fieldsMapping.remove("_"); - Record target = EntityHelper.forNew(targetEntity.getEntityCode(), getUser()); + Record targetRecord = EntityHelper.forNew(targetEntity.getEntityCode(), getUser()); if (defaultValue != null) { for (Map.Entry e : defaultValue.entrySet()) { - target.setObjectValue(e.getKey(), e.getValue()); + targetRecord.setObjectValue(e.getKey(), e.getValue()); } } List validFields = checkAndWarnFields(sourceEntity, fieldsMapping.values()); if (validFields.isEmpty()) { - // Fixed https://github.com/getrebuild/rebuild/issues/633 + // fix: https://github.com/getrebuild/rebuild/issues/633 log.warn("No fields (var) for transform : {}", fieldsMapping); } validFields.add(sourceEntity.getPrimaryField().getName()); - Record source = Application.getQueryFactory().recordNoFilter(sourceRecordId, validFields.toArray(new String[0])); + Record sourceRecord = Application.getQueryFactory().recordNoFilter(sourceRecordId, validFields.toArray(new String[0])); // 所属用户 ID specOwningUser = null; @@ -266,7 +272,7 @@ protected Record transformRecord( final String targetField = e.getKey(); if (e.getValue() == null) { - if (forceNullValue) target.setNull(targetField); + if (forceNullValue) targetRecord.setNull(targetField); continue; } @@ -277,39 +283,40 @@ protected Record transformRecord( if (sourceAny instanceof JSONArray) { Object sourceValue = ((JSONArray) sourceAny).get(0); - RecordVisitor.setValueByLiteral(targetField, sourceValue.toString(), target); + RecordVisitor.setValueByLiteral(targetField, sourceValue.toString(), targetRecord); } else { String sourceField = (String) sourceAny; - Object sourceValue = source.getObjectValue(sourceField); + Object sourceValue = sourceRecord.getObjectValue(sourceField); if (sourceValue != null) { EasyField sourceFieldEasy = EasyMetaFactory.valueOf( Objects.requireNonNull(MetadataHelper.getLastJoinField(sourceEntity, sourceField))); Object targetValue = sourceFieldEasy.convertCompatibleValue(sourceValue, targetFieldEasy); - target.setObjectValue(targetField, targetValue); + targetRecord.setObjectValue(targetField, targetValue); } else if (forceNullValue) { - target.setNull(targetField); + targetRecord.setNull(targetField); } } if (EntityHelper.OwningUser.equals(targetField)) { - specOwningUser = target.getID(EntityHelper.OwningUser); + specOwningUser = targetRecord.getID(EntityHelper.OwningUser); } } if (specOwningUser != null) { - target.setID(EntityHelper.OwningDept, + targetRecord.setID(EntityHelper.OwningDept, (ID) Application.getUserStore().getUser(specOwningUser).getOwningDept().getIdentity()); } if (checkNullable) { - new EntityRecordCreator(targetEntity, JSONUtils.EMPTY_OBJECT, getUser()).verify(target); + AutoFillinManager.instance.fillinRecord(targetRecord); + new EntityRecordCreator(targetEntity, JSONUtils.EMPTY_OBJECT, getUser()).verify(targetRecord); } - return target; + return targetRecord; } private List checkAndWarnFields(Entity entity, Collection fields) { diff --git a/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer37.java b/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer37.java index 611f554456..7319b47050 100644 --- a/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer37.java +++ b/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer37.java @@ -8,7 +8,6 @@ package com.rebuild.core.service.general.transform; import cn.devezhao.persist4j.Entity; -import cn.devezhao.persist4j.Field; import cn.devezhao.persist4j.Record; import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSONArray; @@ -75,21 +74,23 @@ public ID transform(ID sourceRecordId, ID specMainId) { if (filter != null) querySourceSql = querySourceSql.replace("(1=1)", filter); Object[][] dArray = Application.createQueryNoFilter(querySourceSql).array(); + Map dvMap4Details = Collections.singletonMap( + MetadataHelper.getDetailToMainField(dTargetEntity).getName(), EntityHelper.UNSAVED_ID); + for (Object[] d : dArray) { - detailsList.add(transformRecord( - dSourceEntity, dTargetEntity, fmd, (ID) d[0], null, false, false, checkNullable)); + Record dRecord = transformRecord( + dSourceEntity, dTargetEntity, fmd, (ID) d[0], dvMap4Details, false, false, checkNullable); + detailsList.add(dRecord); } } - Map dvMap4Detail = null; - if (specMainId != null) { - Field dtf = MetadataHelper.getDetailToMainField(targetEntity); - dvMap4Detail = Collections.singletonMap(dtf.getName(), specMainId); - } + // 目标是明细 + Map dvMap4Details = specMainId == null ? null : Collections.singletonMap( + MetadataHelper.getDetailToMainField(targetEntity).getName(), specMainId); Entity sourceEntity = MetadataHelper.getEntity(sourceRecordId.getEntityCode()); Record targetRecord = transformRecord( - sourceEntity, targetEntity, fieldsMapping, sourceRecordId, dvMap4Detail, false, false, checkNullable); + sourceEntity, targetEntity, fieldsMapping, sourceRecordId, dvMap4Details, false, false, checkNullable); // v3.5 需要先回填 // 因为可能以回填字段作为条件进行转换一次判断 diff --git a/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer39.java b/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer39.java new file mode 100644 index 0000000000..56187f6c3a --- /dev/null +++ b/src/main/java/com/rebuild/core/service/general/transform/RecordTransfomer39.java @@ -0,0 +1,218 @@ +/*! +Copyright (c) REBUILD and/or its owners. All rights reserved. + +rebuild is dual-licensed under commercial and open source licenses (GPLv3). +See LICENSE and COMMERCIAL in the project root for license information. +*/ + +package com.rebuild.core.service.general.transform; + +import cn.devezhao.persist4j.Entity; +import cn.devezhao.persist4j.Record; +import cn.devezhao.persist4j.engine.ID; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.rebuild.core.Application; +import com.rebuild.core.configuration.ConfigBean; +import com.rebuild.core.configuration.ConfigurationException; +import com.rebuild.core.configuration.general.AutoFillinManager; +import com.rebuild.core.configuration.general.FormsBuilderContextHolder; +import com.rebuild.core.configuration.general.TransformManager; +import com.rebuild.core.metadata.EntityHelper; +import com.rebuild.core.metadata.MetadataHelper; +import com.rebuild.core.service.general.GeneralEntityService; +import com.rebuild.core.service.query.QueryHelper; +import com.rebuild.core.support.CommonsLog; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Zixin + * @since 2024/4/8 + */ +@Slf4j +public class RecordTransfomer39 extends RecordTransfomer37 { + + private ID transid; + // 转换到已存在记录 + volatile private ID targetExistsRecordId; + + public RecordTransfomer39(ID transid) { + super(transid); + this.transid = transid; + } + + /** + * 转换 + * + * @param sourceRecordId + * @param specMainId + * @param targetExistsRecordId + * @return + */ + public ID transform(ID sourceRecordId, ID specMainId, ID targetExistsRecordId) { + this.targetExistsRecordId = targetExistsRecordId; + // 直接转换做非空检查 + this.transConfig.put("checkNullable35", true); + // 指定目标是明细 + if (targetExistsRecordId != null) specMainId = forceGetSpecMainId(targetExistsRecordId); + + ID theNewId = super.transform(sourceRecordId, specMainId); + // #saveRecord afetr + CommonsLog.createTransformLog(getUser(), sourceRecordId, theNewId, transid); + return theNewId; + } + + @Override + protected ID saveRecord(Record record, List detailsList) { + if (targetExistsRecordId == null) { + return super.saveRecord(record, detailsList); + } + + this.mergeExistsAndTarget(record); + + // 其下明细记录直接清空新建,因为没法一一对应去更新 + // 注意这会导致触发器触发动作不准 + JSONArray hasDetailsConf = transConfig.getJSONArray("fieldsMappingDetails"); + if (hasDetailsConf != null && !hasDetailsConf.isEmpty()) { + List detailIds = QueryHelper.detailIdsNoFilter(targetExistsRecordId); + if (!detailIds.isEmpty()) { + Application.getCommonsService().delete(detailIds.toArray(new ID[0]), false); + } + } + + return super.saveRecord(record, detailsList); + } + + /** + * 预览 + * + * @param sourceRecordId + * @param specMainId + * @param targetExistsRecordId + * @return + */ + public JSON preview(ID sourceRecordId, ID specMainId, ID targetExistsRecordId) { + this.targetExistsRecordId = targetExistsRecordId; + // 预览不做非空检查 + this.transConfig.put("checkNullable35", false); + // 指定目标是明细 + if (targetExistsRecordId != null) specMainId = forceGetSpecMainId(targetExistsRecordId); + + Entity sourceEntity = MetadataHelper.getEntity(sourceRecordId.getEntityCode()); + ConfigBean config = TransformManager.instance.getTransformConfig(transid, sourceEntity.getName()); + JSONObject transConfig = (JSONObject) config.getJSON("config"); + + JSONObject fieldsMapping = transConfig.getJSONObject("fieldsMapping"); + JSONArray fieldsMappingDetails = transConfig.getJSONArray("fieldsMappingDetails"); + if (fieldsMapping == null || fieldsMapping.isEmpty()) { + throw new ConfigurationException("INVALID TRANSFORM CONFIG"); + } + if (fieldsMapping.get("_") == null) { + throw new ConfigurationException("INCOMPATIBLE(39) TRANSFORM CONFIG"); + } + + Entity targetEntity = MetadataHelper.getEntity(config.getString("target")); + Record tansTargetRecord = transformRecord(sourceEntity, targetEntity, fieldsMapping, sourceRecordId, + null, false, false, false); + + if (targetExistsRecordId != null) { + this.mergeExistsAndTarget(tansTargetRecord); + // 此处强制回填,因为编辑时前端不会回填 + AutoFillinManager.instance.fillinRecord(tansTargetRecord, true); + } + + TransformerPreview37.fillLabelOfReference(tansTargetRecord); + + JSON formModel = UseFormsBuilder.buildFormWithRecord(targetEntity, tansTargetRecord, specMainId, getUser(), false); + if (fieldsMappingDetails == null || fieldsMappingDetails.isEmpty()) return formModel; + + // 有明细 + ID fakeMainId = EntityHelper.newUnsavedId(sourceEntity.getEntityCode()); + Map formModelDetailsMap = new HashMap<>(); + for (Object o : fieldsMappingDetails) { + JSONObject fmd = (JSONObject) o; + Entity[] fmdEntity = checkEntity(fmd); + if (fmdEntity == null) continue; + + Entity dTargetEntity = fmdEntity[0]; + Entity dSourceEntity = fmdEntity[1]; + + String querySourceSql = buildDetailsSourceSql(dSourceEntity, sourceRecordId); + String filter = appendFilter(fmd); + if (filter != null) querySourceSql = querySourceSql.replace("(1=1)", filter); + + Object[][] dArray = Application.createQueryNoFilter(querySourceSql).array(); + + JSONArray formModelDetails = new JSONArray(); + FormsBuilderContextHolder.setMainIdOfDetail(fakeMainId); + try { + for (Object[] d : dArray) { + Record dRecord = transformRecord( + dSourceEntity, dTargetEntity, fmd, (ID) d[0], null, true, false, false); + TransformerPreview37.fillLabelOfReference(dRecord); + + JSON m = UseFormsBuilder.instance.buildNewForm(dTargetEntity, dRecord, fakeMainId, getUser()); + formModelDetails.add(m); + } + formModelDetailsMap.put(dTargetEntity.getName(), formModelDetails); + + } finally { + FormsBuilderContextHolder.getMainIdOfDetail(true); + } + + // 删除已有的 + if (targetExistsRecordId != null) { + List detailIds = QueryHelper.detailIdsNoFilter(targetExistsRecordId); + if (!detailIds.isEmpty()) { + formModelDetailsMap.put(dTargetEntity.getName() + "$DELETED", detailIds); + } + } + } + + ((JSONObject) formModel).put(GeneralEntityService.HAS_DETAILS, formModelDetailsMap); + return formModel; + } + + // 合并目标 + private void mergeExistsAndTarget(Record transTargetRecord) { + JSONObject fieldsMapping = transConfig.getJSONObject("fieldsMapping"); + Entity targetEntity = transTargetRecord.getEntity(); + + transTargetRecord.setObjectValue(targetEntity.getPrimaryField().getName(), targetExistsRecordId); + transTargetRecord.removeValue(EntityHelper.CreatedBy); + transTargetRecord.removeValue(EntityHelper.CreatedOn); + if (!fieldsMapping.containsKey(EntityHelper.OwningUser)) { + transTargetRecord.removeValue(EntityHelper.OwningUser); + transTargetRecord.removeValue(EntityHelper.OwningDept); + } + +// if (isPreview) { +// Record existsRecordSnap = Application.getQueryFactory().recordNoFilter(targetExistsRecordId); +// for (Field field : existsRecordSnap.getEntity().getFields()) { +// EasyField easyField = EasyMetaFactory.valueOf(field); +// if (MetadataHelper.isCommonsField(field) || easyField.getDisplayType() == DisplayType.SERIES) { +// if (field.getType() == FieldType.PRIMARY) continue; +// +// String fieldName = field.getName(); +// if (EntityHelper.AutoId.equals(fieldName) || EntityHelper.QuickCode.equals(fieldName)) continue; +// +// transTargetRecord.setObjectValue(fieldName, existsRecordSnap.getObjectValue(fieldName)); +// } +// } +// } + } + + // 获取主记录(如果是明细的话) + private ID forceGetSpecMainId(ID targetRecordId) { + Entity targetEntity = MetadataHelper.getEntity(targetRecordId.getEntityCode()); + if (targetEntity.getMainEntity() != null) { + return QueryHelper.getMainIdByDetail(targetRecordId); + } + return null; + } +} diff --git a/src/main/java/com/rebuild/core/service/general/transform/TransformerPreview.java b/src/main/java/com/rebuild/core/service/general/transform/TransformerPreview.java index ed2644797f..ecfe63bd8d 100644 --- a/src/main/java/com/rebuild/core/service/general/transform/TransformerPreview.java +++ b/src/main/java/com/rebuild/core/service/general/transform/TransformerPreview.java @@ -150,7 +150,10 @@ public JSON buildForm(String detailName) { } } - protected void fillLabelOfReference(Record record) { + /** + * @param record + */ + protected static void fillLabelOfReference(Record record) { Entity entity = record.getEntity(); for (String field : record.getAvailableFields()) { DisplayType dt = EasyMetaFactory.getDisplayType(entity.getField(field)); diff --git a/src/main/java/com/rebuild/core/service/general/transform/UseFormsBuilder.java b/src/main/java/com/rebuild/core/service/general/transform/UseFormsBuilder.java index 18ac29971f..95a19684ca 100644 --- a/src/main/java/com/rebuild/core/service/general/transform/UseFormsBuilder.java +++ b/src/main/java/com/rebuild/core/service/general/transform/UseFormsBuilder.java @@ -22,6 +22,8 @@ public class UseFormsBuilder extends FormsBuilder { protected static final UseFormsBuilder instance = new UseFormsBuilder(); /** + * 构建新表单,使用指定记录(数据) + * * @param entity * @param record * @param mainid @@ -29,7 +31,19 @@ public class UseFormsBuilder extends FormsBuilder { * @return */ public JSON buildNewForm(Entity entity, Record record, Object mainid, ID user) { - JSON model = buildForm(entity.getName(), user, null); + return buildForm(entity, record, mainid, user, false); + } + + /** + * @param entity + * @param record + * @param mainid + * @param user + * @param isNew + * @return + */ + public JSON buildForm(Entity entity, Record record, Object mainid, ID user, boolean isNew) { + JSON model = buildForm(entity.getName(), user, isNew ? null : record.getPrimary()); String hasError = ((JSONObject) model).getString("error"); if (hasError != null) throw new DataSpecificationException(hasError); @@ -52,10 +66,22 @@ public JSON buildNewForm(Entity entity, Record record, Object mainid, ID user) { * @return */ public static JSON buildNewFormWithRecord(Entity entity, Record record, ID mainid, ID user) { + return buildFormWithRecord(entity, record, mainid, user, true); + } + + /** + * @param entity + * @param record + * @param mainid 针对明细 + * @param user + * @param isNew + * @return + */ + public static JSON buildFormWithRecord(Entity entity, Record record, ID mainid, ID user, boolean isNew) { if (mainid != null) FormsBuilderContextHolder.setMainIdOfDetail(mainid); try { - return instance.buildNewForm(entity, record, mainid, user); + return instance.buildForm(entity, record, mainid, user, isNew); } finally { if (mainid != null) FormsBuilderContextHolder.getMainIdOfDetail(true); } diff --git a/src/main/java/com/rebuild/core/service/query/AdvFilterParser.java b/src/main/java/com/rebuild/core/service/query/AdvFilterParser.java index 1aafd8362e..ecf1b91347 100644 --- a/src/main/java/com/rebuild/core/service/query/AdvFilterParser.java +++ b/src/main/java/com/rebuild/core/service/query/AdvFilterParser.java @@ -278,6 +278,18 @@ private String parseItem(JSONObject item, JSONObject values, Entity specRootEnti && lastFieldMeta.getReferenceEntity().getEntityCode() == EntityHelper.User; String op = item.getString("op"); + // v3.9 区间兼容 + if (ParseHelper.BW.equals(op)) { + String valueBegin = item.getString("value"); + String valueEnd = item.getString("value2"); + if (valueBegin == null) { + op = ParseHelper.LE; + item.put("value", valueEnd); + } else if (valueEnd == null) { + op = ParseHelper.GE; + } + } + Object checkValue = useValueOfVarField(item.getString("value"), lastFieldMeta); if (checkValue instanceof VarFieldNoValue37) return "(1=2)"; String value = (String) checkValue; diff --git a/src/main/java/com/rebuild/core/service/trigger/RobotTriggerObserver.java b/src/main/java/com/rebuild/core/service/trigger/RobotTriggerObserver.java index ee3bacff35..84d11068c6 100644 --- a/src/main/java/com/rebuild/core/service/trigger/RobotTriggerObserver.java +++ b/src/main/java/com/rebuild/core/service/trigger/RobotTriggerObserver.java @@ -10,11 +10,16 @@ import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; +import com.rebuild.core.Application; +import com.rebuild.core.metadata.EntityHelper; +import com.rebuild.core.metadata.MetadataHelper; import com.rebuild.core.privileges.bizz.InternalPermission; import com.rebuild.core.service.SafeObservable; +import com.rebuild.core.service.approval.ApprovalState; import com.rebuild.core.service.general.OperatingContext; import com.rebuild.core.service.general.OperatingObserver; import com.rebuild.core.service.general.RepeatedRecordsException; +import com.rebuild.core.service.query.QueryHelper; import com.rebuild.core.service.trigger.impl.FieldAggregation; import com.rebuild.core.support.CommandArgs; import com.rebuild.core.support.CommonsLog; @@ -47,7 +52,7 @@ public class RobotTriggerObserver extends OperatingObserver { private static final ThreadLocal LAZY_TRIGGERS = new NamedThreadLocal<>("Lazy triggers"); private static final ThreadLocal> LAZY_TRIGGERS_CTX = new NamedThreadLocal<>("Lazy triggers ctx"); - private static final ThreadLocal ALLOW_TRIGGERS_ONAPPROVED = new NamedThreadLocal<>("Allow triggers on approve-node"); + private static final ThreadLocal ALLOW_TRIGGERS_ON_NODEAPPROVED = new NamedThreadLocal<>("Allow triggers on node-approve"); // 少量触发器日志 public static final boolean _TriggerLessLog = CommandArgs.getBoolean(CommandArgs._TriggerLessLog); @@ -170,12 +175,7 @@ protected void execAction(OperatingContext context, TriggerWhen when) { for (TriggerAction action : beExecuted) { // v3.7 审批节点触发 if (when == TriggerWhen.APPROVED) { - String hasIds = ALLOW_TRIGGERS_ONAPPROVED.get(); - if (hasIds != null) { - if (!hasIds.contains(action.actionContext.getConfigId().toString())) { - continue; - } - } + if (!allowWhenApproved(action, context)) continue; } // v3.7 指定字段通用化 if (when == TriggerWhen.UPDATE) { @@ -257,6 +257,35 @@ protected void execAction(OperatingContext context, TriggerWhen when) { } } + private boolean allowWhenApproved(TriggerAction action, OperatingContext context) { + // 节点触发 + String allowTriggers = ALLOW_TRIGGERS_ON_NODEAPPROVED.get(); + if (allowTriggers != null) { + return allowTriggers.contains(action.actionContext.getConfigId().toString()); + } + + // 最终触发 + final JSONArray whenApproveNodes = ((JSONObject) action.getActionContext().getActionContent()) + .getJSONArray("whenApproveNodes"); + // 无指定步骤 + if (whenApproveNodes == null || whenApproveNodes.isEmpty()) return true; + + ID approveRecordId = context.getFixedRecordId(); + if (MetadataHelper.isDetailEntity(approveRecordId.getEntityCode())) { + approveRecordId = QueryHelper.getMainIdByDetail(approveRecordId); + } + + Object[] state = Application.getQueryFactory().unique( + approveRecordId, EntityHelper.ApprovalState, EntityHelper.ApprovalStepNodeName); + int approvalState = (int) state[0]; + String nodeName = (String) state[1]; + if (approvalState == ApprovalState.APPROVED.getState()) { + return (whenApproveNodes.contains(nodeName) || whenApproveNodes.contains("*")); + } + + return true; + } + // -- /** @@ -311,15 +340,15 @@ public static int executeLazyTriggers(final SafeObservable o) { /** * 设置允许触发的触发器(ID) * - * @param triggerIds + * @param triggerIds eg. [xxx,xxx,xxx] */ - public static void setAllowTriggersOnApproved(String triggerIds) { - ALLOW_TRIGGERS_ONAPPROVED.set(triggerIds); + public static void setAllowTriggersOnNodeApproved(String triggerIds) { + ALLOW_TRIGGERS_ON_NODEAPPROVED.set(triggerIds); } /** */ - public static void clearAllowTriggersOnApproved() { - ALLOW_TRIGGERS_ONAPPROVED.remove(); + public static void clearAllowTriggersOnNodeApproved() { + ALLOW_TRIGGERS_ON_NODEAPPROVED.remove(); } } diff --git a/src/main/java/com/rebuild/core/service/trigger/impl/FieldAggregation.java b/src/main/java/com/rebuild/core/service/trigger/impl/FieldAggregation.java index d27ccb228c..554e29097c 100644 --- a/src/main/java/com/rebuild/core/service/trigger/impl/FieldAggregation.java +++ b/src/main/java/com/rebuild/core/service/trigger/impl/FieldAggregation.java @@ -12,6 +12,7 @@ import cn.devezhao.commons.ObjectUtils; import cn.devezhao.persist4j.Entity; import cn.devezhao.persist4j.Record; +import cn.devezhao.persist4j.dialect.FieldType; import cn.devezhao.persist4j.engine.ID; import cn.devezhao.persist4j.metadata.MissingMetaExcetion; import com.alibaba.fastjson.JSON; @@ -46,6 +47,7 @@ import java.util.Date; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -81,6 +83,8 @@ public class FieldAggregation extends TriggerAction { // 关联字段条件 protected String followSourceWhere; + transient private TargetWithMatchFields targetWithMatchFields; + public FieldAggregation(ActionContext context) { this(context, Boolean.TRUE); } @@ -271,11 +275,10 @@ public Object execute(OperatingContext operatingContext) throws TriggerException } if (operatingContext.getAction() == BizzPermission.UPDATE && this.getClass() == FieldAggregation.class) { - this.fieldAggregationRefresh = new FieldAggregationRefresh(this, operatingContext); + this.fieldAggregationRefresh = new FieldAggregationRefresh(this, operatingContext, targetWithMatchFields); } - // 回填 (v3.1) - // 仅分组聚合有此配置 + // 聚合后回填 (v3.1, 3.9) String fillbackField = ((JSONObject) actionContext.getActionContent()).getString("fillbackField"); if (fillbackField != null && MetadataHelper.checkAndWarnField(sourceEntity, fillbackField)) { String sql = String.format("select %s,%s from %s where %s", @@ -287,8 +290,12 @@ public Object execute(OperatingContext operatingContext) throws TriggerException // FIXME 回填仅更新,无业务规则 Record r = EntityHelper.forUpdate((ID) o[0], UserService.SYSTEM_USER, false); - r.setID(fillbackField, targetRecordId); - Application.getCommonsService().update(r, false); + if (sourceEntity.getField(fillbackField).getType() == FieldType.REFERENCE_LIST) { + r.setIDArray(fillbackField, new ID[]{targetRecordId}); + } else { + r.setID(fillbackField, targetRecordId); + } + Application.getCommonsService().getBaseService().update(r); } } @@ -299,6 +306,8 @@ public Object execute(OperatingContext operatingContext) throws TriggerException public void prepare(OperatingContext operatingContext) throws TriggerException { if (sourceEntity != null) return; // 已经初始化 + final JSONObject actionContent = (JSONObject) actionContext.getActionContent(); + // FIELD.ENTITY String[] targetFieldEntity = ((JSONObject) actionContext.getActionContent()).getString("targetEntity").split("\\."); sourceEntity = actionContext.getSourceEntity(); @@ -306,9 +315,12 @@ public void prepare(OperatingContext operatingContext) throws TriggerException { String followSourceField = targetFieldEntity[0]; if (TARGET_ANY.equals(followSourceField)) { - TargetWithMatchFields targetWithMatchFields = new TargetWithMatchFields(); + targetWithMatchFields = new TargetWithMatchFields(); targetRecordId = targetWithMatchFields.match(actionContext); - followSourceWhere = StringUtils.join(targetWithMatchFields.getQFieldsFollow().iterator(), " and "); + if (targetRecordId == null && actionContent.getBooleanValue("autoCreate")) { + targetRecordId = this.aotuCreateTargetRecord39(targetWithMatchFields); + } + followSourceWhere = StringUtils.join(targetWithMatchFields.getQFieldsFollow(), " and "); return; } @@ -352,6 +364,41 @@ protected boolean isCurrentSame(Record record) { return new RecordDifference(record).isSame(c, false); } + /** + * 自动创建目标记录 + * + * @param twmf + * @return + */ + protected ID aotuCreateTargetRecord39(TargetWithMatchFields twmf) { + if (twmf.getSourceRecord() == null) return null; + + Record newTargetRecord = EntityHelper.forNew(targetEntity.getEntityCode(), UserService.SYSTEM_USER); + for (Map.Entry e : twmf.getMatchFieldsMapping().entrySet()) { + String sourceField = e.getKey(); + String targetField = e.getValue(); + + Object val = twmf.getSourceRecord().getObjectValue(sourceField); + if (val != null) { + newTargetRecord.setObjectValue(targetField, val); + } + } + + // 不必担心必填字段,必填只是前端约束 + // 还可以通过设置字段默认值来完成必填字段的自动填写 + // 240425 需要业务规则,譬如自动编号、默认值等 + + GeneralEntityServiceContextHolder.setSkipGuard(EntityHelper.UNSAVED_ID); + try { + Application.getBestService(targetEntity).create(newTargetRecord); + } finally { + GeneralEntityServiceContextHolder.isSkipGuardOnce(); + } + return newTargetRecord.getPrimary(); + } + + // -- + /** * 清理触发链(在批处理时需要调用) * diff --git a/src/main/java/com/rebuild/core/service/trigger/impl/FieldAggregationRefresh.java b/src/main/java/com/rebuild/core/service/trigger/impl/FieldAggregationRefresh.java index 6b0f2e1517..1f3a22cfc9 100644 --- a/src/main/java/com/rebuild/core/service/trigger/impl/FieldAggregationRefresh.java +++ b/src/main/java/com/rebuild/core/service/trigger/impl/FieldAggregationRefresh.java @@ -8,15 +8,24 @@ package com.rebuild.core.service.trigger.impl; import cn.devezhao.bizz.privileges.impl.BizzPermission; +import cn.devezhao.persist4j.Entity; import cn.devezhao.persist4j.Record; import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSONObject; +import com.rebuild.core.Application; import com.rebuild.core.metadata.EntityHelper; import com.rebuild.core.privileges.UserService; import com.rebuild.core.service.general.OperatingContext; import com.rebuild.core.service.trigger.ActionContext; import com.rebuild.core.service.trigger.TriggerAction; +import com.rebuild.utils.CommonsUtils; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * @author RB @@ -28,10 +37,13 @@ public class FieldAggregationRefresh { final private FieldAggregation parent; final private OperatingContext operatingContext; + final private TargetWithMatchFields targetWithMatchFields; - protected FieldAggregationRefresh(FieldAggregation parent, OperatingContext operatingContext) { + protected FieldAggregationRefresh(FieldAggregation parent, OperatingContext operatingContext, + TargetWithMatchFields targetWithMatchFields) { this.parent = parent; this.operatingContext = operatingContext; + this.targetWithMatchFields = targetWithMatchFields; } /** @@ -47,7 +59,7 @@ public void refresh() { String followSourceField = targetFieldEntity[0]; if (TriggerAction.TARGET_ANY.equals(followSourceField)) { - log.debug("Use match-fields does not support refresh"); + refreshWithGroup(); return; } @@ -78,6 +90,80 @@ public void refresh() { } } + private void refreshWithGroup() { + final List qFieldsRefresh = targetWithMatchFields.getQFieldsRefresh(); + + List targetFields = new ArrayList<>(); + List targetWhere = new ArrayList<>(); + for (String[] s : qFieldsRefresh) { + targetFields.add(s[0]); + if (s[2] != null) { + targetWhere.add(String.format("%s = '%s'", s[0], CommonsUtils.escapeSql(s[2]))); + } + } + + // 只有1个字段会全量刷新,性能较低 + if (targetWhere.size() <= 1) { + targetWhere.clear(); + targetWhere.add("(1=1)"); + log.warn("Force refresh all aggregation target(s)"); + } + + // 1.获取待刷新的目标 + final Entity targetEntity = this.parent.targetEntity; + String sql = String.format("select %s,%s from %s where ( %s )", + StringUtils.join(targetFields, ","), + targetEntity.getPrimaryField().getName(), + targetEntity.getName(), + StringUtils.join(targetWhere, " or ")); + Object[][] targetRecords4Refresh = Application.createQueryNoFilter(sql).array(); + log.info("Maybe refresh target record(s) : {}", targetRecords4Refresh.length); + + final ID triggerUser = UserService.SYSTEM_USER; + final ActionContext parentAc = parent.getActionContext(); + + // 避免重复的无意义更新 + // NOTE 220905 更新时不能忽略触发源本身的更新 + Set refreshedIds = new HashSet<>(); + + // 2.逐一刷新目标 + for (Object[] o : targetRecords4Refresh) { + final ID targetRecordId = (ID) o[o.length - 1]; + // 排重 + if (refreshedIds.contains(targetRecordId)) continue; + else refreshedIds.add(targetRecordId); + + List qFieldsFollow = new ArrayList<>(); + for (int i = 0; i < o.length - 1; i++) { + String[] s = qFieldsRefresh.get(i); + if (o[i] == null) { + qFieldsFollow.add(String.format("%s is null", s[1])); + } else { + qFieldsFollow.add(String.format("%s = '%s'", s[1], CommonsUtils.escapeSql(o[i]))); + } + } + + ActionContext actionContext = new ActionContext(null, + parentAc.getSourceEntity(), parentAc.getActionContent(), parentAc.getConfigId()); + + FieldAggregation fa = new FieldAggregation(actionContext, true); + fa.sourceEntity = parent.sourceEntity; + fa.targetEntity = parent.targetEntity; + fa.targetRecordId = targetRecordId; + fa.followSourceWhere = StringUtils.join(qFieldsFollow, " and "); + + // FIXME v35 可能导致数据聚合条件中的字段变量不准 + Record fakeSourceRecord = EntityHelper.forUpdate(operatingContext.getFixedRecordId(), triggerUser, false); + OperatingContext oCtx = OperatingContext.create(triggerUser, BizzPermission.NONE, fakeSourceRecord, fakeSourceRecord); + + try { + fa.execute(oCtx); + } finally { + fa.clean(); + } + } + } + @Override public String toString() { return parent.toString() + "#Refresh"; diff --git a/src/main/java/com/rebuild/core/service/trigger/impl/FieldWriteback.java b/src/main/java/com/rebuild/core/service/trigger/impl/FieldWriteback.java index a8809aa637..7e1ac045e3 100644 --- a/src/main/java/com/rebuild/core/service/trigger/impl/FieldWriteback.java +++ b/src/main/java/com/rebuild/core/service/trigger/impl/FieldWriteback.java @@ -220,6 +220,8 @@ private Object execute38(OperatingContext operatingContext) throws TriggerExcept public void prepare(OperatingContext operatingContext) throws TriggerException { if (targetRecordIds != null) return; + final JSONObject actionContent = (JSONObject) actionContext.getActionContent(); + // FIELD.ENTITY String[] targetFieldEntity = ((JSONObject) actionContext.getActionContent()).getString("targetEntity").split("\\."); sourceEntity = actionContext.getSourceEntity(); @@ -234,6 +236,10 @@ public void prepare(OperatingContext operatingContext) throws TriggerException { if (TARGET_ANY.equals(targetFieldEntity[0])) { TargetWithMatchFields targetWithMatchFields = new TargetWithMatchFields(); ID[] ids = targetWithMatchFields.matchMultiple(actionContext); + if (ids.length == 0 && actionContent.getBooleanValue("autoCreate")) { + ID n = this.aotuCreateTargetRecord39(targetWithMatchFields); + targetRecordIds.add(n); + } CollectionUtils.addAll(targetRecordIds, ids); } // 自己更新自己 diff --git a/src/main/java/com/rebuild/core/service/trigger/impl/GroupAggregation.java b/src/main/java/com/rebuild/core/service/trigger/impl/GroupAggregation.java index a1a54c7a46..23d0099851 100644 --- a/src/main/java/com/rebuild/core/service/trigger/impl/GroupAggregation.java +++ b/src/main/java/com/rebuild/core/service/trigger/impl/GroupAggregation.java @@ -45,7 +45,9 @@ * * @author devezhao * @since 2021/6/28 + * @deprecated Use {@link FieldAggregation} */ +@Deprecated @Slf4j public class GroupAggregation extends FieldAggregation { diff --git a/src/main/java/com/rebuild/core/service/trigger/impl/GroupAggregationRefresh.java b/src/main/java/com/rebuild/core/service/trigger/impl/GroupAggregationRefresh.java index 9a6dcbdf5f..28afee58d4 100644 --- a/src/main/java/com/rebuild/core/service/trigger/impl/GroupAggregationRefresh.java +++ b/src/main/java/com/rebuild/core/service/trigger/impl/GroupAggregationRefresh.java @@ -30,14 +30,15 @@ * * 场景举例: * 1.1 新建记录:产品A + 仓库A分组(组合A+A) - * 1.2 修改记录:仓库A > B(组合B+A),此时原(组合A+A)纪录不会触发更新 - * 2. 因此需要通过强制更新原纪录刷新原组合(组合A+A)记录 + * 1.2 修改记录:仓库A > B(组合B+A),此时原(组合A+A)记录不会触发更新 + * 2. 因此需要通过强制更新原记录刷新原组合(组合A+A)记录 * 3. NOTE 如果组合值均为空,则无法匹配任何原目标记录,此时需要全量刷新(性能差)(可通过任一字段必填解决) * 4. NOTE 如果只有一个分组字段,因为无法获知之前的值,则需要全量刷新(性能差) * * @author RB * @since 2022/7/8 */ +@Deprecated @Slf4j public class GroupAggregationRefresh { diff --git a/src/main/java/com/rebuild/core/service/trigger/impl/TargetWithMatchFields.java b/src/main/java/com/rebuild/core/service/trigger/impl/TargetWithMatchFields.java index 74f0ee0741..0cbf454cc9 100644 --- a/src/main/java/com/rebuild/core/service/trigger/impl/TargetWithMatchFields.java +++ b/src/main/java/com/rebuild/core/service/trigger/impl/TargetWithMatchFields.java @@ -54,6 +54,11 @@ public class TargetWithMatchFields { @Getter private Object targetRecordId; + @Getter + private Record sourceRecord; + @Getter + private Map matchFieldsMapping; + public TargetWithMatchFields() { super(); } @@ -113,6 +118,7 @@ private Object match(ActionContext actionContext, boolean m) { log.warn("No match-fields specified"); return null; } + this.matchFieldsMapping = matchFieldsMapping; // 1.源记录数据 @@ -123,6 +129,7 @@ private Object match(ActionContext actionContext, boolean m) { final Record sourceRecord = Application.createQueryNoFilter(aSql) .setParameter(1, actionContext.getSourceRecord()) .record(); + this.sourceRecord = sourceRecord; // 2.找到目标记录 diff --git a/src/main/java/com/rebuild/core/support/CommandArgs.java b/src/main/java/com/rebuild/core/support/CommandArgs.java index d8697e8003..3f859f02d3 100644 --- a/src/main/java/com/rebuild/core/support/CommandArgs.java +++ b/src/main/java/com/rebuild/core/support/CommandArgs.java @@ -8,7 +8,15 @@ package com.rebuild.core.support; import cn.devezhao.commons.ObjectUtils; +import com.rebuild.core.BootEnvironmentPostProcessor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Properties; /** * 命令行保留参数 @@ -17,6 +25,7 @@ * @since 2023/2/4 * @see ConfigurationItem */ +@Slf4j public class CommandArgs { public static final String rbdev = "rbdev"; @@ -25,14 +34,28 @@ public class CommandArgs { public static final String _ForceTour = "_ForceTour"; public static final String _HeavyStopWatcher = "_HeavyStopWatcher"; public static final String _UniPush = "_UniPush"; - public static final String _UseDbFullText = "_UseDbFullText"; + public static final String _StartEntityTypeCode = "_StartEntityTypeCode"; + + /** + * 内部消息同步发送短信 + */ public static final String _SmsDistributor = "_SmsDistributor"; + /** + * 内部消息同步发送邮件 + */ public static final String _EmailDistributor = "_EmailDistributor"; - public static final String _StartEntityTypeCode = "_StartEntityTypeCode"; + /** + * FrontJS在所有页面生效 + */ public static final String _UseFrontJSAnywhere = "_UseFrontJSAnywhere"; + /** + * 触发器级联执行深度 + */ public static final String _TriggerMaxDepth = "_TriggerMaxDepth"; - public static final String _ProtectedAdmin = "_ProtectedAdmin"; + /** + * 更少的触发器日志输出 + */ public static final String _TriggerLessLog = "_TriggerLessLog"; /** @@ -40,7 +63,7 @@ public class CommandArgs { * @return default `false` */ public static boolean getBoolean(String name) { - return BooleanUtils.toBoolean(System.getProperty(name)); + return BooleanUtils.toBoolean(getProperty39(name)); } /** @@ -48,7 +71,7 @@ public static boolean getBoolean(String name) { * @return default `-1` */ public static int getInt(String name) { - return ObjectUtils.toInt(System.getProperty(name), -1); + return ObjectUtils.toInt(getProperty39(name), -1); } /** @@ -66,6 +89,41 @@ public static int getInt(String name, int defaultValue) { * @return */ public static String getString(String name) { + return getProperty39(name); + } + + /** + * @param name + * @return + */ + protected static String getStringWithBootEnvironmentPostProcessor(String name) { + String s = getProperty39(name); + if (StringUtils.isEmpty(s)) s = BootEnvironmentPostProcessor.getProperty(name); + return s; + } + + // -- + + // 引入外部配置文件 + private static Properties CONF39; + private static String getProperty39(String name) { + if (CONF39 == null) { + File rebuildConf = RebuildConfiguration.getFileOfData("rebuild.conf"); + if (rebuildConf.exists()) { + Properties conf = new Properties(); + try { + conf.load(Files.newInputStream(rebuildConf.toPath())); + CONF39 = conf; + log.info("Use RB conf file : {}", rebuildConf); + } catch (IOException e) { + log.warn("Cannot load `rebuild.conf` : {}", rebuildConf); + } + } + } + if (CONF39 == null) CONF39 = new Properties(); + + String s = CONF39.getProperty(name); + if (StringUtils.isNotBlank(s)) return s; return System.getProperty(name); } } diff --git a/src/main/java/com/rebuild/core/support/CommonsLog.java b/src/main/java/com/rebuild/core/support/CommonsLog.java index a28e602e84..6db30838e1 100644 --- a/src/main/java/com/rebuild/core/support/CommonsLog.java +++ b/src/main/java/com/rebuild/core/support/CommonsLog.java @@ -10,10 +10,12 @@ import cn.devezhao.commons.CalendarUtils; import cn.devezhao.persist4j.Record; import cn.devezhao.persist4j.engine.ID; +import com.alibaba.fastjson.JSON; import com.rebuild.core.Application; import com.rebuild.core.metadata.EntityHelper; import com.rebuild.core.support.task.TaskExecutors; import com.rebuild.utils.CommonsUtils; +import com.rebuild.utils.JSONUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; @@ -33,6 +35,7 @@ public class CommonsLog { public static final String TYPE_EXPORT = "EXPORT"; public static final String TYPE_ACCESS = "ACCESS"; public static final String TYPE_EXTFORM = "EXTFORM"; + public static final String TYPE_TRANSFORM = "TRANSFORM"; /** * @param type @@ -62,14 +65,29 @@ public static void createLog(String type, ID user, ID source, Throwable error) { * @param status */ public static void createLog(String type, ID user, ID source, String content, int status) { - Record clog = EntityHelper.forNew(EntityHelper.CommonsLog, user); - clog.setString("type", type); - clog.setID("user", user); - clog.setID("source", ObjectUtils.defaultIfNull(source, user)); - clog.setInt("status", status); - clog.setDate("logTime", CalendarUtils.now()); - if (content != null) clog.setString("logContent", CommonsUtils.maxstr(content, 32767)); + Record comLog = EntityHelper.forNew(EntityHelper.CommonsLog, user); + comLog.setString("type", type); + comLog.setID("user", user); + comLog.setID("source", ObjectUtils.defaultIfNull(source, user)); + comLog.setInt("status", status); + comLog.setDate("logTime", CalendarUtils.now()); + if (content != null) comLog.setString("logContent", CommonsUtils.maxstr(content, 32767)); - TaskExecutors.queue(() -> Application.getCommonsService().create(clog, false)); + TaskExecutors.queue(() -> Application.getCommonsService().create(comLog, false)); + } + + /** + * 记录转换日志 + * + * @param user + * @param sourceId + * @param targetId + * @param transformId + */ + public static void createTransformLog(ID user, ID sourceId, ID targetId, ID transformId) { + JSON content = JSONUtils.toJSONObject( + new String[]{"transform", "source"}, + new Object[]{transformId, sourceId}); + createLog(TYPE_TRANSFORM, user, targetId, content.toJSONString(), STATUS_OK); } } diff --git a/src/main/java/com/rebuild/core/support/ConfigurationItem.java b/src/main/java/com/rebuild/core/support/ConfigurationItem.java index f260938395..6fd2e0a7b4 100644 --- a/src/main/java/com/rebuild/core/support/ConfigurationItem.java +++ b/src/main/java/com/rebuild/core/support/ConfigurationItem.java @@ -108,6 +108,11 @@ SmsUser, SmsPassword, SmsSign(AppName), WxworkSyncUsers(false), WxworkSyncUsersRole, WxworkSyncUsersMatch("ID"), + // Feishu + FeishuAppId, FeishuAppSecret, + FeishuSyncUsers(false), + FeishuSyncUsersRole, + FeishuSyncUsersMatch("ID"), // PORTALs PortalBaiduMapAk, @@ -116,7 +121,12 @@ SmsUser, SmsPassword, SmsSign(AppName), MobileNavStyle(34), PageMourningMode(false), - // !!! 命令行适用 + /** + * @see com.rebuild.web.admin.ProtectedAdmin + */ + ProtectedAdmin, + + // !!! 命令行适用 or `rebuild.conf` DataDirectory, // 数据目录 RedisDatabase(0), // Redis DB MobileUrl, // 移动端地址 diff --git a/src/main/java/com/rebuild/core/support/KVStorage.java b/src/main/java/com/rebuild/core/support/KVStorage.java index 082d151a0f..1d44f56b55 100644 --- a/src/main/java/com/rebuild/core/support/KVStorage.java +++ b/src/main/java/com/rebuild/core/support/KVStorage.java @@ -22,6 +22,9 @@ import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; +import static com.rebuild.core.support.ConfigurationItem.DataDirectory; +import static com.rebuild.core.support.ConfigurationItem.RedisDatabase; + /** * K/V 对存储 * @@ -123,7 +126,10 @@ protected static String getValue(final String key, boolean noCache, Object defau if (ConfigurationItem.SN.name().equals(key)) { value = BootEnvironmentPostProcessor.getProperty(key); } else if (ConfigurationItem.inJvmArgs(key)) { - return BootEnvironmentPostProcessor.getProperty(key); + if (DataDirectory.name().equalsIgnoreCase(key) || RedisDatabase.name().equalsIgnoreCase(key)) { + return BootEnvironmentPostProcessor.getProperty(key); + } + return CommandArgs.getStringWithBootEnvironmentPostProcessor(key); } if (Application.isReady()) { diff --git a/src/main/java/com/rebuild/core/support/general/BarCodeSupport.java b/src/main/java/com/rebuild/core/support/general/BarCodeSupport.java index a3722a9508..a5a3f0d6ba 100644 --- a/src/main/java/com/rebuild/core/support/general/BarCodeSupport.java +++ b/src/main/java/com/rebuild/core/support/general/BarCodeSupport.java @@ -107,7 +107,7 @@ public static BufferedImage createQRCode(String content, int width) { * @return */ public static BufferedImage createBarCode(String content, int height, boolean showText) { - return createBarCode(content, 0, height, showText); + return createBarCode(content, 0, height, showText, BarcodeFormat.CODE_128); } /** @@ -119,12 +119,12 @@ public static BufferedImage createBarCode(String content, int height, boolean sh * @param showText 显示底部文字 * @return */ - public static BufferedImage createBarCode(String content, int width, int height, boolean showText) { + public static BufferedImage createBarCode(String content, int width, int height, boolean showText, BarcodeFormat specFormat) { BitMatrix bitMatrix; try { - bitMatrix = createBarCodeImage(content, BarcodeFormat.CODE_128, width, height); + bitMatrix = createBarCodeImage(content, specFormat, width, height); } catch (IllegalArgumentException ex) { - log.error("Cannot encode `{}` to CODE_128", content); + log.error("Cannot encode `{}` to {}", content, specFormat); content = CONTENT_ERROR; bitMatrix = createBarCodeImage(content, BarcodeFormat.CODE_128, width, height); @@ -169,8 +169,7 @@ protected static BitMatrix createBarCodeImage(String content, BarcodeFormat form // 条形码宽度为自适应 if (width == 0 && height > base) { - // 非整数倍数两边有白边 - width = (int) (((height + 0d) / base) * new Code128Writer().encode(content).length); + width = (int) (((height + 0d) / base * 1.51) * new Code128Writer().encode(content).length); } } diff --git a/src/main/java/com/rebuild/core/support/general/DataListBuilderImpl.java b/src/main/java/com/rebuild/core/support/general/DataListBuilderImpl.java index 5579d74473..95219757e2 100644 --- a/src/main/java/com/rebuild/core/support/general/DataListBuilderImpl.java +++ b/src/main/java/com/rebuild/core/support/general/DataListBuilderImpl.java @@ -102,7 +102,8 @@ public JSON getJSONResult() { if (stats2.size() > 1) { stats = new JSONArray(); for (int i = 1; i < stats2.size(); i++) { - stats.add(JSONUtils.toJSONObject(new String[] {"label", "value"}, stats2.get(i))); + stats.add(JSONUtils.toJSONObject( + new String[]{"label", "value", "color"}, stats2.get(i))); } } } @@ -127,7 +128,8 @@ public JSONArray getJSONStats() { JSONArray stats = new JSONArray(); for (int i = 1; i < stats2.size(); i++) { - stats.add(JSONUtils.toJSONObject(new String[] {"label", "value"}, stats2.get(i))); + stats.add(JSONUtils.toJSONObject( + new String[] {"label", "value", "color"}, stats2.get(i))); } return stats; } @@ -141,7 +143,7 @@ protected List getStats() { List stats = new ArrayList<>(); final Object[] count = Application.createQuery(queryParser.toCountSql(), user).unique(); - stats.add(new Object[] {null, count[0]}); + stats.add(new Object[]{null, count[0]}); if (count.length < 2) return stats; List> statsFields = queryParser.getCountFields(); @@ -172,7 +174,7 @@ protected List getStats() { value = FieldValueHelper.desensitized(easyField, value); } - stats.add(new Object[] {label, value}); + stats.add(new Object[]{label, value, c.get("color")}); } return stats; } diff --git a/src/main/java/com/rebuild/core/support/general/ProtocolFilterParser.java b/src/main/java/com/rebuild/core/support/general/ProtocolFilterParser.java index bc320288ae..0da31bf91d 100644 --- a/src/main/java/com/rebuild/core/support/general/ProtocolFilterParser.java +++ b/src/main/java/com/rebuild/core/support/general/ProtocolFilterParser.java @@ -33,8 +33,10 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * 解析已知的个性化过滤条件 @@ -171,16 +173,25 @@ public String parseRef(String content, String cascadingValue) { } // 父级级联字段 - if (hasFieldCascadingField(field) && ID.isId(cascadingValue)) { + ID[] cascadingValueIds = null; + if (hasFieldCascadingField(field) && StringUtils.isNotBlank(cascadingValue)) { + Set cascadingValueIdList = new HashSet<>(); + for (String s : cascadingValue.split("[,;]")) { + if (ID.isId(s)) cascadingValueIdList.add(ID.valueOf(s)); + } + if (!cascadingValueIdList.isEmpty()) cascadingValueIds = cascadingValueIdList.toArray(new ID[0]); + } + + if (cascadingValueIds != null) { // 可能同时存在父子级 String cascadingFieldParent = field.getExtraAttrs().getString("_cascadingFieldParent"); String cascadingFieldChild = field.getExtraAttrs().getString("_cascadingFieldChild"); // v35 多个使用第一个 if (cascadingFieldChild != null) cascadingFieldChild = cascadingFieldChild.split(";")[0]; - ID cascadingValueId = ID.valueOf(cascadingValue); List parentAndChind = new ArrayList<>(); + // 选子级时(只会有一个ID) if (StringUtils.isNotBlank(cascadingFieldParent)) { String[] fs = cascadingFieldParent.split(MetadataHelper.SPLITER_RE); Entity refEntity; @@ -192,11 +203,12 @@ public String parseRef(String content, String cascadingValue) { refEntity = entity.getField(fs[0]).getReferenceEntity(); } - if (refEntity.getEntityCode().equals(cascadingValueId.getEntityCode())) { - parentAndChind.add(String.format("%s = '%s'", fs[1], cascadingValueId)); + if (refEntity.getEntityCode().equals(cascadingValueIds[0].getEntityCode())) { + parentAndChind.add(String.format("%s = '%s'", fs[1], cascadingValueIds[0])); } } - + + // 选父级时(会有多个ID) if (StringUtils.isNotBlank(cascadingFieldChild)) { String[] fs = cascadingFieldChild.split(MetadataHelper.SPLITER_RE); Entity refEntity; @@ -208,11 +220,12 @@ public String parseRef(String content, String cascadingValue) { refEntity = entity.getField(fs[0]).getReferenceEntity(); } - if (refEntity.getEntityCode().equals(cascadingValueId.getEntityCode())) { - String s = String.format("exists (select %s from %s where ^%s = %s and %s = '%s')", + if (refEntity.getEntityCode().equals(cascadingValueIds[0].getEntityCode())) { + String ps = String.format("%s in ('%s')", + refEntity.getPrimaryField().getName(), StringUtils.join(cascadingValueIds, "','")); + String s = String.format("exists (select %s from %s where ^%s = %s and ( %s ))", fs[1], refEntity.getName(), - field.getReferenceEntity().getPrimaryField().getName(), fs[1], - refEntity.getPrimaryField().getName(), cascadingValueId); + field.getReferenceEntity().getPrimaryField().getName(), fs[1], ps); parentAndChind.add(s); } } diff --git a/src/main/java/com/rebuild/core/support/general/QueryParser.java b/src/main/java/com/rebuild/core/support/general/QueryParser.java index d726bcc2a1..cb7055ea30 100644 --- a/src/main/java/com/rebuild/core/support/general/QueryParser.java +++ b/src/main/java/com/rebuild/core/support/general/QueryParser.java @@ -19,6 +19,7 @@ import com.rebuild.core.privileges.UserService; import com.rebuild.core.service.query.AdvFilterParser; import com.rebuild.core.service.query.ParseHelper; +import com.rebuild.utils.CommonsUtils; import lombok.Getter; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; @@ -190,7 +191,10 @@ private void doParseIfNeed() { String protocolFilter = queryExpr.getString("protocolFilter"); if (StringUtils.isNotBlank(protocolFilter)) { String where = new ProtocolFilterParser(protocolFilter).toSqlWhere(); - if (StringUtils.isNotBlank(where)) wheres.add(where); + if (StringUtils.isNotBlank(where)) { + if (CommonsUtils.DEVLOG) System.out.println("[dev] Parse protocolFilter : " + protocolFilter + " >> " + where); + wheres.add(where); + } } // append: AdvFilter diff --git a/src/main/java/com/rebuild/core/support/integration/QiniuCloud.java b/src/main/java/com/rebuild/core/support/integration/QiniuCloud.java index 3c52bd3e63..362d0cbe94 100644 --- a/src/main/java/com/rebuild/core/support/integration/QiniuCloud.java +++ b/src/main/java/com/rebuild/core/support/integration/QiniuCloud.java @@ -311,16 +311,17 @@ public void download(String filePath, File dest) throws IOException { * @see #parseFileName(String) */ public static String formatFileKey(String fileName) { - return formatFileKey(fileName, true); + return formatFileKey(fileName, true, null); } /** * @param fileName * @param keepName + * @param updir * @return * @see #parseFileName(String) */ - public static String formatFileKey(String fileName, boolean keepName) { + public static String formatFileKey(String fileName, boolean keepName, String updir) { if (keepName) { while (fileName.contains("__")) { fileName = fileName.replace("__", "_"); @@ -340,9 +341,13 @@ public static String formatFileKey(String fileName, boolean keepName) { if (StringUtils.isNotBlank(fileExt)) fileName += "." + fileExt; } - String datetime = CalendarUtils.getDateFormat("yyyyMMddHHmmssSSS").format(CalendarUtils.now()); - fileName = String.format("rb/%s/%s__%s", datetime.substring(0, 8), datetime.substring(8), fileName); - return fileName; + String dt = CalendarUtils.getDateFormat("yyyyMMddHHmmssSSS").format(CalendarUtils.now()); + String subdir = dt.substring(0, 8); + String filePrefix = dt.substring(8); + // remove unsafe /\. + if (StringUtils.isNotBlank(updir)) subdir = updir.replaceAll("[./\\\\\\s]", ""); + + return String.format("rb/%s/%s__%s", subdir, filePrefix, fileName); } /** diff --git a/src/main/java/com/rebuild/utils/AppUtils.java b/src/main/java/com/rebuild/utils/AppUtils.java index f82918682b..3a65ca9ad3 100644 --- a/src/main/java/com/rebuild/utils/AppUtils.java +++ b/src/main/java/com/rebuild/utils/AppUtils.java @@ -97,7 +97,7 @@ public static ID getRequestUser(HttpServletRequest request, boolean refreshToken try { user = request.getSession().getAttribute(WebUtils.CURRENT_USER); } catch (Exception resHasBeenCommitted) { - log.warn("resHasBeenCommitted", resHasBeenCommitted); + log.debug("resHasBeenCommitted", resHasBeenCommitted); } if (user == null) user = getRequestUserViaToken(request, refreshToken); @@ -114,7 +114,7 @@ public static ID getRequestUser(HttpServletRequest request, boolean refreshToken protected static ID getRequestUserViaToken(HttpServletRequest request, boolean refreshToken) { String authToken = request.getHeader(HF_AUTHTOKEN); return authToken == null - ? null : AuthTokenManager.verifyToken(authToken, Boolean.FALSE, refreshToken); + ? null : AuthTokenManager.verifyToken(authToken, false, refreshToken); } /** diff --git a/src/main/java/com/rebuild/utils/RbAssert.java b/src/main/java/com/rebuild/utils/RbAssert.java index 653df80dc4..e6eb73c5f8 100644 --- a/src/main/java/com/rebuild/utils/RbAssert.java +++ b/src/main/java/com/rebuild/utils/RbAssert.java @@ -7,7 +7,9 @@ package com.rebuild.utils; +import cn.devezhao.persist4j.engine.ID; import com.rebuild.core.DefinedException; +import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.support.License; import com.rebuild.core.support.NeedRbvException; import com.rebuild.core.support.i18n.Language; @@ -26,7 +28,14 @@ public static void isCommercial(String message) { if (message == null) message = Language.L("免费版不支持此功能"); throw new NeedRbvException(message); } - + + /** + * @param user + */ + public static void isSuperAdmin(ID user) { + is(UserHelper.isSuperAdmin(user), Language.L("非超级管理员用户")); + } + /** * @param expression * @param message diff --git a/src/main/java/com/rebuild/web/OnlineSessionStore.java b/src/main/java/com/rebuild/web/OnlineSessionStore.java index c237602c91..64f73c0329 100644 --- a/src/main/java/com/rebuild/web/OnlineSessionStore.java +++ b/src/main/java/com/rebuild/web/OnlineSessionStore.java @@ -13,17 +13,18 @@ import com.rebuild.api.user.AuthTokenManager; import com.rebuild.core.support.ConfigurationItem; import com.rebuild.core.support.RebuildConfiguration; +import com.rebuild.utils.AppUtils; +import com.rebuild.web.user.signup.LoginChannel; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; -import org.springframework.util.Assert; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; -import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -40,16 +41,10 @@ public class OnlineSessionStore implements HttpSessionListener { private static final Set ONLINE_SESSIONS = new CopyOnWriteArraySet<>(); - private static final Map ONLINE_USERS = new ConcurrentHashMap<>(); - // v3.8 - private static final Map ONLINE_USERS_H5 = new ConcurrentHashMap<>(); + private static final Map ONLINE_USERS = new ConcurrentHashMap<>(); + private static final Map ACTIVE_INFOS39 = new ConcurrentHashMap<>(); - /** - * 最近访问 [时间, 路径] - * - * @see #storeLastActive(HttpServletRequest) - */ - public static final String SK_LASTACTIVE = WebUtils.KEY_PREFIX + "Session-LastActive"; + private static final String SK_LOGINCHANNEL = WebUtils.KEY_PREFIX + "Session-LoginChannel"; @Override public void sessionCreated(HttpSessionEvent event) { @@ -65,152 +60,151 @@ public void sessionDestroyed(HttpSessionEvent event) { HttpSession s = event.getSession(); ONLINE_SESSIONS.remove(s); - for (Map.Entry e : ONLINE_USERS.entrySet()) { + for (Map.Entry e : ONLINE_USERS.entrySet()) { if (s.equals(e.getValue())) { ONLINE_USERS.remove(e.getKey()); + ACTIVE_INFOS39.remove(e.getKey()); break; } } } /** - * 所有会话 + * 用户会话 * + * @param user + * @param loginChannel * @return */ - public Set getAllSession() { - Set all = new HashSet<>(); - all.addAll(ONLINE_SESSIONS); - all.addAll(ONLINE_USERS.values()); - return Collections.unmodifiableSet(all); + public HttpSession getSession(ID user, LoginChannel loginChannel) { + String key = user + ";" + loginChannel.name(); + return ONLINE_USERS.get(key); } /** - * 用户会话 - * - * @param user * @return */ - public HttpSession getSession(ID user) { - return ONLINE_USERS.get(user); + public static Set getAllSessions() { + return Collections.unmodifiableSet(ONLINE_SESSIONS); } /** + * @return + */ + public Map getAllActiveInfos() { + return Collections.unmodifiableMap(ACTIVE_INFOS39); + } + + /** + * 最近活跃 + * + * @param user * @param request + * @param requestUri */ - public void storeLastActive(HttpServletRequest request) { - final String requestUri = request.getRequestURI(); - if (requestUri.contains("/filex/access/")) { + public void storeLastActive(ID user, HttpServletRequest request, String requestUri) { + if (requestUri == null) requestUri = request.getRequestURI(); + if (requestUri.contains("/filex/access/") + || requestUri.contains("/notification/check-state")) { return; } HttpSession s = request.getSession(); - s.setAttribute(SK_LASTACTIVE, - new Object[]{System.currentTimeMillis(), requestUri, ServletUtils.getRemoteAddr(request)}); + String loginChannel = (String) s.getAttribute(SK_LOGINCHANNEL); + // 手机版 + if (loginChannel == null) { + loginChannel = LoginChannel.parse(request.getHeader("user-agent"), true).name(); + s.setAttribute(SK_LOGINCHANNEL, loginChannel); + } + String key = user + ";" + loginChannel; + ACTIVE_INFOS39.put(key, new ActiveInfo(user, request, requestUri)); } /** + * @param user * @param request - * @param h5NoKill + * @param loginChannel */ - public void storeLoginSuccessed(HttpServletRequest request, boolean h5NoKill) { + public void storeLoginSuccessed(ID user, HttpServletRequest request, LoginChannel loginChannel) { HttpSession s = request.getSession(); - Object loginUser = s.getAttribute(WebUtils.CURRENT_USER); - Assert.notNull(loginUser, "No login user found in session!"); + s.setAttribute(SK_LOGINCHANNEL, loginChannel.name()); + final String key = user + ";" + loginChannel.name(); + // 不允许多用户登录 if (!RebuildConfiguration.getBool(ConfigurationItem.MultipleSessions)) { - HttpSession previous = h5NoKill ? null : getSession((ID) loginUser); + HttpSession previous = getSession(user, loginChannel); if (previous != null) { - log.warn("Kill previous session : {} ({})", previous.getId(), loginUser); + log.warn("Kill previous session : {}, {}, {}", previous.getId(), user, loginChannel); try { previous.invalidate(); - } catch (Throwable ignored) { - } + } catch (Throwable ignored) {} } } - ONLINE_USERS.put((ID) loginUser, s); + ONLINE_USERS.put(key, s); } /** - * @param sessionOrUser - * @return + * @param sessionOrTokenOrUser */ - public boolean killSession(Object sessionOrUser) { - HttpSession found = null; - // User - if (sessionOrUser instanceof ID) { - found = getSession((ID) sessionOrUser); - } - // SessionID - else { - for (HttpSession s : getAllSession()) { - if (s.getId().equals(sessionOrUser)) { - found = s; - break; - } + public void killSession(Object sessionOrTokenOrUser) { + // 用户 killall + if (sessionOrTokenOrUser instanceof ID) { + for (LoginChannel ch : LoginChannel.values()) { + HttpSession s = getSession((ID) sessionOrTokenOrUser, ch); + if (s != null) killSession(s.getId()); } + return; + } - // for H5 - if (found == null) { - String token = sessionOrUser.toString(); - ID d = AuthTokenManager.verifyToken(token, true, false); - if (d != null) { - ONLINE_USERS_H5.remove(token); - return true; + // SessionId or AuthToken + for (Map.Entry e : getAllActiveInfos().entrySet()) { + String key = e.getKey(); + ActiveInfo info = e.getValue(); + + // H5 + if (sessionOrTokenOrUser.equals(info.getAuthToken())) { + AuthTokenManager.verifyToken((String) sessionOrTokenOrUser, true, false); + ACTIVE_INFOS39.remove(key); + log.warn("Kill session : {}, {}", sessionOrTokenOrUser, key); + break; + } + // PC + if (sessionOrTokenOrUser.equals(info.getSessionId())) { + for (HttpSession s : ONLINE_USERS.values()) { + if (sessionOrTokenOrUser.equals(s.getId())) { + s.invalidate(); + ACTIVE_INFOS39.remove(key); + log.warn("Kill session : {}, {}", sessionOrTokenOrUser, key); + break; + } } - return false; } } - - try { - if (found != null) { - found.invalidate(); - log.warn("Kill session with {}", sessionOrUser); - } - } catch (Exception ignored) {} - - return found != null; - } - - // for H5/Mobile - - /** - * @param authToken - * @param user - * @param activeUrl - * @param request - */ - public void storeH5LastActive(String authToken, ID user, String activeUrl, HttpServletRequest request) { - ONLINE_USERS_H5.put(authToken, - new Object[]{System.currentTimeMillis(), activeUrl, ServletUtils.getRemoteAddr(request), authToken, user}); } - /** - * @param user - * @return - */ - public String getH5Session(ID user) { - for (Object[] s : ONLINE_USERS_H5.values()) { - if (user.equals(s[4])) return (String) s[3]; - } - return null; - } + // -- /** - * @param clearInvalid - * @return */ - public Collection getAllH5Session(boolean clearInvalid) { - if (clearInvalid) { - for (String token : ONLINE_USERS_H5.keySet()) { - ID valid = AuthTokenManager.verifyToken(token); - if (valid == null) ONLINE_USERS_H5.remove(token); - } - log.info("Clean H5 sessions. Current valid : {}", ONLINE_USERS_H5.size()); + @Getter + public static class ActiveInfo { + + long activeTime; + String activeUri; + String activeIp; + ID user; + String sessionId; // PC + String authToken; // H5 + + protected ActiveInfo(ID user, HttpServletRequest request, String requestUri) { + this.activeTime = System.currentTimeMillis(); + this.activeUri = StringUtils.defaultIfBlank(requestUri, request.getRequestURI()); + this.activeIp = ServletUtils.getRemoteAddr(request); + this.user = user; + this.sessionId = request.getSession().getId(); + this.authToken = request.getHeader(AppUtils.HF_AUTHTOKEN); } - - return ONLINE_USERS_H5.values(); } } diff --git a/src/main/java/com/rebuild/web/RebuildWebInterceptor.java b/src/main/java/com/rebuild/web/RebuildWebInterceptor.java index fe219be44f..a4ca03b5da 100644 --- a/src/main/java/com/rebuild/web/RebuildWebInterceptor.java +++ b/src/main/java/com/rebuild/web/RebuildWebInterceptor.java @@ -17,6 +17,7 @@ import com.rebuild.core.UserContextHolder; import com.rebuild.core.cache.CommonsCache; import com.rebuild.core.privileges.UserHelper; +import com.rebuild.core.privileges.UserService; import com.rebuild.core.privileges.bizz.ZeroEntry; import com.rebuild.core.support.CommonsLog; import com.rebuild.core.support.ConfigurationItem; @@ -130,6 +131,9 @@ else if (!(requestUri.contains("/setup/") || requestUri.contains("/commons/theme } return false; } + + // v3.9 + if (UserService.ADMIN_USER.equals(requestUser)) request.setAttribute("IsSuperAdmin", true); } UserContextHolder.setUser(requestUser); @@ -141,7 +145,7 @@ else if (!(requestUri.contains("/setup/") || requestUri.contains("/commons/theme if (isHtmlRequest(requestUri, request)) { // Last active - Application.getSessionStore().storeLastActive(request); + Application.getSessionStore().storeLastActive(requestUser, request, null); // Nav collapsed String sidebarCollapsed = ServletUtils.readCookie(request, "rb.sidebarCollapsed"); @@ -191,10 +195,6 @@ else if (!(requestUri.contains("/setup/") || requestUri.contains("/commons/theme @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { - // v3.6 H5 session 时间 - if (AppUtils.isRbMobile(request)) { - request.getSession().setMaxInactiveInterval(60 * 5); - } } @Override diff --git a/src/main/java/com/rebuild/web/admin/AdminCli3.java b/src/main/java/com/rebuild/web/admin/AdminCli3.java index 2df97ccfe1..bc3957c32a 100644 --- a/src/main/java/com/rebuild/web/admin/AdminCli3.java +++ b/src/main/java/com/rebuild/web/admin/AdminCli3.java @@ -10,13 +10,16 @@ import com.rebuild.core.Application; import com.rebuild.core.metadata.MetadataHelper; import com.rebuild.core.metadata.impl.TsetEntity; +import com.rebuild.core.rbstore.RbSystemImporter; import com.rebuild.core.service.approval.ApprovalFields2Schema; import com.rebuild.core.support.ConfigurationItem; import com.rebuild.core.support.RebuildConfiguration; import com.rebuild.core.support.setup.DatabaseBackup; import com.rebuild.core.support.setup.DatafileBackup; import com.rebuild.core.support.setup.Installer; +import com.rebuild.core.support.task.TaskExecutors; import com.rebuild.utils.AES; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import java.io.File; @@ -29,6 +32,7 @@ * @author devezhao * @since 2020/3/16 */ +@Slf4j public class AdminCli3 { private static final String C_HELP = "help"; @@ -38,6 +42,7 @@ public class AdminCli3 { private static final String C_AES = "aes"; private static final String C_CLEAN_APPROVAL = "clean-approval"; private static final String C_ADD_TESTENTITY = "add-testentity"; + private static final String C_RBSPKG = "rbspkg"; private static final String SUCCESS = "OK"; @@ -72,11 +77,12 @@ public String exec() { result = " Usage : " + " \ncache [clean|get] [KEY]" + " \nsyscfg NAME [VALUE]" + - " \nsyscfg clean-qiniu|clean-sms|clean-email|clean-wxwork|clean-dingtalk" + + " \nsyscfg clean-qiniu|clean-sms|clean-email|clean-wxwork|clean-dingtalk|clean-feishu" + " \nbackup [database|datafile]" + " \naes [decrypt] VALUE" + " \nclean-approval ENTITY" + - " \nadd-testentity [NAME]"; + " \nadd-testentity [NAME]" + + " \nrbspkg URL"; break; } case C_CACHE: { @@ -103,6 +109,10 @@ public String exec() { result = this.execAddTestentity(); break; } + case C_RBSPKG: { + result = this.execRbspkg(); + break; + } default: { // NOOP } @@ -163,6 +173,9 @@ protected String execSyscfg() { } else if ("clean-dingtalk".equals(name)) { removeItems("Dingtalk"); return SUCCESS; + } else if ("clean-feishu".equals(name)) { + removeItems("Feishu"); + return SUCCESS; } ConfigurationItem item = ConfigurationItem.valueOf(name); @@ -258,4 +271,21 @@ private String execAddTestentity() { if (entityName.startsWith("EXISTS:")) return "WRAN: " + entityName; else return "OK: " + entityName; } + + /** + * @return + */ + private String execRbspkg() { + if (commands.length < 2) return "WRAN: Bad arguments"; + + String fileUrl = commands[1]; + RbSystemImporter importer = new RbSystemImporter(fileUrl); + try { + TaskExecutors.run(importer); + return "OK"; + } catch (Exception ex) { + log.error("RBSPKG", ex); + return "ERROR: " + ex.getLocalizedMessage(); + } + } } diff --git a/src/main/java/com/rebuild/web/admin/AdminVerfiyController.java b/src/main/java/com/rebuild/web/admin/AdminVerfiyController.java index f746ff4303..380f7bab6e 100644 --- a/src/main/java/com/rebuild/web/admin/AdminVerfiyController.java +++ b/src/main/java/com/rebuild/web/admin/AdminVerfiyController.java @@ -15,7 +15,6 @@ import com.rebuild.api.RespBody; import com.rebuild.core.Application; import com.rebuild.core.privileges.UserHelper; -import com.rebuild.core.privileges.UserService; import com.rebuild.core.privileges.bizz.User; import com.rebuild.core.support.SysbaseHeartbeat; import com.rebuild.core.support.i18n.Language; @@ -93,13 +92,13 @@ public RespBody adminDangers() { @GetMapping("/admin/admin-cli") public ModelAndView adminCliConsole(HttpServletRequest request) { - RbAssert.isAllow(UserService.ADMIN_USER.equals(getRequestUser(request)), "ONLY FOR SUPER ADMIN"); + RbAssert.isSuperAdmin(getRequestUser(request)); return createModelAndView("/admin/admin-cli"); } @RequestMapping("/admin/admin-cli/exec") public RespBody adminCliExec(HttpServletRequest request) { - RbAssert.isAllow(UserService.ADMIN_USER.equals(getRequestUser(request)), "ONLY FOR SUPER ADMIN"); + RbAssert.isSuperAdmin(getRequestUser(request)); String command = ServletUtils.getRequestString(request); if (StringUtils.isBlank(command)) return RespBody.error(); diff --git a/src/main/java/com/rebuild/web/admin/ConfigurationController.java b/src/main/java/com/rebuild/web/admin/ConfigurationController.java index 9fa46da2bd..2936f7d960 100644 --- a/src/main/java/com/rebuild/web/admin/ConfigurationController.java +++ b/src/main/java/com/rebuild/web/admin/ConfigurationController.java @@ -27,6 +27,7 @@ import com.rebuild.core.support.i18n.Language; import com.rebuild.core.support.integration.QiniuCloud; import com.rebuild.core.support.integration.SMSender; +import com.rebuild.rbv.integration.feishu.FeishuSdk; import com.rebuild.utils.CommonsUtils; import com.rebuild.utils.JSONUtils; import com.rebuild.utils.RbAssert; @@ -376,6 +377,43 @@ public RespBody postIntegrationWxwork(@RequestBody JSONObject data) { return RespBody.ok(); } + // Fesihu + + @GetMapping("integration/feishu") + public ModelAndView pageIntegrationFeishu() { + RbAssert.isCommercial( + Language.L("免费版不支持飞书集成 [(查看详情)](https://getrebuild.com/docs/rbv-features)")); + + ModelAndView mv = createModelAndView("/admin/integration/feishu"); + for (ConfigurationItem item : ConfigurationItem.values()) { + String name = item.name(); + if (name.startsWith("Feishu")) { + String value = RebuildConfiguration.get(item); + + if (value != null && item == ConfigurationItem.FeishuAppSecret) { + value = DataDesensitized.any(value); + } + mv.getModel().put(name, value); + + if (ID.isId(value) && item == ConfigurationItem.FeishuSyncUsersRole) { + mv.getModel().put(name + "Label", UserHelper.getName(ID.valueOf(value))); + } + } + } + + String homeUrl = RebuildConfiguration.getHomeUrl("/user/feishu"); + mv.getModel().put("_FeishuHomeUrl", homeUrl); + String[] homeUrls = homeUrl.split("//"); + mv.getModel().put("_FeishuAuthUrl", homeUrls[0] + "//" + homeUrls[1].split("/")[0]); + return mv; + } + + @PostMapping("integration/feishu") + public RespBody postIntegrationFeishu(@RequestBody JSONObject data) { + setValues(data); + return RespBody.ok(); + } + // -- private String[] starsAccount(String[] account, int... index) { diff --git a/src/main/java/com/rebuild/web/admin/ProtectedAdmin.java b/src/main/java/com/rebuild/web/admin/ProtectedAdmin.java index 21fa980d54..5ef8ea04bd 100644 --- a/src/main/java/com/rebuild/web/admin/ProtectedAdmin.java +++ b/src/main/java/com/rebuild/web/admin/ProtectedAdmin.java @@ -10,9 +10,11 @@ import cn.devezhao.persist4j.engine.ID; import com.rebuild.core.Application; import com.rebuild.core.privileges.UserService; -import com.rebuild.core.support.CommandArgs; +import com.rebuild.core.support.ConfigurationItem; import com.rebuild.core.support.License; +import com.rebuild.core.support.RebuildConfiguration; import com.rebuild.utils.AppUtils; +import lombok.Getter; import org.apache.commons.lang3.StringUtils; import javax.servlet.http.HttpServletRequest; @@ -49,7 +51,7 @@ public static boolean allow(String uriOrKey, ID adminUser) { if (UserService.ADMIN_USER.equals(adminUser)) return true; if (PA == null) { - String vv = CommandArgs.getString(CommandArgs._ProtectedAdmin); + String vv = RebuildConfiguration.get(ConfigurationItem.ProtectedAdmin); if (StringUtils.isBlank(vv)) PA = new PaEntry[0]; else { List pp = new ArrayList<>(); @@ -67,52 +69,48 @@ public static boolean allow(String uriOrKey, ID adminUser) { return false; } - // 功能 - enum PaEntry { - // 通用配置 - SYS("/systems"), - // 服务集成* - SSI("/integration/"), - // API - API("/apis-manager"), - // 实体管理* (含分类) - ENT("/entities;/entity/;/metadata/"), - // 审批流程 - APR("/robot/approval"), - // 记录转换 - TRA("/robot/transform"), - // 触发器 - TRI("/robot/trigger"), - // 业务进度 - SOP("/robot/sop"), - // 报表设计 - REP("/data/report-template"), - // 数据导入 - IMP("/data/data-imports"), - // 外部表单 - EXF("/extform"), - // 项目 - PRO("/project"), - // FrontJS - FJS("/frontjs-code"), - // 用户* - USR("/bizuser/users;bizuser/departments"), - // 角色 - ROL("/bizuser/role-privileges"), - // 团队 - TEM("/bizuser/teams"), - // 登录日志 - LLG("/audit/login-logs"), - // 变更历史 - REV("/audit/revision-history"), - // 回收站 - RCY("/audit/recycle-bin"), + /** + * 缓存清理 + */ + public static void clean() { + // unsafe + PA = null; + } + + // -- + + /** + * 功能列表 + */ + public enum PaEntry { + SYS("/systems", "通用配置"), + SSI("/integration/", "服务集成"), + API("/apis-manager", "API 秘钥"), + ENT("/entities;/entity/;/metadata/", "实体管理"), + APR("/robot/approval", "审批流程"), + TRA("/robot/transform", "记录转换"), + TRI("/robot/trigger", "触发器"), + SOP("/robot/sop", "业务进度"), + REP("/data/report-template", "报表设计"), + IMP("/data/data-imports", "数据导入"), + EXF("/extform", "外部表单"), + PRO("/project", "项目"), + FJS("/frontjs-code", "FrontJS"), + USR("/bizuser/users;bizuser/departments", "部门用户"), + ROL("/bizuser/role-privileges", "角色权限"), + TEM("/bizuser/teams", "团队"), + LLG("/audit/login-logs", "登录日志"), + REV("/audit/revision-history", "变更历史"), + RCY("/audit/recycle-bin", "回收站"), ; final private String[] paths; - PaEntry(String paths) { + @Getter + final private String name; + PaEntry(String paths, String name) { this.paths = paths.split(";"); + this.name = name; } /** diff --git a/src/main/java/com/rebuild/web/admin/audit/LoginLogController.java b/src/main/java/com/rebuild/web/admin/audit/LoginLogController.java index 172bd77870..9834890d51 100644 --- a/src/main/java/com/rebuild/web/admin/audit/LoginLogController.java +++ b/src/main/java/com/rebuild/web/admin/audit/LoginLogController.java @@ -7,7 +7,6 @@ package com.rebuild.web.admin.audit; -import cn.devezhao.commons.web.WebUtils; import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; @@ -17,11 +16,11 @@ import com.rebuild.core.configuration.general.DataListManager; import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.support.i18n.I18nUtils; -import com.rebuild.core.support.i18n.Language; import com.rebuild.utils.JSONUtils; import com.rebuild.utils.LocationUtils; import com.rebuild.web.EntityController; import com.rebuild.web.OnlineSessionStore; +import com.rebuild.web.user.signup.LoginChannel; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -29,8 +28,8 @@ import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; import java.util.Date; +import java.util.Map; /** * @author devezhao-mac zhaofang123@gmail.com @@ -54,32 +53,22 @@ public JSON getOnlineUsers(HttpServletRequest request) { final String currentSid = request.getSession().getId(); JSONArray online = new JSONArray(); - for (HttpSession s : Application.getSessionStore().getAllSession()) { - ID user = (ID) s.getAttribute(WebUtils.CURRENT_USER); - if (user == null) continue; - Object[] active = (Object[]) s.getAttribute(OnlineSessionStore.SK_LASTACTIVE); - if (active == null) continue; + for (Map.Entry e + : Application.getSessionStore().getAllActiveInfos().entrySet()) { + String[] keySplit = e.getKey().split(";"); + OnlineSessionStore.ActiveInfo info = e.getValue(); - String fullName = UserHelper.getName(user); - if (currentSid.equals(s.getId())) fullName += " [" + Language.L("当前") + "]"; + ID user = ID.valueOf(keySplit[0]); + String usName = UserHelper.getName(user); + if (currentSid.equals(info.getSessionId())) usName += " [CURRENT]"; + String chName = LoginChannel.valueOf(keySplit[1]).getDisplayName(); JSONObject item = JSONUtils.toJSONObject( - new String[] { "user", "fullName", "activeTime", "activeUrl", "activeIp", "sid" }, - new Object[] { user, fullName, active[0], active[1], active[2], s.getId() }); + new String[]{"user", "fullName", "activeTime", "activeUrl", "activeIp", "channel", "sid", "token"}, + new Object[]{user, usName, info.getActiveTime(), info.getActiveUri(), info.getActiveIp(), chName, info.getSessionId(), info.getAuthToken()}); online.add(item); } - // H5 - if (getBoolParameter(request, "h5")) { - for (Object[] s : Application.getSessionStore().getAllH5Session(true)) { - ID user = (ID) s[4]; - JSONObject item = JSONUtils.toJSONObject( - new String[] { "user", "fullName", "activeTime", "activeUrl", "activeIp", "sid", "h5" }, - new Object[] { user, UserHelper.getName(user), s[0], s[1], s[2], s[3], true }); - online.add(item); - } - } - online.sort((o1, o2) -> { long d1 = ((JSONObject) o1).getLong("activeTime"); long d2 = ((JSONObject) o2).getLong("activeTime"); @@ -89,7 +78,6 @@ public JSON getOnlineUsers(HttpServletRequest request) { long activeTime = ((JSONObject) o).getLong("activeTime"); ((JSONObject) o).put("activeTime", I18nUtils.formatDate(new Date(activeTime))); } - return online; } diff --git a/src/main/java/com/rebuild/web/admin/bizz/DepartmentController.java b/src/main/java/com/rebuild/web/admin/bizz/DepartmentController.java index 33687cc54c..3192c4ea09 100644 --- a/src/main/java/com/rebuild/web/admin/bizz/DepartmentController.java +++ b/src/main/java/com/rebuild/web/admin/bizz/DepartmentController.java @@ -58,7 +58,7 @@ public JSON deptTreeGet() { JSONArray dtree = new JSONArray(); Department[] ds = Application.getUserStore().getTopDepartments(); - sortByName(ds); + Arrays.sort(ds); for (Department root : ds) { dtree.add(recursiveDeptTree(root)); } @@ -73,7 +73,7 @@ private JSONObject recursiveDeptTree(Department parent) { JSONArray children = new JSONArray(); BusinessUnit[] ds = parent.getChildren().toArray(new BusinessUnit[0]); - sortByName(ds); + Arrays.sort(ds); for (BusinessUnit child : ds) { children.add(recursiveDeptTree((Department) child)); } @@ -83,13 +83,4 @@ private JSONObject recursiveDeptTree(Department parent) { } return parentJson; } - - private void sortByName(BusinessUnit[] depts) { - // 排序 a-z - Arrays.sort(depts, (o1, o2) -> { - if (DepartmentService.ROOT_DEPT.equals(o1.getIdentity())) return -1; - else if (DepartmentService.ROOT_DEPT.equals(o2.getIdentity())) return 1; - else return o1.getName().compareTo(o2.getName()); - }); - } } diff --git a/src/main/java/com/rebuild/web/admin/bizz/UserController.java b/src/main/java/com/rebuild/web/admin/bizz/UserController.java index 9e397a3d45..887559229e 100644 --- a/src/main/java/com/rebuild/web/admin/bizz/UserController.java +++ b/src/main/java/com/rebuild/web/admin/bizz/UserController.java @@ -36,7 +36,6 @@ import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -145,17 +144,6 @@ public RespBody enableUser(@RequestBody JSONObject data) { Application.getBean(UserService.class) .updateEnableUser(userId, deptNew, roleNew, roleAppends, enableNew); - - // 禁用后马上销毁会话 - enUser = Application.getUserStore().getUser(enUser.getId()); - if (!enUser.isActive()) { - HttpSession s = Application.getSessionStore().getSession(enUser.getId()); - if (s != null) { - log.warn("FORCE DESTROY USER SESSION : {} < {}", enUser.getId(), s.getId()); - s.invalidate(); - } - } - return RespBody.ok(); } diff --git a/src/main/java/com/rebuild/web/admin/metadata/FormDesignController.java b/src/main/java/com/rebuild/web/admin/metadata/FormDesignController.java index 1932fb4921..b2674c06f9 100644 --- a/src/main/java/com/rebuild/web/admin/metadata/FormDesignController.java +++ b/src/main/java/com/rebuild/web/admin/metadata/FormDesignController.java @@ -21,6 +21,7 @@ import com.rebuild.core.configuration.general.LayoutConfigService; import com.rebuild.core.metadata.EntityHelper; import com.rebuild.core.metadata.MetadataHelper; +import com.rebuild.core.metadata.easymeta.EasyEntity; import com.rebuild.core.metadata.easymeta.EasyField; import com.rebuild.core.metadata.easymeta.EasyMetaFactory; import com.rebuild.core.privileges.UserHelper; @@ -48,7 +49,7 @@ public class FormDesignController extends BaseController { @GetMapping("form-design") public ModelAndView page(@PathVariable String entity, HttpServletRequest request) { ModelAndView mv = createModelAndView("/admin/metadata/form-design"); - MetaEntityController.setEntityBase(mv, entity); + EasyEntity easyEntity = MetaEntityController.setEntityBase(mv, entity); mv.getModel().put("isSuperAdmin", UserHelper.isSuperAdmin(getRequestUser(request))); ID formConfigId37 = getIdParameter(request, "id"); @@ -58,6 +59,7 @@ public ModelAndView page(@PathVariable String entity, HttpServletRequest request List attrs = FormsManager.instance.getAllFormsAttr(entity); request.setAttribute("FormsAttr", JSON.toJSONString(attrs)); + mv.getModel().put("isDetailEntity", easyEntity.getRawMeta().getMainEntity() != null); return mv; } diff --git a/src/main/java/com/rebuild/web/admin/metadata/MetaEntityController.java b/src/main/java/com/rebuild/web/admin/metadata/MetaEntityController.java index b506019972..7156382eab 100644 --- a/src/main/java/com/rebuild/web/admin/metadata/MetaEntityController.java +++ b/src/main/java/com/rebuild/web/admin/metadata/MetaEntityController.java @@ -150,7 +150,10 @@ public ModelAndView pageOverview(@PathVariable String entity) { @GetMapping("entity/{entity}/easy-action") public ModelAndView pageEasyAction(@PathVariable String entity) { ModelAndView mv = createModelAndView("/admin/metadata/entity-easy-action"); - setEntityBase(mv, entity); + EasyEntity easyEntity = setEntityBase(mv, entity); + if (easyEntity.getRawMeta().getMainEntity() != null) { + mv.getModel().put("mainEntity", easyEntity.getRawMeta().getMainEntity().getName()); + } ConfigBean cb = EasyActionManager.instance.getEasyActionRaw(entity); if (cb != null) { diff --git a/src/main/java/com/rebuild/web/admin/setup/InstallController.java b/src/main/java/com/rebuild/web/admin/setup/InstallController.java index 71f700d84a..4612de781d 100644 --- a/src/main/java/com/rebuild/web/admin/setup/InstallController.java +++ b/src/main/java/com/rebuild/web/admin/setup/InstallController.java @@ -66,7 +66,6 @@ public ModelAndView index(HttpServletRequest request) throws IOException { // 切换语言 LoginController.putLocales(mv, AppUtils.getReuqestLocale(request)); - return mv; } diff --git a/src/main/java/com/rebuild/web/admin/setup/RbSystemController.java b/src/main/java/com/rebuild/web/admin/setup/RbSystemController.java new file mode 100644 index 0000000000..cbba95935b --- /dev/null +++ b/src/main/java/com/rebuild/web/admin/setup/RbSystemController.java @@ -0,0 +1,51 @@ +/*! +Copyright (c) REBUILD and/or its owners. All rights reserved. + +rebuild is dual-licensed under commercial and open source licenses (GPLv3). +See LICENSE and COMMERCIAL in the project root for license information. +*/ + +package com.rebuild.web.admin.setup; + +import com.rebuild.api.RespBody; +import com.rebuild.core.RebuildException; +import com.rebuild.core.rbstore.RbSystemImporter; +import com.rebuild.core.support.setup.InstallState; +import com.rebuild.core.support.task.TaskExecutors; +import com.rebuild.web.BaseController; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * @author devezhao + * @since 2024/12/1 + */ +@RestController +@RequestMapping("/setup/") +public class RbSystemController extends BaseController implements InstallState { + + @GetMapping({"rbsystems", "appstore"}) + public ModelAndView index() throws IOException { + return createModelAndView("/admin/setup/rbsystem"); + } + + @PostMapping("install-rbsystem") + public RespBody install(HttpServletRequest request) throws IOException { + String file = getParameterNotNull(request, "file"); + RbSystemImporter importer = new RbSystemImporter("rbsystems/" + file); + try { + importer.check(); + } catch (RebuildException ex) { + return RespBody.error(ex.getLocalizedMessage()); + } + + TaskExecutors.run(importer); + return RespBody.ok(); + } +} diff --git a/src/main/java/com/rebuild/web/commons/BarCodeGeneratorController.java b/src/main/java/com/rebuild/web/commons/BarCodeGeneratorController.java index 682fa275b8..c92cbf3c68 100644 --- a/src/main/java/com/rebuild/web/commons/BarCodeGeneratorController.java +++ b/src/main/java/com/rebuild/web/commons/BarCodeGeneratorController.java @@ -10,10 +10,12 @@ import cn.devezhao.commons.web.ServletUtils; import cn.devezhao.persist4j.Field; import cn.devezhao.persist4j.engine.ID; +import com.google.zxing.BarcodeFormat; import com.rebuild.core.metadata.MetadataHelper; import com.rebuild.core.support.general.BarCodeSupport; import com.rebuild.utils.AppUtils; import com.rebuild.web.BaseController; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -29,6 +31,7 @@ * @author devezhao * @since 2020/6/5 */ +@Slf4j @Controller public class BarCodeGeneratorController extends BaseController { @@ -58,11 +61,25 @@ public void render(HttpServletRequest request, HttpServletResponse response) thr bi = BarCodeSupport.createQRCode(content, w); } else { boolean showText = getBoolParameter(request, "b", true); - bi = BarCodeSupport.createBarCode(content, w, showText); + String showFormat = getParameter(request, "format"); + BarcodeFormat specFormat = null; + if (showFormat != null) { + try { + specFormat = BarcodeFormat.valueOf(showFormat); + } catch (Exception ex) { + log.warn("Bad format for BarCode : {}", specFormat); + } + } + + if (specFormat != null) { + bi = BarCodeSupport.createBarCode(content, 0, w, showText, specFormat); + } else { + bi = BarCodeSupport.createBarCode(content, w, showText); + } } - // 6小时缓存 - ServletUtils.addCacheHead(response, 360); + // 24小时缓存 + ServletUtils.addCacheHead(response, 60 * 24); writeTo(bi, response); } diff --git a/src/main/java/com/rebuild/web/commons/FileDownloader.java b/src/main/java/com/rebuild/web/commons/FileDownloader.java index 5167def8b4..e19c8b6fab 100644 --- a/src/main/java/com/rebuild/web/commons/FileDownloader.java +++ b/src/main/java/com/rebuild/web/commons/FileDownloader.java @@ -82,6 +82,12 @@ public void viewImg(HttpServletRequest request, HttpServletResponse response) th imageView2 = null; } + // v3.9 SVG 安全问题 + if (filepath.toLowerCase().endsWith(".svg")) { + writeStream(CommonsUtils.getStreamOfRes("web/assets/img/image.png"), response); + return; + } + final int cacheTime = 60; ServletUtils.addCacheHead(response, cacheTime); diff --git a/src/main/java/com/rebuild/web/commons/FileUploader.java b/src/main/java/com/rebuild/web/commons/FileUploader.java index 64d6a3832a..1b28a0bdd3 100644 --- a/src/main/java/com/rebuild/web/commons/FileUploader.java +++ b/src/main/java/com/rebuild/web/commons/FileUploader.java @@ -62,10 +62,11 @@ public void upload(HttpServletRequest request, HttpServletResponse response) { return; } + String updir = getParameter(request, "updir"); String uploadName; try { boolean noname = getBoolParameter(request, "noname", Boolean.FALSE); - uploadName = QiniuCloud.formatFileKey(file.getOriginalFilename(), !noname); + uploadName = QiniuCloud.formatFileKey(file.getOriginalFilename(), !noname, updir); File dest; // 上传临时文件 diff --git a/src/main/java/com/rebuild/web/commons/QiniuUploadController.java b/src/main/java/com/rebuild/web/commons/QiniuUploadController.java index bbe1939ebf..f1616d99e6 100644 --- a/src/main/java/com/rebuild/web/commons/QiniuUploadController.java +++ b/src/main/java/com/rebuild/web/commons/QiniuUploadController.java @@ -28,8 +28,9 @@ public class QiniuUploadController extends BaseController { public JSON getUploadKeys(HttpServletRequest request) { String fileName = getParameterNotNull(request, "file"); boolean noname = getBoolParameter(request, "noname", Boolean.FALSE); + String updir = getParameter(request, "updir"); - String fileKey = QiniuCloud.formatFileKey(fileName, !noname); + String fileKey = QiniuCloud.formatFileKey(fileName, !noname, updir); String token = QiniuCloud.instance().getUploadToken(fileKey); return JSONUtils.toJSONObject( diff --git a/src/main/java/com/rebuild/web/commons/UsersGetting.java b/src/main/java/com/rebuild/web/commons/UsersGetting.java index d249990b4f..377fe77242 100644 --- a/src/main/java/com/rebuild/web/commons/UsersGetting.java +++ b/src/main/java/com/rebuild/web/commons/UsersGetting.java @@ -110,9 +110,10 @@ public JSON loadUsers(HttpServletRequest request) { /** * 获取符合 UserSelector 组件的数据 - * * @param request + * @param useEntity * @see UserHelper#parseUsers(JSONArray, ID) + * @return */ @PostMapping("user-selector") public JSON parseUserSelectorRaw(HttpServletRequest request, @EntityParam(required = false) Entity useEntity) { diff --git a/src/main/java/com/rebuild/web/contacts/ContactsController.java b/src/main/java/com/rebuild/web/contacts/ContactsController.java new file mode 100644 index 0000000000..8e4db133a8 --- /dev/null +++ b/src/main/java/com/rebuild/web/contacts/ContactsController.java @@ -0,0 +1,155 @@ +/*! +Copyright (c) REBUILD and/or its owners. All rights reserved. + +rebuild is dual-licensed under commercial and open source licenses (GPLv3). +See LICENSE and COMMERCIAL in the project root for license information. +*/ + +package com.rebuild.web.contacts; + +import cn.devezhao.bizz.security.member.BusinessUnit; +import cn.devezhao.bizz.security.member.Member; +import cn.devezhao.persist4j.engine.ID; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.hankcs.hanlp.HanLP; +import com.rebuild.api.RespBody; +import com.rebuild.core.Application; +import com.rebuild.core.privileges.UserFilters; +import com.rebuild.core.privileges.UserService; +import com.rebuild.core.privileges.bizz.Department; +import com.rebuild.core.privileges.bizz.User; +import com.rebuild.utils.JSONUtils; +import com.rebuild.web.BaseController; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * 通讯录 + * FIXME 通讯录过滤:部门用户隔离 + * + * @author devezhao + * @since 2024/11/6 + */ +@RestController +public class ContactsController extends BaseController { + + @GetMapping("/contacts/home") + public ModelAndView pageIndex() { + return createModelAndView("/contacts/home"); + } + + @GetMapping("/contacts/list-depts") + public RespBody listDepts(HttpServletRequest request) { + final ID user = getRequestUser(request); + Department[] ds; + if (UserFilters.isEnableBizzPart(user)) { + User ub = Application.getUserStore().getUser(user); + ds = new Department[]{ub.getOwningDept()}; + } else { + ds = Application.getUserStore().getTopDepartments(); + Arrays.sort(ds); + } + + JSONArray dtree = new JSONArray(); + for (Department d : ds) { + if (d.isDisabled()) continue; + dtree.add(recursiveDeptTree(d)); + } + return RespBody.ok(dtree); + } + + private JSONObject recursiveDeptTree(Department parent) { + JSONObject parentJson = new JSONObject(); + parentJson.put("id", parent.getIdentity()); + parentJson.put("name", parent.getName()); + JSONArray children = new JSONArray(); + + BusinessUnit[] ds = parent.getChildren().toArray(new BusinessUnit[0]); + Arrays.sort(ds); + for (BusinessUnit child : ds) { + if (child.isDisabled()) continue; + children.add(recursiveDeptTree((Department) child)); + } + + if (!children.isEmpty()) { + parentJson.put("children", children); + } + return parentJson; + } + + @GetMapping("/contacts/list-users") + public RespBody listUsers(HttpServletRequest request) { + final ID user = getRequestUser(request); + final ID dept = getIdParameter(request, "dept"); + String q = getParameter(request, "q"); + if (q != null) q = q.toUpperCase().trim(); + + User[] users = Application.getUserStore().getAllUsers(); + Member[] usersMembers = UserFilters.filterMembers32(users, user); + + Set deptAndChild = null; + if (dept != null) { + deptAndChild = new HashSet<>(); + deptAndChild.add(dept); + + Department deptObj = Application.getUserStore().getDepartment(dept); + if (deptObj != null) { + for (BusinessUnit bu : deptObj.getAllChildren()) { + deptAndChild.add((ID) bu.getIdentity()); + } + } + } + + JSONArray array = new JSONArray(); + for (Member m : usersMembers) { + User u = (User) m; + if (UserService.SYSTEM_USER.equals(u.getId())) continue; + if (!u.isActive()) continue; + + Department d = u.getOwningDept(); + if (deptAndChild != null) { + if (d == null) continue; + if (!deptAndChild.contains((ID) d.getIdentity())) continue; + } + + if (q != null) { + if (q.endsWith("*")) { + String prefix = q.substring(0, q.length() - 1); + // A-Z + if (prefix.length() == 1 && Character.isUpperCase(prefix.charAt(0))) { + String piny = HanLP.convertToPinyinString(u.getFullName(), "", false); + if (!piny.toUpperCase().startsWith(prefix)) { + continue; + } + } + // prefix + else if (!(StringUtils.startsWithIgnoreCase(u.getFullName(), q) + || StringUtils.startsWithIgnoreCase(u.getEmail(), q) + || StringUtils.startsWithIgnoreCase(u.getWorkphone(), q))) { + continue; + } + } + // includes + else if (!(StringUtils.containsIgnoreCase(u.getFullName(), q) + || StringUtils.containsIgnoreCase(u.getEmail(), q) + || StringUtils.containsIgnoreCase(u.getWorkphone(), q))) { + continue; + } + } + + JSONObject item = JSONUtils.toJSONObject( + new String[]{"id", "fullName", "email", "workphone", "deptName", "avtive"}, + new Object[]{u.getId(), u.getFullName(), u.getEmail(), u.getWorkphone(), d == null ? "-" : d.getName(), u.isActive()}); + array.add(item); + } + return RespBody.ok(array); + } +} diff --git a/src/main/java/com/rebuild/web/feeds/FeedsListController.java b/src/main/java/com/rebuild/web/feeds/FeedsListController.java index 8147055cec..8c85674a1a 100644 --- a/src/main/java/com/rebuild/web/feeds/FeedsListController.java +++ b/src/main/java/com/rebuild/web/feeds/FeedsListController.java @@ -18,6 +18,7 @@ import com.rebuild.core.metadata.easymeta.EasyEntity; import com.rebuild.core.metadata.easymeta.EasyMetaFactory; import com.rebuild.core.privileges.UserHelper; +import com.rebuild.core.privileges.bizz.User; import com.rebuild.core.service.NoRecordFoundException; import com.rebuild.core.service.feeds.FeedsHelper; import com.rebuild.core.service.feeds.FeedsScope; @@ -40,9 +41,11 @@ import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -152,9 +155,10 @@ public RespBody fetchFeeds(HttpServletRequest request) { array = add2Top(foucsFeed, array); if (userTopFeeds != null) { // 最多 3 - if (userTopFeeds.size() > 2) array = add2Top(userTopFeeds.get(2), array); - if (userTopFeeds.size() > 1) array = add2Top(userTopFeeds.get(1), array); - if (userTopFeeds.size() > 0) array = add2Top(userTopFeeds.get(0), array); + int topSize = userTopFeeds.size(); + if (topSize > 2) array = add2Top(userTopFeeds.get(2), array); + if (topSize > 1) array = add2Top(userTopFeeds.get(1), array); + if (topSize > 0) array = add2Top(userTopFeeds.get(0), array); } Set set = new HashSet<>(); @@ -248,7 +252,7 @@ private JSONObject buildItem(Object[] o, ID user) { .array(); List statusList = new ArrayList<>(); for (Object[] s : status) { - statusList.add(new Object[] { s[0], UserHelper.getName((ID) s[0]), s[1] }); + statusList.add(new Object[]{s[0], UserHelper.getName((ID) s[0]), s[1]}); } item.put("readStatus", statusList); } @@ -257,6 +261,44 @@ private JSONObject buildItem(Object[] o, ID user) { return item; } + private List getAnnouncementReadList(ID announcementId) { + Object[][] status = Application.createQueryNoFilter( + "select createdBy,createdOn from FeedsStatus where feedsId = ? order by createdOn") + .setParameter(1, announcementId) + .array(); + List readList = new ArrayList<>(); + for (Object[] s : status) { + readList.add(new Object[]{s[0], UserHelper.getName((ID) s[0]), s[1]}); + } + return readList; + } + + @GetMapping("/feeds/announcement-read-status") + public RespBody announcementReadStatus(@IdParam ID announcementId) { + Object[][] status = Application.createQueryNoFilter( + "select createdBy,createdOn from FeedsStatus where feedsId = ?") + .setParameter(1, announcementId) + .array(); + final Map readMap = new HashMap<>(); + for (Object[] s : status) readMap.put((ID) s[0], (Date) s[1]); + + List readList = new ArrayList<>(); + List unreadList = new ArrayList<>(); + for (User u : Application.getUserStore().getAllUsers()) { + // 未激活的不管 + if (!u.isActive()) continue; + + if (readMap.containsKey(u.getId())) { + readList.add(new Object[]{u.getIdentity(), u.getFullName(), readMap.get(u.getId())}); + } else { + unreadList.add(new Object[]{u.getIdentity(), u.getFullName(), null}); + } + } + + JSON res = JSONUtils.toJSONObject(new String[]{"read", "unread"}, new Object[]{readList, unreadList}); + return RespBody.ok(res); + } + @GetMapping("/feeds/comments-list") public RespBody fetchComments(HttpServletRequest request) { final ID user = getRequestUser(request); diff --git a/src/main/java/com/rebuild/web/general/CommonOperatingController.java b/src/main/java/com/rebuild/web/general/CommonOperatingController.java index f5ce9e32d1..c91e640d90 100644 --- a/src/main/java/com/rebuild/web/general/CommonOperatingController.java +++ b/src/main/java/com/rebuild/web/general/CommonOperatingController.java @@ -79,7 +79,9 @@ public JSON delete(@IdParam ID recordId) { @RequestMapping("common-get") public RespBody get(@IdParam ID recordId, HttpServletRequest request) { String fields = getParameter(request, "fields"); - if (StringUtils.isEmpty(fields)) fields = getAllFields(MetadataHelper.getEntity(recordId.getEntityCode())); + if (StringUtils.isEmpty(fields)) { + fields = getAllFields(MetadataHelper.getEntity(recordId.getEntityCode())); + } Record record = Application.getQueryFactory().record(recordId, fields.split("[,;]")); if (record == null) { diff --git a/src/main/java/com/rebuild/web/general/GeneralListController.java b/src/main/java/com/rebuild/web/general/GeneralListController.java index ada2b3e024..5baefdad14 100644 --- a/src/main/java/com/rebuild/web/general/GeneralListController.java +++ b/src/main/java/com/rebuild/web/general/GeneralListController.java @@ -102,7 +102,12 @@ public ModelAndView pageList(@PathVariable String entity, HttpServletRequest req JSON listConfig = null; if (listMode == 1) { - listConfig = DataListManager.instance.getListFields(entity, user); + String def39 = getParameter(request, "def"); + ID useLayout = null; + if (def39 != null && def39.contains(":")) { + useLayout = ID.valueOf(def39.split(":")[1]); + } + listConfig = DataListManager.instance.getListFields(entity, user, useLayout == null ? null : useLayout.toLiteral()); // 侧栏 diff --git a/src/main/java/com/rebuild/web/general/GeneralModelController.java b/src/main/java/com/rebuild/web/general/GeneralModelController.java index 60eab65190..1eb9520fa2 100644 --- a/src/main/java/com/rebuild/web/general/GeneralModelController.java +++ b/src/main/java/com/rebuild/web/general/GeneralModelController.java @@ -16,6 +16,7 @@ import com.alibaba.fastjson.JSONObject; import com.rebuild.core.Application; import com.rebuild.core.configuration.ConfigBean; +import com.rebuild.core.configuration.general.EasyActionManager; import com.rebuild.core.configuration.general.FormsBuilder; import com.rebuild.core.configuration.general.FormsBuilderContextHolder; import com.rebuild.core.configuration.general.FormsManager; @@ -25,7 +26,6 @@ import com.rebuild.core.metadata.MetadataHelper; import com.rebuild.core.metadata.easymeta.EasyMetaFactory; import com.rebuild.core.privileges.UserHelper; -import com.rebuild.core.service.general.transform.TransformerPreview37; import com.rebuild.core.service.query.QueryHelper; import com.rebuild.core.support.ConfigurationItem; import com.rebuild.core.support.RebuildConfiguration; @@ -34,7 +34,6 @@ import com.rebuild.web.EntityController; import com.rebuild.web.IdParam; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -87,6 +86,8 @@ public ModelAndView pageView(@PathVariable String entity, @PathVariable ID id, mv.getModel().putAll(getViewExtras(user, entity, isDetail)); // 显示历史 mv.getModel().put("ShowViewHistory", RebuildConfiguration.getBool(ConfigurationItem.ShowViewHistory)); + // EasyAction + mv.getModel().put("easyAction", EasyActionManager.instance.getEasyAction(entity, user)); mv.getModel().put("id", id); return mv; @@ -128,26 +129,20 @@ public JSON entityForm(@PathVariable String entity, @IdParam(required = false) I FormsBuilderContextHolder.setMainIdOfDetail(ID.valueOf(mainid)); } // v2.8 - else if (FormsBuilder.DV_MAINID.equals(mainid)) { + // v3.9 明细直接新建 + else if (FormsBuilder.DV_MAINID.equals(mainid) || FormsBuilder.DV_MAINID_FJS.equals(mainid)) { ID fakeMainid = EntityHelper.newUnsavedId(modelEntity.getMainEntity().getEntityCode()); FormsBuilderContextHolder.setMainIdOfDetail(fakeMainid); } } } - // 记录转换:预览模式 - final String previewid = request.getParameter("previewid"); // 指定布局 final ID specLayout = getIdParameter(request, "layout"); try { - JSON model; - if (StringUtils.isNotBlank(previewid)) { - model = new TransformerPreview37(previewid, user).buildForm(); - } else { - if (specLayout != null) FormsBuilderContextHolder.setSpecLayout(specLayout); - model = FormsBuilder.instance.buildForm(entity, user, id); - } + if (specLayout != null) FormsBuilderContextHolder.setSpecLayout(specLayout); + JSON model = FormsBuilder.instance.buildForm(entity, user, id); // 填充前端设定的初始值 if (id == null && initialVal != null) { @@ -246,12 +241,6 @@ public JSON entityFormDetails( HttpServletRequest request) { final ID user = getRequestUser(request); - // 记录转换预览模式 - String previewid = request.getParameter("previewid"); - if (StringUtils.isNotBlank(previewid)) { - return new TransformerPreview37(previewid, user).buildForm(entity); - } - Entity detailEntityMeta = MetadataHelper.getEntity(entity); List ids = QueryHelper.detailIdsNoFilter(mainid, detailEntityMeta); if (ids.isEmpty()) return JSONUtils.EMPTY_ARRAY; @@ -272,6 +261,7 @@ public JSON entityFormDetails( } finally { FormsBuilderContextHolder.getSpecLayout(true); } + return details; } } diff --git a/src/main/java/com/rebuild/web/general/GeneralOperatingController.java b/src/main/java/com/rebuild/web/general/GeneralOperatingController.java index 747ddf2b45..a72481e220 100644 --- a/src/main/java/com/rebuild/web/general/GeneralOperatingController.java +++ b/src/main/java/com/rebuild/web/general/GeneralOperatingController.java @@ -100,7 +100,7 @@ record = EntityHelper.parse((JSONObject) formJson, user); return CommonOperatingController.saveRecord(record); } - // 明细 + // 明细列表 List detailsList = new ArrayList<>(); if (details != null) { try { @@ -129,8 +129,6 @@ record = EntityHelper.parse((JSONObject) formJson, user); GeneralEntityServiceContextHolder.setRepeatedCheckMode(GeneralEntityServiceContextHolder.RCM_CHECK_DETAILS); } - final boolean isNew = record.getPrimary() == null; - // v3.4 TODO 单字段修改检查,有性能问题 final boolean singleField = getBoolParameter(request, "singleField"); Record beforeSnap = null; @@ -165,12 +163,12 @@ record = ies.createOrUpdate(record); } // 转换后回填 - final String previewid = request.getParameter("previewid"); - if (isNew && StringUtils.isNotBlank(previewid)) { + String previewid = request.getParameter("previewid"); + if (StringUtils.isNotBlank(previewid)) { try { new TransformerPreview37(previewid, user).fillback(record.getPrimary()); } catch (Exception ex) { - log.error("Transformer fillback error!", ex); + log.error("Transforme with fillback fails : {}", previewid, ex); } } diff --git a/src/main/java/com/rebuild/web/general/MetaFormatter.java b/src/main/java/com/rebuild/web/general/MetaFormatter.java index b680998993..812025a2fc 100644 --- a/src/main/java/com/rebuild/web/general/MetaFormatter.java +++ b/src/main/java/com/rebuild/web/general/MetaFormatter.java @@ -194,6 +194,7 @@ public static JSONArray buildFieldsWithRefs(Entity entity, int deep, boolean ric if (name.endsWith(".roleId.createdOn")) continue; if (name.endsWith(".roleId.modifiedBy")) continue; if (name.endsWith(".roleId.modifiedOn")) continue; + if (name.endsWith(".seq")) continue; res.add(item); } diff --git a/src/main/java/com/rebuild/web/general/ModelExtrasController.java b/src/main/java/com/rebuild/web/general/ModelExtrasController.java index 8c9e6842bb..da2ddeaabf 100644 --- a/src/main/java/com/rebuild/web/general/ModelExtrasController.java +++ b/src/main/java/com/rebuild/web/general/ModelExtrasController.java @@ -25,8 +25,7 @@ import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.privileges.bizz.User; import com.rebuild.core.service.general.RepeatedRecordsException; -import com.rebuild.core.service.general.transform.RecordTransfomer; -import com.rebuild.core.service.general.transform.RecordTransfomer37; +import com.rebuild.core.service.general.transform.RecordTransfomer39; import com.rebuild.core.support.general.CalcFormulaSupport; import com.rebuild.core.support.i18n.I18nUtils; import com.rebuild.core.support.i18n.Language; @@ -71,22 +70,36 @@ public JSON getFillinValue(@EntityParam Entity entity, @IdParam(name = "source") } // 记录转换 - @RequestMapping("transform") - public RespBody transform(HttpServletRequest request) { - ID transid = getIdParameterNotNull(request, "transid"); - ID sourceRecord = getIdParameterNotNull(request, "source"); - ID mainid = getIdParameter(request, "mainid"); - - RecordTransfomer transfomer = new RecordTransfomer37(transid); - if (!transfomer.checkFilter(sourceRecord)) { + @PostMapping("transform39") + public RespBody transform39(HttpServletRequest request) { + final JSONObject post = (JSONObject) ServletUtils.getRequestJson(request); + final ID transid = ID.valueOf(post.getString("transid")); + final Object sourceRecordAny = post.get("sourceRecord"); + if (sourceRecordAny instanceof JSONArray) { + return this.transform39Muilt(transid, (JSONArray) sourceRecordAny); + } + // 单个 + ID sourceRecord = ID.valueOf(sourceRecordAny.toString()); + + RecordTransfomer39 transfomer39 = new RecordTransfomer39(transid); + if (!transfomer39.checkFilter(sourceRecord)) { return RespBody.error(Language.L("当前记录不符合转换条件"), 400); } + ID mainRecord = ID.isId(post.getString("mainRecord")) ? ID.valueOf(post.getString("mainRecord")) : null; + ID existsRecord = ID.isId(post.getString("existsRecord")) ? ID.valueOf(post.getString("existsRecord")) : null; + try { - ID theNewId = transfomer.transform(sourceRecord, mainid); - return RespBody.ok(theNewId); + Object res; + if (post.getBooleanValue("preview")) { + res = transfomer39.preview(sourceRecord, mainRecord, existsRecord); + } else { + res = transfomer39.transform(sourceRecord, mainRecord, existsRecord); + } + return RespBody.ok(res); + } catch (Exception ex) { - log.warn(">>>>> {}", ex.getLocalizedMessage()); + log.warn(">>>>> {} : {}", sourceRecord, ex.getLocalizedMessage()); String error = ex.getLocalizedMessage(); if (ex instanceof RepeatedRecordsException) { @@ -94,8 +107,26 @@ public RespBody transform(HttpServletRequest request) { } return RespBody.errorl("记录转换失败 (%s)", - Objects.toString(error, ex.getClass().getSimpleName())); + Objects.toString(error, ex.getClass().getSimpleName().toUpperCase())); + } + } + + // 批量转换 + private RespBody transform39Muilt(ID transid, JSONArray sourceRecords) { + List newIds = new ArrayList<>(); + RecordTransfomer39 transfomer39 = new RecordTransfomer39(transid); + for (Object o : sourceRecords) { + ID sourceRecord = ID.valueOf((String) o); + if (!transfomer39.checkFilter(sourceRecord)) continue; + + try { + ID newId = transfomer39.transform(sourceRecord, null, null); + newIds.add(newId); + } catch (Exception ex) { + log.warn(">>>>> {} : {}", sourceRecord, ex.getLocalizedMessage()); + } } + return RespBody.ok(newIds); } @GetMapping("record-last-modified") diff --git a/src/main/java/com/rebuild/web/general/ReferenceSearchController.java b/src/main/java/com/rebuild/web/general/ReferenceSearchController.java index d77f3ffde9..877d6ab079 100644 --- a/src/main/java/com/rebuild/web/general/ReferenceSearchController.java +++ b/src/main/java/com/rebuild/web/general/ReferenceSearchController.java @@ -9,6 +9,7 @@ import cn.devezhao.persist4j.Entity; import cn.devezhao.persist4j.Field; +import cn.devezhao.persist4j.dialect.FieldType; import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; @@ -72,9 +73,7 @@ public JSON referenceSearch(@EntityParam Entity entity, HttpServletRequest reque Entity searchEntity = referenceField.getReferenceEntity(); // 引用字段数据过滤 - String cascadingValue = getParameter(request, "cascadingValue", StringUtils.EMPTY); - if (cascadingValue.contains(",")) cascadingValue = cascadingValue.split(",")[0]; // N2N - + String cascadingValue = getParameter(request, "cascadingValue"); String protocolFilter = new ProtocolFilterParser() .parseRef(referenceField.getName() + "." + entity.getName(), cascadingValue); @@ -268,9 +267,7 @@ public RespBody referenceLabel(HttpServletRequest request) { Map labels = new HashMap<>(); for (String id : ids.split("[|,]")) { if (!ID.isId(id)) { - if (_SELF.equals(id)) { - labels.put(_SELF, Language.L("本人/本部门")); - } + if (_SELF.equals(id)) labels.put(_SELF, Language.L("本人/本部门")); continue; } @@ -305,7 +302,8 @@ public ModelAndView referenceSearchPage(HttpServletRequest request, HttpServletR Entity entity = MetadataHelper.getEntity(fieldAndEntity[1]); Field field = entity.getField(fieldAndEntity[0]); - Entity searchEntity = field.getReferenceEntity(); + // v3.9 支持主键字段 + Entity searchEntity = field.getType() == FieldType.PRIMARY ? entity : field.getReferenceEntity(); ModelAndView mv = createModelAndView("/general/reference-search"); putEntityMeta(mv, searchEntity); diff --git a/src/main/java/com/rebuild/web/notification/NotificationController.java b/src/main/java/com/rebuild/web/notification/NotificationController.java index 8806829b73..56fbb5421c 100644 --- a/src/main/java/com/rebuild/web/notification/NotificationController.java +++ b/src/main/java/com/rebuild/web/notification/NotificationController.java @@ -18,7 +18,6 @@ import com.rebuild.core.service.notification.MessageBuilder; import com.rebuild.core.support.i18n.I18nUtils; import com.rebuild.core.support.i18n.Language; -import com.rebuild.utils.AppUtils; import com.rebuild.utils.JSONUtils; import com.rebuild.web.BaseController; import com.rebuild.web.admin.ConfigurationController; @@ -62,13 +61,10 @@ public JSON checkMessage(HttpServletRequest request) { JSON mm = buildMM(); if (mm != null) state.put("mm", mm); - // v3.8 + // v3.8 手机版利用通知检查做最近活跃信息存储 String h5referer = getParameter(request, "h5referer"); if (StringUtils.isNotBlank(h5referer)) { - String authToken = request.getHeader(AppUtils.HF_AUTHTOKEN); - if (authToken != null) { - Application.getSessionStore().storeH5LastActive(authToken, user, h5referer, request); - } + Application.getSessionStore().storeLastActive(user, request, h5referer); } return state; } diff --git a/src/main/java/com/rebuild/web/robot/approval/ApprovalController.java b/src/main/java/com/rebuild/web/robot/approval/ApprovalController.java index 9d59de473d..1f77727cf3 100644 --- a/src/main/java/com/rebuild/web/robot/approval/ApprovalController.java +++ b/src/main/java/com/rebuild/web/robot/approval/ApprovalController.java @@ -192,8 +192,8 @@ private JSONArray formatUsers(Collection users) { } @GetMapping("fetch-workedsteps") - public JSON fetchWorkedSteps(@IdParam(name = "record") ID recordId) { - return new ApprovalProcessor(recordId).getWorkedSteps(); + public JSON fetchWorkedSteps(@IdParam(name = "record") ID recordId, HttpServletRequest request) { + return new ApprovalProcessor(recordId).getWorkedSteps(getBoolParameter(request, "his")); } @GetMapping("fetch-backsteps") diff --git a/src/main/java/com/rebuild/web/robot/trigger/FieldAggregationController.java b/src/main/java/com/rebuild/web/robot/trigger/FieldAggregationController.java index a8bb31231f..e560a9b739 100644 --- a/src/main/java/com/rebuild/web/robot/trigger/FieldAggregationController.java +++ b/src/main/java/com/rebuild/web/robot/trigger/FieldAggregationController.java @@ -135,7 +135,7 @@ public JSON getTargetFields(@EntityParam(name = "source") Entity sourceEntity, ObjectUtils.defaultIfNull(targetEntity.getMainEntity(), targetEntity), null) != null; return JSONUtils.toJSONObject( - new String[] { "source", "target", "hadApproval", "target2" }, + new String[] { "source", "target", "hadApproval", "target4Group" }, new Object[] { sourceFields, targetFields, hadApproval, targetFields2 }); } diff --git a/src/main/java/com/rebuild/web/user/UCenterController.java b/src/main/java/com/rebuild/web/user/UCenterController.java index 601716a09d..e07337902e 100644 --- a/src/main/java/com/rebuild/web/user/UCenterController.java +++ b/src/main/java/com/rebuild/web/user/UCenterController.java @@ -8,15 +8,17 @@ package com.rebuild.web.user; import cn.devezhao.commons.CodecUtils; -import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSONObject; import com.rebuild.api.RespBody; import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.support.License; -import com.rebuild.core.support.i18n.Language; import com.rebuild.utils.RbAssert; import com.rebuild.web.BaseController; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; @@ -32,8 +34,7 @@ public class UCenterController extends BaseController { @PostMapping("/bind") public RespBody bindCloudAccount(@RequestBody JSONObject body, HttpServletRequest request) { - final ID user = getRequestUser(request); - RbAssert.isAllow(UserHelper.isSuperAdmin(user), Language.L("仅超级管理员可操作")); + RbAssert.isSuperAdmin(getRequestUser(request)); String account = body.getString("cloudAccount"); String passwd = body.getString("cloudPasswd"); diff --git a/src/main/java/com/rebuild/web/user/UserSettingsController.java b/src/main/java/com/rebuild/web/user/UserSettingsController.java index d2a3b317d3..13fd19c024 100644 --- a/src/main/java/com/rebuild/web/user/UserSettingsController.java +++ b/src/main/java/com/rebuild/web/user/UserSettingsController.java @@ -13,6 +13,7 @@ import cn.devezhao.persist4j.Record; import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSONObject; +import com.rebuild.api.Controller; import com.rebuild.api.RespBody; import com.rebuild.core.Application; import com.rebuild.core.DefinedException; @@ -26,7 +27,9 @@ import com.rebuild.core.support.i18n.I18nUtils; import com.rebuild.core.support.i18n.Language; import com.rebuild.core.support.integration.SMSender; +import com.rebuild.rbv.integration.ExternalUserAuth; import com.rebuild.web.BaseController; +import com.rebuild.web.user.signup.LoginAction; import com.rebuild.web.user.signup.LoginController; import org.apache.commons.lang.StringUtils; import org.springframework.web.bind.annotation.GetMapping; @@ -37,6 +40,7 @@ import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.Date; /** @@ -77,6 +81,11 @@ public ModelAndView pageUser(HttpServletRequest request) { .unique(); if (wxworkUser != null) mv.getModelMap().put("wxworkUser", wxworkUser[0]); } + String feishuAppid = RebuildConfiguration.get(ConfigurationItem.FeishuAppId); + if (feishuAppid != null) { + String feishuUser = ExternalUserAuth.getExternalUserId(ub.getId(), feishuAppid); + if (feishuUser != null) mv.getModelMap().put("feishuUser", feishuUser); + } return mv; } @@ -122,7 +131,7 @@ public RespBody saveEmail(HttpServletRequest request) { } @PostMapping("/user/save-passwd") - public RespBody savePasswd(HttpServletRequest request) { + public RespBody savePasswd(HttpServletRequest request, HttpServletResponse response) { final ID user = getRequestUser(request); JSONObject p = (JSONObject) ServletUtils.getRequestJson(request); @@ -134,7 +143,14 @@ public RespBody savePasswd(HttpServletRequest request) { return RespBody.errorl("原密码输入有误"); } - return savePasswd(user, newp); + RespBody res = savePasswd(user, newp); + if (res.getErrorCode() == Controller.CODE_OK) { + try { + ServletUtils.removeCookie(request, response, LoginAction.CK_AUTOLOGIN); + request.getSession().invalidate(); + } catch (Exception ignored) {} + } + return res; } @GetMapping("/user/login-logs") @@ -183,10 +199,11 @@ private RespBody savePasswd(ID user, String password) { @PostMapping("/cancel-external-user") public RespBody cancelExternalUser(HttpServletRequest request) { int appType = getIntParameter(request, "type", 0); - // 1=Dingtalk, 2=Wxwork + // 1=Dingtalk, 2=Wxwork, 3=Feishu String appId = appType == 1 ? RebuildConfiguration.get(ConfigurationItem.DingtalkCorpid) : RebuildConfiguration.get(ConfigurationItem.WxworkCorpid); + if (appType == 3) appId = RebuildConfiguration.get(ConfigurationItem.FeishuAppId); Object[] externalUser = Application.createQueryNoFilter( "select userId from ExternalUser where bindUser = ? and appId = ?") diff --git a/src/main/java/com/rebuild/web/user/signup/LoginAction.java b/src/main/java/com/rebuild/web/user/signup/LoginAction.java index 5a7df673f6..8c5107ea08 100644 --- a/src/main/java/com/rebuild/web/user/signup/LoginAction.java +++ b/src/main/java/com/rebuild/web/user/signup/LoginAction.java @@ -27,7 +27,6 @@ import com.rebuild.core.support.task.TaskExecutors; import com.rebuild.web.BaseController; import com.rebuild.web.user.UserAvatar; -import eu.bitwalker.useragentutils.DeviceType; import eu.bitwalker.useragentutils.OperatingSystem; import eu.bitwalker.useragentutils.UserAgent; import lombok.extern.slf4j.Slf4j; @@ -109,13 +108,14 @@ private Integer loginSuccessed(HttpServletRequest request, HttpServletResponse r ServletUtils.removeCookie(request, response, CK_AUTOLOGIN); } - createLoginLog(request, user); + LoginChannel loginChannel = createLoginLog(request, user, fromH5); - // TODO H5 - if (!fromH5) { + if (fromH5) { + Application.getSessionStore().storeLoginSuccessed(user, request, loginChannel); + } else { ServletUtils.setSessionAttribute(request, WebUtils.CURRENT_USER, user); ServletUtils.setSessionAttribute(request, SK_USER_THEME, KVStorage.getCustomValue("THEME." + user)); - Application.getSessionStore().storeLoginSuccessed(request, fromH5); + Application.getSessionStore().storeLoginSuccessed(user, request, loginChannel); // 头像缓存 ServletUtils.setSessionAttribute(request, UserAvatar.SK_DAVATAR, System.currentTimeMillis()); @@ -149,45 +149,49 @@ private Integer loginSuccessed(HttpServletRequest request, HttpServletResponse r * * @param request * @param user + * @param fromH5 + * @return */ - private void createLoginLog(HttpServletRequest request, ID user) { + private LoginChannel createLoginLog(HttpServletRequest request, ID user, boolean fromH5) { final String userAgent = request.getHeader("user-agent"); + String uaSimple; + LoginChannel loginChannel; try { - final UserAgent UA = UserAgent.parseUserAgentString(userAgent); - - uaSimple = UA.getBrowser().name(); - if (UA.getBrowserVersion() != null) { - String mv = UA.getBrowserVersion().getMajorVersion(); + UserAgent uaObj = UserAgent.parseUserAgentString(userAgent); + uaSimple = uaObj.getBrowser().name(); + if (uaObj.getBrowserVersion() != null) { + String mv = uaObj.getBrowserVersion().getMajorVersion(); if (!uaSimple.endsWith(mv)) uaSimple += "-" + mv; } - OperatingSystem os = UA.getOperatingSystem(); - if (os != null) { - uaSimple += " (" + os + ")"; - if (os.getDeviceType() != null && os.getDeviceType() == DeviceType.MOBILE) uaSimple += " [Mobile]"; + OperatingSystem osObj = uaObj.getOperatingSystem(); + if (osObj != null) { + uaSimple += " (" + osObj + ")"; } + loginChannel = LoginChannel.parse(userAgent, fromH5); + uaSimple += " [" + loginChannel.name() + "]"; if (request.getAttribute(SK_TEMP_AUTH) != null) uaSimple += " [TempAuth]"; - if (userAgent.contains("DingTalk")) uaSimple += " [DingTalk]"; - if (userAgent.contains("wxwork")) uaSimple += " [WeCom]"; } catch (Exception ex) { - log.warn("Unknown User-Agent : {}", userAgent); + log.warn("Unknown user-agent : {}", userAgent); + uaSimple = "UNKNOW"; + loginChannel = LoginChannel.PC_WEB; } final String ipAddr = StringUtils.defaultString(ServletUtils.getRemoteAddr(request), "127.0.0.1"); final String reqUrl = request.getRequestURL() == null ? "" : request.getRequestURL().toString(); - final Record llog = EntityHelper.forNew(EntityHelper.LoginLog, UserService.SYSTEM_USER); - llog.setID("user", user); - llog.setString("ipAddr", ipAddr); - llog.setString("userAgent", uaSimple); - llog.setDate("loginTime", CalendarUtils.now()); + final Record lgLog = EntityHelper.forNew(EntityHelper.LoginLog, UserService.SYSTEM_USER); + lgLog.setID("user", user); + lgLog.setString("ipAddr", ipAddr); + lgLog.setString("userAgent", uaSimple); + lgLog.setDate("loginTime", CalendarUtils.now()); TaskExecutors.queue(() -> { - Application.getCommonsService().create(llog); + Application.getCommonsService().create(lgLog); User u = Application.getUserStore().getUser(user); String uid = StringUtils.defaultString(u.getEmail(), u.getName()); @@ -198,5 +202,7 @@ private void createLoginLog(HttpServletRequest request, ID user) { CodecUtils.base64UrlEncode(reqUrl)); License.siteApiNoCache(uaUrl); }); + + return loginChannel; } } diff --git a/src/main/java/com/rebuild/web/user/signup/LoginChannel.java b/src/main/java/com/rebuild/web/user/signup/LoginChannel.java new file mode 100644 index 0000000000..76276ca467 --- /dev/null +++ b/src/main/java/com/rebuild/web/user/signup/LoginChannel.java @@ -0,0 +1,90 @@ +/*! +Copyright (c) REBUILD and/or its owners. All rights reserved. + +rebuild is dual-licensed under commercial and open source licenses (GPLv3). +See LICENSE and COMMERCIAL in the project root for license information. +*/ + +package com.rebuild.web.user.signup; + +import eu.bitwalker.useragentutils.DeviceType; +import eu.bitwalker.useragentutils.OperatingSystem; +import eu.bitwalker.useragentutils.UserAgent; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * 登录渠道/类型 + * + * @author devezhao + * @since 2024/11/15 + */ +@Slf4j +public enum LoginChannel { + + PC_WEB("PC浏览器"), + PC_DINGTALK("PC钉钉"), + PC_WECOM("PC企业微信"), + PC_WECHAT("PC微信"), + PC_FEISHU("PC飞书"), + PC_DESKTOP("PC桌面"), + + MOB_WEB("手机浏览器"), + MOB_DINGTALK("手机钉钉"), + MOB_WECOM("手机企业微信"), + MOB_WECHAT("手机微信"), + MOB_FEISHU("手机飞书"), + MOB_ANDROID("手机APP"), + MOB_IOS("iOS APP"), // 保留 + + ; + + @Getter + private final String displayName; + LoginChannel(String name) { + this.displayName = name; + } + + /** + * @param userAgent + * @return + */ + public static LoginChannel parse(String userAgent) { + return parse(userAgent, false); + } + + /** + * @param userAgent + * @param forceH5 + * @return + */ + public static LoginChannel parse(String userAgent, boolean forceH5) { + userAgent = userAgent.toUpperCase(); + UserAgent UA = UserAgent.parseUserAgentString(userAgent); + OperatingSystem OS = UA.getOperatingSystem(); + + boolean isDingtalk = userAgent.contains("DINGTALK"); + boolean isWecom = userAgent.contains("WXWORK"); + boolean isWechat = !isWecom && userAgent.contains("MICROMESSENGER"); + boolean isFeishu = userAgent.contains("LARK"); + boolean isH5PlusApp = userAgent.contains("HTML5PLUS"); + + if ((OS != null && OS.getDeviceType() == DeviceType.MOBILE) || forceH5) { + if (isDingtalk) return MOB_DINGTALK; + else if (isWecom) return MOB_WECOM; + else if (isWechat) return MOB_WECHAT; + else if (isFeishu) return MOB_FEISHU; + else if (isH5PlusApp) return MOB_ANDROID; + return MOB_WEB; + } + + boolean isDesktop = userAgent.contains("ELECTRON"); + + if (isDingtalk) return PC_DINGTALK; + else if (isWecom) return PC_WECOM; + else if (isWechat) return PC_WECHAT; + else if (isFeishu) return PC_FEISHU; + else if (isDesktop) return PC_DESKTOP; + return PC_WEB; + } +} diff --git a/src/main/java/com/rebuild/web/user/signup/LoginController.java b/src/main/java/com/rebuild/web/user/signup/LoginController.java index ce33a6568a..0c9fefc2a8 100644 --- a/src/main/java/com/rebuild/web/user/signup/LoginController.java +++ b/src/main/java/com/rebuild/web/user/signup/LoginController.java @@ -122,9 +122,12 @@ public ModelAndView checkLogin(HttpServletRequest request, HttpServletResponse r mv.getModel().put("ssoDingtalk", RebuildConfiguration.get(ConfigurationItem.DingtalkAppkey)); // WxWork mv.getModel().put("ssoWxwork", RebuildConfiguration.get(ConfigurationItem.WxworkCorpid)); + // Feishu + mv.getModel().put("ssoFeishu", RebuildConfiguration.get(ConfigurationItem.FeishuAppId)); } else { mv.getModel().put("ssoDingtalk", "#"); mv.getModel().put("ssoWxwork", "#"); +// mv.getModel().put("ssoFeishu", "#"); } mv.getModelMap().put("UsersMsg", SysbaseHeartbeat.getUsersDanger()); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8b380b57d6..c563bd297f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,7 +14,7 @@ server: cookie: same-site: lax name: RBSESSION - timeout: 7200 + timeout: 120m error: whitelabel.enabled: false tomcat: diff --git a/src/main/resources/i18n/lang.zh_CN.json b/src/main/resources/i18n/lang.zh_CN.json index 4b82836c99..9cd78b9e38 100644 --- a/src/main/resources/i18n/lang.zh_CN.json +++ b/src/main/resources/i18n/lang.zh_CN.json @@ -3257,5 +3257,83 @@ "尝试使用连续日期":"尝试使用连续日期", "尝试使用日期对比":"尝试使用日期对比", "选择操作":"选择操作", - "倒序":"倒序" + "倒序":"倒序", + "关闭限流":"关闭限流", + "显示扩展按钮":"显示扩展按钮", + "查看角色权限":"查看角色权限", + "本次将批量转换 %d 条记录":"本次将批量转换 %d 条记录", + "起始实体":"起始实体", + "放在指定 [文件](../../../files/docs) 目录中":"放在指定 [文件](../../../files/docs) 目录中", + "此触发器已在执行中,不能同时执行。是否显示执行状态?":"此触发器已在执行中,不能同时执行。是否显示执行状态?", + "确认转换?":"确认转换?", + "万":"万", + "快速编辑":"快速编辑", + "无效 AUTHCODE,请重新打开应用":"无效 AUTHCODE,请重新打开应用", + "允许自定义仪表盘、图表":"允许自定义仪表盘、图表", + "通讯录":"通讯录", + "非超级管理员用户":"非超级管理员用户", + "主页":"主页", + "所有用户均已读":"所有用户均已读", + "数据列表行":"数据列表行", + "仅使用“通过字段匹配”时有效":"仅使用“通过字段匹配”时有效", + "使用报表模版":"使用报表模版", + "暂无可用":"暂无可用", + "转换新明细记录时需要选择主记录":"转换新明细记录时需要选择主记录", + "绑定 REBUILD 云账号,便捷使用服务与资源,还有免费系统安全与健康检测。":"绑定 REBUILD 云账号,便捷使用服务与资源,还有免费系统安全与健康检测。", + "选择起始实体":"选择起始实体", + "重定向 URL":"重定向 URL", + "更新时保持同步":"更新时保持同步", + "保存并新建":"保存并新建", + "已关闭":"已关闭", + "修改值":"修改值", + "将使用你的飞书账号信息自动注册,是否继续?":"将使用你的飞书账号信息自动注册,是否继续?", + "发布日期":"发布日期", + "千万":"千万", + "详情页":"详情页", + "已有记录不能是当前记录":"已有记录不能是当前记录", + "仅当启用“更新时”可用":"仅当启用“更新时”可用", + "飞书授权":"飞书授权", + "单选框":"单选框", + "选择**非超级管理员**所能使用的管理中心功能":"选择**非超级管理员**所能使用的管理中心功能", + "切换视图":"切换视图", + "还有 %d 人未读":"还有 %d 人未读", + "亿":"亿", + "更新同步":"更新同步", + "显示格式":"显示格式", + "显示顺序":"显示顺序", + "数字单位":"数字单位", + "千":"千", + "配置管理中心功能":"配置管理中心功能", + "密码修改后需要重新登录":"密码修改后需要重新登录", + "百万":"百万", + "选择系统模版":"选择系统模版", + "已开启":"已开启", + "数据列表页":"数据列表页", + "免费版不支持飞书集成 [(查看详情)](https://getrebuild.com/docs/rbv-features)":"免费版不支持飞书集成 [(查看详情)](https://getrebuild.com/docs/rbv-features)", + "确认修改吗?":"确认修改吗?", + "成功转换 %d 条记录":"成功转换 %d 条记录", + "查看未读用户":"查看未读用户", + "切换视图为卡片模式或列表模式":"切换视图为卡片模式或列表模式", + "修改哪些字段":"修改哪些字段", + "仅数字":"仅数字", + "部分必填字段未映射,可能导致直接转换失败":"部分必填字段未映射,可能导致直接转换失败", + "飞书":"飞书", + "新记录":"新记录", + "需要先添加 [记录转换](../../robot/transforms) 才能在此处选择":"需要先添加 [记录转换](../../robot/transforms) 才能在此处选择", + "本公告要求已读":"本公告要求已读", + "自定":"自定", + "保存并添加":"保存并添加", + "H5 可信域名":"H5 可信域名", + "限流":"限流", + "含分类管理":"含分类管理", + "转换":"转换", + "所在部门":"所在部门", + "请选择已有记录":"请选择已有记录", + "十万":"十万", + "选择已有记录":"选择已有记录", + "开始安装":"开始安装", + "飞书侧配置":"飞书侧配置", + "已有记录":"已有记录", + "存在同名字段,是否继续添加?":"存在同名字段,是否继续添加?", + "安装系统模版将清空您现有系统的所有数据,包括系统配置、业务实体、数据等。安装前强烈建议您做好系统备份。":"安装系统模版将清空您现有系统的所有数据,包括系统配置、业务实体、数据等。安装前强烈建议您做好系统备份。" } \ No newline at end of file diff --git a/src/main/resources/metadata-conf.xml b/src/main/resources/metadata-conf.xml index 27f6be08a4..9613f6b9e2 100644 --- a/src/main/resources/metadata-conf.xml +++ b/src/main/resources/metadata-conf.xml @@ -23,6 +23,7 @@ + @@ -37,6 +38,7 @@ + @@ -444,7 +446,6 @@ - @@ -590,6 +591,7 @@ + diff --git a/src/main/resources/scripts/db-init.sql b/src/main/resources/scripts/db-init.sql index 04e55b04ea..1dc803b14c 100644 --- a/src/main/resources/scripts/db-init.sql +++ b/src/main/resources/scripts/db-init.sql @@ -32,6 +32,7 @@ create table if not exists `user` ( `ROLE_ID` char(20) comment '角色', `IS_DISABLED` char(1) default 'F' comment '是否禁用', `QUICK_CODE` varchar(50), + `SEQ` int(11) default '0' comment '排序 (小到大)', `EXTERNAL_ID` varchar(100) comment '外部用户ID', `MODIFIED_ON` timestamp not null default current_timestamp comment '修改时间', `MODIFIED_BY` char(20) not null comment '修改人', @@ -52,6 +53,7 @@ create table if not exists `department` ( `PRINCIPAL_ID` char(20) comment '负责人', `IS_DISABLED` char(1) default 'F' comment '是否禁用', `QUICK_CODE` varchar(50), + `SEQ` int(11) default '0' comment '排序 (小到大)', `EXTERNAL_ID` varchar(100) comment '外部部门ID', `MODIFIED_ON` timestamp not null default current_timestamp comment '修改时间', `MODIFIED_BY` char(20) not null comment '修改人', @@ -337,7 +339,7 @@ create table if not exists `attachment` ( `RELATED_RECORD` char(20) comment '相关记录', `FILE_PATH` varchar(300) not null comment '文件路径', `FILE_TYPE` varchar(20) comment '文件类型', - `FILE_SIZE` bigint(20) default '0' comment '执行顺序', + `FILE_SIZE` bigint(20) default '0' comment '文件大小', `FILE_NAME` varchar(100) comment '文件名称', `IN_FOLDER` char(20) comment '所在目录', `IS_DELETED` char(1) default 'F' comment '是否删除', @@ -787,6 +789,7 @@ create table if not exists `extform_config` ( `BIND_USER` char(20) comment '数据绑定用户', `HOOK_URL` varchar(300) comment '回调地址', `HOOK_SECRET` varchar(300) comment '回调安全码', + `NO_RATE_LIMITER` char(1) default 'F' comment '关闭限流', `CREATED_BY` char(20) not null comment '创建人', `CREATED_ON` timestamp not null default current_timestamp comment '创建时间', `MODIFIED_ON` timestamp not null default current_timestamp comment '修改时间', @@ -907,13 +910,13 @@ insert into `team_member` (`MEMBER_ID`, `TEAM_ID`, `USER_ID`) -- Layouts insert into `layout_config` (`CONFIG_ID`, `BELONG_ENTITY`, `CONFIG`, `APPLY_TYPE`, `SHARE_TO`, `CREATED_ON`, `CREATED_BY`, `MODIFIED_ON`, `MODIFIED_BY`) values - ('013-9000000000000001', 'Department', '[{"field":"name"},{"field":"principalId"},{"field":"parentDept"},{"field":"isDisabled"}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), - ('013-9000000000000002', 'User', '[{"field":"fullName"},{"field":"jobTitle"},{"field":"workphone"},{"field":"email"},{"field":"loginName"},{"field":"password"},{"field":"$DIVIDER$"},{"field":"deptId"},{"field":"roleId"},{"field":"isDisabled"}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), - ('013-9000000000000003', 'Role', '[{"field":"name"},{"field":"isDisabled"}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), - ('013-9000000000000004', 'Team', '[{"field":"name"},{"field":"principalId"},{"field":"isDisabled"}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), - ('013-9000000000000005', 'User', '[{"field":"fullName"},{"field":"jobTitle"},{"field":"deptId"},{"field":"workphone"},{"field":"email"},{"field":"loginName"},{"field":"roleId"},{"field":"isDisabled"},{"field":"createdOn"}]', 'DATALIST', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), - ('013-9000000000000006', 'Department', '[{"field":"name"},{"field":"principalId"},{"field":"parentDept"},{"field":"isDisabled"},{"field":"createdOn"}]', 'DATALIST', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), - ('013-9000000000000007', 'Team', '[{"field":"name"},{"field":"principalId"},{"field":"isDisabled"},{"field":"createdOn"}]', 'DATALIST', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'); + ('013-0000000000000001', 'Department', '[{"field":"name"},{"field":"principalId"},{"field":"parentDept"},{"field":"isDisabled"}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), + ('013-0000000000000002', 'User', '[{"field":"fullName"},{"field":"jobTitle"},{"field":"workphone"},{"field":"email"},{"field":"loginName"},{"field":"password"},{"field":"$DIVIDER$"},{"field":"deptId"},{"field":"roleId"},{"field":"isDisabled"}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), + ('013-0000000000000003', 'Role', '[{"field":"name"},{"field":"isDisabled"}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), + ('013-0000000000000004', 'Team', '[{"field":"name"},{"field":"principalId"},{"field":"isDisabled"}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), + ('013-0000000000000005', 'User', '[{"field":"fullName"},{"field":"jobTitle"},{"field":"deptId"},{"field":"workphone"},{"field":"email"},{"field":"loginName"},{"field":"roleId"},{"field":"isDisabled"},{"field":"createdOn"}]', 'DATALIST', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), + ('013-0000000000000006', 'Department', '[{"field":"name"},{"field":"principalId"},{"field":"parentDept"},{"field":"isDisabled"},{"field":"createdOn"}]', 'DATALIST', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'), + ('013-0000000000000007', 'Team', '[{"field":"name"},{"field":"principalId"},{"field":"isDisabled"},{"field":"createdOn"}]', 'DATALIST', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'); -- Classifications (No data) insert into `classification` (`DATA_ID`, `NAME`, `DESCRIPTION`, `OPEN_LEVEL`, `IS_DISABLED`, `CREATED_ON`, `CREATED_BY`, `MODIFIED_ON`, `MODIFIED_BY`) @@ -924,13 +927,16 @@ insert into `classification` (`DATA_ID`, `NAME`, `DESCRIPTION`, `OPEN_LEVEL`, `I -- Projects insert into `project_config` (`CONFIG_ID`, `PROJECT_NAME`, `PROJECT_CODE`, `PRINCIPAL`, `MEMBERS`, `SCOPE`, `CREATED_BY`, `CREATED_ON`, `MODIFIED_BY`, `MODIFIED_ON`) values - ('050-0000000000000001', 'RB示例项目', 'RB', '001-0000000000000001', '001-9000000000000001', 1, '001-0000000000000000', CURRENT_TIMESTAMP, '001-0000000000000000', CURRENT_TIMESTAMP); + ('050-9000000000000001', 'RB示例项目', 'RB', '001-0000000000000001', '001-9000000000000001', 1, '001-0000000000000000', CURRENT_TIMESTAMP, '001-0000000000000000', CURRENT_TIMESTAMP); insert into `project_plan_config` (`CONFIG_ID`, `PROJECT_ID`, `PLAN_NAME`, `SEQ`, `FLOW_STATUS`, `FLOW_NEXTS`) values - ('051-0000000000000001', '050-0000000000000001', '待处理', 1000, 1, '051-0000000000000002,051-0000000000000003'), - ('051-0000000000000002', '050-0000000000000001', '进行中', 2000, 2, '051-0000000000000001,051-0000000000000003'), - ('051-0000000000000003', '050-0000000000000001', '已完成', 3000, 3, '051-0000000000000001,051-0000000000000002'); + ('051-9000000000000001', '050-9000000000000001', '待处理', 1000, 1, '051-9000000000000002,051-9000000000000003'), + ('051-9000000000000002', '050-9000000000000001', '进行中', 2000, 2, '051-9000000000000001,051-9000000000000003'), + ('051-9000000000000003', '050-9000000000000001', '已完成', 3000, 3, '051-9000000000000001,051-9000000000000002'); +insert into `project_task` (`TASK_ID`, `PROJECT_ID`, `PROJECT_PLAN_ID`, `TASK_NUMBER`, `TASK_NAME`, `SEQ`, `CREATED_BY`, `CREATED_ON`, `MODIFIED_BY`, `MODIFIED_ON`) + values + ('052-9000000000000001', '050-9000000000000001', '051-9000000000000001', 1, 'RB示例任务', 1000, '001-0000000000000000', CURRENT_TIMESTAMP, '001-0000000000000000', CURRENT_TIMESTAMP); -- DB Version (see `db-upgrade.sql`) insert into `system_config` (`CONFIG_ID`, `ITEM`, `VALUE`) - values ('021-9000000000000001', 'DBVer', 58); + values ('021-9000000000000001', 'DBVer', 60); diff --git a/src/main/resources/scripts/db-upgrade.sql b/src/main/resources/scripts/db-upgrade.sql index 4c57a2d300..df4425c4c5 100644 --- a/src/main/resources/scripts/db-upgrade.sql +++ b/src/main/resources/scripts/db-upgrade.sql @@ -1,6 +1,16 @@ -- Database upgrade scripts for rebuild 1.x and 2.x -- Each upgraded starts with `-- #VERSION` +-- #60 (v3.9) +alter table `user` + add column `SEQ` int(11) default '0' comment '排序 (小到大)'; +alter table `department` + add column `SEQ` int(11) default '0' comment '排序 (小到大)'; + +-- #59 (v3.9) +alter table `extform_config` + add column `NO_RATE_LIMITER` char(1) default 'F' comment '关闭限流'; + -- #58 (v3.8) alter table `classification_data` add column `COLOR` varchar(10); @@ -450,7 +460,7 @@ insert into `team` (`TEAM_ID`, `NAME`, `CREATED_ON`, `CREATED_BY`, `MODIFIED_ON` ('006-9000000000000001', 'RB示例团队', CURRENT_TIMESTAMP, '001-0000000000000000', CURRENT_TIMESTAMP, '001-0000000000000000', 'RBSLTD'); insert into `layout_config` (`CONFIG_ID`, `BELONG_ENTITY`, `CONFIG`, `APPLY_TYPE`, `SHARE_TO`, `CREATED_ON`, `CREATED_BY`, `MODIFIED_ON`, `MODIFIED_BY`) values - ('013-9000000000000004', 'Team', '[{"field":"name","isFull":false},{"field":"isDisabled","isFull":false}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'); + ('013-0000000000000004', 'Team', '[{"field":"name","isFull":false},{"field":"isDisabled","isFull":false}]', 'FORM', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'); -- Principal alter table `department` @@ -740,4 +750,4 @@ create table if not exists `login_log` ( index IX0_login_log (`USER`, `LOGIN_TIME`) )Engine=InnoDB; insert into `layout_config` (`CONFIG_ID`, `BELONG_ENTITY`, `CONFIG`, `APPLY_TYPE`, `SHARE_TO`, `CREATED_ON`, `CREATED_BY`, `MODIFIED_ON`, `MODIFIED_BY`) - values ('013-9000000000000005', 'LoginLog', '[{"field":"user"},{"field":"loginTime"},{"field":"userAgent"},{"field":"ipAddr"},{"field":"logoutTime"}]', 'DATALIST', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'); + values ('013-0000000000000005', 'LoginLog', '[{"field":"user"},{"field":"loginTime"},{"field":"userAgent"},{"field":"ipAddr"},{"field":"logoutTime"}]', 'DATALIST', 'ALL', CURRENT_TIMESTAMP, '001-0000000000000001', CURRENT_TIMESTAMP, '001-0000000000000001'); diff --git a/src/main/resources/web/_include/forms.html b/src/main/resources/web/_include/forms.html index 0f1503a22f..b5243087a8 100644 --- a/src/main/resources/web/_include/forms.html +++ b/src/main/resources/web/_include/forms.html @@ -1,6 +1,7 @@ + diff --git a/src/main/resources/web/_include/nav-left-admin.html b/src/main/resources/web/_include/nav-left-admin.html index c2606cf4d4..ff9164564f 100644 --- a/src/main/resources/web/_include/nav-left-admin.html +++ b/src/main/resources/web/_include/nav-left-admin.html @@ -30,6 +30,9 @@
  • [[${bundle.L('钉钉')}]]
  • +
  • + [[${bundle.L('飞书')}]] +
  • @@ -98,6 +101,9 @@ +
    + +
    diff --git a/src/main/resources/web/_include/nav-top.html b/src/main/resources/web/_include/nav-top.html index 6a271f5fb7..d0f2ff5c0f 100644 --- a/src/main/resources/web/_include/nav-top.html +++ b/src/main/resources/web/_include/nav-top.html @@ -58,7 +58,7 @@ ').appendTo($ol) + $li.text(item[0]) + }) + location.hash = `!/Department/${item.id}` + }} + />, + 'navTree', + function () { + _AsideTree = this + } + ) + }) +} + +function _findPaths(active, into) { + const $a = active.find('>a') + into.unshift([$a.text(), $a.data('id')]) + const $li = active.parent('ul').prev('li') + if ($li.length > 0) _findPaths($li, into) +} diff --git a/src/main/resources/web/assets/js/feeds/announcement.js b/src/main/resources/web/assets/js/feeds/announcement.js index a17c6db923..9adc8f450e 100644 --- a/src/main/resources/web/assets/js/feeds/announcement.js +++ b/src/main/resources/web/assets/js/feeds/announcement.js @@ -33,12 +33,12 @@ class AnnouncementModal extends React.Component { {state.readState && ( {state.readState === 1 && ( - this._makeRead()}> + this._makeRead()}> {$L('点击已读')} )} {typeof state.readState === 'string' && ( - + {$L('已读')} )} @@ -89,8 +89,10 @@ const $showAnnouncement = function () { $.get('/commons/announcements', (res) => { if (res.error_code !== 0 || !res.data || res.data.length === 0) return - const shows = res.data.map((item) => { + let popup39 = [] + const anShows = res.data.map((item) => { const stateClazz = item.readState === 1 ? 'read-state1' : typeof item.readState === 'string' ? 'read-state2' : null + if (item.readState === 1) popup39.push(`anno-${item.id}`) return (
    renderRbcomp()}> @@ -99,10 +101,18 @@ const $showAnnouncement = function () { ) }) - renderRbcomp({shows}, $aw, function () { + renderRbcomp({anShows}, $aw, function () { $(this) .find('p>a[href]') .on('click', (e) => e.stopPropagation()) + + // v3.9 弹窗 + // 登录页不弹、已读不弹 + if (location.pathname.includes('/login')) popup39 = [] + if (location.pathname.includes('/feeds/home')) $('.side-wrapper').css('position', 'static') + setTimeout(() => { + popup39.forEach((p) => $('#' + p)[0].click()) + }, 600) }) }) } diff --git a/src/main/resources/web/assets/js/feeds/feeds-list.js b/src/main/resources/web/assets/js/feeds/feeds-list.js index 887babd879..5859e32d08 100644 --- a/src/main/resources/web/assets/js/feeds/feeds-list.js +++ b/src/main/resources/web/assets/js/feeds/feeds-list.js @@ -147,7 +147,7 @@ class FeedsList extends React.Component { )}

    - {__renderRichContent(item, this._inView)} + {_renderRichContent(item, this._inView)} @@ -218,8 +218,9 @@ class FeedsList extends React.Component { this.setState({ data: _data.data, focusFeed: firstFetch ? s.focusFeed : null }, () => { $(this._$feedsList) .find('.rich-content .texts a[data-uid]') - // eslint-disable-next-line no-undef - .each((idx, item) => UserPopup.create(item)) + .on('click', function () { + window.open(`${rb.baseUrl}/contacts/home#gs=${encodeURIComponent($(this).text().substr(1))}`) + }) }) }) } @@ -360,7 +361,7 @@ class FeedsComments extends React.Component { - {__renderRichContent(item)} + {_renderRichContent(item)}
    @@ -423,8 +424,9 @@ class FeedsComments extends React.Component { this.setState({ data: _data.data }, () => { $(this._$commentsList) .find('.rich-content .texts a[data-uid]') - // eslint-disable-next-line no-undef - .each((idx, item) => UserPopup.create(item)) + .on('click', function () { + window.open(`${rb.baseUrl}/contacts/home#gs=${encodeURIComponent($(this).text().substr(1))}`) + }) }) }) } @@ -573,7 +575,7 @@ class Pagination extends React.Component { } // 渲染动态内容 -function __renderRichContent(e, _inView) { +function _renderRichContent(e, _inView) { // 表情和换行不在后台转换,因为不同客户端所需的格式不同 const contentHtml = $converEmoji(e.content.replace(/\n/g, '
    ')) const contentMore = e.contentMore || {} @@ -673,6 +675,13 @@ function __renderRichContent(e, _inView) { {e.readStatus.map((item) => { return })} + {rb.isAdminUser && ( + renderRbcomp()}> +
    + +
    +
    + )}
    )} @@ -765,3 +774,32 @@ function _updateCommentsNum(id, comp, num) { }) comp.setState({ data: _data }) } + +// 公告未读用户 +class AnnouncementUnreadView extends RbAlert { + renderContent() { + if (!this.state.unread) return null + + if (this.state.unread.length === 0) { + return + } + + return ( +
    +
    + {$L('还有 %d 人未读', this.state.unread.length)} +
    + {this.state.unread.map((item) => { + return + })} +
    + ) + } + + componentDidMount() { + super.componentDidMount() + $.get('/feeds/announcement-read-status?id=' + this.props.id, (res) => { + this.setState({ ...res.data }) + }) + } +} diff --git a/src/main/resources/web/assets/js/file-preview.js b/src/main/resources/web/assets/js/file-preview.js index cc3075b50e..87dea0d836 100644 --- a/src/main/resources/web/assets/js/file-preview.js +++ b/src/main/resources/web/assets/js/file-preview.js @@ -9,7 +9,7 @@ See LICENSE and COMMERCIAL in the project root for license information. const TYPE_TEXTS = ['.txt', '.xml', '.json', '.md', '.yml', '.css', '.js', '.htm', '.html', '.log', '.sql', '.conf', '.sh', '.bat', '.java', '.ini'] const TYPE_DOCS = ['.doc', '.docx', '.rtf', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf'] -const TYPE_IMGS = ['.jpg', '.jpeg', '.gif', '.png', '.bmp', '.jfif', '.svg', '.webp'] +const TYPE_IMGS = ['.jpg', '.jpeg', '.gif', '.png', '.bmp', '.jfif', '.webp'] const TYPE_AUDIOS = ['.mp3', '.wma', '.m4a', '.flac', '.ogg', '.acc'] const TYPE_VIDEOS = ['.mp4', '.wmv', '.mov', '.avi', '.mkv', '.webm', '.m4v', '.mpg', '.mpge'] diff --git a/src/main/resources/web/assets/js/files/files-attachment.js b/src/main/resources/web/assets/js/files/files-attachment.js index dd00693bc5..4ff78ecedb 100644 --- a/src/main/resources/web/assets/js/files/files-attachment.js +++ b/src/main/resources/web/assets/js/files/files-attachment.js @@ -21,7 +21,7 @@ const EntityTree = { activeItem={__DEFAULT_ALL} onItemClick={(item) => { filesList && filesList.loadData(item.id) - $('.file-path .active').text(item.text) + $('.file-path li').text(item.text) location.hash = `!/Entity/${item.id}` }} />, diff --git a/src/main/resources/web/assets/js/files/files-docs.js b/src/main/resources/web/assets/js/files/files-docs.js index dfc85ed651..90e71b924b 100644 --- a/src/main/resources/web/assets/js/files/files-docs.js +++ b/src/main/resources/web/assets/js/files/files-docs.js @@ -39,7 +39,7 @@ const FolderTree = { const $ol = $('.file-path ol').empty() paths.forEach((item) => { - const $li = $('').appendTo($ol) + const $li = $('').appendTo($ol) $li.text(item[0]) }) location.hash = `!/Folder/${item.id}` diff --git a/src/main/resources/web/assets/js/files/files.js b/src/main/resources/web/assets/js/files/files.js index 58a5351fab..85e245533e 100644 --- a/src/main/resources/web/assets/js/files/files.js +++ b/src/main/resources/web/assets/js/files/files.js @@ -56,10 +56,10 @@ class FilesList extends React.Component { {this.state.currentSize >= PAGE_SIZE && (
    { + $stopEvent(e, true) this.loadData(null, this._pageNo + 1) - e.preventDefault() }}> {$L('显示更多')} diff --git a/src/main/resources/web/assets/js/general/list-fields.js b/src/main/resources/web/assets/js/general/list-fields.js index 4917e416aa..dc2f7d5d4c 100644 --- a/src/main/resources/web/assets/js/general/list-fields.js +++ b/src/main/resources/web/assets/js/general/list-fields.js @@ -51,7 +51,8 @@ $(document).ready(() => { $.get(`${settingsUrl}/alist`, (res) => { const ccfg = res.data.find((x) => x[0] === cfgid) if (rb.isAdminUser) { - renderRbcomp(, 'shareTo', function () { + const shareTo39 = _data.configId ? _data.shareTo : 'ALL' + renderRbcomp(, 'shareTo', function () { shareToComp = this }) } else { @@ -168,7 +169,6 @@ render_item_after = function ($item) { refreshConfigStar() }} />, - null, function () { ShowStyles_Comps[fkey] = this } diff --git a/src/main/resources/web/assets/js/general/rb-advfilter.js b/src/main/resources/web/assets/js/general/rb-advfilter.js index 5c1f2d8b25..225a3d17cb 100644 --- a/src/main/resources/web/assets/js/general/rb-advfilter.js +++ b/src/main/resources/web/assets/js/general/rb-advfilter.js @@ -131,7 +131,7 @@ class AdvFilter extends React.Component { } componentDidMount() { - const deep = this.props.deep3 || location.href.includes('/admin/') ? 3 : 2 + const deep = this.props.deep3 || location.href.includes('/admin/') || window.__LAB_ADVFILTER_FSDEEP3 ? 3 : 2 const referer = this.props.referer || '' $.get(`/commons/metadata/fields?deep=${deep}&entity=${this.props.entity}&referer=${referer}`, (res) => { const validFs = [] @@ -199,7 +199,7 @@ class AdvFilter extends React.Component { if (!this._fields) return const items = [...this.state.items] - if (items.length >= 9) { + if (items.length >= (window.__LAB_ADVFILTER_LIMIT || 9)) { RbHighbar.create($L('最多可添加 9 个条件')) return } @@ -851,6 +851,26 @@ class FilterItem extends React.Component { getFilterData() { const s = this.state // DON'T CHANGES `state`!!! + + // v3.9 + if (this._inFilterPane && s.op === 'BW' && (s.value || s.value2)) { + const item = { + index: s.index, + field: s.field, + op: s.op, + } + if (s.value) item.value = s.value + if (s.value2) item.value2 = s.value2 + return item + } else if (this._inFilterPane && s.op === 'BW' && s.op4) { + const item = { + index: s.index, + field: s.field, + op: s.op4, + } + return item + } + let noValue = false if (!s.value) { if (OP_NOVALUE.includes(s.op)) { @@ -1040,7 +1060,7 @@ class ListAdvFilterSave extends RbFormHandler {
    diff --git a/src/main/resources/web/assets/js/general/rb-approval.js b/src/main/resources/web/assets/js/general/rb-approval.js index 6877b57e5d..5b05b72216 100644 --- a/src/main/resources/web/assets/js/general/rb-approval.js +++ b/src/main/resources/web/assets/js/general/rb-approval.js @@ -751,7 +751,8 @@ class ApprovalStepViewer extends React.Component { } render() { - const stateLast = this.state.steps ? this.state.steps[0].approvalState : 0 + // const stateLast = this.state.steps ? this.state.steps[0].approvalState : 0 + let stateLast = 0 return (
    (this._dlg = c)} style={{ zIndex: 1051 }}> @@ -764,16 +765,32 @@ class ApprovalStepViewer extends React.Component {
    {!this.state.steps && } +
      {(this.state.steps || []).map((item, idx) => { - return idx === 0 ? this.renderSubmitter(item) : this.renderApprover(item, stateLast) + if (item.submitter) stateLast = item.approvalState + return idx === 0 || item.submitter ? this.renderSubmitter(item, idx) : this.renderApprover(item, stateLast) })} + {stateLast >= 10 && (
    • {stateLast === 13 || stateLast === 12 ? $L('重审') : $L('结束')}
    • )}
    + + {this.state.steps && ( + + )}
    @@ -781,28 +798,31 @@ class ApprovalStepViewer extends React.Component { ) } - renderSubmitter(s) { + renderSubmitter(s, idx) { return ( -
  • - {this._formatTime(s.createdOn)} -
    -
    - Avatar -
    -
    -

    {$L('由 %s 提交审批', s.submitter === rb.currentUser ? $L('你') : s.submitterName)}

    - {s.approvalName && ( -
    -

    - - {s.approvalName} - -

    -
    - )} + + {idx > 0 && {$L('再次提交')}} +
  • + {this._formatTime(s.createdOn)} +
    +
    + Avatar +
    +
    +

    {$L('由 %s 提交审批', s.submitter === rb.currentUser ? $L('你') : s.submitterName)}

    + {s.approvalName && ( +
    +

    + + {s.approvalName} + +

    +
    + )} +
    - -
  • + + ) } @@ -936,7 +956,11 @@ class ApprovalStepViewer extends React.Component { componentDidMount() { this.show() - $.get(`/app/entity/approval/fetch-workedsteps?record=${this.props.id}`, (res) => { + this._load() + } + + _load(his) { + $.get(`/app/entity/approval/fetch-workedsteps?record=${this.props.id}&his=${!!his}`, (res) => { if (!res.data || res.data.length === 0) { RbHighbar.create($L('未查询到流程详情')) this.hide() diff --git a/src/main/resources/web/assets/js/general/rb-assignshare.js b/src/main/resources/web/assets/js/general/rb-assignshare.js index e6322eef9b..d53a411041 100644 --- a/src/main/resources/web/assets/js/general/rb-assignshare.js +++ b/src/main/resources/web/assets/js/general/rb-assignshare.js @@ -71,9 +71,9 @@ class DlgAssign extends RbModalHandler { - this.hide()}> + @@ -224,9 +224,9 @@ class DlgUnshare extends RbModalHandler { - this.hide()}> + @@ -380,3 +380,155 @@ class DlgShareManager extends RbModalHandler { }) } } + +// ~~ 记录转换 +class DlgTransform extends RbModalHandler { + constructor(props) { + super(props) + this.state.transType = 0 + // 支持多个 + this._isMuilt = Array.isArray(props.sourceRecord) && props.sourceRecord.length > 1 + } + + render() { + return ( + (this._dlg = c)} disposeOnHide> +
    +
    + +
    + + + {this._isMuilt && } +
    +
    +
    + +
    + (this._existsRecord = c)} /> +
    +
    + {this.props.mainEntity && ( +
    + +
    + (this._mainRecord = c)} /> +

    {$L('转换新明细记录时需要选择主记录')}

    +
    +
    + )} +
    +
    (this._$btn = c)}> + + + +
    +
    +
    +
    + ) + } + + componentDidMount() { + if (this.props.existsRecord) { + this._existsRecord.setValue(this.props.existsRecord) + } + if (this.props.mainEntity && this.props.mainRecord) { + this._mainRecord.setValue(this.props.mainRecord) + } + } + + post(preview) { + const props = this.props + const _post = { + transid: props.transid, + sourceRecord: props.sourceRecord, + existsRecord: this.state.transType === 1 ? this._existsRecord.val() || null : null, + mainRecord: this.state.transType === 0 && this._mainRecord ? this._mainRecord.val() || null : null, + preview: preview || false, + } + // 单条兼容 + if (Array.isArray(_post.sourceRecord) && !this._isMuilt) _post.sourceRecord = _post.sourceRecord[0] + + if (this.state.transType === 1 && !_post.existsRecord) { + return RbHighbar.createl('请选择已有记录') + } + if (props.mainEntity) { + if (this.state.transType !== 1 && !_post.mainRecord) { + return RbHighbar.createl('请选择主记录') + } + } + if (_post.sourceRecord === _post.existsRecord) { + return RbHighbar.createl('已有记录不能是当前记录') + } + + const $btn = $(this._$btn).find('.btn').button('loading') + $.post('/app/entity/extras/transform39', JSON.stringify(_post), (res) => { + $btn.button('reset') + if (res.error_code === 0) { + this.reset() + this.hide(true) + + if (_post.preview) { + const modalProps = { + title: $L('新建%s', props.entityLabel), + entity: props.entity, + icon: props.icon, + initialFormModel: res.data, + previewid: `${props.transid}.${props.sourceRecord}`, + } + if (_post.existsRecord) { + modalProps.title = $L('编辑%s', props.entityLabel) + modalProps.id = _post.existsRecord + } + // From + RbFormModal.create(modalProps, true) + } else { + if (this._isMuilt) { + RbHighbar.success($L('成功转换 %d 条记录', (res.data || []).length)) + return + } + + // View + setTimeout(() => { + if (window.RbViewModal) { + window.RbViewModal.create({ id: res.data, entity: this.props.entity }) + window.RbListPage && window.RbListPage.reload() + } else if (window.RbViewPage) { + window.RbViewPage.clickView(`#!/View/${this.props.entity}/${res.data}`) + } else { + window.open(`${rb.baseUrl}/app/${this.props.entity}/view/${res.data}`) + } + }, 200) + } + } else { + res.error_code === 400 ? RbHighbar.create(res.error_msg) : RbHighbar.error(res.error_msg) + } + }) + } + + reset() { + this.setState({ transType: 0 }) + this._existsRecord && this._existsRecord.reset() + this._mainRecord && this._mainRecord.reset() + } +} diff --git a/src/main/resources/web/assets/js/general/rb-datalist.common.js b/src/main/resources/web/assets/js/general/rb-datalist.common.js index 1ae53aa41c..2c6f59c0d9 100644 --- a/src/main/resources/web/assets/js/general/rb-datalist.common.js +++ b/src/main/resources/web/assets/js/general/rb-datalist.common.js @@ -798,6 +798,13 @@ const RbListCommon = { RbListPage.reload() $cleanVia.remove() }) + } else { + let def39 = $urlp('def') + if (def39) { + def39 = def39.split(':') // FILTER:LAYOUT + if (def39[0]) wpc.protocolFilter = `via:${def39[0]}` + if (def39[1]) console.log('Use listConfig :', def39[1]) + } } const entity = wpc.entity @@ -913,7 +920,7 @@ class RbList extends React.Component { render() { const lastIndex = this.state.fields.length let rowActions = window.FrontJS ? window.FrontJS.DataList.__rowActions : [] - if (wpc.type !== 'RecordList') rowActions = [] + if (!['RecordList', 'DetailList'].includes(wpc.type)) rowActions = [] return ( @@ -1092,15 +1099,14 @@ class RbList extends React.Component { fetchList(filter) { const fields = [] - let fieldSort = null + let sort = null this.state.fields.forEach((item) => { fields.push(item.field) - if (item.sort) fieldSort = `${item.field}:${item.sort.replace('sort-', '')}` + if (item.sort) sort = `${item.field}:${item.sort.replace('sort-', '')}` }) - if (!fieldSort && this.__defaultSort) fieldSort = this.__defaultSort + if (!sort && this.__defaultSort) sort = this.__defaultSort this.lastFilter = filter || this.lastFilter - const reload = this._forceReload || this.pageNo === 1 this._forceReload = false @@ -1112,7 +1118,7 @@ class RbList extends React.Component { filter: this.lastFilter, advFilter: this.advFilterId, protocolFilter: this.props.protocolFilter || wpc.protocolFilter, - sort: fieldSort, + sort: sort, reload: reload, statsField: wpc.statsField === true && rb.commercial > 0, } @@ -1487,7 +1493,8 @@ class RbListPagination extends React.Component { {(this.state.rowsStats || []).map((item, idx) => { return ( - {item.label} {item.value} + {item.label} + {item.value} ) })} @@ -2182,118 +2189,34 @@ const CategoryWidget = { } const _FrontJS = window.FrontJS -const EasyAction = { +const _EasyAction = window.EasyAction +// eslint-disable-next-line no-unused-vars +const EasyAction4List = { init(items) { + if (!(_FrontJS && _EasyAction && items)) return const _List = _FrontJS.DataList - items.forEach((item) => { - if (!item.icon && !item.text) { - console.log('Bad button of EasyAction :', item) - return - } + items['datalist'] && + items['datalist'].forEach((item) => { + item = _EasyAction.fixItem(item) + if (!item) return - if (~~item.showType === 1) { - item.text = '' - if (!item.icon) item.icon = 'texture' - } - if (~~item.showType === 2) item.icon = null - // L2 - if (item.items && item.items.length === 0) item.items = null - // Event - item.onClick = () => EasyAction.handleOp(item) - item.items && - item.items.forEach((itemL2) => { - itemL2.onClick = () => EasyAction.handleOp(itemL2) - }) - - _List.addButton(item) - }) - }, - - handleOp(item) { - if (item.opType === 1) EasyAction.handleOp1(item) - if (item.opType === 2) EasyAction.handleOp2(item) - if (item.opType === 3) EasyAction.handleOp3(item) - if (item.opType === 4) EasyAction.handleOp4(item) - if (item.opType === 10) EasyAction.handleOp10(item) - }, - - handleOp1(item) { - _FrontJS.openForm(item.op1Value || wpc.entity[0], null, item.op1Value2 || null) - }, - - handleOp2(item) { - const _List = _FrontJS.DataList - const ids = _List.getSelectedIds() - if (!ids[0]) return RbHighbar.create($L('请选择一条记录')) - - let fields = [] - item.op2Value.forEach((item) => { - let o = { field: item.field } - if (item.tip2) o.tip = item.tip2 - if (item.readonly2) o.readonly = true - if (item.required2) o.nullable = false - fields.push(o) - }) - _FrontJS.openLiteForm(ids[0], fields) - }, - - handleOp3(item) { - const _List = _FrontJS.DataList - const ids = _List.getSelectedIds() - if (!ids[0]) return RbHighbar.create($L('请至少选择一条记录')) - - if (!confirm(item.op3Value3 || $L('确认操作?'))) return - - const data = { - [item.op3Value]: item.op3Value2, - } - - let idsLen = ids.length - ids.forEach((id) => { - data.metadata = { id: id } - $.post('/app/entity/record-save', JSON.stringify(data), () => { - idsLen-- - if (idsLen === 0) { - RbHighbar.success('操作成功') - _List.reload() - } + item.onClick = () => _EasyAction.handleOp(item) + item.items && + item.items.forEach((itemL2) => { + itemL2.onClick = () => _EasyAction.handleOp(itemL2) + }) + _List.addButton(item) }) - }) - }, - handleOp4(item) { - const _List = _FrontJS.DataList - const ids = _List.getSelectedIds() - if (!ids[0]) return RbHighbar.create($L('请至少选择一条记录')) - - // ID[:DETAIL_NAME] - const rr = item.op4Value.split(':') - // 明细实体的 - if (rr[1]) { - $.get(`/commons/frontjs/get-detailids?ids=${ids.join(',')}`, (res) => { - if (res.error_code === 0) { - if (res.data && res.data.length > 0) { - _List.exportReport(rr[0], { isMerge: true, recordId: res.data }) - } else { - RbHighbar.createl('选择的记录暂无明细数据') - } - } else { - RbHighbar.error(res.error_msg) - } - }) - } else { - _List.exportReport(rr[0], { isMerge: true }) - } - }, + items['datarow'] && + items['datarow'].forEach((item) => { + item = _EasyAction.fixItem(item) + if (!item) return - handleOp10(item) { - try { - const FN = Function - FN(item.op10Value)() // eval - } catch (err) { - console.log(err) - } + item.onClick = (id) => _EasyAction.handleOp(item, id) + _List.regRowButton(item) + }) }, } diff --git a/src/main/resources/web/assets/js/general/rb-datalist.js b/src/main/resources/web/assets/js/general/rb-datalist.js index a265edabe5..bc504a2c54 100644 --- a/src/main/resources/web/assets/js/general/rb-datalist.js +++ b/src/main/resources/web/assets/js/general/rb-datalist.js @@ -294,6 +294,6 @@ $(document).ready(() => { $wtab.trigger('click') } - // eslint-disable-next-line no-undef - wpc.easyAction && window.EasyAction && window.EasyAction.init(wpc.easyAction) + // v3.8, v3.9 + wpc.easyAction && window.EasyAction4List && window.EasyAction4List.init(wpc.easyAction) }) diff --git a/src/main/resources/web/assets/js/general/rb-forms.append.js b/src/main/resources/web/assets/js/general/rb-forms.append.js index 8e6c865238..e1cac724ef 100644 --- a/src/main/resources/web/assets/js/general/rb-forms.append.js +++ b/src/main/resources/web/assets/js/general/rb-forms.append.js @@ -96,11 +96,10 @@ class ClassificationSelector extends React.Component { componentDidMount() { const $root = this.show() $root.on('hidden.bs.modal', () => { + this.props.keepModalOpen && $keepModalOpen() if (this.props.disposeOnHide === true) { $root.modal('dispose') $unmount($root.parent()) - } else if (this.props.keepModalOpen === true) { - $(document.body).addClass('modal-open') // Keep scroll } }) @@ -174,7 +173,6 @@ class ClassificationSelector extends React.Component { window.referenceSearch__call = function (selected) {} window.referenceSearch__dlg - // see `reference-search.html` class ReferenceSearcher extends RbModal { renderContent() { @@ -765,11 +763,7 @@ class RepeatedViewer extends RbModalHandler { if (!window.RbForm) window.RbForm = function () {} // eslint-disable-next-line no-unused-vars class LiteForm extends RbForm { - renderCustomizedFormArea() { - return null - } - - renderDetailsForm() { + renderDetailForms() { return null } diff --git a/src/main/resources/web/assets/js/general/rb-forms.js b/src/main/resources/web/assets/js/general/rb-forms.js index 14c0d8c6ae..e6d92c8387 100644 --- a/src/main/resources/web/assets/js/general/rb-forms.js +++ b/src/main/resources/web/assets/js/general/rb-forms.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 SimpleMDE, RepeatedViewer, ProTable, Md2Html */ +/* global SimpleMDE, RepeatedViewer, ProTable, Md2Html, ClassificationSelector */ /** * Callback API: @@ -38,7 +38,7 @@ class RbFormModal extends React.Component { return (
    -
    (this._rbmodal = c)}> +