This commit is contained in:
孙小云 2025-12-20 15:01:47 +08:00
parent 5c2fcf5e34
commit 041ed5ff46
26 changed files with 2600 additions and 1 deletions

View File

@ -11,7 +11,7 @@
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>tuoheng-airport-ddd</artifactId>
<artifactId>tuoheng-ddd</artifactId>
<packaging>jar</packaging>
<name>Tuoheng Airport DDD</name>

View File

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

View File

@ -0,0 +1,55 @@
package com.tuoheng.airport.device.application.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 设备创建请求DTOApplication层
* 用于接收前端创建设备的请求参数
*
* @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;
}

View File

@ -0,0 +1,43 @@
package com.tuoheng.airport.device.application.dto;
import lombok.Data;
/**
* 设备查询请求DTOApplication层
* 用于接收前端查询设备的请求参数
*
* @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;
}

View File

@ -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;
/**
* 设备响应DTOApplication层
* 用于返回给前端的设备信息
*
* @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;
}

View File

@ -0,0 +1,46 @@
package com.tuoheng.airport.device.application.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 设备更新请求DTOApplication层
* 用于接收前端更新设备的请求参数
*
* @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;
}

View File

@ -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<DeviceResponse> getAllDevices();
/**
* 根据条件查询设备列表
*
* @param request 查询请求
* @return 设备列表
*/
List<DeviceResponse> queryDevices(DeviceQueryRequest request);
/**
* 根据机场ID查询设备列表
*
* @param airportId 机场ID
* @return 设备列表
*/
List<DeviceResponse> 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);
}

View File

@ -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<DeviceResponse> getAllDevices() {
log.info("Application: 查询所有设备");
// 调用 Domain Service 查询所有激活状态的设备
List<Device> devices = deviceDomainService.getDevicesByStatus(DeviceStatus.ACTIVE);
return devices.stream()
.map(DeviceDtoConverter::toResponse)
.collect(Collectors.toList());
}
@Override
public List<DeviceResponse> queryDevices(DeviceQueryRequest request) {
log.info("Application: 根据条件查询设备列表,查询条件: {}", request);
List<Device> 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<DeviceResponse> getDevicesByAirportId(Long airportId) {
log.info("Application: 查询机场下的设备列表机场ID: {}", airportId);
// 调用 Domain Service 查询机场下的可用设备
List<Device> 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;
}
}

View File

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

View File

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

View File

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

View File

@ -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<Device> findById(Long id);
/**
* 根据设备编码查询设备
*
* @param deviceCode 设备编码
* @return 设备领域模型
*/
Optional<Device> findByDeviceCode(String deviceCode);
/**
* 查询所有设备
*
* @return 设备列表
*/
List<Device> findAll();
/**
* 根据机场ID查询设备列表
*
* @param airportId 机场ID
* @return 设备列表
*/
List<Device> findByAirportId(Long airportId);
/**
* 根据设备状态查询设备列表
*
* @param status 设备状态
* @return 设备列表
*/
List<Device> findByStatus(DeviceStatus status);
/**
* 根据设备类型查询设备列表
*
* @param deviceType 设备类型
* @return 设备列表
*/
List<Device> findByDeviceType(DeviceType deviceType);
/**
* 根据机场ID和状态查询设备列表
*
* @param airportId 机场ID
* @param status 设备状态
* @return 设备列表
*/
List<Device> 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);
}

View File

@ -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<Device> getAvailableDevicesByAirport(Long airportId);
/**
* 查询指定类型的设备列表
*
* @param deviceType 设备类型
* @return 设备列表
*/
List<Device> getDevicesByType(DeviceType deviceType);
/**
* 查询指定状态的设备列表
*
* @param status 设备状态
* @return 设备列表
*/
List<Device> 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);
}

View File

@ -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<Device> getAvailableDevicesByAirport(Long airportId) {
log.debug("Domain: 查询机场下的可用设备机场ID: {}", airportId);
// 业务规则只返回激活状态的设备
return deviceRepository.findByAirportIdAndStatus(airportId, DeviceStatus.ACTIVE);
}
@Override
public List<Device> getDevicesByType(DeviceType deviceType) {
log.debug("Domain: 查询指定类型的设备,设备类型: {}", deviceType);
return deviceRepository.findByDeviceType(deviceType);
}
@Override
public List<Device> 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 > v20 表示 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;
}
}

View File

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

View File

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

View File

@ -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<DeviceEntity> {
/**
* 根据机场ID和状态查询设备列表
*
* @param airportId 机场ID
* @param status 设备状态
* @return 设备实体列表
*/
List<DeviceEntity> 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<Long> deviceIds,
@Param("status") Integer status);
}

View File

@ -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<Device> findById(Long id) {
DeviceEntity entity = deviceMapper.selectById(id);
return Optional.ofNullable(DeviceConverter.toDomain(entity));
}
@Override
public Optional<Device> findByDeviceCode(String deviceCode) {
LambdaQueryWrapper<DeviceEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DeviceEntity::getDeviceCode, deviceCode);
DeviceEntity entity = deviceMapper.selectOne(wrapper);
return Optional.ofNullable(DeviceConverter.toDomain(entity));
}
@Override
public List<Device> findAll() {
List<DeviceEntity> entities = deviceMapper.selectList(null);
return entities.stream()
.map(DeviceConverter::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Device> findByAirportId(Long airportId) {
LambdaQueryWrapper<DeviceEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DeviceEntity::getAirportId, airportId)
.orderByDesc(DeviceEntity::getCreateTime);
List<DeviceEntity> entities = deviceMapper.selectList(wrapper);
return entities.stream()
.map(DeviceConverter::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Device> findByStatus(DeviceStatus status) {
LambdaQueryWrapper<DeviceEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DeviceEntity::getStatus, status.getCode())
.orderByDesc(DeviceEntity::getCreateTime);
List<DeviceEntity> entities = deviceMapper.selectList(wrapper);
return entities.stream()
.map(DeviceConverter::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Device> findByDeviceType(DeviceType deviceType) {
LambdaQueryWrapper<DeviceEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DeviceEntity::getDeviceType, deviceType.getCode())
.orderByDesc(DeviceEntity::getCreateTime);
List<DeviceEntity> entities = deviceMapper.selectList(wrapper);
return entities.stream()
.map(DeviceConverter::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Device> findByAirportIdAndStatus(Long airportId, DeviceStatus status) {
List<DeviceEntity> 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<DeviceEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DeviceEntity::getDeviceCode, deviceCode);
return deviceMapper.selectCount(wrapper) > 0;
}
@Override
public long countByAirportId(Long airportId) {
return deviceMapper.countByAirportId(airportId);
}
}

View File

@ -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<DeviceVO> 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<DeviceVO> 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<DeviceVO> 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<DeviceVO> 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<List<DeviceVO>> queryDevices(DeviceQueryVO vo) {
log.info("接收到查询设备列表请求,查询条件: {}", vo);
// VO DTO
DeviceQueryRequest request = DeviceVoConverter.toQueryRequest(vo);
// 调用 Application
List<DeviceResponse> responses = deviceApplicationService.queryDevices(request);
// DTO 列表转 VO 列表
List<DeviceVO> results = responses.stream()
.map(DeviceVoConverter::toVO)
.collect(Collectors.toList());
return Result.success(results);
}
/**
* 根据机场ID查询设备列表
*/
@GetMapping("/airport/{airportId}")
@Operation(summary = "查询机场设备", description = "查询指定机场下的所有设备")
public Result<List<DeviceVO>> getDevicesByAirportId(
@Parameter(description = "机场ID") @PathVariable Long airportId) {
log.info("接收到查询机场设备列表请求机场ID: {}", airportId);
List<DeviceResponse> responses = deviceApplicationService.getDevicesByAirportId(airportId);
List<DeviceVO> results = responses.stream()
.map(DeviceVoConverter::toVO)
.collect(Collectors.toList());
return Result.success(results);
}
/**
* 激活设备
*/
@PostMapping("/{id}/activate")
@Operation(summary = "激活设备", description = "将设备状态设置为激活")
public Result<DeviceVO> 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<DeviceVO> 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<DeviceVO> 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<DeviceVO> 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<DeviceVO> 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<Void> 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<Long> countDevicesByAirportId(
@Parameter(description = "机场ID") @PathVariable Long airportId) {
log.info("接收到统计机场设备数量请求机场ID: {}", airportId);
long count = deviceApplicationService.countDevicesByAirportId(airportId);
return Result.success(count);
}
}

View File

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

View File

@ -0,0 +1,55 @@
package com.tuoheng.airport.device.presentation.vo;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 设备创建请求VOPresentation层
* 用于接收前端创建设备的请求参数
*
* @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;
}

View File

@ -0,0 +1,43 @@
package com.tuoheng.airport.device.presentation.vo;
import lombok.Data;
/**
* 设备查询请求VOPresentation层
* 用于接收前端查询设备的请求参数
*
* @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;
}

View File

@ -0,0 +1,46 @@
package com.tuoheng.airport.device.presentation.vo;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 设备更新请求VOPresentation层
* 用于接收前端更新设备的请求参数
*
* @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;
}

View File

@ -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;
/**
* 设备响应VOPresentation层
* 用于返回给前端的设备信息
*
* @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;
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tuoheng.airport.device.infrastructure.persistence.mapper.DeviceMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="com.tuoheng.airport.device.infrastructure.persistence.entity.DeviceEntity">
<id column="id" property="id"/>
<result column="device_code" property="deviceCode"/>
<result column="device_name" property="deviceName"/>
<result column="device_type" property="deviceType"/>
<result column="status" property="status"/>
<result column="airport_id" property="airportId"/>
<result column="manufacturer" property="manufacturer"/>
<result column="firmware_version" property="firmwareVersion"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="remark" property="remark"/>
<result column="is_deleted" property="isDeleted"/>
</resultMap>
<!-- 根据机场ID和状态查询设备列表 -->
<select id="selectByAirportIdAndStatus" resultMap="BaseResultMap">
SELECT *
FROM th_device
WHERE is_deleted = 0
AND airport_id = #{airportId}
AND status = #{status}
ORDER BY create_time DESC
</select>
<!-- 统计机场下的设备数量 -->
<select id="countByAirportId" resultType="java.lang.Long">
SELECT COUNT(*)
FROM th_device
WHERE is_deleted = 0
AND airport_id = #{airportId}
</select>
<!-- 批量更新设备状态 -->
<update id="batchUpdateStatus">
UPDATE th_device
SET status = #{status},
update_time = NOW()
WHERE id IN
<foreach collection="deviceIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND is_deleted = 0
</update>
</mapper>

View File

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