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');