feat: add custom command call API, event report API with RabbitMQ and gRPC

- Add dc3_command_record and dc3_event_record DTOs, DOs, mappers, managers
- Implement CommandRecordService: call flow with validation, RabbitMQ dispatch,
  result/dead receivers, full lifecycle (PENDING→SENT→SUCCESS/FAILED/TIMEOUT/DEAD)
- Implement EventReportService: report flow with validation and persistence
- Add REST controllers: /command_record (call/get/list), /event_report (report/get/list)
- Add RabbitMQ exchanges/queues/bindings for command dispatch, result, dead letter
- Add event exchange for driver→center event reporting
- Add gRPC CommandRecordApi and EventReportApi with proto definitions and servers
- Fix PointCommandValidator parameter type (String→PointExt)
This commit is contained in:
pnoker
2026-05-23 18:26:28 +08:00
parent aa5c4ddf8e
commit 150ba33244
36 changed files with 2405 additions and 1 deletions
@@ -0,0 +1,95 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
syntax = "proto3";
package api.common.data;
import "api/common/r.proto";
import "api/common/page.proto";
option java_package = "io.github.pnoker.api.center.data";
option java_outer_classname = "CommandRecordProto";
option objc_class_prefix = "Data";
option java_multiple_files = true;
service CommandRecordApi {
rpc Call (GrpcCommandCallVO) returns (GrpcRString);
rpc GetByRecordId (GrpcStringQuery) returns (GrpcRCommandRecordDTO);
rpc List (GrpcCommandRecordQuery) returns (GrpcRPageCommandRecordDTO);
}
message GrpcCommandCallVO {
int64 device_id = 1;
int64 command_id = 2;
map<string, string> param_values = 3;
int64 tenant_id = 4;
}
message GrpcCommandRecordDTO {
int64 id = 1;
string record_id = 2;
int64 tenant_id = 3;
int64 device_id = 4;
int64 command_id = 5;
string command_code = 6;
map<string, string> param_values = 7;
map<string, string> result_values = 8;
string status = 9;
string error_code = 10;
string error_message = 11;
string source = 12;
int64 source_user_id = 13;
int64 occurred_at = 14;
int64 sent_at = 15;
int64 finished_at = 16;
int32 schema_version = 17;
int64 create_time = 18;
int64 update_time = 19;
}
message GrpcCommandRecordQuery {
int64 device_id = 1;
int64 command_id = 2;
string status = 3;
int64 tenant_id = 4;
GrpcPage page = 5;
}
message GrpcRCommandRecordDTO {
GrpcR result = 1;
GrpcCommandRecordDTO data = 2;
}
message GrpcPageCommandRecordDTO {
GrpcPage page = 1;
repeated GrpcCommandRecordDTO data = 2;
}
message GrpcRPageCommandRecordDTO {
GrpcR result = 1;
GrpcPageCommandRecordDTO data = 2;
}
message GrpcStringQuery {
string value = 1;
}
message GrpcRString {
GrpcR result = 1;
string data = 2;
}
@@ -0,0 +1,85 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
syntax = "proto3";
package api.common.data;
import "api/common/r.proto";
import "api/common/page.proto";
import "api/center/data/command_record.proto";
option java_package = "io.github.pnoker.api.center.data";
option java_outer_classname = "EventReportProto";
option objc_class_prefix = "Data";
option java_multiple_files = true;
service EventReportApi {
rpc Report (GrpcEventReportVO) returns (GrpcRString);
rpc GetByRecordId (GrpcStringQuery) returns (GrpcREventRecordDTO);
rpc List (GrpcEventRecordQuery) returns (GrpcRPageEventRecordDTO);
}
message GrpcEventReportVO {
int64 device_id = 1;
int64 event_id = 2;
map<string, string> param_values = 3;
string message = 4;
int64 tenant_id = 5;
}
message GrpcEventRecordDTO {
int64 id = 1;
string record_id = 2;
int64 tenant_id = 3;
int64 device_id = 4;
int64 event_id = 5;
string event_code = 6;
int32 event_type_flag = 7;
int32 event_level_flag = 8;
map<string, string> param_values = 9;
string message = 10;
int64 occur_time = 11;
int64 receive_time = 12;
int32 acknowledge_flag = 13;
int32 schema_version = 14;
int64 create_time = 15;
int64 update_time = 16;
}
message GrpcEventRecordQuery {
int64 device_id = 1;
int64 event_id = 2;
int32 event_type_flag = 3;
int64 tenant_id = 4;
GrpcPage page = 5;
}
message GrpcREventRecordDTO {
GrpcR result = 1;
GrpcEventRecordDTO data = 2;
}
message GrpcPageEventRecordDTO {
GrpcPage page = 1;
repeated GrpcEventRecordDTO data = 2;
}
message GrpcRPageEventRecordDTO {
GrpcR result = 1;
GrpcPageEventRecordDTO data = 2;
}
@@ -106,6 +106,21 @@ public class RabbitConstant {
// Point Command Result
public static String TOPIC_EXCHANGE_POINT_COMMAND_RESULT = "dc3.e.point_command_result";
public static String QUEUE_POINT_COMMAND_RESULT = "dc3.q.point_command_result";
// Custom Command
public static String TOPIC_EXCHANGE_COMMAND = "dc3.e.command";
public static String QUEUE_COMMAND_PREFIX = "dc3.q.command.";
public static String ROUTING_COMMAND_PREFIX = "dc3.r.command.";
// Custom Command Result
public static String TOPIC_EXCHANGE_COMMAND_RESULT = "dc3.e.command_result";
public static String QUEUE_COMMAND_RESULT = "dc3.q.command_result";
public static String ROUTING_COMMAND_RESULT = "dc3.r.command_result";
// Custom Command Dead Letter
public static String TOPIC_EXCHANGE_COMMAND_DEAD = "dc3.e.command_dead";
public static String QUEUE_COMMAND_DEAD = "dc3.q.command_dead";
// Event Report
public static String TOPIC_EXCHANGE_EVENT = "dc3.e.event";
public static String QUEUE_EVENT_PREFIX = "dc3.q.event.";
public static String ROUTING_EVENT_PREFIX = "dc3.r.event.";
private RabbitConstant() {
throw new IllegalStateException(BaseConstant.UTILITY_CLASS);
@@ -54,6 +54,10 @@ public class DataConstant {
public static final String MESSAGE_URL_PREFIX = "/message";
public static final String COMMAND_RECORD_URL_PREFIX = "/command_record";
public static final String EVENT_REPORT_URL_PREFIX = "/event_report";
public static final String DRIVER_STATUS_URL_PREFIX = "/driver/status";
public static final String DRIVER_EVENT_URL_PREFIX = "/driver/event";
@@ -0,0 +1,83 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Optional;
/**
* Enumeration of custom command record sources.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Getter
@AllArgsConstructor
public enum CommandRecordSourceEnum {
/**
* HTTP API
*/
HTTP((byte) 0, "http", "HTTP API"),
/**
* gRPC API
*/
GRPC((byte) 1, "grpc", "gRPC API"),
/**
* Agentic center
*/
AGENTIC((byte) 2, "agentic", "Agentic center"),
;
@EnumValue
private final Byte index;
private final String code;
private final String remark;
public static CommandRecordSourceEnum ofIndex(Byte index) {
Optional<CommandRecordSourceEnum> any = Arrays.stream(CommandRecordSourceEnum.values())
.filter(type -> type.getIndex().equals(index))
.findFirst();
return any.orElse(null);
}
public static CommandRecordSourceEnum ofCode(String code) {
Optional<CommandRecordSourceEnum> any = Arrays.stream(CommandRecordSourceEnum.values())
.filter(type -> type.getCode().equals(code))
.findFirst();
return any.orElse(null);
}
public static CommandRecordSourceEnum ofName(String name) {
try {
return valueOf(name);
} catch (IllegalArgumentException e) {
return null;
}
}
}
@@ -0,0 +1,78 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Optional;
/**
* Enumeration of event record acknowledge flags.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Getter
@AllArgsConstructor
public enum EventRecordAcknowledgeFlagEnum {
/**
* Not acknowledged
*/
NO((byte) 0, "no", "Not acknowledged"),
/**
* Acknowledged
*/
YES((byte) 1, "yes", "Acknowledged"),
;
@EnumValue
private final Byte index;
private final String code;
private final String remark;
public static EventRecordAcknowledgeFlagEnum ofIndex(Byte index) {
Optional<EventRecordAcknowledgeFlagEnum> any = Arrays.stream(EventRecordAcknowledgeFlagEnum.values())
.filter(type -> type.getIndex().equals(index))
.findFirst();
return any.orElse(null);
}
public static EventRecordAcknowledgeFlagEnum ofCode(String code) {
Optional<EventRecordAcknowledgeFlagEnum> any = Arrays.stream(EventRecordAcknowledgeFlagEnum.values())
.filter(type -> type.getCode().equals(code))
.findFirst();
return any.orElse(null);
}
public static EventRecordAcknowledgeFlagEnum ofName(String name) {
try {
return valueOf(name);
} catch (IllegalArgumentException e) {
return null;
}
}
}
@@ -267,4 +267,50 @@ public class DataTopicConfig {
.with(RabbitConstant.ROUTING_POINT_COMMAND_RESULT + ".*");
}
// ===== Custom command dead letter ========================================
@Bean
Queue commandDeadQueue() {
return QueueBuilder.durable(RabbitConstant.QUEUE_COMMAND_DEAD).build();
}
@Bean
Binding commandDeadBinding(Queue commandDeadQueue, TopicExchange commandDeadExchange) {
return BindingBuilder.bind(commandDeadQueue)
.to(commandDeadExchange)
.with("#");
}
// ===== Custom command result =============================================
@Bean
Queue commandResultQueue() {
return QueueBuilder.durable(RabbitConstant.QUEUE_COMMAND_RESULT)
.ttl(60000)
.build();
}
@Bean
Binding commandResultBinding(Queue commandResultQueue, TopicExchange commandResultExchange) {
return BindingBuilder.bind(commandResultQueue)
.to(commandResultExchange)
.with(RabbitConstant.ROUTING_COMMAND_RESULT + ".*");
}
// ===== Event report ======================================================
@Bean
Queue eventReportQueue() {
return QueueBuilder.durable(RabbitConstant.QUEUE_EVENT_PREFIX + "report")
.ttl(60000)
.build();
}
@Bean
Binding eventReportBinding(Queue eventReportQueue, TopicExchange eventExchange) {
return BindingBuilder.bind(eventReportQueue)
.to(eventExchange)
.with(RabbitConstant.ROUTING_EVENT_PREFIX + "*");
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.biz;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
import io.github.pnoker.common.data.entity.vo.CommandCallVO;
import io.github.pnoker.common.data.entity.vo.CommandRecordQueryVO;
/**
* Business service for custom command call operations.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
public interface CommandRecordService {
String call(Long tenantId, CommandCallVO entityVO);
CommandRecordDO getByRecordId(String recordId);
Page<CommandRecordDO> list(Long tenantId, CommandRecordQueryVO queryVO);
}
@@ -0,0 +1,40 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.biz;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.common.data.entity.model.EventRecordDO;
import io.github.pnoker.common.data.entity.vo.EventRecordQueryVO;
import io.github.pnoker.common.data.entity.vo.EventReportVO;
/**
* Business service for event report operations.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
public interface EventReportService {
String report(Long tenantId, EventReportVO entityVO);
EventRecordDO getByRecordId(String recordId);
Page<EventRecordDO> list(Long tenantId, EventRecordQueryVO queryVO);
}
@@ -0,0 +1,185 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.biz.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.common.constant.common.ExceptionConstant;
import io.github.pnoker.common.constant.driver.RabbitConstant;
import io.github.pnoker.common.data.biz.CommandRecordService;
import io.github.pnoker.common.data.dal.CommandRecordManager;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
import io.github.pnoker.common.data.entity.model.EntityStateDO;
import io.github.pnoker.common.data.entity.vo.CommandCallVO;
import io.github.pnoker.common.data.entity.vo.CommandRecordQueryVO;
import io.github.pnoker.common.data.mapper.EntityStateMapper;
import io.github.pnoker.common.entity.dto.CommandCallDTO;
import io.github.pnoker.common.enums.CommandRecordSourceEnum;
import io.github.pnoker.common.enums.EnableFlagEnum;
import io.github.pnoker.common.enums.EntityStatusEnum;
import io.github.pnoker.common.enums.EntityTypeFlagEnum;
import io.github.pnoker.common.enums.PointCommandStatusEnum;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.exception.ServiceException;
import io.github.pnoker.common.exception.UnAuthorizedException;
import io.github.pnoker.common.facade.api.CommandFacade;
import io.github.pnoker.common.facade.api.DeviceFacade;
import io.github.pnoker.common.facade.api.DriverFacade;
import io.github.pnoker.common.facade.entity.bo.FacadeCommandBO;
import io.github.pnoker.common.facade.entity.bo.FacadeDeviceBO;
import io.github.pnoker.common.facade.entity.bo.FacadeDriverBO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.UUID;
/**
* Business service implementation for custom command call operations.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CommandRecordServiceImpl implements CommandRecordService {
private final DeviceFacade deviceFacade;
private final DriverFacade driverFacade;
private final CommandFacade commandFacade;
private final RabbitTemplate rabbitTemplate;
private final CommandRecordManager commandRecordManager;
private final EntityStateMapper entityStateMapper;
@Override
public String call(Long tenantId, CommandCallVO entityVO) {
validateCommandScope(tenantId, entityVO.getDeviceId(), entityVO.getCommandId());
FacadeDriverBO driver = driverFacade.getByDeviceId(tenantId, entityVO.getDeviceId());
if (Objects.isNull(driver)) {
throw new ServiceException("No driver registered for this device");
}
checkDriverOnline(tenantId, driver.getId());
FacadeCommandBO command = commandFacade.getById(tenantId, entityVO.getCommandId());
String recordId = UUID.randomUUID().toString();
LocalDateTime nowLocal = LocalDateTime.now();
CommandRecordDO recordDO = new CommandRecordDO();
recordDO.setRecordId(recordId);
recordDO.setTenantId(tenantId);
recordDO.setDeviceId(entityVO.getDeviceId());
recordDO.setCommandId(entityVO.getCommandId());
recordDO.setCommandCode(command.getCommandCode());
recordDO.setParamValues(Objects.isNull(entityVO.getParamValues()) ? null : entityVO.getParamValues().toString());
recordDO.setStatus(PointCommandStatusEnum.PENDING.getCode());
recordDO.setSource(CommandRecordSourceEnum.HTTP.getCode());
recordDO.setOccurredAt(nowLocal);
recordDO.setExpireAt(nowLocal.plusSeconds(30));
recordDO.setSchemaVersion((short) 1);
commandRecordManager.save(recordDO);
publishCommand(CommandCallDTO.builder()
.recordId(recordId)
.tenantId(tenantId)
.deviceId(entityVO.getDeviceId())
.commandId(entityVO.getCommandId())
.commandCode(command.getCommandCode())
.paramValues(entityVO.getParamValues())
.source(CommandRecordSourceEnum.HTTP.getCode())
.occurredAt(java.time.Instant.now())
.expireAt(java.time.Instant.now().plusSeconds(30))
.schemaVersion(1)
.build(), driver.getServiceName(), recordId);
recordDO.setStatus(PointCommandStatusEnum.SENT.getCode());
recordDO.setSentAt(LocalDateTime.now());
commandRecordManager.updateById(recordDO);
return recordId;
}
@Override
public CommandRecordDO getByRecordId(String recordId) {
return commandRecordManager.lambdaQuery()
.eq(CommandRecordDO::getRecordId, recordId)
.one();
}
@Override
public Page<CommandRecordDO> list(Long tenantId, CommandRecordQueryVO queryVO) {
LambdaQueryWrapper<CommandRecordDO> wrapper = new LambdaQueryWrapper<CommandRecordDO>()
.eq(CommandRecordDO::getTenantId, tenantId)
.eq(Objects.nonNull(queryVO.getDeviceId()), CommandRecordDO::getDeviceId, queryVO.getDeviceId())
.eq(Objects.nonNull(queryVO.getCommandId()), CommandRecordDO::getCommandId, queryVO.getCommandId())
.eq(Objects.nonNull(queryVO.getStatus()), CommandRecordDO::getStatus, queryVO.getStatus())
.orderByDesc(CommandRecordDO::getOccurredAt);
return commandRecordManager.page(queryVO.toPage(), wrapper);
}
private void checkDriverOnline(Long tenantId, Long driverId) {
EntityStateDO driverState = entityStateMapper.selectOne(
new LambdaQueryWrapper<EntityStateDO>()
.eq(EntityStateDO::getTenantId, tenantId)
.eq(EntityStateDO::getEntityTypeFlag, EntityTypeFlagEnum.DRIVER.getIndex())
.eq(EntityStateDO::getEntityId, driverId));
if (Objects.isNull(driverState) || !EntityStatusEnum.ONLINE.getIndex().equals(driverState.getStateFlag())) {
throw new ServiceException("Driver is offline");
}
}
private void validateCommandScope(Long tenantId, Long deviceId, Long commandId) {
FacadeDeviceBO device = deviceFacade.getById(tenantId, deviceId);
if (Objects.isNull(device)) {
throw new NotFoundException("Device does not exist");
}
if (EnableFlagEnum.DISABLE.equals(device.getEnableFlag())) {
throw new ServiceException("Device is disabled");
}
FacadeCommandBO command = commandFacade.getById(tenantId, commandId);
if (Objects.isNull(command)) {
throw new NotFoundException("Command does not exist");
}
if (EnableFlagEnum.DISABLE.equals(command.getEnableFlag())) {
throw new ServiceException("Command is disabled");
}
if (Objects.isNull(device.getProfileId()) || !Objects.equals(device.getProfileId(), command.getProfileId())) {
throw new UnAuthorizedException(ExceptionConstant.NO_AVAILABLE_AUTH);
}
}
private void publishCommand(CommandCallDTO dto, String serviceName, String recordId) {
CorrelationData correlationData = new CorrelationData(recordId);
rabbitTemplate.convertAndSend(RabbitConstant.TOPIC_EXCHANGE_COMMAND,
RabbitConstant.ROUTING_COMMAND_PREFIX + serviceName, dto, correlationData);
}
}
@@ -0,0 +1,130 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.biz.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.common.constant.common.ExceptionConstant;
import io.github.pnoker.common.data.biz.EventReportService;
import io.github.pnoker.common.data.dal.EventRecordManager;
import io.github.pnoker.common.data.entity.model.EventRecordDO;
import io.github.pnoker.common.data.entity.vo.EventRecordQueryVO;
import io.github.pnoker.common.data.entity.vo.EventReportVO;
import io.github.pnoker.common.enums.EnableFlagEnum;
import io.github.pnoker.common.enums.EventRecordAcknowledgeFlagEnum;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.exception.ServiceException;
import io.github.pnoker.common.exception.UnAuthorizedException;
import io.github.pnoker.common.facade.api.DeviceFacade;
import io.github.pnoker.common.facade.api.EventFacade;
import io.github.pnoker.common.facade.entity.bo.FacadeDeviceBO;
import io.github.pnoker.common.facade.entity.bo.FacadeEventBO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.UUID;
/**
* Business service implementation for event report operations.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class EventReportServiceImpl implements EventReportService {
private final DeviceFacade deviceFacade;
private final EventFacade eventFacade;
private final EventRecordManager eventRecordManager;
@Override
public String report(Long tenantId, EventReportVO entityVO) {
validateEventScope(tenantId, entityVO.getDeviceId(), entityVO.getEventId());
FacadeEventBO event = eventFacade.getById(tenantId, entityVO.getEventId());
String recordId = UUID.randomUUID().toString();
LocalDateTime nowLocal = LocalDateTime.now();
EventRecordDO recordDO = new EventRecordDO();
recordDO.setRecordId(recordId);
recordDO.setTenantId(tenantId);
recordDO.setDeviceId(entityVO.getDeviceId());
recordDO.setEventId(entityVO.getEventId());
recordDO.setEventCode(event.getEventCode());
recordDO.setEventTypeFlag(event.getEventTypeFlag().getIndex());
recordDO.setEventLevelFlag(event.getEventLevelFlag().getIndex());
recordDO.setParamValues(Objects.isNull(entityVO.getParamValues()) ? null : entityVO.getParamValues().toString());
recordDO.setMessage(entityVO.getMessage());
recordDO.setOccurTime(nowLocal);
recordDO.setReceiveTime(nowLocal);
recordDO.setAcknowledgeFlag(EventRecordAcknowledgeFlagEnum.NO.getIndex());
recordDO.setSchemaVersion((short) 1);
eventRecordManager.save(recordDO);
return recordId;
}
@Override
public EventRecordDO getByRecordId(String recordId) {
return eventRecordManager.lambdaQuery()
.eq(EventRecordDO::getRecordId, recordId)
.one();
}
@Override
public Page<EventRecordDO> list(Long tenantId, EventRecordQueryVO queryVO) {
LambdaQueryWrapper<EventRecordDO> wrapper = new LambdaQueryWrapper<EventRecordDO>()
.eq(EventRecordDO::getTenantId, tenantId)
.eq(Objects.nonNull(queryVO.getDeviceId()), EventRecordDO::getDeviceId, queryVO.getDeviceId())
.eq(Objects.nonNull(queryVO.getEventId()), EventRecordDO::getEventId, queryVO.getEventId())
.eq(Objects.nonNull(queryVO.getEventTypeFlag()), EventRecordDO::getEventTypeFlag, queryVO.getEventTypeFlag())
.orderByDesc(EventRecordDO::getOccurTime);
return eventRecordManager.page(queryVO.toPage(), wrapper);
}
private void validateEventScope(Long tenantId, Long deviceId, Long eventId) {
FacadeDeviceBO device = deviceFacade.getById(tenantId, deviceId);
if (Objects.isNull(device)) {
throw new NotFoundException("Device does not exist");
}
if (EnableFlagEnum.DISABLE.equals(device.getEnableFlag())) {
throw new ServiceException("Device is disabled");
}
FacadeEventBO event = eventFacade.getById(tenantId, eventId);
if (Objects.isNull(event)) {
throw new NotFoundException("Event does not exist");
}
if (EnableFlagEnum.DISABLE.equals(event.getEnableFlag())) {
throw new ServiceException("Event is disabled");
}
if (Objects.isNull(device.getProfileId()) || !Objects.equals(device.getProfileId(), event.getProfileId())) {
throw new UnAuthorizedException(ExceptionConstant.NO_AVAILABLE_AUTH);
}
}
}
@@ -0,0 +1,78 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.controller;
import io.github.pnoker.common.base.BaseController;
import io.github.pnoker.common.constant.service.DataConstant;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.common.data.biz.CommandRecordService;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
import io.github.pnoker.common.data.entity.vo.CommandCallVO;
import io.github.pnoker.common.data.entity.vo.CommandRecordQueryVO;
import io.github.pnoker.common.entity.R;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.Objects;
/**
* REST controller for custom command call management.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Slf4j
@RestController
@RequestMapping(DataConstant.COMMAND_RECORD_URL_PREFIX)
@RequiredArgsConstructor
public class CommandRecordController implements BaseController {
private final CommandRecordService commandRecordService;
@PostMapping("/call")
public Mono<R<String>> call(@Validated @RequestBody CommandCallVO entityVO) {
return getTenantId().flatMap(tenantId -> async(() -> {
String recordId = commandRecordService.call(tenantId, entityVO);
return R.ok(recordId);
}));
}
@GetMapping("/{recordId}")
public Mono<R<CommandRecordDO>> getByRecordId(@NotBlank @PathVariable String recordId) {
return async(() -> R.ok(commandRecordService.getByRecordId(recordId)));
}
@PostMapping("/list")
public Mono<R<Page<CommandRecordDO>>> list(@RequestBody(required = false) CommandRecordQueryVO queryVO) {
return getTenantId().flatMap(tenantId -> async(() -> {
CommandRecordQueryVO query = Objects.isNull(queryVO) ? new CommandRecordQueryVO() : queryVO;
return R.ok(commandRecordService.list(tenantId, query));
}));
}
}
@@ -0,0 +1,78 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.controller;
import io.github.pnoker.common.base.BaseController;
import io.github.pnoker.common.constant.service.DataConstant;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.common.data.biz.EventReportService;
import io.github.pnoker.common.data.entity.model.EventRecordDO;
import io.github.pnoker.common.data.entity.vo.EventRecordQueryVO;
import io.github.pnoker.common.data.entity.vo.EventReportVO;
import io.github.pnoker.common.entity.R;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.Objects;
/**
* REST controller for event report management.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Slf4j
@RestController
@RequestMapping(DataConstant.EVENT_REPORT_URL_PREFIX)
@RequiredArgsConstructor
public class EventReportController implements BaseController {
private final EventReportService eventReportService;
@PostMapping("/report")
public Mono<R<String>> report(@Validated @RequestBody EventReportVO entityVO) {
return getTenantId().flatMap(tenantId -> async(() -> {
String recordId = eventReportService.report(tenantId, entityVO);
return R.ok(recordId);
}));
}
@GetMapping("/{recordId}")
public Mono<R<EventRecordDO>> getByRecordId(@NotBlank @PathVariable String recordId) {
return async(() -> R.ok(eventReportService.getByRecordId(recordId)));
}
@PostMapping("/list")
public Mono<R<Page<EventRecordDO>>> list(@RequestBody(required = false) EventRecordQueryVO queryVO) {
return getTenantId().flatMap(tenantId -> async(() -> {
EventRecordQueryVO query = Objects.isNull(queryVO) ? new EventRecordQueryVO() : queryVO;
return R.ok(eventReportService.list(tenantId, query));
}));
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.dal;
import com.baomidou.mybatisplus.extension.service.IService;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
/**
* Manager interface for dc3_command_record table.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
public interface CommandRecordManager extends IService<CommandRecordDO> {
}
@@ -0,0 +1,31 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.dal;
import com.baomidou.mybatisplus.extension.service.IService;
import io.github.pnoker.common.data.entity.model.EventRecordDO;
/**
* Manager interface for dc3_event_record table.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
public interface EventRecordManager extends IService<EventRecordDO> {
}
@@ -0,0 +1,35 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.dal.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.github.pnoker.common.data.dal.CommandRecordManager;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
import io.github.pnoker.common.data.mapper.CommandRecordMapper;
import org.springframework.stereotype.Service;
/**
* Manager implementation for dc3_command_record table.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Service
public class CommandRecordManagerImpl extends ServiceImpl<CommandRecordMapper, CommandRecordDO> implements CommandRecordManager {
}
@@ -0,0 +1,35 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.dal.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.github.pnoker.common.data.dal.EventRecordManager;
import io.github.pnoker.common.data.entity.model.EventRecordDO;
import io.github.pnoker.common.data.mapper.EventRecordMapper;
import org.springframework.stereotype.Service;
/**
* Manager implementation for dc3_event_record table.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Service
public class EventRecordManagerImpl extends ServiceImpl<EventRecordMapper, EventRecordDO> implements EventRecordManager {
}
@@ -0,0 +1,108 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.entity.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* Persistence object for the dc3_command_record table.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Getter
@Setter
@ToString
@TableName(value = "dc3_command_record")
public class CommandRecordDO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("record_id")
private String recordId;
@TableField("tenant_id")
private Long tenantId;
@TableField("device_id")
private Long deviceId;
@TableField("command_id")
private Long commandId;
@TableField("command_code")
private String commandCode;
@TableField("param_values")
private String paramValues;
@TableField("result_values")
private String resultValues;
@TableField("status")
private String status;
@TableField("error_code")
private String errorCode;
@TableField("error_message")
private String errorMessage;
@TableField("source")
private String source;
@TableField("source_user_id")
private Long sourceUserId;
@TableField("occurred_at")
private LocalDateTime occurredAt;
@TableField("sent_at")
private LocalDateTime sentAt;
@TableField("finished_at")
private LocalDateTime finishedAt;
@TableField("expire_at")
private LocalDateTime expireAt;
@TableField("schema_version")
private Short schemaVersion;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_time")
private LocalDateTime updateTime;
}
@@ -0,0 +1,96 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.entity.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* Persistence object for the dc3_event_record table.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Getter
@Setter
@ToString
@TableName(value = "dc3_event_record")
public class EventRecordDO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("record_id")
private String recordId;
@TableField("tenant_id")
private Long tenantId;
@TableField("device_id")
private Long deviceId;
@TableField("event_id")
private Long eventId;
@TableField("event_code")
private String eventCode;
@TableField("event_type_flag")
private Byte eventTypeFlag;
@TableField("event_level_flag")
private Byte eventLevelFlag;
@TableField("param_values")
private String paramValues;
@TableField("message")
private String message;
@TableField("occur_time")
private LocalDateTime occurTime;
@TableField("receive_time")
private LocalDateTime receiveTime;
@TableField("acknowledge_flag")
private Byte acknowledgeFlag;
@TableField("schema_version")
private Short schemaVersion;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_time")
private LocalDateTime updateTime;
}
@@ -0,0 +1,55 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.entity.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
/**
* VO for submitting a custom command call.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Getter
@Setter
@ToString
public class CommandCallVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@NotNull
private Long deviceId;
@NotNull
private Long commandId;
private Map<String, String> paramValues;
private String commandId_;
}
@@ -0,0 +1,58 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.entity.vo;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serial;
import java.io.Serializable;
/**
* VO for querying command records with pagination and filters.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Getter
@Setter
@ToString
public class CommandRecordQueryVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long deviceId;
private Long commandId;
private String status;
private Integer page = 1;
private Integer size = 20;
public <T> Page<T> toPage() {
return new Page<>(page, size);
}
}
@@ -0,0 +1,58 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.entity.vo;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.common.data.entity.model.EventRecordDO;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serial;
import java.io.Serializable;
/**
* VO for querying event records with pagination and filters.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Getter
@Setter
@ToString
public class EventRecordQueryVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long deviceId;
private Long eventId;
private Byte eventTypeFlag;
private Integer page = 1;
private Integer size = 20;
public <T> Page<T> toPage() {
return new Page<>(page, size);
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.entity.vo;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
/**
* VO for reporting an event from a device or external system.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Getter
@Setter
@ToString
public class EventReportVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@NotNull
private Long deviceId;
@NotNull
private Long eventId;
private Map<String, String> paramValues;
private String message;
}
@@ -0,0 +1,183 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.grpc.server;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.api.center.data.CommandRecordApiGrpc;
import io.github.pnoker.api.center.data.GrpcCommandCallVO;
import io.github.pnoker.api.center.data.GrpcCommandRecordDTO;
import io.github.pnoker.api.center.data.GrpcCommandRecordQuery;
import io.github.pnoker.api.center.data.GrpcPageCommandRecordDTO;
import io.github.pnoker.api.center.data.GrpcRCommandRecordDTO;
import io.github.pnoker.api.center.data.GrpcRPageCommandRecordDTO;
import io.github.pnoker.api.center.data.GrpcRString;
import io.github.pnoker.api.center.data.GrpcStringQuery;
import io.github.pnoker.api.common.GrpcPage;
import io.github.pnoker.api.common.GrpcR;
import io.github.pnoker.common.data.biz.CommandRecordService;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
import io.github.pnoker.common.data.entity.vo.CommandCallVO;
import io.github.pnoker.common.data.entity.vo.CommandRecordQueryVO;
import io.github.pnoker.common.enums.ResponseEnum;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* gRPC server implementation for the CommandRecord service.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CommandRecordServer extends CommandRecordApiGrpc.CommandRecordApiImplBase {
private final CommandRecordService commandRecordService;
@Override
public void call(GrpcCommandCallVO request, StreamObserver<GrpcRString> responseObserver) {
try {
CommandCallVO vo = new CommandCallVO();
vo.setDeviceId(request.getDeviceId());
vo.setCommandId(request.getCommandId());
vo.setParamValues(request.getParamValuesMap());
String recordId = commandRecordService.call(request.getTenantId(), vo);
responseObserver.onNext(GrpcRString.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(true)
.setCode(ResponseEnum.OK.getCode())
.setMessage(ResponseEnum.OK.getText())
.build())
.setData(recordId)
.build());
responseObserver.onCompleted();
} catch (Exception e) {
log.error("CommandRecordServer.call failed", e);
responseObserver.onNext(GrpcRString.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(false)
.setCode(ResponseEnum.FAILURE.getCode())
.setMessage(e.getMessage())
.build())
.build());
responseObserver.onCompleted();
}
}
@Override
public void getByRecordId(GrpcStringQuery request, StreamObserver<GrpcRCommandRecordDTO> responseObserver) {
try {
CommandRecordDO recordDO = commandRecordService.getByRecordId(request.getValue());
GrpcRCommandRecordDTO.Builder response = GrpcRCommandRecordDTO.newBuilder();
if (Objects.nonNull(recordDO)) {
response.setResult(GrpcR.newBuilder()
.setOk(true)
.setCode(ResponseEnum.OK.getCode())
.setMessage(ResponseEnum.OK.getText())
.build());
response.setData(toGrpcDTO(recordDO));
} else {
response.setResult(GrpcR.newBuilder()
.setOk(false)
.setCode(ResponseEnum.NO_RESOURCE.getCode())
.setMessage(ResponseEnum.NO_RESOURCE.getText())
.build());
}
responseObserver.onNext(response.build());
responseObserver.onCompleted();
} catch (Exception e) {
log.error("CommandRecordServer.getByRecordId failed", e);
responseObserver.onNext(GrpcRCommandRecordDTO.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(false)
.setCode(ResponseEnum.FAILURE.getCode())
.setMessage(e.getMessage())
.build())
.build());
responseObserver.onCompleted();
}
}
@Override
public void list(GrpcCommandRecordQuery request, StreamObserver<GrpcRPageCommandRecordDTO> responseObserver) {
try {
CommandRecordQueryVO queryVO = new CommandRecordQueryVO();
queryVO.setDeviceId(request.getDeviceId() != 0 ? request.getDeviceId() : null);
queryVO.setCommandId(request.getCommandId() != 0 ? request.getCommandId() : null);
queryVO.setStatus(Objects.isNull(request.getStatus()) || request.getStatus().isEmpty() ? null : request.getStatus());
queryVO.setPage(Math.toIntExact(request.getPage().getCurrent()));
queryVO.setSize(Math.toIntExact(request.getPage().getSize()));
Page<CommandRecordDO> page = commandRecordService.list(request.getTenantId(), queryVO);
GrpcPageCommandRecordDTO.Builder pageDataBuilder = GrpcPageCommandRecordDTO.newBuilder()
.setPage(GrpcPage.newBuilder()
.setCurrent(page.getCurrent())
.setSize(page.getSize())
.setTotal(page.getTotal())
.setPages(page.getPages())
.build());
page.getRecords().forEach(record -> pageDataBuilder.addData(toGrpcDTO(record)));
responseObserver.onNext(GrpcRPageCommandRecordDTO.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(true)
.setCode(ResponseEnum.OK.getCode())
.setMessage(ResponseEnum.OK.getText())
.build())
.setData(pageDataBuilder.build())
.build());
responseObserver.onCompleted();
} catch (Exception e) {
log.error("CommandRecordServer.list failed", e);
responseObserver.onNext(GrpcRPageCommandRecordDTO.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(false)
.setCode(ResponseEnum.FAILURE.getCode())
.setMessage(e.getMessage())
.build())
.build());
responseObserver.onCompleted();
}
}
private GrpcCommandRecordDTO toGrpcDTO(CommandRecordDO recordDO) {
return GrpcCommandRecordDTO.newBuilder()
.setId(Objects.nonNull(recordDO.getId()) ? recordDO.getId() : 0)
.setRecordId(Objects.nonNull(recordDO.getRecordId()) ? recordDO.getRecordId() : "")
.setTenantId(Objects.nonNull(recordDO.getTenantId()) ? recordDO.getTenantId() : 0)
.setDeviceId(Objects.nonNull(recordDO.getDeviceId()) ? recordDO.getDeviceId() : 0)
.setCommandId(Objects.nonNull(recordDO.getCommandId()) ? recordDO.getCommandId() : 0)
.setCommandCode(Objects.nonNull(recordDO.getCommandCode()) ? recordDO.getCommandCode() : "")
.setStatus(Objects.nonNull(recordDO.getStatus()) ? recordDO.getStatus() : "")
.setErrorCode(Objects.nonNull(recordDO.getErrorCode()) ? recordDO.getErrorCode() : "")
.setErrorMessage(Objects.nonNull(recordDO.getErrorMessage()) ? recordDO.getErrorMessage() : "")
.setSource(Objects.nonNull(recordDO.getSource()) ? recordDO.getSource() : "")
.setSourceUserId(Objects.nonNull(recordDO.getSourceUserId()) ? recordDO.getSourceUserId() : 0)
.setSchemaVersion(Objects.nonNull(recordDO.getSchemaVersion()) ? recordDO.getSchemaVersion() : 0)
.build();
}
}
@@ -0,0 +1,185 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.grpc.server;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.api.center.data.EventReportApiGrpc;
import io.github.pnoker.api.center.data.GrpcEventRecordDTO;
import io.github.pnoker.api.center.data.GrpcEventRecordQuery;
import io.github.pnoker.api.center.data.GrpcEventReportVO;
import io.github.pnoker.api.center.data.GrpcPageEventRecordDTO;
import io.github.pnoker.api.center.data.GrpcREventRecordDTO;
import io.github.pnoker.api.center.data.GrpcRPageEventRecordDTO;
import io.github.pnoker.api.center.data.GrpcRString;
import io.github.pnoker.api.center.data.GrpcStringQuery;
import io.github.pnoker.api.common.GrpcPage;
import io.github.pnoker.api.common.GrpcR;
import io.github.pnoker.common.data.biz.EventReportService;
import io.github.pnoker.common.data.entity.model.EventRecordDO;
import io.github.pnoker.common.data.entity.vo.EventRecordQueryVO;
import io.github.pnoker.common.data.entity.vo.EventReportVO;
import io.github.pnoker.common.enums.ResponseEnum;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* gRPC server implementation for the EventReport service.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class EventReportServer extends EventReportApiGrpc.EventReportApiImplBase {
private final EventReportService eventReportService;
@Override
public void report(GrpcEventReportVO request, StreamObserver<GrpcRString> responseObserver) {
try {
EventReportVO vo = new EventReportVO();
vo.setDeviceId(request.getDeviceId());
vo.setEventId(request.getEventId());
vo.setParamValues(request.getParamValuesMap());
vo.setMessage(request.getMessage());
String recordId = eventReportService.report(request.getTenantId(), vo);
responseObserver.onNext(GrpcRString.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(true)
.setCode(ResponseEnum.OK.getCode())
.setMessage(ResponseEnum.OK.getText())
.build())
.setData(recordId)
.build());
responseObserver.onCompleted();
} catch (Exception e) {
log.error("EventReportServer.report failed", e);
responseObserver.onNext(GrpcRString.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(false)
.setCode(ResponseEnum.FAILURE.getCode())
.setMessage(e.getMessage())
.build())
.build());
responseObserver.onCompleted();
}
}
@Override
public void getByRecordId(GrpcStringQuery request, StreamObserver<GrpcREventRecordDTO> responseObserver) {
try {
EventRecordDO recordDO = eventReportService.getByRecordId(request.getValue());
GrpcREventRecordDTO.Builder response = GrpcREventRecordDTO.newBuilder();
if (Objects.nonNull(recordDO)) {
response.setResult(GrpcR.newBuilder()
.setOk(true)
.setCode(ResponseEnum.OK.getCode())
.setMessage(ResponseEnum.OK.getText())
.build());
response.setData(toGrpcDTO(recordDO));
} else {
response.setResult(GrpcR.newBuilder()
.setOk(false)
.setCode(ResponseEnum.NO_RESOURCE.getCode())
.setMessage(ResponseEnum.NO_RESOURCE.getText())
.build());
}
responseObserver.onNext(response.build());
responseObserver.onCompleted();
} catch (Exception e) {
log.error("EventReportServer.getByRecordId failed", e);
responseObserver.onNext(GrpcREventRecordDTO.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(false)
.setCode(ResponseEnum.FAILURE.getCode())
.setMessage(e.getMessage())
.build())
.build());
responseObserver.onCompleted();
}
}
@Override
public void list(GrpcEventRecordQuery request, StreamObserver<GrpcRPageEventRecordDTO> responseObserver) {
try {
EventRecordQueryVO queryVO = new EventRecordQueryVO();
queryVO.setDeviceId(request.getDeviceId() != 0 ? request.getDeviceId() : null);
queryVO.setEventId(request.getEventId() != 0 ? request.getEventId() : null);
if (request.getEventTypeFlag() != 0) {
queryVO.setEventTypeFlag((byte) request.getEventTypeFlag());
}
queryVO.setPage(Math.toIntExact(request.getPage().getCurrent()));
queryVO.setSize(Math.toIntExact(request.getPage().getSize()));
Page<EventRecordDO> page = eventReportService.list(request.getTenantId(), queryVO);
GrpcPageEventRecordDTO.Builder pageDataBuilder = GrpcPageEventRecordDTO.newBuilder()
.setPage(GrpcPage.newBuilder()
.setCurrent(page.getCurrent())
.setSize(page.getSize())
.setTotal(page.getTotal())
.setPages(page.getPages())
.build());
page.getRecords().forEach(record -> pageDataBuilder.addData(toGrpcDTO(record)));
responseObserver.onNext(GrpcRPageEventRecordDTO.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(true)
.setCode(ResponseEnum.OK.getCode())
.setMessage(ResponseEnum.OK.getText())
.build())
.setData(pageDataBuilder.build())
.build());
responseObserver.onCompleted();
} catch (Exception e) {
log.error("EventReportServer.list failed", e);
responseObserver.onNext(GrpcRPageEventRecordDTO.newBuilder()
.setResult(GrpcR.newBuilder()
.setOk(false)
.setCode(ResponseEnum.FAILURE.getCode())
.setMessage(e.getMessage())
.build())
.build());
responseObserver.onCompleted();
}
}
private GrpcEventRecordDTO toGrpcDTO(EventRecordDO recordDO) {
return GrpcEventRecordDTO.newBuilder()
.setId(Objects.nonNull(recordDO.getId()) ? recordDO.getId() : 0)
.setRecordId(Objects.nonNull(recordDO.getRecordId()) ? recordDO.getRecordId() : "")
.setTenantId(Objects.nonNull(recordDO.getTenantId()) ? recordDO.getTenantId() : 0)
.setDeviceId(Objects.nonNull(recordDO.getDeviceId()) ? recordDO.getDeviceId() : 0)
.setEventId(Objects.nonNull(recordDO.getEventId()) ? recordDO.getEventId() : 0)
.setEventCode(Objects.nonNull(recordDO.getEventCode()) ? recordDO.getEventCode() : "")
.setEventTypeFlag(Objects.nonNull(recordDO.getEventTypeFlag()) ? recordDO.getEventTypeFlag() : 0)
.setEventLevelFlag(Objects.nonNull(recordDO.getEventLevelFlag()) ? recordDO.getEventLevelFlag() : 0)
.setMessage(Objects.nonNull(recordDO.getMessage()) ? recordDO.getMessage() : "")
.setAcknowledgeFlag(Objects.nonNull(recordDO.getAcknowledgeFlag()) ? recordDO.getAcknowledgeFlag() : 0)
.setSchemaVersion(Objects.nonNull(recordDO.getSchemaVersion()) ? recordDO.getSchemaVersion() : 0)
.build();
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
import org.apache.ibatis.annotations.Mapper;
/**
* MyBatis-Plus mapper for dc3_command_record table.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Mapper
public interface CommandRecordMapper extends BaseMapper<CommandRecordDO> {
}
@@ -0,0 +1,33 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import io.github.pnoker.common.data.entity.model.EventRecordDO;
import org.apache.ibatis.annotations.Mapper;
/**
* MyBatis-Plus mapper for dc3_event_record table.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Mapper
public interface EventRecordMapper extends BaseMapper<EventRecordDO> {
}
@@ -0,0 +1,75 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.rabbit;
import com.rabbitmq.client.Channel;
import io.github.pnoker.common.data.dal.CommandRecordManager;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
import io.github.pnoker.common.enums.PointCommandStatusEnum;
import io.github.pnoker.common.utils.RabbitAckUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* RabbitMQ receiver for custom command messages rejected into the dead letter exchange.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CommandDeadReceiver {
private final CommandRecordManager commandRecordManager;
@RabbitHandler
@RabbitListener(queues = "#{commandDeadQueue.name}")
public void onDeadLetter(Channel channel, Message message) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
String correlationId = message.getMessageProperties().getCorrelationId();
if (Objects.nonNull(correlationId)) {
CommandRecordDO recordDO = commandRecordManager.lambdaQuery()
.eq(CommandRecordDO::getRecordId, correlationId)
.one();
if (Objects.nonNull(recordDO)) {
recordDO.setStatus(PointCommandStatusEnum.DEAD.getCode());
recordDO.setErrorCode("DLX");
recordDO.setErrorMessage("Message rejected to dead letter queue");
recordDO.setFinishedAt(LocalDateTime.now());
commandRecordManager.updateById(recordDO);
log.info("Marked dead command record: recordId={}", correlationId);
}
}
RabbitAckUtil.ack(channel, deliveryTag);
} catch (Exception e) {
log.error("Command dead letter processing failed", e);
RabbitAckUtil.nack(channel, deliveryTag, true);
}
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.data.rabbit;
import com.rabbitmq.client.Channel;
import io.github.pnoker.common.data.dal.CommandRecordManager;
import io.github.pnoker.common.data.entity.model.CommandRecordDO;
import io.github.pnoker.common.entity.dto.CommandCallResultDTO;
import io.github.pnoker.common.utils.RabbitAckUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Objects;
/**
* RabbitMQ receiver for custom command call result receipts sent by drivers.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CommandResultReceiver {
private final CommandRecordManager commandRecordManager;
@RabbitHandler
@RabbitListener(queues = "#{commandResultQueue.name}")
public void onResult(Channel channel, Message message, CommandCallResultDTO resultDTO) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
if (Objects.isNull(resultDTO) || Objects.isNull(resultDTO.recordId())) {
RabbitAckUtil.reject(channel, deliveryTag);
return;
}
log.info("Receive command result: recordId={}, status={}", resultDTO.recordId(), resultDTO.status());
CommandRecordDO recordDO = commandRecordManager.lambdaQuery()
.eq(CommandRecordDO::getRecordId, resultDTO.recordId())
.one();
if (Objects.nonNull(recordDO)) {
recordDO.setStatus(resultDTO.status());
recordDO.setErrorCode(resultDTO.errorCode());
recordDO.setErrorMessage(resultDTO.errorMessage());
if (Objects.nonNull(resultDTO.resultValues())) {
recordDO.setResultValues(resultDTO.resultValues().toString());
}
if (Objects.nonNull(resultDTO.finishedAt())) {
recordDO.setFinishedAt(LocalDateTime.ofInstant(resultDTO.finishedAt(), ZoneId.systemDefault()));
} else {
recordDO.setFinishedAt(LocalDateTime.now());
}
commandRecordManager.updateById(recordDO);
log.info("Updated command record status: recordId={}, status={}", resultDTO.recordId(), resultDTO.status());
} else {
log.warn("Command record not found for result: recordId={}", resultDTO.recordId());
}
RabbitAckUtil.ack(channel, deliveryTag);
} catch (Exception e) {
log.error("Command result processing failed, deliveryTag={}", deliveryTag, e);
RabbitAckUtil.nack(channel, deliveryTag, true);
}
}
}
@@ -17,6 +17,7 @@
package io.github.pnoker.common.data.validator;
import io.github.pnoker.common.entity.ext.PointExt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -43,7 +44,7 @@ public class PointCommandValidator {
* @param pointExt point extension JSON (may contain constraints for future use)
* @throws IllegalArgumentException if the value fails validation
*/
public void validateWriteValue(String value, String pointExt) {
public void validateWriteValue(String value, PointExt pointExt) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Write value must not be blank");
}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2016-present the IoT DC3 original author or authors.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.github.pnoker.common.data.mapper.CommandRecordMapper">
</mapper>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2016-present the IoT DC3 original author or authors.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.github.pnoker.common.data.mapper.EventRecordMapper">
</mapper>
@@ -0,0 +1,78 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.entity.dto;
import java.time.Instant;
import java.util.Map;
/**
* Custom command call dispatch DTO sent via RabbitMQ to the driver.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
public record CommandCallDTO(
String recordId,
Long tenantId,
Long deviceId,
Long commandId,
String commandCode,
Map<String, String> paramValues,
String source,
Long sourceUserId,
Instant occurredAt,
Instant expireAt,
int schemaVersion
) {
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String recordId;
private Long tenantId;
private Long deviceId;
private Long commandId;
private String commandCode;
private Map<String, String> paramValues;
private String source;
private Long sourceUserId;
private Instant occurredAt;
private Instant expireAt;
private int schemaVersion;
public Builder recordId(String recordId) { this.recordId = recordId; return this; }
public Builder tenantId(Long tenantId) { this.tenantId = tenantId; return this; }
public Builder deviceId(Long deviceId) { this.deviceId = deviceId; return this; }
public Builder commandId(Long commandId) { this.commandId = commandId; return this; }
public Builder commandCode(String commandCode) { this.commandCode = commandCode; return this; }
public Builder paramValues(Map<String, String> paramValues) { this.paramValues = paramValues; return this; }
public Builder source(String source) { this.source = source; return this; }
public Builder sourceUserId(Long sourceUserId) { this.sourceUserId = sourceUserId; return this; }
public Builder occurredAt(Instant occurredAt) { this.occurredAt = occurredAt; return this; }
public Builder expireAt(Instant expireAt) { this.expireAt = expireAt; return this; }
public Builder schemaVersion(int schemaVersion) { this.schemaVersion = schemaVersion; return this; }
public CommandCallDTO build() {
return new CommandCallDTO(recordId, tenantId, deviceId, commandId, commandCode,
paramValues, source, sourceUserId, occurredAt, expireAt, schemaVersion);
}
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.entity.dto;
import java.time.Instant;
import java.util.Map;
/**
* Result receipt sent by the driver after executing a custom command.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
public record CommandCallResultDTO(
String recordId,
Long tenantId,
String status,
Map<String, String> resultValues,
String errorCode,
String errorMessage,
Instant finishedAt,
int schemaVersion
) {
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String recordId;
private Long tenantId;
private String status;
private Map<String, String> resultValues;
private String errorCode;
private String errorMessage;
private Instant finishedAt;
private int schemaVersion;
public Builder recordId(String recordId) { this.recordId = recordId; return this; }
public Builder tenantId(Long tenantId) { this.tenantId = tenantId; return this; }
public Builder status(String status) { this.status = status; return this; }
public Builder resultValues(Map<String, String> resultValues) { this.resultValues = resultValues; return this; }
public Builder errorCode(String errorCode) { this.errorCode = errorCode; return this; }
public Builder errorMessage(String errorMessage) { this.errorMessage = errorMessage; return this; }
public Builder finishedAt(Instant finishedAt) { this.finishedAt = finishedAt; return this; }
public Builder schemaVersion(int schemaVersion) { this.schemaVersion = schemaVersion; return this; }
public CommandCallResultDTO build() {
return new CommandCallResultDTO(recordId, tenantId, status, resultValues,
errorCode, errorMessage, finishedAt, schemaVersion);
}
}
}
@@ -0,0 +1,78 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.entity.dto;
import java.time.Instant;
import java.util.Map;
/**
* Event report DTO sent from driver to data center via RabbitMQ.
*
* @author pnoker
* @version 2026.5.23
* @since 2026.5.23
*/
public record EventReportDTO(
String recordId,
Long tenantId,
Long deviceId,
Long eventId,
String eventCode,
Byte eventTypeFlag,
Byte eventLevelFlag,
Map<String, String> paramValues,
String message,
Instant occurTime,
int schemaVersion
) {
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String recordId;
private Long tenantId;
private Long deviceId;
private Long eventId;
private String eventCode;
private Byte eventTypeFlag;
private Byte eventLevelFlag;
private Map<String, String> paramValues;
private String message;
private Instant occurTime;
private int schemaVersion;
public Builder recordId(String recordId) { this.recordId = recordId; return this; }
public Builder tenantId(Long tenantId) { this.tenantId = tenantId; return this; }
public Builder deviceId(Long deviceId) { this.deviceId = deviceId; return this; }
public Builder eventId(Long eventId) { this.eventId = eventId; return this; }
public Builder eventCode(String eventCode) { this.eventCode = eventCode; return this; }
public Builder eventTypeFlag(Byte eventTypeFlag) { this.eventTypeFlag = eventTypeFlag; return this; }
public Builder eventLevelFlag(Byte eventLevelFlag) { this.eventLevelFlag = eventLevelFlag; return this; }
public Builder paramValues(Map<String, String> paramValues) { this.paramValues = paramValues; return this; }
public Builder message(String message) { this.message = message; return this; }
public Builder occurTime(Instant occurTime) { this.occurTime = occurTime; return this; }
public Builder schemaVersion(int schemaVersion) { this.schemaVersion = schemaVersion; return this; }
public EventReportDTO build() {
return new EventReportDTO(recordId, tenantId, deviceId, eventId, eventCode,
eventTypeFlag, eventLevelFlag, paramValues, message, occurTime, schemaVersion);
}
}
}
@@ -123,4 +123,24 @@ public class ExchangeConfig {
return new TopicExchange(RabbitConstant.TOPIC_EXCHANGE_STATE_TIMEOUT_CHECK, true, false);
}
@Bean
TopicExchange commandExchange() {
return new TopicExchange(RabbitConstant.TOPIC_EXCHANGE_COMMAND, true, false);
}
@Bean
TopicExchange commandResultExchange() {
return new TopicExchange(RabbitConstant.TOPIC_EXCHANGE_COMMAND_RESULT, true, false);
}
@Bean
TopicExchange commandDeadExchange() {
return new TopicExchange(RabbitConstant.TOPIC_EXCHANGE_COMMAND_DEAD, true, false);
}
@Bean
TopicExchange eventExchange() {
return new TopicExchange(RabbitConstant.TOPIC_EXCHANGE_EVENT, true, false);
}
}