diff --git a/pom.xml b/pom.xml
index 1d059d2..6afd897 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
0.0.1-SNAPSHOT
- tuoheng-airport-ddd
+ tuoheng-ddd
jar
Tuoheng Airport DDD
diff --git a/src/main/java/com/tuoheng/airport/device/application/converter/DeviceDtoConverter.java b/src/main/java/com/tuoheng/airport/device/application/converter/DeviceDtoConverter.java
new file mode 100644
index 0000000..d422015
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/application/converter/DeviceDtoConverter.java
@@ -0,0 +1,66 @@
+package com.tuoheng.airport.device.application.converter;
+
+import com.tuoheng.airport.device.application.dto.DeviceCreateRequest;
+import com.tuoheng.airport.device.application.dto.DeviceResponse;
+import com.tuoheng.airport.device.domain.model.Device;
+import com.tuoheng.airport.device.domain.model.DeviceType;
+
+/**
+ * 设备DTO转换器(Application层)
+ * 负责 Application 层 DTO 和 Domain 层领域模型之间的转换
+ *
+ * @author tuoheng
+ */
+public class DeviceDtoConverter {
+
+ /**
+ * 创建请求DTO转领域模型
+ *
+ * @param request 创建请求DTO
+ * @return 领域模型
+ */
+ public static Device toDomain(DeviceCreateRequest request) {
+ if (request == null) {
+ return null;
+ }
+
+ DeviceType deviceType = DeviceType.fromCode(request.getDeviceType());
+
+ return Device.create(
+ request.getDeviceCode(),
+ request.getDeviceName(),
+ deviceType,
+ request.getManufacturer(),
+ request.getAirportId()
+ );
+ }
+
+ /**
+ * 领域模型转响应DTO
+ *
+ * @param device 领域模型
+ * @return 响应DTO
+ */
+ public static DeviceResponse toResponse(Device device) {
+ if (device == null) {
+ return null;
+ }
+
+ return DeviceResponse.builder()
+ .id(device.getId())
+ .deviceCode(device.getDeviceCode())
+ .deviceName(device.getDeviceName())
+ .deviceType(device.getDeviceType() != null ? device.getDeviceType().getCode() : null)
+ .deviceTypeDesc(device.getDeviceType() != null ? device.getDeviceType().getDescription() : null)
+ .status(device.getStatus() != null ? device.getStatus().getCode() : null)
+ .statusDesc(device.getStatus() != null ? device.getStatus().getDescription() : null)
+ .airportId(device.getAirportId())
+ .manufacturer(device.getManufacturer())
+ .firmwareVersion(device.getFirmwareVersion())
+ .createTime(device.getCreateTime())
+ .updateTime(device.getUpdateTime())
+ .remark(device.getRemark())
+ .canExecuteTask(device.canExecuteTask())
+ .build();
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/application/dto/DeviceCreateRequest.java b/src/main/java/com/tuoheng/airport/device/application/dto/DeviceCreateRequest.java
new file mode 100644
index 0000000..50571fa
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/application/dto/DeviceCreateRequest.java
@@ -0,0 +1,55 @@
+package com.tuoheng.airport.device.application.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 设备创建请求DTO(Application层)
+ * 用于接收前端创建设备的请求参数
+ *
+ * @author tuoheng
+ */
+@Data
+public class DeviceCreateRequest {
+
+ /**
+ * 设备编码(必填,唯一)
+ */
+ @NotBlank(message = "设备编码不能为空")
+ private String deviceCode;
+
+ /**
+ * 设备名称(必填)
+ */
+ @NotBlank(message = "设备名称不能为空")
+ private String deviceName;
+
+ /**
+ * 设备类型(必填)
+ * 1:多旋翼 2:固定翼 3:垂直起降 4:摄像头 5:喇叭 6:探照灯
+ */
+ @NotNull(message = "设备类型不能为空")
+ private Integer deviceType;
+
+ /**
+ * 制造商
+ */
+ private String manufacturer;
+
+ /**
+ * 所属机场ID
+ */
+ private Long airportId;
+
+ /**
+ * 固件版本
+ */
+ private String firmwareVersion;
+
+ /**
+ * 备注
+ */
+ private String remark;
+}
diff --git a/src/main/java/com/tuoheng/airport/device/application/dto/DeviceQueryRequest.java b/src/main/java/com/tuoheng/airport/device/application/dto/DeviceQueryRequest.java
new file mode 100644
index 0000000..ffd23b1
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/application/dto/DeviceQueryRequest.java
@@ -0,0 +1,43 @@
+package com.tuoheng.airport.device.application.dto;
+
+import lombok.Data;
+
+/**
+ * 设备查询请求DTO(Application层)
+ * 用于接收前端查询设备的请求参数
+ *
+ * @author tuoheng
+ */
+@Data
+public class DeviceQueryRequest {
+
+ /**
+ * 设备编码(模糊查询)
+ */
+ private String deviceCode;
+
+ /**
+ * 设备名称(模糊查询)
+ */
+ private String deviceName;
+
+ /**
+ * 设备类型
+ */
+ private Integer deviceType;
+
+ /**
+ * 设备状态
+ */
+ private Integer status;
+
+ /**
+ * 所属机场ID
+ */
+ private Long airportId;
+
+ /**
+ * 制造商
+ */
+ private String manufacturer;
+}
diff --git a/src/main/java/com/tuoheng/airport/device/application/dto/DeviceResponse.java b/src/main/java/com/tuoheng/airport/device/application/dto/DeviceResponse.java
new file mode 100644
index 0000000..82ae11d
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/application/dto/DeviceResponse.java
@@ -0,0 +1,94 @@
+package com.tuoheng.airport.device.application.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 设备响应DTO(Application层)
+ * 用于返回给前端的设备信息
+ *
+ * @author tuoheng
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeviceResponse {
+
+ /**
+ * 设备ID
+ */
+ private Long id;
+
+ /**
+ * 设备编码
+ */
+ private String deviceCode;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 设备类型代码
+ */
+ private Integer deviceType;
+
+ /**
+ * 设备类型描述
+ */
+ private String deviceTypeDesc;
+
+ /**
+ * 设备状态代码
+ */
+ private Integer status;
+
+ /**
+ * 设备状态描述
+ */
+ private String statusDesc;
+
+ /**
+ * 所属机场ID
+ */
+ private Long airportId;
+
+ /**
+ * 制造商
+ */
+ private String manufacturer;
+
+ /**
+ * 固件版本
+ */
+ private String firmwareVersion;
+
+ /**
+ * 创建时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime createTime;
+
+ /**
+ * 更新时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime updateTime;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 是否可以执行任务
+ */
+ private Boolean canExecuteTask;
+}
diff --git a/src/main/java/com/tuoheng/airport/device/application/dto/DeviceUpdateRequest.java b/src/main/java/com/tuoheng/airport/device/application/dto/DeviceUpdateRequest.java
new file mode 100644
index 0000000..8e832e8
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/application/dto/DeviceUpdateRequest.java
@@ -0,0 +1,46 @@
+package com.tuoheng.airport.device.application.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 设备更新请求DTO(Application层)
+ * 用于接收前端更新设备的请求参数
+ *
+ * @author tuoheng
+ */
+@Data
+public class DeviceUpdateRequest {
+
+ /**
+ * 设备ID(必填)
+ */
+ @NotNull(message = "设备ID不能为空")
+ private Long id;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 制造商
+ */
+ private String manufacturer;
+
+ /**
+ * 所属机场ID
+ */
+ private Long airportId;
+
+ /**
+ * 固件版本
+ */
+ private String firmwareVersion;
+
+ /**
+ * 备注
+ */
+ private String remark;
+}
diff --git a/src/main/java/com/tuoheng/airport/device/application/service/DeviceApplicationService.java b/src/main/java/com/tuoheng/airport/device/application/service/DeviceApplicationService.java
new file mode 100644
index 0000000..fc1f718
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/application/service/DeviceApplicationService.java
@@ -0,0 +1,127 @@
+package com.tuoheng.airport.device.application.service;
+
+import com.tuoheng.airport.device.application.dto.*;
+
+import java.util.List;
+
+/**
+ * 设备应用服务接口(Application层)
+ * 定义设备相关的用例(Use Cases)
+ * 协调领域模型完成业务操作
+ *
+ * @author tuoheng
+ */
+public interface DeviceApplicationService {
+
+ /**
+ * 创建设备
+ *
+ * @param request 创建请求
+ * @return 设备响应
+ */
+ DeviceResponse createDevice(DeviceCreateRequest request);
+
+ /**
+ * 更新设备信息
+ *
+ * @param request 更新请求
+ * @return 设备响应
+ */
+ DeviceResponse updateDevice(DeviceUpdateRequest request);
+
+ /**
+ * 根据ID查询设备
+ *
+ * @param id 设备ID
+ * @return 设备响应
+ */
+ DeviceResponse getDeviceById(Long id);
+
+ /**
+ * 根据设备编码查询设备
+ *
+ * @param deviceCode 设备编码
+ * @return 设备响应
+ */
+ DeviceResponse getDeviceByCode(String deviceCode);
+
+ /**
+ * 查询所有设备
+ *
+ * @return 设备列表
+ */
+ List getAllDevices();
+
+ /**
+ * 根据条件查询设备列表
+ *
+ * @param request 查询请求
+ * @return 设备列表
+ */
+ List queryDevices(DeviceQueryRequest request);
+
+ /**
+ * 根据机场ID查询设备列表
+ *
+ * @param airportId 机场ID
+ * @return 设备列表
+ */
+ List getDevicesByAirportId(Long airportId);
+
+ /**
+ * 激活设备
+ *
+ * @param id 设备ID
+ * @return 设备响应
+ */
+ DeviceResponse activateDevice(Long id);
+
+ /**
+ * 停用设备
+ *
+ * @param id 设备ID
+ * @return 设备响应
+ */
+ DeviceResponse deactivateDevice(Long id);
+
+ /**
+ * 标记设备为故障状态
+ *
+ * @param id 设备ID
+ * @return 设备响应
+ */
+ DeviceResponse markDeviceAsFaulty(Long id);
+
+ /**
+ * 更新设备固件版本
+ *
+ * @param id 设备ID
+ * @param newVersion 新版本号
+ * @return 设备响应
+ */
+ DeviceResponse updateDeviceFirmware(Long id, String newVersion);
+
+ /**
+ * 分配设备到机场
+ *
+ * @param id 设备ID
+ * @param airportId 机场ID
+ * @return 设备响应
+ */
+ DeviceResponse assignDeviceToAirport(Long id, Long airportId);
+
+ /**
+ * 删除设备
+ *
+ * @param id 设备ID
+ */
+ void deleteDevice(Long id);
+
+ /**
+ * 统计机场下的设备数量
+ *
+ * @param airportId 机场ID
+ * @return 设备数量
+ */
+ long countDevicesByAirportId(Long airportId);
+}
diff --git a/src/main/java/com/tuoheng/airport/device/application/service/impl/DeviceApplicationServiceImpl.java b/src/main/java/com/tuoheng/airport/device/application/service/impl/DeviceApplicationServiceImpl.java
new file mode 100644
index 0000000..8e227d0
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/application/service/impl/DeviceApplicationServiceImpl.java
@@ -0,0 +1,267 @@
+package com.tuoheng.airport.device.application.service.impl;
+
+import com.tuoheng.airport.device.application.converter.DeviceDtoConverter;
+import com.tuoheng.airport.device.application.dto.*;
+import com.tuoheng.airport.device.application.service.DeviceApplicationService;
+import com.tuoheng.airport.device.domain.model.Device;
+import com.tuoheng.airport.device.domain.model.DeviceStatus;
+import com.tuoheng.airport.device.domain.model.DeviceType;
+import com.tuoheng.airport.device.domain.service.DeviceDomainService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 设备应用服务实现类(Application层)
+ * 实现业务用例,协调领域服务完成业务操作
+ *
+ * 职责:
+ * 1. 接收 DTO,转换为领域模型
+ * 2. 调用 Domain Service 完成业务逻辑
+ * 3. 管理事务边界
+ * 4. 异常处理和日志记录
+ * 5. 将领域模型转换为 DTO 返回
+ *
+ * 注意:Application 层不直接调用 Repository,而是调用 Domain Service
+ *
+ * @author tuoheng
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DeviceApplicationServiceImpl implements DeviceApplicationService {
+
+ private final DeviceDomainService deviceDomainService;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public DeviceResponse createDevice(DeviceCreateRequest request) {
+ log.info("Application: 开始创建设备,设备编码: {}", request.getDeviceCode());
+
+ // 1. DTO 转领域模型(使用领域模型的工厂方法创建)
+ Device device = DeviceDtoConverter.toDomain(request);
+
+ // 2. 设置固件版本和备注
+ if (request.getFirmwareVersion() != null) {
+ device.setFirmwareVersion(request.getFirmwareVersion());
+ }
+ if (request.getRemark() != null) {
+ device.setRemark(request.getRemark());
+ }
+
+ // 3. 调用 Domain Service 注册设备(Domain Service 会处理业务规则)
+ Device savedDevice = deviceDomainService.registerDevice(device);
+
+ log.info("Application: 设备创建成功,设备ID: {}, 设备编码: {}",
+ savedDevice.getId(), savedDevice.getDeviceCode());
+
+ // 4. 领域模型转 DTO 返回
+ return DeviceDtoConverter.toResponse(savedDevice);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public DeviceResponse updateDevice(DeviceUpdateRequest request) {
+ log.info("Application: 开始更新设备,设备ID: {}", request.getId());
+
+ // 1. 通过 Domain Service 查询设备
+ Device device = deviceDomainService.getDeviceById(request.getId());
+
+ // 2. 更新设备信息
+ if (request.getDeviceName() != null) {
+ device.setDeviceName(request.getDeviceName());
+ }
+ if (request.getManufacturer() != null) {
+ device.setManufacturer(request.getManufacturer());
+ }
+ if (request.getFirmwareVersion() != null) {
+ device.setFirmwareVersion(request.getFirmwareVersion());
+ }
+ if (request.getRemark() != null) {
+ device.setRemark(request.getRemark());
+ }
+
+ // 3. 如果需要分配到机场,调用 Domain Service
+ if (request.getAirportId() != null && !request.getAirportId().equals(device.getAirportId())) {
+ device = deviceDomainService.assignToAirport(device.getId(), request.getAirportId());
+ }
+
+ log.info("Application: 设备更新成功,设备ID: {}", device.getId());
+
+ // 4. 领域模型转 DTO 返回
+ return DeviceDtoConverter.toResponse(device);
+ }
+
+ @Override
+ public DeviceResponse getDeviceById(Long id) {
+ log.info("Application: 查询设备,设备ID: {}", id);
+
+ // 调用 Domain Service 查询
+ Device device = deviceDomainService.getDeviceById(id);
+
+ return DeviceDtoConverter.toResponse(device);
+ }
+
+ @Override
+ public DeviceResponse getDeviceByCode(String deviceCode) {
+ log.info("Application: 查询设备,设备编码: {}", deviceCode);
+
+ // 调用 Domain Service 查询
+ Device device = deviceDomainService.getDeviceByCode(deviceCode);
+
+ return DeviceDtoConverter.toResponse(device);
+ }
+
+ @Override
+ public List getAllDevices() {
+ log.info("Application: 查询所有设备");
+
+ // 调用 Domain Service 查询所有激活状态的设备
+ List devices = deviceDomainService.getDevicesByStatus(DeviceStatus.ACTIVE);
+
+ return devices.stream()
+ .map(DeviceDtoConverter::toResponse)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List queryDevices(DeviceQueryRequest request) {
+ log.info("Application: 根据条件查询设备列表,查询条件: {}", request);
+
+ List devices;
+
+ // 根据不同条件调用 Domain Service
+ if (request.getAirportId() != null && request.getStatus() != null) {
+ // 按机场ID和状态查询
+ if (request.getStatus() == DeviceStatus.ACTIVE.getCode()) {
+ devices = deviceDomainService.getAvailableDevicesByAirport(request.getAirportId());
+ } else {
+ DeviceStatus status = DeviceStatus.fromCode(request.getStatus());
+ devices = deviceDomainService.getDevicesByStatus(status).stream()
+ .filter(d -> d.getAirportId() != null && d.getAirportId().equals(request.getAirportId()))
+ .collect(Collectors.toList());
+ }
+ } else if (request.getStatus() != null) {
+ // 按状态查询
+ DeviceStatus status = DeviceStatus.fromCode(request.getStatus());
+ devices = deviceDomainService.getDevicesByStatus(status);
+ } else if (request.getDeviceType() != null) {
+ // 按设备类型查询
+ DeviceType deviceType = DeviceType.fromCode(request.getDeviceType());
+ devices = deviceDomainService.getDevicesByType(deviceType);
+ } else {
+ // 查询所有激活状态的设备
+ devices = deviceDomainService.getDevicesByStatus(DeviceStatus.ACTIVE);
+ }
+
+ return devices.stream()
+ .map(DeviceDtoConverter::toResponse)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List getDevicesByAirportId(Long airportId) {
+ log.info("Application: 查询机场下的设备列表,机场ID: {}", airportId);
+
+ // 调用 Domain Service 查询机场下的可用设备
+ List devices = deviceDomainService.getAvailableDevicesByAirport(airportId);
+
+ return devices.stream()
+ .map(DeviceDtoConverter::toResponse)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public DeviceResponse activateDevice(Long id) {
+ log.info("Application: 激活设备,设备ID: {}", id);
+
+ // 调用 Domain Service 激活设备(Domain Service 会处理业务规则)
+ Device device = deviceDomainService.activateDevice(id);
+
+ log.info("Application: 设备激活成功,设备ID: {}, 当前状态: {}",
+ id, device.getStatus());
+
+ return DeviceDtoConverter.toResponse(device);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public DeviceResponse deactivateDevice(Long id) {
+ log.info("Application: 停用设备,设备ID: {}", id);
+
+ // 调用 Domain Service 停用设备(Domain Service 会处理业务规则)
+ Device device = deviceDomainService.deactivateDevice(id);
+
+ log.info("Application: 设备停用成功,设备ID: {}, 当前状态: {}",
+ id, device.getStatus());
+
+ return DeviceDtoConverter.toResponse(device);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public DeviceResponse markDeviceAsFaulty(Long id) {
+ log.info("Application: 标记设备为故障状态,设备ID: {}", id);
+
+ // 调用 Domain Service 标记设备故障
+ Device device = deviceDomainService.markDeviceAsFaulty(id, "系统标记");
+
+ log.info("Application: 设备已标记为故障状态,设备ID: {}", id);
+
+ return DeviceDtoConverter.toResponse(device);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public DeviceResponse updateDeviceFirmware(Long id, String newVersion) {
+ log.info("Application: 更新设备固件版本,设备ID: {}, 新版本: {}", id, newVersion);
+
+ // 调用 Domain Service 更新固件(Domain Service 会处理业务规则)
+ Device device = deviceDomainService.updateFirmware(id, newVersion);
+
+ log.info("Application: 设备固件版本更新成功,设备ID: {}, 新版本: {}", id, newVersion);
+
+ return DeviceDtoConverter.toResponse(device);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public DeviceResponse assignDeviceToAirport(Long id, Long airportId) {
+ log.info("Application: 分配设备到机场,设备ID: {}, 机场ID: {}", id, airportId);
+
+ // 调用 Domain Service 分配设备(Domain Service 会处理业务规则)
+ Device device = deviceDomainService.assignToAirport(id, airportId);
+
+ log.info("Application: 设备分配成功,设备ID: {}, 机场ID: {}", id, airportId);
+
+ return DeviceDtoConverter.toResponse(device);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteDevice(Long id) {
+ log.info("Application: 删除设备,设备ID: {}", id);
+
+ // 调用 Domain Service 删除设备(Domain Service 会处理业务规则)
+ deviceDomainService.deleteDevice(id);
+
+ log.info("Application: 设备删除成功,设备ID: {}", id);
+ }
+
+ @Override
+ public long countDevicesByAirportId(Long airportId) {
+ log.info("Application: 统计机场下的设备数量,机场ID: {}", airportId);
+
+ // 调用 Domain Service 统计
+ long count = deviceDomainService.countDevicesByAirport(airportId);
+
+ log.info("Application: 机场下的设备数量: {}, 机场ID: {}", count, airportId);
+
+ return count;
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/domain/model/Device.java b/src/main/java/com/tuoheng/airport/device/domain/model/Device.java
new file mode 100644
index 0000000..23ec0d4
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/domain/model/Device.java
@@ -0,0 +1,167 @@
+package com.tuoheng.airport.device.domain.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 设备领域模型(聚合根)
+ * 这是 domain 层的核心实体,包含业务逻辑和业务规则
+ *
+ * @author tuoheng
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Device {
+
+ /**
+ * 设备ID
+ */
+ private Long id;
+
+ /**
+ * 设备编码(唯一标识)
+ */
+ private String deviceCode;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 设备类型
+ */
+ private DeviceType deviceType;
+
+ /**
+ * 设备状态
+ */
+ private DeviceStatus status;
+
+ /**
+ * 所属机场ID
+ */
+ private Long airportId;
+
+ /**
+ * 制造商
+ */
+ private String manufacturer;
+
+ /**
+ * 固件版本
+ */
+ private String firmwareVersion;
+
+ /**
+ * 创建时间
+ */
+ private LocalDateTime createTime;
+
+ /**
+ * 更新时间
+ */
+ private LocalDateTime updateTime;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ // ==================== 业务方法 ====================
+
+ /**
+ * 激活设备
+ * 业务规则:只有离线或故障状态的设备才能被激活
+ */
+ public void activate() {
+ if (this.status == DeviceStatus.ACTIVE) {
+ throw new IllegalStateException("设备已经是激活状态,无需重复激活");
+ }
+ this.status = DeviceStatus.ACTIVE;
+ this.updateTime = LocalDateTime.now();
+ }
+
+ /**
+ * 停用设备
+ * 业务规则:只有激活状态的设备才能被停用
+ */
+ public void deactivate() {
+ if (this.status == DeviceStatus.INACTIVE) {
+ throw new IllegalStateException("设备已经是停用状态");
+ }
+ this.status = DeviceStatus.INACTIVE;
+ this.updateTime = LocalDateTime.now();
+ }
+
+ /**
+ * 标记设备为故障状态
+ */
+ public void markAsFaulty() {
+ this.status = DeviceStatus.FAULTY;
+ this.updateTime = LocalDateTime.now();
+ }
+
+ /**
+ * 更新固件版本
+ */
+ public void updateFirmware(String newVersion) {
+ if (newVersion == null || newVersion.trim().isEmpty()) {
+ throw new IllegalArgumentException("固件版本不能为空");
+ }
+ this.firmwareVersion = newVersion;
+ this.updateTime = LocalDateTime.now();
+ }
+
+ /**
+ * 分配到机场
+ */
+ public void assignToAirport(Long airportId) {
+ if (airportId == null || airportId <= 0) {
+ throw new IllegalArgumentException("机场ID无效");
+ }
+ this.airportId = airportId;
+ this.updateTime = LocalDateTime.now();
+ }
+
+ /**
+ * 验证设备是否可以执行任务
+ */
+ public boolean canExecuteTask() {
+ return this.status == DeviceStatus.ACTIVE && this.airportId != null;
+ }
+
+ /**
+ * 创建新设备(工厂方法)
+ */
+ public static Device create(String deviceCode, String deviceName, DeviceType deviceType,
+ String manufacturer, Long airportId) {
+ if (deviceCode == null || deviceCode.trim().isEmpty()) {
+ throw new IllegalArgumentException("设备编码不能为空");
+ }
+ if (deviceName == null || deviceName.trim().isEmpty()) {
+ throw new IllegalArgumentException("设备名称不能为空");
+ }
+ if (deviceType == null) {
+ throw new IllegalArgumentException("设备类型不能为空");
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ return Device.builder()
+ .deviceCode(deviceCode)
+ .deviceName(deviceName)
+ .deviceType(deviceType)
+ .status(DeviceStatus.INACTIVE) // 新设备默认为停用状态
+ .manufacturer(manufacturer)
+ .airportId(airportId)
+ .createTime(now)
+ .updateTime(now)
+ .build();
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/domain/model/DeviceStatus.java b/src/main/java/com/tuoheng/airport/device/domain/model/DeviceStatus.java
new file mode 100644
index 0000000..d79b146
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/domain/model/DeviceStatus.java
@@ -0,0 +1,52 @@
+package com.tuoheng.airport.device.domain.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备状态枚举(值对象)
+ *
+ * @author tuoheng
+ */
+@Getter
+@AllArgsConstructor
+public enum DeviceStatus {
+
+ /**
+ * 激活状态(可用)
+ */
+ ACTIVE(1, "激活"),
+
+ /**
+ * 停用状态(不可用)
+ */
+ INACTIVE(0, "停用"),
+
+ /**
+ * 故障状态
+ */
+ FAULTY(2, "故障"),
+
+ /**
+ * 维护中
+ */
+ MAINTENANCE(3, "维护中");
+
+ private final Integer code;
+ private final String description;
+
+ /**
+ * 根据code获取枚举
+ */
+ public static DeviceStatus fromCode(Integer code) {
+ if (code == null) {
+ return null;
+ }
+ for (DeviceStatus status : DeviceStatus.values()) {
+ if (status.getCode().equals(code)) {
+ return status;
+ }
+ }
+ throw new IllegalArgumentException("未知的设备状态: " + code);
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/domain/model/DeviceType.java b/src/main/java/com/tuoheng/airport/device/domain/model/DeviceType.java
new file mode 100644
index 0000000..f4e0e9c
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/domain/model/DeviceType.java
@@ -0,0 +1,62 @@
+package com.tuoheng.airport.device.domain.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备类型枚举(值对象)
+ *
+ * @author tuoheng
+ */
+@Getter
+@AllArgsConstructor
+public enum DeviceType {
+
+ /**
+ * 多旋翼无人机
+ */
+ MULTICOPTER(1, "多旋翼无人机"),
+
+ /**
+ * 固定翼无人机
+ */
+ FIXED_WING(2, "固定翼无人机"),
+
+ /**
+ * 垂直起降无人机
+ */
+ VTOL(3, "垂直起降无人机"),
+
+ /**
+ * 摄像头
+ */
+ CAMERA(4, "摄像头"),
+
+ /**
+ * 喇叭
+ */
+ MEGAPHONE(5, "喇叭"),
+
+ /**
+ * 探照灯
+ */
+ SEARCHLIGHT(6, "探照灯");
+
+ private final Integer code;
+ private final String description;
+
+ /**
+ * 根据code获取枚举
+ */
+ public static DeviceType fromCode(Integer code) {
+ if (code == null) {
+ return null;
+ }
+ for (DeviceType type : DeviceType.values()) {
+ if (type.getCode().equals(code)) {
+ return type;
+ }
+ }
+ throw new IllegalArgumentException("未知的设备类型: " + code);
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/domain/repository/DeviceRepository.java b/src/main/java/com/tuoheng/airport/device/domain/repository/DeviceRepository.java
new file mode 100644
index 0000000..b36422c
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/domain/repository/DeviceRepository.java
@@ -0,0 +1,105 @@
+package com.tuoheng.airport.device.domain.repository;
+
+import com.tuoheng.airport.device.domain.model.Device;
+import com.tuoheng.airport.device.domain.model.DeviceStatus;
+import com.tuoheng.airport.device.domain.model.DeviceType;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 设备仓储接口(Domain层定义,Infrastructure层实现)
+ * 这是领域层和基础设施层之间的契约
+ *
+ * @author tuoheng
+ */
+public interface DeviceRepository {
+
+ /**
+ * 保存设备(新增或更新)
+ *
+ * @param device 设备领域模型
+ * @return 保存后的设备
+ */
+ Device save(Device device);
+
+ /**
+ * 根据ID查询设备
+ *
+ * @param id 设备ID
+ * @return 设备领域模型
+ */
+ Optional findById(Long id);
+
+ /**
+ * 根据设备编码查询设备
+ *
+ * @param deviceCode 设备编码
+ * @return 设备领域模型
+ */
+ Optional findByDeviceCode(String deviceCode);
+
+ /**
+ * 查询所有设备
+ *
+ * @return 设备列表
+ */
+ List findAll();
+
+ /**
+ * 根据机场ID查询设备列表
+ *
+ * @param airportId 机场ID
+ * @return 设备列表
+ */
+ List findByAirportId(Long airportId);
+
+ /**
+ * 根据设备状态查询设备列表
+ *
+ * @param status 设备状态
+ * @return 设备列表
+ */
+ List findByStatus(DeviceStatus status);
+
+ /**
+ * 根据设备类型查询设备列表
+ *
+ * @param deviceType 设备类型
+ * @return 设备列表
+ */
+ List findByDeviceType(DeviceType deviceType);
+
+ /**
+ * 根据机场ID和状态查询设备列表
+ *
+ * @param airportId 机场ID
+ * @param status 设备状态
+ * @return 设备列表
+ */
+ List findByAirportIdAndStatus(Long airportId, DeviceStatus status);
+
+ /**
+ * 删除设备
+ *
+ * @param id 设备ID
+ * @return 是否删除成功
+ */
+ boolean deleteById(Long id);
+
+ /**
+ * 检查设备编码是否存在
+ *
+ * @param deviceCode 设备编码
+ * @return 是否存在
+ */
+ boolean existsByDeviceCode(String deviceCode);
+
+ /**
+ * 统计机场下的设备数量
+ *
+ * @param airportId 机场ID
+ * @return 设备数量
+ */
+ long countByAirportId(Long airportId);
+}
diff --git a/src/main/java/com/tuoheng/airport/device/domain/service/DeviceDomainService.java b/src/main/java/com/tuoheng/airport/device/domain/service/DeviceDomainService.java
new file mode 100644
index 0000000..e1b6e30
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/domain/service/DeviceDomainService.java
@@ -0,0 +1,177 @@
+package com.tuoheng.airport.device.domain.service;
+
+import com.tuoheng.airport.device.domain.model.Device;
+import com.tuoheng.airport.device.domain.model.DeviceStatus;
+import com.tuoheng.airport.device.domain.model.DeviceType;
+
+import java.util.List;
+
+/**
+ * 设备领域服务接口(Domain层)
+ * 封装设备相关的业务逻辑和业务规则
+ *
+ * Domain Service 的职责:
+ * 1. 封装复杂的业务逻辑(跨多个聚合根的操作)
+ * 2. 协调领域模型和仓储
+ * 3. 保证业务规则的一致性
+ * 4. 不包含事务管理(事务由 Application 层管理)
+ *
+ * @author tuoheng
+ */
+public interface DeviceDomainService {
+
+ /**
+ * 注册新设备
+ * 业务规则:
+ * 1. 设备编码不能重复
+ * 2. 新设备默认为停用状态
+ * 3. 必须指定设备类型
+ *
+ * @param device 设备领域模型
+ * @return 注册后的设备
+ */
+ Device registerDevice(Device device);
+
+ /**
+ * 激活设备
+ * 业务规则:
+ * 1. 只有停用或故障状态的设备才能激活
+ * 2. 设备必须已分配到机场
+ * 3. 设备固件版本不能为空
+ *
+ * @param deviceId 设备ID
+ * @return 激活后的设备
+ */
+ Device activateDevice(Long deviceId);
+
+ /**
+ * 停用设备
+ * 业务规则:
+ * 1. 只有激活状态的设备才能停用
+ * 2. 停用前需要检查设备是否正在执行任务
+ *
+ * @param deviceId 设备ID
+ * @return 停用后的设备
+ */
+ Device deactivateDevice(Long deviceId);
+
+ /**
+ * 标记设备为故障状态
+ * 业务规则:
+ * 1. 任何状态的设备都可以标记为故障
+ * 2. 故障设备自动停止执行任务
+ *
+ * @param deviceId 设备ID
+ * @param faultReason 故障原因
+ * @return 标记后的设备
+ */
+ Device markDeviceAsFaulty(Long deviceId, String faultReason);
+
+ /**
+ * 更新设备固件版本
+ * 业务规则:
+ * 1. 只有停用状态的设备才能更新固件
+ * 2. 新版本号必须大于当前版本号
+ * 3. 更新固件后需要重新激活设备
+ *
+ * @param deviceId 设备ID
+ * @param newVersion 新固件版本
+ * @return 更新后的设备
+ */
+ Device updateFirmware(Long deviceId, String newVersion);
+
+ /**
+ * 分配设备到机场
+ * 业务规则:
+ * 1. 设备必须是停用状态才能分配
+ * 2. 机场必须存在且状态正常
+ * 3. 检查机场的设备容量限制
+ *
+ * @param deviceId 设备ID
+ * @param airportId 机场ID
+ * @return 分配后的设备
+ */
+ Device assignToAirport(Long deviceId, Long airportId);
+
+ /**
+ * 从机场移除设备
+ * 业务规则:
+ * 1. 设备必须是停用状态
+ * 2. 设备不能有未完成的任务
+ *
+ * @param deviceId 设备ID
+ * @return 移除后的设备
+ */
+ Device removeFromAirport(Long deviceId);
+
+ /**
+ * 查询设备详情
+ *
+ * @param deviceId 设备ID
+ * @return 设备领域模型
+ */
+ Device getDeviceById(Long deviceId);
+
+ /**
+ * 根据设备编码查询设备
+ *
+ * @param deviceCode 设备编码
+ * @return 设备领域模型
+ */
+ Device getDeviceByCode(String deviceCode);
+
+ /**
+ * 查询机场下的可用设备列表
+ * 业务规则:只返回激活状态的设备
+ *
+ * @param airportId 机场ID
+ * @return 可用设备列表
+ */
+ List getAvailableDevicesByAirport(Long airportId);
+
+ /**
+ * 查询指定类型的设备列表
+ *
+ * @param deviceType 设备类型
+ * @return 设备列表
+ */
+ List getDevicesByType(DeviceType deviceType);
+
+ /**
+ * 查询指定状态的设备列表
+ *
+ * @param status 设备状态
+ * @return 设备列表
+ */
+ List getDevicesByStatus(DeviceStatus status);
+
+ /**
+ * 删除设备
+ * 业务规则:
+ * 1. 只能删除停用状态的设备
+ * 2. 设备不能有历史任务记录(或者只是逻辑删除)
+ *
+ * @param deviceId 设备ID
+ */
+ void deleteDevice(Long deviceId);
+
+ /**
+ * 检查设备是否可以执行任务
+ * 业务规则:
+ * 1. 设备必须是激活状态
+ * 2. 设备必须已分配到机场
+ * 3. 设备固件版本必须是最新的
+ *
+ * @param deviceId 设备ID
+ * @return 是否可以执行任务
+ */
+ boolean canExecuteTask(Long deviceId);
+
+ /**
+ * 统计机场下的设备数量
+ *
+ * @param airportId 机场ID
+ * @return 设备数量
+ */
+ long countDevicesByAirport(Long airportId);
+}
diff --git a/src/main/java/com/tuoheng/airport/device/domain/service/impl/DeviceDomainServiceImpl.java b/src/main/java/com/tuoheng/airport/device/domain/service/impl/DeviceDomainServiceImpl.java
new file mode 100644
index 0000000..8396916
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/domain/service/impl/DeviceDomainServiceImpl.java
@@ -0,0 +1,352 @@
+package com.tuoheng.airport.device.domain.service.impl;
+
+import com.tuoheng.airport.device.domain.model.Device;
+import com.tuoheng.airport.device.domain.model.DeviceStatus;
+import com.tuoheng.airport.device.domain.model.DeviceType;
+import com.tuoheng.airport.device.domain.repository.DeviceRepository;
+import com.tuoheng.airport.device.domain.service.DeviceDomainService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 设备领域服务实现类(Domain层)
+ * 封装设备相关的核心业务逻辑
+ *
+ * 职责:
+ * 1. 实现业务规则和业务逻辑
+ * 2. 调用 Repository 进行数据操作
+ * 3. 协调领域模型完成复杂业务
+ * 4. 不处理事务(事务由 Application 层管理)
+ *
+ * @author tuoheng
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DeviceDomainServiceImpl implements DeviceDomainService {
+
+ private final DeviceRepository deviceRepository;
+
+ @Override
+ public Device registerDevice(Device device) {
+ log.info("Domain: 开始注册设备,设备编码: {}", device.getDeviceCode());
+
+ // 业务规则1:检查设备编码是否已存在
+ if (deviceRepository.existsByDeviceCode(device.getDeviceCode())) {
+ throw new IllegalArgumentException("设备编码已存在: " + device.getDeviceCode());
+ }
+
+ // 业务规则2:新设备默认为停用状态(在 Device.create() 工厂方法中已处理)
+ // 业务规则3:必须指定设备类型(在 Device.create() 工厂方法中已验证)
+
+ // 持久化设备
+ Device savedDevice = deviceRepository.save(device);
+
+ log.info("Domain: 设备注册成功,设备ID: {}, 设备编码: {}",
+ savedDevice.getId(), savedDevice.getDeviceCode());
+
+ return savedDevice;
+ }
+
+ @Override
+ public Device activateDevice(Long deviceId) {
+ log.info("Domain: 开始激活设备,设备ID: {}", deviceId);
+
+ // 查询设备
+ Device device = deviceRepository.findById(deviceId)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,ID: " + deviceId));
+
+ // 业务规则1:只有停用或故障状态的设备才能激活(在 Device.activate() 中验证)
+
+ // 业务规则2:设备必须已分配到机场
+ if (device.getAirportId() == null) {
+ throw new IllegalStateException("设备未分配到机场,无法激活");
+ }
+
+ // 业务规则3:设备固件版本不能为空
+ if (device.getFirmwareVersion() == null || device.getFirmwareVersion().trim().isEmpty()) {
+ throw new IllegalStateException("设备固件版本为空,无法激活");
+ }
+
+ // 调用领域模型的业务方法
+ device.activate();
+
+ // 持久化
+ Device updatedDevice = deviceRepository.save(device);
+
+ log.info("Domain: 设备激活成功,设备ID: {}, 当前状态: {}",
+ deviceId, updatedDevice.getStatus());
+
+ return updatedDevice;
+ }
+
+ @Override
+ public Device deactivateDevice(Long deviceId) {
+ log.info("Domain: 开始停用设备,设备ID: {}", deviceId);
+
+ // 查询设备
+ Device device = deviceRepository.findById(deviceId)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,ID: " + deviceId));
+
+ // 业务规则1:只有激活状态的设备才能停用(在 Device.deactivate() 中验证)
+
+ // 业务规则2:停用前需要检查设备是否正在执行任务
+ // TODO: 这里应该调用任务领域服务检查设备是否有正在执行的任务
+ // 示例:if (taskDomainService.hasRunningTask(deviceId)) { throw ... }
+
+ // 调用领域模型的业务方法
+ device.deactivate();
+
+ // 持久化
+ Device updatedDevice = deviceRepository.save(device);
+
+ log.info("Domain: 设备停用成功,设备ID: {}, 当前状态: {}",
+ deviceId, updatedDevice.getStatus());
+
+ return updatedDevice;
+ }
+
+ @Override
+ public Device markDeviceAsFaulty(Long deviceId, String faultReason) {
+ log.info("Domain: 标记设备为故障状态,设备ID: {}, 故障原因: {}", deviceId, faultReason);
+
+ // 查询设备
+ Device device = deviceRepository.findById(deviceId)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,ID: " + deviceId));
+
+ // 业务规则1:任何状态的设备都可以标记为故障
+ // 业务规则2:故障设备自动停止执行任务
+ // TODO: 这里应该调用任务领域服务停止设备的所有任务
+ // 示例:taskDomainService.stopAllTasksByDevice(deviceId);
+
+ // 调用领域模型的业务方法
+ device.markAsFaulty();
+
+ // 记录故障原因到备注
+ String remark = device.getRemark() != null ? device.getRemark() : "";
+ device.setRemark(remark + " [故障原因: " + faultReason + "]");
+
+ // 持久化
+ Device updatedDevice = deviceRepository.save(device);
+
+ log.info("Domain: 设备已标记为故障状态,设备ID: {}", deviceId);
+
+ return updatedDevice;
+ }
+
+ @Override
+ public Device updateFirmware(Long deviceId, String newVersion) {
+ log.info("Domain: 更新设备固件版本,设备ID: {}, 新版本: {}", deviceId, newVersion);
+
+ // 查询设备
+ Device device = deviceRepository.findById(deviceId)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,ID: " + deviceId));
+
+ // 业务规则1:只有停用状态的设备才能更新固件
+ if (device.getStatus() != DeviceStatus.INACTIVE) {
+ throw new IllegalStateException("只有停用状态的设备才能更新固件,当前状态: " + device.getStatus());
+ }
+
+ // 业务规则2:新版本号必须大于当前版本号
+ if (device.getFirmwareVersion() != null &&
+ compareVersion(newVersion, device.getFirmwareVersion()) <= 0) {
+ throw new IllegalArgumentException("新版本号必须大于当前版本号");
+ }
+
+ // 调用领域模型的业务方法
+ device.updateFirmware(newVersion);
+
+ // 持久化
+ Device updatedDevice = deviceRepository.save(device);
+
+ log.info("Domain: 设备固件版本更新成功,设备ID: {}, 新版本: {}", deviceId, newVersion);
+
+ return updatedDevice;
+ }
+
+ @Override
+ public Device assignToAirport(Long deviceId, Long airportId) {
+ log.info("Domain: 分配设备到机场,设备ID: {}, 机场ID: {}", deviceId, airportId);
+
+ // 查询设备
+ Device device = deviceRepository.findById(deviceId)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,ID: " + deviceId));
+
+ // 业务规则1:设备必须是停用状态才能分配
+ if (device.getStatus() != DeviceStatus.INACTIVE) {
+ throw new IllegalStateException("只有停用状态的设备才能分配到机场,当前状态: " + device.getStatus());
+ }
+
+ // 业务规则2:机场必须存在且状态正常
+ // TODO: 这里应该调用机场领域服务验证机场状态
+ // 示例:airportDomainService.validateAirportAvailable(airportId);
+
+ // 业务规则3:检查机场的设备容量限制
+ long deviceCount = deviceRepository.countByAirportId(airportId);
+ final int MAX_DEVICES_PER_AIRPORT = 100; // 示例:每个机场最多100台设备
+ if (deviceCount >= MAX_DEVICES_PER_AIRPORT) {
+ throw new IllegalStateException("机场设备数量已达上限,无法分配新设备");
+ }
+
+ // 调用领域模型的业务方法
+ device.assignToAirport(airportId);
+
+ // 持久化
+ Device updatedDevice = deviceRepository.save(device);
+
+ log.info("Domain: 设备分配成功,设备ID: {}, 机场ID: {}", deviceId, airportId);
+
+ return updatedDevice;
+ }
+
+ @Override
+ public Device removeFromAirport(Long deviceId) {
+ log.info("Domain: 从机场移除设备,设备ID: {}", deviceId);
+
+ // 查询设备
+ Device device = deviceRepository.findById(deviceId)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,ID: " + deviceId));
+
+ // 业务规则1:设备必须是停用状态
+ if (device.getStatus() != DeviceStatus.INACTIVE) {
+ throw new IllegalStateException("只有停用状态的设备才能从机场移除,当前状态: " + device.getStatus());
+ }
+
+ // 业务规则2:设备不能有未完成的任务
+ // TODO: 这里应该调用任务领域服务检查
+ // 示例:if (taskDomainService.hasPendingTask(deviceId)) { throw ... }
+
+ // 移除机场分配
+ device.setAirportId(null);
+
+ // 持久化
+ Device updatedDevice = deviceRepository.save(device);
+
+ log.info("Domain: 设备已从机场移除,设备ID: {}", deviceId);
+
+ return updatedDevice;
+ }
+
+ @Override
+ public Device getDeviceById(Long deviceId) {
+ log.debug("Domain: 查询设备,设备ID: {}", deviceId);
+ return deviceRepository.findById(deviceId)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,ID: " + deviceId));
+ }
+
+ @Override
+ public Device getDeviceByCode(String deviceCode) {
+ log.debug("Domain: 查询设备,设备编码: {}", deviceCode);
+ return deviceRepository.findByDeviceCode(deviceCode)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,设备编码: " + deviceCode));
+ }
+
+ @Override
+ public List getAvailableDevicesByAirport(Long airportId) {
+ log.debug("Domain: 查询机场下的可用设备,机场ID: {}", airportId);
+
+ // 业务规则:只返回激活状态的设备
+ return deviceRepository.findByAirportIdAndStatus(airportId, DeviceStatus.ACTIVE);
+ }
+
+ @Override
+ public List getDevicesByType(DeviceType deviceType) {
+ log.debug("Domain: 查询指定类型的设备,设备类型: {}", deviceType);
+ return deviceRepository.findByDeviceType(deviceType);
+ }
+
+ @Override
+ public List getDevicesByStatus(DeviceStatus status) {
+ log.debug("Domain: 查询指定状态的设备,设备状态: {}", status);
+ return deviceRepository.findByStatus(status);
+ }
+
+ @Override
+ public void deleteDevice(Long deviceId) {
+ log.info("Domain: 删除设备,设备ID: {}", deviceId);
+
+ // 查询设备
+ Device device = deviceRepository.findById(deviceId)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,ID: " + deviceId));
+
+ // 业务规则1:只能删除停用状态的设备
+ if (device.getStatus() != DeviceStatus.INACTIVE) {
+ throw new IllegalStateException("只能删除停用状态的设备,当前状态: " + device.getStatus());
+ }
+
+ // 业务规则2:设备不能有历史任务记录(这里使用逻辑删除,所以可以保留历史记录)
+ // TODO: 如果需要物理删除,应该检查任务记录
+ // 示例:if (taskDomainService.hasHistoryTask(deviceId)) { throw ... }
+
+ // 删除设备(逻辑删除)
+ boolean deleted = deviceRepository.deleteById(deviceId);
+
+ if (!deleted) {
+ throw new IllegalStateException("设备删除失败,ID: " + deviceId);
+ }
+
+ log.info("Domain: 设备删除成功,设备ID: {}", deviceId);
+ }
+
+ @Override
+ public boolean canExecuteTask(Long deviceId) {
+ log.debug("Domain: 检查设备是否可以执行任务,设备ID: {}", deviceId);
+
+ // 查询设备
+ Device device = deviceRepository.findById(deviceId)
+ .orElseThrow(() -> new IllegalArgumentException("设备不存在,ID: " + deviceId));
+
+ // 业务规则1:设备必须是激活状态
+ if (device.getStatus() != DeviceStatus.ACTIVE) {
+ log.debug("设备状态不是激活状态: {}", device.getStatus());
+ return false;
+ }
+
+ // 业务规则2:设备必须已分配到机场
+ if (device.getAirportId() == null) {
+ log.debug("设备未分配到机场");
+ return false;
+ }
+
+ // 业务规则3:设备固件版本必须是最新的
+ // TODO: 这里应该检查固件版本是否是最新的
+ // 示例:String latestVersion = firmwareService.getLatestVersion(device.getDeviceType());
+ // if (!device.getFirmwareVersion().equals(latestVersion)) { return false; }
+
+ // 调用领域模型的业务方法
+ return device.canExecuteTask();
+ }
+
+ @Override
+ public long countDevicesByAirport(Long airportId) {
+ log.debug("Domain: 统计机场下的设备数量,机场ID: {}", airportId);
+ return deviceRepository.countByAirportId(airportId);
+ }
+
+ /**
+ * 比较版本号
+ * 返回值:1 表示 v1 > v2,0 表示 v1 = v2,-1 表示 v1 < v2
+ */
+ private int compareVersion(String v1, String v2) {
+ String[] parts1 = v1.split("\\.");
+ String[] parts2 = v2.split("\\.");
+ int maxLength = Math.max(parts1.length, parts2.length);
+
+ for (int i = 0; i < maxLength; i++) {
+ int num1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0;
+ int num2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0;
+
+ if (num1 > num2) {
+ return 1;
+ } else if (num1 < num2) {
+ return -1;
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/converter/DeviceConverter.java b/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/converter/DeviceConverter.java
new file mode 100644
index 0000000..8eeca1e
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/converter/DeviceConverter.java
@@ -0,0 +1,67 @@
+package com.tuoheng.airport.device.infrastructure.persistence.converter;
+
+import com.tuoheng.airport.device.domain.model.Device;
+import com.tuoheng.airport.device.domain.model.DeviceStatus;
+import com.tuoheng.airport.device.domain.model.DeviceType;
+import com.tuoheng.airport.device.infrastructure.persistence.entity.DeviceEntity;
+
+/**
+ * 设备领域模型与数据库实体转换器
+ * 负责 Domain 层和 Infrastructure 层之间的对象转换
+ *
+ * @author tuoheng
+ */
+public class DeviceConverter {
+
+ /**
+ * 领域模型转数据库实体
+ *
+ * @param device 领域模型
+ * @return 数据库实体
+ */
+ public static DeviceEntity toEntity(Device device) {
+ if (device == null) {
+ return null;
+ }
+
+ return DeviceEntity.builder()
+ .id(device.getId())
+ .deviceCode(device.getDeviceCode())
+ .deviceName(device.getDeviceName())
+ .deviceType(device.getDeviceType() != null ? device.getDeviceType().getCode() : null)
+ .status(device.getStatus() != null ? device.getStatus().getCode() : null)
+ .airportId(device.getAirportId())
+ .manufacturer(device.getManufacturer())
+ .firmwareVersion(device.getFirmwareVersion())
+ .createTime(device.getCreateTime())
+ .updateTime(device.getUpdateTime())
+ .remark(device.getRemark())
+ .build();
+ }
+
+ /**
+ * 数据库实体转领域模型
+ *
+ * @param entity 数据库实体
+ * @return 领域模型
+ */
+ public static Device toDomain(DeviceEntity entity) {
+ if (entity == null) {
+ return null;
+ }
+
+ return Device.builder()
+ .id(entity.getId())
+ .deviceCode(entity.getDeviceCode())
+ .deviceName(entity.getDeviceName())
+ .deviceType(DeviceType.fromCode(entity.getDeviceType()))
+ .status(DeviceStatus.fromCode(entity.getStatus()))
+ .airportId(entity.getAirportId())
+ .manufacturer(entity.getManufacturer())
+ .firmwareVersion(entity.getFirmwareVersion())
+ .createTime(entity.getCreateTime())
+ .updateTime(entity.getUpdateTime())
+ .remark(entity.getRemark())
+ .build();
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/entity/DeviceEntity.java b/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/entity/DeviceEntity.java
new file mode 100644
index 0000000..53457c9
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/entity/DeviceEntity.java
@@ -0,0 +1,96 @@
+package com.tuoheng.airport.device.infrastructure.persistence.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 设备数据库实体(Infrastructure层)
+ * 对应数据库表 th_device
+ *
+ * @author tuoheng
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName("th_device")
+public class DeviceEntity {
+
+ /**
+ * 主键ID(自增)
+ */
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ /**
+ * 设备编码(唯一)
+ */
+ @TableField("device_code")
+ private String deviceCode;
+
+ /**
+ * 设备名称
+ */
+ @TableField("device_name")
+ private String deviceName;
+
+ /**
+ * 设备类型(1:多旋翼 2:固定翼 3:垂直起降 4:摄像头 5:喇叭 6:探照灯)
+ */
+ @TableField("device_type")
+ private Integer deviceType;
+
+ /**
+ * 设备状态(0:停用 1:激活 2:故障 3:维护中)
+ */
+ @TableField("status")
+ private Integer status;
+
+ /**
+ * 所属机场ID
+ */
+ @TableField("airport_id")
+ private Long airportId;
+
+ /**
+ * 制造商
+ */
+ @TableField("manufacturer")
+ private String manufacturer;
+
+ /**
+ * 固件版本
+ */
+ @TableField("firmware_version")
+ private String firmwareVersion;
+
+ /**
+ * 创建时间(自动填充)
+ */
+ @TableField(value = "create_time", fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+
+ /**
+ * 更新时间(自动填充)
+ */
+ @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+
+ /**
+ * 备注
+ */
+ @TableField("remark")
+ private String remark;
+
+ /**
+ * 逻辑删除标记(0:未删除 1:已删除)
+ */
+ @TableLogic
+ @TableField("is_deleted")
+ private Integer isDeleted;
+}
diff --git a/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/mapper/DeviceMapper.java b/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/mapper/DeviceMapper.java
new file mode 100644
index 0000000..720fb86
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/mapper/DeviceMapper.java
@@ -0,0 +1,46 @@
+package com.tuoheng.airport.device.infrastructure.persistence.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tuoheng.airport.device.infrastructure.persistence.entity.DeviceEntity;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 设备Mapper接口(Infrastructure层)
+ * 使用 MyBatis Plus 提供基础 CRUD 能力
+ *
+ * @author tuoheng
+ */
+@Mapper
+public interface DeviceMapper extends BaseMapper {
+
+ /**
+ * 根据机场ID和状态查询设备列表
+ *
+ * @param airportId 机场ID
+ * @param status 设备状态
+ * @return 设备实体列表
+ */
+ List selectByAirportIdAndStatus(@Param("airportId") Long airportId,
+ @Param("status") Integer status);
+
+ /**
+ * 统计机场下的设备数量
+ *
+ * @param airportId 机场ID
+ * @return 设备数量
+ */
+ Long countByAirportId(@Param("airportId") Long airportId);
+
+ /**
+ * 批量更新设备状态
+ *
+ * @param deviceIds 设备ID列表
+ * @param status 目标状态
+ * @return 更新数量
+ */
+ int batchUpdateStatus(@Param("deviceIds") List deviceIds,
+ @Param("status") Integer status);
+}
diff --git a/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/repository/DeviceRepositoryImpl.java b/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/repository/DeviceRepositoryImpl.java
new file mode 100644
index 0000000..fdc4d68
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/infrastructure/persistence/repository/DeviceRepositoryImpl.java
@@ -0,0 +1,129 @@
+package com.tuoheng.airport.device.infrastructure.persistence.repository;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.tuoheng.airport.device.domain.model.Device;
+import com.tuoheng.airport.device.domain.model.DeviceStatus;
+import com.tuoheng.airport.device.domain.model.DeviceType;
+import com.tuoheng.airport.device.domain.repository.DeviceRepository;
+import com.tuoheng.airport.device.infrastructure.persistence.converter.DeviceConverter;
+import com.tuoheng.airport.device.infrastructure.persistence.entity.DeviceEntity;
+import com.tuoheng.airport.device.infrastructure.persistence.mapper.DeviceMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * 设备仓储实现类(Infrastructure层)
+ * 实现 Domain 层定义的 DeviceRepository 接口
+ * 负责数据持久化和查询
+ *
+ * @author tuoheng
+ */
+@Repository
+@RequiredArgsConstructor
+public class DeviceRepositoryImpl implements DeviceRepository {
+
+ private final DeviceMapper deviceMapper;
+
+ @Override
+ public Device save(Device device) {
+ DeviceEntity entity = DeviceConverter.toEntity(device);
+
+ if (entity.getId() == null) {
+ // 新增
+ deviceMapper.insert(entity);
+ } else {
+ // 更新
+ deviceMapper.updateById(entity);
+ }
+
+ // 返回保存后的领域模型(包含生成的ID)
+ device.setId(entity.getId());
+ return device;
+ }
+
+ @Override
+ public Optional findById(Long id) {
+ DeviceEntity entity = deviceMapper.selectById(id);
+ return Optional.ofNullable(DeviceConverter.toDomain(entity));
+ }
+
+ @Override
+ public Optional findByDeviceCode(String deviceCode) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(DeviceEntity::getDeviceCode, deviceCode);
+ DeviceEntity entity = deviceMapper.selectOne(wrapper);
+ return Optional.ofNullable(DeviceConverter.toDomain(entity));
+ }
+
+ @Override
+ public List findAll() {
+ List entities = deviceMapper.selectList(null);
+ return entities.stream()
+ .map(DeviceConverter::toDomain)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List findByAirportId(Long airportId) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(DeviceEntity::getAirportId, airportId)
+ .orderByDesc(DeviceEntity::getCreateTime);
+ List entities = deviceMapper.selectList(wrapper);
+ return entities.stream()
+ .map(DeviceConverter::toDomain)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List findByStatus(DeviceStatus status) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(DeviceEntity::getStatus, status.getCode())
+ .orderByDesc(DeviceEntity::getCreateTime);
+ List entities = deviceMapper.selectList(wrapper);
+ return entities.stream()
+ .map(DeviceConverter::toDomain)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List findByDeviceType(DeviceType deviceType) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(DeviceEntity::getDeviceType, deviceType.getCode())
+ .orderByDesc(DeviceEntity::getCreateTime);
+ List entities = deviceMapper.selectList(wrapper);
+ return entities.stream()
+ .map(DeviceConverter::toDomain)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List findByAirportIdAndStatus(Long airportId, DeviceStatus status) {
+ List entities = deviceMapper.selectByAirportIdAndStatus(
+ airportId, status.getCode());
+ return entities.stream()
+ .map(DeviceConverter::toDomain)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public boolean deleteById(Long id) {
+ // 使用逻辑删除
+ return deviceMapper.deleteById(id) > 0;
+ }
+
+ @Override
+ public boolean existsByDeviceCode(String deviceCode) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(DeviceEntity::getDeviceCode, deviceCode);
+ return deviceMapper.selectCount(wrapper) > 0;
+ }
+
+ @Override
+ public long countByAirportId(Long airportId) {
+ return deviceMapper.countByAirportId(airportId);
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/presentation/controller/DeviceController.java b/src/main/java/com/tuoheng/airport/device/presentation/controller/DeviceController.java
new file mode 100644
index 0000000..c9ea7b1
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/presentation/controller/DeviceController.java
@@ -0,0 +1,229 @@
+package com.tuoheng.airport.device.presentation.controller;
+
+import com.tuoheng.airport.device.application.dto.*;
+import com.tuoheng.airport.device.application.service.DeviceApplicationService;
+import com.tuoheng.airport.device.presentation.converter.DeviceVoConverter;
+import com.tuoheng.airport.device.presentation.vo.*;
+import com.tuoheng.common.response.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 设备控制器(Presentation层)
+ * 提供设备管理的 REST API 接口
+ * 负责接收 HTTP 请求,调用 Application 层服务,返回统一响应格式
+ *
+ * 职责:
+ * 1. 接收前端的 VO 对象
+ * 2. 将 VO 转换为 DTO 传递给 Application 层
+ * 3. 将 Application 层返回的 DTO 转换为 VO 返回给前端
+ * 4. 不包含任何业务逻辑
+ *
+ * @author tuoheng
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/devices")
+@RequiredArgsConstructor
+@Validated
+@Tag(name = "设备管理", description = "设备管理相关接口")
+public class DeviceController {
+
+ private final DeviceApplicationService deviceApplicationService;
+
+ /**
+ * 创建设备
+ */
+ @PostMapping
+ @Operation(summary = "创建设备", description = "创建新的设备记录")
+ public Result createDevice(@Valid @RequestBody DeviceCreateVO vo) {
+ log.info("接收到创建设备请求: {}", vo);
+ // VO 转 DTO
+ DeviceCreateRequest request = DeviceVoConverter.toCreateRequest(vo);
+ // 调用 Application 层
+ DeviceResponse response = deviceApplicationService.createDevice(request);
+ // DTO 转 VO
+ DeviceVO result = DeviceVoConverter.toVO(response);
+ return Result.success(result);
+ }
+
+ /**
+ * 更新设备信息
+ */
+ @PutMapping("/{id}")
+ @Operation(summary = "更新设备信息", description = "更新指定设备的基本信息")
+ public Result updateDevice(
+ @Parameter(description = "设备ID") @PathVariable Long id,
+ @Valid @RequestBody DeviceUpdateVO vo) {
+ log.info("接收到更新设备请求,设备ID: {}, 请求参数: {}", id, vo);
+ vo.setId(id);
+ // VO 转 DTO
+ DeviceUpdateRequest request = DeviceVoConverter.toUpdateRequest(vo);
+ // 调用 Application 层
+ DeviceResponse response = deviceApplicationService.updateDevice(request);
+ // DTO 转 VO
+ DeviceVO result = DeviceVoConverter.toVO(response);
+ return Result.success(result);
+ }
+
+ /**
+ * 根据ID查询设备
+ */
+ @GetMapping("/{id}")
+ @Operation(summary = "查询设备详情", description = "根据设备ID查询设备详细信息")
+ public Result getDeviceById(
+ @Parameter(description = "设备ID") @PathVariable Long id) {
+ log.info("接收到查询设备请求,设备ID: {}", id);
+ DeviceResponse response = deviceApplicationService.getDeviceById(id);
+ DeviceVO result = DeviceVoConverter.toVO(response);
+ return Result.success(result);
+ }
+
+ /**
+ * 根据设备编码查询设备
+ */
+ @GetMapping("/code/{deviceCode}")
+ @Operation(summary = "根据编码查询设备", description = "根据设备编码查询设备详细信息")
+ public Result getDeviceByCode(
+ @Parameter(description = "设备编码") @PathVariable String deviceCode) {
+ log.info("接收到查询设备请求,设备编码: {}", deviceCode);
+ DeviceResponse response = deviceApplicationService.getDeviceByCode(deviceCode);
+ DeviceVO result = DeviceVoConverter.toVO(response);
+ return Result.success(result);
+ }
+
+ /**
+ * 查询所有设备
+ */
+ @GetMapping
+ @Operation(summary = "查询设备列表", description = "查询所有设备或根据条件筛选设备")
+ public Result> queryDevices(DeviceQueryVO vo) {
+ log.info("接收到查询设备列表请求,查询条件: {}", vo);
+ // VO 转 DTO
+ DeviceQueryRequest request = DeviceVoConverter.toQueryRequest(vo);
+ // 调用 Application 层
+ List responses = deviceApplicationService.queryDevices(request);
+ // DTO 列表转 VO 列表
+ List results = responses.stream()
+ .map(DeviceVoConverter::toVO)
+ .collect(Collectors.toList());
+ return Result.success(results);
+ }
+
+ /**
+ * 根据机场ID查询设备列表
+ */
+ @GetMapping("/airport/{airportId}")
+ @Operation(summary = "查询机场设备", description = "查询指定机场下的所有设备")
+ public Result> getDevicesByAirportId(
+ @Parameter(description = "机场ID") @PathVariable Long airportId) {
+ log.info("接收到查询机场设备列表请求,机场ID: {}", airportId);
+ List responses = deviceApplicationService.getDevicesByAirportId(airportId);
+ List results = responses.stream()
+ .map(DeviceVoConverter::toVO)
+ .collect(Collectors.toList());
+ return Result.success(results);
+ }
+
+ /**
+ * 激活设备
+ */
+ @PostMapping("/{id}/activate")
+ @Operation(summary = "激活设备", description = "将设备状态设置为激活")
+ public Result activateDevice(
+ @Parameter(description = "设备ID") @PathVariable Long id) {
+ log.info("接收到激活设备请求,设备ID: {}", id);
+ DeviceResponse response = deviceApplicationService.activateDevice(id);
+ DeviceVO result = DeviceVoConverter.toVO(response);
+ return Result.success(result);
+ }
+
+ /**
+ * 停用设备
+ */
+ @PostMapping("/{id}/deactivate")
+ @Operation(summary = "停用设备", description = "将设备状态设置为停用")
+ public Result deactivateDevice(
+ @Parameter(description = "设备ID") @PathVariable Long id) {
+ log.info("接收到停用设备请求,设备ID: {}", id);
+ DeviceResponse response = deviceApplicationService.deactivateDevice(id);
+ DeviceVO result = DeviceVoConverter.toVO(response);
+ return Result.success(result);
+ }
+
+ /**
+ * 标记设备为故障状态
+ */
+ @PostMapping("/{id}/faulty")
+ @Operation(summary = "标记设备故障", description = "将设备状态标记为故障")
+ public Result markDeviceAsFaulty(
+ @Parameter(description = "设备ID") @PathVariable Long id) {
+ log.info("接收到标记设备故障请求,设备ID: {}", id);
+ DeviceResponse response = deviceApplicationService.markDeviceAsFaulty(id);
+ DeviceVO result = DeviceVoConverter.toVO(response);
+ return Result.success(result);
+ }
+
+ /**
+ * 更新设备固件版本
+ */
+ @PostMapping("/{id}/firmware")
+ @Operation(summary = "更新固件版本", description = "更新设备的固件版本")
+ public Result updateDeviceFirmware(
+ @Parameter(description = "设备ID") @PathVariable Long id,
+ @Parameter(description = "新固件版本") @RequestParam @NotBlank(message = "固件版本不能为空") String version) {
+ log.info("接收到更新设备固件请求,设备ID: {}, 新版本: {}", id, version);
+ DeviceResponse response = deviceApplicationService.updateDeviceFirmware(id, version);
+ DeviceVO result = DeviceVoConverter.toVO(response);
+ return Result.success(result);
+ }
+
+ /**
+ * 分配设备到机场
+ */
+ @PostMapping("/{id}/assign")
+ @Operation(summary = "分配设备到机场", description = "将设备分配到指定机场")
+ public Result assignDeviceToAirport(
+ @Parameter(description = "设备ID") @PathVariable Long id,
+ @Parameter(description = "机场ID") @RequestParam @NotNull(message = "机场ID不能为空") Long airportId) {
+ log.info("接收到分配设备到机场请求,设备ID: {}, 机场ID: {}", id, airportId);
+ DeviceResponse response = deviceApplicationService.assignDeviceToAirport(id, airportId);
+ DeviceVO result = DeviceVoConverter.toVO(response);
+ return Result.success(result);
+ }
+
+ /**
+ * 删除设备
+ */
+ @DeleteMapping("/{id}")
+ @Operation(summary = "删除设备", description = "删除指定的设备(逻辑删除)")
+ public Result deleteDevice(
+ @Parameter(description = "设备ID") @PathVariable Long id) {
+ log.info("接收到删除设备请求,设备ID: {}", id);
+ deviceApplicationService.deleteDevice(id);
+ return Result.success();
+ }
+
+ /**
+ * 统计机场下的设备数量
+ */
+ @GetMapping("/airport/{airportId}/count")
+ @Operation(summary = "统计机场设备数量", description = "统计指定机场下的设备总数")
+ public Result countDevicesByAirportId(
+ @Parameter(description = "机场ID") @PathVariable Long airportId) {
+ log.info("接收到统计机场设备数量请求,机场ID: {}", airportId);
+ long count = deviceApplicationService.countDevicesByAirportId(airportId);
+ return Result.success(count);
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/presentation/converter/DeviceVoConverter.java b/src/main/java/com/tuoheng/airport/device/presentation/converter/DeviceVoConverter.java
new file mode 100644
index 0000000..1d61baf
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/presentation/converter/DeviceVoConverter.java
@@ -0,0 +1,94 @@
+package com.tuoheng.airport.device.presentation.converter;
+
+import com.tuoheng.airport.device.application.dto.*;
+import com.tuoheng.airport.device.presentation.vo.*;
+
+/**
+ * VO 和 DTO 转换器(Presentation层)
+ * 负责 Presentation 层的 VO 和 Application 层的 DTO 之间的转换
+ *
+ * @author tuoheng
+ */
+public class DeviceVoConverter {
+
+ /**
+ * 创建请求VO转DTO
+ */
+ public static DeviceCreateRequest toCreateRequest(DeviceCreateVO vo) {
+ if (vo == null) {
+ return null;
+ }
+
+ DeviceCreateRequest dto = new DeviceCreateRequest();
+ dto.setDeviceCode(vo.getDeviceCode());
+ dto.setDeviceName(vo.getDeviceName());
+ dto.setDeviceType(vo.getDeviceType());
+ dto.setManufacturer(vo.getManufacturer());
+ dto.setAirportId(vo.getAirportId());
+ dto.setFirmwareVersion(vo.getFirmwareVersion());
+ dto.setRemark(vo.getRemark());
+ return dto;
+ }
+
+ /**
+ * 更新请求VO转DTO
+ */
+ public static DeviceUpdateRequest toUpdateRequest(DeviceUpdateVO vo) {
+ if (vo == null) {
+ return null;
+ }
+
+ DeviceUpdateRequest dto = new DeviceUpdateRequest();
+ dto.setId(vo.getId());
+ dto.setDeviceName(vo.getDeviceName());
+ dto.setManufacturer(vo.getManufacturer());
+ dto.setAirportId(vo.getAirportId());
+ dto.setFirmwareVersion(vo.getFirmwareVersion());
+ dto.setRemark(vo.getRemark());
+ return dto;
+ }
+
+ /**
+ * 查询请求VO转DTO
+ */
+ public static DeviceQueryRequest toQueryRequest(DeviceQueryVO vo) {
+ if (vo == null) {
+ return null;
+ }
+
+ DeviceQueryRequest dto = new DeviceQueryRequest();
+ dto.setDeviceCode(vo.getDeviceCode());
+ dto.setDeviceName(vo.getDeviceName());
+ dto.setDeviceType(vo.getDeviceType());
+ dto.setStatus(vo.getStatus());
+ dto.setAirportId(vo.getAirportId());
+ dto.setManufacturer(vo.getManufacturer());
+ return dto;
+ }
+
+ /**
+ * 响应DTO转VO
+ */
+ public static DeviceVO toVO(DeviceResponse dto) {
+ if (dto == null) {
+ return null;
+ }
+
+ return DeviceVO.builder()
+ .id(dto.getId())
+ .deviceCode(dto.getDeviceCode())
+ .deviceName(dto.getDeviceName())
+ .deviceType(dto.getDeviceType())
+ .deviceTypeDesc(dto.getDeviceTypeDesc())
+ .status(dto.getStatus())
+ .statusDesc(dto.getStatusDesc())
+ .airportId(dto.getAirportId())
+ .manufacturer(dto.getManufacturer())
+ .firmwareVersion(dto.getFirmwareVersion())
+ .createTime(dto.getCreateTime())
+ .updateTime(dto.getUpdateTime())
+ .remark(dto.getRemark())
+ .canExecuteTask(dto.getCanExecuteTask())
+ .build();
+ }
+}
diff --git a/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceCreateVO.java b/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceCreateVO.java
new file mode 100644
index 0000000..304614d
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceCreateVO.java
@@ -0,0 +1,55 @@
+package com.tuoheng.airport.device.presentation.vo;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 设备创建请求VO(Presentation层)
+ * 用于接收前端创建设备的请求参数
+ *
+ * @author tuoheng
+ */
+@Data
+public class DeviceCreateVO {
+
+ /**
+ * 设备编码(必填,唯一)
+ */
+ @NotBlank(message = "设备编码不能为空")
+ private String deviceCode;
+
+ /**
+ * 设备名称(必填)
+ */
+ @NotBlank(message = "设备名称不能为空")
+ private String deviceName;
+
+ /**
+ * 设备类型(必填)
+ * 1:多旋翼 2:固定翼 3:垂直起降 4:摄像头 5:喇叭 6:探照灯
+ */
+ @NotNull(message = "设备类型不能为空")
+ private Integer deviceType;
+
+ /**
+ * 制造商
+ */
+ private String manufacturer;
+
+ /**
+ * 所属机场ID
+ */
+ private Long airportId;
+
+ /**
+ * 固件版本
+ */
+ private String firmwareVersion;
+
+ /**
+ * 备注
+ */
+ private String remark;
+}
diff --git a/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceQueryVO.java b/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceQueryVO.java
new file mode 100644
index 0000000..0f109fb
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceQueryVO.java
@@ -0,0 +1,43 @@
+package com.tuoheng.airport.device.presentation.vo;
+
+import lombok.Data;
+
+/**
+ * 设备查询请求VO(Presentation层)
+ * 用于接收前端查询设备的请求参数
+ *
+ * @author tuoheng
+ */
+@Data
+public class DeviceQueryVO {
+
+ /**
+ * 设备编码(模糊查询)
+ */
+ private String deviceCode;
+
+ /**
+ * 设备名称(模糊查询)
+ */
+ private String deviceName;
+
+ /**
+ * 设备类型
+ */
+ private Integer deviceType;
+
+ /**
+ * 设备状态
+ */
+ private Integer status;
+
+ /**
+ * 所属机场ID
+ */
+ private Long airportId;
+
+ /**
+ * 制造商
+ */
+ private String manufacturer;
+}
diff --git a/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceUpdateVO.java b/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceUpdateVO.java
new file mode 100644
index 0000000..8976e24
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceUpdateVO.java
@@ -0,0 +1,46 @@
+package com.tuoheng.airport.device.presentation.vo;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 设备更新请求VO(Presentation层)
+ * 用于接收前端更新设备的请求参数
+ *
+ * @author tuoheng
+ */
+@Data
+public class DeviceUpdateVO {
+
+ /**
+ * 设备ID(必填)
+ */
+ @NotNull(message = "设备ID不能为空")
+ private Long id;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 制造商
+ */
+ private String manufacturer;
+
+ /**
+ * 所属机场ID
+ */
+ private Long airportId;
+
+ /**
+ * 固件版本
+ */
+ private String firmwareVersion;
+
+ /**
+ * 备注
+ */
+ private String remark;
+}
diff --git a/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceVO.java b/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceVO.java
new file mode 100644
index 0000000..e3b1ede
--- /dev/null
+++ b/src/main/java/com/tuoheng/airport/device/presentation/vo/DeviceVO.java
@@ -0,0 +1,94 @@
+package com.tuoheng.airport.device.presentation.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 设备响应VO(Presentation层)
+ * 用于返回给前端的设备信息
+ *
+ * @author tuoheng
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeviceVO {
+
+ /**
+ * 设备ID
+ */
+ private Long id;
+
+ /**
+ * 设备编码
+ */
+ private String deviceCode;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 设备类型代码
+ */
+ private Integer deviceType;
+
+ /**
+ * 设备类型描述
+ */
+ private String deviceTypeDesc;
+
+ /**
+ * 设备状态代码
+ */
+ private Integer status;
+
+ /**
+ * 设备状态描述
+ */
+ private String statusDesc;
+
+ /**
+ * 所属机场ID
+ */
+ private Long airportId;
+
+ /**
+ * 制造商
+ */
+ private String manufacturer;
+
+ /**
+ * 固件版本
+ */
+ private String firmwareVersion;
+
+ /**
+ * 创建时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime createTime;
+
+ /**
+ * 更新时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime updateTime;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 是否可以执行任务
+ */
+ private Boolean canExecuteTask;
+}
diff --git a/src/main/resources/mapper/device/DeviceMapper.xml b/src/main/resources/mapper/device/DeviceMapper.xml
new file mode 100644
index 0000000..90a1665
--- /dev/null
+++ b/src/main/resources/mapper/device/DeviceMapper.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UPDATE th_device
+ SET status = #{status},
+ update_time = NOW()
+ WHERE id IN
+
+ #{id}
+
+ AND is_deleted = 0
+
+
+
diff --git a/src/main/resources/sql/device_schema.sql b/src/main/resources/sql/device_schema.sql
new file mode 100644
index 0000000..49dd7f0
--- /dev/null
+++ b/src/main/resources/sql/device_schema.sql
@@ -0,0 +1,36 @@
+-- =============================================
+-- 设备管理表结构
+-- 用于 tuoheng-ddd 项目的 device 子域
+-- =============================================
+
+-- 创建设备表
+DROP TABLE IF EXISTS `th_device`;
+CREATE TABLE `th_device` (
+ `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `device_code` VARCHAR(64) NOT NULL COMMENT '设备编码(唯一标识)',
+ `device_name` VARCHAR(128) NOT NULL COMMENT '设备名称',
+ `device_type` TINYINT(2) NOT NULL COMMENT '设备类型(1:多旋翼 2:固定翼 3:垂直起降 4:摄像头 5:喇叭 6:探照灯)',
+ `status` TINYINT(2) NOT NULL DEFAULT 0 COMMENT '设备状态(0:停用 1:激活 2:故障 3:维护中)',
+ `airport_id` BIGINT(20) DEFAULT NULL COMMENT '所属机场ID',
+ `manufacturer` VARCHAR(128) DEFAULT NULL COMMENT '制造商',
+ `firmware_version` VARCHAR(64) DEFAULT NULL COMMENT '固件版本',
+ `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注',
+ `is_deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记(0:未删除 1:已删除)',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_device_code` (`device_code`),
+ KEY `idx_airport_id` (`airport_id`),
+ KEY `idx_status` (`status`),
+ KEY `idx_device_type` (`device_type`),
+ KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备表';
+
+-- 插入测试数据
+INSERT INTO `th_device` (`device_code`, `device_name`, `device_type`, `status`, `airport_id`, `manufacturer`, `firmware_version`, `remark`) VALUES
+('DRONE-001', '多旋翼无人机-01', 1, 1, 1, 'DJI', 'v1.2.3', '测试设备1'),
+('DRONE-002', '固定翼无人机-01', 2, 0, 1, 'DJI', 'v1.0.0', '测试设备2'),
+('DRONE-003', '垂直起降无人机-01', 3, 1, 2, 'DJI', 'v2.0.1', '测试设备3'),
+('CAMERA-001', '可见光摄像头-01', 4, 1, 1, 'Sony', 'v1.5.0', '测试摄像头1'),
+('MEGAPHONE-001', '喇叭设备-01', 5, 1, 2, 'Generic', 'v1.0.0', '测试喇叭1'),
+('SEARCHLIGHT-001', '探照灯设备-01', 6, 0, 1, 'Generic', 'v1.0.0', '测试探照灯1');