Răsfoiți Sursa

feat(ai-terminal): add device events endpoint with project_id isolation

- Add POST /api/v1/devices/{deviceId}/events endpoint to DeviceController
- Create AiDeviceEventDO, DeviceEventMapper, DeviceEventService in emoon-ai-device
- Add project_id column to ai_device_event table for multi-tenant audit isolation
- Ingest endpoint receives eventId/eventType/eventPayload/occurredAt/traceId
- Resolves: /devices/{deviceId}/events was in contract whitelist but had no implementation
WangKang 1 săptămână în urmă
părinte
comite
27a7024511

+ 40 - 0
emoon-infra/emoon-modules/emoon-ai/emoon-ai-device/src/main/java/com/emoon/ai/device/application/DeviceEventService.java

@@ -0,0 +1,40 @@
+package com.emoon.ai.device.application;
+
+import cn.hutool.json.JSONUtil;
+import com.emoon.ai.device.domain.AiDeviceEventDO;
+import com.emoon.ai.device.mapper.DeviceEventMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DeviceEventService {
+
+    private final DeviceEventMapper eventMapper;
+
+    @Transactional
+    public void ingest(String deviceId, String projectId, String eventId, String eventType,
+                        Map<String, Object> eventPayload, LocalDateTime occurredAt,
+                        String traceId) {
+        AiDeviceEventDO event = new AiDeviceEventDO();
+        event.setEventId(eventId);
+        event.setDeviceId(deviceId);
+        event.setProjectId(projectId);
+        event.setEventType(eventType);
+        event.setEventPayload(eventPayload != null && !eventPayload.isEmpty()
+                ? JSONUtil.toJsonStr(eventPayload) : null);
+        event.setOccurredAt(occurredAt != null ? occurredAt : LocalDateTime.now());
+        event.setTraceId(traceId);
+        event.setCreatedAt(LocalDateTime.now());
+        eventMapper.insert(event);
+
+        log.info("[DeviceEvent] ingested deviceId={} eventType={} eventId={} traceId={}",
+                deviceId, eventType, eventId, traceId);
+    }
+}

+ 23 - 0
emoon-infra/emoon-modules/emoon-ai/emoon-ai-device/src/main/java/com/emoon/ai/device/domain/AiDeviceEventDO.java

@@ -0,0 +1,23 @@
+package com.emoon.ai.device.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("ai_device_event")
+public class AiDeviceEventDO {
+
+    @TableId
+    private Long id;
+    private String eventId;
+    private String deviceId;
+    private String projectId;
+    private String eventType;
+    private String eventPayload;
+    private String traceId;
+    private LocalDateTime occurredAt;
+    private LocalDateTime createdAt;
+}

+ 9 - 0
emoon-infra/emoon-modules/emoon-ai/emoon-ai-device/src/main/java/com/emoon/ai/device/mapper/DeviceEventMapper.java

@@ -0,0 +1,9 @@
+package com.emoon.ai.device.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.emoon.ai.device.domain.AiDeviceEventDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DeviceEventMapper extends BaseMapper<AiDeviceEventDO> {
+}

+ 37 - 0
emoon-openplatform/src/main/java/com/emoon/openplatform/controller/v1/DeviceController.java

@@ -5,6 +5,7 @@ import com.emoon.ai.device.api.DeviceRegisterResult;
 import com.emoon.ai.device.api.DeviceRegistryFacade;
 import com.emoon.ai.device.api.SceneProfileResult;
 import com.emoon.ai.device.application.DeviceCommandService;
+import com.emoon.ai.device.application.DeviceEventService;
 import com.emoon.common.core.domain.R;
 import com.emoon.openplatform.auth.AuthContextHolder;
 import com.emoon.openplatform.auth.OpenPlatformAuthContext;
@@ -14,6 +15,9 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.Map;
 
 /**
@@ -27,6 +31,7 @@ public class DeviceController {
 
     private final DeviceRegistryFacade deviceRegistryFacade;
     private final DeviceCommandService deviceCommandService;
+    private final DeviceEventService deviceEventService;
 
     @PostMapping("/register")
     public R<DeviceRegisterResult> register(
@@ -104,4 +109,36 @@ public class DeviceController {
                 result != null ? result : Map.of());
         return R.ok(Map.of("acknowledged", "true"));
     }
+
+    @PostMapping("/{deviceId}/events")
+    public R<Map<String, String>> ingestEvent(
+            HttpServletRequest httpRequest,
+            @PathVariable String deviceId,
+            @RequestBody Map<String, Object> body) {
+        OpenPlatformAuthContext auth = AuthContextHolder.require(httpRequest);
+        if (deviceRegistryFacade.scene(String.valueOf(auth.getProjectId()), deviceId) == null)
+            return R.fail("DEVICE_NOT_FOUND");
+
+        String eventId = (String) body.getOrDefault("eventId",
+                "evt_" + java.util.UUID.randomUUID().toString().substring(0, 12));
+        String eventType = (String) body.getOrDefault("eventType", "CUSTOM");
+        String traceId = (String) body.getOrDefault("traceId", null);
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> eventPayload = (Map<String, Object>) body.get("eventPayload");
+
+        LocalDateTime occurredAt;
+        try {
+            String occurredAtStr = (String) body.get("occurredAt");
+            occurredAt = occurredAtStr != null
+                    ? LocalDateTime.parse(occurredAtStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
+                    : LocalDateTime.now();
+        } catch (DateTimeParseException e) {
+            occurredAt = LocalDateTime.now();
+        }
+
+        deviceEventService.ingest(deviceId, String.valueOf(auth.getProjectId()),
+                eventId, eventType, eventPayload, occurredAt, traceId);
+        return R.ok(Map.of("accepted", "true", "eventId", eventId));
+    }
 }

+ 3 - 1
script/sql/ai-terminal-mvp.sql

@@ -52,13 +52,15 @@ CREATE TABLE IF NOT EXISTS ai_device_event (
   id BIGINT PRIMARY KEY AUTO_INCREMENT,
   event_id VARCHAR(96) NOT NULL,
   device_id VARCHAR(64) NOT NULL,
+  project_id VARCHAR(64) NOT NULL COMMENT '项目标识,用于多租户隔离和审计',
   event_type VARCHAR(64) NOT NULL,
   event_payload JSON,
   trace_id VARCHAR(96),
   occurred_at DATETIME NOT NULL,
   created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
   UNIQUE KEY uk_device_event (device_id, event_id),
-  KEY idx_device_time (device_id, occurred_at)
+  KEY idx_device_time (device_id, occurred_at),
+  KEY idx_project (project_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- -----------------------------------------------------------