diff --git a/orgmanax-infra/pom.xml b/orgmanax-infra/pom.xml
index a4e24bf..d5331bc 100644
--- a/orgmanax-infra/pom.xml
+++ b/orgmanax-infra/pom.xml
@@ -125,5 +125,10 @@
msg-center-api-v2
1.0.1-SNAPSHOT
+
+
+ com.xuxueli
+ xxl-job-core
+
\ No newline at end of file
diff --git a/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/client/RpcWrapper.java b/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/client/RpcWrapper.java
index 3a87ad0..45dc765 100644
--- a/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/client/RpcWrapper.java
+++ b/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/client/RpcWrapper.java
@@ -4,6 +4,7 @@ import cn.axzo.foundation.exception.BusinessException;
import cn.axzo.foundation.result.ApiResult;
import cn.axzo.foundation.util.TraceUtils;
import cn.axzo.orgmanax.common.config.BizResultCode;
+import cn.azxo.framework.common.model.CommonResponse;
import cn.hutool.core.convert.Convert;
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
@@ -39,6 +40,23 @@ public class RpcWrapper {
return wrapApiResult(supplier, desc, params, true);
}
+ /**
+ * common res 返回
+ * @param t
+ * @return
+ * @param
+ */
+ public static T commonRes(Supplier> t) {
+ CommonResponse result = t.get();
+ if (result == null) {
+ throw new BusinessException(BizResultCode.RPC_ERROR);
+ }
+ if (!Objects.equals(result.getCode(), HttpStatus.HTTP_OK)) {
+ throw new BusinessException(Convert.toStr(result.getCode()), result.getMsg(), null);
+ }
+ return result.getData();
+ }
+
/**
* Api result 返回
*
diff --git a/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/client/workspace/WorkspaceGateway.java b/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/client/workspace/WorkspaceGateway.java
index 3ffa262..8ad227d 100644
--- a/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/client/workspace/WorkspaceGateway.java
+++ b/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/client/workspace/WorkspaceGateway.java
@@ -10,12 +10,26 @@ import cn.axzo.apollo.workspace.api.v2.workspace.req.WorkspaceUpsertReq;
import cn.axzo.apollo.workspace.api.v2.workspace.resp.WorkspaceDTO;
import cn.axzo.apollo.workspace.api.v2.workspace.resp.WorkspaceDetailResp;
import cn.axzo.apollo.workspace.api.v2.workspace.resp.WorkspaceUpsertResp;
+import cn.axzo.apollo.workspace.api.workspace.WorkspaceSceneConfigApi;
+import cn.axzo.apollo.workspace.api.workspace.req.scene.WorkspaceSceneConfigListReq;
+import cn.axzo.apollo.workspace.api.workspace.res.scene.SceneConfigRes;
+import cn.axzo.apollo.workspace.common.enums.SceneConfigSwitch;
+import cn.axzo.apollo.workspace.common.enums.WorkspaceSceneConfigType;
import cn.axzo.orgmanax.infra.client.RpcWrapper;
+import cn.azxo.framework.common.logger.MethodAroundLog;
+import cn.hutool.core.collection.CollUtil;
+import com.google.common.collect.ImmutableList;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
/**
*
@@ -25,9 +39,15 @@ import java.util.List;
@Slf4j
public class WorkspaceGateway {
+ private static final ImmutableList AUTO_LEAVE_CONFIG_TYPES = ImmutableList.of(
+ WorkspaceSceneConfigType.WORKER_AUTO_LEAVE,
+ WorkspaceSceneConfigType.WORKER_AUTO_LEAVE_DAYS
+ );
+
private final WorkspaceV2Api workspaceV2Api;
private final ParticipatingUnitV2Api participatingUnitV2Api;
+ private final WorkspaceSceneConfigApi workspaceSceneConfigApi;
/**
* 更新和新增
@@ -62,4 +82,55 @@ public class WorkspaceGateway {
public List listParticipatingUnits(ParticipatingUnitListReq unitListReq) {
return RpcWrapper.wrapApiResult(() -> participatingUnitV2Api.list(unitListReq));
}
+
+ /**
+ * 查询项目相关配置项
+ *
+ * @param request 入参
+ * @return 配置项列表
+ */
+ @MethodAroundLog(source = "orgmanax", target = "workspace", value = "查询项目相关配置项")
+ public List listConfigs(WorkspaceSceneConfigListReq request) {
+ return RpcWrapper.commonRes(() -> workspaceSceneConfigApi.listConfigs(request));
+ }
+
+ /**
+ * 获取人员自动离场的天数配置(连续多少天没有打卡记录)
+ *
+ * @param workspaceIds 项目id列表
+ * @return 配置项的map
+ */
+ @MethodAroundLog(source = "orgmanax", target = "workspace", value = "获取人员自动离场的天数配置")
+ public Map listAutoLeaveDaysConfig(Collection workspaceIds) {
+ if (CollUtil.isEmpty(workspaceIds)) {
+ return Collections.emptyMap();
+ }
+ WorkspaceSceneConfigListReq request = WorkspaceSceneConfigListReq.builder()
+ .workspaceIds(CollUtil.distinct(workspaceIds))
+ .types(AUTO_LEAVE_CONFIG_TYPES)
+ .build();
+ // list
+ List sceneConfigs = listConfigs(request);
+ if (CollUtil.isEmpty(sceneConfigs)) {
+ return Collections.emptyMap();
+ }
+ // grouping
+ Map> groupingByWorkspaceId = sceneConfigs.stream()
+ .collect(Collectors.groupingBy(SceneConfigRes::getWorkspaceId));
+ // mapper function
+ Function, SceneConfigRes> mapper = list -> {
+ if (list.stream().anyMatch(e -> WorkspaceSceneConfigType.WORKER_AUTO_LEAVE.name().equals(e.getType())
+ && Objects.equals(e.getValue(), SceneConfigSwitch.OPEN.getValue()))) {
+ return list.stream()
+ .filter(e -> WorkspaceSceneConfigType.WORKER_AUTO_LEAVE_DAYS.name().equals(e.getType()))
+ .findFirst().orElse(null);
+ }
+ // switch not exist or closed
+ return null;
+ };
+ return groupingByWorkspaceId.values().stream()
+ .map(mapper)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toMap(SceneConfigRes::getWorkspaceId, e -> Integer.valueOf(e.getValue()), (oldVal, newVal) -> oldVal));
+ }
}
diff --git a/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/dao/nodeuser/repository/NodeUserQueryRepository.java b/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/dao/nodeuser/repository/NodeUserQueryRepository.java
index 5c98268..513dda7 100644
--- a/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/dao/nodeuser/repository/NodeUserQueryRepository.java
+++ b/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/dao/nodeuser/repository/NodeUserQueryRepository.java
@@ -18,7 +18,12 @@ import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
public interface NodeUserQueryRepository {
@@ -52,6 +57,8 @@ public interface NodeUserQueryRepository {
private Long id;
@CriteriaField(field = "id", operator = Operator.GT)
private Long idGt;
+ @CriteriaField(field = "id", operator = Operator.LT)
+ private Long idLt;
@CriteriaField(field = "id", operator = Operator.IN)
private Collection ids;
@@ -156,6 +163,8 @@ public interface NodeUserQueryRepository {
*/
@CriteriaField
private Long workspaceId;
+ @CriteriaField(field = "workspaceId", operator = Operator.GT)
+ private Long workspaceIdGt;
@CriteriaField(field = "workspaceId", operator = Operator.IN)
private Collection workspaceIds;
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/cooperateship/foundation/CooperateShipFoundationService.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/cooperateship/foundation/CooperateShipFoundationService.java
index 4f41b66..f5e027a 100644
--- a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/cooperateship/foundation/CooperateShipFoundationService.java
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/cooperateship/foundation/CooperateShipFoundationService.java
@@ -1,11 +1,11 @@
package cn.axzo.orgmanax.server.cooperateship.foundation;
import cn.axzo.orgmanax.infra.dao.cooperateship.entity.SaasCooperateShip;
+import cn.axzo.orgmanax.infra.dao.cooperateship.repository.CooperateShipQueryRepository.ListReq;
import cn.axzo.orgmanax.server.cooperateship.foundation.dto.CooperateShipCreator;
import java.util.Collection;
import java.util.List;
-import java.util.Set;
import java.util.function.Function;
public interface CooperateShipFoundationService {
@@ -76,4 +76,12 @@ public interface CooperateShipFoundationService {
* 创建协同关系通用方法
*/
SaasCooperateShip create(CooperateShipCreator creator);
+
+ /**
+ * 协同关系通用查询
+ *
+ * @param req 入参
+ * @return 协同关系节点列表
+ */
+ List list(ListReq req);
}
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/cooperateship/foundation/impl/CooperateShipFoundationServiceImpl.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/cooperateship/foundation/impl/CooperateShipFoundationServiceImpl.java
index dc65725..130f65e 100644
--- a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/cooperateship/foundation/impl/CooperateShipFoundationServiceImpl.java
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/cooperateship/foundation/impl/CooperateShipFoundationServiceImpl.java
@@ -217,4 +217,9 @@ public class CooperateShipFoundationServiceImpl implements CooperateShipFoundati
return savedCooperateShip;
}
+ @Override
+ public List list(CooperateShipQueryRepository.ListReq req) {
+ return cooperateShipQueryRepository.list(req);
+ }
+
}
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/NodeUserExtraFoundationService.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/NodeUserExtraFoundationService.java
new file mode 100644
index 0000000..1b5ed62
--- /dev/null
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/NodeUserExtraFoundationService.java
@@ -0,0 +1,31 @@
+package cn.axzo.orgmanax.server.nodeuser.foundation;
+
+import cn.axzo.foundation.page.PageResp;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserExtraQueryRepository.ListReq;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserExtraQueryRepository.NodeUserExtraResp;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author luofu
+ * @version 1.0
+ * @date 2025/3/11
+ */
+public interface NodeUserExtraFoundationService {
+
+ /***
+ * 分页查询
+ *
+ * @param req 入参
+ * @return node_user_extra 列表
+ */
+ PageResp page(ListReq req);
+
+ /**
+ * 批量设置用户标签
+ *
+ * @param idTagsMap key:id, value:标签集合
+ */
+ void updateTagsMap(Map> idTagsMap);
+}
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/NodeUserFoundationService.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/NodeUserFoundationService.java
index 9806e56..6cf3bd2 100644
--- a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/NodeUserFoundationService.java
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/NodeUserFoundationService.java
@@ -1,8 +1,11 @@
package cn.axzo.orgmanax.server.nodeuser.foundation;
+import cn.axzo.foundation.page.PageResp;
import cn.axzo.orgmanax.dto.nodeuser.req.SearchEntNodeUserReq;
import cn.axzo.orgmanax.dto.nodeuser.resp.SearchEntNodeUserResp;
import cn.axzo.orgmanax.infra.dao.nodeuser.entity.OrganizationalNodeUser;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserQueryRepository.ListReq;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserQueryRepository.NodeUserResp;
import cn.axzo.orgmanax.server.nodeuser.foundation.req.NodeUserCreate;
import cn.axzo.orgmanax.server.nodeuser.foundation.req.NodeUserDelete;
import cn.axzo.orgmanax.server.nodeuser.foundation.req.NodeUserUpdate;
@@ -55,4 +58,12 @@ public interface NodeUserFoundationService {
* @return
*/
List searchEntUser(SearchEntNodeUserReq req);
+
+ /**
+ * 通用分页查询
+ *
+ * @param req 入参
+ * @return node_user列表
+ */
+ PageResp page(ListReq req);
}
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/impl/NodeUserExtraFoundationServiceImpl.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/impl/NodeUserExtraFoundationServiceImpl.java
new file mode 100644
index 0000000..275e2ce
--- /dev/null
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/impl/NodeUserExtraFoundationServiceImpl.java
@@ -0,0 +1,38 @@
+package cn.axzo.orgmanax.server.nodeuser.foundation.impl;
+
+import cn.axzo.foundation.page.PageResp;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserExtraQueryRepository;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserExtraQueryRepository.ListReq;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserExtraQueryRepository.NodeUserExtraResp;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserExtraUpsertRepository;
+import cn.axzo.orgmanax.server.nodeuser.foundation.NodeUserExtraFoundationService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author luofu
+ * @version 1.0
+ * @date 2025/3/11
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class NodeUserExtraFoundationServiceImpl implements NodeUserExtraFoundationService {
+
+ private final NodeUserExtraQueryRepository queryRepository;
+ private final NodeUserExtraUpsertRepository upsertRepository;
+
+ @Override
+ public PageResp page(ListReq req) {
+ return queryRepository.page(req);
+ }
+
+ @Override
+ public void updateTagsMap(Map> idTagsMap) {
+ upsertRepository.updateTagsMap(idTagsMap);
+ }
+}
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/impl/NodeUserFoundationServiceImpl.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/impl/NodeUserFoundationServiceImpl.java
index 1ce877f..148ee8d 100644
--- a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/impl/NodeUserFoundationServiceImpl.java
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/nodeuser/foundation/impl/NodeUserFoundationServiceImpl.java
@@ -5,6 +5,7 @@ import cn.axzo.apollo.workspace.api.v2.workspace.resp.WorkspaceDetailResp;
import cn.axzo.foundation.event.support.Event;
import cn.axzo.foundation.event.support.producer.EventProducer;
import cn.axzo.foundation.exception.Axssert;
+import cn.axzo.foundation.page.PageResp;
import cn.axzo.orgmanax.common.config.BizResultCode;
import cn.axzo.orgmanax.dto.common.util.NumberUtil;
import cn.axzo.orgmanax.dto.nodeuser.enums.NodeUserTypeEnum;
@@ -21,6 +22,8 @@ import cn.axzo.orgmanax.infra.dao.node.repository.NodeQueryRepository;
import cn.axzo.orgmanax.infra.dao.nodeuser.entity.OrganizationalNodeUser;
import cn.axzo.orgmanax.infra.dao.nodeuser.mapper.OrganizationalNodeUserMapper;
import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserQueryRepository;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserQueryRepository.ListReq;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserQueryRepository.NodeUserResp;
import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserUpsertRepository;
import cn.axzo.orgmanax.infra.dao.orgjob.entity.OrgJob;
import cn.axzo.orgmanax.infra.dao.orgjob.repository.OrgJobQueryRepository;
@@ -46,7 +49,11 @@ import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import java.util.*;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -258,6 +265,11 @@ public class NodeUserFoundationServiceImpl implements NodeUserFoundationService
return organizationalNodeUserMapper.searchEntUser(req);
}
+ @Override
+ public PageResp page(ListReq req) {
+ return nodeUserQueryRepository.page(req);
+ }
+
private Long resolveWorkspaceId(OrganizationalNode node) {
if (Objects.equals(node.getNodeType(), NodeUserTypeEnum.TEAM.getValue())) {
return 0L;
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/config/PersonTagConfig.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/config/PersonTagConfig.java
new file mode 100644
index 0000000..45685c6
--- /dev/null
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/config/PersonTagConfig.java
@@ -0,0 +1,59 @@
+package cn.axzo.orgmanax.server.orguser.config;
+
+import com.alibaba.fastjson.JSON;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.annotation.Configuration;
+
+import java.io.Serializable;
+
+/**
+ * @author luofu
+ * @version 1.0
+ * @description 人员标签配置
+ * @date 2025/3/6
+ */
+@Data
+@RefreshScope
+@Configuration
+@ConfigurationProperties(prefix = "person-tag")
+public class PersonTagConfig {
+
+ /**
+ * 离场标签
+ */
+ private TagNode leaveTagNode = TagNode.of("station", "leave proj");
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class TagNode implements Serializable {
+
+ private static final long serialVersionUID = -8933389377639107477L;
+
+ private String nodeCode;
+
+ private String valueCode;
+
+ public String format(String spliter) {
+ return this.nodeCode + spliter + this.valueCode;
+ }
+
+ private static TagNode of(String nodeCode, String valueCode) {
+ return TagNode.builder()
+ .nodeCode(nodeCode)
+ .valueCode(valueCode)
+ .build();
+ }
+
+ @Override
+ public String toString() {
+ return JSON.toJSONString(this);
+ }
+ }
+}
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/xxl/AutoAddWorkerLeavedTagJob.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/xxl/AutoAddWorkerLeavedTagJob.java
new file mode 100644
index 0000000..49030bd
--- /dev/null
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/xxl/AutoAddWorkerLeavedTagJob.java
@@ -0,0 +1,212 @@
+package cn.axzo.orgmanax.server.orguser.xxl;
+
+import cn.axzo.foundation.page.PageResp;
+import cn.axzo.orgmanax.dto.common.IdentityType;
+import cn.axzo.orgmanax.dto.cooperateship.enums.CooperateShipStatusEnum;
+import cn.axzo.orgmanax.dto.cooperateship.enums.CooperateShipTypeEnum;
+import cn.axzo.orgmanax.dto.nodeuser.enums.TagOperateEnum;
+import cn.axzo.orgmanax.dto.nodeuser.req.TagOperateReq;
+import cn.axzo.orgmanax.infra.client.workspace.dto.Workspace;
+import cn.axzo.orgmanax.infra.dao.cooperateship.entity.SaasCooperateShip;
+import cn.axzo.orgmanax.infra.dao.cooperateship.repository.CooperateShipQueryRepository;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserQueryRepository.ListReq;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserQueryRepository.NodeUserResp;
+import cn.axzo.orgmanax.server.cooperateship.foundation.CooperateShipFoundationService;
+import cn.axzo.orgmanax.server.nodeuser.foundation.NodeUserFoundationService;
+import cn.hutool.core.collection.CollUtil;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author luofu
+ * @version 1.0
+ * @description 自动退场OR标记为离场人员的JOB
+ * @date 2025/3/6
+ */
+@Slf4j
+@Component
+public class AutoAddWorkerLeavedTagJob extends BaseAutoOperatePersonLeavedTagJob {
+
+ private static final String LOG_PREFIX = "[ADD_WORKER_LEAVED_TAG ] ";
+
+ private static final ImmutableList INCLUDE_WORKSPACE_TYPES = ImmutableList.of(Workspace.WorkspaceTypeEnum.GENERAL_PROJECT.getValue());
+ private static final ImmutableList INCLUDE_COOPERATE_TYPES = ImmutableList.of(CooperateShipTypeEnum.PROJ_TEAM.getCode(),
+ CooperateShipTypeEnum.PROJ_GROUP.getCode());
+ private static final ImmutableList INCLUDE_STATUS = ImmutableList.of(CooperateShipStatusEnum.PRESENT.getCode());
+
+ @Resource
+ private NodeUserFoundationService nodeUserFoundationService;
+ @Resource
+ private CooperateShipFoundationService cooperateShipFoundationService;
+
+ private Context context;
+
+ @Override
+ @XxlJob("autoAddWorkerLeavedTagJob")
+ public ReturnT execute(String param) {
+ // init context of job
+ initContext();
+ if (context.invalid()) {
+ // failed to init context.
+ error("failed to init context.");
+ return ReturnT.FAIL;
+ }
+ // start to execute
+ doExecute(param);
+ return ReturnT.SUCCESS;
+ }
+
+ @Override
+ String logPrefix() {
+ return LOG_PREFIX;
+ }
+
+ @Override
+ LocalDateTime startDateTimeOfJobExecution() {
+ return context.getStartDateTime();
+ }
+
+ @Override
+ List scrollList(long scrollIndex) {
+ ListReq page = new ListReq();
+ page.setIdLt(scrollIndex);
+ page.setIdentityType(IdentityType.WORKER.getCode());
+ page.setWorkspaceIdGt(0L);
+ PageResp pageResult = nodeUserFoundationService.page(page);
+ return Optional.ofNullable(pageResult.getData()).orElseGet(Collections::emptyList);
+ }
+
+ @Override
+ Long extractId(NodeUserResp record) {
+ return record.getId();
+ }
+
+ @Override
+ Long extractPersonId(NodeUserResp record) {
+ return record.getPersonId();
+ }
+
+ @Override
+ Long extractWorkspaceId(NodeUserResp record) {
+ return record.getWorkspaceId();
+ }
+
+ @Override
+ void handle(Long workspaceId, List records, List clockedPersonIds) {
+ Date joinedAtLe = context.getJoinedAtLe(workspaceId);
+ if (Objects.isNull(joinedAtLe)) {
+ return;
+ }
+ List withoutClockedPersonIds = records.stream()
+ // filter join_at condition
+ .filter(e -> Objects.nonNull(e.getJoinAt()) && joinedAtLe.after(e.getJoinAt()))
+ .map(NodeUserResp::getPersonId)
+ .filter(personId -> !clockedPersonIds.contains(personId))
+ .distinct().collect(Collectors.toList());
+ if (CollUtil.isEmpty(withoutClockedPersonIds)) {
+ // all person contains clocked record.
+ return;
+ }
+ // auto add leaved tag
+ addLeavedTag(workspaceId, withoutClockedPersonIds);
+ }
+
+ @Override
+ Integer listAutoLeaveDaysConfig(Long workspaceId) {
+ return context.getWorkspaceDaysConfig().get(workspaceId);
+ }
+
+ private void initContext() {
+ context = new Context();
+ assembleDataScope(context);
+ assembleWorkspaceConfig(context);
+ }
+
+ private void assembleDataScope(Context context) {
+ CooperateShipQueryRepository.ListReq listReq = new CooperateShipQueryRepository.ListReq();
+ listReq.setWorkspaceTypes(INCLUDE_WORKSPACE_TYPES);
+ listReq.setIncludeCooperateTypes(INCLUDE_COOPERATE_TYPES);
+ listReq.setStatuses(INCLUDE_STATUS);
+ Map> dataScope = cooperateShipFoundationService.list(listReq).stream()
+ .collect(Collectors.groupingBy(SaasCooperateShip::getWorkspaceId));
+ context.setActiveProjectTeamAndGroups(dataScope);
+ }
+
+ private void assembleWorkspaceConfig(Context context) {
+ if (context.invalid()) {
+ return;
+ }
+ Map workspaceDaysConfig = Maps.newHashMap();
+ context.setWorkspaceDaysConfig(workspaceDaysConfig);
+ Set workspaceIds = context.getWorkspaceDaysConfig().keySet();
+ List> partition = Lists.partition(Lists.newArrayList(workspaceIds), 20);
+ for (List sub : partition) {
+ Map autoLeaveDaysConfigMap = workspaceGateway.listAutoLeaveDaysConfig(sub);
+ if (CollUtil.isEmpty(autoLeaveDaysConfigMap)) {
+ continue;
+ }
+ workspaceDaysConfig.putAll(autoLeaveDaysConfigMap);
+ }
+ }
+
+ private void addLeavedTag(Long workspaceId, List personIds) {
+ TagOperateReq param = new TagOperateReq();
+ param.setPersonIds(personIds);
+ param.setLoginWorkspaceId(workspaceId);
+ param.setTagNodeCode(personTagConfig.getLeaveTagNode().getNodeCode());
+ param.setTagValueCode(personTagConfig.getLeaveTagNode().getValueCode());
+ param.setOperateType(TagOperateEnum.ADD);
+ orgUserService.operateTag(param);
+ }
+
+ @Data
+ private static class Context {
+
+ private LocalDateTime startDateTime = LocalDateTime.now();
+
+ /**
+ * 在场的项目内班组和小组节点
+ * key: workspaceId
+ * value: 在场的项目内班组和小组节点列表
+ */
+ private Map> activeProjectTeamAndGroups;
+
+ /**
+ * 开关开启的项目id集合
+ * key: workspaceId
+ * value: days
+ */
+ private Map workspaceDaysConfig;
+
+ boolean invalid() {
+ return CollUtil.isEmpty(activeProjectTeamAndGroups);
+ }
+
+ Date getJoinedAtLe(Long workspaceId) {
+ Integer days = workspaceDaysConfig.get(workspaceId);
+ if (Objects.isNull(days)) {
+ return null;
+ }
+ LocalDateTime le = startDateTime.minusDays(days);
+ return Date.from(le.atZone(ZoneId.systemDefault()).toInstant());
+ }
+ }
+}
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/xxl/AutoClearPersonLeavedTagJob.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/xxl/AutoClearPersonLeavedTagJob.java
new file mode 100644
index 0000000..34af79a
--- /dev/null
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/xxl/AutoClearPersonLeavedTagJob.java
@@ -0,0 +1,136 @@
+package cn.axzo.orgmanax.server.orguser.xxl;
+
+import cn.axzo.foundation.page.PageResp;
+import cn.axzo.orgmanax.dto.nodeuser.enums.TagOperateEnum;
+import cn.axzo.orgmanax.dto.nodeuser.req.TagOperateReq;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserExtraQueryRepository;
+import cn.axzo.orgmanax.infra.dao.nodeuser.repository.NodeUserExtraQueryRepository.NodeUserExtraResp;
+import cn.axzo.orgmanax.server.nodeuser.foundation.NodeUserExtraFoundationService;
+import cn.hutool.core.collection.CollUtil;
+import com.google.common.collect.Maps;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * @author luofu
+ * @version 1.0
+ * @description 自动清除人员离场标签的JOB
+ * @date 2025/3/6
+ */
+@Slf4j
+@Component
+public class AutoClearPersonLeavedTagJob extends BaseAutoOperatePersonLeavedTagJob {
+
+ private static final String LOG_PREFIX = "[CLEAR_PERSON_LEAVED_TAG] ";
+
+ @Resource
+ private NodeUserExtraFoundationService nodeUserExtraFoundationService;
+
+ private Context context;
+
+ @Override
+ @XxlJob("autoClearPersonLeavedTagJob")
+ public ReturnT execute(String param) {
+ // init context
+ context = new Context();
+ doExecute(param);
+ return ReturnT.SUCCESS;
+ }
+
+ @Override
+ String logPrefix() {
+ return LOG_PREFIX;
+ }
+
+ @Override
+ LocalDateTime startDateTimeOfJobExecution() {
+ return context.getStartDateTime();
+ }
+
+ @Override
+ List scrollList(long scrollIndex) {
+ String tag = personTagConfig.getLeaveTagNode().format(":");
+ NodeUserExtraQueryRepository.ListReq page = new NodeUserExtraQueryRepository.ListReq();
+ page.setIdLt(scrollIndex);
+ page.setTag(tag);
+ page.setPageSize(MAX_CNT_ONCE);
+ page.setSort(Collections.singletonList(sortDesc("id")));
+ PageResp pageResult = nodeUserExtraFoundationService.page(page);
+ return Optional.ofNullable(pageResult.getData()).orElseGet(Collections::emptyList);
+ }
+
+ @Override
+ Long extractId(NodeUserExtraResp record) {
+ return record.getId();
+ }
+
+ @Override
+ Long extractPersonId(NodeUserExtraResp record) {
+ return record.getPersonId();
+ }
+
+ @Override
+ Long extractWorkspaceId(NodeUserExtraResp record) {
+ return record.getWorkspaceId();
+ }
+
+ @Override
+ void handle(Long workspaceId, List records, List clockedPersonIds) {
+ if (CollUtil.isEmpty(clockedPersonIds)) {
+ // none clocked person.
+ return;
+ }
+ TagOperateReq param = new TagOperateReq();
+ param.setPersonIds(clockedPersonIds);
+ param.setLoginWorkspaceId(workspaceId);
+ param.setTagNodeCode(personTagConfig.getLeaveTagNode().getNodeCode());
+ param.setTagValueCode(personTagConfig.getLeaveTagNode().getValueCode());
+ param.setOperateType(TagOperateEnum.REMOVE);
+ orgUserService.operateTag(param);
+ }
+
+ @Override
+ Integer listAutoLeaveDaysConfig(Long workspaceId) {
+ // fetch from local cache first.
+ Integer days = context.getWorkspaceDaysConfig().get(workspaceId);
+ if (Objects.nonNull(days)) {
+ return days;
+ }
+ // fetch from attendance service.
+ Map map = workspaceGateway.listAutoLeaveDaysConfig(Collections.singletonList(workspaceId));
+ if (CollUtil.isEmpty(map)) {
+ return null;
+ }
+ // cache the config of this workspace
+ context.getWorkspaceDaysConfig().putAll(map);
+ // return days config
+ return context.getWorkspaceDaysConfig().get(workspaceId);
+ }
+
+ @Data
+ private static class Context {
+
+ /**
+ * start time of job execute
+ */
+ private LocalDateTime startDateTime = LocalDateTime.now();
+
+ /**
+ * days configuration for add person tag of workspace.
+ * key: workspaceId
+ * value: days
+ */
+ private Map workspaceDaysConfig = Maps.newHashMap();
+ }
+}
diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/xxl/BaseAutoOperatePersonLeavedTagJob.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/xxl/BaseAutoOperatePersonLeavedTagJob.java
new file mode 100644
index 0000000..577d9fb
--- /dev/null
+++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/xxl/BaseAutoOperatePersonLeavedTagJob.java
@@ -0,0 +1,172 @@
+package cn.axzo.orgmanax.server.orguser.xxl;
+
+import cn.axzo.foundation.page.IPageReq;
+import cn.axzo.orgmanax.infra.client.attendance.AttendanceRecordClient;
+import cn.axzo.orgmanax.infra.client.attendance.dto.AttendanceClockRecordListReq;
+import cn.axzo.orgmanax.infra.client.attendance.dto.AttendanceClockRecordResp;
+import cn.axzo.orgmanax.infra.client.workspace.WorkspaceGateway;
+import cn.axzo.orgmanax.server.orguser.config.PersonTagConfig;
+import cn.axzo.orgmanax.server.orguser.service.OrgUserService;
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.fastjson.JSON;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.xxl.job.core.handler.IJobHandler;
+import com.xxl.job.core.log.XxlJobLogger;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author luofu
+ * @version 1.0
+ * @date 2025/3/6
+ */
+@Slf4j
+public abstract class BaseAutoOperatePersonLeavedTagJob extends IJobHandler {
+
+ static final int MAX_CNT_ONCE = 4;
+
+ @Resource
+ PersonTagConfig personTagConfig;
+ @Resource
+ OrgUserService orgUserService;
+
+ @Resource
+ AttendanceRecordClient attendanceRecordApiClient;
+ @Resource
+ WorkspaceGateway workspaceGateway;
+
+ void doExecute(String param) {
+ long scrollIndex = Long.MAX_VALUE;
+ info("start to scan org_user.");
+ // key: workspaceId value: records
+ Map> localCache = Maps.newHashMap();
+ List records = scrollList(scrollIndex);
+ while (CollUtil.isNotEmpty(records)) {
+ // the min id of records.
+ scrollIndex = records.stream().mapToLong(this::extractId).min().orElse(0);
+ // cache first, then trigger if enough.
+ cacheAndTrigger(localCache, records);
+ // the next
+ records = scrollList(scrollIndex);
+ }
+ // scan cached records
+ localCache.forEach(this::trigger);
+ info("completed to scan org_user.");
+ }
+
+ abstract String logPrefix();
+
+ abstract LocalDateTime startDateTimeOfJobExecution();
+
+ abstract List scrollList(long scrollIndex);
+
+ abstract Long extractId(T record);
+
+ abstract Long extractPersonId(T record);
+
+ abstract Long extractWorkspaceId(T record);
+
+ abstract void handle(Long workspaceId, List records, List clockedPersonIds);
+
+ abstract Integer listAutoLeaveDaysConfig(Long workspaceId);
+
+ private void cacheAndTrigger(Map> localCache, List records) {
+ // grouping by workspaceId
+ Map> groupingByWorkspaceId = records.stream().collect(Collectors.groupingBy(this::extractWorkspaceId));
+ // scan for cache and trigger
+ groupingByWorkspaceId.forEach((k, v) -> cacheAndTrigger(localCache, k, v));
+ }
+
+ private void cacheAndTrigger(Map> localCache, Long workspaceId, List sub) {
+ List cacheRecords = localCache.computeIfAbsent(workspaceId, k -> Lists.newArrayList());
+ // merge cur records and cached records
+ cacheRecords.addAll(sub);
+ if (cacheRecords.size() < MAX_CNT_ONCE) {
+ // return if not matched
+ return;
+ }
+ // remove from local cache
+ localCache.remove(workspaceId);
+ // deal matched partitions and cache not matched
+ List> partitions = Lists.partition(cacheRecords, MAX_CNT_ONCE);
+ for (List partition : partitions) {
+ if (partition.size() < MAX_CNT_ONCE) {
+ // cache
+ localCache.put(workspaceId, partition);
+ continue;
+ }
+ // trigger
+ trigger(workspaceId, partition);
+ }
+ }
+
+ public static String sortDesc(String field) {
+ return field + IPageReq.SORT_DELIMITER + IPageReq.SORT_DESC;
+ }
+
+ private void trigger(Long workspaceId, List records) {
+ try {
+ // list days config of cur workspace.
+ Integer days = listAutoLeaveDaysConfig(workspaceId);
+ if (Objects.isNull(days)) {
+ info("lack of days config for this workspace. workspaceId:[{}]", workspaceId);
+ return;
+ }
+ // in try ... catch block for continue the next ...
+ // find persons which contains clocked record during specified time.
+ List personIds = findClockedPersons(workspaceId, days, records);
+ info("start to remove person tag of workspace. [{}]", workspaceId);
+ // begin to handle the personIds.
+ handle(workspaceId, records, personIds);
+ info("completed to remove person tag of workspace. [{}]", workspaceId);
+ } catch (Exception e) {
+ error("broke out some exception. workspaceId:[{}]", workspaceId, e);
+ }
+ }
+
+ private List findClockedPersons(Long workspaceId, Integer days, List records) {
+ List personIds = CollUtil.map(records, this::extractPersonId, true);
+ if (CollUtil.isEmpty(personIds)) {
+ List ids = CollUtil.map(records, this::extractId, true);
+ error("invalid records, lack of personId. workspaceId:[{}], recordIds:{}", workspaceId, JSON.toJSONString(ids));
+ return Collections.emptyList();
+ }
+ // list persons which clocked during the range of specified time.
+ List clockRecords = listClockRecords(workspaceId, days, personIds);
+ // extract personId
+ return CollUtil.map(clockRecords, AttendanceClockRecordResp::getPersonId, true);
+ }
+
+ private List listClockRecords(Long workspaceId, Integer days, List personIds) {
+ LocalDateTime to = startDateTimeOfJobExecution();
+ LocalDateTime from = to.minusDays(days);
+ AttendanceClockRecordListReq request = new AttendanceClockRecordListReq();
+ request.setPersonIds(personIds);
+ request.setWorkspaceId(workspaceId);
+ request.setClockAtFrom(Date.from(from.atZone(ZoneOffset.systemDefault()).toInstant()));
+ request.setClockAtTo(Date.from(to.atZone(ZoneOffset.systemDefault()).toInstant()));
+ return attendanceRecordApiClient.listClockRecords(request);
+ }
+
+ void info(String msgFormat, Object... args) {
+ msgFormat = logPrefix() + msgFormat;
+ log.info(msgFormat, args);
+ XxlJobLogger.log(msgFormat, args);
+ }
+
+ void error(String msgFormat, Object... args) {
+ msgFormat = logPrefix() + msgFormat;
+ log.error(msgFormat, args);
+ XxlJobLogger.log(msgFormat, args);
+ }
+}