diff --git a/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/event/config/RocketMQEventConfig.java b/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/event/config/RocketMQEventConfig.java index 47e853e..1f095d6 100644 --- a/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/event/config/RocketMQEventConfig.java +++ b/orgmanax-infra/src/main/java/cn/axzo/orgmanax/infra/event/config/RocketMQEventConfig.java @@ -1,6 +1,11 @@ package cn.axzo.orgmanax.infra.event.config; -import cn.axzo.foundation.event.support.consumer.*; +import cn.axzo.foundation.event.support.consumer.DefaultEventConsumer; +import cn.axzo.foundation.event.support.consumer.DefaultRocketMQListener; +import cn.axzo.foundation.event.support.consumer.EventConsumer; +import cn.axzo.foundation.event.support.consumer.EventHandlerRepository; +import cn.axzo.foundation.event.support.consumer.RetryableEventConsumer; +import cn.axzo.foundation.event.support.consumer.RocketRetryableEventConsumer; import cn.axzo.foundation.event.support.producer.EventProducer; import cn.axzo.foundation.event.support.producer.RocketMQEventProducer; import cn.axzo.foundation.web.support.AppRuntime; @@ -72,6 +77,17 @@ public class RocketMQEventConfig { } } + @NonLocalCondition.Conditional + @Component + @RocketMQMessageListener(topic = "topic_attendance_common_${spring.profiles.active}", + consumerGroup = "GID_${spring.application.name}_attendance_common_${spring.profiles.active}", + consumeMode = ConsumeMode.ORDERLY, maxReconsumeTimes = 20) + public static class AttendanceListener extends DefaultRocketMQListener { + public AttendanceListener(EventConsumer eventConsumer) { + super(eventConsumer); + } + } + @Bean EventHandlerRepository eventHandlerRepository() { return EventHandlerRepository.builder() diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/event/outer/AttendanceEventEnum.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/event/outer/AttendanceEventEnum.java new file mode 100644 index 0000000..d6e8a45 --- /dev/null +++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/event/outer/AttendanceEventEnum.java @@ -0,0 +1,24 @@ +package cn.axzo.orgmanax.server.orguser.event.outer; + +import cn.axzo.foundation.event.support.Event; +import lombok.Getter; + +/** + * @author luofu + * @version 1.0 + * @description 考勤事件枚举 + * @date 2025/3/5 + */ +@Getter +public enum AttendanceEventEnum { + + ATTENDANCE_RECORD("ATTENDANCE", "attendance_record", "发送新增考勤记录"); + + private final Event.EventCode eventCode; + private final String desc; + + AttendanceEventEnum(String module, String tag, String desc) { + this.eventCode = Event.EventCode.builder().module(module).name(tag).build(); + this.desc = desc; + } +} diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/event/outer/handler/AttendanceRecordEventHandler.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/event/outer/handler/AttendanceRecordEventHandler.java new file mode 100644 index 0000000..9f53a19 --- /dev/null +++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/event/outer/handler/AttendanceRecordEventHandler.java @@ -0,0 +1,103 @@ +package cn.axzo.orgmanax.server.orguser.event.outer.handler; + +import cn.axzo.foundation.event.support.Event; +import cn.axzo.foundation.event.support.EventHandler; +import cn.axzo.foundation.event.support.consumer.EventConsumer; +import cn.axzo.orgmanax.dto.common.util.NumberUtil; +import cn.axzo.orgmanax.server.orguser.event.outer.AttendanceEventEnum; +import cn.axzo.orgmanax.server.orguser.event.outer.payload.AttendanceRecordPayload; +import com.google.common.collect.ImmutableList; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * @author luofu + * @version 1.0 + * @description 考勤新增记录事件处理器 + * @date 2025/3/5 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AttendanceRecordEventHandler implements EventHandler, InitializingBean { + + private static final String HANDLER_ALIAS = "ATTENDANCE_RECORD_EVENT_HANDLER"; + private static final String LOG_PREFIX = "[" + HANDLER_ALIAS + "] "; + + private static final ImmutableList EXPECTED_CLOCK_TYPES = ImmutableList.of( + AttendanceRecordPayload.CLOCK_TYPE_ENTRY, + AttendanceRecordPayload.CLOCK_TYPE_EXIT + ); + + private final EventConsumer eventConsumer; + + @Override + public void onEvent(Event event, EventConsumer.Context context) { + info("received the event. msgId:{}", context.getMsgId()); + AttendanceRecordPayload payload = parsePayload(event); + if (Objects.isNull(payload)) { + info("the event data is invalid. msgId:{}", context.getMsgId()); + return; + } + if (invalidPayload(payload)) { + error("invalid payload. msgId:{}, data:{}", context.getMsgId(), payload); + return; + } + if (unexpectedPayload(payload)) { + info("unexpected payload. msgId:{}, data:{}", context.getMsgId(), payload); + return; + } + info("started to handle the event. msgId:{}", context.getMsgId()); + handle(payload, context.getMsgId()); + info("completed to handle the event. msgId:{}", context.getMsgId()); + } + + private AttendanceRecordPayload parsePayload(Event event) { + try { + return event.normalizedData(AttendanceRecordPayload.class); + } catch (Exception e) { + error("broke out some exception while parsing payload.", e); + return null; + } + } + + private boolean invalidPayload(AttendanceRecordPayload payload) { + return NumberUtil.isNotPositiveNumber(payload.getPersonId()) + || NumberUtil.isNotPositiveNumber(payload.getOuId()) + || NumberUtil.isNotPositiveNumber(payload.getWorkspaceId()) + || Objects.isNull(payload.getUserType()) + || Objects.isNull(payload.getClockType()) + || Objects.isNull(payload.getClockAt()) + // 非从业人员,需要通过teamId去获取其真实的单位id + || (payload.isNotPractitioner() && NumberUtil.isNotPositiveNumber(payload.getTeamId())); + } + + private boolean unexpectedPayload(AttendanceRecordPayload payload) { + return EXPECTED_CLOCK_TYPES.stream().noneMatch(e -> e.equals(payload.getClockType())); + } + + private void handle(AttendanceRecordPayload payload, String msgId) { + // FIXME: cold_blade 暂时废弃通过接MQ来实现标签取消的方案,改用XXL-JOB方式实现 + // 缘由:考勤MQ的消息集中在上下班的高峰,且有暂离标签的用户是少数,因此大多数的MQ消息都是空转 + } + + @Override + public void afterPropertiesSet() throws Exception { + eventConsumer.registerHandler(AttendanceEventEnum.ATTENDANCE_RECORD.getEventCode(), this, HANDLER_ALIAS); + } + + private static void info(String msgFormat, Object... args) { + msgFormat = LOG_PREFIX + msgFormat; + log.info(msgFormat, args); + } + + private static void error(String msgFormat, Object... args) { + msgFormat = LOG_PREFIX + msgFormat; + // FIXME: cold_blade 基于MQ方案暂时不考虑的前提,此处就不用告警出来了 + log.warn(msgFormat, args); + } +} diff --git a/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/event/outer/payload/AttendanceRecordPayload.java b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/event/outer/payload/AttendanceRecordPayload.java new file mode 100644 index 0000000..755470b --- /dev/null +++ b/orgmanax-server/src/main/java/cn/axzo/orgmanax/server/orguser/event/outer/payload/AttendanceRecordPayload.java @@ -0,0 +1,206 @@ +package cn.axzo.orgmanax.server.orguser.event.outer.payload; + +import com.alibaba.fastjson.JSON; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.Objects; +import java.util.Set; + +/** + * @author luofu + * @version 1.0 + * @description 新增考勤记录事件数据 + * @date 2025/3/5 + */ +@Data +public class AttendanceRecordPayload implements Serializable { + + public static final int CLOCK_TYPE_ENTRY = 1; + public static final int CLOCK_TYPE_EXIT = 2; + + private static final int USER_TYPE_PRACTITIONER = 2; + + /** + * 考勤记录主键 + */ + private Long id; + /** + * 分包机构Id + */ + private Long ouId; + + /** + * 分包机构名称 + */ + private String ouName; + /** + * 项目部id + */ + private Long workspaceId; + /** + * 项目部名称 + */ + private String workspaceName; + /** + * 考勤归属项目json数组,可以为null + */ + private Set projectIds; + /** + * 项目内班组id + */ + private Long teamId; + /** + * 小组Id + */ + private Long groupId; + /** + * 用户类型:用户类型: + * 1工人, + * 2从业人员, + * 3突击工 + * 4班组长 + */ + private Integer userType; + /** + * 用户identityId + */ + private Long userId; + /** + * 用户名称 + */ + private String userName; + /** + * 用户手机号 + */ + private String userPhone; + /** + * 用户身份证号 + */ + private String userIdCard; + /** + * 性别:0-未知 1-男 2-女 + */ + private Integer userSex; + /** + * 班组名称 + */ + private String teamName; + /** + * 工种id + */ + private String professionId; + /** + * 工种名称 + */ + private String professionName; + /** + * 岗位名称 + */ + private String jobName; + /** + * 岗位标签码 + */ + private String jobCode; + /** + * personId + */ + private Long personId; + /** + * 本次打卡归属日期(为满足跨夜考勤) + */ + private Date dataDate; + /** + * 打卡时间 + */ + private Date clockAt; + /** + * 设备名称 + */ + private String deviceSn; + + /** + * 设备序列号 + */ + private String deviceName; + /** + * 打卡类型:1进场,2出场,3抓拍 + */ + private Integer clockType; + /** + * 打卡方式:0-闸机 1-电子围栏 2-考勤补卡 + */ + private Integer clockMethod; + /** + * 考勤时的抓拍图片 可能为空未获取到 + */ + private String capturePhoto; + + /** + * 考勤时的抓拍图片key 可能为空未获取到 + */ + private String capturePhotoKey; + /** + * 创建时间 + */ + private Date createAt; + /** + * 修改时间 + */ + private Date updateAt; + /** + * 打卡地址 + */ + private String address; + /** + * 打卡经度 + */ + private BigDecimal longitude; + /** + * 打卡纬度 + */ + private BigDecimal latitude; + + /** + * 企业id + */ + private Long entId; + + /** + * 企业名称 + */ + private String entName; + + /** + * 小组名称 + */ + private String groupName; + + /** + * 小组类型 0-直属小组 1-合作小组 + */ + private Integer groupType; + + /** + * 0:内部部门 1:总包部门 2:建设单位 3:监理单位 4:劳务分包 5:专业分包 + */ + private Integer ouType; + + /** + * 设备位置信息 + */ + private String deviceAddress; + + private String dataSource; + + public boolean isNotPractitioner() { + return !Objects.equals(USER_TYPE_PRACTITIONER, userType); + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } +}