6 Commits

Author SHA1 Message Date
lzh
c75696c644 feat(ops): 新增业务日志表
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
- 新增 ops_business_event_log 表结构

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 18:44:30 +08:00
lzh
c4ef31bb98 refactor(ops): 保洁工单相关服务改用新业务日志框架
- CleanOrderAuditEventHandler 使用 @BusinessLog 注解
- CleanOrderConfirmEventHandler 使用 @BusinessLog 注解
- VoiceBroadcastService 使用新的日志记录方式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 18:44:21 +08:00
lzh
457fc556e2 refactor(ops): 移除保洁工单清理日志相关代码
- 删除 OpsOrderCleanLogDO 实体类
- 删除 OpsOrderCleanLogMapper Mapper接口
- 统一使用新的业务日志框架

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 18:44:07 +08:00
lzh
5063fc8dd1 refactor(ops): 移除旧的publisher模式
- 删除 BusinessLogPublisher 接口
- 删除 DefaultBusinessLogPublisher 实现
- 改用 recorder 模式替代 publisher 模式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 18:43:54 +08:00
lzh
a49994cc57 refactor(ops): 重构业务日志注解和切面
- 更新 BusinessLog 注解以支持新的事件日志模型
- 更新 BusinessLogAspect 切面使用 EventLogRecorder

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 18:43:39 +08:00
lzh
a82b59ac46 feat(ops): 新增业务日志枚举类
- 新增 EventDomain:事件领域枚举
- 新增 EventLevel:事件级别枚举

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 18:43:32 +08:00
21 changed files with 1415 additions and 609 deletions

View File

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

View File

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

View File

@@ -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实现
}

View File

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

View File

@@ -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}开始作业"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(".")) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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