Parent cascading field (#451)

* fix: calcFormula

* trigger forceUpdate

* 发起人/审批人 支持部门引用字段

* single-queue

* better code
This commit is contained in:
RB 2022-04-08 17:59:04 +08:00 committed by GitHub
parent 588baa9861
commit 2491297ad0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 338 additions and 187 deletions

2
@rbv

@ -1 +1 @@
Subproject commit f62d1b05ef27ccf62d1a213f4ddf03184e6deb66
Subproject commit e6951a36179a8e1087a7888f948c8b3eede860b4

View file

@ -273,7 +273,7 @@
<dependency>
<groupId>com.github.devezhao</groupId>
<artifactId>persist4j</artifactId>
<version>ft-time-SNAPSHOT</version>
<version>1.5.8</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>

View file

@ -10,7 +10,6 @@ package com.rebuild.api;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.commons.EncryptUtils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.commons.ThreadPool;
import cn.devezhao.commons.web.ServletUtils;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
@ -21,6 +20,7 @@ import com.rebuild.core.configuration.RebuildApiManager;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.DataSpecificationException;
import com.rebuild.core.support.task.TaskExecutors;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.utils.RateLimiters;
import es.moki.ratelimitj.core.limiter.request.RequestRateLimiter;
@ -246,7 +246,6 @@ public class ApiGateway extends Controller implements Initialization {
* @param result
*/
protected void logRequestAsync(Date requestTime, String remoteIp, String requestId, String apiName, ApiContext context, JSON result) {
ThreadPool.exec(() -> {
Record record = EntityHelper.forNew(EntityHelper.RebuildApiRequest, UserService.SYSTEM_USER);
record.setString("requestUrl", apiName);
record.setString("remoteIp", remoteIp);
@ -267,7 +266,7 @@ public class ApiGateway extends Controller implements Initialization {
} else {
record.setString("appId", "0");
}
Application.getCommonsService().create(record, false);
});
TaskExecutors.queue(() -> Application.getCommonsService().create(record, false));
}
}

View file

@ -127,7 +127,7 @@ public class AutoFillinManager implements ConfigManager {
}
// NOTE 忽略空值
if (value == null || NullValue.is(value) || StringUtils.isBlank(value.toString())) {
if (NullValue.isNull(value) || StringUtils.isBlank(value.toString())) {
continue;
}

View file

@ -484,7 +484,7 @@ public class FormsBuilder extends FormsManager {
}
// 处理日期格式
if (field.getDisplayType() == DisplayType.REFERENCE && value != null && ((ID) value).getLabelRaw() != null) {
if (field.getDisplayType() == DisplayType.REFERENCE && value instanceof ID && ((ID) value).getLabelRaw() != null) {
Field nameField = field.getRawMeta().getReferenceEntity().getNameField();
if (nameField.getType() == FieldType.DATE || nameField.getType() == FieldType.TIMESTAMP) {
Object newLabel = EasyMetaFactory.valueOf(nameField).wrapValue(((ID) value).getLabelRaw());

View file

@ -99,10 +99,9 @@ public class EntityRecordCreator extends JsonRecordCreator {
}
Object hasVal = record.getObjectValue(field.getName());
boolean isNull = hasVal == null || NullValue.is(hasVal);
boolean canNull = field.isNullable() || autoReadonlyFields.contains(field.getName());
if (isNull) {
if (NullValue.isNull(hasVal)) {
if (!canNull) {
notNulls.add(easyField.getLabel());
}
@ -127,11 +126,10 @@ public class EntityRecordCreator extends JsonRecordCreator {
if (MetadataHelper.isCommonsField(field)) continue;
Object hasVal = record.getObjectValue(field.getName());
boolean isNull = hasVal == null || NullValue.is(hasVal);
boolean canNull = field.isNullable() || autoReadonlyFields.contains(field.getName());
EasyField easyField = EasyMetaFactory.valueOf(field);
if (isNull) {
if (NullValue.isNull(hasVal)) {
if (!canNull) {
notNulls.add(easyField.getLabel());
}

View file

@ -44,9 +44,7 @@ public class PrivilegesGuardContextHolder {
*/
public static ID getSkipGuardOnce() {
ID recordId = SKIP_GUARD.get();
if (recordId != null) {
SKIP_GUARD.remove();
}
if (recordId != null) SKIP_GUARD.remove();
return recordId;
}
}

View file

@ -178,16 +178,22 @@ public class FlowNode {
if (whichUser != null) {
Field userField = ApprovalHelper.checkVirtualField(def);
if (userField != null) {
Object[] refUser;
Object[] ud;
// 部门中的用户如上级
if (userField.getOwnEntity().getEntityCode() == EntityHelper.Department) {
Department refDept = Application.getUserStore().getUser(whichUser).getOwningDept();
refUser = Application.getQueryFactory().uniqueNoFilter(
(ID) refDept.getIdentity(), userField.getName());
Department d = Application.getUserStore().getUser(whichUser).getOwningDept();
ud = Application.getQueryFactory().uniqueNoFilter((ID) d.getIdentity(), userField.getName());
} else {
refUser = Application.getQueryFactory().uniqueNoFilter(whichUser, userField.getName());
ud = Application.getQueryFactory().uniqueNoFilter(whichUser, userField.getName());
}
if (refUser != null && refUser[0] != null) users.add((ID) refUser[0]);
if (ud != null && ud[0] != null) {
if (userField.getReferenceEntity().getEntityCode() == EntityHelper.Department) {
defsList.add(ud[0].toString());
} else {
users.add((ID) ud[0]);
}
}
}
}
@ -197,6 +203,8 @@ public class FlowNode {
}
users.addAll(UserHelper.parseUsers(defsList, record));
users.removeIf(id -> !UserHelper.isActive(id));
return users;
}

View file

@ -470,15 +470,15 @@ public class GeneralEntityService extends ObservableService implements EntitySer
return false;
}
boolean rejected = false;
boolean unallow = false;
if (action == BizzPermission.DELETE) {
rejected = currentState == ApprovalState.APPROVED || currentState == ApprovalState.PROCESSING;
unallow = currentState == ApprovalState.APPROVED || currentState == ApprovalState.PROCESSING;
} else if (action == BizzPermission.UPDATE) {
rejected = (currentState == ApprovalState.APPROVED && changeState != ApprovalState.CANCELED) /* 管理员撤销 */
unallow = (currentState == ApprovalState.APPROVED && changeState != ApprovalState.CANCELED) /* 管理员撤销 */
|| (currentState == ApprovalState.PROCESSING && !GeneralEntityServiceContextHolder.isAllowForceUpdateOnce() /* 审批时修改 */);
}
if (rejected) {
if (unallow) {
if (RobotTriggerObserver.getTriggerSource() != null) {
recordType = Language.L("关联记录");
}
@ -489,6 +489,11 @@ public class GeneralEntityService extends ObservableService implements EntitySer
}
}
}
if (action == BizzPermission.CREATE || action == BizzPermission.UPDATE) {
// TODO 父级级联字段强校验兼容问题???
}
return true;
}

View file

@ -7,7 +7,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.trigger.impl;
import cn.devezhao.bizz.privileges.impl.BizzPermission;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Record;
@ -23,6 +23,7 @@ import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.privileges.PrivilegesGuardContextHolder;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.ServiceSpec;
import com.rebuild.core.service.general.GeneralEntityServiceContextHolder;
import com.rebuild.core.service.general.OperatingContext;
import com.rebuild.core.service.query.AdvFilterParser;
import com.rebuild.core.service.trigger.ActionContext;
@ -54,8 +55,6 @@ public class FieldAggregation implements TriggerAction {
final protected ActionContext context;
// 允许无权限更新
final protected boolean allowNoPermissionUpdate;
// 最大触发链深度
final protected int maxTriggerDepth;
// 此触发器可能产生连锁反应
@ -76,17 +75,15 @@ public class FieldAggregation implements TriggerAction {
* @param context
*/
public FieldAggregation(ActionContext context) {
this(context, Boolean.TRUE, 9);
this(context, 9);
}
/**
* @param context
* @param allowNoPermissionUpdate
* @param maxTriggerDepth
*/
protected FieldAggregation(ActionContext context, boolean allowNoPermissionUpdate, int maxTriggerDepth) {
protected FieldAggregation(ActionContext context, int maxTriggerDepth) {
this.context = context;
this.allowNoPermissionUpdate = allowNoPermissionUpdate;
this.maxTriggerDepth = maxTriggerDepth;
}
@ -133,13 +130,6 @@ public class FieldAggregation implements TriggerAction {
return;
}
// 如果当前用户对目标记录无修改权限
if (!allowNoPermissionUpdate
&& !Application.getPrivilegesManager().allow(operatingContext.getOperator(), targetRecordId, BizzPermission.UPDATE)) {
log.warn("No permission to update record of target : {}", targetRecordId);
return;
}
// 聚合数据过滤
JSONObject dataFilter = ((JSONObject) context.getActionContent()).getJSONObject("dataFilter");
String dataFilterSql = null;
@ -149,6 +139,7 @@ public class FieldAggregation implements TriggerAction {
// 构建目标记录数据
Record targetRecord = EntityHelper.forUpdate(targetRecordId, UserService.SYSTEM_USER, false);
JSONArray items = ((JSONObject) context.getActionContent()).getJSONArray("items");
for (Object o : items) {
JSONObject item = (JSONObject) o;
@ -178,9 +169,14 @@ public class FieldAggregation implements TriggerAction {
}
// 有需要才执行
if (targetRecord.getAvailableFields().size() > 1) {
if (allowNoPermissionUpdate) {
if (!targetRecord.isEmpty()) {
final boolean forceUpdate = ((JSONObject) context.getActionContent()).getBooleanValue("forceUpdate");
// 跳过权限
PrivilegesGuardContextHolder.setSkipGuard(targetRecordId);
// 强制更新 (v2.9)
if (forceUpdate) {
GeneralEntityServiceContextHolder.setAllowForceUpdate(targetRecordId);
}
// 会关联触发下一触发器如有
@ -191,10 +187,12 @@ public class FieldAggregation implements TriggerAction {
? Application.getEntityService(targetEntity.getEntityCode())
: Application.getService(targetEntity.getEntityCode());
targetRecord.setDate(EntityHelper.ModifiedOn, CalendarUtils.now());
try {
useService.update(targetRecord);
} finally {
PrivilegesGuardContextHolder.getSkipGuardOnce();
GeneralEntityServiceContextHolder.isAllowForceUpdateOnce();
}
}
}

View file

@ -81,15 +81,17 @@ public class FieldWriteback extends FieldAggregation {
this.prepare(operatingContext);
if (targetRecordIds.isEmpty()) return;
if (targetRecordData.getAvailableFields().isEmpty()) {
if (targetRecordData.isEmpty()) {
log.info("No data of target record available : {}", targetRecordId);
return;
}
final ServiceSpec targetService = MetadataHelper.isBusinessEntity(targetEntity)
final ServiceSpec useService = MetadataHelper.isBusinessEntity(targetEntity)
? Application.getEntityService(targetEntity.getEntityCode())
: Application.getService(targetEntity.getEntityCode());
final boolean forceUpdate = ((JSONObject) context.getActionContent()).getBooleanValue("forceUpdate");
boolean tschainAdded = false;
for (ID targetRecordId : targetRecordIds) {
if (operatingContext.getAction() == BizzPermission.DELETE
@ -98,13 +100,11 @@ public class FieldWriteback extends FieldAggregation {
continue;
}
if (allowNoPermissionUpdate) {
// 跳过权限
PrivilegesGuardContextHolder.setSkipGuard(targetRecordId);
}
// 如果当前用户对目标记录无修改权限
else if (!Application.getPrivilegesManager().allow(operatingContext.getOperator(), targetRecordId, BizzPermission.UPDATE)) {
log.warn("No permission to update record of target : {}", targetRecordId);
continue;
// 强制更新 (v2.9)
if (forceUpdate) {
GeneralEntityServiceContextHolder.setAllowForceUpdate(targetRecordId);
}
// 会关联触发下一触发器
@ -116,12 +116,15 @@ public class FieldWriteback extends FieldAggregation {
Record targetRecord = targetRecordData.clone();
targetRecord.setID(targetEntity.getPrimaryField().getName(), targetRecordId);
targetRecord.setDate(EntityHelper.ModifiedOn, CalendarUtils.now());
GeneralEntityServiceContextHolder.setRepeatedCheckMode(GeneralEntityServiceContextHolder.RCM_CHECK_MAIN);
try {
targetService.createOrUpdate(targetRecord);
useService.createOrUpdate(targetRecord);
} finally {
PrivilegesGuardContextHolder.getSkipGuardOnce();
GeneralEntityServiceContextHolder.isAllowForceUpdateOnce();
GeneralEntityServiceContextHolder.getRepeatedCheckModeOnce();
}
}

View file

@ -298,7 +298,7 @@ public class FieldValueHelper {
* @return
*/
public static boolean hasLength(Object o) {
if (o == null || NullValue.is(o)) return false;
if (NullValue.isNull(o)) return false;
if (o.getClass().isArray()) return ((Object[]) o).length > 0;
else return o.toString().length() > 0;
}

View file

@ -18,6 +18,7 @@ import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.*;
/**
@ -34,12 +35,17 @@ public class TaskExecutors extends DistributedJobLock {
private static final int MAX_TASKS_NUMBER = Integer.max(Runtime.getRuntime().availableProcessors() / 2, 2);
private static final ExecutorService EXECS = new ThreadPoolExecutor(
private static final ExecutorService EXEC = new ThreadPoolExecutor(
MAX_TASKS_NUMBER, MAX_TASKS_NUMBER, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(MAX_TASKS_NUMBER * 6));
private static final Map<String, HeavyTask<?>> TASKS = new ConcurrentHashMap<>();
// 队列执行
private static final ExecutorService SINGLE_QUEUE = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
/**
* 异步执行提交给任务调度
*
@ -50,7 +56,7 @@ public class TaskExecutors extends DistributedJobLock {
public static String submit(HeavyTask<?> task, ID execUser) {
String taskid = task.getClass().getSimpleName() + "-" + CodecUtils.randomCode(20);
task.setUser(execUser);
EXECS.execute(task);
EXEC.execute(task);
TASKS.put(taskid, task);
return taskid;
}
@ -79,6 +85,8 @@ public class TaskExecutors extends DistributedJobLock {
}
/**
* 获取任务
*
* @param taskid
* @return
*/
@ -95,13 +103,27 @@ public class TaskExecutors extends DistributedJobLock {
task.run();
}
/**
* 排队执行单线程
*
* @param command
*/
public static void queue(Runnable command) {
SINGLE_QUEUE.execute(command);
}
/**
* 停止任务执行器
*/
public static void shutdown() {
List<Runnable> runs = EXECS.shutdownNow();
if (!runs.isEmpty()) {
log.warn("{} task(s) were interrupted", runs.size());
List<Runnable> t = EXEC.shutdownNow();
if (!t.isEmpty()) {
log.warn("{} task(s) were interrupted", t.size());
}
List<Runnable> c = SINGLE_QUEUE.shutdownNow();
if (!c.isEmpty()) {
log.warn("{} command(s) were interrupted", c.size());
}
}
@ -109,10 +131,10 @@ public class TaskExecutors extends DistributedJobLock {
@Scheduled(fixedRate = 300000, initialDelay = 300000)
public void executeJob() {
if (TASKS.isEmpty() || !tryLock()) return;
if (!tryLock()) return;
if (!TASKS.isEmpty()) {
log.info("{} task(s) in the queue", TASKS.size());
for (Map.Entry<String, HeavyTask<?>> e : TASKS.entrySet()) {
HeavyTask<?> task = e.getValue();
if (task.getCompletedTime() == null || !task.isCompleted()) {
@ -126,4 +148,10 @@ public class TaskExecutors extends DistributedJobLock {
}
}
}
Queue<Runnable> queue = ((ThreadPoolExecutor) SINGLE_QUEUE).getQueue();
if (!queue.isEmpty()) {
log.info("{} command(s) in the single-queue", queue.size());
}
}
}

View file

@ -32,7 +32,7 @@ public class RbDateCodec extends DateCodec {
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features)
throws IOException {
SerializeWriter out = serializer.out;
if (object == null || NullValue.is(object)) {
if (NullValue.isNull(object)) {
out.writeNull();
return;
}

View file

@ -198,26 +198,44 @@ public class MetaFieldController extends BaseController {
Field refField = currentEntity.getField(getParameterNotNull(request, "field"));
Entity referenceEntity = refField.getReferenceEntity();
// 找到共同的引用字段
Field[] currentEntityFields = MetadataSorter.sortFields(currentEntity, DisplayType.REFERENCE);
List<JSONObject> list = getCoReferenceFields(currentEntity, referenceEntity, false);
// // TODO 开放明细实体关联主实体父级级联
// if (currentEntity.getMainEntity() != null) {
// list.addAll(getCoReferenceFields(currentEntity.getMainEntity(), referenceEntity, true));
// }
return RespBody.ok(list);
}
// 获取共同引用字段
private List<JSONObject> getCoReferenceFields(Entity entity, Entity referenceEntity, boolean fromDetail) {
Field[] entityFields = MetadataSorter.sortFields(entity, DisplayType.REFERENCE);
Field[] referenceEntityFields = MetadataSorter.sortFields(referenceEntity, DisplayType.REFERENCE);
List<JSONObject> together = new ArrayList<>();
for (Field foo : currentEntityFields) {
List<JSONObject> co = new ArrayList<>();
for (Field foo : entityFields) {
if (MetadataHelper.isCommonsField(foo)) continue;
Entity fooEntity = foo.getReferenceEntity();
for (Field bar : referenceEntityFields) {
if (MetadataHelper.isCommonsField(bar)) continue;
if (fooEntity.equals(bar.getReferenceEntity())) {
// 当前实体字段$$$$引用实体字段
String name = foo.getName() + MetadataHelper.SPLITER + bar.getName();
String label = String.format("%s (%s)", EasyMetaFactory.getLabel(foo), EasyMetaFactory.getLabel(bar));
together.add(JSONUtils.toJSONObject(
String label = String.format("%s (%s)",
EasyMetaFactory.getLabel(foo), EasyMetaFactory.getLabel(bar));
if (fromDetail) {
label = EasyMetaFactory.getLabel(entity) + "." + label;
name = entity.getName() + "." + name;
}
co.add(JSONUtils.toJSONObject(
new String[] { "name", "label" }, new String[] { name, label } ));
}
}
}
return RespBody.ok(together);
return co;
}
}

View file

@ -35,8 +35,7 @@ import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
/**
* @author devezhao zhaofang123@gmail.com
@ -126,31 +125,35 @@ public class ApprovalAdminController extends BaseController {
Field[] deptRefFields = MetadataSorter.sortFields(
MetadataHelper.getEntity(EntityHelper.Department), DisplayType.REFERENCE);
Set<String> filterNames = new HashSet<>();
Collections.addAll(filterNames, EntityHelper.ApprovalLastUser, "deptId");
// 发起人
for (Field field : userRefFields) {
if (isRefUserField(field)) {
if (isRefUserOrDeptField(field, filterNames, true)) {
fields.add(new String[] {
ApprovalHelper.APPROVAL_SUBMITOR + field.getName(),
textSubmitor + EasyMetaFactory.getLabel(field)} );
}
}
for (Field field : deptRefFields) {
if (isRefUserField(field)) {
if (isRefUserOrDeptField(field, filterNames, true)) {
fields.add(new String[] {
ApprovalHelper.APPROVAL_SUBMITOR + "deptId." + field.getName(),
textSubmitor + textDept + EasyMetaFactory.getLabel(field)} );
}
}
// 上一审批人
for (Field field : userRefFields) {
if (isRefUserField(field)) {
if (isRefUserOrDeptField(field, filterNames, true)) {
fields.add(new String[] {
ApprovalHelper.APPROVAL_APPROVER + field.getName(),
textApprover + EasyMetaFactory.getLabel(field)} );
}
}
for (Field field : deptRefFields) {
if (isRefUserField(field)) {
if (isRefUserOrDeptField(field, filterNames, true)) {
fields.add(new String[] {
ApprovalHelper.APPROVAL_APPROVER + "deptId." + field.getName(),
textApprover + textDept + EasyMetaFactory.getLabel(field)} );
@ -160,8 +163,7 @@ public class ApprovalAdminController extends BaseController {
// 本实体字段
Field[] refFields = MetadataSorter.sortFields(entity, DisplayType.REFERENCE);
for (Field field : refFields) {
int refEntity = field.getReferenceEntity().getEntityCode();
if (refEntity == EntityHelper.User || refEntity == EntityHelper.Department) {
if (isRefUserOrDeptField(field, filterNames, false)) {
fields.add(new String[] { field.getName(), EasyMetaFactory.getLabel(field)} );
}
}
@ -170,9 +172,12 @@ public class ApprovalAdminController extends BaseController {
new String[] { "id", "text" }, fields.toArray(new String[0][]));
}
private boolean isRefUserField(Field field) {
return field.getReferenceEntity().getEntityCode() == EntityHelper.User
&& !MetadataHelper.isCommonsField(field);
private boolean isRefUserOrDeptField(Field field, Set<String> filterNames, boolean excludeCommon) {
if (excludeCommon && MetadataHelper.isCommonsField(field)) return false;
if (filterNames.contains(field.getName())) return false;
int ec = field.getReferenceEntity().getEntityCode();
return ec == EntityHelper.User || ec == EntityHelper.Department;
}
@PostMapping("approval/user-fields-show")
@ -201,8 +206,8 @@ public class ApprovalAdminController extends BaseController {
if (userField.getOwnEntity().getEntityCode() == EntityHelper.Department) {
fieldText = textDept + fieldText;
}
fieldText = (idOrField.startsWith(ApprovalHelper.APPROVAL_SUBMITOR) ? textSubmitor : textApprover)
+ fieldText;
fieldText = (idOrField.startsWith(ApprovalHelper.APPROVAL_SUBMITOR)
? textSubmitor : textApprover) + fieldText;
shows.add(new String[] { idOrField, fieldText });
}

View file

@ -85,13 +85,11 @@ public class ApprovalController extends BaseController {
}
// 审批中提交人可撤回
if (stateVal == ApprovalState.PROCESSING.getState()) {
ID submitter = ApprovalHelper.getSubmitter(recordId);
if (user.equals(submitter)) {
if (stateVal == ApprovalState.PROCESSING.getState()
&& user.equals(ApprovalHelper.getSubmitter(recordId))) {
data.put("canCancel", true);
}
}
}
return RespBody.ok(data);
}
@ -123,6 +121,12 @@ public class ApprovalController extends BaseController {
data.put("signMode", nextNodes.getSignMode());
data.put("useGroup", nextNodes.getGroupId());
// // 审批中提交人可撤回
// if (ApprovalHelper.getApprovalState(recordId) == ApprovalState.PROCESSING
// && user.equals(ApprovalHelper.getSubmitter(recordId))) {
// data.put("canCancel", true);
// }
// 可修改字段
JSONArray editableFields = approvalProcessor.getCurrentNode().getEditableFields();
if (editableFields != null && !editableFields.isEmpty()) {

View file

@ -88,6 +88,13 @@ public class UserAvatar extends BaseController {
ServletUtils.addCacheHead(response, 30);
String avatarUrl = realUser.getAvatarUrl();
// 外部地址
if (avatarUrl != null && (avatarUrl.startsWith("http://") || avatarUrl.startsWith("https://"))) {
response.sendRedirect(avatarUrl);
return;
}
avatarUrl = QiniuCloud.encodeUrl(avatarUrl);
if (avatarUrl != null) {
int w = getIntParameter(request, "w", 100);

View file

@ -10,7 +10,6 @@ package com.rebuild.web.user.signup;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.commons.CodecUtils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.commons.ThreadPool;
import cn.devezhao.commons.web.ServletUtils;
import cn.devezhao.commons.web.WebUtils;
import cn.devezhao.persist4j.Record;
@ -20,6 +19,7 @@ import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.support.KVStorage;
import com.rebuild.core.support.License;
import com.rebuild.core.support.task.TaskExecutors;
import com.rebuild.utils.AES;
import com.rebuild.web.BaseController;
import eu.bitwalker.useragentutils.DeviceType;
@ -64,7 +64,7 @@ public class LoginAction extends BaseController {
ServletUtils.removeCookie(request, response, CK_AUTOLOGIN);
}
ThreadPool.exec(() -> createLoginLog(request, user));
createLoginLog(request, user);
ServletUtils.setSessionAttribute(request, WebUtils.CURRENT_USER, user);
ServletUtils.setSessionAttribute(request, SK_USER_THEME, KVStorage.getCustomValue("THEME." + user));
@ -114,14 +114,17 @@ public class LoginAction extends BaseController {
String ipAddr = StringUtils.defaultString(ServletUtils.getRemoteAddr(request), "127.0.0.1");
Record record = EntityHelper.forNew(EntityHelper.LoginLog, UserService.SYSTEM_USER);
final Record record = EntityHelper.forNew(EntityHelper.LoginLog, UserService.SYSTEM_USER);
record.setID("user", user);
record.setString("ipAddr", ipAddr);
record.setString("userAgent", uaClear);
record.setDate("loginTime", CalendarUtils.now());
TaskExecutors.queue(() -> {
Application.getCommonsService().create(record);
License.siteApiNoCache(
String.format("api/authority/user/echo?user=%s&ip=%s&ua=%s", user, ipAddr, CodecUtils.urlEncode(ua)));
});
}
}

View file

@ -223,12 +223,9 @@ a#entityIcon:hover {
}
.common-patt .badge {
margin: 0;
margin-right: 3px;
margin-top: 6px;
margin: 6px 3px 0 0;
font-weight: normal;
border: 0 none;
background-color: #eee;
background-color: rgb(245, 247, 249);
}

View file

@ -196,7 +196,7 @@ button[disabled] {
.badge.text-id {
font-weight: normal;
color: #777;
font-size: 12px;
font-size: 12px !important;
text-transform: uppercase;
line-height: 1.2 !important;
}
@ -4716,3 +4716,12 @@ pre.unstyle {
flex: 0 0 66.666667%;
max-width: 66.666667%;
}
.dropdown-menu.entity-switch {
min-width: 220px;
}
.dropdown-menu.entity-switch .dropdown-item.current,
.dropdown-menu.entity-switch .dropdown-item.current .icon {
color: #4285f4;
}

View file

@ -14,11 +14,14 @@ $(document).ready(() => {
href = href.split('/field/')[0] + '/fields'
}
const $ul = $('<ul class="dropdown-menu auto-scroller"></ul>').appendTo('.aside-header')
$ul.perfectScrollbar()
const $ul = $('<ul class="dropdown-menu auto-scroller entity-switch"></ul>').appendTo('.aside-header')
function _render(item) {
$(`<a class="dropdown-item" href="${href.replace(`/${entity}/`, `/${item.entityName}/`)}"><i class="icon zmdi zmdi-${item.icon}"></i> ${item.entityLabel}</a>`).appendTo($ul)
const $item = $(`<a class="dropdown-item" href="${href.replace(`/${entity}/`, `/${item.entityName}/`)}"><i class="icon zmdi zmdi-${item.icon}"></i> ${item.entityLabel}</a>`)
if (entity === item.entityName) {
$item.addClass('current')
}
$item.appendTo($ul)
}
$.get('/admin/entity/entity-list?detail=true&bizz=true', (res) => {
@ -28,6 +31,8 @@ $(document).ready(() => {
$(res.data).each((idx, item) => {
if (item.builtin === false) _render(item)
})
$ul.perfectScrollbar()
})
$('<i class="icon zmdi zmdi-caret-down ml-1 text-muted"></i>').appendTo('.aside-header .title')

View file

@ -193,6 +193,7 @@ class ReferenceSearcher extends RbModal {
destroy() {
this.setState({ destroy: true })
window.referenceSearch__dlg = null
}
}

View file

@ -313,11 +313,12 @@ class RbForm extends React.Component {
}
componentDidMount() {
// 新纪录初始值
if (this.isNew) {
this.props.children.map((child) => {
const val = child.props.value
if (val && child.props.readonly !== true) {
// 复合型值 {id:xxx, text:xxx}
// {id:xxx, text:xxx}
this.setFieldValue(child.props.field, typeof val === 'object' ? val.id : val)
}
})
@ -340,16 +341,16 @@ class RbForm extends React.Component {
// 设置字段值
setFieldValue(field, value, error) {
this.__FormData[field] = { value: value, error: error }
if (!error && this._onFieldValueChange_calls) this._onFieldValueChange_calls.forEach((c) => c({ name: field, value: value }))
if (!error) this._onFieldValueChangeCall(field, value)
// eslint-disable-next-line no-console
if (rb.env === 'dev') console.log('FV1 ... ' + JSON.stringify(this.__FormData))
}
// 避免无意义更新
setFieldUnchanged(field) {
setFieldUnchanged(field, originValue) {
delete this.__FormData[field]
if (this._onFieldValueChange_calls) this._onFieldValueChange_calls.forEach((c) => c({ name: field }))
this._onFieldValueChangeCall(field, originValue)
// eslint-disable-next-line no-console
if (rb.env === 'dev') console.log('FV2 ... ' + JSON.stringify(this.__FormData))
@ -361,6 +362,11 @@ class RbForm extends React.Component {
c.push(call)
this._onFieldValueChange_calls = c
}
_onFieldValueChangeCall(field, value) {
if (this._onFieldValueChange_calls) {
this._onFieldValueChange_calls.forEach((c) => c({ name: field, value: value }))
}
}
// 保存并添加明细
static __NEXT_ADDDETAIL = 102
@ -529,8 +535,8 @@ class RbFormElement extends React.Component {
title={this.state.hasError}
type="text"
value={value || ''}
onChange={(e) => this.handleChange(e)}
onBlur={this.props.readonly ? null : () => this.checkValue()}
onChange={(e) => this.handleChange(e, this.props.readonly ? false : true)}
// onBlur={this.props.readonly ? null : () => this.checkValue()}
readOnly={this.props.readonly}
maxLength={this.props.maxLength || 200}
/>
@ -574,7 +580,7 @@ class RbFormElement extends React.Component {
if (this.isValueUnchanged() && !this.props.$$$parent.isNew) {
if (err) this.props.$$$parent.setFieldValue(this.props.field, this.state.value, errMsg)
else this.props.$$$parent.setFieldUnchanged(this.props.field)
else this.props.$$$parent.setFieldUnchanged(this.props.field, this.state.value)
} else {
this.setState({ hasError: err })
this.props.$$$parent.setFieldValue(this.props.field, this.state.value, errMsg)
@ -761,8 +767,8 @@ class RbFormNumber extends RbFormText {
title={this.state.hasError}
type="text"
value={this._removeComma(value) || ''}
onChange={(e) => this.handleChange(e)}
onBlur={this.props.readonly ? null : () => this.checkValue()}
onChange={(e) => this.handleChange(e, this.props.readonly ? false : true)}
// onBlur={this.props.readonly ? null : () => this.checkValue()}
readOnly={this.props.readonly}
maxLength="29"
/>
@ -775,41 +781,44 @@ class RbFormNumber extends RbFormText {
// 表单计算视图下无效
if (this.props.calcFormula && !this.props.onView) {
const calcFormula = this.props.calcFormula.replace(new RegExp('×', 'ig'), '*').replace(new RegExp('÷', 'ig'), '/')
const fixed = this.props.decimalFormat ? (this.props.decimalFormat.split('.')[1] || '').length : 0
// 等待字段初始化完毕
setTimeout(() => {
const calcFormulaValues = {}
const watchFields = calcFormula.match(/\{([a-z0-9]+)\}/gi) || []
this.calcFormula__values = {}
// 初始值
setTimeout(() => {
watchFields.forEach((item) => {
const name = item.substr(1, item.length - 2)
const fieldComp = this.props.$$$parent.refs[`fieldcomp-${name}`]
if (fieldComp && fieldComp.state.value) {
this.calcFormula__values[name] = this._removeComma(fieldComp.state.value)
calcFormulaValues[name] = this._removeComma(fieldComp.state.value)
}
})
}, 400)
// 小数位
const fixed = this.props.decimalFormat ? (this.props.decimalFormat.split('.')[1] || '').length : 0
// 表单计算
this.props.$$$parent.onFieldValueChange((s) => {
if (!watchFields.includes(`{${s.name}}`)) return
if (!watchFields.includes(`{${s.name}}`)) {
if (rb.env === 'dev') console.log('onFieldValueChange :', s)
return false
} else if (rb.env === 'dev') {
console.log('onFieldValueChange for calcFormula :', s)
}
if (s.value) {
this.calcFormula__values[s.name] = this._removeComma(s.value)
calcFormulaValues[s.name] = this._removeComma(s.value)
} else {
delete this.calcFormula__values[s.name]
delete calcFormulaValues[s.name]
}
let formula = calcFormula
for (let key in this.calcFormula__values) {
formula = formula.replace(new RegExp(`{${key}}`, 'ig'), this.calcFormula__values[key] || 0)
for (let key in calcFormulaValues) {
formula = formula.replace(new RegExp(`{${key}}`, 'ig'), calcFormulaValues[key] || 0)
}
if (formula.includes('{')) {
this.setValue(null)
return
return false
}
try {
@ -819,7 +828,9 @@ class RbFormNumber extends RbFormText {
} catch (err) {
if (rb.env === 'dev') console.log(err)
}
return true
})
}, 200)
}
}
@ -859,8 +870,8 @@ class RbFormTextarea extends RbFormElement {
className={`form-control form-control-sm row3x ${this.state.hasError ? 'is-invalid' : ''} ${this.props.useMdedit && this.props.readonly ? 'cm-readonly' : ''}`}
title={this.state.hasError}
value={this.state.value || ''}
onChange={(e) => this.handleChange(e)}
onBlur={this.props.readonly ? null : () => this.checkValue()}
onChange={(e) => this.handleChange(e, this.props.readonly ? false : true)}
// onBlur={this.props.readonly ? null : () => this.checkValue()}
readOnly={this.props.readonly}
maxLength="6000"
/>
@ -973,8 +984,8 @@ class RbFormDateTime extends RbFormElement {
title={this.state.hasError}
type="text"
value={this.state.value || ''}
onChange={(e) => this.handleChange(e)}
onBlur={this.props.readonly ? null : () => this.checkValue()}
onChange={(e) => this.handleChange(e, this.props.readonly ? false : true)}
// onBlur={this.props.readonly ? null : () => this.checkValue()}
maxLength="20"
/>
<span className={'zmdi zmdi-close clean ' + (this.state.value ? '' : 'hide')} onClick={() => this.handleClear()} />
@ -1405,7 +1416,8 @@ class RbFormReference extends RbFormElement {
componentWillUnmount() {
super.componentWillUnmount()
if (this._ReferenceSearcher) {
if (this._ReferenceSearcher && !this._hasCascadingField) {
this._ReferenceSearcher.destroy()
this._ReferenceSearcher = null
}

View file

@ -32,7 +32,7 @@ class ContentFieldAggregation extends ActionContentSpec {
{this.state.hadApproval && (
<div className="form-text text-danger">
<i className="zmdi zmdi-alert-triangle fs-16 down-1 mr-1" />
{$L('目标实体已启用审批流程可能影响源实体操作 (触发动作)')}
{$L('目标实体已启用审批流程可能影响源实体操作 (触发动作)建议启用允许强制更新')}
</div>
)}
</div>
@ -120,6 +120,16 @@ class ContentFieldAggregation extends ActionContentSpec {
</div>
</div>
<div className="form-group row">
<label className="col-md-12 col-lg-3 col-form-label text-lg-right">{$L('聚合数据条件')}</label>
<div className="col-md-12 col-lg-9">
<a className="btn btn-sm btn-link pl-0 text-left down-2" onClick={() => this.dataAdvFilter()}>
{this.state.dataFilterItems ? `${$L('已设置条件')} (${this.state.dataFilterItems})` : $L('点击设置')}
</a>
<div className="form-text mt-0">{$L('仅会聚合符合过滤条件的数据')}</div>
</div>
</div>
<div className="form-group row pb-0">
<label className="col-md-12 col-lg-3 col-form-label" />
<div className="col-md-12 col-lg-9">
@ -130,17 +140,16 @@ class ContentFieldAggregation extends ActionContentSpec {
<i className="zmdi zmdi-help zicon down-1" data-toggle="tooltip" title={$L('本选项仅针对表单有效')} />
</span>
</label>
<div className="mt-2">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline mb-0">
<input className="custom-control-input" type="checkbox" ref={(c) => (this._$forceUpdate = c)} />
<span className="custom-control-label">
{$L('允许强制更新')}
<i className="zmdi zmdi-help zicon down-1" data-toggle="tooltip" title={$L('强制更新只读记录')} />
</span>
</label>
</div>
</div>
<div className="form-group row">
<label className="col-md-12 col-lg-3 col-form-label text-lg-right">{$L('聚合数据条件')}</label>
<div className="col-md-12 col-lg-9">
<a className="btn btn-sm btn-link pl-0 text-left down-2" onClick={() => this.dataAdvFilter()}>
{this.state.dataFilterItems ? `${$L('已设置条件')} (${this.state.dataFilterItems})` : $L('点击设置')}
</a>
<div className="form-text mt-0">{$L('仅会聚合符合过滤条件的数据')}</div>
</div>
</div>
</form>
</div>
@ -169,6 +178,7 @@ class ContentFieldAggregation extends ActionContentSpec {
if (content) {
$(this._$readonlyFields).attr('checked', content.readonlyFields === true)
$(this._$forceUpdate).attr('checked', content.forceUpdate === true)
this.saveAdvFilter(content.dataFilter)
}
}
@ -320,6 +330,7 @@ class ContentFieldAggregation extends ActionContentSpec {
targetEntity: $(this._$targetEntity).val(),
items: this.state.items || [],
readonlyFields: $(this._$readonlyFields).prop('checked'),
forceUpdate: $(this._$forceUpdate).prop('checked'),
dataFilter: this._advFilter__data,
}

View file

@ -45,7 +45,7 @@ class ContentFieldWriteback extends ActionContentSpec {
{this.state.hadApproval && (
<div className="form-text text-danger">
<i className="zmdi zmdi-alert-triangle fs-16 down-1 mr-1" />
{$L('目标实体已启用审批流程可能影响源实体操作 (触发动作)')}
{$L('目标实体已启用审批流程可能影响源实体操作 (触发动作)建议启用允许强制更新')}
</div>
)}
</div>
@ -155,6 +155,15 @@ class ContentFieldWriteback extends ActionContentSpec {
<i className="zmdi zmdi-help zicon down-1" data-toggle="tooltip" title={$L('本选项仅针对表单有效')} />
</span>
</label>
<div className="mt-2">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline mb-0">
<input className="custom-control-input" type="checkbox" ref={(c) => (this._$forceUpdate = c)} />
<span className="custom-control-label">
{$L('允许强制更新')}
<i className="zmdi zmdi-help zicon down-1" data-toggle="tooltip" title={$L('强制更新只读记录')} />
</span>
</label>
</div>
</div>
</div>
</form>
@ -183,6 +192,7 @@ class ContentFieldWriteback extends ActionContentSpec {
if (content) {
$(this._$readonlyFields).attr('checked', content.readonlyFields === true)
$(this._$forceUpdate).attr('checked', content.forceUpdate === true)
}
}
@ -290,6 +300,7 @@ class ContentFieldWriteback extends ActionContentSpec {
targetEntity: $(this._$targetEntity).val(),
items: this.state.items,
readonlyFields: $(this._$readonlyFields).prop('checked'),
forceUpdate: $(this._$forceUpdate).prop('checked'),
}
if (!content.targetEntity) {
RbHighbar.create($L('请选择目标实体'))

View file

@ -0,0 +1,31 @@
/*
Copyright (c) REBUILD <https://getrebuild.com/> and/or its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.core.support.task;
import cn.devezhao.commons.ThreadPool;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author devezhao
* @since 04/08/2022
*/
class TaskExecutorsTest {
@Test
void queue() {
AtomicInteger idx = new AtomicInteger(0);
for (int i = 0; i < 10; i++) {
TaskExecutors.queue(() -> {
ThreadPool.waitFor(500);
System.out.println("command " + idx.incrementAndGet());
});
}
}
}