feat(REQ-3714) 工人退场流程优化 - 考勤记录事件MQ处理

This commit is contained in:
luofu 2025-03-05 13:53:19 +08:00
parent 83658d7edf
commit 28f6b7736c
4 changed files with 350 additions and 1 deletions

View File

@ -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()

View File

@ -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;
}
}

View File

@ -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<Integer> 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);
}
}

View File

@ -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<Long> 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);
}
}