Compare commits
6 Commits
c88dc3fc10
...
c75696c644
| Author | SHA1 | Date | |
|---|---|---|---|
| c75696c644 | |||
| c4ef31bb98 | |||
| 457fc556e2 | |||
| 5063fc8dd1 | |||
| a49994cc57 | |||
| a82b59ac46 |
@@ -196,37 +196,6 @@ CREATE TABLE `ops_cleaner_status` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保洁员实时状态';
|
||||
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ops_order_clean_log
|
||||
-- 保洁业务日志表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ops_order_clean_log`;
|
||||
CREATE TABLE `ops_order_clean_log` (
|
||||
`id` bigint NOT NULL COMMENT '日志ID',
|
||||
`event_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '事件发生时间',
|
||||
`event_level` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'INFO' COMMENT '日志级别(INFO=信息/WARN=警告/ERROR=错误)',
|
||||
`event_domain` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '领域(RULE=规则引擎/DISPATCH=调度/BADGE=工牌/BEACON=信标/SYSTEM=系统)',
|
||||
`event_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '事件类型',
|
||||
`ops_order_id` bigint COMMENT '关联工单ID',
|
||||
`area_id` bigint COMMENT '区域ID',
|
||||
`cleaner_id` bigint COMMENT '保洁员ID',
|
||||
`device_id` bigint COMMENT '设备ID(工牌/信标)',
|
||||
`event_message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '可读日志内容',
|
||||
`event_payload` json COMMENT '结构化上下文',
|
||||
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '创建者',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '更新者',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_event_time` (`event_time`),
|
||||
KEY `idx_ops_order_id` (`ops_order_id`),
|
||||
KEY `idx_cleaner_id` (`cleaner_id`),
|
||||
KEY `idx_event_domain_type` (`event_domain`, `event_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保洁业务日志';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ops_bus_area
|
||||
-- 业务区域表(园区/楼栋/楼层/功能区域)
|
||||
@@ -292,4 +261,58 @@ CREATE TABLE `ops_cleaner_performance_monthly` (
|
||||
KEY `idx_year_month` (`year_month`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保洁员绩效月度汇总';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ops_business_event_log
|
||||
-- 通用业务事件日志表(支持多模块日志记录)
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ops_business_event_log`;
|
||||
CREATE TABLE `ops_business_event_log` (
|
||||
`id` bigint NOT NULL COMMENT '主键ID',
|
||||
|
||||
-- 时间与级别
|
||||
`event_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '事件时间',
|
||||
`event_level` varchar(16) NOT NULL DEFAULT 'INFO' COMMENT '级别:INFO/WARN/ERROR',
|
||||
|
||||
-- 业务分类
|
||||
`module` varchar(32) NOT NULL COMMENT '模块:clean/repair/patrol',
|
||||
`event_domain` varchar(32) NOT NULL COMMENT '领域:DISPATCH/BEACON/TRAFFIC/DEVICE/SYSTEM/AUDIT',
|
||||
`event_type` varchar(64) NOT NULL COMMENT '事件类型',
|
||||
|
||||
-- 设备关联(方案A:冗余存储名称)
|
||||
`device_id` bigint DEFAULT NULL COMMENT '设备ID',
|
||||
`device_name` varchar(128) DEFAULT NULL COMMENT '设备名称(冗余)',
|
||||
`device_code` varchar(64) DEFAULT NULL COMMENT '设备编码(冗余)',
|
||||
`device_type` varchar(32) DEFAULT NULL COMMENT '设备类型(冗余)',
|
||||
|
||||
-- 人员关联(方案A:冗余存储名称)
|
||||
`person_id` bigint DEFAULT NULL COMMENT '人员ID',
|
||||
`person_name` varchar(64) DEFAULT NULL COMMENT '人员姓名(冗余)',
|
||||
`person_type` varchar(32) DEFAULT NULL COMMENT '人员类型(冗余)',
|
||||
|
||||
-- 业务实体关联
|
||||
`target_id` bigint DEFAULT NULL COMMENT '业务实体ID(工单ID/区域ID)',
|
||||
`target_type` varchar(32) DEFAULT NULL COMMENT '业务类型:order/area/task',
|
||||
|
||||
-- 日志内容
|
||||
`event_message` varchar(500) NOT NULL COMMENT '事件描述',
|
||||
`event_summary` varchar(200) DEFAULT NULL COMMENT '简要摘要',
|
||||
|
||||
-- 扩展数据
|
||||
`event_payload` json DEFAULT NULL COMMENT '扩展数据',
|
||||
|
||||
-- 标准字段
|
||||
`creator` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_event_time` (`event_time`),
|
||||
KEY `idx_module_domain_type` (`module`, `event_domain`, `event_type`),
|
||||
KEY `idx_device_person` (`device_id`, `person_id`),
|
||||
KEY `idx_target` (`target_type`, `target_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通用业务事件日志';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.viewsh.module.ops.environment.dal.dataobject.log;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 保洁业务日志 DO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@TableName(value = "ops_order_clean_log", autoResultMap = true)
|
||||
@KeySequence("ops_order_clean_log_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OpsOrderCleanLogDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 日志ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 事件发生时间
|
||||
*/
|
||||
private LocalDateTime eventTime;
|
||||
/**
|
||||
* 日志级别(INFO=信息/WARN=警告/ERROR=错误)
|
||||
*/
|
||||
private String eventLevel;
|
||||
/**
|
||||
* 领域(RULE=规则引擎/DISPATCH=调度/BADGE=工牌/BEACON=信标/SYSTEM=系统)
|
||||
*
|
||||
* 枚举 {@link com.viewsh.module.ops.enums.EventDomainEnum}
|
||||
*/
|
||||
private String eventDomain;
|
||||
/**
|
||||
* 事件类型
|
||||
*/
|
||||
private String eventType;
|
||||
/**
|
||||
* 关联工单ID
|
||||
*
|
||||
* 关联 {@link com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO#getId()}
|
||||
*/
|
||||
private Long opsOrderId;
|
||||
/**
|
||||
* 区域ID
|
||||
*
|
||||
* 关联 {@link com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO#getId()}
|
||||
*/
|
||||
private Long areaId;
|
||||
/**
|
||||
* 保洁员ID
|
||||
*/
|
||||
private Long cleanerId;
|
||||
/**
|
||||
* 设备ID(工牌/信标)
|
||||
*/
|
||||
private Long deviceId;
|
||||
/**
|
||||
* 可读日志内容
|
||||
*/
|
||||
private String eventMessage;
|
||||
/**
|
||||
* 结构化上下文
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> eventPayload;
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.viewsh.module.ops.environment.dal.mysql.log;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.log.OpsOrderCleanLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 保洁业务日志 Mapper
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Mapper
|
||||
public interface OpsOrderCleanLogMapper extends BaseMapperX<OpsOrderCleanLogDO> {
|
||||
|
||||
/**
|
||||
* 根据工单ID查询日志
|
||||
*/
|
||||
default List<OpsOrderCleanLogDO> selectListByOpsOrderId(Long opsOrderId) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsOrderCleanLogDO>()
|
||||
.eq(OpsOrderCleanLogDO::getOpsOrderId, opsOrderId)
|
||||
.orderByDesc(OpsOrderCleanLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据保洁员查询日志
|
||||
*/
|
||||
default List<OpsOrderCleanLogDO> selectListByCleanerId(Long cleanerId) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsOrderCleanLogDO>()
|
||||
.eq(OpsOrderCleanLogDO::getCleanerId, cleanerId)
|
||||
.orderByDesc(OpsOrderCleanLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据事件领域和类型查询日志
|
||||
*/
|
||||
default List<OpsOrderCleanLogDO> selectListByDomainAndType(String eventDomain, String eventType) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsOrderCleanLogDO>()
|
||||
.eq(OpsOrderCleanLogDO::getEventDomain, eventDomain)
|
||||
.eq(OpsOrderCleanLogDO::getEventType, eventType)
|
||||
.orderByDesc(OpsOrderCleanLogDO::getEventTime));
|
||||
}
|
||||
|
||||
// 注意:分页查询方法需要在Service层实现,这里只提供基础查询方法
|
||||
// 具体分页查询请参考Service实现
|
||||
|
||||
}
|
||||
@@ -2,18 +2,15 @@ package com.viewsh.module.ops.environment.integration.consumer;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.viewsh.module.ops.environment.integration.dto.CleanOrderAuditEventDTO;
|
||||
import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
|
||||
import com.viewsh.module.ops.infrastructure.log.publisher.BusinessLogPublisher;
|
||||
import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
|
||||
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.spring.annotation.ConsumeMode;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
@@ -21,14 +18,14 @@ import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 保洁工单审计事件消费者
|
||||
* <p>
|
||||
* 订阅 IoT 模块<EFBFBD><EFBFBD>布的保洁工单审计事件
|
||||
* 订阅 IoT 模块发布的保洁工单审计事件
|
||||
* 用于记录非状态变更的业务审计日志(如警告发送、抑制操作等)
|
||||
* <p>
|
||||
* RocketMQ 配置:
|
||||
@@ -76,7 +73,7 @@ public class CleanOrderAuditEventHandler implements RocketMQListener<String> {
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Resource
|
||||
private BusinessLogPublisher businessLogPublisher;
|
||||
private EventLogRecorder eventLogRecorder;
|
||||
|
||||
@Resource
|
||||
private VoiceBroadcastService voiceBroadcastService;
|
||||
@@ -121,49 +118,22 @@ public class CleanOrderAuditEventHandler implements RocketMQListener<String> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 确定日志级别和类型
|
||||
LogType logType = determineLogType(event.getAuditType());
|
||||
boolean isSuccess = determineSuccess(event.getAuditType());
|
||||
// 1. 确定日志级别和域
|
||||
EventDomain domain = determineDomain(event.getAuditType());
|
||||
EventLevel level = determineLevel(event.getAuditType());
|
||||
String eventType = event.getAuditType() != null ? event.getAuditType() : "AUDIT";
|
||||
|
||||
// 2. 构建业务日志上下文
|
||||
BusinessLogContext logContext = BusinessLogContext.builder()
|
||||
.type(logType)
|
||||
.scope(LogScope.ORDER)
|
||||
.description(event.getMessage())
|
||||
.targetId(event.getOrderId())
|
||||
.targetType("order")
|
||||
.operatorType("SYSTEM")
|
||||
.success(isSuccess)
|
||||
.build();
|
||||
|
||||
// 3. 添加扩展信息
|
||||
if (event.getDeviceId() != null) {
|
||||
logContext.putExtra("deviceId", event.getDeviceId());
|
||||
}
|
||||
if (event.getDeviceKey() != null) {
|
||||
logContext.putExtra("deviceKey", event.getDeviceKey());
|
||||
}
|
||||
if (event.getAreaId() != null) {
|
||||
logContext.putExtra("areaId", event.getAreaId());
|
||||
}
|
||||
if (event.getAuditType() != null) {
|
||||
logContext.putExtra("auditType", event.getAuditType());
|
||||
}
|
||||
if (event.getData() != null) {
|
||||
event.getData().forEach(logContext::putExtra);
|
||||
}
|
||||
|
||||
// 4. 发布业务日志
|
||||
if (isSuccess) {
|
||||
businessLogPublisher.publishSuccess(logContext);
|
||||
} else {
|
||||
businessLogPublisher.publishFailure(logContext, event.getMessage());
|
||||
}
|
||||
// 2. 记录审计日志
|
||||
eventLogRecorder.info("clean", domain, eventType,
|
||||
event.getMessage(),
|
||||
event.getOrderId(),
|
||||
event.getDeviceId(),
|
||||
null);
|
||||
|
||||
log.debug("[CleanOrderAuditEventHandler] 审计日志已记录: eventId={}, auditType={}",
|
||||
event.getEventId(), event.getAuditType());
|
||||
|
||||
// 5. 如果是 TTS 请求,调用 IoT 模块下发语音
|
||||
// 3. 如果是 TTS 请求,调用 IoT 模块下发语音
|
||||
if ("TTS_REQUEST".equals(event.getAuditType()) && event.getDeviceId() != null) {
|
||||
handleTtsRequest(event);
|
||||
}
|
||||
@@ -238,22 +208,30 @@ public class CleanOrderAuditEventHandler implements RocketMQListener<String> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定日志类型
|
||||
* 确定事件域
|
||||
*/
|
||||
private LogType determineLogType(String auditType) {
|
||||
private EventDomain determineDomain(String auditType) {
|
||||
if (auditType == null) {
|
||||
return EventDomain.SYSTEM;
|
||||
}
|
||||
if (auditType.startsWith("BEACON_") || auditType.contains("BEACON")) {
|
||||
return LogType.DEVICE;
|
||||
return EventDomain.BEACON;
|
||||
} else if (auditType.equals("TTS_REQUEST")) {
|
||||
return LogType.NOTIFICATION;
|
||||
return EventDomain.DEVICE;
|
||||
} else {
|
||||
return LogType.SYSTEM;
|
||||
return EventDomain.AUDIT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定是否成功
|
||||
* 确定日志级别
|
||||
*/
|
||||
private boolean determineSuccess(String auditType) {
|
||||
return !auditType.endsWith("_WARNING") && !auditType.endsWith("_SUPPRESSED") && !auditType.endsWith("_REJECTED");
|
||||
private EventLevel determineLevel(String auditType) {
|
||||
if (auditType != null && (auditType.endsWith("_WARNING") ||
|
||||
auditType.endsWith("_SUPPRESSED") ||
|
||||
auditType.endsWith("_REJECTED"))) {
|
||||
return EventLevel.WARN;
|
||||
}
|
||||
return EventLevel.INFO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,8 @@ import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import com.viewsh.module.ops.enums.OperatorTypeEnum;
|
||||
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
|
||||
import com.viewsh.module.ops.environment.integration.dto.CleanOrderConfirmEventDTO;
|
||||
import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
|
||||
import com.viewsh.module.ops.infrastructure.log.publisher.BusinessLogPublisher;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.spring.annotation.ConsumeMode;
|
||||
@@ -20,8 +19,6 @@ import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -60,7 +57,7 @@ public class CleanOrderConfirmEventHandler implements RocketMQListener<String> {
|
||||
private OrderLifecycleManager orderLifecycleManager;
|
||||
|
||||
@Resource
|
||||
private BusinessLogPublisher businessLogPublisher;
|
||||
private EventLogRecorder eventLogRecorder;
|
||||
|
||||
@Resource
|
||||
private VoiceBroadcastService voiceBroadcastService;
|
||||
@@ -120,12 +117,8 @@ public class CleanOrderConfirmEventHandler implements RocketMQListener<String> {
|
||||
orderLifecycleManager.transition(request);
|
||||
|
||||
// 6. 记录业务日志
|
||||
BusinessLogContext logContext = BusinessLogContext.forOrder(
|
||||
LogType.TRANSITION,
|
||||
"工单已确认 (工牌按键)",
|
||||
orderId
|
||||
);
|
||||
businessLogPublisher.publishSuccess(logContext);
|
||||
eventLogRecorder.info("clean", EventDomain.AUDIT, "ORDER_CONFIRM",
|
||||
"工单已确认 (工牌按键)", orderId, event.getDeviceId(), order.getAssigneeId());
|
||||
|
||||
// 7. 发送 TTS 通知
|
||||
// "工单已确认,请前往{AreaName}开始作业"
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.viewsh.module.ops.environment.service.voice;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
@@ -15,6 +17,7 @@ import org.springframework.stereotype.Service;
|
||||
* 1. 统一所有 TTS 下发入口
|
||||
* 2. 提供同步/异步播报接口
|
||||
* 3. 管理播报音量参数
|
||||
* 4. 记录播报日志
|
||||
* <p>
|
||||
* 设计原则:
|
||||
* - 接受 deviceId 参数(而非 cleanerId)
|
||||
@@ -30,6 +33,9 @@ public class VoiceBroadcastService {
|
||||
@Resource
|
||||
private IotDeviceControlApi iotDeviceControlApi;
|
||||
|
||||
@Resource
|
||||
private EventLogRecorder eventLogRecorder;
|
||||
|
||||
/**
|
||||
* 播报语音(同步)
|
||||
*
|
||||
@@ -64,8 +70,17 @@ public class VoiceBroadcastService {
|
||||
|
||||
iotDeviceControlApi.invokeService(reqDTO);
|
||||
log.debug("[VoiceBroadcast] 播报成功: deviceId={}, text={}", deviceId, text);
|
||||
|
||||
// 记录日志
|
||||
eventLogRecorder.info("clean", EventDomain.DEVICE, "TTS_SENT",
|
||||
"语音播报: " + text, deviceId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[VoiceBroadcast] 播报失败: deviceId={}, text={}", deviceId, text, e);
|
||||
|
||||
// 记录错误日志
|
||||
eventLogRecorder.error("clean", EventDomain.DEVICE, "TTS_FAILED",
|
||||
"语音播报失败: " + e.getMessage(), deviceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.viewsh.module.ops.dal.dataobject.log;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通用业务事件日志 DO
|
||||
* <p>
|
||||
* 用于记录所有模块的业务事件日志,支持多模块(clean/repair/patrol等)
|
||||
* 采用方案A:在记录时<E5BD95><E697B6>步填充设备/人员名称冗余字段,便于展示时直接查询
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@TableName(value = "ops_business_event_log", autoResultMap = true)
|
||||
@KeySequence("ops_business_event_log_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OpsBusinessEventLogDO extends BaseDO {
|
||||
|
||||
// ==================== 主键 ====================
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
// ==================== 时间与级别 ====================
|
||||
|
||||
/**
|
||||
* 事件时间
|
||||
*/
|
||||
private LocalDateTime eventTime;
|
||||
|
||||
/**
|
||||
* 日志级别(INFO=信息/WARN=警告/ERROR=错误)
|
||||
*/
|
||||
private String eventLevel;
|
||||
|
||||
// ==================== 业务分类 ====================
|
||||
|
||||
/**
|
||||
* 模块标识(clean=保洁/repair=维修/patrol=巡检)
|
||||
*/
|
||||
private String module;
|
||||
|
||||
/**
|
||||
* 事件域(DISPATCH=调度/BEACON=信标/TRAFFIC=客流/DEVICE=设备/SYSTEM=系统/AUDIT=审计)
|
||||
*/
|
||||
private String eventDomain;
|
||||
|
||||
/**
|
||||
* 事件类型(如:ARRIVE_CONFIRMED/TTS_SENT/AUTO_DISPATCH)
|
||||
*/
|
||||
private String eventType;
|
||||
|
||||
// ==================== 设备关联 ====================
|
||||
|
||||
/**
|
||||
* 设备ID(关联 iot_device.id)
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* 设备名称(冗余字段,异步填充)
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 设备编码(冗余字段,如设备序列号)
|
||||
*/
|
||||
private String deviceCode;
|
||||
|
||||
/**
|
||||
* 设备类型(冗余字段,如:BADGE/TRAFFIC_COUNTER/BEACON)
|
||||
*/
|
||||
private String deviceType;
|
||||
|
||||
// ==================== 人员关联 ====================
|
||||
|
||||
/**
|
||||
* 人员ID(保洁员/巡检员等)
|
||||
*/
|
||||
private Long personId;
|
||||
|
||||
/**
|
||||
* 人员姓名(冗余字段,异步填充)
|
||||
*/
|
||||
private String personName;
|
||||
|
||||
/**
|
||||
* 人员类型(冗余字段,如:CLEANER/INSPECTOR)
|
||||
*/
|
||||
private String personType;
|
||||
|
||||
// ==================== 业务实体关联 ====================
|
||||
|
||||
/**
|
||||
* 业务实体ID(如工单ID、区域ID)
|
||||
*/
|
||||
private Long targetId;
|
||||
|
||||
/**
|
||||
* 业务实体类型(order=工单/area=区域/task=任务)
|
||||
*/
|
||||
private String targetType;
|
||||
|
||||
// ==================== 日志内容 ====================
|
||||
|
||||
/**
|
||||
* 事件描述(完整描述)
|
||||
*/
|
||||
private String eventMessage;
|
||||
|
||||
/**
|
||||
* 简要摘要(用于列表展示,可选)
|
||||
*/
|
||||
private String eventSummary;
|
||||
|
||||
/**
|
||||
* 扩展数据(JSON格式,存储额外结构化信息)
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> eventPayload;
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.viewsh.module.ops.dal.dataobject.log;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 保洁业务日志 DO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@TableName(value = "ops_order_clean_log", autoResultMap = true)
|
||||
@KeySequence("ops_order_clean_log_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OpsOrderCleanLogDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 日志ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 事件发生时间
|
||||
*/
|
||||
private LocalDateTime eventTime;
|
||||
/**
|
||||
* 日志级别(INFO=信息/WARN=警告/ERROR=错误)
|
||||
*/
|
||||
private String eventLevel;
|
||||
/**
|
||||
* 领域(RULE=规则引擎/DISPATCH=调度/BADGE=工牌/BEACON=信标/SYSTEM=系统)
|
||||
*
|
||||
* 枚举 {@link com.viewsh.module.ops.enums.EventDomainEnum}
|
||||
*/
|
||||
private String eventDomain;
|
||||
/**
|
||||
* 事件类型
|
||||
*/
|
||||
private String eventType;
|
||||
/**
|
||||
* 关联工单ID
|
||||
*
|
||||
* 关联 {@link com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO#getId()}
|
||||
*/
|
||||
private Long opsOrderId;
|
||||
/**
|
||||
* 区域ID
|
||||
*
|
||||
* 关联 {@link com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO#getId()}
|
||||
*/
|
||||
private Long areaId;
|
||||
/**
|
||||
* 保洁员ID
|
||||
*/
|
||||
private Long cleanerId;
|
||||
/**
|
||||
* 设备ID(工牌/信标)
|
||||
*/
|
||||
private Long deviceId;
|
||||
/**
|
||||
* 可读日志内容
|
||||
*/
|
||||
private String eventMessage;
|
||||
/**
|
||||
* 结构化上下文
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> eventPayload;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.viewsh.module.ops.dal.mysql.log;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.dal.dataobject.log.OpsBusinessEventLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通用业务事件日志 Mapper
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Mapper
|
||||
public interface OpsBusinessEventLogMapper extends BaseMapperX<OpsBusinessEventLogDO> {
|
||||
|
||||
/**
|
||||
* 根据模块查询日志
|
||||
*
|
||||
* @param module 模块标识(clean/repair/patrol)
|
||||
* @return 日志列表
|
||||
*/
|
||||
default List<OpsBusinessEventLogDO> selectListByModule(String module) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsBusinessEventLogDO>()
|
||||
.eq(OpsBusinessEventLogDO::getModule, module)
|
||||
.orderByDesc(OpsBusinessEventLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模块和事件域查询日志
|
||||
*
|
||||
* @param module 模块标识
|
||||
* @param eventDomain 事件域
|
||||
* @return 日志列表
|
||||
*/
|
||||
default List<OpsBusinessEventLogDO> selectListByModuleAndDomain(String module, String eventDomain) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsBusinessEventLogDO>()
|
||||
.eq(OpsBusinessEventLogDO::getModule, module)
|
||||
.eq(OpsBusinessEventLogDO::getEventDomain, eventDomain)
|
||||
.orderByDesc(OpsBusinessEventLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备ID查询日志
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 日志列表
|
||||
*/
|
||||
default List<OpsBusinessEventLogDO> selectListByDeviceId(Long deviceId) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsBusinessEventLogDO>()
|
||||
.eq(OpsBusinessEventLogDO::getDeviceId, deviceId)
|
||||
.orderByDesc(OpsBusinessEventLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据人员ID查询日志
|
||||
*
|
||||
* @param personId 人员ID
|
||||
* @return 日志列表
|
||||
*/
|
||||
default List<OpsBusinessEventLogDO> selectListByPersonId(Long personId) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsBusinessEventLogDO>()
|
||||
.eq(OpsBusinessEventLogDO::getPersonId, personId)
|
||||
.orderByDesc(OpsBusinessEventLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据目标实体查询日志
|
||||
*
|
||||
* @param targetType 目标类型(order/area/task)
|
||||
* @param targetId 目标ID
|
||||
* @return 日志列表
|
||||
*/
|
||||
default List<OpsBusinessEventLogDO> selectListByTarget(String targetType, Long targetId) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsBusinessEventLogDO>()
|
||||
.eq(OpsBusinessEventLogDO::getTargetType, targetType)
|
||||
.eq(OpsBusinessEventLogDO::getTargetId, targetId)
|
||||
.orderByDesc(OpsBusinessEventLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据工单ID查询日志
|
||||
*
|
||||
* @param orderId 工单ID
|
||||
* @return 日志列表
|
||||
*/
|
||||
default List<OpsBusinessEventLogDO> selectListByOrderId(Long orderId) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsBusinessEventLogDO>()
|
||||
.eq(OpsBusinessEventLogDO::getTargetType, "order")
|
||||
.eq(OpsBusinessEventLogDO::getTargetId, orderId)
|
||||
.orderByDesc(OpsBusinessEventLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据事件域和类型查询日志
|
||||
*
|
||||
* @param eventDomain 事件域
|
||||
* @param eventType 事件类型
|
||||
* @return 日志列表
|
||||
*/
|
||||
default List<OpsBusinessEventLogDO> selectListByDomainAndType(String eventDomain, String eventType) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsBusinessEventLogDO>()
|
||||
.eq(OpsBusinessEventLogDO::getEventDomain, eventDomain)
|
||||
.eq(OpsBusinessEventLogDO::getEventType, eventType)
|
||||
.orderByDesc(OpsBusinessEventLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模块、事件域和类型查询日志
|
||||
*
|
||||
* @param module 模块标识
|
||||
* @param eventDomain 事件域
|
||||
* @param eventType 事件类型
|
||||
* @return 日志列表
|
||||
*/
|
||||
default List<OpsBusinessEventLogDO> selectListByModuleDomainAndType(String module, String eventDomain, String eventType) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsBusinessEventLogDO>()
|
||||
.eq(OpsBusinessEventLogDO::getModule, module)
|
||||
.eq(OpsBusinessEventLogDO::getEventDomain, eventDomain)
|
||||
.eq(OpsBusinessEventLogDO::getEventType, eventType)
|
||||
.orderByDesc(OpsBusinessEventLogDO::getEventTime));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.viewsh.module.ops.dal.mysql.log;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.dal.dataobject.log.OpsOrderCleanLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 保洁业务日志 Mapper
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Mapper
|
||||
public interface OpsOrderCleanLogMapper extends BaseMapperX<OpsOrderCleanLogDO> {
|
||||
|
||||
/**
|
||||
* 根据工单ID查询日志
|
||||
*/
|
||||
default List<OpsOrderCleanLogDO> selectListByOpsOrderId(Long opsOrderId) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsOrderCleanLogDO>()
|
||||
.eq(OpsOrderCleanLogDO::getOpsOrderId, opsOrderId)
|
||||
.orderByDesc(OpsOrderCleanLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据保洁员查询日志
|
||||
*/
|
||||
default List<OpsOrderCleanLogDO> selectListByCleanerId(Long cleanerId) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsOrderCleanLogDO>()
|
||||
.eq(OpsOrderCleanLogDO::getCleanerId, cleanerId)
|
||||
.orderByDesc(OpsOrderCleanLogDO::getEventTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据事件领域和类型查询日志
|
||||
*/
|
||||
default List<OpsOrderCleanLogDO> selectListByDomainAndType(String eventDomain, String eventType) {
|
||||
return selectList(new LambdaQueryWrapperX<OpsOrderCleanLogDO>()
|
||||
.eq(OpsOrderCleanLogDO::getEventDomain, eventDomain)
|
||||
.eq(OpsOrderCleanLogDO::getEventType, eventType)
|
||||
.orderByDesc(OpsOrderCleanLogDO::getEventTime));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.annotation;
|
||||
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
|
||||
|
||||
@@ -13,11 +15,20 @@ import java.lang.annotation.*;
|
||||
* 使用示例:
|
||||
* <pre>
|
||||
* {@code
|
||||
* // 基础用法(使用 LogType/LogScope)
|
||||
* @BusinessLog(type = LogType.DISPATCH, scope = LogScope.ORDER,
|
||||
* description = "自动派单", includeParams = true, includeResult = true)
|
||||
* description = "自动派单", includeParams = true)
|
||||
* public DispatchResult dispatch(OrderDispatchContext context) {
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
* // 新用法(直接指定 EventDomain 和 Module)
|
||||
* @BusinessLog(module = "clean", domain = EventDomain.DEVICE,
|
||||
* eventType = "TTS_SENT", description = "语音播报",
|
||||
* deviceId = "#deviceId", personId = "#context.cleanerId")
|
||||
* public void broadcast(String text, Long deviceId) {
|
||||
* // ...
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
@@ -28,15 +39,19 @@ import java.lang.annotation.*;
|
||||
@Documented
|
||||
public @interface BusinessLog {
|
||||
|
||||
/**
|
||||
* 日志类型
|
||||
*/
|
||||
LogType type();
|
||||
// ==================== 旧版字段(保持兼容) ====================
|
||||
|
||||
/**
|
||||
* 日志作用域
|
||||
* 日志类型(旧版,保持兼容)
|
||||
* <p>
|
||||
* 当指定了 domain 和 eventType 时,此字段可忽略
|
||||
*/
|
||||
LogScope scope();
|
||||
LogType type() default LogType.SYSTEM;
|
||||
|
||||
/**
|
||||
* 日志作用域(旧版,保持兼容)
|
||||
*/
|
||||
LogScope scope() default LogScope.SYSTEM;
|
||||
|
||||
/**
|
||||
* 操作描述
|
||||
@@ -84,4 +99,70 @@ public @interface BusinessLog {
|
||||
* 自定义标签
|
||||
*/
|
||||
String[] tags() default {};
|
||||
|
||||
// ==================== 新版字段(支持 EventLogRecorder) ====================
|
||||
|
||||
/**
|
||||
* 模块标识(新版)
|
||||
* <p>
|
||||
* 如:clean, repair, patrol
|
||||
* <p>
|
||||
* 当指定此字段时,将使用 EventLogRecorder 记录日志
|
||||
*/
|
||||
String module() default "";
|
||||
|
||||
/**
|
||||
* 事件域(新版)
|
||||
* <p>
|
||||
* 当指定此字段时,将使用 EventLogRecorder 记录日志
|
||||
*/
|
||||
EventDomain domain() default EventDomain.SYSTEM;
|
||||
|
||||
/**
|
||||
* 事件类型(新版)
|
||||
* <p>
|
||||
* 当指定此字段时,将使用 EventLogRecorder 记录日志
|
||||
*/
|
||||
String eventType() default "";
|
||||
|
||||
/**
|
||||
* 日志级别(新版)
|
||||
*/
|
||||
EventLevel level() default EventLevel.INFO;
|
||||
|
||||
/**
|
||||
* 设备ID提取表达式(SpEL)
|
||||
* <p>
|
||||
* 示例:"#deviceId", "#context.deviceId", "#request.deviceId"
|
||||
*/
|
||||
String deviceId() default "";
|
||||
|
||||
/**
|
||||
* 人员ID提取表达式(SpEL)
|
||||
* <p>
|
||||
* 示例:"#cleanerId", "#context.userId", "#request.assigneeId"
|
||||
*/
|
||||
String personId() default "";
|
||||
|
||||
/**
|
||||
* 业务实体ID提取表达式(SpEL)
|
||||
* <p>
|
||||
* 示例:"#orderId", "#context.orderId"
|
||||
*/
|
||||
String targetId() default "";
|
||||
|
||||
/**
|
||||
* 业务实体类型
|
||||
* <p>
|
||||
* 如:order, area, task
|
||||
*/
|
||||
String targetType() default "";
|
||||
|
||||
/**
|
||||
* 简要摘要模板(用于列表展示)
|
||||
* <p>
|
||||
* 示例:"播报: {text}", "到岗: {areaName}"
|
||||
*/
|
||||
String summary() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.aspect;
|
||||
|
||||
import com.viewsh.module.ops.infrastructure.log.annotation.BusinessLog;
|
||||
import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
|
||||
import com.viewsh.module.ops.infrastructure.log.publisher.BusinessLogPublisher;
|
||||
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord;
|
||||
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
@@ -19,8 +21,6 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业务日志切面
|
||||
@@ -28,12 +28,12 @@ import java.util.List;
|
||||
* 职责:
|
||||
* 1. 拦截 @BusinessLog 注解的方法
|
||||
* 2. 提取方法参数和结果
|
||||
* 3. 发布业务日志
|
||||
* 3. 统一通过 EventLogRecorder 记录日志
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* - 使用环绕通知
|
||||
* - 支持 SpEL 表达式提取参数和结果
|
||||
* - 支持异步日志发布
|
||||
* - 所有日志异步记录,不阻塞主业务流程
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@@ -43,69 +43,67 @@ import java.util.List;
|
||||
public class BusinessLogAspect {
|
||||
|
||||
@Resource
|
||||
private BusinessLogPublisher businessLogPublisher;
|
||||
private EventLogRecorder eventLogRecorder;
|
||||
|
||||
private final ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
@Around("@annotation(businessLog)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, BusinessLog businessLog) throws Throwable {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 构建日志上下文
|
||||
BusinessLogContext context = buildContext(joinPoint, businessLog);
|
||||
|
||||
Object result = null;
|
||||
Throwable throwable = null;
|
||||
|
||||
try {
|
||||
// 执行目标方法
|
||||
result = joinPoint.proceed();
|
||||
|
||||
// 提取结果信息
|
||||
if (businessLog.includeResult() || !businessLog.result().isEmpty()) {
|
||||
extractResultInfo(context, result, businessLog);
|
||||
}
|
||||
|
||||
// 发布成功日志
|
||||
if (businessLog.async()) {
|
||||
businessLogPublisher.publishSuccess(context);
|
||||
} else {
|
||||
businessLogPublisher.publishSuccess(context);
|
||||
}
|
||||
// 发布日志
|
||||
publishLog(joinPoint, businessLog, result, null);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
throwable = e;
|
||||
|
||||
// 发布失败日志
|
||||
if (businessLog.async()) {
|
||||
businessLogPublisher.publishError(context, e);
|
||||
} else {
|
||||
businessLogPublisher.publishError(context, e);
|
||||
}
|
||||
|
||||
publishLog(joinPoint, businessLog, null, e);
|
||||
throw e;
|
||||
|
||||
} finally {
|
||||
// 记录执行时长
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
context.putExtra("duration", duration);
|
||||
|
||||
// 记录参数信息
|
||||
if (businessLog.includeParams()) {
|
||||
extractParamsInfo(context, joinPoint, businessLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建日志上下文
|
||||
* 发布日志
|
||||
*/
|
||||
private BusinessLogContext buildContext(ProceedingJoinPoint joinPoint, BusinessLog businessLog) {
|
||||
LogType type = businessLog.type();
|
||||
LogScope scope = businessLog.scope();
|
||||
private void publishLog(ProceedingJoinPoint joinPoint, BusinessLog businessLog, Object result, Throwable throwable) {
|
||||
try {
|
||||
EventLogRecord record = buildEventLogRecord(joinPoint, businessLog);
|
||||
|
||||
// 设置日志级别
|
||||
if (throwable != null) {
|
||||
record.setLevel(EventLevel.ERROR);
|
||||
} else {
|
||||
record.setLevel(businessLog.level());
|
||||
}
|
||||
|
||||
// 提取关联ID和扩展信息
|
||||
EvaluationContext evalContext = createEvaluationContext(joinPoint, result);
|
||||
extractIds(record, businessLog, evalContext);
|
||||
extractExtraInfo(record, businessLog, evalContext, throwable);
|
||||
|
||||
// 处理摘要模板
|
||||
if (!businessLog.summary().isEmpty()) {
|
||||
String summary = processSummaryTemplate(businessLog.summary(), evalContext);
|
||||
record.setSummary(summary);
|
||||
}
|
||||
|
||||
// 异步记录(record 方法已默认异步)
|
||||
eventLogRecorder.record(record);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("[BusinessLogAspect] 记录日志失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 EventLogRecord
|
||||
*/
|
||||
private EventLogRecord buildEventLogRecord(ProceedingJoinPoint joinPoint, BusinessLog businessLog) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
String className = method.getDeclaringClass().getSimpleName();
|
||||
@@ -116,98 +114,170 @@ public class BusinessLogAspect {
|
||||
description = className + "." + methodName;
|
||||
}
|
||||
|
||||
BusinessLogContext context = BusinessLogContext.builder()
|
||||
.type(type)
|
||||
.scope(scope)
|
||||
.description(description)
|
||||
// 判断使用新版还是旧版注解
|
||||
String module = !businessLog.module().isEmpty() ? businessLog.module() : "ops";
|
||||
EventDomain domain = businessLog.domain() != EventDomain.SYSTEM ? businessLog.domain() : mapLogTypeToEventDomain(businessLog.type());
|
||||
String eventType = !businessLog.eventType().isEmpty() ? businessLog.eventType() :
|
||||
(businessLog.type() != null ? businessLog.type().getCode() : "UNKNOWN");
|
||||
|
||||
EventLogRecord record = EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(description)
|
||||
.build();
|
||||
|
||||
// 添加标签
|
||||
for (String tag : businessLog.tags()) {
|
||||
context.putExtra("tag:" + tag, true);
|
||||
// 添加扩展信息
|
||||
if (businessLog.tags().length > 0) {
|
||||
for (String tag : businessLog.tags()) {
|
||||
record.putPayload("tag:" + tag, true);
|
||||
}
|
||||
}
|
||||
record.putPayload("className", className);
|
||||
record.putPayload("methodName", methodName);
|
||||
record.putPayload("scope", businessLog.scope().getCode());
|
||||
|
||||
// 添加方法信息
|
||||
context.putExtra("className", className);
|
||||
context.putExtra("methodName", methodName);
|
||||
|
||||
return context;
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取参数信息
|
||||
* 提取设备、人员、业务实体ID
|
||||
*/
|
||||
private void extractParamsInfo(BusinessLogContext context, ProceedingJoinPoint joinPoint,
|
||||
BusinessLog businessLog) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
String[] parameterNames = signature.getParameterNames();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
if (parameterNames == null || parameterNames.length == 0) {
|
||||
return;
|
||||
private void extractIds(EventLogRecord record, BusinessLog businessLog, EvaluationContext evalContext) {
|
||||
// 提取设备ID
|
||||
if (!businessLog.deviceId().isEmpty()) {
|
||||
extractId(record, businessLog.deviceId(), evalContext, "deviceId", true);
|
||||
}
|
||||
|
||||
// 提取指定参数
|
||||
// 提取人员ID
|
||||
if (!businessLog.personId().isEmpty()) {
|
||||
extractId(record, businessLog.personId(), evalContext, "personId", true);
|
||||
}
|
||||
|
||||
// 提取业务实体ID
|
||||
if (!businessLog.targetId().isEmpty()) {
|
||||
extractId(record, businessLog.targetId(), evalContext, "targetId", true);
|
||||
}
|
||||
|
||||
// 设置业务实体类型
|
||||
if (!businessLog.targetType().isEmpty()) {
|
||||
record.setTargetType(businessLog.targetType());
|
||||
} else if (businessLog.scope() == LogScope.ORDER) {
|
||||
record.setTargetType("order");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取扩展信息
|
||||
*/
|
||||
private void extractExtraInfo(EventLogRecord record, BusinessLog businessLog,
|
||||
EvaluationContext evalContext, Throwable throwable) {
|
||||
// 提取结果信息
|
||||
if (businessLog.includeResult() && !businessLog.result().isEmpty()) {
|
||||
try {
|
||||
Expression expression = parser.parseExpression(businessLog.result());
|
||||
Object value = expression.getValue(evalContext);
|
||||
if (value != null) {
|
||||
String key = extractKey(businessLog.result());
|
||||
record.putPayload(key, value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to extract result: {}", businessLog.result());
|
||||
}
|
||||
}
|
||||
|
||||
// 提取自定义参数
|
||||
if (businessLog.params().length > 0) {
|
||||
EvaluationContext evalContext = createEvaluationContext(joinPoint);
|
||||
for (String param : businessLog.params()) {
|
||||
try {
|
||||
Expression expression = parser.parseExpression(param);
|
||||
Object value = expression.getValue(evalContext);
|
||||
String key = extractKey(param);
|
||||
context.putExtra(key, value);
|
||||
if (value != null) {
|
||||
String key = extractKey(param);
|
||||
record.putPayload(key, value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to extract param: {}", param, e);
|
||||
log.debug("Failed to extract param: {}", param);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 提取所有参数
|
||||
for (int i = 0; i < Math.min(parameterNames.length, args.length); i++) {
|
||||
context.putExtra("param:" + parameterNames[i], args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加错误信息
|
||||
if (throwable != null) {
|
||||
record.putPayload("errorMessage", throwable.getMessage());
|
||||
record.putPayload("errorClass", throwable.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取结果信息
|
||||
* 提取ID值
|
||||
*/
|
||||
private void extractResultInfo(BusinessLogContext context, Object result, BusinessLog businessLog) {
|
||||
if (!businessLog.result().isEmpty()) {
|
||||
// 使用 SpEL 表达式提取
|
||||
EvaluationContext evalContext = new StandardEvaluationContext(result);
|
||||
try {
|
||||
Expression expression = parser.parseExpression(businessLog.result());
|
||||
Object value = expression.getValue(evalContext);
|
||||
String key = extractKey(businessLog.result());
|
||||
context.putExtra(key, value);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to extract result: {}", businessLog.result(), e);
|
||||
}
|
||||
} else {
|
||||
// 存储完整结果
|
||||
context.putExtra("result", result);
|
||||
}
|
||||
|
||||
// 尝试提取 success 字段
|
||||
if (result != null) {
|
||||
try {
|
||||
EvaluationContext evalContext = new StandardEvaluationContext(result);
|
||||
Expression expression = parser.parseExpression("success");
|
||||
Object success = expression.getValue(evalContext);
|
||||
if (success instanceof Boolean) {
|
||||
context.setSuccess((Boolean) success);
|
||||
private void extractId(EventLogRecord record, String expression, EvaluationContext evalContext,
|
||||
String fieldName, boolean setField) {
|
||||
try {
|
||||
Expression expr = parser.parseExpression(expression);
|
||||
Object value = expr.getValue(evalContext);
|
||||
if (value instanceof Long) {
|
||||
if (setField && "deviceId".equals(fieldName)) {
|
||||
record.setDeviceId((Long) value);
|
||||
} else if (setField && "personId".equals(fieldName)) {
|
||||
record.setPersonId((Long) value);
|
||||
} else if (setField && "targetId".equals(fieldName)) {
|
||||
record.setTargetId((Long) value);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 没有 success 字段,默认为成功
|
||||
context.setSuccess(true);
|
||||
} else if (value instanceof Integer) {
|
||||
long longValue = ((Integer) value).longValue();
|
||||
if (setField && "deviceId".equals(fieldName)) {
|
||||
record.setDeviceId(longValue);
|
||||
} else if (setField && "personId".equals(fieldName)) {
|
||||
record.setPersonId(longValue);
|
||||
} else if (setField && "targetId".equals(fieldName)) {
|
||||
record.setTargetId(longValue);
|
||||
}
|
||||
} else if (value != null) {
|
||||
record.putPayload(fieldName, value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to extract {}: {}", fieldName, expression);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理摘要模板
|
||||
* <p>
|
||||
* 支持 {paramName} 占位符替换
|
||||
*/
|
||||
private String processSummaryTemplate(String template, EvaluationContext evalContext) {
|
||||
try {
|
||||
String result = template;
|
||||
int start = template.indexOf('{');
|
||||
while (start >= 0) {
|
||||
int end = template.indexOf('}', start);
|
||||
if (end < 0) break;
|
||||
|
||||
String paramName = template.substring(start + 1, end);
|
||||
try {
|
||||
Expression expression = parser.parseExpression("#" + paramName);
|
||||
Object value = expression.getValue(evalContext);
|
||||
if (value != null) {
|
||||
result = result.replace("{" + paramName + "}", String.valueOf(value));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 保持原样
|
||||
}
|
||||
|
||||
start = template.indexOf('{', end + 1);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 SpEL 评估上下文
|
||||
*/
|
||||
private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint) {
|
||||
private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint, Object result) {
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
@@ -220,15 +290,41 @@ public class BusinessLogAspect {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 result 变量(用于结果提取)
|
||||
if (result != null) {
|
||||
context.setVariable("result", result);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 LogType 映射到 EventDomain
|
||||
*/
|
||||
private EventDomain mapLogTypeToEventDomain(LogType logType) {
|
||||
if (logType == null) {
|
||||
return EventDomain.SYSTEM;
|
||||
}
|
||||
|
||||
switch (logType) {
|
||||
case DISPATCH:
|
||||
return EventDomain.DISPATCH;
|
||||
case DEVICE:
|
||||
return EventDomain.DEVICE;
|
||||
case NOTIFICATION:
|
||||
return EventDomain.DEVICE;
|
||||
case CLEANER:
|
||||
case SYSTEM:
|
||||
case QUEUE:
|
||||
case LIFECYCLE:
|
||||
case TRANSITION:
|
||||
default:
|
||||
return EventDomain.SYSTEM;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从表达式中提取键名
|
||||
* <p>
|
||||
* 示例:
|
||||
* - "#context.orderId" -> "orderId"
|
||||
* - "#result.assigneeId" -> "assigneeId"
|
||||
*/
|
||||
private String extractKey(String expression) {
|
||||
if (expression.contains(".")) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.enumeration;
|
||||
|
||||
/**
|
||||
* 事件域枚举
|
||||
* <p>
|
||||
* 用于标识业务事件发生的领域/模块
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public enum EventDomain {
|
||||
|
||||
/**
|
||||
* 调度域 - 工单派发、分配等
|
||||
*/
|
||||
DISPATCH("dispatch", "调度"),
|
||||
|
||||
/**
|
||||
* 信标域 - 信标检测、到岗确认等
|
||||
*/
|
||||
BEACON("beacon", "信标"),
|
||||
|
||||
/**
|
||||
* 客流域 - 客流统计、阈值触发等
|
||||
*/
|
||||
TRAFFIC("traffic", "客流"),
|
||||
|
||||
/**
|
||||
* 设备域 - 设备控制、TTS<54><53>震动等
|
||||
*/
|
||||
DEVICE("device", "设备"),
|
||||
|
||||
/**
|
||||
* 系统域 - 系统级事件
|
||||
*/
|
||||
SYSTEM("system", "系统"),
|
||||
|
||||
/**
|
||||
* 审计域 - 审计日志
|
||||
*/
|
||||
AUDIT("audit", "审计");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
EventDomain(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.enumeration;
|
||||
|
||||
/**
|
||||
* 事件日志级别枚举
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public enum EventLevel {
|
||||
|
||||
/**
|
||||
* 信息级别 - 正常业务流程
|
||||
*/
|
||||
INFO("INFO", "信息"),
|
||||
|
||||
/**
|
||||
* 警告级别 - 需要关注但不影响流程
|
||||
*/
|
||||
WARN("WARN", "警告"),
|
||||
|
||||
/**
|
||||
* 错误级别 - 业务异常或失败
|
||||
*/
|
||||
ERROR("ERROR", "错误");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
EventLevel(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.publisher;
|
||||
|
||||
import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext;
|
||||
|
||||
/**
|
||||
* 业务日志发布器接口
|
||||
* <p>
|
||||
* 用于发布业务日志,支持异步发布
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface BusinessLogPublisher {
|
||||
|
||||
/**
|
||||
* 发布业务日志
|
||||
*
|
||||
* @param context 日志上下文
|
||||
*/
|
||||
void publish(BusinessLogContext context);
|
||||
|
||||
/**
|
||||
* 异步发布业务日志
|
||||
*
|
||||
* @param context 日志上下文
|
||||
*/
|
||||
void publishAsync(BusinessLogContext context);
|
||||
|
||||
/**
|
||||
* 发布成功日志
|
||||
*
|
||||
* @param context 日志上下文
|
||||
*/
|
||||
void publishSuccess(BusinessLogContext context);
|
||||
|
||||
/**
|
||||
* 发布失败日志
|
||||
*
|
||||
* @param context 日志上下文
|
||||
* @param errorMessage 错误消息
|
||||
*/
|
||||
void publishFailure(BusinessLogContext context, String errorMessage);
|
||||
|
||||
/**
|
||||
* 发布异常日志
|
||||
*
|
||||
* @param context 日志上下文
|
||||
* @param throwable 异常
|
||||
*/
|
||||
void publishError(BusinessLogContext context, Throwable throwable);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.publisher;
|
||||
|
||||
import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 默认业务日志发布器
|
||||
* <p>
|
||||
* 职责:
|
||||
* 1. 发布业务日志到日志系统
|
||||
* 2. 支持异步发布
|
||||
* 3. 支持结构化日志输出
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DefaultBusinessLogPublisher implements BusinessLogPublisher {
|
||||
|
||||
// TODO: 注入实际的日志存储服务(如 MongoDB、Elasticsearch 等)
|
||||
// @Resource
|
||||
// private BusinessLogRepository businessLogRepository;
|
||||
|
||||
@Override
|
||||
public void publish(BusinessLogContext context) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 输出结构化日志
|
||||
log.info("[BusinessLog] type={}, scope={}, description={}, targetId={}, targetType={}, success={}",
|
||||
context.getType(),
|
||||
context.getScope(),
|
||||
context.getDescription(),
|
||||
context.getTargetId(),
|
||||
context.getTargetType(),
|
||||
context.getSuccess());
|
||||
|
||||
// TODO: 持久化到日志存储
|
||||
// businessLogRepository.save(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async("ops-task-executor")
|
||||
public void publishAsync(BusinessLogContext context) {
|
||||
publish(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishSuccess(BusinessLogContext context) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
context.setSuccess(true);
|
||||
publish(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishFailure(BusinessLogContext context, String errorMessage) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
context.setSuccess(false);
|
||||
context.setErrorMessage(errorMessage);
|
||||
publish(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishError(BusinessLogContext context, Throwable throwable) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
context.setSuccess(false);
|
||||
context.setErrorMessage(throwable != null ? throwable.getMessage() : "Unknown error");
|
||||
publish(context);
|
||||
|
||||
// 同时输出异常堆栈
|
||||
log.error("[BusinessLog] Error occurred", throwable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.recorder;
|
||||
|
||||
import com.viewsh.module.ops.dal.dataobject.log.OpsBusinessEventLogDO;
|
||||
|
||||
/**
|
||||
* 事件日志持久化接口
|
||||
* <p>
|
||||
* 负责将日志记录持久化到数据库
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface EventLogPersister {
|
||||
|
||||
/**
|
||||
* 持久化日志记录(同步)
|
||||
*
|
||||
* @param recordDO 日志DO
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean persist(OpsBusinessEventLogDO recordDO);
|
||||
|
||||
/**
|
||||
* 持久化日志记录(异步)
|
||||
*
|
||||
* @param recordDO 日志DO
|
||||
*/
|
||||
void persistAsync(OpsBusinessEventLogDO recordDO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.recorder;
|
||||
|
||||
import com.viewsh.module.ops.dal.dataobject.log.OpsBusinessEventLogDO;
|
||||
import com.viewsh.module.ops.dal.mysql.log.OpsBusinessEventLogMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 事件日志持久化实现
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class EventLogPersisterImpl implements EventLogPersister {
|
||||
|
||||
@Resource
|
||||
private OpsBusinessEventLogMapper eventLogMapper;
|
||||
|
||||
@Override
|
||||
public boolean persist(OpsBusinessEventLogDO recordDO) {
|
||||
try {
|
||||
int rows = eventLogMapper.insert(recordDO);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("[EventLogPersister] 持久化失败: module={}, domain={}, type={}",
|
||||
recordDO.getModule(), recordDO.getEventDomain(), recordDO.getEventType(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async("ops-task-executor")
|
||||
public void persistAsync(OpsBusinessEventLogDO recordDO) {
|
||||
persist(recordDO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.recorder;
|
||||
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 事件日志记录实体
|
||||
* <p>
|
||||
* 用于记录业务事件日志,包含设备、人员、业务实体的关联信息
|
||||
* 采用方案A:在记录时异步填充名称冗余字段,便于展示时直接查询
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class EventLogRecord {
|
||||
|
||||
// ==================== 基础分类字段 ====================
|
||||
|
||||
/**
|
||||
* 模块标识(如:clean, repair, patrol)
|
||||
*/
|
||||
private String module;
|
||||
|
||||
/**
|
||||
* 事件域
|
||||
*/
|
||||
private EventDomain domain;
|
||||
|
||||
/**
|
||||
* 事件类型(如:ARRIVE_CONFIRMED, TTS_SENT, AUTO_DISPATCH)
|
||||
*/
|
||||
private String eventType;
|
||||
|
||||
/**
|
||||
* 日志级别
|
||||
*/
|
||||
@Builder.Default
|
||||
private EventLevel level = EventLevel.INFO;
|
||||
|
||||
// ==================== 内容字段 ====================
|
||||
|
||||
/**
|
||||
* 事件描述(完整描述)
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 简要摘要(用于列表展示,可选)
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
// ==================== 设备关联字段 ====================
|
||||
|
||||
/**
|
||||
* 设备ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* 设备名称(冗余字段,异步填充)
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 设备编码(冗余字段,如设备序列号)
|
||||
*/
|
||||
private String deviceCode;
|
||||
|
||||
/**
|
||||
* 设备类型(冗余字段,如:BADGE, TRAFFIC_COUNTER)
|
||||
*/
|
||||
private String deviceType;
|
||||
|
||||
// ==================== 人员关联字段 ====================
|
||||
|
||||
/**
|
||||
* 人员ID(保洁员、巡检员等)
|
||||
*/
|
||||
private Long personId;
|
||||
|
||||
/**
|
||||
* 人员姓名(冗余字段,异步填充)
|
||||
*/
|
||||
private String personName;
|
||||
|
||||
/**
|
||||
* 人员类型(冗余字段,如:CLEANER, INSPECTOR)
|
||||
*/
|
||||
private String personType;
|
||||
|
||||
// ==================== 业务实体关联字段 ====================
|
||||
|
||||
/**
|
||||
* 业务实体ID(如工单ID、区域ID)
|
||||
*/
|
||||
private Long targetId;
|
||||
|
||||
/**
|
||||
* 业务实体类型(如:order, area, task)
|
||||
*/
|
||||
private String targetType;
|
||||
|
||||
// ==================== 扩展字段 ====================
|
||||
|
||||
/**
|
||||
* 扩展数据(JSON格式)
|
||||
*/
|
||||
@Builder.Default
|
||||
private Map<String, Object> payload = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 事件时间
|
||||
*/
|
||||
@Builder.Default
|
||||
private LocalDateTime eventTime = LocalDateTime.now();
|
||||
|
||||
// ==================== 便捷方法 ====================
|
||||
|
||||
/**
|
||||
* 添加扩展数据
|
||||
*/
|
||||
public void putPayload(String key, Object value) {
|
||||
if (this.payload == null) {
|
||||
this.payload = new HashMap<>();
|
||||
}
|
||||
this.payload.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展数据
|
||||
*/
|
||||
public Object getPayload(String key) {
|
||||
return payload != null ? payload.get(key) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展数据(String类型)
|
||||
*/
|
||||
public String getPayloadString(String key) {
|
||||
Object value = getPayload(key);
|
||||
return value != null ? String.valueOf(value) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展数据(Long类型)
|
||||
*/
|
||||
public Long getPayloadLong(String key) {
|
||||
Object value = getPayload(key);
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展数据(Integer类型)
|
||||
*/
|
||||
public Integer getPayloadInt(String key) {
|
||||
Object value = getPayload(key);
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== 静态构建方法 ====================
|
||||
|
||||
/**
|
||||
* 创建设备事件日志
|
||||
*/
|
||||
public static EventLogRecord forDevice(String module, EventDomain domain, String eventType,
|
||||
String message, Long deviceId) {
|
||||
return EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.deviceId(deviceId)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工单事件日志
|
||||
*/
|
||||
public static EventLogRecord forOrder(String module, EventDomain domain, String eventType,
|
||||
String message, Long orderId, Long deviceId, Long personId) {
|
||||
return EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.targetId(orderId)
|
||||
.targetType("order")
|
||||
.deviceId(deviceId)
|
||||
.personId(personId)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统事件日志
|
||||
*/
|
||||
public static EventLogRecord forSystem(String module, EventDomain domain, String eventType,
|
||||
String message) {
|
||||
return EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.recorder;
|
||||
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
|
||||
/**
|
||||
* 事件日志记录器接口
|
||||
* <p>
|
||||
* 提供业务事件日志记录的统一入口
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface EventLogRecorder {
|
||||
|
||||
/**
|
||||
* 记录事件日志(同步)
|
||||
*
|
||||
* @param record 日志记录
|
||||
*/
|
||||
void record(EventLogRecord record);
|
||||
|
||||
/**
|
||||
* 记录事件日志(异步)
|
||||
*
|
||||
* @param record 日志记录
|
||||
*/
|
||||
void recordAsync(EventLogRecord record);
|
||||
|
||||
// ==================== 便捷方法:按级别记录 ====================
|
||||
|
||||
/**
|
||||
* 记录信息级别日志
|
||||
*
|
||||
* @param module 模块标识
|
||||
* @param domain 事件域
|
||||
* @param eventType 事件类型
|
||||
* @param message 事件消息
|
||||
*/
|
||||
void info(String module, EventDomain domain, String eventType, String message);
|
||||
|
||||
/**
|
||||
* 记录信息级别日志(带设备关联)
|
||||
*
|
||||
* @param module 模块标识
|
||||
* @param domain 事件域
|
||||
* @param eventType 事件类型
|
||||
* @param message 事件消息
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
void info(String module, EventDomain domain, String eventType, String message, Long deviceId);
|
||||
|
||||
/**
|
||||
* 记录信息级别日志(带工单关联)
|
||||
*
|
||||
* @param module 模块标识
|
||||
* @param domain 事件域
|
||||
* @param eventType 事件类型
|
||||
* @param message 事件消息
|
||||
* @param orderId 工单ID
|
||||
* @param deviceId 设备ID
|
||||
* @param personId 人员ID
|
||||
*/
|
||||
void info(String module, EventDomain domain, String eventType, String message,
|
||||
Long orderId, Long deviceId, Long personId);
|
||||
|
||||
/**
|
||||
* 记录警告级别日志
|
||||
*
|
||||
* @param module 模块标识
|
||||
* @param domain 事件域
|
||||
* @param eventType 事件类型
|
||||
* @param message 事件消息
|
||||
*/
|
||||
void warn(String module, EventDomain domain, String eventType, String message);
|
||||
|
||||
/**
|
||||
* 记录警告级别日志(带设备关联)
|
||||
*
|
||||
* @param module 模块标识
|
||||
* @param domain 事件域
|
||||
* @param eventType 事件类型
|
||||
* @param message 事件消息
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
void warn(String module, EventDomain domain, String eventType, String message, Long deviceId);
|
||||
|
||||
/**
|
||||
* 记录错误级别日志
|
||||
*
|
||||
* @param module 模块标识
|
||||
* @param domain 事件域
|
||||
* @param eventType 事件类型
|
||||
* @param message 事件消息
|
||||
* @param throwable 异常信息
|
||||
*/
|
||||
void error(String module, EventDomain domain, String eventType, String message, Throwable throwable);
|
||||
|
||||
/**
|
||||
* 记录错误级别日志(带设备关联)
|
||||
*
|
||||
* @param module 模块标识
|
||||
* @param domain 事件域
|
||||
* @param eventType 事件类型
|
||||
* @param message 事件消息
|
||||
* @param deviceId 设备ID
|
||||
* @param throwable 异常信息
|
||||
*/
|
||||
void error(String module, EventDomain domain, String eventType, String message,
|
||||
Long deviceId, Throwable throwable);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.recorder;
|
||||
|
||||
import com.viewsh.module.ops.dal.dataobject.log.OpsBusinessEventLogDO;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 事件日志记录器实现
|
||||
* <p>
|
||||
* 负责记录业务事件日志,包含:
|
||||
* 1. 将 EventLogRecord 转换为 OpsBusinessEventLogDO
|
||||
* 2. 异步填充设备/人员名称(方案A:冗余存储)
|
||||
* 3. 持久化到数据库
|
||||
* 4. 输出到控制台(用于调试)
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* - record() 方法默认异步执行,不阻塞主业务流程
|
||||
* - recordSync() 方法同步执行,用于需要确认日志写入成功的场景
|
||||
* - recordAsync() 方法保留,语义更明确
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class EventLogRecorderImpl implements EventLogRecorder {
|
||||
|
||||
@Resource
|
||||
private EventLogPersister persister;
|
||||
|
||||
/**
|
||||
* 异步记录日志(默认方式)
|
||||
* <p>
|
||||
* 使用 @Async 确保日志记录不阻塞主业务流程
|
||||
*/
|
||||
@Override
|
||||
@Async("ops-task-executor")
|
||||
public void record(EventLogRecord record) {
|
||||
doRecord(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步记录日志(语义明确的方法名)
|
||||
*/
|
||||
@Override
|
||||
@Async("ops-task-executor")
|
||||
public void recordAsync(EventLogRecord record) {
|
||||
doRecord(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步记录日志
|
||||
* <p>
|
||||
* 用于需要确认日志写入成功的场景(如测试、关键业务)
|
||||
*/
|
||||
public void recordSync(EventLogRecord record) {
|
||||
doRecord(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际执行记录逻辑
|
||||
*/
|
||||
private void doRecord(EventLogRecord record) {
|
||||
try {
|
||||
// 转换为 DO
|
||||
OpsBusinessEventLogDO recordDO = convertToDO(record);
|
||||
|
||||
// TODO: 方案A - 异步填充设备/人员名称
|
||||
// 这里预留接口,后续通过 Feign 调用 IoT/Ops 模块获取名称
|
||||
enrichRecord(recordDO);
|
||||
|
||||
// 持久化(同步写入,但在异步线程中执行)
|
||||
persister.persist(recordDO);
|
||||
|
||||
// 控制台输出(便于调试)
|
||||
logConsole(record);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[EventLogRecorder] 记录失败: module={}, domain={}, type={}",
|
||||
record.getModule(), record.getDomain(), record.getEventType(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String module, EventDomain domain, String eventType, String message) {
|
||||
record(EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.level(EventLevel.INFO)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String module, EventDomain domain, String eventType, String message, Long deviceId) {
|
||||
record(EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.deviceId(deviceId)
|
||||
.level(EventLevel.INFO)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String module, EventDomain domain, String eventType, String message,
|
||||
Long orderId, Long deviceId, Long personId) {
|
||||
record(EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.targetId(orderId)
|
||||
.targetType("order")
|
||||
.deviceId(deviceId)
|
||||
.personId(personId)
|
||||
.level(EventLevel.INFO)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(String module, EventDomain domain, String eventType, String message) {
|
||||
record(EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.level(EventLevel.WARN)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(String module, EventDomain domain, String eventType, String message, Long deviceId) {
|
||||
record(EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.deviceId(deviceId)
|
||||
.level(EventLevel.WARN)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String module, EventDomain domain, String eventType, String message, Throwable throwable) {
|
||||
EventLogRecord record = EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.level(EventLevel.ERROR)
|
||||
.build();
|
||||
if (throwable != null) {
|
||||
record.putPayload("errorMessage", throwable.getMessage());
|
||||
record.putPayload("errorClass", throwable.getClass().getSimpleName());
|
||||
}
|
||||
record(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String module, EventDomain domain, String eventType, String message,
|
||||
Long deviceId, Throwable throwable) {
|
||||
EventLogRecord record = EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(message)
|
||||
.deviceId(deviceId)
|
||||
.level(EventLevel.ERROR)
|
||||
.build();
|
||||
if (throwable != null) {
|
||||
record.putPayload("errorMessage", throwable.getMessage());
|
||||
record.putPayload("errorClass", throwable.getClass().getSimpleName());
|
||||
}
|
||||
record(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 DO
|
||||
*/
|
||||
private OpsBusinessEventLogDO convertToDO(EventLogRecord record) {
|
||||
return OpsBusinessEventLogDO.builder()
|
||||
.eventTime(record.getEventTime())
|
||||
.eventLevel(record.getLevel() != null ? record.getLevel().getCode() : EventLevel.INFO.getCode())
|
||||
.module(record.getModule())
|
||||
.eventDomain(record.getDomain() != null ? record.getDomain().getCode() : null)
|
||||
.eventType(record.getEventType())
|
||||
.deviceId(record.getDeviceId())
|
||||
.deviceName(record.getDeviceName())
|
||||
.deviceCode(record.getDeviceCode())
|
||||
.deviceType(record.getDeviceType())
|
||||
.personId(record.getPersonId())
|
||||
.personName(record.getPersonName())
|
||||
.personType(record.getPersonType())
|
||||
.targetId(record.getTargetId())
|
||||
.targetType(record.getTargetType())
|
||||
.eventMessage(record.getMessage())
|
||||
.eventSummary(record.getSummary())
|
||||
.eventPayload(record.getPayload())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步填充设备/人员名称(方案A)
|
||||
* <p>
|
||||
* TODO: 后续通过 Feign 调用以下接口异步填充:
|
||||
* - IotDeviceApi.getBasicInfo(deviceId) -> 获取设备名称、编码、类型
|
||||
* - CleanerService.getBasicInfo(personId) -> 获取人员姓名、类型
|
||||
* <p>
|
||||
* 当前先留空,待 Feign 接口创建后再补充
|
||||
*/
|
||||
private void enrichRecord(OpsBusinessEventLogDO recordDO) {
|
||||
// 暂不实现异步填充,待后续完成 Feign 接口后再补充
|
||||
// 设计时考虑:
|
||||
// 1. 如果 recordDO.getDeviceId() 不为空,调用 IoT 模块获取设备信息
|
||||
// 2. 如果 recordDO.getPersonId() 不为空,调用 Ops 模块获取人员信息
|
||||
// 3. 填充冗余字段:deviceName, deviceCode, deviceType, personName, personType
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台输出(便于调试)
|
||||
*/
|
||||
private void logConsole(EventLogRecord record) {
|
||||
String deviceInfo = record.getDeviceId() != null
|
||||
? " [设备:" + record.getDeviceId() + (record.getDeviceName() != null ? "(" + record.getDeviceName() + ")" : "") + "]"
|
||||
: "";
|
||||
String personInfo = record.getPersonId() != null
|
||||
? " [人员:" + record.getPersonId() + (record.getPersonName() != null ? "(" + record.getPersonName() + ")" : "") + "]"
|
||||
: "";
|
||||
String targetInfo = record.getTargetId() != null
|
||||
? " [" + record.getTargetType() + ":" + record.getTargetId() + "]"
|
||||
: "";
|
||||
|
||||
log.info("[EventLog] [{}] {} {} {} - {}{}{}",
|
||||
record.getLevel() != null ? record.getLevel().getCode() : "INFO",
|
||||
record.getModule(),
|
||||
record.getDomain() != null ? record.getDomain().getCode() : "UNKNOWN",
|
||||
record.getEventType(),
|
||||
record.getMessage(),
|
||||
deviceInfo,
|
||||
personInfo,
|
||||
targetInfo);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user