diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineCreateRequest.java b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineCreateRequest.java new file mode 100644 index 0000000..06ea6ed --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineCreateRequest.java @@ -0,0 +1,40 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 航线创建请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineCreateRequest { + + /** + * 航线编码 + */ + private String airlineCode; + + /** + * 航线名称 + */ + private String airlineName; + + /** + * 描述 + */ + private String description; + + /** + * 航点列表 + */ + private List waypoints; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineQueryRequest.java b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineQueryRequest.java new file mode 100644 index 0000000..f9cfbac --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineQueryRequest.java @@ -0,0 +1,38 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 航线查询请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineQueryRequest { + + /** + * 航线名称(模糊查询) + */ + private String airlineName; + + /** + * 航线状态 + */ + private String status; + + /** + * 创建人ID + */ + private Long creatorId; + + /** + * 租户ID + */ + private Long tenantId; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineResponse.java b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineResponse.java new file mode 100644 index 0000000..f23e866 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineResponse.java @@ -0,0 +1,36 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 航线响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineResponse { + + private Long id; + private String airlineCode; + private String airlineName; + private String description; + private String status; + private String fileUrl; + private Double totalDistance; + private Long estimatedDuration; + private Long tenantId; + private Long creatorId; + private Long reviewerId; + private String reviewComment; + private LocalDateTime reviewTime; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineStatisticsResponse.java b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineStatisticsResponse.java new file mode 100644 index 0000000..fbe1d34 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineStatisticsResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.Data; + +@Data +public class AirlineStatisticsResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineUpdateRequest.java b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineUpdateRequest.java new file mode 100644 index 0000000..96b35fa --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineUpdateRequest.java @@ -0,0 +1,33 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 航线更新请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineUpdateRequest { + + /** + * 航线ID + */ + private Long id; + + /** + * 航线名称 + */ + private String airlineName; + + /** + * 描述 + */ + private String description; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineUploadRequest.java b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineUploadRequest.java new file mode 100644 index 0000000..1bc62ab --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineUploadRequest.java @@ -0,0 +1,34 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +/** + * 航线文件上传请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineUploadRequest { + + /** + * 航线文件(KML/KMZ) + */ + private MultipartFile file; + + /** + * 航线名称 + */ + private String name; + + /** + * 描述 + */ + private String description; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineValidationResultResponse.java b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineValidationResultResponse.java new file mode 100644 index 0000000..f87ae0e --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/AirlineValidationResultResponse.java @@ -0,0 +1,45 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 航线验证结果响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineValidationResultResponse { + + /** + * 是否验证通过 + */ + private Boolean valid; + + /** + * 错误信息列表 + */ + private List errors; + + /** + * 警告信息列表 + */ + private List warnings; + + /** + * 是否穿越禁飞区 + */ + private Boolean crossesNoFlyZone; + + /** + * 是否有空域冲突 + */ + private Boolean hasAirspaceConflict; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/AirspaceConflictCheckResponse.java b/src/main/java/com/tuoheng/airport/airline/application/dto/AirspaceConflictCheckResponse.java new file mode 100644 index 0000000..44bb41f --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/AirspaceConflictCheckResponse.java @@ -0,0 +1,35 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 空域冲突检查响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirspaceConflictCheckResponse { + + /** + * 是否有冲突 + */ + private Boolean hasConflict; + + /** + * 冲突的飞行记录ID列表 + */ + private List conflictFlightRecordIds; + + /** + * 冲突描述 + */ + private String conflictDescription; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/EmergencyAirlineRequest.java b/src/main/java/com/tuoheng/airport/airline/application/dto/EmergencyAirlineRequest.java new file mode 100644 index 0000000..566e076 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/EmergencyAirlineRequest.java @@ -0,0 +1,48 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 紧急航线生成请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EmergencyAirlineRequest { + + /** + * 起点纬度 + */ + private Double startLatitude; + + /** + * 起点经度 + */ + private Double startLongitude; + + /** + * 终点纬度 + */ + private Double endLatitude; + + /** + * 终点经度 + */ + private Double endLongitude; + + /** + * 飞行高度(米) + */ + private Double altitude; + + /** + * 飞行速度(m/s) + */ + private Double speed; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/WaypointRequest.java b/src/main/java/com/tuoheng/airport/airline/application/dto/WaypointRequest.java new file mode 100644 index 0000000..9c43926 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/WaypointRequest.java @@ -0,0 +1,53 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 航点请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WaypointRequest { + + /** + * 航点索引 + */ + private Integer waypointIndex; + + /** + * 纬度 + */ + private Double latitude; + + /** + * 经度 + */ + private Double longitude; + + /** + * 高度(米) + */ + private Double altitude; + + /** + * 速度(m/s) + */ + private Double speed; + + /** + * 动作 + */ + private String action; + + /** + * 动作参数 + */ + private String actionParam; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/dto/WaypointResponse.java b/src/main/java/com/tuoheng/airport/airline/application/dto/WaypointResponse.java new file mode 100644 index 0000000..5bd968d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/dto/WaypointResponse.java @@ -0,0 +1,28 @@ +package com.tuoheng.airport.airline.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 航点响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WaypointResponse { + + private Long id; + private Long airlineId; + private Integer waypointIndex; + private Double latitude; + private Double longitude; + private Double altitude; + private Double speed; + private String action; + private String actionParam; +} diff --git a/src/main/java/com/tuoheng/airport/airline/application/service/AirlineApplicationService.java b/src/main/java/com/tuoheng/airport/airline/application/service/AirlineApplicationService.java new file mode 100644 index 0000000..d869cc2 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/application/service/AirlineApplicationService.java @@ -0,0 +1,226 @@ +package com.tuoheng.airport.airline.application.service; + +import com.tuoheng.airport.airline.application.dto.*; + +import java.util.List; + +/** + * 航线应用服务接口(Application层) + * 定义航线相关的用例(Use Cases) + * 协调领域模型完成航线管理操作 + * + * @author tuoheng + */ +public interface AirlineApplicationService { + + /** + * 创建航线 + * 业务逻辑: + * 1. 验证航线名称唯一性 + * 2. 验证航点数据合法性 + * 3. 创建航线记录 + * 4. 保存航点信息 + * + * @param request 创建请求 + * @return 航线响应 + */ + AirlineResponse createAirline(AirlineCreateRequest request); + + /** + * 上传航线文件 + * 业务逻辑: + * 1. 验证文件格式(KML/KMZ) + * 2. 解析航线文件 + * 3. 提取航点信息 + * 4. 创建航线记录 + * 5. 上传文件到存储服务 + * + * @param request 上传请求 + * @return 航线响应 + */ + AirlineResponse uploadAirlineFile(AirlineUploadRequest request); + + /** + * 更新航线信息 + * 业务逻辑: + * 1. 验证航线存在 + * 2. 检查航线状态(已审核的航线不能修改) + * 3. 更新航线基本信息 + * + * @param request 更新请求 + * @return 航线响应 + */ + AirlineResponse updateAirline(AirlineUpdateRequest request); + + /** + * 根据ID查询航线 + * + * @param id 航线ID + * @return 航线响应 + */ + AirlineResponse getAirlineById(Long id); + + /** + * 根据条件查询航线列表 + * + * @param request 查询请求 + * @return 航线列表 + */ + List queryAirlines(AirlineQueryRequest request); + + /** + * 删除航线 + * 业务逻辑: + * 1. 验证航线存在 + * 2. 检查航线是否被任务使用 + * 3. 逻辑删除航线 + * 4. 删除关联的航点 + * + * @param id 航线ID + */ + void deleteAirline(Long id); + + /** + * 验证航线 + * 业务逻辑: + * 1. 验证航点数量(至少2个) + * 2. 验证航点坐标合法性 + * 3. 验证飞行高度范围 + * 4. 验证飞行速度范围 + * 5. 检查空域冲突 + * 6. 检查禁飞区 + * + * @param id 航线ID + * @return 验证结果 + */ + AirlineValidationResultResponse validateAirline(Long id); + + /** + * 提交航线审核 + * 业务逻辑: + * 1. 验证航线合法性 + * 2. 更新航线状态为待审核 + * 3. 创建审核记录 + * 4. 发送审核通知 + * + * @param id 航线ID + * @return 航线响应 + */ + AirlineResponse submitAirlineForReview(Long id); + + /** + * 审核通过航线 + * 业务逻辑: + * 1. 验证航线状态为待审核 + * 2. 更新航线状态为已审核 + * 3. 记录审核意见 + * 4. 发送审核结果通知 + * + * @param id 航线ID + * @param comment 审核意见 + * @return 航线响应 + */ + AirlineResponse approveAirline(Long id, String comment); + + /** + * 审核拒绝航线 + * 业务逻辑: + * 1. 验证航线状态为待审核 + * 2. 更新航线状态为审核拒绝 + * 3. 记录拒绝原因 + * 4. 发送审核结果通知 + * + * @param id 航线ID + * @param reason 拒绝原因 + * @return 航线响应 + */ + AirlineResponse rejectAirline(Long id, String reason); + + /** + * 复制航线 + * 业务逻辑: + * 1. 验证源航线存在 + * 2. 复制航线基本信息 + * 3. 复制所有航点 + * 4. 新航线状态为草稿 + * + * @param id 源航线ID + * @param newName 新航线名称 + * @return 新航线响应 + */ + AirlineResponse copyAirline(Long id, String newName); + + /** + * 查询航线的航点列表 + * + * @param airlineId 航线ID + * @return 航点列表 + */ + List getAirlineWaypoints(Long airlineId); + + /** + * 计算航线飞行时长 + * 业务逻辑: + * 1. 获取航线的所有航点 + * 2. 计算航点间的距离 + * 3. 根据飞行速度计算时长 + * 4. 加上起降时间 + * + * @param airlineId 航线ID + * @param speed 飞行速度(m/s) + * @return 飞行时长(秒) + */ + Long calculateFlightDuration(Long airlineId, Double speed); + + /** + * 生成紧急航线 + * 业务逻辑: + * 1. 验证起点和终点坐标 + * 2. 使用最短路径算法生成航线 + * 3. 避开禁飞区和障碍物 + * 4. 创建临时航线记录 + * + * @param request 紧急航线请求 + * @return 航线响应 + */ + AirlineResponse generateEmergencyAirline(EmergencyAirlineRequest request); + + /** + * 下载航线文件 + * 业务逻辑: + * 1. 验证航线存在 + * 2. 生成KML/KMZ文件 + * 3. 返回文件流 + * + * @param id 航线ID + * @param format 文件格式(kml/kmz) + */ + void downloadAirlineFile(Long id, String format); + + /** + * 优化航线 + * 业务逻辑: + * 1. 分析航线路径 + * 2. 优化航点顺序 + * 3. 减少不必要的航点 + * 4. 平滑飞行路径 + * + * @param id 航线ID + * @return 优化后的航线 + */ + AirlineResponse optimizeAirline(Long id); + + /** + * 检查航线空域冲突 + * 业务逻辑: + * 1. 查询指定时间段内的其他飞行计划 + * 2. 检查航线是否有重叠 + * 3. 返回冲突信息 + * + * @param airlineId 航线ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 冲突检查结果 + */ + AirspaceConflictCheckResponse checkAirspaceConflict(Long airlineId, String startTime, String endTime); +} diff --git a/src/main/java/com/tuoheng/airport/airline/domain/model/Airline.java b/src/main/java/com/tuoheng/airport/airline/domain/model/Airline.java new file mode 100644 index 0000000..9bcc84d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/domain/model/Airline.java @@ -0,0 +1,96 @@ +package com.tuoheng.airport.airline.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 航线领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Airline { + + private Long id; + private String airlineCode; + private String airlineName; + private String description; + private AirlineStatus status; + private String fileUrl; + private Double totalDistance; + private Long estimatedDuration; + private Long tenantId; + private Long creatorId; + private Long reviewerId; + private String reviewComment; + private LocalDateTime reviewTime; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Boolean deleted; + + // ==================== 业务方法 ==================== + + public void submitForReview() { + if (this.status != AirlineStatus.DRAFT) { + throw new IllegalStateException("只有草稿状态的航线可以提交审核"); + } + this.status = AirlineStatus.PENDING_REVIEW; + this.updateTime = LocalDateTime.now(); + } + + public void approve(Long reviewerId, String comment) { + if (this.status != AirlineStatus.PENDING_REVIEW) { + throw new IllegalStateException("只有待审核状态的航线可以审核"); + } + this.status = AirlineStatus.APPROVED; + this.reviewerId = reviewerId; + this.reviewComment = comment; + this.reviewTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); + } + + public void reject(Long reviewerId, String reason) { + if (this.status != AirlineStatus.PENDING_REVIEW) { + throw new IllegalStateException("只有待审核状态的航线可以拒绝"); + } + this.status = AirlineStatus.REJECTED; + this.reviewerId = reviewerId; + this.reviewComment = reason; + this.reviewTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); + } + + public boolean isApproved() { + return this.status == AirlineStatus.APPROVED; + } + + public boolean canModify() { + return this.status == AirlineStatus.DRAFT || this.status == AirlineStatus.REJECTED; + } + + public void delete() { + this.deleted = true; + this.updateTime = LocalDateTime.now(); + } + + public static Airline create(String airlineCode, String airlineName, Long tenantId, Long creatorId) { + LocalDateTime now = LocalDateTime.now(); + return Airline.builder() + .airlineCode(airlineCode) + .airlineName(airlineName) + .status(AirlineStatus.DRAFT) + .tenantId(tenantId) + .creatorId(creatorId) + .createTime(now) + .updateTime(now) + .deleted(false) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/airline/domain/model/AirlineStatus.java b/src/main/java/com/tuoheng/airport/airline/domain/model/AirlineStatus.java new file mode 100644 index 0000000..9c51ba0 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/domain/model/AirlineStatus.java @@ -0,0 +1,24 @@ +package com.tuoheng.airport.airline.domain.model; + +/** + * 航线状态枚举 + * + * @author tuoheng + */ +public enum AirlineStatus { + + DRAFT("草稿"), + PENDING_REVIEW("待审核"), + APPROVED("已审核"), + REJECTED("已拒绝"); + + private final String description; + + AirlineStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/tuoheng/airport/airline/domain/model/AirlineValidationResult.java b/src/main/java/com/tuoheng/airport/airline/domain/model/AirlineValidationResult.java new file mode 100644 index 0000000..af11fe9 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/domain/model/AirlineValidationResult.java @@ -0,0 +1,53 @@ +package com.tuoheng.airport.airline.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 航线验证结果领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineValidationResult { + + /** + * 是否验证通过 + */ + private Boolean valid; + + /** + * 错误信息列表 + */ + private List errors; + + /** + * 警告信息列表 + */ + private List warnings; + + /** + * 是否穿越禁飞区 + */ + private Boolean crossesNoFlyZone; + + /** + * 是否有空域冲突 + */ + private Boolean hasAirspaceConflict; + + public boolean hasErrors() { + return errors != null && !errors.isEmpty(); + } + + public boolean hasWarnings() { + return warnings != null && !warnings.isEmpty(); + } +} diff --git a/src/main/java/com/tuoheng/airport/airline/domain/model/Waypoint.java b/src/main/java/com/tuoheng/airport/airline/domain/model/Waypoint.java new file mode 100644 index 0000000..18d76f1 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/domain/model/Waypoint.java @@ -0,0 +1,56 @@ +package com.tuoheng.airport.airline.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 航点领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Waypoint { + + private Long id; + private Long airlineId; + private Integer waypointIndex; + private Double latitude; + private Double longitude; + private Double altitude; + private Double speed; + private String action; + private String actionParam; + + // ==================== 业务方法 ==================== + + public void validateCoordinates() { + if (latitude == null || latitude < -90 || latitude > 90) { + throw new IllegalArgumentException("纬度必须在-90到90之间"); + } + if (longitude == null || longitude < -180 || longitude > 180) { + throw new IllegalArgumentException("经度必须在-180到180之间"); + } + if (altitude == null || altitude < 0 || altitude > 500) { + throw new IllegalArgumentException("高度必须在0到500米之间"); + } + } + + public static Waypoint create(Long airlineId, Integer waypointIndex, Double latitude, + Double longitude, Double altitude, Double speed) { + Waypoint waypoint = Waypoint.builder() + .airlineId(airlineId) + .waypointIndex(waypointIndex) + .latitude(latitude) + .longitude(longitude) + .altitude(altitude) + .speed(speed) + .build(); + waypoint.validateCoordinates(); + return waypoint; + } +} diff --git a/src/main/java/com/tuoheng/airport/airline/domain/repository/AirlineRepository.java b/src/main/java/com/tuoheng/airport/airline/domain/repository/AirlineRepository.java new file mode 100644 index 0000000..581d349 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/domain/repository/AirlineRepository.java @@ -0,0 +1,32 @@ +package com.tuoheng.airport.airline.domain.repository; + +import com.tuoheng.airport.airline.domain.model.Airline; + +import java.util.List; +import java.util.Optional; + +/** + * 航线仓储接口 + * + * @author tuoheng + */ +public interface AirlineRepository { + + Airline save(Airline airline); + + Optional findById(Long id); + + Optional findByAirlineCode(String airlineCode); + + List findByStatus(String status); + + List findByTenantId(Long tenantId); + + List findByCreatorId(Long creatorId); + + void delete(Long id); + + boolean existsByAirlineName(String airlineName, Long tenantId); + + long countByStatus(String status); +} diff --git a/src/main/java/com/tuoheng/airport/airline/domain/repository/WaypointRepository.java b/src/main/java/com/tuoheng/airport/airline/domain/repository/WaypointRepository.java new file mode 100644 index 0000000..a52caca --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/domain/repository/WaypointRepository.java @@ -0,0 +1,26 @@ +package com.tuoheng.airport.airline.domain.repository; + +import com.tuoheng.airport.airline.domain.model.Waypoint; + +import java.util.List; +import java.util.Optional; + +/** + * 航点仓储接口 + * + * @author tuoheng + */ +public interface WaypointRepository { + + Waypoint save(Waypoint waypoint); + + Optional findById(Long id); + + List findByAirlineId(Long airlineId); + + void delete(Long id); + + void deleteByAirlineId(Long airlineId); + + long countByAirlineId(Long airlineId); +} diff --git a/src/main/java/com/tuoheng/airport/airline/domain/service/AirlineDomainService.java b/src/main/java/com/tuoheng/airport/airline/domain/service/AirlineDomainService.java new file mode 100644 index 0000000..2367a93 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/domain/service/AirlineDomainService.java @@ -0,0 +1,305 @@ +package com.tuoheng.airport.airline.domain.service; + +import com.tuoheng.airport.airline.domain.model.Airline; +import com.tuoheng.airport.airline.domain.model.Waypoint; +import com.tuoheng.airport.airline.domain.model.AirlineValidationResult; + +import java.util.List; + +/** + * 航线领域服务接口(Domain层) + * 封装航线相关的业务逻辑和业务规则 + * + * Domain Service 的职责: + * 1. 封装航线管理的核心业务规则 + * 2. 协调航线和航点的关系 + * 3. 保证航线数据的一致性 + * 4. 不包含事务管理(事务由 Application 层管理) + * + * @author tuoheng + */ +public interface AirlineDomainService { + + /** + * 创建航线 + * 业务规则: + * 1. 航线名称不能重复(同一租户下) + * 2. 航点数量至少2个 + * 3. 航点顺序必须连续 + * 4. 新航线默认为草稿状态 + * + * @param airline 航线领域模型 + * @return 创建后的航线 + */ + Airline createAirline(Airline airline); + + /** + * 更新航线信息 + * 业务规则: + * 1. 已审核的航线不能修改 + * 2. 不能修改航线编码 + * 3. 修改后需要重新审核 + * + * @param airline 航线领域模型 + * @return 更新后的航线 + */ + Airline updateAirline(Airline airline); + + /** + * 删除航线 + * 业务规则: + * 1. 检查航线是否被任务使用 + * 2. 被使用的航线不能删除 + * 3. 逻辑删除航线 + * 4. 删除关联的航点 + * + * @param airlineId 航线ID + */ + void deleteAirline(Long airlineId); + + /** + * 验证航线合法性 + * 业务规则: + * 1. 航点数量至少2个 + * 2. 航点坐标必须合法(经纬度范围) + * 3. 飞行高度在允许范围内(0-500米) + * 4. 飞行速度在允许范围内(1-20m/s) + * 5. 不能穿越禁飞区 + * 6. 检查空域冲突 + * + * @param airlineId 航线ID + * @return 验证结果 + */ + AirlineValidationResult validateAirline(Long airlineId); + + /** + * 提交航线审核 + * 业务规则: + * 1. 航线必须通过验证 + * 2. 更新航线状态为待审核 + * 3. 记录提交时间和提交人 + * + * @param airlineId 航线ID + * @return 更新后的航线 + */ + Airline submitForReview(Long airlineId); + + /** + * 审核通过航线 + * 业务规则: + * 1. 航线状态必须为待审核 + * 2. 更新航线状态为已审核 + * 3. 记录审核时间和审核人 + * + * @param airlineId 航线ID + * @param reviewerId 审核人ID + * @param comment 审核意见 + * @return 更新后的航线 + */ + Airline approveAirline(Long airlineId, Long reviewerId, String comment); + + /** + * 审核拒绝航线 + * 业务规则: + * 1. 航线状态必须为待审核 + * 2. 更新航线状态为审核拒绝 + * 3. 记录拒绝原因 + * + * @param airlineId 航线ID + * @param reviewerId 审核人ID + * @param reason 拒绝原因 + * @return 更新后的航线 + */ + Airline rejectAirline(Long airlineId, Long reviewerId, String reason); + + /** + * 复制航线 + * 业务规则: + * 1. 复制航线基本信息 + * 2. 复制所有航点 + * 3. 新航线状态为草稿 + * 4. 新航线名称不能重复 + * + * @param sourceAirlineId 源航线ID + * @param newName 新航线名称 + * @return 新航线 + */ + Airline copyAirline(Long sourceAirlineId, String newName); + + /** + * 查询航线详情 + * + * @param airlineId 航线ID + * @return 航线领域模型 + */ + Airline getAirlineById(Long airlineId); + + /** + * 查询航线的航点列表 + * + * @param airlineId 航线ID + * @return 航点列表(按顺序排序) + */ + List getAirlineWaypoints(Long airlineId); + + /** + * 添加航点到航线 + * 业务规则: + * 1. 航线必须是草稿状态 + * 2. 航点顺序自动递增 + * 3. 验证航点坐标合法性 + * + * @param airlineId 航线ID + * @param waypoint 航点 + * @return 添加后的航点 + */ + Waypoint addWaypoint(Long airlineId, Waypoint waypoint); + + /** + * 更新航点信息 + * 业务规则: + * 1. 航线必须是草稿状态 + * 2. 验证航点坐标合法性 + * + * @param waypoint 航点 + * @return 更新后的航点 + */ + Waypoint updateWaypoint(Waypoint waypoint); + + /** + * 删除航点 + * 业务规则: + * 1. 航线必须是草稿状态 + * 2. 删除后重新排序剩余航点 + * 3. 航点数量不能少于2个 + * + * @param waypointId 航点ID + */ + void deleteWaypoint(Long waypointId); + + /** + * 计算航线总距离 + * 根据航点坐标计算航线的总飞行距离 + * + * @param airlineId 航线ID + * @return 总距离(米) + */ + Double calculateTotalDistance(Long airlineId); + + /** + * 计算航线飞行时长 + * 根据航线距离和飞行速度计算预计飞行时长 + * + * @param airlineId 航线ID + * @param speed 飞行速度(m/s) + * @return 飞行时长(秒) + */ + Long calculateFlightDuration(Long airlineId, Double speed); + + /** + * 生成紧急航线 + * 业务规则: + * 1. 使用Dijkstra最短路径算法 + * 2. 避开禁飞区 + * 3. 避开障碍物 + * 4. 生成临时航线(不保存到数据库) + * + * @param startLat 起点纬度 + * @param startLng 起点经度 + * @param endLat 终点纬度 + * @param endLng 终点经度 + * @param altitude 飞行高度 + * @return 紧急航线 + */ + Airline generateEmergencyAirline(Double startLat, Double startLng, Double endLat, Double endLng, Double altitude); + + /** + * 优化航线 + * 业务规则: + * 1. 移除冗余航点 + * 2. 平滑飞行路径 + * 3. 优化转弯角度 + * 4. 保持航线总体方向不变 + * + * @param airlineId 航线ID + * @return 优化后的航线 + */ + Airline optimizeAirline(Long airlineId); + + /** + * 检查航线是否穿越禁飞区 + * 业务规则: + * 1. 查询所有禁飞区 + * 2. 检查航线路径是否与禁飞区相交 + * + * @param airlineId 航线ID + * @return 是否穿越禁飞区 + */ + boolean crossesNoFlyZone(Long airlineId); + + /** + * 检查航线空域冲突 + * 业务规则: + * 1. 查询指定时间段内的其他飞行计划 + * 2. 检查航线是否有重叠 + * 3. 检查飞行高度是否有冲突 + * + * @param airlineId 航线ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 是否有冲突 + */ + boolean hasAirspaceConflict(Long airlineId, String startTime, String endTime); + + /** + * 检查航线名称是否存在 + * + * @param name 航线名称 + * @param tenantId 租户ID + * @return 是否存在 + */ + boolean isAirlineNameExists(String name, Long tenantId); + + /** + * 检查航线是否被使用 + * 业务规则: + * 1. 检查是否有任务使用该航线 + * 2. 被使用的航线不能删除 + * + * @param airlineId 航线ID + * @return 是否被使用 + */ + boolean isAirlineInUse(Long airlineId); + + /** + * 解析航线文件 + * 从KML/KMZ文件中解析航点信息 + * + * @param fileContent 文件内容 + * @param fileType 文件类型(kml/kmz) + * @return 航点列表 + */ + List parseAirlineFile(String fileContent, String fileType); + + /** + * 生成航线文件 + * 将航线和航点信息导出为KML/KMZ文件 + * + * @param airlineId 航线ID + * @param fileType 文件类型(kml/kmz) + * @return 文件内容 + */ + String generateAirlineFile(Long airlineId, String fileType); + + /** + * 计算两点之间的距离 + * 使用Haversine公式计算地球表面两点间的距离 + * + * @param lat1 点1纬度 + * @param lng1 点1经度 + * @param lat2 点2纬度 + * @param lng2 点2经度 + * @return 距离(米) + */ + Double calculateDistance(Double lat1, Double lng1, Double lat2, Double lng2); +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/controller/AirlineController.java b/src/main/java/com/tuoheng/airport/airline/presentation/controller/AirlineController.java new file mode 100644 index 0000000..743a98c --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/controller/AirlineController.java @@ -0,0 +1,265 @@ +package com.tuoheng.airport.airline.presentation.controller; + +import com.tuoheng.airport.airline.application.dto.*; +import com.tuoheng.airport.airline.application.service.AirlineApplicationService; +import com.tuoheng.airport.airline.presentation.converter.AirlineVoConverter; +import com.tuoheng.airport.airline.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 org.springframework.web.multipart.MultipartFile; + +import javax.validation.Valid; +import java.util.List; + +/** + * 航线管理控制器(Presentation层) + * 提供航线管理的 REST API 接口 + * 负责航线、航点、空域的管理 + * + * 职责: + * 1. 接收前端的 VO 对象 + * 2. 将 VO 转换为 DTO 传递给 Application 层 + * 3. 将 Application 层返回的 DTO 转换为 VO 返回给前端 + * 4. 不包含任何业务逻辑 + * + * @author tuoheng + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/airlines") +@RequiredArgsConstructor +@Validated +@Tag(name = "航线管理", description = "航线管理相关接口") +public class AirlineController { + + private final AirlineApplicationService airlineApplicationService; + + /** + * 创建航线 + * POST /api/v1/airlines + */ + @PostMapping + @Operation(summary = "创建航线", description = "创建新的飞行航线") + public Result createAirline(@Valid @RequestBody AirlineCreateVO vo) { + log.info("接收到创建航线请求: {}", vo); + AirlineCreateRequest request = AirlineVoConverter.toCreateRequest(vo); + AirlineResponse response = airlineApplicationService.createAirline(request); + AirlineVO result = AirlineVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 上传航线文件 + * POST /api/v1/airlines/upload + */ + @PostMapping("/upload") + @Operation(summary = "上传航线文件", description = "上传KML/KMZ格式的航线文件") + public Result uploadAirlineFile( + @Parameter(description = "航线文件") @RequestParam("file") MultipartFile file, + @Parameter(description = "航线名称") @RequestParam String name, + @Parameter(description = "航线描述") @RequestParam(required = false) String description) { + log.info("接收到上传航线文件请求,文件名: {}, 航线名称: {}", file.getOriginalFilename(), name); + + AirlineUploadRequest request = AirlineUploadRequest.builder() + .file(file) + .name(name) + .description(description) + .build(); + + AirlineResponse response = airlineApplicationService.uploadAirlineFile(request); + AirlineVO result = AirlineVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 更新航线信息 + * PUT /api/v1/airlines/{id} + */ + @PutMapping("/{id}") + @Operation(summary = "更新航线信息", description = "更新指定航线的基本信息") + public Result updateAirline( + @Parameter(description = "航线ID") @PathVariable Long id, + @Valid @RequestBody AirlineUpdateVO vo) { + log.info("接收到更新航线请求,航线ID: {}, 请求参数: {}", id, vo); + vo.setId(id); + AirlineUpdateRequest request = AirlineVoConverter.toUpdateRequest(vo); + AirlineResponse response = airlineApplicationService.updateAirline(request); + AirlineVO result = AirlineVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询航线详情 + * GET /api/v1/airlines/{id} + */ + @GetMapping("/{id}") + @Operation(summary = "查询航线详情", description = "根据航线ID查询航线详细信息") + public Result getAirlineById( + @Parameter(description = "航线ID") @PathVariable Long id) { + log.info("接收到查询航线请求,航线ID: {}", id); + AirlineResponse response = airlineApplicationService.getAirlineById(id); + AirlineVO result = AirlineVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询航线列表 + * GET /api/v1/airlines + */ + @GetMapping + @Operation(summary = "查询航线列表", description = "根据条件查询航线列表") + public Result> queryAirlines(AirlineQueryVO vo) { + log.info("接收到查询航线列表请求,查询条件: {}", vo); + AirlineQueryRequest request = AirlineVoConverter.toQueryRequest(vo); + List responses = airlineApplicationService.queryAirlines(request); + List results = AirlineVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 删除航线 + * DELETE /api/v1/airlines/{id} + */ + @DeleteMapping("/{id}") + @Operation(summary = "删除航线", description = "删除指定的航线(逻辑删除)") + public Result deleteAirline( + @Parameter(description = "航线ID") @PathVariable Long id) { + log.info("接收到删除航线请求,航线ID: {}", id); + airlineApplicationService.deleteAirline(id); + return Result.success(); + } + + /** + * 验证航线 + * POST /api/v1/airlines/{id}/validate + */ + @PostMapping("/{id}/validate") + @Operation(summary = "验证航线", description = "验证航线的合法性和安全性") + public Result validateAirline( + @Parameter(description = "航线ID") @PathVariable Long id) { + log.info("接收到验证航线请求,航线ID: {}", id); + AirlineValidationResultResponse response = airlineApplicationService.validateAirline(id); + AirlineValidationResultVO result = AirlineVoConverter.toValidationResultVO(response); + return Result.success(result); + } + + /** + * 提交航线审核 + * POST /api/v1/airlines/{id}/submit-review + */ + @PostMapping("/{id}/submit-review") + @Operation(summary = "提交航线审核", description = "提交航线进行审核") + public Result submitAirlineForReview( + @Parameter(description = "航线ID") @PathVariable Long id) { + log.info("接收到提交航线审核请求,航线ID: {}", id); + AirlineResponse response = airlineApplicationService.submitAirlineForReview(id); + AirlineVO result = AirlineVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 审核通过航线 + * POST /api/v1/airlines/{id}/approve + */ + @PostMapping("/{id}/approve") + @Operation(summary = "审核通过航线", description = "审核通过指定的航线") + public Result approveAirline( + @Parameter(description = "航线ID") @PathVariable Long id, + @RequestParam(required = false) String comment) { + log.info("接收到审核通过航线请求,航线ID: {}, 审核意见: {}", id, comment); + AirlineResponse response = airlineApplicationService.approveAirline(id, comment); + AirlineVO result = AirlineVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 审核拒绝航线 + * POST /api/v1/airlines/{id}/reject + */ + @PostMapping("/{id}/reject") + @Operation(summary = "审核拒绝航线", description = "审核拒绝指定的航线") + public Result rejectAirline( + @Parameter(description = "航线ID") @PathVariable Long id, + @RequestParam String reason) { + log.info("接收到审核拒绝航线请求,航线ID: {}, 拒绝原因: {}", id, reason); + AirlineResponse response = airlineApplicationService.rejectAirline(id, reason); + AirlineVO result = AirlineVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 复制航线 + * POST /api/v1/airlines/{id}/copy + */ + @PostMapping("/{id}/copy") + @Operation(summary = "复制航线", description = "复制现有航线创建新航线") + public Result copyAirline( + @Parameter(description = "航线ID") @PathVariable Long id, + @RequestParam String newName) { + log.info("接收到复制航线请求,航线ID: {}, 新航线名称: {}", id, newName); + AirlineResponse response = airlineApplicationService.copyAirline(id, newName); + AirlineVO result = AirlineVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询航线的航点列表 + * GET /api/v1/airlines/{id}/waypoints + */ + @GetMapping("/{id}/waypoints") + @Operation(summary = "查询航线航点", description = "查询指定航线的所有航点") + public Result> getAirlineWaypoints( + @Parameter(description = "航线ID") @PathVariable Long id) { + log.info("接收到查询航线航点请求,航线ID: {}", id); + List responses = airlineApplicationService.getAirlineWaypoints(id); + List results = AirlineVoConverter.toWaypointVOList(responses); + return Result.success(results); + } + + /** + * 计算航线飞行时长 + * GET /api/v1/airlines/{id}/duration + */ + @GetMapping("/{id}/duration") + @Operation(summary = "计算飞行时长", description = "计算航线的预计飞行时长") + public Result calculateFlightDuration( + @Parameter(description = "航线ID") @PathVariable Long id, + @Parameter(description = "飞行速度(m/s)") @RequestParam(defaultValue = "10") Double speed) { + log.info("接收到计算飞行时长请求,航线ID: {}, 飞行速度: {}m/s", id, speed); + Long duration = airlineApplicationService.calculateFlightDuration(id, speed); + return Result.success(duration); + } + + /** + * 生成紧急航线 + * POST /api/v1/airlines/emergency + */ + @PostMapping("/emergency") + @Operation(summary = "生成紧急航线", description = "根据起点和终点动态生成紧急航线") + public Result generateEmergencyAirline(@Valid @RequestBody EmergencyAirlineRequestVO vo) { + log.info("接收到生成紧急航线请求: {}", vo); + EmergencyAirlineRequest request = AirlineVoConverter.toEmergencyRequest(vo); + AirlineResponse response = airlineApplicationService.generateEmergencyAirline(request); + AirlineVO result = AirlineVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 下载航线文件 + * GET /api/v1/airlines/{id}/download + */ + @GetMapping("/{id}/download") + @Operation(summary = "下载航线文件", description = "下载航线的KML/KMZ文件") + public void downloadAirlineFile( + @Parameter(description = "航线ID") @PathVariable Long id, + @Parameter(description = "文件格式") @RequestParam(defaultValue = "kml") String format) { + log.info("接收到下载航线文件请求,航线ID: {}, 格式: {}", id, format); + airlineApplicationService.downloadAirlineFile(id, format); + } +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/converter/AirlineVoConverter.java b/src/main/java/com/tuoheng/airport/airline/presentation/converter/AirlineVoConverter.java new file mode 100644 index 0000000..95178ef --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/converter/AirlineVoConverter.java @@ -0,0 +1,57 @@ +package com.tuoheng.airport.airline.presentation.converter; + +import com.tuoheng.airport.airline.application.dto.*; +import com.tuoheng.airport.airline.presentation.vo.*; +import java.util.List; +import java.util.stream.Collectors; + +public class AirlineVoConverter { + + public static AirlineCreateRequest toCreateRequest(AirlineCreateVO vo) { + // TODO: 实现转换逻辑 + return new AirlineCreateRequest(); + } + + public static AirlineUpdateRequest toUpdateRequest(AirlineUpdateVO vo) { + // TODO: 实现转换逻辑 + return new AirlineUpdateRequest(); + } + + public static AirlineQueryRequest toQueryRequest(AirlineQueryVO vo) { + // TODO: 实现转换逻辑 + return new AirlineQueryRequest(); + } + + public static AirlineVO toVO(AirlineResponse response) { + // TODO: 实现转换逻辑 + return new AirlineVO(); + } + + public static List toVOList(List responses) { + return responses.stream().map(AirlineVoConverter::toVO).collect(Collectors.toList()); + } + + public static AirlineStatisticsVO toStatisticsVO(AirlineStatisticsResponse response) { + // TODO: 实现转换逻辑 + return new AirlineStatisticsVO(); + } + + public static WaypointVO toWaypointVO(WaypointResponse response) { + // TODO: 实现转换逻辑 + return new WaypointVO(); + } + + public static List toWaypointVOList(List responses) { + return responses.stream().map(AirlineVoConverter::toWaypointVO).collect(Collectors.toList()); + } + + public static EmergencyAirlineRequest toEmergencyRequest(EmergencyAirlineRequestVO vo) { + // TODO: 实现转换逻辑 + return new EmergencyAirlineRequest(); + } + + public static AirlineValidationResultVO toValidationResultVO(AirlineValidationResultResponse response) { + // TODO: 实现转换逻辑 + return new AirlineValidationResultVO(); + } +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineCreateVO.java b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineCreateVO.java new file mode 100644 index 0000000..8020c99 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineCreateVO.java @@ -0,0 +1,45 @@ +package com.tuoheng.airport.airline.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 航线创建 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineCreateVO { + + /** + * 航线编码 + */ + @NotBlank(message = "航线编码不能为空") + private String airlineCode; + + /** + * 航线名称 + */ + @NotBlank(message = "航线名称不能为空") + private String airlineName; + + /** + * 描述 + */ + private String description; + + /** + * 航点列表 + */ + @NotNull(message = "航点列表不能为空") + private List waypoints; +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineQueryVO.java b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineQueryVO.java new file mode 100644 index 0000000..8588585 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineQueryVO.java @@ -0,0 +1,33 @@ +package com.tuoheng.airport.airline.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 航线查询 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineQueryVO { + + /** + * 航线名称(模糊查询) + */ + private String airlineName; + + /** + * 航线状态 + */ + private String status; + + /** + * 创建人ID + */ + private Long creatorId; +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineStatisticsVO.java b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineStatisticsVO.java new file mode 100644 index 0000000..8bfec71 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineStatisticsVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.airline.presentation.vo; + +import lombok.Data; + +@Data +public class AirlineStatisticsVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineUpdateVO.java b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineUpdateVO.java new file mode 100644 index 0000000..3406fed --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineUpdateVO.java @@ -0,0 +1,36 @@ +package com.tuoheng.airport.airline.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * 航线更新 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineUpdateVO { + + /** + * 航线ID + */ + private Long id; + + /** + * 航线名称 + */ + @NotBlank(message = "航线名称不能为空") + private String airlineName; + + /** + * 描述 + */ + private String description; +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineVO.java b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineVO.java new file mode 100644 index 0000000..80250bd --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineVO.java @@ -0,0 +1,36 @@ +package com.tuoheng.airport.airline.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 航线展示 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineVO { + + private Long id; + private String airlineCode; + private String airlineName; + private String description; + private String status; + private String fileUrl; + private Double totalDistance; + private Long estimatedDuration; + private Long tenantId; + private Long creatorId; + private Long reviewerId; + private String reviewComment; + private LocalDateTime reviewTime; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineValidationResultVO.java b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineValidationResultVO.java new file mode 100644 index 0000000..312dc70 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/vo/AirlineValidationResultVO.java @@ -0,0 +1,45 @@ +package com.tuoheng.airport.airline.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 航线验证结果 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AirlineValidationResultVO { + + /** + * 是否验证通过 + */ + private Boolean valid; + + /** + * 错误信息列表 + */ + private List errors; + + /** + * 警告信息列表 + */ + private List warnings; + + /** + * 是否穿越禁飞区 + */ + private Boolean crossesNoFlyZone; + + /** + * 是否有空域冲突 + */ + private Boolean hasAirspaceConflict; +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/vo/EmergencyAirlineRequestVO.java b/src/main/java/com/tuoheng/airport/airline/presentation/vo/EmergencyAirlineRequestVO.java new file mode 100644 index 0000000..0e08b03 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/vo/EmergencyAirlineRequestVO.java @@ -0,0 +1,55 @@ +package com.tuoheng.airport.airline.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * 紧急航线生成请求 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EmergencyAirlineRequestVO { + + /** + * 起点纬度 + */ + @NotNull(message = "起点纬度不能为空") + private Double startLatitude; + + /** + * 起点经度 + */ + @NotNull(message = "起点经度不能为空") + private Double startLongitude; + + /** + * 终点纬度 + */ + @NotNull(message = "终点纬度不能为空") + private Double endLatitude; + + /** + * 终点经度 + */ + @NotNull(message = "终点经度不能为空") + private Double endLongitude; + + /** + * 飞行高度(米) + */ + @NotNull(message = "飞行高度不能为空") + private Double altitude; + + /** + * 飞行速度(m/s) + */ + private Double speed; +} diff --git a/src/main/java/com/tuoheng/airport/airline/presentation/vo/WaypointVO.java b/src/main/java/com/tuoheng/airport/airline/presentation/vo/WaypointVO.java new file mode 100644 index 0000000..ca42dc7 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/airline/presentation/vo/WaypointVO.java @@ -0,0 +1,62 @@ +package com.tuoheng.airport.airline.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * 航点 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WaypointVO { + + private Long id; + private Long airlineId; + + /** + * 航点索引 + */ + @NotNull(message = "航点索引不能为空") + private Integer waypointIndex; + + /** + * 纬度 + */ + @NotNull(message = "纬度不能为空") + private Double latitude; + + /** + * 经度 + */ + @NotNull(message = "经度不能为空") + private Double longitude; + + /** + * 高度(米) + */ + @NotNull(message = "高度不能为空") + private Double altitude; + + /** + * 速度(m/s) + */ + private Double speed; + + /** + * 动作 + */ + private String action; + + /** + * 动作参数 + */ + private String actionParam; +} diff --git a/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalCreateRequest.java b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalCreateRequest.java new file mode 100644 index 0000000..29afbda --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalCreateRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.application.dto; + +import lombok.Data; + +@Data +public class ApprovalCreateRequest { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalDecisionRequest.java b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalDecisionRequest.java new file mode 100644 index 0000000..681e393 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalDecisionRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.application.dto; + +import lombok.Data; + +@Data +public class ApprovalDecisionRequest { + private String comment; +} diff --git a/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalHistoryResponse.java b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalHistoryResponse.java new file mode 100644 index 0000000..ff04da3 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalHistoryResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.application.dto; + +import lombok.Data; + +@Data +public class ApprovalHistoryResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalQueryRequest.java b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalQueryRequest.java new file mode 100644 index 0000000..70c8137 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalQueryRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.application.dto; + +import lombok.Data; + +@Data +public class ApprovalQueryRequest { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalResponse.java b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalResponse.java new file mode 100644 index 0000000..e0c9bc4 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.application.dto; + +import lombok.Data; + +@Data +public class ApprovalResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalStatisticsResponse.java b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalStatisticsResponse.java new file mode 100644 index 0000000..e996afe --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/application/dto/ApprovalStatisticsResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.application.dto; + +import lombok.Data; + +@Data +public class ApprovalStatisticsResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/application/dto/BatchApprovalRequest.java b/src/main/java/com/tuoheng/airport/approval/application/dto/BatchApprovalRequest.java new file mode 100644 index 0000000..cd23a19 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/application/dto/BatchApprovalRequest.java @@ -0,0 +1,10 @@ +package com.tuoheng.airport.approval.application.dto; + +import lombok.Data; +import java.util.List; + +@Data +public class BatchApprovalRequest { + private List approvalIds; + private String comment; +} diff --git a/src/main/java/com/tuoheng/airport/approval/application/dto/BatchApprovalResultResponse.java b/src/main/java/com/tuoheng/airport/approval/application/dto/BatchApprovalResultResponse.java new file mode 100644 index 0000000..9ab5d7a --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/application/dto/BatchApprovalResultResponse.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.approval.application.dto; + +import lombok.Data; + +@Data +public class BatchApprovalResultResponse { + private Integer successCount; + private Integer failureCount; +} diff --git a/src/main/java/com/tuoheng/airport/approval/application/service/ApprovalApplicationService.java b/src/main/java/com/tuoheng/airport/approval/application/service/ApprovalApplicationService.java new file mode 100644 index 0000000..e66860d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/application/service/ApprovalApplicationService.java @@ -0,0 +1,226 @@ +package com.tuoheng.airport.approval.application.service; + +import com.tuoheng.airport.approval.application.dto.*; + +import java.util.List; + +/** + * 审批应用服务接口(Application层) + * 定义审批流程相关的用例(Use Cases) + * 协调领域模型完成审批流程管理操作 + * + * @author tuoheng + */ +public interface ApprovalApplicationService { + + /** + * 创建审批申请 + * 业务逻辑: + * 1. 验证申请数据完整性 + * 2. 创建审批记录 + * 3. 初始化审批状态为草稿 + * 4. 关联业务对象(航线、设备等) + * + * @param request 创建请求 + * @return 审批响应 + */ + ApprovalResponse createApproval(ApprovalCreateRequest request); + + /** + * 提交审批申请 + * 业务逻辑: + * 1. 验证审批状态为草稿 + * 2. 验证申请数据完整性 + * 3. 确定审批人(根据审批类型和规则) + * 4. 更新审批状态为待审批 + * 5. 发送审批通知 + * + * @param id 审批ID + * @return 审批响应 + */ + ApprovalResponse submitApproval(Long id); + + /** + * 审批通过 + * 业务逻辑: + * 1. 验证审批状态为待审批 + * 2. 验证当前用户是审批人 + * 3. 记录审批意见 + * 4. 更新审批状态为已通过 + * 5. 执行业务回调(如航线审核通过) + * 6. 发送审批结果通知 + * + * @param id 审批ID + * @param request 审批决策请求 + * @return 审批响应 + */ + ApprovalResponse approveApproval(Long id, ApprovalDecisionRequest request); + + /** + * 审批拒绝 + * 业务逻辑: + * 1. 验证审批状态为待审批 + * 2. 验证当前用户是审批人 + * 3. 记录拒绝原因 + * 4. 更新审批状态为已拒绝 + * 5. 执行业务回调(如航线审核拒绝) + * 6. 发送审批结果通知 + * + * @param id 审批ID + * @param request 审批决策请求 + * @return 审批响应 + */ + ApprovalResponse rejectApproval(Long id, ApprovalDecisionRequest request); + + /** + * 撤回审批申请 + * 业务逻辑: + * 1. 验证审批状态为待审批 + * 2. 验证当前用户是申请人 + * 3. 更新审批状态为已撤回 + * 4. 记录撤回原因 + * 5. 发送撤回通知 + * + * @param id 审批ID + * @param reason 撤回原因 + * @return 审批响应 + */ + ApprovalResponse withdrawApproval(Long id, String reason); + + /** + * 转审 + * 业务逻辑: + * 1. 验证审批状态为待审批 + * 2. 验证当前用户是审批人 + * 3. 验证目标审批人有效 + * 4. 更新审批人 + * 5. 记录转审原因 + * 6. 发送转审通知 + * + * @param id 审批ID + * @param targetApproverId 目标审批人ID + * @param reason 转审原因 + * @return 审批响应 + */ + ApprovalResponse transferApproval(Long id, Long targetApproverId, String reason); + + /** + * 查询审批详情 + * + * @param id 审批ID + * @return 审批响应 + */ + ApprovalResponse getApprovalById(Long id); + + /** + * 根据条件查询审批列表 + * + * @param request 查询请求 + * @return 审批列表 + */ + List queryApprovals(ApprovalQueryRequest request); + + /** + * 查询我的待审批列表 + * 业务逻辑: + * 1. 获取当前用户ID + * 2. 查询审批人为当前用户且状态为待审批的记录 + * 3. 按提交时间倒序排序 + * + * @return 审批列表 + */ + List getMyPendingApprovals(); + + /** + * 查询我的申请列表 + * 业务逻辑: + * 1. 获取当前用户ID + * 2. 查询申请人为当前用户的记录 + * 3. 可按状态筛选 + * 4. 按提交时间倒序排序 + * + * @param status 审批状态(可选) + * @return 审批列表 + */ + List getMyApplications(String status); + + /** + * 查询审批历史 + * 业务逻辑: + * 1. 查询审批的所有流转记录 + * 2. 包括提交、审批、转审、撤回等操作 + * 3. 按时间顺序排序 + * + * @param id 审批ID + * @return 审批历史列表 + */ + List getApprovalHistory(Long id); + + /** + * 查询审批统计信息 + * 业务逻辑: + * 1. 统计审批总数 + * 2. 按状态分组统计(待审批、已通过、已拒绝) + * 3. 按审批类型分组统计 + * 4. 计算平均审批时长 + * 5. 按日期分组统计 + * + * @param approvalType 审批类型(可选) + * @param startDate 开始日期(可选) + * @param endDate 结束日期(可选) + * @return 审批统计信息 + */ + ApprovalStatisticsResponse getApprovalStatistics(String approvalType, String startDate, String endDate); + + /** + * 删除审批申请 + * 业务逻辑: + * 1. 验证审批状态为草稿 + * 2. 验证当前用户是申请人 + * 3. 逻辑删除审批记录 + * + * @param id 审批ID + */ + void deleteApproval(Long id); + + /** + * 批量审批 + * 业务逻辑: + * 1. 验证所有审批的状态和权限 + * 2. 批量更新审批状态 + * 3. 批量执行业务回调 + * 4. 批量发送通知 + * 5. 返回成功和失败的统计 + * + * @param request 批量审批请求 + * @return 批量审批结果 + */ + BatchApprovalResultResponse batchApprove(BatchApprovalRequest request); + + /** + * 催办审批 + * 业务逻辑: + * 1. 验证审批状态为待审批 + * 2. 验证当前用户是申请人 + * 3. 检查距离上次催办时间(至少间隔1小时) + * 4. 发送催办通知给审批人 + * + * @param id 审批ID + */ + void urgeApproval(Long id); + + /** + * 加签(增加审批人) + * 业务逻辑: + * 1. 验证审批状态为待审批 + * 2. 验证当前用户是审批人 + * 3. 添加新的审批人 + * 4. 发送加签通知 + * + * @param id 审批ID + * @param additionalApproverId 新增审批人ID + * @param reason 加签原因 + * @return 审批响应 + */ + ApprovalResponse addApprover(Long id, Long additionalApproverId, String reason); +} diff --git a/src/main/java/com/tuoheng/airport/approval/domain/model/Approval.java b/src/main/java/com/tuoheng/airport/approval/domain/model/Approval.java new file mode 100644 index 0000000..b3c9bb4 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/domain/model/Approval.java @@ -0,0 +1,131 @@ +package com.tuoheng.airport.approval.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 审批领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Approval { + + private Long id; + private String approvalCode; + private String approvalType; + private String title; + private String content; + private ApprovalStatus status; + private Long applicantId; + private Long approverId; + private String approvalComment; + private Long bizId; + private String bizType; + private Long tenantId; + private LocalDateTime submitTime; + private LocalDateTime approvalTime; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Boolean deleted; + + // ==================== 业务方法 ==================== + + public void submit(Long approverId) { + if (this.status != ApprovalStatus.DRAFT) { + throw new IllegalStateException("只有草稿状态的审批可以提交"); + } + this.status = ApprovalStatus.PENDING; + this.approverId = approverId; + this.submitTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); + } + + public void approve(Long approverId, String comment) { + if (this.status != ApprovalStatus.PENDING) { + throw new IllegalStateException("只有待审批状态的审批可以通过"); + } + if (!this.approverId.equals(approverId)) { + throw new IllegalStateException("只有审批人可以审批"); + } + this.status = ApprovalStatus.APPROVED; + this.approvalComment = comment; + this.approvalTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); + } + + public void reject(Long approverId, String reason) { + if (this.status != ApprovalStatus.PENDING) { + throw new IllegalStateException("只有待审批状态的审批可以拒绝"); + } + if (!this.approverId.equals(approverId)) { + throw new IllegalStateException("只有审批人可以审批"); + } + this.status = ApprovalStatus.REJECTED; + this.approvalComment = reason; + this.approvalTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); + } + + public void withdraw(Long applicantId, String reason) { + if (this.status != ApprovalStatus.PENDING) { + throw new IllegalStateException("只有待审批状态的审批可以撤回"); + } + if (!this.applicantId.equals(applicantId)) { + throw new IllegalStateException("只有申请人可以撤回"); + } + this.status = ApprovalStatus.WITHDRAWN; + this.approvalComment = reason; + this.updateTime = LocalDateTime.now(); + } + + public void transfer(Long currentApproverId, Long targetApproverId, String reason) { + if (this.status != ApprovalStatus.PENDING) { + throw new IllegalStateException("只有待审批状态的审批可以转审"); + } + if (!this.approverId.equals(currentApproverId)) { + throw new IllegalStateException("只有当前审批人可以转审"); + } + this.approverId = targetApproverId; + this.approvalComment = reason; + this.updateTime = LocalDateTime.now(); + } + + public boolean isPending() { + return this.status == ApprovalStatus.PENDING; + } + + public void delete() { + if (this.status != ApprovalStatus.DRAFT) { + throw new IllegalStateException("只有草稿状态的审批可以删除"); + } + this.deleted = true; + this.updateTime = LocalDateTime.now(); + } + + public static Approval create(String approvalCode, String approvalType, String title, + String content, Long applicantId, String bizType, Long bizId, Long tenantId) { + LocalDateTime now = LocalDateTime.now(); + return Approval.builder() + .approvalCode(approvalCode) + .approvalType(approvalType) + .title(title) + .content(content) + .status(ApprovalStatus.DRAFT) + .applicantId(applicantId) + .bizType(bizType) + .bizId(bizId) + .tenantId(tenantId) + .createTime(now) + .updateTime(now) + .deleted(false) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/approval/domain/model/ApprovalHistory.java b/src/main/java/com/tuoheng/airport/approval/domain/model/ApprovalHistory.java new file mode 100644 index 0000000..2ceeb0d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/domain/model/ApprovalHistory.java @@ -0,0 +1,40 @@ +package com.tuoheng.airport.approval.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 审批历史领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApprovalHistory { + + private Long id; + private Long approvalId; + private String action; + private Long operatorId; + private String operatorName; + private String comment; + private LocalDateTime operateTime; + + public static ApprovalHistory create(Long approvalId, String action, Long operatorId, + String operatorName, String comment) { + return ApprovalHistory.builder() + .approvalId(approvalId) + .action(action) + .operatorId(operatorId) + .operatorName(operatorName) + .comment(comment) + .operateTime(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/approval/domain/model/ApprovalStatistics.java b/src/main/java/com/tuoheng/airport/approval/domain/model/ApprovalStatistics.java new file mode 100644 index 0000000..07cce45 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/domain/model/ApprovalStatistics.java @@ -0,0 +1,68 @@ +package com.tuoheng.airport.approval.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 审批统计领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApprovalStatistics { + + /** + * 审批总数 + */ + private Long totalCount; + + /** + * 待审批数量 + */ + private Long pendingCount; + + /** + * 已通过数量 + */ + private Long approvedCount; + + /** + * 已拒绝数量 + */ + private Long rejectedCount; + + /** + * 已撤回数量 + */ + private Long withdrawnCount; + + /** + * 平均审批时长(小时) + */ + private Double averageApprovalDuration; + + /** + * 通过率 + */ + public double getApprovalRate() { + if (totalCount == null || totalCount == 0) { + return 0.0; + } + return (double) approvedCount / totalCount * 100; + } + + /** + * 拒绝率 + */ + public double getRejectionRate() { + if (totalCount == null || totalCount == 0) { + return 0.0; + } + return (double) rejectedCount / totalCount * 100; + } +} diff --git a/src/main/java/com/tuoheng/airport/approval/domain/model/ApprovalStatus.java b/src/main/java/com/tuoheng/airport/approval/domain/model/ApprovalStatus.java new file mode 100644 index 0000000..988fe83 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/domain/model/ApprovalStatus.java @@ -0,0 +1,25 @@ +package com.tuoheng.airport.approval.domain.model; + +/** + * 审批状态枚举 + * + * @author tuoheng + */ +public enum ApprovalStatus { + + DRAFT("草稿"), + PENDING("待审批"), + APPROVED("已通过"), + REJECTED("已拒绝"), + WITHDRAWN("已撤回"); + + private final String description; + + ApprovalStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/tuoheng/airport/approval/domain/repository/ApprovalHistoryRepository.java b/src/main/java/com/tuoheng/airport/approval/domain/repository/ApprovalHistoryRepository.java new file mode 100644 index 0000000..11d10ed --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/domain/repository/ApprovalHistoryRepository.java @@ -0,0 +1,19 @@ +package com.tuoheng.airport.approval.domain.repository; + +import com.tuoheng.airport.approval.domain.model.ApprovalHistory; + +import java.util.List; + +/** + * 审批历史仓储接口 + * + * @author tuoheng + */ +public interface ApprovalHistoryRepository { + + ApprovalHistory save(ApprovalHistory approvalHistory); + + List findByApprovalId(Long approvalId); + + void deleteByApprovalId(Long approvalId); +} diff --git a/src/main/java/com/tuoheng/airport/approval/domain/repository/ApprovalRepository.java b/src/main/java/com/tuoheng/airport/approval/domain/repository/ApprovalRepository.java new file mode 100644 index 0000000..0204f94 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/domain/repository/ApprovalRepository.java @@ -0,0 +1,36 @@ +package com.tuoheng.airport.approval.domain.repository; + +import com.tuoheng.airport.approval.domain.model.Approval; + +import java.util.List; +import java.util.Optional; + +/** + * 审批仓储接口 + * + * @author tuoheng + */ +public interface ApprovalRepository { + + Approval save(Approval approval); + + Optional findById(Long id); + + Optional findByApprovalCode(String approvalCode); + + List findByApplicantId(Long applicantId); + + List findByApproverId(Long approverId); + + List findByStatus(String status); + + List findByApprovalType(String approvalType); + + List findByBizTypeAndBizId(String bizType, Long bizId); + + void delete(Long id); + + long countByStatus(String status); + + long countByApprovalType(String approvalType); +} diff --git a/src/main/java/com/tuoheng/airport/approval/domain/service/ApprovalDomainService.java b/src/main/java/com/tuoheng/airport/approval/domain/service/ApprovalDomainService.java new file mode 100644 index 0000000..4aa7c6d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/domain/service/ApprovalDomainService.java @@ -0,0 +1,296 @@ +package com.tuoheng.airport.approval.domain.service; + +import com.tuoheng.airport.approval.domain.model.Approval; +import com.tuoheng.airport.approval.domain.model.ApprovalHistory; +import com.tuoheng.airport.approval.domain.model.ApprovalStatistics; + +import java.util.List; + +/** + * 审批领域服务接口(Domain层) + * 封装审批流程相关的业务逻辑和业务规则 + * + * Domain Service 的职责: + * 1. 封装审批流程的核心业务规则 + * 2. 协调审批记录和审批历史 + * 3. 保证审批数据的一致性 + * 4. 不包含事务管理(事务由 Application 层管理) + * + * @author tuoheng + */ +public interface ApprovalDomainService { + + /** + * 创建审批申请 + * 业务规则: + * 1. 生成唯一的审批编号 + * 2. 初始化审批状态为草稿 + * 3. 记录申请人和申请时间 + * 4. 关联业务对象 + * + * @param approval 审批领域模型 + * @return 创建后的审批 + */ + Approval createApproval(Approval approval); + + /** + * 提交审批申请 + * 业务规则: + * 1. 验证审批状态为草稿 + * 2. 验证申请数据完整性 + * 3. 确定审批人(根据审批类型和规则) + * 4. 更新审批状态为待审批 + * 5. 记录提交时间 + * 6. 创建审批历史记录 + * + * @param approvalId 审批ID + * @return 更新后的审批 + */ + Approval submitApproval(Long approvalId); + + /** + * 审批通过 + * 业务规则: + * 1. 验证审批状态为待审批 + * 2. 验证审批人权限 + * 3. 更新审批状态为已通过 + * 4. 记录审批意见和审批时间 + * 5. 创建审批历史记录 + * + * @param approvalId 审批ID + * @param approverId 审批人ID + * @param comment 审批意见 + * @return 更新后的审批 + */ + Approval approveApproval(Long approvalId, Long approverId, String comment); + + /** + * 审批拒绝 + * 业务规则: + * 1. 验证审批状态为待审批 + * 2. 验证审批人权限 + * 3. 更新审批状态为已拒绝 + * 4. 记录拒绝原因和审批时间 + * 5. 创建审批历史记录 + * + * @param approvalId 审批ID + * @param approverId 审批人ID + * @param reason 拒绝原因 + * @return 更新后的审批 + */ + Approval rejectApproval(Long approvalId, Long approverId, String reason); + + /** + * 撤回审批申请 + * 业务规则: + * 1. 验证审批状态为待审批 + * 2. 验证申请人权限 + * 3. 更新审批状态为已撤回 + * 4. 记录撤回原因和时间 + * 5. 创建审批历史记录 + * + * @param approvalId 审批ID + * @param applicantId 申请人ID + * @param reason 撤回原因 + * @return 更新后的审批 + */ + Approval withdrawApproval(Long approvalId, Long applicantId, String reason); + + /** + * 转审 + * 业务规则: + * 1. 验证审批状态为待审批 + * 2. 验证当前审批人权限 + * 3. 验证目标审批人有效 + * 4. 更新审批人 + * 5. 记录转审原因和时间 + * 6. 创建审批历史记录 + * + * @param approvalId 审批ID + * @param currentApproverId 当前审批人ID + * @param targetApproverId 目标审批人ID + * @param reason 转审原因 + * @return 更新后的审批 + */ + Approval transferApproval(Long approvalId, Long currentApproverId, Long targetApproverId, String reason); + + /** + * 加签(增加审批人) + * 业务规则: + * 1. 验证审批状态为待审批 + * 2. 验证当前审批人权限 + * 3. 添加新的审批人 + * 4. 记录加签原因和时间 + * 5. 创建审批历史记录 + * + * @param approvalId 审批ID + * @param currentApproverId 当前审批人ID + * @param additionalApproverId 新增审批人ID + * @param reason 加签原因 + * @return 更新后的审批 + */ + Approval addApprover(Long approvalId, Long currentApproverId, Long additionalApproverId, String reason); + + /** + * 删除审批申请 + * 业务规则: + * 1. 只能删除草稿状态的审批 + * 2. 验证申请人权限 + * 3. 逻辑删除审批记录 + * + * @param approvalId 审批ID + * @param applicantId 申请人ID + */ + void deleteApproval(Long approvalId, Long applicantId); + + /** + * 查询审批详情 + * + * @param approvalId 审批ID + * @return 审批领域模型 + */ + Approval getApprovalById(Long approvalId); + + /** + * 查询审批历史 + * + * @param approvalId 审批ID + * @return 审批历史列表 + */ + List getApprovalHistory(Long approvalId); + + /** + * 确定审批人 + * 业务规则: + * 1. 根据审批类型确定审批人 + * 2. 航线审批:航线管理员 + * 3. 设备审批:设备管理员 + * 4. 任务审批:任务管理员 + * 5. 支持多级审批 + * + * @param approvalType 审批类型 + * @param bizId 业务ID + * @return 审批人ID列表 + */ + List determineApprovers(String approvalType, Long bizId); + + /** + * 验证审批人权限 + * 业务规则: + * 1. 检查用户是否是当前审批人 + * 2. 检查用户是否有审批权限 + * + * @param approvalId 审批ID + * @param userId 用户ID + * @return 是否有权限 + */ + boolean hasApprovalPermission(Long approvalId, Long userId); + + /** + * 验证申请人权限 + * 业务规则: + * 1. 检查用户是否是申请人 + * + * @param approvalId 审批ID + * @param userId 用户ID + * @return 是否是申请人 + */ + boolean isApplicant(Long approvalId, Long userId); + + /** + * 检查审批是否超时 + * 业务规则: + * 1. 待审批状态超过3天视为超时 + * 2. 发送超时提醒 + * + * @param approvalId 审批ID + * @return 是否超时 + */ + boolean isApprovalTimeout(Long approvalId); + + /** + * 计算审批时长 + * 从提交到审批完成的时长(小时) + * + * @param approvalId 审批ID + * @return 审批时长(小时) + */ + Long calculateApprovalDuration(Long approvalId); + + /** + * 计算审批统计信息 + * 业务规则: + * 1. 统计审批总数 + * 2. 按状态分组统计 + * 3. 按审批类型分组统计 + * 4. 计算平均审批时长 + * 5. 按日期分组统计 + * + * @param approvalType 审批类型(可选) + * @param startDate 开始日期(可选) + * @param endDate 结束日期(可选) + * @return 审批统计信息 + */ + ApprovalStatistics calculateStatistics(String approvalType, String startDate, String endDate); + + /** + * 创建审批历史记录 + * + * @param approvalId 审批ID + * @param action 操作类型(提交、审批、拒绝、撤回、转审) + * @param operatorId 操作人ID + * @param comment 操作说明 + */ + void createApprovalHistory(Long approvalId, String action, Long operatorId, String comment); + + /** + * 检查是否可以催办 + * 业务规则: + * 1. 审批状态为待审批 + * 2. 距离上次催办至少1小时 + * + * @param approvalId 审批ID + * @return 是否可以催办 + */ + boolean canUrge(Long approvalId); + + /** + * 记录催办 + * + * @param approvalId 审批ID + * @param applicantId 申请人ID + */ + void recordUrge(Long approvalId, Long applicantId); + + /** + * 执行业务回调 + * 审批通过或拒绝后,执行相应的业务逻辑 + * 例如:航线审批通过后,更新航线状态为已审核 + * + * @param approvalId 审批ID + * @param approved 是否通过 + */ + void executeBusinessCallback(Long approvalId, boolean approved); + + /** + * 检查审批是否可以删除 + * 业务规则: + * 1. 只能删除草稿状态的审批 + * + * @param approvalId 审批ID + * @return 是否可以删除 + */ + boolean canDelete(Long approvalId); + + /** + * 检查审批是否可以撤回 + * 业务规则: + * 1. 只能撤回待审批状态的审批 + * 2. 只有申请人可以撤回 + * + * @param approvalId 审批ID + * @param userId 用户ID + * @return 是否可以撤回 + */ + boolean canWithdraw(Long approvalId, Long userId); +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/controller/ApprovalController.java b/src/main/java/com/tuoheng/airport/approval/presentation/controller/ApprovalController.java new file mode 100644 index 0000000..97dd326 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/controller/ApprovalController.java @@ -0,0 +1,244 @@ +package com.tuoheng.airport.approval.presentation.controller; + +import com.tuoheng.airport.approval.application.dto.*; +import com.tuoheng.airport.approval.application.service.ApprovalApplicationService; +import com.tuoheng.airport.approval.presentation.converter.ApprovalVoConverter; +import com.tuoheng.airport.approval.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 java.util.List; + +/** + * 审批管理控制器(Presentation层) + * 提供审批流程管理的 REST API 接口 + * 负责审批申请、审核、流程管理 + * + * 职责: + * 1. 接收前端的 VO 对象 + * 2. 将 VO 转换为 DTO 传递给 Application 层 + * 3. 将 Application 层返回的 DTO 转换为 VO 返回给前端 + * 4. 不包含任何业务逻辑 + * + * @author tuoheng + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/approvals") +@RequiredArgsConstructor +@Validated +@Tag(name = "审批管理", description = "审批流程管理相关接口") +public class ApprovalController { + + private final ApprovalApplicationService approvalApplicationService; + + /** + * 创建审批申请 + * POST /api/v1/approvals + */ + @PostMapping + @Operation(summary = "创建审批申请", description = "创建新的审批申请") + public Result createApproval(@Valid @RequestBody ApprovalCreateVO vo) { + log.info("接收到创建审批申请请求: {}", vo); + ApprovalCreateRequest request = ApprovalVoConverter.toCreateRequest(vo); + ApprovalResponse response = approvalApplicationService.createApproval(request); + ApprovalVO result = ApprovalVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 提交审批申请 + * POST /api/v1/approvals/{id}/submit + */ + @PostMapping("/{id}/submit") + @Operation(summary = "提交审批申请", description = "提交审批申请进入审批流程") + public Result submitApproval( + @Parameter(description = "审批ID") @PathVariable Long id) { + log.info("接收到提交审批申请请求,审批ID: {}", id); + ApprovalResponse response = approvalApplicationService.submitApproval(id); + ApprovalVO result = ApprovalVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 审批通过 + * POST /api/v1/approvals/{id}/approve + */ + @PostMapping("/{id}/approve") + @Operation(summary = "审批通过", description = "审批通过指定的申请") + public Result approveApproval( + @Parameter(description = "审批ID") @PathVariable Long id, + @Valid @RequestBody ApprovalDecisionVO vo) { + log.info("接收到审批通过请求,审批ID: {}, 审批意见: {}", id, vo); + ApprovalDecisionRequest request = ApprovalVoConverter.toDecisionRequest(vo); + ApprovalResponse response = approvalApplicationService.approveApproval(id, request); + ApprovalVO result = ApprovalVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 审批拒绝 + * POST /api/v1/approvals/{id}/reject + */ + @PostMapping("/{id}/reject") + @Operation(summary = "审批拒绝", description = "审批拒绝指定的申请") + public Result rejectApproval( + @Parameter(description = "审批ID") @PathVariable Long id, + @Valid @RequestBody ApprovalDecisionVO vo) { + log.info("接收到审批拒绝请求,审批ID: {}, 拒绝原因: {}", id, vo); + ApprovalDecisionRequest request = ApprovalVoConverter.toDecisionRequest(vo); + ApprovalResponse response = approvalApplicationService.rejectApproval(id, request); + ApprovalVO result = ApprovalVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 撤回审批申请 + * POST /api/v1/approvals/{id}/withdraw + */ + @PostMapping("/{id}/withdraw") + @Operation(summary = "撤回审批申请", description = "撤回已提交的审批申请") + public Result withdrawApproval( + @Parameter(description = "审批ID") @PathVariable Long id, + @RequestParam(required = false) String reason) { + log.info("接收到撤回审批申请请求,审批ID: {}, 撤回原因: {}", id, reason); + ApprovalResponse response = approvalApplicationService.withdrawApproval(id, reason); + ApprovalVO result = ApprovalVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 转审 + * POST /api/v1/approvals/{id}/transfer + */ + @PostMapping("/{id}/transfer") + @Operation(summary = "转审", description = "将审批转交给其他审批人") + public Result transferApproval( + @Parameter(description = "审批ID") @PathVariable Long id, + @Parameter(description = "目标审批人ID") @RequestParam Long targetApproverId, + @RequestParam(required = false) String reason) { + log.info("接收到转审请求,审批ID: {}, 目标审批人ID: {}, 转审原因: {}", id, targetApproverId, reason); + ApprovalResponse response = approvalApplicationService.transferApproval(id, targetApproverId, reason); + ApprovalVO result = ApprovalVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询审批详情 + * GET /api/v1/approvals/{id} + */ + @GetMapping("/{id}") + @Operation(summary = "查询审批详情", description = "根据审批ID查询审批详细信息") + public Result getApprovalById( + @Parameter(description = "审批ID") @PathVariable Long id) { + log.info("接收到查询审批详情请求,审批ID: {}", id); + ApprovalResponse response = approvalApplicationService.getApprovalById(id); + ApprovalVO result = ApprovalVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询审批列表 + * GET /api/v1/approvals + */ + @GetMapping + @Operation(summary = "查询审批列表", description = "根据条件查询审批列表") + public Result> queryApprovals(ApprovalQueryVO vo) { + log.info("接收到查询审批列表请求,查询条件: {}", vo); + ApprovalQueryRequest request = ApprovalVoConverter.toQueryRequest(vo); + List responses = approvalApplicationService.queryApprovals(request); + List results = ApprovalVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 查询我的待审批列表 + * GET /api/v1/approvals/pending + */ + @GetMapping("/pending") + @Operation(summary = "查询待审批列表", description = "查询当前用户的待审批列表") + public Result> getMyPendingApprovals() { + log.info("接收到查询待审批列表请求"); + List responses = approvalApplicationService.getMyPendingApprovals(); + List results = ApprovalVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 查询我的申请列表 + * GET /api/v1/approvals/my-applications + */ + @GetMapping("/my-applications") + @Operation(summary = "查询我的申请列表", description = "查询当前用户提交的所有审批申请") + public Result> getMyApplications( + @Parameter(description = "审批状态") @RequestParam(required = false) String status) { + log.info("接收到查询我的申请列表请求,状态: {}", status); + List responses = approvalApplicationService.getMyApplications(status); + List results = ApprovalVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 查询审批历史 + * GET /api/v1/approvals/{id}/history + */ + @GetMapping("/{id}/history") + @Operation(summary = "查询审批历史", description = "查询审批的流转历史记录") + public Result> getApprovalHistory( + @Parameter(description = "审批ID") @PathVariable Long id) { + log.info("接收到查询审批历史请求,审批ID: {}", id); + List responses = approvalApplicationService.getApprovalHistory(id); + List results = ApprovalVoConverter.toHistoryVOList(responses); + return Result.success(results); + } + + /** + * 查询审批统计信息 + * GET /api/v1/approvals/statistics + */ + @GetMapping("/statistics") + @Operation(summary = "查询审批统计", description = "查询审批统计信息") + public Result getApprovalStatistics( + @Parameter(description = "审批类型") @RequestParam(required = false) String approvalType, + @Parameter(description = "开始日期") @RequestParam(required = false) String startDate, + @Parameter(description = "结束日期") @RequestParam(required = false) String endDate) { + log.info("接收到查询审批统计请求,审批类型: {}, 开始日期: {}, 结束日期: {}", approvalType, startDate, endDate); + ApprovalStatisticsResponse response = approvalApplicationService.getApprovalStatistics(approvalType, startDate, endDate); + ApprovalStatisticsVO result = ApprovalVoConverter.toStatisticsVO(response); + return Result.success(result); + } + + /** + * 删除审批申请 + * DELETE /api/v1/approvals/{id} + */ + @DeleteMapping("/{id}") + @Operation(summary = "删除审批申请", description = "删除指定的审批申请(仅草稿状态可删除)") + public Result deleteApproval( + @Parameter(description = "审批ID") @PathVariable Long id) { + log.info("接收到删除审批申请请求,审批ID: {}", id); + approvalApplicationService.deleteApproval(id); + return Result.success(); + } + + /** + * 批量审批 + * POST /api/v1/approvals/batch-approve + */ + @PostMapping("/batch-approve") + @Operation(summary = "批量审批", description = "批量审批通过多个申请") + public Result batchApprove(@Valid @RequestBody BatchApprovalVO vo) { + log.info("接收到批量审批请求,审批数量: {}", vo.getApprovalIds().size()); + BatchApprovalRequest request = ApprovalVoConverter.toBatchRequest(vo); + BatchApprovalResultResponse response = approvalApplicationService.batchApprove(request); + BatchApprovalResultVO result = ApprovalVoConverter.toBatchResultVO(response); + return Result.success(result); + } +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/converter/ApprovalVoConverter.java b/src/main/java/com/tuoheng/airport/approval/presentation/converter/ApprovalVoConverter.java new file mode 100644 index 0000000..0605785 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/converter/ApprovalVoConverter.java @@ -0,0 +1,57 @@ +package com.tuoheng.airport.approval.presentation.converter; + +import com.tuoheng.airport.approval.application.dto.*; +import com.tuoheng.airport.approval.presentation.vo.*; +import java.util.List; +import java.util.stream.Collectors; + +public class ApprovalVoConverter { + + public static ApprovalCreateRequest toCreateRequest(ApprovalCreateVO vo) { + // TODO: 实现转换逻辑 + return new ApprovalCreateRequest(); + } + + public static ApprovalDecisionRequest toDecisionRequest(ApprovalDecisionVO vo) { + // TODO: 实现转换逻辑 + return new ApprovalDecisionRequest(); + } + + public static ApprovalQueryRequest toQueryRequest(ApprovalQueryVO vo) { + // TODO: 实现转换逻辑 + return new ApprovalQueryRequest(); + } + + public static ApprovalVO toVO(ApprovalResponse response) { + // TODO: 实现转换逻辑 + return new ApprovalVO(); + } + + public static List toVOList(List responses) { + return responses.stream().map(ApprovalVoConverter::toVO).collect(Collectors.toList()); + } + + public static ApprovalHistoryVO toHistoryVO(ApprovalHistoryResponse response) { + // TODO: 实现转换逻辑 + return new ApprovalHistoryVO(); + } + + public static List toHistoryVOList(List responses) { + return responses.stream().map(ApprovalVoConverter::toHistoryVO).collect(Collectors.toList()); + } + + public static ApprovalStatisticsVO toStatisticsVO(ApprovalStatisticsResponse response) { + // TODO: 实现转换逻辑 + return new ApprovalStatisticsVO(); + } + + public static BatchApprovalRequest toBatchRequest(BatchApprovalVO vo) { + // TODO: 实现转换逻辑 + return new BatchApprovalRequest(); + } + + public static BatchApprovalResultVO toBatchResultVO(BatchApprovalResultResponse response) { + // TODO: 实现转换逻辑 + return new BatchApprovalResultVO(); + } +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalCreateVO.java b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalCreateVO.java new file mode 100644 index 0000000..f4f9997 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalCreateVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.presentation.vo; + +import lombok.Data; + +@Data +public class ApprovalCreateVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalDecisionVO.java b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalDecisionVO.java new file mode 100644 index 0000000..3878374 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalDecisionVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.presentation.vo; + +import lombok.Data; + +@Data +public class ApprovalDecisionVO { + private String comment; +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalHistoryVO.java b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalHistoryVO.java new file mode 100644 index 0000000..e7d9087 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalHistoryVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.presentation.vo; + +import lombok.Data; + +@Data +public class ApprovalHistoryVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalQueryVO.java b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalQueryVO.java new file mode 100644 index 0000000..c94098e --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalQueryVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.presentation.vo; + +import lombok.Data; + +@Data +public class ApprovalQueryVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalStatisticsVO.java b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalStatisticsVO.java new file mode 100644 index 0000000..78706b6 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalStatisticsVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.presentation.vo; + +import lombok.Data; + +@Data +public class ApprovalStatisticsVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalVO.java b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalVO.java new file mode 100644 index 0000000..6ac483b --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/vo/ApprovalVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.approval.presentation.vo; + +import lombok.Data; + +@Data +public class ApprovalVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/vo/BatchApprovalResultVO.java b/src/main/java/com/tuoheng/airport/approval/presentation/vo/BatchApprovalResultVO.java new file mode 100644 index 0000000..74f3f1b --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/vo/BatchApprovalResultVO.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.approval.presentation.vo; + +import lombok.Data; + +@Data +public class BatchApprovalResultVO { + private Integer successCount; + private Integer failureCount; +} diff --git a/src/main/java/com/tuoheng/airport/approval/presentation/vo/BatchApprovalVO.java b/src/main/java/com/tuoheng/airport/approval/presentation/vo/BatchApprovalVO.java new file mode 100644 index 0000000..f84d92f --- /dev/null +++ b/src/main/java/com/tuoheng/airport/approval/presentation/vo/BatchApprovalVO.java @@ -0,0 +1,10 @@ +package com.tuoheng.airport.approval.presentation.vo; + +import lombok.Data; +import java.util.List; + +@Data +public class BatchApprovalVO { + private List approvalIds; + private String comment; +} diff --git a/src/main/java/com/tuoheng/airport/fms/application/dto/EmergencyFlightRequest.java b/src/main/java/com/tuoheng/airport/fms/application/dto/EmergencyFlightRequest.java new file mode 100644 index 0000000..e08ed1b --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/application/dto/EmergencyFlightRequest.java @@ -0,0 +1,48 @@ +package com.tuoheng.airport.fms.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 紧急飞行请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EmergencyFlightRequest { + + /** + * 起点纬度 + */ + private Double startLatitude; + + /** + * 起点经度 + */ + private Double startLongitude; + + /** + * 终点纬度 + */ + private Double endLatitude; + + /** + * 终点经度 + */ + private Double endLongitude; + + /** + * 飞行高度(米) + */ + private Double altitude; + + /** + * 设备ID(可选,不指定则自动选择最近的可用设备) + */ + private Long deviceId; +} diff --git a/src/main/java/com/tuoheng/airport/fms/application/dto/FlightExecuteRequest.java b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightExecuteRequest.java new file mode 100644 index 0000000..febf5cb --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightExecuteRequest.java @@ -0,0 +1,38 @@ +package com.tuoheng.airport.fms.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 飞行执行请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightExecuteRequest { + + /** + * 任务ID(可选) + */ + private Long taskId; + + /** + * 航线ID + */ + private Long airlineId; + + /** + * 设备ID + */ + private Long deviceId; + + /** + * 计划开始时间 + */ + private String planStartTime; +} diff --git a/src/main/java/com/tuoheng/airport/fms/application/dto/FlightRecordQueryRequest.java b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightRecordQueryRequest.java new file mode 100644 index 0000000..a083f77 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightRecordQueryRequest.java @@ -0,0 +1,48 @@ +package com.tuoheng.airport.fms.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 飞行记录查询请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightRecordQueryRequest { + + /** + * 飞行编号 + */ + private String flightCode; + + /** + * 任务ID + */ + private Long taskId; + + /** + * 设备ID + */ + private Long deviceId; + + /** + * 飞行状态 + */ + private String status; + + /** + * 开始日期 + */ + private String startDate; + + /** + * 结束日期 + */ + private String endDate; +} diff --git a/src/main/java/com/tuoheng/airport/fms/application/dto/FlightRecordResponse.java b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightRecordResponse.java new file mode 100644 index 0000000..1eac5e4 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightRecordResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.fms.application.dto; + +import lombok.Data; + +@Data +public class FlightRecordResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/fms/application/dto/FlightStatisticsResponse.java b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightStatisticsResponse.java new file mode 100644 index 0000000..a654880 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightStatisticsResponse.java @@ -0,0 +1,58 @@ +package com.tuoheng.airport.fms.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 飞行统计响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightStatisticsResponse { + + /** + * 飞行总次数 + */ + private Long totalFlights; + + /** + * 成功次数 + */ + private Long successCount; + + /** + * 失败次数 + */ + private Long failureCount; + + /** + * 取消次数 + */ + private Long cancelledCount; + + /** + * 总飞行时长(秒) + */ + private Long totalDuration; + + /** + * 总飞行距离(米) + */ + private Double totalDistance; + + /** + * 平均飞行时长(秒) + */ + private Long averageDuration; + + /** + * 成功率(%) + */ + private Double successRate; +} diff --git a/src/main/java/com/tuoheng/airport/fms/application/dto/FlightStatusResponse.java b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightStatusResponse.java new file mode 100644 index 0000000..2de3a62 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightStatusResponse.java @@ -0,0 +1,75 @@ +package com.tuoheng.airport.fms.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 飞行状态响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightStatusResponse { + + /** + * 飞行记录ID + */ + private Long flightRecordId; + + /** + * 飞行状态 + */ + private String status; + + /** + * 当前纬度 + */ + private Double latitude; + + /** + * 当前经度 + */ + private Double longitude; + + /** + * 当前高度(米) + */ + private Double altitude; + + /** + * 当前速度(m/s) + */ + private Double speed; + + /** + * 电池电量(%) + */ + private Integer batteryLevel; + + /** + * 当前航点索引 + */ + private Integer currentWaypointIndex; + + /** + * 已飞行距离(米) + */ + private Double flownDistance; + + /** + * 已飞行时长(秒) + */ + private Long flownDuration; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/tuoheng/airport/fms/application/dto/FlightStatusUpdateRequest.java b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightStatusUpdateRequest.java new file mode 100644 index 0000000..8ec81ab --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/application/dto/FlightStatusUpdateRequest.java @@ -0,0 +1,53 @@ +package com.tuoheng.airport.fms.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 飞行状态更新请求 DTO(由设备上报触发) + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightStatusUpdateRequest { + + /** + * 飞行记录ID + */ + private Long flightRecordId; + + /** + * 当前纬度 + */ + private Double latitude; + + /** + * 当前经度 + */ + private Double longitude; + + /** + * 当前高度(米) + */ + private Double altitude; + + /** + * 当前速度(m/s) + */ + private Double speed; + + /** + * 电池电量(%) + */ + private Integer batteryLevel; + + /** + * 当前航点索引 + */ + private Integer currentWaypointIndex; +} diff --git a/src/main/java/com/tuoheng/airport/fms/application/dto/TrajectoryPointResponse.java b/src/main/java/com/tuoheng/airport/fms/application/dto/TrajectoryPointResponse.java new file mode 100644 index 0000000..f801789 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/application/dto/TrajectoryPointResponse.java @@ -0,0 +1,29 @@ +package com.tuoheng.airport.fms.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 飞行轨迹点响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TrajectoryPointResponse { + + private Long id; + private Long flightRecordId; + private Double latitude; + private Double longitude; + private Double altitude; + private Double speed; + private Integer batteryLevel; + private LocalDateTime recordTime; +} diff --git a/src/main/java/com/tuoheng/airport/fms/application/service/FlightApplicationService.java b/src/main/java/com/tuoheng/airport/fms/application/service/FlightApplicationService.java new file mode 100644 index 0000000..de95f97 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/application/service/FlightApplicationService.java @@ -0,0 +1,263 @@ +package com.tuoheng.airport.fms.application.service; + +import com.tuoheng.airport.fms.application.dto.*; + +import java.util.List; + +/** + * 飞行应用服务接口(Application层) + * 定义单次飞行相关的用例(Use Cases) + * 协调领域模型完成飞行执行、控制、监控操作 + * + * @author tuoheng + */ +public interface FlightApplicationService { + + /** + * 执行单次飞行 + * 业务逻辑: + * 1. 验证航线有效性(已审核) + * 2. 检查设备可用性(在线、激活) + * 3. 检查空域冲突 + * 4. 分配设备资源(锁定设备) + * 5. 下发飞行指令到设备 + * 6. 创建飞行记录 + * 7. 启动实时监控 + * + * @param request 飞行执行请求 + * @return 飞行记录响应 + */ + FlightRecordResponse executeFlight(FlightExecuteRequest request); + + /** + * 紧急飞行 + * 业务逻辑: + * 1. 动态生成紧急航线(最短路径) + * 2. 选择最近的可用设备 + * 3. 优先级最高,可中断其他飞行 + * 4. 立即执行飞行 + * + * @param request 紧急飞行请求 + * @return 飞行记录响应 + */ + FlightRecordResponse emergencyFlight(EmergencyFlightRequest request); + + /** + * 取消飞行 + * 业务逻辑: + * 1. 验证飞行状态(准备中或执行中) + * 2. 发送取消指令到设备 + * 3. 释放设备资源 + * 4. 更新飞行记录状态为已取消 + * 5. 记录取消原因 + * + * @param recordId 飞行记录ID + * @param reason 取消原因 + */ + void cancelFlight(Long recordId, String reason); + + /** + * 暂停飞行 + * 业务逻辑: + * 1. 验证飞行状态为执行中 + * 2. 发送暂停指令到设备 + * 3. 更新飞行状态为已暂停 + * + * @param recordId 飞行记录ID + */ + void pauseFlight(Long recordId); + + /** + * 恢复飞行 + * 业务逻辑: + * 1. 验证飞行状态为已暂停 + * 2. 发送恢复指令到设备 + * 3. 更新飞行状态为执行中 + * + * @param recordId 飞行记录ID + */ + void resumeFlight(Long recordId); + + /** + * 返航 + * 业务逻辑: + * 1. 验证飞行状态为执行中 + * 2. 发送返航指令到设备 + * 3. 记录返航时间 + * + * @param recordId 飞行记录ID + */ + void returnHome(Long recordId); + + /** + * 无人机起飞 + * 业务逻辑: + * 1. 验证飞行状态为准备中 + * 2. 发送起飞指令到设备 + * 3. 更新飞行状态为执行中 + * 4. 记录起飞时间 + * + * @param recordId 飞行记录ID + */ + void takeoff(Long recordId); + + /** + * 无人机降落 + * 业务逻辑: + * 1. 验证飞行状态为执行中 + * 2. 发送降落指令到设备 + * 3. 记录降落时间 + * + * @param recordId 飞行记录ID + */ + void land(Long recordId); + + /** + * 调整飞行高度 + * 业务逻辑: + * 1. 验证飞行状态为执行中 + * 2. 验证目标高度合法性(0-500米) + * 3. 发送调整高度指令到设备 + * + * @param recordId 飞行记录ID + * @param altitude 目标高度(米) + */ + void adjustAltitude(Long recordId, Integer altitude); + + /** + * 调整飞行速度 + * 业务逻辑: + * 1. 验证飞行状态为执行中 + * 2. 验证目标速度合法性(1-20m/s) + * 3. 发送调整速度指令到设备 + * + * @param recordId 飞行记录ID + * @param speed 目标速度(m/s) + */ + void adjustSpeed(Long recordId, Double speed); + + /** + * 跳转到指定航点 + * 业务逻辑: + * 1. 验证飞行状态为执行中 + * 2. 验证航点索引合法性 + * 3. 发送跳转航点指令到设备 + * + * @param recordId 飞行记录ID + * @param waypointIndex 航点索引 + */ + void gotoWaypoint(Long recordId, Integer waypointIndex); + + /** + * 查询飞行记录详情 + * + * @param recordId 飞行记录ID + * @return 飞行记录响应 + */ + FlightRecordResponse getFlightRecord(Long recordId); + + /** + * 查询飞行实时状态 + * 业务逻辑: + * 1. 查询飞行记录 + * 2. 从缓存或设备获取实时状态 + * 3. 返回位置、高度、速度、电量等信息 + * + * @param recordId 飞行记录ID + * @return 飞行状态响应 + */ + FlightStatusResponse getFlightStatus(Long recordId); + + /** + * 查询飞行轨迹 + * 业务逻辑: + * 1. 查询飞行记录 + * 2. 查询所有轨迹点 + * 3. 按时间排序返回 + * + * @param recordId 飞行记录ID + * @return 轨迹点列表 + */ + List getFlightTrajectory(Long recordId); + + /** + * 根据条件查询飞行记录列表 + * + * @param request 查询请求 + * @return 飞行记录列表 + */ + List queryFlightRecords(FlightRecordQueryRequest request); + + /** + * 根据设备ID查询飞行记录列表 + * + * @param deviceId 设备ID + * @return 飞行记录列表 + */ + List getFlightRecordsByDevice(Long deviceId); + + /** + * 查询正在执行的飞行列表 + * 业务逻辑: + * 1. 查询状态为执行中的飞行记录 + * 2. 返回实时状态信息 + * + * @return 飞行记录列表 + */ + List getActiveFlights(); + + /** + * 查询飞行统计信息 + * 业务逻辑: + * 1. 统计飞行总次数 + * 2. 统计成功/失败次数 + * 3. 统计总飞行时长 + * 4. 统计总飞行距离 + * 5. 按日期分组统计 + * + * @param deviceId 设备ID(可选) + * @param startDate 开始日期(可选) + * @param endDate 结束日期(可选) + * @return 飞行统计信息 + */ + FlightStatisticsResponse getFlightStatistics(Long deviceId, String startDate, String endDate); + + /** + * 更新飞行实时状态(由设备上报触发) + * 业务逻辑: + * 1. 接收设备上报的实时数据 + * 2. 更新飞行状态 + * 3. 记录轨迹点 + * 4. 检查异常情况(低电量、失联等) + * + * @param request 状态更新请求 + */ + void updateFlightStatus(FlightStatusUpdateRequest request); + + /** + * 完成飞行(由设备上报触发) + * 业务逻辑: + * 1. 验证飞行状态 + * 2. 记录降落时间 + * 3. 计算飞行时长和距离 + * 4. 释放设备资源 + * 5. 更新飞行状态为已完成 + * 6. 触发媒体文件上传 + * + * @param recordId 飞行记录ID + */ + void completeFlight(Long recordId); + + /** + * 飞行失败(由设备上报或系统检测触发) + * 业务逻辑: + * 1. 记录失败原因 + * 2. 释放设备资源 + * 3. 更新飞行状态为失败 + * 4. 发送告警通知 + * + * @param recordId 飞行记录ID + * @param reason 失败原因 + */ + void failFlight(Long recordId, String reason); +} diff --git a/src/main/java/com/tuoheng/airport/fms/domain/model/FlightRecord.java b/src/main/java/com/tuoheng/airport/fms/domain/model/FlightRecord.java new file mode 100644 index 0000000..8aea1e6 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/domain/model/FlightRecord.java @@ -0,0 +1,110 @@ +package com.tuoheng.airport.fms.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 飞行记录领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightRecord { + + private Long id; + private String flightCode; + private Long taskId; + private Long airlineId; + private Long deviceId; + private FlightStatus status; + private LocalDateTime planStartTime; + private LocalDateTime actualStartTime; + private LocalDateTime actualEndTime; + private Long actualDuration; + private Double actualDistance; + private String failureReason; + private Long tenantId; + private LocalDateTime createTime; + private LocalDateTime updateTime; + + // ==================== 业务方法 ==================== + + public void start() { + if (this.status != FlightStatus.PREPARING) { + throw new IllegalStateException("只有准备中的飞行可以开始"); + } + this.status = FlightStatus.EXECUTING; + this.actualStartTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); + } + + public void complete() { + if (this.status != FlightStatus.EXECUTING) { + throw new IllegalStateException("只有执行中的飞行可以完成"); + } + this.status = FlightStatus.COMPLETED; + this.actualEndTime = LocalDateTime.now(); + if (this.actualStartTime != null) { + this.actualDuration = java.time.Duration.between(this.actualStartTime, this.actualEndTime).getSeconds(); + } + this.updateTime = LocalDateTime.now(); + } + + public void cancel(String reason) { + if (this.status == FlightStatus.COMPLETED || this.status == FlightStatus.FAILED) { + throw new IllegalStateException("已完成或已失败的飞行不能取消"); + } + this.status = FlightStatus.CANCELLED; + this.failureReason = reason; + this.updateTime = LocalDateTime.now(); + } + + public void fail(String reason) { + this.status = FlightStatus.FAILED; + this.failureReason = reason; + this.actualEndTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); + } + + public void pause() { + if (this.status != FlightStatus.EXECUTING) { + throw new IllegalStateException("只有执行中的飞行可以暂停"); + } + this.status = FlightStatus.PAUSED; + this.updateTime = LocalDateTime.now(); + } + + public void resume() { + if (this.status != FlightStatus.PAUSED) { + throw new IllegalStateException("只有已暂停的飞行可以恢复"); + } + this.status = FlightStatus.EXECUTING; + this.updateTime = LocalDateTime.now(); + } + + public boolean isActive() { + return this.status == FlightStatus.EXECUTING || this.status == FlightStatus.PAUSED; + } + + public static FlightRecord create(String flightCode, Long taskId, Long airlineId, Long deviceId, Long tenantId) { + LocalDateTime now = LocalDateTime.now(); + return FlightRecord.builder() + .flightCode(flightCode) + .taskId(taskId) + .airlineId(airlineId) + .deviceId(deviceId) + .status(FlightStatus.PREPARING) + .planStartTime(now) + .tenantId(tenantId) + .createTime(now) + .updateTime(now) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/fms/domain/model/FlightStatistics.java b/src/main/java/com/tuoheng/airport/fms/domain/model/FlightStatistics.java new file mode 100644 index 0000000..fb65178 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/domain/model/FlightStatistics.java @@ -0,0 +1,68 @@ +package com.tuoheng.airport.fms.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 飞行统计领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightStatistics { + + /** + * 飞行总次数 + */ + private Long totalFlights; + + /** + * 成功次数 + */ + private Long successCount; + + /** + * 失败次数 + */ + private Long failureCount; + + /** + * 取消次数 + */ + private Long cancelledCount; + + /** + * 总飞行时长(秒) + */ + private Long totalDuration; + + /** + * 总飞行距离(米) + */ + private Double totalDistance; + + /** + * 平均飞行时长(秒) + */ + public Long getAverageDuration() { + if (successCount == null || successCount == 0) { + return 0L; + } + return totalDuration / successCount; + } + + /** + * 成功率 + */ + public double getSuccessRate() { + if (totalFlights == null || totalFlights == 0) { + return 0.0; + } + return (double) successCount / totalFlights * 100; + } +} diff --git a/src/main/java/com/tuoheng/airport/fms/domain/model/FlightStatus.java b/src/main/java/com/tuoheng/airport/fms/domain/model/FlightStatus.java new file mode 100644 index 0000000..3fce932 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/domain/model/FlightStatus.java @@ -0,0 +1,26 @@ +package com.tuoheng.airport.fms.domain.model; + +/** + * 飞行状态枚举 + * + * @author tuoheng + */ +public enum FlightStatus { + + PREPARING("准备中"), + EXECUTING("执行中"), + PAUSED("已暂停"), + COMPLETED("已完成"), + CANCELLED("已取消"), + FAILED("失败"); + + private final String description; + + FlightStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/tuoheng/airport/fms/domain/model/TrajectoryPoint.java b/src/main/java/com/tuoheng/airport/fms/domain/model/TrajectoryPoint.java new file mode 100644 index 0000000..18fbbb1 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/domain/model/TrajectoryPoint.java @@ -0,0 +1,42 @@ +package com.tuoheng.airport.fms.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 飞行轨迹点领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TrajectoryPoint { + + private Long id; + private Long flightRecordId; + private Double latitude; + private Double longitude; + private Double altitude; + private Double speed; + private Integer batteryLevel; + private LocalDateTime recordTime; + + public static TrajectoryPoint create(Long flightRecordId, Double latitude, Double longitude, + Double altitude, Double speed, Integer batteryLevel) { + return TrajectoryPoint.builder() + .flightRecordId(flightRecordId) + .latitude(latitude) + .longitude(longitude) + .altitude(altitude) + .speed(speed) + .batteryLevel(batteryLevel) + .recordTime(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/fms/domain/repository/FlightRecordRepository.java b/src/main/java/com/tuoheng/airport/fms/domain/repository/FlightRecordRepository.java new file mode 100644 index 0000000..b1fb5c7 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/domain/repository/FlightRecordRepository.java @@ -0,0 +1,36 @@ +package com.tuoheng.airport.fms.domain.repository; + +import com.tuoheng.airport.fms.domain.model.FlightRecord; + +import java.util.List; +import java.util.Optional; + +/** + * 飞行记录仓储接口 + * + * @author tuoheng + */ +public interface FlightRecordRepository { + + FlightRecord save(FlightRecord flightRecord); + + Optional findById(Long id); + + Optional findByFlightCode(String flightCode); + + List findByTaskId(Long taskId); + + List findByDeviceId(Long deviceId); + + List findByStatus(String status); + + List findActiveFlights(); + + void delete(Long id); + + long countByTaskId(Long taskId); + + long countByDeviceId(Long deviceId); + + long countByStatus(String status); +} diff --git a/src/main/java/com/tuoheng/airport/fms/domain/repository/TrajectoryPointRepository.java b/src/main/java/com/tuoheng/airport/fms/domain/repository/TrajectoryPointRepository.java new file mode 100644 index 0000000..e6ac2db --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/domain/repository/TrajectoryPointRepository.java @@ -0,0 +1,21 @@ +package com.tuoheng.airport.fms.domain.repository; + +import com.tuoheng.airport.fms.domain.model.TrajectoryPoint; + +import java.util.List; + +/** + * 飞行轨迹点仓储接口 + * + * @author tuoheng + */ +public interface TrajectoryPointRepository { + + TrajectoryPoint save(TrajectoryPoint trajectoryPoint); + + List findByFlightRecordId(Long flightRecordId); + + void deleteByFlightRecordId(Long flightRecordId); + + long countByFlightRecordId(Long flightRecordId); +} diff --git a/src/main/java/com/tuoheng/airport/fms/domain/service/FlightDomainService.java b/src/main/java/com/tuoheng/airport/fms/domain/service/FlightDomainService.java new file mode 100644 index 0000000..5203c2d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/domain/service/FlightDomainService.java @@ -0,0 +1,359 @@ +package com.tuoheng.airport.fms.domain.service; + +import com.tuoheng.airport.fms.domain.model.FlightRecord; +import com.tuoheng.airport.fms.domain.model.FlightStatus; +import com.tuoheng.airport.fms.domain.model.TrajectoryPoint; +import com.tuoheng.airport.fms.domain.model.FlightStatistics; + +import java.util.List; + +/** + * 飞行领域服务接口(Domain层) + * 封装飞行执行相关的业务逻辑和业务规则 + * + * Domain Service 的职责: + * 1. 封装飞行执行的核心业务规则 + * 2. 协调飞行记录、设备、航线的关系 + * 3. 保证飞行数据的一致性 + * 4. 不包含事务管理(事务由 Application 层管理) + * + * @author tuoheng + */ +public interface FlightDomainService { + + /** + * 验证飞行可以执行 + * 业务规则: + * 1. 航线必须已审核通过 + * 2. 设备必须在线且可用(激活状态) + * 3. 设备必须已分配到机场 + * 4. 天气条件满足飞行要求(风速、降雨等) + * 5. 空域没有冲突 + * 6. 设备电量充足(>20%) + * + * @param airlineId 航线ID + * @param deviceId 设备ID + * @return 是否可以执行 + */ + boolean canExecuteFlight(Long airlineId, Long deviceId); + + /** + * 创建飞行记录 + * 业务规则: + * 1. 生成唯一的飞行编号 + * 2. 记录飞行计划信息(航线、设备、预计时长) + * 3. 初始化飞行状态为准备中 + * 4. 锁定设备资源 + * + * @param flightRecord 飞行记录领域模型 + * @return 创建后的飞行记录 + */ + FlightRecord createFlightRecord(FlightRecord flightRecord); + + /** + * 开始飞行 + * 业务规则: + * 1. 验证飞行状态为准备中 + * 2. 下发飞行指令到设备 + * 3. 更新飞行状态为执行中 + * 4. 记录起飞时间 + * 5. 启动实时监控 + * + * @param recordId 飞行记录ID + * @return 更新后的飞行记录 + */ + FlightRecord startFlight(Long recordId); + + /** + * 取消飞行 + * 业务规则: + * 1. 只有准备中或执行中的飞行可以取消 + * 2. 发送取消指令到设备 + * 3. 释放设备资源 + * 4. 更新飞行状态为已取消 + * 5. 记录取消原因和时间 + * + * @param recordId 飞行记录ID + * @param reason 取消原因 + * @return 更新后的飞行记录 + */ + FlightRecord cancelFlight(Long recordId, String reason); + + /** + * 暂停飞行 + * 业务规则: + * 1. 只有执行中的飞行可以暂停 + * 2. 发送暂停指令到设备(悬停) + * 3. 更新飞行状态为已暂停 + * 4. 记录暂停时间 + * + * @param recordId 飞行记录ID + * @return 更新后的飞行记录 + */ + FlightRecord pauseFlight(Long recordId); + + /** + * 恢复飞行 + * 业务规则: + * 1. 只有已暂停的飞行可以恢复 + * 2. 发送恢复指令到设备 + * 3. 更新飞行状态为执行中 + * 4. 记录恢复时间 + * + * @param recordId 飞行记录ID + * @return 更新后的飞行记录 + */ + FlightRecord resumeFlight(Long recordId); + + /** + * 完成飞行 + * 业务规则: + * 1. 记录降落时间 + * 2. 计算实际飞行时长 + * 3. 计算实际飞行距离 + * 4. 释放设备资源 + * 5. 更新飞行状态为已完成 + * 6. 触发媒体文件上传任务 + * + * @param recordId 飞行记录ID + * @return 更新后的飞行记录 + */ + FlightRecord completeFlight(Long recordId); + + /** + * 飞行失败 + * 业务规则: + * 1. 记录失败原因和时间 + * 2. 释放设备资源 + * 3. 更新飞行状态为失败 + * 4. 发送告警通知 + * 5. 记录异常日志 + * + * @param recordId 飞行记录ID + * @param reason 失败原因 + * @return 更新后的飞行记录 + */ + FlightRecord failFlight(Long recordId, String reason); + + /** + * 更新飞行实时状态 + * 接收设备上报的实时数据(位置、高度、速度、电量等) + * 业务规则: + * 1. 验证飞行状态为执行中 + * 2. 更新实时状态信息 + * 3. 记录轨迹点 + * 4. 检查异常情况(低电量、失联、偏航等) + * + * @param recordId 飞行记录ID + * @param status 飞行状态 + * @return 更新后的飞行记录 + */ + FlightRecord updateFlightStatus(Long recordId, FlightStatus status); + + /** + * 记录飞行轨迹点 + * 业务规则: + * 1. 验证飞行状态为执行中 + * 2. 记录轨迹点(位置、高度、速度、时间) + * 3. 轨迹点按时间排序 + * + * @param recordId 飞行记录ID + * @param point 轨迹点 + */ + void recordTrajectoryPoint(Long recordId, TrajectoryPoint point); + + /** + * 查询飞行记录详情 + * + * @param recordId 飞行记录ID + * @return 飞行记录领域模型 + */ + FlightRecord getFlightRecordById(Long recordId); + + /** + * 查询飞行实时状态 + * + * @param recordId 飞行记录ID + * @return 飞行状态 + */ + FlightStatus getFlightStatus(Long recordId); + + /** + * 查询飞行轨迹 + * + * @param recordId 飞行记录ID + * @return 轨迹点列表 + */ + List getFlightTrajectory(Long recordId); + + /** + * 查询正在执行的飞行列表 + * + * @return 飞行记录列表 + */ + List getActiveFlights(); + + /** + * 检查飞行是否超时 + * 业务规则: + * 1. 飞行时长超过预计时长的 150% + * 2. 自动标记为异常 + * 3. 发送告警通知 + * + * @param recordId 飞行记录ID + * @return 是否超时 + */ + boolean isFlightTimeout(Long recordId); + + /** + * 检查设备是否失联 + * 业务规则: + * 1. 超过30秒未收到设备心跳 + * 2. 标记为失联状态 + * 3. 发送告警通知 + * + * @param recordId 飞行记录ID + * @return 是否失联 + */ + boolean isDeviceDisconnected(Long recordId); + + /** + * 检查设备电量是否充足 + * 业务规则: + * 1. 电量低于20%时发送低电量告警 + * 2. 电量低于10%时强制返航 + * + * @param recordId 飞行记录ID + * @return 电量是否充足 + */ + boolean isBatteryLevelSufficient(Long recordId); + + /** + * 检查飞行是否偏航 + * 业务规则: + * 1. 实际位置与计划航线偏离超过50米 + * 2. 标记为偏航状态 + * 3. 发送告警通知 + * + * @param recordId 飞行记录ID + * @return 是否偏航 + */ + boolean isFlightOffCourse(Long recordId); + + /** + * 检查空域冲突 + * 业务规则: + * 1. 检查同一时间段内是否有其他飞行 + * 2. 检查航线是否有重叠 + * 3. 检查飞行高度是否有冲突 + * + * @param airlineId 航线ID + * @param startTime 开始时间 + * @return 是否有冲突 + */ + boolean hasAirspaceConflict(Long airlineId, String startTime); + + /** + * 计算飞行统计信息 + * 业务规则: + * 1. 统计飞行总次数 + * 2. 统计成功/失败次数 + * 3. 统计总飞行时长 + * 4. 统计总飞行距离 + * 5. 计算平均飞行时长 + * 6. 按日期分组统计 + * + * @param deviceId 设备ID(可选) + * @param startDate 开始日期(可选) + * @param endDate 结束日期(可选) + * @return 飞行统计信息 + */ + FlightStatistics calculateStatistics(Long deviceId, String startDate, String endDate); + + /** + * 发送飞行控制指令 + * 业务规则: + * 1. 验证飞行状态 + * 2. 构建控制指令 + * 3. 通过MQTT发送到设备 + * 4. 记录指令日志 + * + * @param recordId 飞行记录ID + * @param command 控制指令 + * @param params 指令参数 + */ + void sendFlightCommand(Long recordId, String command, Object params); + + /** + * 返航 + * 业务规则: + * 1. 验证飞行状态为执行中 + * 2. 发送返航指令到设备 + * 3. 记录返航时间 + * + * @param recordId 飞行记录ID + * @return 更新后的飞行记录 + */ + FlightRecord returnHome(Long recordId); + + /** + * 起飞 + * 业务规则: + * 1. 验证飞行状态为准备中 + * 2. 发送起飞指令到设备 + * 3. 更新飞行状态为执行中 + * 4. 记录起飞时间 + * + * @param recordId 飞行记录ID + * @return 更新后的飞行记录 + */ + FlightRecord takeoff(Long recordId); + + /** + * 降落 + * 业务规则: + * 1. 验证飞行状态为执行中 + * 2. 发送降落指令到设备 + * 3. 记录降落时间 + * + * @param recordId 飞行记录ID + * @return 更新后的飞行记录 + */ + FlightRecord land(Long recordId); + + /** + * 调整飞行高度 + * 业务规则: + * 1. 验证飞行状态为执行中 + * 2. 验证目标高度合法性(0-500米) + * 3. 发送调整高度指令到设备 + * + * @param recordId 飞行记录ID + * @param altitude 目标高度(米) + */ + void adjustAltitude(Long recordId, Integer altitude); + + /** + * 调整飞行速度 + * 业务规则: + * 1. 验证飞行状态为执行中 + * 2. 验证目标速度合法性(1-20m/s) + * 3. 发送调整速度指令到设备 + * + * @param recordId 飞行记录ID + * @param speed 目标速度(m/s) + */ + void adjustSpeed(Long recordId, Double speed); + + /** + * 跳转到指定航点 + * 业务规则: + * 1. 验证飞行状态为执行中 + * 2. 验证航点索引合法性 + * 3. 发送跳转航点指令到设备 + * + * @param recordId 飞行记录ID + * @param waypointIndex 航点索引 + */ + void gotoWaypoint(Long recordId, Integer waypointIndex); +} diff --git a/src/main/java/com/tuoheng/airport/fms/presentation/controller/FlightController.java b/src/main/java/com/tuoheng/airport/fms/presentation/controller/FlightController.java new file mode 100644 index 0000000..dd91f33 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/presentation/controller/FlightController.java @@ -0,0 +1,289 @@ +package com.tuoheng.airport.fms.presentation.controller; + +import com.tuoheng.airport.fms.application.dto.*; +import com.tuoheng.airport.fms.application.service.FlightApplicationService; +import com.tuoheng.airport.fms.presentation.converter.FlightVoConverter; +import com.tuoheng.airport.fms.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 java.util.List; + +/** + * 飞行管理控制器(Presentation层) + * 提供单次飞行管理的 REST API 接口 + * 负责飞行执行、控制、监控 + * + * 职责: + * 1. 接收前端的 VO 对象 + * 2. 将 VO 转换为 DTO 传递给 Application 层 + * 3. 将 Application 层返回的 DTO 转换为 VO 返回给前端 + * 4. 不包含任何业务逻辑 + * + * @author tuoheng + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/flights") +@RequiredArgsConstructor +@Validated +@Tag(name = "飞行管理", description = "单次飞行管理相关接口") +public class FlightController { + + private final FlightApplicationService flightApplicationService; + + /** + * 执行单次飞行 + * POST /api/v1/flights/execute + */ + @PostMapping("/execute") + @Operation(summary = "执行单次飞行", description = "执行一次飞行任务") + public Result executeFlight(@Valid @RequestBody FlightExecuteVO vo) { + log.info("接收到执行飞行请求: {}", vo); + FlightExecuteRequest request = FlightVoConverter.toExecuteRequest(vo); + FlightRecordResponse response = flightApplicationService.executeFlight(request); + FlightRecordVO result = FlightVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 紧急飞行(一键起飞) + * POST /api/v1/flights/emergency + */ + @PostMapping("/emergency") + @Operation(summary = "紧急飞行", description = "执行紧急飞行任务,动态生成航线") + public Result emergencyFlight(@Valid @RequestBody EmergencyFlightVO vo) { + log.info("接收到紧急飞行请求: {}", vo); + EmergencyFlightRequest request = FlightVoConverter.toEmergencyRequest(vo); + FlightRecordResponse response = flightApplicationService.emergencyFlight(request); + FlightRecordVO result = FlightVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 取消飞行 + * POST /api/v1/flights/{recordId}/cancel + */ + @PostMapping("/{recordId}/cancel") + @Operation(summary = "取消飞行", description = "取消正在执行或准备中的飞行") + public Result cancelFlight( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId, + @RequestParam(required = false) String reason) { + log.info("接收到取消飞行请求,飞行记录ID: {}, 取消原因: {}", recordId, reason); + flightApplicationService.cancelFlight(recordId, reason); + return Result.success(); + } + + /** + * 暂停飞行 + * POST /api/v1/flights/{recordId}/pause + */ + @PostMapping("/{recordId}/pause") + @Operation(summary = "暂停飞行", description = "暂停正在执行的飞行") + public Result pauseFlight( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId) { + log.info("接收到暂停飞行请求,飞行记录ID: {}", recordId); + flightApplicationService.pauseFlight(recordId); + return Result.success(); + } + + /** + * 恢复飞行 + * POST /api/v1/flights/{recordId}/resume + */ + @PostMapping("/{recordId}/resume") + @Operation(summary = "恢复飞行", description = "恢复已暂停的飞行") + public Result resumeFlight( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId) { + log.info("接收到恢复飞行请求,飞行记录ID: {}", recordId); + flightApplicationService.resumeFlight(recordId); + return Result.success(); + } + + /** + * 返航 + * POST /api/v1/flights/{recordId}/return-home + */ + @PostMapping("/{recordId}/return-home") + @Operation(summary = "返航", description = "命令无人机返回起飞点") + public Result returnHome( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId) { + log.info("接收到返航请求,飞行记录ID: {}", recordId); + flightApplicationService.returnHome(recordId); + return Result.success(); + } + + /** + * 无人机起飞 + * POST /api/v1/flights/{recordId}/takeoff + */ + @PostMapping("/{recordId}/takeoff") + @Operation(summary = "无人机起飞", description = "命令无人机起飞") + public Result takeoff( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId) { + log.info("接收到起飞请求,飞行记录ID: {}", recordId); + flightApplicationService.takeoff(recordId); + return Result.success(); + } + + /** + * 无人机降落 + * POST /api/v1/flights/{recordId}/land + */ + @PostMapping("/{recordId}/land") + @Operation(summary = "无人机降落", description = "命令无人机降落") + public Result land( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId) { + log.info("接收到降落请求,飞行记录ID: {}", recordId); + flightApplicationService.land(recordId); + return Result.success(); + } + + /** + * 调整飞行高度 + * POST /api/v1/flights/{recordId}/altitude + */ + @PostMapping("/{recordId}/altitude") + @Operation(summary = "调整飞行高度", description = "调整无人机的飞行高度") + public Result adjustAltitude( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId, + @Parameter(description = "目标高度(米)") @RequestParam Integer altitude) { + log.info("接收到调整飞行高度请求,飞行记录ID: {}, 目标高度: {}米", recordId, altitude); + flightApplicationService.adjustAltitude(recordId, altitude); + return Result.success(); + } + + /** + * 调整飞行速度 + * POST /api/v1/flights/{recordId}/speed + */ + @PostMapping("/{recordId}/speed") + @Operation(summary = "调整飞行速度", description = "调整无人机的飞行速度") + public Result adjustSpeed( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId, + @Parameter(description = "目标速度(m/s)") @RequestParam Double speed) { + log.info("接收到调整飞行速度请求,飞行记录ID: {}, 目标速度: {}m/s", recordId, speed); + flightApplicationService.adjustSpeed(recordId, speed); + return Result.success(); + } + + /** + * 跳转到指定航点 + * POST /api/v1/flights/{recordId}/goto-waypoint + */ + @PostMapping("/{recordId}/goto-waypoint") + @Operation(summary = "跳转到指定航点", description = "命令无人机跳转到指定航点") + public Result gotoWaypoint( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId, + @Parameter(description = "航点索引") @RequestParam Integer waypointIndex) { + log.info("接收到跳转航点请求,飞行记录ID: {}, 航点索引: {}", recordId, waypointIndex); + flightApplicationService.gotoWaypoint(recordId, waypointIndex); + return Result.success(); + } + + /** + * 查询飞行记录详情 + * GET /api/v1/flights/{recordId} + */ + @GetMapping("/{recordId}") + @Operation(summary = "查询飞行记录详情", description = "根据飞行记录ID查询详细信息") + public Result getFlightRecord( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId) { + log.info("接收到查询飞行记录请求,飞行记录ID: {}", recordId); + FlightRecordResponse response = flightApplicationService.getFlightRecord(recordId); + FlightRecordVO result = FlightVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询飞行实时状态 + * GET /api/v1/flights/{recordId}/status + */ + @GetMapping("/{recordId}/status") + @Operation(summary = "查询飞行实时状态", description = "查询飞行的实时状态信息") + public Result getFlightStatus( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId) { + log.info("接收到查询飞行状态请求,飞行记录ID: {}", recordId); + FlightStatusResponse response = flightApplicationService.getFlightStatus(recordId); + FlightStatusVO result = FlightVoConverter.toStatusVO(response); + return Result.success(result); + } + + /** + * 查询飞行轨迹 + * GET /api/v1/flights/{recordId}/trajectory + */ + @GetMapping("/{recordId}/trajectory") + @Operation(summary = "查询飞行轨迹", description = "查询飞行的历史轨迹点") + public Result> getFlightTrajectory( + @Parameter(description = "飞行记录ID") @PathVariable Long recordId) { + log.info("接收到查询飞行轨迹请求,飞行记录ID: {}", recordId); + List responses = flightApplicationService.getFlightTrajectory(recordId); + List results = FlightVoConverter.toTrajectoryVOList(responses); + return Result.success(results); + } + + /** + * 查询飞行记录列表 + * GET /api/v1/flights + */ + @GetMapping + @Operation(summary = "查询飞行记录列表", description = "根据条件查询飞行记录列表") + public Result> queryFlightRecords(FlightRecordQueryVO vo) { + log.info("接收到查询飞行记录列表请求,查询条件: {}", vo); + FlightRecordQueryRequest request = FlightVoConverter.toQueryRequest(vo); + List responses = flightApplicationService.queryFlightRecords(request); + List results = FlightVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 查询设备的飞行记录 + * GET /api/v1/flights/device/{deviceId} + */ + @GetMapping("/device/{deviceId}") + @Operation(summary = "查询设备飞行记录", description = "查询指定设备的所有飞行记录") + public Result> getFlightRecordsByDevice( + @Parameter(description = "设备ID") @PathVariable Long deviceId) { + log.info("接收到查询设备飞行记录请求,设备ID: {}", deviceId); + List responses = flightApplicationService.getFlightRecordsByDevice(deviceId); + List results = FlightVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 查询正在执行的飞行列表 + * GET /api/v1/flights/active + */ + @GetMapping("/active") + @Operation(summary = "查询正在执行的飞行", description = "查询所有正在执行的飞行任务") + public Result> getActiveFlights() { + log.info("接收到查询正在执行的飞行请求"); + List responses = flightApplicationService.getActiveFlights(); + List results = FlightVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 查询飞行统计信息 + * GET /api/v1/flights/statistics + */ + @GetMapping("/statistics") + @Operation(summary = "查询飞行统计", description = "查询飞行统计信息") + public Result getFlightStatistics( + @Parameter(description = "设备ID") @RequestParam(required = false) Long deviceId, + @Parameter(description = "开始日期") @RequestParam(required = false) String startDate, + @Parameter(description = "结束日期") @RequestParam(required = false) String endDate) { + log.info("接收到查询飞行统计请求,设备ID: {}, 开始日期: {}, 结束日期: {}", deviceId, startDate, endDate); + FlightStatisticsResponse response = flightApplicationService.getFlightStatistics(deviceId, startDate, endDate); + FlightStatisticsVO result = FlightVoConverter.toStatisticsVO(response); + return Result.success(result); + } +} diff --git a/src/main/java/com/tuoheng/airport/fms/presentation/converter/FlightVoConverter.java b/src/main/java/com/tuoheng/airport/fms/presentation/converter/FlightVoConverter.java new file mode 100644 index 0000000..85f6ef0 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/presentation/converter/FlightVoConverter.java @@ -0,0 +1,52 @@ +package com.tuoheng.airport.fms.presentation.converter; + +import com.tuoheng.airport.fms.application.dto.*; +import com.tuoheng.airport.fms.presentation.vo.*; +import java.util.List; +import java.util.stream.Collectors; + +public class FlightVoConverter { + + public static FlightExecuteRequest toExecuteRequest(FlightExecuteVO vo) { + // TODO: 实现转换逻辑 + return new FlightExecuteRequest(); + } + + public static EmergencyFlightRequest toEmergencyRequest(EmergencyFlightVO vo) { + // TODO: 实现转换逻辑 + return new EmergencyFlightRequest(); + } + + public static FlightRecordQueryRequest toQueryRequest(FlightRecordQueryVO vo) { + // TODO: 实现转换逻辑 + return new FlightRecordQueryRequest(); + } + + public static FlightRecordVO toVO(FlightRecordResponse response) { + // TODO: 实现转换逻辑 + return new FlightRecordVO(); + } + + public static List toVOList(List responses) { + return responses.stream().map(FlightVoConverter::toVO).collect(Collectors.toList()); + } + + public static FlightStatusVO toStatusVO(FlightStatusResponse response) { + // TODO: 实现转换逻辑 + return new FlightStatusVO(); + } + + public static TrajectoryPointVO toTrajectoryVO(TrajectoryPointResponse response) { + // TODO: 实现转换逻辑 + return new TrajectoryPointVO(); + } + + public static List toTrajectoryVOList(List responses) { + return responses.stream().map(FlightVoConverter::toTrajectoryVO).collect(Collectors.toList()); + } + + public static FlightStatisticsVO toStatisticsVO(FlightStatisticsResponse response) { + // TODO: 实现转换逻辑 + return new FlightStatisticsVO(); + } +} diff --git a/src/main/java/com/tuoheng/airport/fms/presentation/vo/EmergencyFlightVO.java b/src/main/java/com/tuoheng/airport/fms/presentation/vo/EmergencyFlightVO.java new file mode 100644 index 0000000..94e3aca --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/presentation/vo/EmergencyFlightVO.java @@ -0,0 +1,55 @@ +package com.tuoheng.airport.fms.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * 紧急飞行 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EmergencyFlightVO { + + /** + * 起点纬度 + */ + @NotNull(message = "起点纬度不能为空") + private Double startLatitude; + + /** + * 起点经度 + */ + @NotNull(message = "起点经度不能为空") + private Double startLongitude; + + /** + * 终点纬度 + */ + @NotNull(message = "终点纬度不能为空") + private Double endLatitude; + + /** + * 终点经度 + */ + @NotNull(message = "终点经度不能为空") + private Double endLongitude; + + /** + * 飞行高度(米) + */ + @NotNull(message = "飞行高度不能为空") + private Double altitude; + + /** + * 设备ID(可选) + */ + private Long deviceId; +} diff --git a/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightExecuteVO.java b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightExecuteVO.java new file mode 100644 index 0000000..439ef46 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightExecuteVO.java @@ -0,0 +1,42 @@ +package com.tuoheng.airport.fms.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +/** + * 飞行执行 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightExecuteVO { + + /** + * 任务ID(可选) + */ + private Long taskId; + + /** + * 航线ID + */ + @NotNull(message = "航线ID不能为空") + private Long airlineId; + + /** + * 设备ID + */ + @NotNull(message = "设备ID不能为空") + private Long deviceId; + + /** + * 计划开始时间 + */ + private String planStartTime; +} diff --git a/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightRecordQueryVO.java b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightRecordQueryVO.java new file mode 100644 index 0000000..3d201ab --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightRecordQueryVO.java @@ -0,0 +1,48 @@ +package com.tuoheng.airport.fms.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 飞行记录查询 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightRecordQueryVO { + + /** + * 飞行编号 + */ + private String flightCode; + + /** + * 任务ID + */ + private Long taskId; + + /** + * 设备ID + */ + private Long deviceId; + + /** + * 飞行状态 + */ + private String status; + + /** + * 开始日期 + */ + private String startDate; + + /** + * 结束日期 + */ + private String endDate; +} diff --git a/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightRecordVO.java b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightRecordVO.java new file mode 100644 index 0000000..7d79837 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightRecordVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.fms.presentation.vo; + +import lombok.Data; + +@Data +public class FlightRecordVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightStatisticsVO.java b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightStatisticsVO.java new file mode 100644 index 0000000..8654ca7 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightStatisticsVO.java @@ -0,0 +1,27 @@ +package com.tuoheng.airport.fms.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 飞行统计 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightStatisticsVO { + + private Long totalFlights; + private Long successCount; + private Long failureCount; + private Long cancelledCount; + private Long totalDuration; + private Double totalDistance; + private Long averageDuration; + private Double successRate; +} diff --git a/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightStatusVO.java b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightStatusVO.java new file mode 100644 index 0000000..3a617c6 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/presentation/vo/FlightStatusVO.java @@ -0,0 +1,32 @@ +package com.tuoheng.airport.fms.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 飞行状态 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FlightStatusVO { + + private Long flightRecordId; + private String status; + private Double latitude; + private Double longitude; + private Double altitude; + private Double speed; + private Integer batteryLevel; + private Integer currentWaypointIndex; + private Double flownDistance; + private Long flownDuration; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/tuoheng/airport/fms/presentation/vo/TrajectoryPointVO.java b/src/main/java/com/tuoheng/airport/fms/presentation/vo/TrajectoryPointVO.java new file mode 100644 index 0000000..6f06fa7 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/fms/presentation/vo/TrajectoryPointVO.java @@ -0,0 +1,29 @@ +package com.tuoheng.airport.fms.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 飞行轨迹点 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TrajectoryPointVO { + + private Long id; + private Long flightRecordId; + private Double latitude; + private Double longitude; + private Double altitude; + private Double speed; + private Integer batteryLevel; + private LocalDateTime recordTime; +} diff --git a/src/main/java/com/tuoheng/airport/media/application/dto/MediaAnalysisResultRequest.java b/src/main/java/com/tuoheng/airport/media/application/dto/MediaAnalysisResultRequest.java new file mode 100644 index 0000000..e800520 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/application/dto/MediaAnalysisResultRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.media.application.dto; + +import lombok.Data; + +@Data +public class MediaAnalysisResultRequest { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/application/dto/MediaCreateFromDeviceRequest.java b/src/main/java/com/tuoheng/airport/media/application/dto/MediaCreateFromDeviceRequest.java new file mode 100644 index 0000000..54abef0 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/application/dto/MediaCreateFromDeviceRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.media.application.dto; + +import lombok.Data; + +@Data +public class MediaCreateFromDeviceRequest { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/application/dto/MediaInfoResponse.java b/src/main/java/com/tuoheng/airport/media/application/dto/MediaInfoResponse.java new file mode 100644 index 0000000..2d54f70 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/application/dto/MediaInfoResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.media.application.dto; + +import lombok.Data; + +@Data +public class MediaInfoResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/application/dto/MediaQueryRequest.java b/src/main/java/com/tuoheng/airport/media/application/dto/MediaQueryRequest.java new file mode 100644 index 0000000..5f294a0 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/application/dto/MediaQueryRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.media.application.dto; + +import lombok.Data; + +@Data +public class MediaQueryRequest { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/application/dto/MediaStatisticsResponse.java b/src/main/java/com/tuoheng/airport/media/application/dto/MediaStatisticsResponse.java new file mode 100644 index 0000000..264735c --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/application/dto/MediaStatisticsResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.media.application.dto; + +import lombok.Data; + +@Data +public class MediaStatisticsResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/application/dto/MediaUpdateRequest.java b/src/main/java/com/tuoheng/airport/media/application/dto/MediaUpdateRequest.java new file mode 100644 index 0000000..ab7ca48 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/application/dto/MediaUpdateRequest.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.media.application.dto; + +import lombok.Data; + +@Data +public class MediaUpdateRequest { + private Long id; + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/application/service/MediaApplicationService.java b/src/main/java/com/tuoheng/airport/media/application/service/MediaApplicationService.java new file mode 100644 index 0000000..0f94367 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/application/service/MediaApplicationService.java @@ -0,0 +1,196 @@ +package com.tuoheng.airport.media.application.service; + +import com.tuoheng.airport.media.application.dto.*; + +import java.util.List; + +/** + * 媒体资源应用服务接口(Application层) + * 定义媒体资源相关的用例(Use Cases) + * 协调领域模型完成媒体资源管理操作 + * + * @author tuoheng + */ +public interface MediaApplicationService { + + /** + * 查询媒体资源详情 + * + * @param id 媒体资源ID + * @return 媒体资源响应 + */ + MediaInfoResponse getMediaById(Long id); + + /** + * 根据条件查询媒体资源列表 + * + * @param request 查询请求 + * @return 媒体资源列表 + */ + List queryMedia(MediaQueryRequest request); + + /** + * 根据飞行记录ID查询媒体资源列表 + * 业务逻辑: + * 1. 验证飞行记录存在 + * 2. 查询该飞行记录的所有媒体资源 + * 3. 按拍摄时间排序 + * + * @param flightRecordId 飞行记录ID + * @return 媒体资源列表 + */ + List getMediaByFlightRecordId(Long flightRecordId); + + /** + * 根据设备ID查询媒体资源列表 + * + * @param deviceId 设备ID + * @return 媒体资源列表 + */ + List getMediaByDeviceId(Long deviceId); + + /** + * 更新媒体资源信息 + * 业务逻辑: + * 1. 验证媒体资源存在 + * 2. 更新元数据信息(标题、描述、标签等) + * 3. 不允许修改文件本身 + * + * @param request 更新请求 + * @return 媒体资源响应 + */ + MediaInfoResponse updateMedia(MediaUpdateRequest request); + + /** + * 删除媒体资源 + * 业务逻辑: + * 1. 验证媒体资源存在 + * 2. 检查是否被引用(分析结果等) + * 3. 从存储服务删除文件 + * 4. 删除媒体资源元数据(逻辑删除) + * + * @param id 媒体资源ID + */ + void deleteMedia(Long id); + + /** + * 批量删除媒体资源 + * 业务逻辑: + * 1. 验证所有媒体资源 + * 2. 批量从存储服务删除 + * 3. 批量删除元数据 + * + * @param mediaIds 媒体资源ID列表 + */ + void batchDeleteMedia(List mediaIds); + + /** + * 下载媒体资源 + * 业务逻辑: + * 1. 验证媒体资源存在 + * 2. 检查访问权限 + * 3. 从存储服务获取文件 + * 4. 记录下载日志 + * + * @param id 媒体资源ID + */ + void downloadMedia(Long id); + + /** + * 批量下载媒体资源(ZIP压缩包) + * 业务逻辑: + * 1. 验证所有媒体资源 + * 2. 从存储服务获取所有文件 + * 3. 打包为ZIP文件 + * 4. 返回ZIP文件流 + * + * @param mediaIds 媒体资源ID列表 + */ + void batchDownloadMedia(List mediaIds); + + /** + * 获取媒体资源访问URL + * 业务逻辑: + * 1. 验证媒体资源存在 + * 2. 调用存储服务生成临时访问URL + * 3. 设置URL有效期 + * + * @param id 媒体资源ID + * @param expireSeconds URL有效期(秒) + * @return 媒体资源访问URL + */ + String getMediaUrl(Long id, Long expireSeconds); + + /** + * 标记媒体资源为已分析 + * 业务逻辑: + * 1. 验证媒体资源存在 + * 2. 保存AI分析结果 + * 3. 更新媒体资源状态为已分析 + * 4. 触发后续处理(告警、统计等) + * + * @param id 媒体资源ID + * @param analysisResult 分析结果 + * @return 媒体资源响应 + */ + MediaInfoResponse markMediaAsAnalyzed(Long id, MediaAnalysisResultRequest analysisResult); + + /** + * 查询媒体资源统计信息 + * 业务逻辑: + * 1. 统计媒体资源总数 + * 2. 按类型分组统计(图片、视频) + * 3. 统计存储空间使用量 + * 4. 统计分析完成率 + * + * @param flightRecordId 飞行记录ID(可选) + * @param deviceId 设备ID(可选) + * @return 媒体统计信息 + */ + MediaStatisticsResponse getMediaStatistics(Long flightRecordId, Long deviceId); + + /** + * 同步媒体资源上传状态 + * 业务逻辑: + * 1. 查询存储服务中的文件状态 + * 2. 更新媒体资源的上传状态 + * 3. 处理上传失败的情况 + * + * @param id 媒体资源ID + * @return 媒体资源响应 + */ + MediaInfoResponse syncMediaUploadStatus(Long id); + + /** + * 创建媒体资源记录(由设备上报触发) + * 业务逻辑: + * 1. 接收设备上报的媒体信息 + * 2. 创建媒体资源记录 + * 3. 关联飞行记录 + * 4. 触发文件上传任务 + * + * @param request 创建请求 + * @return 媒体资源响应 + */ + MediaInfoResponse createMediaFromDevice(MediaCreateFromDeviceRequest request); + + /** + * 批量创建媒体资源记录 + * + * @param requests 创建请求列表 + * @return 媒体资源列表 + */ + List batchCreateMediaFromDevice(List requests); + + /** + * 重新上传媒体资源 + * 业务逻辑: + * 1. 验证媒体资源存在 + * 2. 检查上传状态为失败 + * 3. 重新触发上传任务 + * + * @param id 媒体资源ID + * @return 媒体资源响应 + */ + MediaInfoResponse retryUploadMedia(Long id); +} diff --git a/src/main/java/com/tuoheng/airport/media/domain/model/MediaAnalysisResult.java b/src/main/java/com/tuoheng/airport/media/domain/model/MediaAnalysisResult.java new file mode 100644 index 0000000..c8bc667 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/domain/model/MediaAnalysisResult.java @@ -0,0 +1,50 @@ +package com.tuoheng.airport.media.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 媒体分析结果领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MediaAnalysisResult { + + /** + * 是否发现异常 + */ + private Boolean hasAbnormality; + + /** + * 异常类型列表 + */ + private List abnormalityTypes; + + /** + * 异常描述 + */ + private String abnormalityDescription; + + /** + * 置信度(0-1) + */ + private Double confidence; + + /** + * AI模型版本 + */ + private String modelVersion; + + /** + * 分析耗时(毫秒) + */ + private Long analysisTime; +} diff --git a/src/main/java/com/tuoheng/airport/media/domain/model/MediaAnalysisStatus.java b/src/main/java/com/tuoheng/airport/media/domain/model/MediaAnalysisStatus.java new file mode 100644 index 0000000..02b06e2 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/domain/model/MediaAnalysisStatus.java @@ -0,0 +1,24 @@ +package com.tuoheng.airport.media.domain.model; + +/** + * 媒体分析状态枚举 + * + * @author tuoheng + */ +public enum MediaAnalysisStatus { + + PENDING("待分析"), + ANALYZING("分析中"), + COMPLETED("分析完成"), + FAILED("分析失败"); + + private final String description; + + MediaAnalysisStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/tuoheng/airport/media/domain/model/MediaInfo.java b/src/main/java/com/tuoheng/airport/media/domain/model/MediaInfo.java new file mode 100644 index 0000000..45587f9 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/domain/model/MediaInfo.java @@ -0,0 +1,90 @@ +package com.tuoheng.airport.media.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 媒体资源领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MediaInfo { + + private Long id; + private String mediaName; + private MediaType mediaType; + private String fileUrl; + private Long fileSize; + private MediaUploadStatus uploadStatus; + private Long flightRecordId; + private Long deviceId; + private String shootingLocation; + private LocalDateTime shootingTime; + private MediaAnalysisStatus analysisStatus; + private String analysisResult; + private Long tenantId; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Boolean deleted; + + // ==================== 业务方法 ==================== + + public void markAsUploading() { + this.uploadStatus = MediaUploadStatus.UPLOADING; + this.updateTime = LocalDateTime.now(); + } + + public void markAsUploaded(String fileUrl) { + this.uploadStatus = MediaUploadStatus.COMPLETED; + this.fileUrl = fileUrl; + this.updateTime = LocalDateTime.now(); + } + + public void markAsUploadFailed() { + this.uploadStatus = MediaUploadStatus.FAILED; + this.updateTime = LocalDateTime.now(); + } + + public void markAsAnalyzed(String analysisResult) { + this.analysisStatus = MediaAnalysisStatus.COMPLETED; + this.analysisResult = analysisResult; + this.updateTime = LocalDateTime.now(); + } + + public boolean isUploaded() { + return this.uploadStatus == MediaUploadStatus.COMPLETED; + } + + public boolean isAnalyzed() { + return this.analysisStatus == MediaAnalysisStatus.COMPLETED; + } + + public void delete() { + this.deleted = true; + this.updateTime = LocalDateTime.now(); + } + + public static MediaInfo create(String mediaName, MediaType mediaType, Long flightRecordId, Long deviceId, Long tenantId) { + LocalDateTime now = LocalDateTime.now(); + return MediaInfo.builder() + .mediaName(mediaName) + .mediaType(mediaType) + .uploadStatus(MediaUploadStatus.PENDING) + .analysisStatus(MediaAnalysisStatus.PENDING) + .flightRecordId(flightRecordId) + .deviceId(deviceId) + .tenantId(tenantId) + .createTime(now) + .updateTime(now) + .deleted(false) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/media/domain/model/MediaStatistics.java b/src/main/java/com/tuoheng/airport/media/domain/model/MediaStatistics.java new file mode 100644 index 0000000..af37cd1 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/domain/model/MediaStatistics.java @@ -0,0 +1,73 @@ +package com.tuoheng.airport.media.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 媒体统计领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MediaStatistics { + + /** + * 媒体总数 + */ + private Long totalCount; + + /** + * 图片数量 + */ + private Long imageCount; + + /** + * 视频数量 + */ + private Long videoCount; + + /** + * 已上传数量 + */ + private Long uploadedCount; + + /** + * 已分析数量 + */ + private Long analyzedCount; + + /** + * 发现异常数量 + */ + private Long abnormalityCount; + + /** + * 总存储空间(字节) + */ + private Long totalStorageSize; + + /** + * 分析完成率 + */ + public double getAnalysisCompletionRate() { + if (totalCount == null || totalCount == 0) { + return 0.0; + } + return (double) analyzedCount / totalCount * 100; + } + + /** + * 异常率 + */ + public double getAbnormalityRate() { + if (analyzedCount == null || analyzedCount == 0) { + return 0.0; + } + return (double) abnormalityCount / analyzedCount * 100; + } +} diff --git a/src/main/java/com/tuoheng/airport/media/domain/model/MediaType.java b/src/main/java/com/tuoheng/airport/media/domain/model/MediaType.java new file mode 100644 index 0000000..7f3e039 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/domain/model/MediaType.java @@ -0,0 +1,22 @@ +package com.tuoheng.airport.media.domain.model; + +/** + * 媒体类型枚举 + * + * @author tuoheng + */ +public enum MediaType { + + IMAGE("图片"), + VIDEO("视频"); + + private final String description; + + MediaType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/tuoheng/airport/media/domain/model/MediaUploadStatus.java b/src/main/java/com/tuoheng/airport/media/domain/model/MediaUploadStatus.java new file mode 100644 index 0000000..9f3907c --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/domain/model/MediaUploadStatus.java @@ -0,0 +1,24 @@ +package com.tuoheng.airport.media.domain.model; + +/** + * 媒体上传状态枚举 + * + * @author tuoheng + */ +public enum MediaUploadStatus { + + PENDING("待上传"), + UPLOADING("上传中"), + COMPLETED("上传完成"), + FAILED("上传失败"); + + private final String description; + + MediaUploadStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/tuoheng/airport/media/domain/repository/MediaInfoRepository.java b/src/main/java/com/tuoheng/airport/media/domain/repository/MediaInfoRepository.java new file mode 100644 index 0000000..6518dfe --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/domain/repository/MediaInfoRepository.java @@ -0,0 +1,36 @@ +package com.tuoheng.airport.media.domain.repository; + +import com.tuoheng.airport.media.domain.model.MediaInfo; + +import java.util.List; +import java.util.Optional; + +/** + * 媒体资源仓储接口 + * + * @author tuoheng + */ +public interface MediaInfoRepository { + + MediaInfo save(MediaInfo mediaInfo); + + Optional findById(Long id); + + List findByFlightRecordId(Long flightRecordId); + + List findByDeviceId(Long deviceId); + + List findByUploadStatus(String uploadStatus); + + List findByAnalysisStatus(String analysisStatus); + + void delete(Long id); + + void batchDelete(List ids); + + long countByFlightRecordId(Long flightRecordId); + + long countByDeviceId(Long deviceId); + + long sumFileSizeByTenantId(Long tenantId); +} diff --git a/src/main/java/com/tuoheng/airport/media/domain/service/MediaDomainService.java b/src/main/java/com/tuoheng/airport/media/domain/service/MediaDomainService.java new file mode 100644 index 0000000..5bee333 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/domain/service/MediaDomainService.java @@ -0,0 +1,227 @@ +package com.tuoheng.airport.media.domain.service; + +import com.tuoheng.airport.media.domain.model.MediaInfo; +import com.tuoheng.airport.media.domain.model.MediaAnalysisResult; +import com.tuoheng.airport.media.domain.model.MediaStatistics; + +import java.util.List; + +/** + * 媒体资源领域服务接口(Domain层) + * 封装媒体资源相关的业务逻辑和业务规则 + * + * Domain Service 的职责: + * 1. 封装媒体资源管理的核心业务规则 + * 2. 协调媒体资源和存储服务 + * 3. 保证媒体资源数据的一致性 + * 4. 不包含事务管理(事务由 Application 层管理) + * + * @author tuoheng + */ +public interface MediaDomainService { + + /** + * 创建媒体资源记录 + * 业务规则: + * 1. 生成唯一的媒体资源ID + * 2. 验证文件类型(图片/视频) + * 3. 关联飞行记录和设备 + * 4. 初始化上传状态为待上传 + * + * @param mediaInfo 媒体资源领域模型 + * @return 创建后的媒体资源 + */ + MediaInfo createMediaInfo(MediaInfo mediaInfo); + + /** + * 批量创建媒体资源记录 + * 业务规则: + * 1. 验证所有媒体资源 + * 2. 批量创建记录 + * 3. 关联同一飞行记录 + * + * @param mediaInfos 媒体资源列表 + * @return 创建后的媒体资源列表 + */ + List batchCreateMediaInfo(List mediaInfos); + + /** + * 更新媒体资源上传状态 + * 业务规则: + * 1. 验证状态转换合法性(待上传->上传中->完成/失败) + * 2. 记录上传时间 + * 3. 上传完成后触发分析任务 + * + * @param mediaId 媒体资源ID + * @param uploadStatus 上传状态 + * @param fileUrl 文件URL(上传完成时) + * @return 更新后的媒体资源 + */ + MediaInfo updateUploadStatus(Long mediaId, String uploadStatus, String fileUrl); + + /** + * 标记媒体资源为已分析 + * 业务规则: + * 1. 验证媒体资源已上传完成 + * 2. 保存AI分析结果 + * 3. 更新分析状态 + * 4. 如果发现异常,触发告警 + * + * @param mediaId 媒体资源ID + * @param analysisResult 分析结果 + * @return 更新后的媒体资源 + */ + MediaInfo markAsAnalyzed(Long mediaId, MediaAnalysisResult analysisResult); + + /** + * 删除媒体资源 + * 业务规则: + * 1. 检查媒体资源是否被引用 + * 2. 从存储服务删除文件 + * 3. 逻辑删除媒体资源记录 + * 4. 更新统计信息 + * + * @param mediaId 媒体资源ID + */ + void deleteMediaInfo(Long mediaId); + + /** + * 批量删除媒体资源 + * 业务规则: + * 1. 验证所有媒体资源可以删除 + * 2. 批量从存储服务删除 + * 3. 批量删除记录 + * + * @param mediaIds 媒体资源ID列表 + */ + void batchDeleteMediaInfo(List mediaIds); + + /** + * 查询媒体资源详情 + * + * @param mediaId 媒体资源ID + * @return 媒体资源领域模型 + */ + MediaInfo getMediaInfoById(Long mediaId); + + /** + * 根据飞行记录ID查询媒体资源列表 + * + * @param flightRecordId 飞行记录ID + * @return 媒体资源列表 + */ + List getMediaInfoByFlightRecordId(Long flightRecordId); + + /** + * 根据设备ID查询媒体资源列表 + * + * @param deviceId 设备ID + * @return 媒体资源列表 + */ + List getMediaInfoByDeviceId(Long deviceId); + + /** + * 生成媒体资源访问URL + * 业务规则: + * 1. 验证媒体资源已上传完成 + * 2. 调用存储服务生成临时URL + * 3. 设置URL有效期 + * + * @param mediaId 媒体资源ID + * @param expireSeconds URL有效期(秒) + * @return 媒体资源访问URL + */ + String generateMediaUrl(Long mediaId, Long expireSeconds); + + /** + * 检查媒体资源是否被引用 + * 业务规则: + * 1. 检查是否有分析结果引用 + * 2. 检查是否有告警引用 + * 3. 被引用的媒体资源不能删除 + * + * @param mediaId 媒体资源ID + * @return 是否被引用 + */ + boolean isMediaReferenced(Long mediaId); + + /** + * 计算媒体资源统计信息 + * 业务规则: + * 1. 统计媒体资源总数 + * 2. 按类型分组统计(图片、视频) + * 3. 统计存储空间使用量 + * 4. 统计分析完成率 + * + * @param flightRecordId 飞行记录ID(可选) + * @param deviceId 设备ID(可选) + * @return 媒体统计信息 + */ + MediaStatistics calculateStatistics(Long flightRecordId, Long deviceId); + + /** + * 同步媒体资源上传状态 + * 业务规则: + * 1. 查询存储服务中的文件状态 + * 2. 更新媒体资源的上传状态 + * 3. 处理上传超时的情况 + * + * @param mediaId 媒体资源ID + * @return 更新后的媒体资源 + */ + MediaInfo syncUploadStatus(Long mediaId); + + /** + * 重新上传媒体资源 + * 业务规则: + * 1. 验证上传状态为失败 + * 2. 重置上传状态为待上传 + * 3. 触发上传任务 + * + * @param mediaId 媒体资源ID + * @return 更新后的媒体资源 + */ + MediaInfo retryUpload(Long mediaId); + + /** + * 验证媒体文件类型 + * 业务规则: + * 1. 检查文件扩展名 + * 2. 支持的图片格式:jpg, jpeg, png, bmp + * 3. 支持的视频格式:mp4, avi, mov + * + * @param fileName 文件名 + * @return 是否为有效的媒体文件类型 + */ + boolean isValidMediaType(String fileName); + + /** + * 获取媒体文件类型 + * 根据文件扩展名判断是图片还是视频 + * + * @param fileName 文件名 + * @return 媒体类型(IMAGE/VIDEO) + */ + String getMediaType(String fileName); + + /** + * 清理过期的未上传媒体资源 + * 业务规则: + * 1. 查找创建超过24小时但未上传的媒体资源 + * 2. 批量删除这些记录 + * + * @return 清理的媒体资源数量 + */ + int cleanExpiredUnuploadedMedia(); + + /** + * 检查媒体资源上传是否超时 + * 业务规则: + * 1. 上传中状态超过30分钟视为超时 + * 2. 超时后标记为上传失败 + * + * @param mediaId 媒体资源ID + * @return 是否超时 + */ + boolean isUploadTimeout(Long mediaId); +} diff --git a/src/main/java/com/tuoheng/airport/media/presentation/controller/MediaController.java b/src/main/java/com/tuoheng/airport/media/presentation/controller/MediaController.java new file mode 100644 index 0000000..b7cc204 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/presentation/controller/MediaController.java @@ -0,0 +1,221 @@ +package com.tuoheng.airport.media.presentation.controller; + +import com.tuoheng.airport.media.application.dto.*; +import com.tuoheng.airport.media.application.service.MediaApplicationService; +import com.tuoheng.airport.media.presentation.converter.MediaVoConverter; +import com.tuoheng.airport.media.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 java.util.List; + +/** + * 媒体资源管理控制器(Presentation层) + * 提供媒体资源管理的 REST API 接口 + * 负责图片、视频等媒体文件的管理 + * + * 职责: + * 1. 接收前端的 VO 对象 + * 2. 将 VO 转换为 DTO 传递给 Application 层 + * 3. 将 Application 层返回的 DTO 转换为 VO 返回给前端 + * 4. 不包含任何业务逻辑 + * + * @author tuoheng + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/media") +@RequiredArgsConstructor +@Validated +@Tag(name = "媒体资源管理", description = "媒体资源管理相关接口") +public class MediaController { + + private final MediaApplicationService mediaApplicationService; + + /** + * 查询媒体资源详情 + * GET /api/v1/media/{id} + */ + @GetMapping("/{id}") + @Operation(summary = "查询媒体资源详情", description = "根据媒体资源ID查询详细信息") + public Result getMediaById( + @Parameter(description = "媒体资源ID") @PathVariable Long id) { + log.info("接收到查询媒体资源请求,媒体资源ID: {}", id); + MediaInfoResponse response = mediaApplicationService.getMediaById(id); + MediaInfoVO result = MediaVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询媒体资源列表 + * GET /api/v1/media + */ + @GetMapping + @Operation(summary = "查询媒体资源列表", description = "根据条件查询媒体资源列表") + public Result> queryMedia(MediaQueryVO vo) { + log.info("接收到查询媒体资源列表请求,查询条件: {}", vo); + MediaQueryRequest request = MediaVoConverter.toQueryRequest(vo); + List responses = mediaApplicationService.queryMedia(request); + List results = MediaVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 根据飞行记录ID查询媒体资源列表 + * GET /api/v1/media/flight/{flightRecordId} + */ + @GetMapping("/flight/{flightRecordId}") + @Operation(summary = "查询飞行媒体资源", description = "查询指定飞行记录的所有媒体资源") + public Result> getMediaByFlightRecordId( + @Parameter(description = "飞行记录ID") @PathVariable Long flightRecordId) { + log.info("接收到查询飞行媒体资源请求,飞行记录ID: {}", flightRecordId); + List responses = mediaApplicationService.getMediaByFlightRecordId(flightRecordId); + List results = MediaVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 根据设备ID查询媒体资源列表 + * GET /api/v1/media/device/{deviceId} + */ + @GetMapping("/device/{deviceId}") + @Operation(summary = "查询设备媒体资源", description = "查询指定设备的所有媒体资源") + public Result> getMediaByDeviceId( + @Parameter(description = "设备ID") @PathVariable Long deviceId) { + log.info("接收到查询设备媒体资源请求,设备ID: {}", deviceId); + List responses = mediaApplicationService.getMediaByDeviceId(deviceId); + List results = MediaVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 更新媒体资源信息 + * PUT /api/v1/media/{id} + */ + @PutMapping("/{id}") + @Operation(summary = "更新媒体资源信息", description = "更新媒体资源的元数据信息") + public Result updateMedia( + @Parameter(description = "媒体资源ID") @PathVariable Long id, + @Valid @RequestBody MediaUpdateVO vo) { + log.info("接收到更新媒体资源请求,媒体资源ID: {}, 请求参数: {}", id, vo); + vo.setId(id); + MediaUpdateRequest request = MediaVoConverter.toUpdateRequest(vo); + MediaInfoResponse response = mediaApplicationService.updateMedia(request); + MediaInfoVO result = MediaVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 删除媒体资源 + * DELETE /api/v1/media/{id} + */ + @DeleteMapping("/{id}") + @Operation(summary = "删除媒体资源", description = "删除指定的媒体资源") + public Result deleteMedia( + @Parameter(description = "媒体资源ID") @PathVariable Long id) { + log.info("接收到删除媒体资源请求,媒体资源ID: {}", id); + mediaApplicationService.deleteMedia(id); + return Result.success(); + } + + /** + * 批量删除媒体资源 + * POST /api/v1/media/batch-delete + */ + @PostMapping("/batch-delete") + @Operation(summary = "批量删除媒体资源", description = "批量删除多个媒体资源") + public Result batchDeleteMedia(@RequestBody List mediaIds) { + log.info("接收到批量删除媒体资源请求,媒体资源数量: {}", mediaIds.size()); + mediaApplicationService.batchDeleteMedia(mediaIds); + return Result.success(); + } + + /** + * 下载媒体资源 + * GET /api/v1/media/{id}/download + */ + @GetMapping("/{id}/download") + @Operation(summary = "下载媒体资源", description = "下载指定的媒体资源文件") + public void downloadMedia( + @Parameter(description = "媒体资源ID") @PathVariable Long id) { + log.info("接收到下载媒体资源请求,媒体资源ID: {}", id); + mediaApplicationService.downloadMedia(id); + } + + /** + * 批量下载媒体资源(ZIP压缩包) + * POST /api/v1/media/batch-download + */ + @PostMapping("/batch-download") + @Operation(summary = "批量下载媒体资源", description = "批量下载多个媒体资源,打包为ZIP文件") + public void batchDownloadMedia(@RequestBody List mediaIds) { + log.info("接收到批量下载媒体资源请求,媒体资源数量: {}", mediaIds.size()); + mediaApplicationService.batchDownloadMedia(mediaIds); + } + + /** + * 获取媒体资源访问URL + * GET /api/v1/media/{id}/url + */ + @GetMapping("/{id}/url") + @Operation(summary = "获取媒体资源URL", description = "获取媒体资源的临时访问URL") + public Result getMediaUrl( + @Parameter(description = "媒体资源ID") @PathVariable Long id, + @Parameter(description = "URL有效期(秒)") @RequestParam(defaultValue = "3600") Long expireSeconds) { + log.info("接收到获取媒体资源URL请求,媒体资源ID: {}, 有效期: {}秒", id, expireSeconds); + String url = mediaApplicationService.getMediaUrl(id, expireSeconds); + return Result.success(url); + } + + /** + * 标记媒体资源为已分析 + * POST /api/v1/media/{id}/analyzed + */ + @PostMapping("/{id}/analyzed") + @Operation(summary = "标记媒体已分析", description = "标记媒体资源已完成AI分析") + public Result markMediaAsAnalyzed( + @Parameter(description = "媒体资源ID") @PathVariable Long id, + @Valid @RequestBody MediaAnalysisResultVO analysisResult) { + log.info("接收到标记媒体已分析请求,媒体资源ID: {}", id); + MediaAnalysisResultRequest request = MediaVoConverter.toAnalysisResultRequest(analysisResult); + MediaInfoResponse response = mediaApplicationService.markMediaAsAnalyzed(id, request); + MediaInfoVO result = MediaVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询媒体资源统计信息 + * GET /api/v1/media/statistics + */ + @GetMapping("/statistics") + @Operation(summary = "查询媒体统计", description = "查询媒体资源统计信息") + public Result getMediaStatistics( + @Parameter(description = "飞行记录ID") @RequestParam(required = false) Long flightRecordId, + @Parameter(description = "设备ID") @RequestParam(required = false) Long deviceId) { + log.info("接收到查询媒体统计请求,飞行记录ID: {}, 设备ID: {}", flightRecordId, deviceId); + MediaStatisticsResponse response = mediaApplicationService.getMediaStatistics(flightRecordId, deviceId); + MediaStatisticsVO result = MediaVoConverter.toStatisticsVO(response); + return Result.success(result); + } + + /** + * 同步媒体资源上传状态 + * POST /api/v1/media/{id}/sync-status + */ + @PostMapping("/{id}/sync-status") + @Operation(summary = "同步上传状态", description = "同步媒体资源的上传状态") + public Result syncMediaUploadStatus( + @Parameter(description = "媒体资源ID") @PathVariable Long id) { + log.info("接收到同步媒体上传状态请求,媒体资源ID: {}", id); + MediaInfoResponse response = mediaApplicationService.syncMediaUploadStatus(id); + MediaInfoVO result = MediaVoConverter.toVO(response); + return Result.success(result); + } +} diff --git a/src/main/java/com/tuoheng/airport/media/presentation/converter/MediaVoConverter.java b/src/main/java/com/tuoheng/airport/media/presentation/converter/MediaVoConverter.java new file mode 100644 index 0000000..eb92527 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/presentation/converter/MediaVoConverter.java @@ -0,0 +1,43 @@ +package com.tuoheng.airport.media.presentation.converter; + +import com.tuoheng.airport.media.application.dto.*; +import com.tuoheng.airport.media.presentation.vo.*; +import java.util.List; +import java.util.stream.Collectors; + +public class MediaVoConverter { + + public static MediaQueryRequest toQueryRequest(MediaQueryVO vo) { + // TODO: 实现转换逻辑 + return new MediaQueryRequest(); + } + + public static MediaUpdateRequest toUpdateRequest(MediaUpdateVO vo) { + // TODO: 实现转换逻辑 + return new MediaUpdateRequest(); + } + + public static MediaInfoVO toVO(MediaInfoResponse response) { + // TODO: 实现转换逻辑 + return new MediaInfoVO(); + } + + public static List toVOList(List responses) { + return responses.stream().map(MediaVoConverter::toVO).collect(Collectors.toList()); + } + + public static MediaAnalysisResultRequest toAnalysisRequest(MediaAnalysisResultVO vo) { + // TODO: 实现转换逻辑 + return new MediaAnalysisResultRequest(); + } + + public static MediaAnalysisResultRequest toAnalysisResultRequest(MediaAnalysisResultVO vo) { + // TODO: 实现转换逻辑 + return new MediaAnalysisResultRequest(); + } + + public static MediaStatisticsVO toStatisticsVO(MediaStatisticsResponse response) { + // TODO: 实现转换逻辑 + return new MediaStatisticsVO(); + } +} diff --git a/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaAnalysisResultVO.java b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaAnalysisResultVO.java new file mode 100644 index 0000000..1ceee1e --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaAnalysisResultVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.media.presentation.vo; + +import lombok.Data; + +@Data +public class MediaAnalysisResultVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaInfoVO.java b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaInfoVO.java new file mode 100644 index 0000000..12853a5 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaInfoVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.media.presentation.vo; + +import lombok.Data; + +@Data +public class MediaInfoVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaQueryVO.java b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaQueryVO.java new file mode 100644 index 0000000..773c2a7 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaQueryVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.media.presentation.vo; + +import lombok.Data; + +@Data +public class MediaQueryVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaStatisticsVO.java b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaStatisticsVO.java new file mode 100644 index 0000000..0765c8e --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaStatisticsVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.media.presentation.vo; + +import lombok.Data; + +@Data +public class MediaStatisticsVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaUpdateVO.java b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaUpdateVO.java new file mode 100644 index 0000000..73a1dc0 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/media/presentation/vo/MediaUpdateVO.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.media.presentation.vo; + +import lombok.Data; + +@Data +public class MediaUpdateVO { + private Long id; + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/storage/application/dto/BatchFileUploadRequest.java b/src/main/java/com/tuoheng/airport/storage/application/dto/BatchFileUploadRequest.java new file mode 100644 index 0000000..5a099d3 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/application/dto/BatchFileUploadRequest.java @@ -0,0 +1,34 @@ +package com.tuoheng.airport.storage.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +/** + * 批量文件上传请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BatchFileUploadRequest { + + /** + * 文件列表 + */ + private MultipartFile[] files; + + /** + * 业务类型 + */ + private String bizType; + + /** + * 业务ID + */ + private String bizId; +} diff --git a/src/main/java/com/tuoheng/airport/storage/application/dto/FileInfoResponse.java b/src/main/java/com/tuoheng/airport/storage/application/dto/FileInfoResponse.java new file mode 100644 index 0000000..5f70134 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/application/dto/FileInfoResponse.java @@ -0,0 +1,34 @@ +package com.tuoheng.airport.storage.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 文件信息响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileInfoResponse { + + private String fileId; + private String fileName; + private Long fileSize; + private String contentType; + private String fileExtension; + private String fileUrl; + private String bizType; + private String bizId; + private String uploadStatus; + private Long tenantId; + private Long creatorId; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/tuoheng/airport/storage/application/dto/FileQueryRequest.java b/src/main/java/com/tuoheng/airport/storage/application/dto/FileQueryRequest.java new file mode 100644 index 0000000..5723714 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/application/dto/FileQueryRequest.java @@ -0,0 +1,48 @@ +package com.tuoheng.airport.storage.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 文件查询请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileQueryRequest { + + /** + * 文件名(模糊查询) + */ + private String fileName; + + /** + * 业务类型 + */ + private String bizType; + + /** + * 业务ID + */ + private String bizId; + + /** + * 上传状态 + */ + private String uploadStatus; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 创建人ID + */ + private Long creatorId; +} diff --git a/src/main/java/com/tuoheng/airport/storage/application/dto/FileUploadRequest.java b/src/main/java/com/tuoheng/airport/storage/application/dto/FileUploadRequest.java new file mode 100644 index 0000000..500e56d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/application/dto/FileUploadRequest.java @@ -0,0 +1,34 @@ +package com.tuoheng.airport.storage.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +/** + * 文件上传请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileUploadRequest { + + /** + * 文件 + */ + private MultipartFile file; + + /** + * 业务类型 + */ + private String bizType; + + /** + * 业务ID + */ + private String bizId; +} diff --git a/src/main/java/com/tuoheng/airport/storage/application/dto/StorageStatisticsResponse.java b/src/main/java/com/tuoheng/airport/storage/application/dto/StorageStatisticsResponse.java new file mode 100644 index 0000000..82f8a01 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/application/dto/StorageStatisticsResponse.java @@ -0,0 +1,27 @@ +package com.tuoheng.airport.storage.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 存储统计响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StorageStatisticsResponse { + + private Long totalFileCount; + private Long totalStorageUsed; + private Map fileCountByType; + private Map storageByType; + private Map fileCountByBizType; + private Long tenantId; +} diff --git a/src/main/java/com/tuoheng/airport/storage/application/service/StorageApplicationService.java b/src/main/java/com/tuoheng/airport/storage/application/service/StorageApplicationService.java new file mode 100644 index 0000000..8e2c310 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/application/service/StorageApplicationService.java @@ -0,0 +1,152 @@ +package com.tuoheng.airport.storage.application.service; + +import com.tuoheng.airport.storage.application.dto.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 存储应用服务接口(Application层) + * 定义文件存储相关的用例(Use Cases) + * 协调领域模型完成文件存储操作 + * + * @author tuoheng + */ +public interface StorageApplicationService { + + /** + * 上传文件 + * 业务逻辑: + * 1. 验证文件大小和类型 + * 2. 生成唯一文件ID + * 3. 上传到OSS存储 + * 4. 保存文件元数据 + * + * @param request 文件上传请求 + * @return 文件信息响应 + */ + FileInfoResponse uploadFile(FileUploadRequest request); + + /** + * 批量上传文件 + * 业务逻辑: + * 1. 验证所有文件 + * 2. 并发上传到OSS + * 3. 批量保存文件元数据 + * + * @param request 批量文件上传请求 + * @return 文件信息列表 + */ + List batchUploadFiles(BatchFileUploadRequest request); + + /** + * 下载文件 + * 业务逻辑: + * 1. 验证文件访问权限 + * 2. 从OSS下载文件 + * 3. 记录下载日志 + * + * @param fileId 文件ID + */ + void downloadFile(String fileId); + + /** + * 获取文件访问URL + * 业务逻辑: + * 1. 验证文件存在 + * 2. 生成临时访问URL(带签名) + * 3. 设置URL有效期 + * + * @param fileId 文件ID + * @param expireSeconds URL有效期(秒) + * @return 文件访问URL + */ + String getFileUrl(String fileId, Long expireSeconds); + + /** + * 查询文件信息 + * + * @param fileId 文件ID + * @return 文件信息响应 + */ + FileInfoResponse getFileInfo(String fileId); + + /** + * 根据条件查询文件列表 + * + * @param request 查询请求 + * @return 文件信息列表 + */ + List queryFiles(FileQueryRequest request); + + /** + * 根据业务ID查询文件列表 + * + * @param bizId 业务ID + * @return 文件信息列表 + */ + List getFilesByBizId(String bizId); + + /** + * 删除文件 + * 业务逻辑: + * 1. 验证文件存在 + * 2. 检查文件是否被引用 + * 3. 从OSS删除文件 + * 4. 删除文件元数据(逻辑删除) + * + * @param fileId 文件ID + */ + void deleteFile(String fileId); + + /** + * 批量删除文件 + * 业务逻辑: + * 1. 验证所有文件 + * 2. 批量从OSS删除 + * 3. 批量删除元数据 + * + * @param fileIds 文件ID列表 + */ + void batchDeleteFiles(List fileIds); + + /** + * 查询存储统计信息 + * 业务逻辑: + * 1. 统计文件总数 + * 2. 统计存储空间使用量 + * 3. 按文件类型分组统计 + * + * @param tenantId 租户ID(可选) + * @return 存储统计信息 + */ + StorageStatisticsResponse getStorageStatistics(Long tenantId); + + /** + * 复制文件 + * 业务逻辑: + * 1. 验证源文件存在 + * 2. 在OSS中复制文件 + * 3. 创建新的文件元数据 + * + * @param sourceFileId 源文件ID + * @param targetBizType 目标业务类型 + * @param targetBizId 目标业务ID + * @return 新文件信息 + */ + FileInfoResponse copyFile(String sourceFileId, String targetBizType, String targetBizId); + + /** + * 移动文件 + * 业务逻辑: + * 1. 验证源文件存在 + * 2. 更新文件的业务关联 + * 3. 不改变OSS中的文件位置 + * + * @param fileId 文件ID + * @param targetBizType 目标业务类型 + * @param targetBizId 目标业务ID + * @return 更新后的文件信息 + */ + FileInfoResponse moveFile(String fileId, String targetBizType, String targetBizId); +} diff --git a/src/main/java/com/tuoheng/airport/storage/domain/model/FileInfo.java b/src/main/java/com/tuoheng/airport/storage/domain/model/FileInfo.java new file mode 100644 index 0000000..6287457 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/domain/model/FileInfo.java @@ -0,0 +1,207 @@ +package com.tuoheng.airport.storage.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 文件信息领域模型 + * 文件存储的核心实体 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileInfo { + + /** + * 文件ID + */ + private String fileId; + + /** + * 文件名 + */ + private String fileName; + + /** + * 文件大小(字节) + */ + private Long fileSize; + + /** + * 文件类型(MIME类型) + */ + private String contentType; + + /** + * 文件扩展名 + */ + private String fileExtension; + + /** + * 文件MD5值(用于去重) + */ + private String md5; + + /** + * 存储路径 + */ + private String storagePath; + + /** + * 文件访问URL + */ + private String fileUrl; + + /** + * 业务类型(airline/media/log等) + */ + private String bizType; + + /** + * 业务ID + */ + private String bizId; + + /** + * 上传状态 + */ + private FileUploadStatus uploadStatus; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 创建人ID + */ + private Long creatorId; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; + + /** + * 是否删除 + */ + private Boolean deleted; + + // ==================== 业务方法 ==================== + + /** + * 标记为上传中 + */ + public void markAsUploading() { + this.uploadStatus = FileUploadStatus.UPLOADING; + this.updateTime = LocalDateTime.now(); + } + + /** + * 标记为上传完成 + */ + public void markAsUploaded(String fileUrl) { + if (fileUrl == null || fileUrl.trim().isEmpty()) { + throw new IllegalArgumentException("文件URL不能为空"); + } + this.uploadStatus = FileUploadStatus.COMPLETED; + this.fileUrl = fileUrl; + this.updateTime = LocalDateTime.now(); + } + + /** + * 标记为上传失败 + */ + public void markAsUploadFailed() { + this.uploadStatus = FileUploadStatus.FAILED; + this.updateTime = LocalDateTime.now(); + } + + /** + * 关联业务对象 + */ + public void associateWithBusiness(String bizType, String bizId) { + if (bizType == null || bizType.trim().isEmpty()) { + throw new IllegalArgumentException("业务类型不能为空"); + } + this.bizType = bizType; + this.bizId = bizId; + this.updateTime = LocalDateTime.now(); + } + + /** + * 验证文件是否已上传完成 + */ + public boolean isUploaded() { + return this.uploadStatus == FileUploadStatus.COMPLETED; + } + + /** + * 验证文件是否可以删除 + */ + public boolean canDelete() { + return !this.deleted; + } + + /** + * 删除文件(逻辑删除) + */ + public void delete() { + this.deleted = true; + this.updateTime = LocalDateTime.now(); + } + + /** + * 创建新文件信息(工厂方法) + */ + public static FileInfo create(String fileId, String fileName, Long fileSize, + String contentType, String md5, Long tenantId, Long creatorId) { + if (fileId == null || fileId.trim().isEmpty()) { + throw new IllegalArgumentException("文件ID不能为空"); + } + if (fileName == null || fileName.trim().isEmpty()) { + throw new IllegalArgumentException("文件名不能为空"); + } + + // 提取文件扩展名 + String fileExtension = extractFileExtension(fileName); + + LocalDateTime now = LocalDateTime.now(); + return FileInfo.builder() + .fileId(fileId) + .fileName(fileName) + .fileSize(fileSize) + .contentType(contentType) + .fileExtension(fileExtension) + .md5(md5) + .uploadStatus(FileUploadStatus.PENDING) + .tenantId(tenantId) + .creatorId(creatorId) + .createTime(now) + .updateTime(now) + .deleted(false) + .build(); + } + + /** + * 提取文件扩展名 + */ + private static String extractFileExtension(String fileName) { + if (fileName == null || !fileName.contains(".")) { + return ""; + } + return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(); + } +} diff --git a/src/main/java/com/tuoheng/airport/storage/domain/model/FileUploadStatus.java b/src/main/java/com/tuoheng/airport/storage/domain/model/FileUploadStatus.java new file mode 100644 index 0000000..78558ee --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/domain/model/FileUploadStatus.java @@ -0,0 +1,39 @@ +package com.tuoheng.airport.storage.domain.model; + +/** + * 文件上传状态枚举 + * + * @author tuoheng + */ +public enum FileUploadStatus { + + /** + * 待上传 + */ + PENDING("待上传"), + + /** + * 上传中 + */ + UPLOADING("上传中"), + + /** + * 上传完成 + */ + COMPLETED("上传完成"), + + /** + * 上传失败 + */ + FAILED("上传失败"); + + private final String description; + + FileUploadStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/tuoheng/airport/storage/domain/model/StorageStatistics.java b/src/main/java/com/tuoheng/airport/storage/domain/model/StorageStatistics.java new file mode 100644 index 0000000..4ff16c3 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/domain/model/StorageStatistics.java @@ -0,0 +1,90 @@ +package com.tuoheng.airport.storage.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 存储统计领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StorageStatistics { + + /** + * 文件总数 + */ + private Long totalFileCount; + + /** + * 存储空间使用量(字节) + */ + private Long totalStorageUsed; + + /** + * 按文件类型分组统计 + * key: 文件类型(image/video/document等) + * value: 文件数量 + */ + private Map fileCountByType; + + /** + * 按文件类型分组的存储空间统计 + * key: 文件类型 + * value: 存储空间(字节) + */ + private Map storageByType; + + /** + * 按业务类型分组统计 + * key: 业务类型(airline/media/log等) + * value: 文件数量 + */ + private Map fileCountByBizType; + + /** + * 租户ID(可选) + */ + private Long tenantId; + + /** + * 计算存储空间使用率(假设配额为100GB) + */ + public double calculateUsageRate(Long quotaBytes) { + if (quotaBytes == null || quotaBytes == 0) { + return 0.0; + } + return (double) totalStorageUsed / quotaBytes * 100; + } + + /** + * 格式化存储空间大小 + */ + public String formatStorageSize() { + return formatBytes(totalStorageUsed); + } + + /** + * 格式化字节大小 + */ + private String formatBytes(Long bytes) { + if (bytes == null || bytes == 0) { + return "0 B"; + } + String[] units = {"B", "KB", "MB", "GB", "TB"}; + int unitIndex = 0; + double size = bytes; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + return String.format("%.2f %s", size, units[unitIndex]); + } +} diff --git a/src/main/java/com/tuoheng/airport/storage/domain/repository/FileInfoRepository.java b/src/main/java/com/tuoheng/airport/storage/domain/repository/FileInfoRepository.java new file mode 100644 index 0000000..0147c35 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/domain/repository/FileInfoRepository.java @@ -0,0 +1,75 @@ +package com.tuoheng.airport.storage.domain.repository; + +import com.tuoheng.airport.storage.domain.model.FileInfo; + +import java.util.List; +import java.util.Optional; + +/** + * 文件信息仓储接口 + * 定义文件信息的数据访问操作 + * + * @author tuoheng + */ +public interface FileInfoRepository { + + /** + * 保存文件信息 + */ + FileInfo save(FileInfo fileInfo); + + /** + * 根据文件ID查询 + */ + Optional findById(String fileId); + + /** + * 根据MD5查询(用于文件去重) + */ + Optional findByMd5(String md5); + + /** + * 根据业务ID查询文件列表 + */ + List findByBizId(String bizId); + + /** + * 根据业务类型和业务ID查询 + */ + List findByBizTypeAndBizId(String bizType, String bizId); + + /** + * 根据上传状态查询 + */ + List findByUploadStatus(String uploadStatus); + + /** + * 根据租户ID查询 + */ + List findByTenantId(Long tenantId); + + /** + * 删除文件信息(逻辑删除) + */ + void delete(String fileId); + + /** + * 批量删除 + */ + void batchDelete(List fileIds); + + /** + * 统计文件总数 + */ + long countByTenantId(Long tenantId); + + /** + * 统计存储空间使用量 + */ + long sumFileSizeByTenantId(Long tenantId); + + /** + * 查询过期的临时文件 + */ + List findExpiredTempFiles(int retentionDays); +} diff --git a/src/main/java/com/tuoheng/airport/storage/domain/service/StorageDomainService.java b/src/main/java/com/tuoheng/airport/storage/domain/service/StorageDomainService.java new file mode 100644 index 0000000..8471d97 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/domain/service/StorageDomainService.java @@ -0,0 +1,213 @@ +package com.tuoheng.airport.storage.domain.service; + +import com.tuoheng.airport.storage.domain.model.FileInfo; +import com.tuoheng.airport.storage.domain.model.StorageStatistics; + +import java.io.InputStream; +import java.util.List; + +/** + * 存储领域服务接口(Domain层) + * 封装文件存储相关的业务逻辑和业务规则 + * + * Domain Service 的职责: + * 1. 封装文件存储的核心业务规则 + * 2. 协调文件元数据和OSS存储 + * 3. 保证文件存储的一致性 + * 4. 不包含事务管理(事务由 Application 层管理) + * + * @author tuoheng + */ +public interface StorageDomainService { + + /** + * 存储文件 + * 业务规则: + * 1. 文件大小不能超过限制(默认100MB) + * 2. 文件类型必须在允许列表中 + * 3. 生成唯一的文件ID和存储路径 + * 4. 计算文件MD5用于去重 + * + * @param fileInfo 文件信息领域模型 + * @param inputStream 文件输入流 + * @return 存储后的文件信息 + */ + FileInfo storeFile(FileInfo fileInfo, InputStream inputStream); + + /** + * 批量存储文件 + * 业务规则: + * 1. 验证所有文件 + * 2. 并发上传提高效率 + * 3. 部分失败不影响其他文件 + * + * @param fileInfos 文件信息列表 + * @param inputStreams 文件输入流列表 + * @return 存储后的文件信息列表 + */ + List batchStoreFiles(List fileInfos, List inputStreams); + + /** + * 删除文件 + * 业务规则: + * 1. 检查文件是否被其他业务引用 + * 2. 从OSS删除物理文件 + * 3. 逻辑删除文件元数据 + * 4. 更新存储空间统计 + * + * @param fileId 文件ID + */ + void deleteFile(String fileId); + + /** + * 批量删除文件 + * 业务规则: + * 1. 验证所有文件可以删除 + * 2. 批量从OSS删除 + * 3. 批量更新元数据 + * + * @param fileIds 文件ID列表 + */ + void batchDeleteFiles(List fileIds); + + /** + * 获取文件信息 + * + * @param fileId 文件ID + * @return 文件信息领域模型 + */ + FileInfo getFileInfo(String fileId); + + /** + * 根据MD5查询文件(用于文件去重) + * 业务规则: + * 1. 如果存在相同MD5的文件,可以直接引用 + * 2. 避免重复上传相同文件 + * + * @param md5 文件MD5值 + * @return 文件信息领域模型 + */ + FileInfo getFileByMd5(String md5); + + /** + * 根据业务ID查询文件列表 + * + * @param bizId 业务ID + * @return 文件信息列表 + */ + List getFilesByBizId(String bizId); + + /** + * 根据业务类型和业务ID查询文件列表 + * + * @param bizType 业务类型 + * @param bizId 业务ID + * @return 文件信息列表 + */ + List getFilesByBizTypeAndBizId(String bizType, String bizId); + + /** + * 生成文件访问URL + * 业务规则: + * 1. 生成带签名的临时URL + * 2. 设置URL有效期 + * 3. 记录访问日志 + * + * @param fileId 文件ID + * @param expireSeconds URL有效期(秒) + * @return 文件访问URL + */ + String generateFileUrl(String fileId, Long expireSeconds); + + /** + * 复制文件 + * 业务规则: + * 1. 验证源文件存在 + * 2. 在OSS中复制文件(或直接引用) + * 3. 创建新的文件元数据记录 + * + * @param sourceFileId 源文件ID + * @param targetBizType 目标业务类型 + * @param targetBizId 目标业务ID + * @return 新文件信息 + */ + FileInfo copyFile(String sourceFileId, String targetBizType, String targetBizId); + + /** + * 移动文件(更改业务关联) + * 业务规则: + * 1. 验证源文件存在 + * 2. 更新文件的业务类型和业务ID + * 3. 不改变OSS中的物理位置 + * + * @param fileId 文件ID + * @param targetBizType 目标业务类型 + * @param targetBizId 目标业务ID + * @return 更新后的文件信息 + */ + FileInfo moveFile(String fileId, String targetBizType, String targetBizId); + + /** + * 验证文件类型是否允许 + * 业务规则: + * 1. 检查文件扩展名 + * 2. 检查MIME类型 + * 3. 根据业务类型限制文件类型 + * + * @param fileName 文件名 + * @param contentType 内容类型 + * @param bizType 业务类型 + * @return 是否允许 + */ + boolean isFileTypeAllowed(String fileName, String contentType, String bizType); + + /** + * 验证文件大小是否允许 + * 业务规则: + * 1. 检查文件大小是否超过限制 + * 2. 根据业务类型设置不同的大小限制 + * 3. 检查租户存储配额 + * + * @param fileSize 文件大小(字节) + * @param bizType 业务类型 + * @param tenantId 租户ID + * @return 是否允许 + */ + boolean isFileSizeAllowed(Long fileSize, String bizType, Long tenantId); + + /** + * 计算存储统计信息 + * 业务规则: + * 1. 统计文件总数 + * 2. 统计存储空间使用量 + * 3. 按文件类型分组统计 + * 4. 按业务类型分组统计 + * + * @param tenantId 租户ID(可选) + * @return 存储统计信息 + */ + StorageStatistics calculateStatistics(Long tenantId); + + /** + * 检查文件是否被引用 + * 业务规则: + * 1. 检查是否有其他业务引用此文件 + * 2. 被引用的文件不能删除 + * + * @param fileId 文件ID + * @return 是否被引用 + */ + boolean isFileReferenced(String fileId); + + /** + * 清理过期的临时文件 + * 业务规则: + * 1. 查找超过保留期的临时文件 + * 2. 批量删除过期文件 + * 3. 释放存储空间 + * + * @param retentionDays 保留天数 + * @return 清理的文件数量 + */ + int cleanExpiredTempFiles(int retentionDays); +} diff --git a/src/main/java/com/tuoheng/airport/storage/presentation/controller/StorageController.java b/src/main/java/com/tuoheng/airport/storage/presentation/controller/StorageController.java new file mode 100644 index 0000000..208974f --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/presentation/controller/StorageController.java @@ -0,0 +1,196 @@ +package com.tuoheng.airport.storage.presentation.controller; + +import com.tuoheng.airport.storage.application.dto.*; +import com.tuoheng.airport.storage.application.service.StorageApplicationService; +import com.tuoheng.airport.storage.presentation.converter.StorageVoConverter; +import com.tuoheng.airport.storage.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 org.springframework.web.multipart.MultipartFile; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.util.List; + +/** + * 存储管理控制器(Presentation层) + * 提供文件存储管理的 REST API 接口 + * 负责文件上传、下载、删除等操作 + * + * 职责: + * 1. 接收前端的文件上传请求 + * 2. 将 VO 转换为 DTO 传递给 Application 层 + * 3. 将 Application 层返回的 DTO 转换为 VO 返回给前端 + * 4. 不包含任何业务逻辑 + * + * @author tuoheng + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/storage") +@RequiredArgsConstructor +@Validated +@Tag(name = "存储管理", description = "文件存储管理相关接口") +public class StorageController { + + private final StorageApplicationService storageApplicationService; + + /** + * 上传文件 + * POST /api/v1/storage/upload + */ + @PostMapping("/upload") + @Operation(summary = "上传文件", description = "上传单个文件到存储服务") + public Result uploadFile( + @Parameter(description = "文件") @RequestParam("file") MultipartFile file, + @Parameter(description = "业务类型") @RequestParam(required = false) String bizType, + @Parameter(description = "业务ID") @RequestParam(required = false) String bizId) { + log.info("接收到文件上传请求,文件名: {}, 业务类型: {}, 业务ID: {}", file.getOriginalFilename(), bizType, bizId); + + FileUploadRequest request = FileUploadRequest.builder() + .file(file) + .bizType(bizType) + .bizId(bizId) + .build(); + + FileInfoResponse response = storageApplicationService.uploadFile(request); + FileInfoVO result = StorageVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 批量上传文件 + * POST /api/v1/storage/batch-upload + */ + @PostMapping("/batch-upload") + @Operation(summary = "批量上传文件", description = "批量上传多个文件到存储服务") + public Result> batchUploadFiles( + @Parameter(description = "文件列表") @RequestParam("files") MultipartFile[] files, + @Parameter(description = "业务类型") @RequestParam(required = false) String bizType, + @Parameter(description = "业务ID") @RequestParam(required = false) String bizId) { + log.info("接收到批量文件上传请求,文件数量: {}, 业务类型: {}, 业务ID: {}", files.length, bizType, bizId); + + BatchFileUploadRequest request = BatchFileUploadRequest.builder() + .files(files) + .bizType(bizType) + .bizId(bizId) + .build(); + + List responses = storageApplicationService.batchUploadFiles(request); + List results = StorageVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 下载文件 + * GET /api/v1/storage/download/{fileId} + */ + @GetMapping("/download/{fileId}") + @Operation(summary = "下载文件", description = "根据文件ID下载文件") + public void downloadFile( + @Parameter(description = "文件ID") @PathVariable String fileId) { + log.info("接收到文件下载请求,文件ID: {}", fileId); + storageApplicationService.downloadFile(fileId); + } + + /** + * 获取文件访问URL + * GET /api/v1/storage/url/{fileId} + */ + @GetMapping("/url/{fileId}") + @Operation(summary = "获取文件访问URL", description = "获取文件的临时访问URL") + public Result getFileUrl( + @Parameter(description = "文件ID") @PathVariable String fileId, + @Parameter(description = "URL有效期(秒)") @RequestParam(defaultValue = "3600") Long expireSeconds) { + log.info("接收到获取文件URL请求,文件ID: {}, 有效期: {}秒", fileId, expireSeconds); + String url = storageApplicationService.getFileUrl(fileId, expireSeconds); + return Result.success(url); + } + + /** + * 查询文件信息 + * GET /api/v1/storage/files/{fileId} + */ + @GetMapping("/files/{fileId}") + @Operation(summary = "查询文件信息", description = "根据文件ID查询文件详细信息") + public Result getFileInfo( + @Parameter(description = "文件ID") @PathVariable String fileId) { + log.info("接收到查询文件信息请求,文件ID: {}", fileId); + FileInfoResponse response = storageApplicationService.getFileInfo(fileId); + FileInfoVO result = StorageVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询文件列表 + * GET /api/v1/storage/files + */ + @GetMapping("/files") + @Operation(summary = "查询文件列表", description = "根据条件查询文件列表") + public Result> queryFiles(FileQueryVO vo) { + log.info("接收到查询文件列表请求,查询条件: {}", vo); + FileQueryRequest request = StorageVoConverter.toQueryRequest(vo); + List responses = storageApplicationService.queryFiles(request); + List results = StorageVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 根据业务ID查询文件列表 + * GET /api/v1/storage/files/biz/{bizId} + */ + @GetMapping("/files/biz/{bizId}") + @Operation(summary = "根据业务ID查询文件", description = "查询指定业务ID关联的所有文件") + public Result> getFilesByBizId( + @Parameter(description = "业务ID") @PathVariable String bizId) { + log.info("接收到根据业务ID查询文件请求,业务ID: {}", bizId); + List responses = storageApplicationService.getFilesByBizId(bizId); + List results = StorageVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 删除文件 + * DELETE /api/v1/storage/files/{fileId} + */ + @DeleteMapping("/files/{fileId}") + @Operation(summary = "删除文件", description = "删除指定的文件") + public Result deleteFile( + @Parameter(description = "文件ID") @PathVariable String fileId) { + log.info("接收到删除文件请求,文件ID: {}", fileId); + storageApplicationService.deleteFile(fileId); + return Result.success(); + } + + /** + * 批量删除文件 + * POST /api/v1/storage/files/batch-delete + */ + @PostMapping("/files/batch-delete") + @Operation(summary = "批量删除文件", description = "批量删除多个文件") + public Result batchDeleteFiles(@RequestBody List fileIds) { + log.info("接收到批量删除文件请求,文件数量: {}", fileIds.size()); + storageApplicationService.batchDeleteFiles(fileIds); + return Result.success(); + } + + /** + * 查询存储统计信息 + * GET /api/v1/storage/statistics + */ + @GetMapping("/statistics") + @Operation(summary = "查询存储统计", description = "查询存储空间使用统计信息") + public Result getStorageStatistics( + @Parameter(description = "租户ID") @RequestParam(required = false) Long tenantId) { + log.info("接收到查询存储统计请求,租户ID: {}", tenantId); + StorageStatisticsResponse response = storageApplicationService.getStorageStatistics(tenantId); + StorageStatisticsVO result = StorageVoConverter.toStatisticsVO(response); + return Result.success(result); + } +} diff --git a/src/main/java/com/tuoheng/airport/storage/presentation/converter/StorageVoConverter.java b/src/main/java/com/tuoheng/airport/storage/presentation/converter/StorageVoConverter.java new file mode 100644 index 0000000..81891dc --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/presentation/converter/StorageVoConverter.java @@ -0,0 +1,28 @@ +package com.tuoheng.airport.storage.presentation.converter; + +import com.tuoheng.airport.storage.application.dto.*; +import com.tuoheng.airport.storage.presentation.vo.*; +import java.util.List; +import java.util.stream.Collectors; + +public class StorageVoConverter { + + public static FileQueryRequest toQueryRequest(FileQueryVO vo) { + // TODO: 实现转换逻辑 + return new FileQueryRequest(); + } + + public static FileInfoVO toVO(FileInfoResponse response) { + // TODO: 实现转换逻辑 + return new FileInfoVO(); + } + + public static List toVOList(List responses) { + return responses.stream().map(StorageVoConverter::toVO).collect(Collectors.toList()); + } + + public static StorageStatisticsVO toStatisticsVO(StorageStatisticsResponse response) { + // TODO: 实现转换逻辑 + return new StorageStatisticsVO(); + } +} diff --git a/src/main/java/com/tuoheng/airport/storage/presentation/vo/FileInfoVO.java b/src/main/java/com/tuoheng/airport/storage/presentation/vo/FileInfoVO.java new file mode 100644 index 0000000..415a40a --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/presentation/vo/FileInfoVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.storage.presentation.vo; + +import lombok.Data; + +@Data +public class FileInfoVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/storage/presentation/vo/FileQueryVO.java b/src/main/java/com/tuoheng/airport/storage/presentation/vo/FileQueryVO.java new file mode 100644 index 0000000..98356bd --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/presentation/vo/FileQueryVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.storage.presentation.vo; + +import lombok.Data; + +@Data +public class FileQueryVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/storage/presentation/vo/StorageStatisticsVO.java b/src/main/java/com/tuoheng/airport/storage/presentation/vo/StorageStatisticsVO.java new file mode 100644 index 0000000..edc27c7 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/storage/presentation/vo/StorageStatisticsVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.storage.presentation.vo; + +import lombok.Data; + +@Data +public class StorageStatisticsVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/task/application/dto/BatchOperationResultResponse.java b/src/main/java/com/tuoheng/airport/task/application/dto/BatchOperationResultResponse.java new file mode 100644 index 0000000..21d6f14 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/dto/BatchOperationResultResponse.java @@ -0,0 +1,45 @@ +package com.tuoheng.airport.task.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 批量操作结果响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BatchOperationResultResponse { + + /** + * 总数 + */ + private Integer total; + + /** + * 成功数量 + */ + private Integer successCount; + + /** + * 失败数量 + */ + private Integer failureCount; + + /** + * 失败的任务ID列表 + */ + private List failedTaskIds; + + /** + * 失败原因列表 + */ + private List failureReasons; +} diff --git a/src/main/java/com/tuoheng/airport/task/application/dto/TaskCreateRequest.java b/src/main/java/com/tuoheng/airport/task/application/dto/TaskCreateRequest.java new file mode 100644 index 0000000..6dc71ef --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/dto/TaskCreateRequest.java @@ -0,0 +1,48 @@ +package com.tuoheng.airport.task.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务创建请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskCreateRequest { + + /** + * 任务编码 + */ + private String taskCode; + + /** + * 任务名称 + */ + private String taskName; + + /** + * 描述 + */ + private String description; + + /** + * 航线ID + */ + private Long airlineId; + + /** + * 设备ID + */ + private Long deviceId; + + /** + * 调度策略 + */ + private TaskScheduleRequest schedule; +} diff --git a/src/main/java/com/tuoheng/airport/task/application/dto/TaskExecutionCalendarResponse.java b/src/main/java/com/tuoheng/airport/task/application/dto/TaskExecutionCalendarResponse.java new file mode 100644 index 0000000..eecab7b --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/dto/TaskExecutionCalendarResponse.java @@ -0,0 +1,45 @@ +package com.tuoheng.airport.task.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 任务执行日历响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskExecutionCalendarResponse { + + /** + * 日期 + */ + private String date; + + /** + * 计划执行时间 + */ + private LocalDateTime planExecutionTime; + + /** + * 是否已执行 + */ + private Boolean executed; + + /** + * 执行结果(成功/失败) + */ + private String executionResult; + + /** + * 飞行记录ID + */ + private Long flightRecordId; +} diff --git a/src/main/java/com/tuoheng/airport/task/application/dto/TaskQueryRequest.java b/src/main/java/com/tuoheng/airport/task/application/dto/TaskQueryRequest.java new file mode 100644 index 0000000..e59b94d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/dto/TaskQueryRequest.java @@ -0,0 +1,48 @@ +package com.tuoheng.airport.task.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务查询请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskQueryRequest { + + /** + * 任务名称(模糊查询) + */ + private String taskName; + + /** + * 任务状态 + */ + private String status; + + /** + * 航线ID + */ + private Long airlineId; + + /** + * 设备ID + */ + private Long deviceId; + + /** + * 创建人ID + */ + private Long creatorId; + + /** + * 租户ID + */ + private Long tenantId; +} diff --git a/src/main/java/com/tuoheng/airport/task/application/dto/TaskResponse.java b/src/main/java/com/tuoheng/airport/task/application/dto/TaskResponse.java new file mode 100644 index 0000000..d2618a5 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/dto/TaskResponse.java @@ -0,0 +1,38 @@ +package com.tuoheng.airport.task.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 任务响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskResponse { + + private Long id; + private String taskCode; + private String taskName; + private String description; + private String status; + private Long airlineId; + private Long deviceId; + private TaskScheduleResponse schedule; + private LocalDateTime nextExecutionTime; + private LocalDateTime lastExecutionTime; + private Integer executionCount; + private Integer successCount; + private Integer failureCount; + private Long tenantId; + private Long creatorId; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/tuoheng/airport/task/application/dto/TaskScheduleRequest.java b/src/main/java/com/tuoheng/airport/task/application/dto/TaskScheduleRequest.java new file mode 100644 index 0000000..048ccce --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/dto/TaskScheduleRequest.java @@ -0,0 +1,53 @@ +package com.tuoheng.airport.task.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务调度策略请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskScheduleRequest { + + /** + * 调度类型:ONCE(单次), DAILY(每天), WEEKLY(每周), MONTHLY(每月), CRON(Cron表达式) + */ + private String scheduleType; + + /** + * 执行时间(时:分,如 "09:00") + */ + private String executeTime; + + /** + * 星期几(1-7,用于每周任务) + */ + private Integer dayOfWeek; + + /** + * 日期(1-31,用于每月任务) + */ + private Integer dayOfMonth; + + /** + * Cron表达式(用于复杂调度) + */ + private String cronExpression; + + /** + * 开始日期 + */ + private String startDate; + + /** + * 结束日期 + */ + private String endDate; +} diff --git a/src/main/java/com/tuoheng/airport/task/application/dto/TaskScheduleResponse.java b/src/main/java/com/tuoheng/airport/task/application/dto/TaskScheduleResponse.java new file mode 100644 index 0000000..98e7bd6 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/dto/TaskScheduleResponse.java @@ -0,0 +1,26 @@ +package com.tuoheng.airport.task.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务调度策略响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskScheduleResponse { + + private String scheduleType; + private String executeTime; + private Integer dayOfWeek; + private Integer dayOfMonth; + private String cronExpression; + private String startDate; + private String endDate; +} diff --git a/src/main/java/com/tuoheng/airport/task/application/dto/TaskStatisticsResponse.java b/src/main/java/com/tuoheng/airport/task/application/dto/TaskStatisticsResponse.java new file mode 100644 index 0000000..ecce824 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/dto/TaskStatisticsResponse.java @@ -0,0 +1,27 @@ +package com.tuoheng.airport.task.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务统计响应 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskStatisticsResponse { + + private Integer totalExecutions; + private Integer successCount; + private Integer failureCount; + private Long totalDuration; + private Double totalDistance; + private Long averageDuration; + private Double successRate; + private Double failureRate; +} diff --git a/src/main/java/com/tuoheng/airport/task/application/dto/TaskUpdateRequest.java b/src/main/java/com/tuoheng/airport/task/application/dto/TaskUpdateRequest.java new file mode 100644 index 0000000..86f174c --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/dto/TaskUpdateRequest.java @@ -0,0 +1,48 @@ +package com.tuoheng.airport.task.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务更新请求 DTO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskUpdateRequest { + + /** + * 任务ID + */ + private Long id; + + /** + * 任务名称 + */ + private String taskName; + + /** + * 描述 + */ + private String description; + + /** + * 航线ID + */ + private Long airlineId; + + /** + * 设备ID + */ + private Long deviceId; + + /** + * 调度策略 + */ + private TaskScheduleRequest schedule; +} diff --git a/src/main/java/com/tuoheng/airport/task/application/service/TaskApplicationService.java b/src/main/java/com/tuoheng/airport/task/application/service/TaskApplicationService.java new file mode 100644 index 0000000..d592e25 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/application/service/TaskApplicationService.java @@ -0,0 +1,289 @@ +package com.tuoheng.airport.task.application.service; + +import com.tuoheng.airport.task.application.dto.*; +import com.tuoheng.airport.fms.application.dto.FlightRecordResponse; + +import java.util.List; + +/** + * 任务应用服务接口(Application层) + * 定义多次飞行任务相关的用例(Use Cases) + * 协调领域模型完成任务调度、计划管理操作 + * + * @author tuoheng + */ +public interface TaskApplicationService { + + /** + * 创建飞行任务 + * 业务逻辑: + * 1. 验证任务名称唯一性 + * 2. 验证航线有效性 + * 3. 验证设备可用性 + * 4. 验证调度策略合法性 + * 5. 创建任务记录 + * 6. 初始化任务状态为待执行 + * + * @param request 创建请求 + * @return 任务响应 + */ + TaskResponse createTask(TaskCreateRequest request); + + /** + * 更新任务信息 + * 业务逻辑: + * 1. 验证任务存在 + * 2. 检查任务状态(执行中的任务不能修改) + * 3. 更新任务基本信息 + * 4. 如果修改了调度策略,重新计算下次执行时间 + * + * @param request 更新请求 + * @return 任务响应 + */ + TaskResponse updateTask(TaskUpdateRequest request); + + /** + * 根据ID查询任务 + * + * @param id 任务ID + * @return 任务响应 + */ + TaskResponse getTaskById(Long id); + + /** + * 根据条件查询任务列表 + * + * @param request 查询请求 + * @return 任务列表 + */ + List queryTasks(TaskQueryRequest request); + + /** + * 删除任务 + * 业务逻辑: + * 1. 验证任务存在 + * 2. 检查任务状态(执行中的任务不能删除) + * 3. 停止任务调度 + * 4. 逻辑删除任务 + * + * @param id 任务ID + */ + void deleteTask(Long id); + + /** + * 启动任务调度 + * 业务逻辑: + * 1. 验证任务配置完整性(航线、设备、调度策略) + * 2. 检查设备可用性 + * 3. 创建调度计划 + * 4. 启动定时调度器 + * 5. 更新任务状态为执行中 + * 6. 计算下次执行时间 + * + * @param id 任务ID + * @return 任务响应 + */ + TaskResponse startTask(Long id); + + /** + * 暂停任务调度 + * 业务逻辑: + * 1. 验证任务状态为执行中 + * 2. 停止定时调度器 + * 3. 不影响正在执行的飞行 + * 4. 更新任务状态为已暂停 + * + * @param id 任务ID + * @return 任务响应 + */ + TaskResponse pauseTask(Long id); + + /** + * 恢复任务调度 + * 业务逻辑: + * 1. 验证任务状态为已暂停 + * 2. 重新启动定时调度器 + * 3. 更新任务状态为执行中 + * 4. 重新计算下次执行时间 + * + * @param id 任务ID + * @return 任务响应 + */ + TaskResponse resumeTask(Long id); + + /** + * 停止任务调度 + * 业务逻辑: + * 1. 验证任务状态为执行中或已暂停 + * 2. 停止定时调度器 + * 3. 取消所有待执行的飞行 + * 4. 尝试终止正在执行的飞行 + * 5. 更新任务状态为已停止 + * + * @param id 任务ID + * @return 任务响应 + */ + TaskResponse stopTask(Long id); + + /** + * 立即执行任务 + * 业务逻辑: + * 1. 验证任务存在 + * 2. 检查设备可用性 + * 3. 调用 FMS 执行单次飞行 + * 4. 记录执行结果 + * 5. 不影响任务的定时调度 + * + * @param id 任务ID + * @return 飞行记录响应 + */ + FlightRecordResponse executeTaskNow(Long id); + + /** + * 查询任务的执行记录列表 + * 业务逻辑: + * 1. 查询任务的所有飞行记录 + * 2. 按执行时间倒序排序 + * 3. 支持分页 + * + * @param taskId 任务ID + * @param pageNum 页码 + * @param pageSize 每页数量 + * @return 飞行记录列表 + */ + List getTaskRecords(Long taskId, Integer pageNum, Integer pageSize); + + /** + * 查询任务的最新执行记录 + * + * @param taskId 任务ID + * @return 飞行记录响应 + */ + FlightRecordResponse getLatestRecord(Long taskId); + + /** + * 查询任务统计信息 + * 业务逻辑: + * 1. 统计总执行次数 + * 2. 统计成功/失败次数 + * 3. 计算成功率 + * 4. 统计总飞行时长 + * 5. 计算平均飞行时长 + * + * @param taskId 任务ID + * @return 任务统计信息 + */ + TaskStatisticsResponse getTaskStatistics(Long taskId); + + /** + * 批量启动任务 + * 业务逻辑: + * 1. 验证所有任务的状态和配置 + * 2. 批量启动定时调度器 + * 3. 返回成功和失败的统计 + * + * @param taskIds 任务ID列表 + * @return 批量操作结果 + */ + BatchOperationResultResponse batchStartTasks(List taskIds); + + /** + * 批量暂停任务 + * 业务逻辑: + * 1. 验证所有任务的状态 + * 2. 批量停止定时调度器 + * 3. 返回成功和失败的统计 + * + * @param taskIds 任务ID列表 + * @return 批量操作结果 + */ + BatchOperationResultResponse batchPauseTasks(List taskIds); + + /** + * 批量删除任务 + * 业务逻辑: + * 1. 验证所有任务的状态 + * 2. 批量停止调度器 + * 3. 批量删除任务 + * 4. 返回成功和失败的统计 + * + * @param taskIds 任务ID列表 + * @return 批量操作结果 + */ + BatchOperationResultResponse batchDeleteTasks(List taskIds); + + /** + * 查询正在执行的任务列表 + * 业务逻辑: + * 1. 查询状态为执行中的任务 + * 2. 返回任务列表 + * + * @return 任务列表 + */ + List getActiveTasks(); + + /** + * 查询任务的下次执行时间 + * 业务逻辑: + * 1. 根据调度策略计算下次执行时间 + * 2. 考虑任务的执行状态 + * + * @param taskId 任务ID + * @return 下次执行时间 + */ + String getNextExecutionTime(Long taskId); + + /** + * 复制任务 + * 业务逻辑: + * 1. 验证源任务存在 + * 2. 复制任务基本信息 + * 3. 新任务状态为待执行 + * 4. 新任务名称不能重复 + * + * @param taskId 源任务ID + * @param newName 新任务名称 + * @return 新任务响应 + */ + TaskResponse copyTask(Long taskId, String newName); + + /** + * 查询任务执行日历 + * 业务逻辑: + * 1. 根据调度策略计算指定月份的所有执行时间 + * 2. 标记已执行和未执行的日期 + * 3. 返回日历数据 + * + * @param taskId 任务ID + * @param year 年份 + * @param month 月份 + * @return 任务执行日历列表 + */ + List getTaskExecutionCalendar(Long taskId, Integer year, Integer month); + + /** + * 任务调度触发(由调度器调用) + * 业务逻辑: + * 1. 检查任务是否应该执行 + * 2. 检查设备可用性 + * 3. 调用 FMS 执行单次飞行 + * 4. 记录执行结果 + * 5. 计算下次执行时间 + * + * @param taskId 任务ID + */ + void triggerTaskExecution(Long taskId); + + /** + * 处理任务执行结果(由 FMS 回调) + * 业务逻辑: + * 1. 更新任务的执行统计 + * 2. 如果是单次任务且执行成功,停止任务 + * 3. 如果执行失败,根据重试策略决定是否重试 + * + * @param taskId 任务ID + * @param flightRecordId 飞行记录ID + * @param success 是否成功 + */ + void handleTaskExecutionResult(Long taskId, Long flightRecordId, boolean success); +} diff --git a/src/main/java/com/tuoheng/airport/task/domain/model/Task.java b/src/main/java/com/tuoheng/airport/task/domain/model/Task.java new file mode 100644 index 0000000..93a5114 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/domain/model/Task.java @@ -0,0 +1,126 @@ +package com.tuoheng.airport.task.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 任务领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Task { + + private Long id; + private String taskCode; + private String taskName; + private String description; + private TaskStatus status; + private Long airlineId; + private Long deviceId; + private TaskSchedule schedule; + private LocalDateTime nextExecutionTime; + private LocalDateTime lastExecutionTime; + private Integer executionCount; + private Integer successCount; + private Integer failureCount; + private Long tenantId; + private Long creatorId; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Boolean deleted; + + // ==================== 业务方法 ==================== + + public void start() { + if (this.status != TaskStatus.PENDING && this.status != TaskStatus.PAUSED) { + throw new IllegalStateException("只有待执行或已暂停的任务可以启动"); + } + this.status = TaskStatus.RUNNING; + this.updateTime = LocalDateTime.now(); + } + + public void pause() { + if (this.status != TaskStatus.RUNNING) { + throw new IllegalStateException("只有执行中的任务可以暂停"); + } + this.status = TaskStatus.PAUSED; + this.updateTime = LocalDateTime.now(); + } + + public void resume() { + if (this.status != TaskStatus.PAUSED) { + throw new IllegalStateException("只有已暂停的任务可以恢复"); + } + this.status = TaskStatus.RUNNING; + this.updateTime = LocalDateTime.now(); + } + + public void stop() { + if (this.status != TaskStatus.RUNNING && this.status != TaskStatus.PAUSED) { + throw new IllegalStateException("只有执行中或已暂停的任务可以停止"); + } + this.status = TaskStatus.STOPPED; + this.updateTime = LocalDateTime.now(); + } + + public void recordExecution(boolean success, LocalDateTime nextExecutionTime) { + this.executionCount = (this.executionCount == null ? 0 : this.executionCount) + 1; + if (success) { + this.successCount = (this.successCount == null ? 0 : this.successCount) + 1; + } else { + this.failureCount = (this.failureCount == null ? 0 : this.failureCount) + 1; + } + this.lastExecutionTime = LocalDateTime.now(); + this.nextExecutionTime = nextExecutionTime; + this.updateTime = LocalDateTime.now(); + } + + public boolean isRunning() { + return this.status == TaskStatus.RUNNING; + } + + public boolean canModify() { + return this.status != TaskStatus.RUNNING; + } + + public boolean canDelete() { + return this.status != TaskStatus.RUNNING; + } + + public void delete() { + if (!canDelete()) { + throw new IllegalStateException("执行中的任务不能删除"); + } + this.deleted = true; + this.updateTime = LocalDateTime.now(); + } + + public static Task create(String taskCode, String taskName, Long airlineId, Long deviceId, + TaskSchedule schedule, Long tenantId, Long creatorId) { + LocalDateTime now = LocalDateTime.now(); + return Task.builder() + .taskCode(taskCode) + .taskName(taskName) + .status(TaskStatus.PENDING) + .airlineId(airlineId) + .deviceId(deviceId) + .schedule(schedule) + .executionCount(0) + .successCount(0) + .failureCount(0) + .tenantId(tenantId) + .creatorId(creatorId) + .createTime(now) + .updateTime(now) + .deleted(false) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/task/domain/model/TaskSchedule.java b/src/main/java/com/tuoheng/airport/task/domain/model/TaskSchedule.java new file mode 100644 index 0000000..2e41e24 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/domain/model/TaskSchedule.java @@ -0,0 +1,111 @@ +package com.tuoheng.airport.task.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务调度策略领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskSchedule { + + /** + * 调度类型:ONCE(单次), DAILY(每天), WEEKLY(每周), MONTHLY(每月), CRON(Cron表达式) + */ + private String scheduleType; + + /** + * 执行时间(时:分,如 "09:00") + */ + private String executeTime; + + /** + * 星期几(1-7,用于每周任务) + */ + private Integer dayOfWeek; + + /** + * 日期(1-31,用于每月任务) + */ + private Integer dayOfMonth; + + /** + * Cron表达式(用于复杂调度) + */ + private String cronExpression; + + /** + * 开始日期 + */ + private String startDate; + + /** + * 结束日期 + */ + private String endDate; + + // ==================== 业务方法 ==================== + + public boolean isValid() { + if (scheduleType == null) { + return false; + } + switch (scheduleType) { + case "ONCE": + return true; + case "DAILY": + return executeTime != null; + case "WEEKLY": + return executeTime != null && dayOfWeek != null && dayOfWeek >= 1 && dayOfWeek <= 7; + case "MONTHLY": + return executeTime != null && dayOfMonth != null && dayOfMonth >= 1 && dayOfMonth <= 31; + case "CRON": + return cronExpression != null && !cronExpression.trim().isEmpty(); + default: + return false; + } + } + + public static TaskSchedule createOnce() { + return TaskSchedule.builder() + .scheduleType("ONCE") + .build(); + } + + public static TaskSchedule createDaily(String executeTime) { + return TaskSchedule.builder() + .scheduleType("DAILY") + .executeTime(executeTime) + .build(); + } + + public static TaskSchedule createWeekly(String executeTime, Integer dayOfWeek) { + return TaskSchedule.builder() + .scheduleType("WEEKLY") + .executeTime(executeTime) + .dayOfWeek(dayOfWeek) + .build(); + } + + public static TaskSchedule createMonthly(String executeTime, Integer dayOfMonth) { + return TaskSchedule.builder() + .scheduleType("MONTHLY") + .executeTime(executeTime) + .dayOfMonth(dayOfMonth) + .build(); + } + + public static TaskSchedule createCron(String cronExpression) { + return TaskSchedule.builder() + .scheduleType("CRON") + .cronExpression(cronExpression) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/task/domain/model/TaskStatistics.java b/src/main/java/com/tuoheng/airport/task/domain/model/TaskStatistics.java new file mode 100644 index 0000000..8b385db --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/domain/model/TaskStatistics.java @@ -0,0 +1,73 @@ +package com.tuoheng.airport.task.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务统计领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskStatistics { + + /** + * 总执行次数 + */ + private Integer totalExecutions; + + /** + * 成功次数 + */ + private Integer successCount; + + /** + * 失败次数 + */ + private Integer failureCount; + + /** + * 总飞行时长(秒) + */ + private Long totalDuration; + + /** + * 总飞行距离(米) + */ + private Double totalDistance; + + /** + * 平均飞行时长(秒) + */ + public Long getAverageDuration() { + if (successCount == null || successCount == 0) { + return 0L; + } + return totalDuration / successCount; + } + + /** + * 成功率 + */ + public double getSuccessRate() { + if (totalExecutions == null || totalExecutions == 0) { + return 0.0; + } + return (double) successCount / totalExecutions * 100; + } + + /** + * 失败率 + */ + public double getFailureRate() { + if (totalExecutions == null || totalExecutions == 0) { + return 0.0; + } + return (double) failureCount / totalExecutions * 100; + } +} diff --git a/src/main/java/com/tuoheng/airport/task/domain/model/TaskStatus.java b/src/main/java/com/tuoheng/airport/task/domain/model/TaskStatus.java new file mode 100644 index 0000000..8ebded8 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/domain/model/TaskStatus.java @@ -0,0 +1,25 @@ +package com.tuoheng.airport.task.domain.model; + +/** + * 任务状态枚举 + * + * @author tuoheng + */ +public enum TaskStatus { + + PENDING("待执行"), + RUNNING("执行中"), + PAUSED("已暂停"), + STOPPED("已停止"), + COMPLETED("已完成"); + + private final String description; + + TaskStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/tuoheng/airport/task/domain/repository/TaskRepository.java b/src/main/java/com/tuoheng/airport/task/domain/repository/TaskRepository.java new file mode 100644 index 0000000..2b8a739 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/domain/repository/TaskRepository.java @@ -0,0 +1,41 @@ +package com.tuoheng.airport.task.domain.repository; + +import com.tuoheng.airport.task.domain.model.Task; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * 任务仓储接口 + * + * @author tuoheng + */ +public interface TaskRepository { + + Task save(Task task); + + Optional findById(Long id); + + Optional findByTaskCode(String taskCode); + + List findByStatus(String status); + + List findByAirlineId(Long airlineId); + + List findByDeviceId(Long deviceId); + + List findByCreatorId(Long creatorId); + + List findByTenantId(Long tenantId); + + List findActiveTasks(); + + List findUpcomingTasks(LocalDateTime startTime, LocalDateTime endTime); + + void delete(Long id); + + boolean existsByTaskName(String taskName, Long tenantId); + + long countByStatus(String status); +} diff --git a/src/main/java/com/tuoheng/airport/task/domain/service/TaskDomainService.java b/src/main/java/com/tuoheng/airport/task/domain/service/TaskDomainService.java new file mode 100644 index 0000000..27e8963 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/domain/service/TaskDomainService.java @@ -0,0 +1,337 @@ +package com.tuoheng.airport.task.domain.service; + +import com.tuoheng.airport.task.domain.model.Task; +import com.tuoheng.airport.task.domain.model.TaskSchedule; +import com.tuoheng.airport.task.domain.model.TaskStatistics; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 任务领域服务接口(Domain层) + * 封装任务调度相关的业务逻辑和业务规则 + * + * Domain Service 的职责: + * 1. 封装任务调度的核心业务规则 + * 2. 协调任务、航线、设备的关系 + * 3. 保证任务数据的一致性 + * 4. 不包含事务管理(事务由 Application 层管理) + * + * @author tuoheng + */ +public interface TaskDomainService { + + /** + * 创建任务 + * 业务规则: + * 1. 任务名称不能重复(同一租户下) + * 2. 必须指定有效的航线 + * 3. 必须指定调度策略(单次/周期) + * 4. 周期任务必须指定调度时间 + * 5. 新任务默认为待执行状态 + * + * @param task 任务领域模型 + * @return 创建后的任务 + */ + Task createTask(Task task); + + /** + * 更新任务信息 + * 业务规则: + * 1. 执行中的任务不能修改 + * 2. 不能修改任务编码 + * 3. 修改调度策略后需要重新计算下次执行时间 + * + * @param task 任务领域模型 + * @return 更新后的任务 + */ + Task updateTask(Task task); + + /** + * 删除任务 + * 业务规则: + * 1. 执行中的任务不能删除 + * 2. 停止任务调度 + * 3. 逻辑删除任务 + * 4. 不删除历史执行记录 + * + * @param taskId 任务ID + */ + void deleteTask(Long taskId); + + /** + * 验证任务可以启动 + * 业务规则: + * 1. 任务状态必须是待执行或已暂停 + * 2. 航线必须已审核通过 + * 3. 指定的设备必须可用 + * 4. 调度时间配置必须有效 + * + * @param taskId 任务ID + * @return 是否可以启动 + */ + boolean canStartTask(Long taskId); + + /** + * 启动任务调度 + * 业务规则: + * 1. 验证任务可以启动 + * 2. 创建调度计划 + * 3. 更新任务状态为执行中 + * 4. 计算下次执行时间 + * 5. 记录启动时间 + * + * @param taskId 任务ID + * @return 更新后的任务 + */ + Task startTask(Long taskId); + + /** + * 暂停任务调度 + * 业务规则: + * 1. 只有执行中的任务可以暂停 + * 2. 暂停不影响正在执行的飞行 + * 3. 更新任务状态为已暂停 + * 4. 记录暂停时间 + * + * @param taskId 任务ID + * @return 更新后的任务 + */ + Task pauseTask(Long taskId); + + /** + * 恢复任务调度 + * 业务规则: + * 1. 只有已暂停的任务可以恢复 + * 2. 更新任务状态为执行中 + * 3. 重新计算下次执行时间 + * 4. 记录恢复时间 + * + * @param taskId 任务ID + * @return 更新后的任务 + */ + Task resumeTask(Long taskId); + + /** + * 停止任务调度 + * 业务规则: + * 1. 取消所有待执行的飞行 + * 2. 尝试终止正在执行的飞行 + * 3. 更新任务状态为已停止 + * 4. 记录停止时间 + * + * @param taskId 任务ID + * @return 更新后的任务 + */ + Task stopTask(Long taskId); + + /** + * 计算下次执行时间 + * 根据调度策略计算任务的下次执行时间 + * 业务规则: + * 1. 单次任务:立即执行 + * 2. 每天任务:每天指定时间执行 + * 3. 每周任务:每周指定星期的指定时间执行 + * 4. 每月任务:每月指定日期的指定时间执行 + * 5. Cron表达式:按Cron规则执行 + * + * @param taskId 任务ID + * @return 下次执行时间 + */ + LocalDateTime calculateNextExecutionTime(Long taskId); + + /** + * 检查任务是否应该执行 + * 业务规则: + * 1. 任务状态为执行中 + * 2. 当前时间满足调度策略 + * 3. 设备可用 + * 4. 没有正在执行的飞行(避免重复执行) + * + * @param taskId 任务ID + * @return 是否应该执行 + */ + boolean shouldExecuteTask(Long taskId); + + /** + * 记录任务执行结果 + * 业务规则: + * 1. 更新任务的执行统计 + * 2. 记录最后执行时间 + * 3. 如果是单次任务且执行成功,停止任务 + * 4. 如果执行失败,根据重试策略决定是否重试 + * + * @param taskId 任务ID + * @param flightRecordId 飞行记录ID + * @param success 是否成功 + */ + void recordTaskExecution(Long taskId, Long flightRecordId, boolean success); + + /** + * 查询任务详情 + * + * @param taskId 任务ID + * @return 任务领域模型 + */ + Task getTaskById(Long taskId); + + /** + * 查询任务统计信息 + * 业务规则: + * 1. 统计总执行次数 + * 2. 统计成功/失败次数 + * 3. 计算成功率 + * 4. 统计总飞行时长 + * 5. 计算平均飞行时长 + * + * @param taskId 任务ID + * @return 任务统计信息 + */ + TaskStatistics getTaskStatistics(Long taskId); + + /** + * 复制任务 + * 业务规则: + * 1. 复制任务基本信息 + * 2. 复制调度策略 + * 3. 新任务状态为待执行 + * 4. 新任务名称不能重复 + * + * @param sourceTaskId 源任务ID + * @param newName 新任务名称 + * @return 新任务 + */ + Task copyTask(Long sourceTaskId, String newName); + + /** + * 生成任务执行日历 + * 根据调度策略计算指定月份的所有执行时间 + * 业务规则: + * 1. 根据调度策略生成执行时间列表 + * 2. 标记已执行和未执行的日期 + * 3. 考虑任务的开始和结束时间 + * + * @param taskId 任务ID + * @param year 年份 + * @param month 月份 + * @return 执行时间列表 + */ + List generateExecutionCalendar(Long taskId, Integer year, Integer month); + + /** + * 检查任务名称是否存在 + * + * @param name 任务名称 + * @param tenantId 租户ID + * @return 是否存在 + */ + boolean isTaskNameExists(String name, Long tenantId); + + /** + * 检查任务是否可以删除 + * 业务规则: + * 1. 执行中的任务不能删除 + * + * @param taskId 任务ID + * @return 是否可以删除 + */ + boolean canDeleteTask(Long taskId); + + /** + * 检查任务是否可以修改 + * 业务规则: + * 1. 执行中的任务不能修改 + * + * @param taskId 任务ID + * @return 是否可以修改 + */ + boolean canUpdateTask(Long taskId); + + /** + * 验证调度策略合法性 + * 业务规则: + * 1. 单次任务:无需额外配置 + * 2. 每天任务:必须指定执行时间(时:分) + * 3. 每周任务:必须指定星期和执行时间 + * 4. 每月任务:必须指定日期和执行时间 + * 5. Cron表达式:必须是有效的Cron表达式 + * + * @param schedule 调度策略 + * @return 是否合法 + */ + boolean isScheduleValid(TaskSchedule schedule); + + /** + * 检查设备是否可用于任务 + * 业务规则: + * 1. 设备必须在线 + * 2. 设备必须是激活状态 + * 3. 设备必须已分配到机场 + * 4. 设备没有正在执行的飞行 + * + * @param taskId 任务ID + * @return 设备是否可用 + */ + boolean isDeviceAvailable(Long taskId); + + /** + * 检查航线是否可用于任务 + * 业务规则: + * 1. 航线必须已审核通过 + * 2. 航线没有被删除 + * + * @param taskId 任务ID + * @return 航线是否可用 + */ + boolean isAirlineAvailable(Long taskId); + + /** + * 检查任务是否超时未执行 + * 业务规则: + * 1. 执行中的任务超过预定时间30分钟未执行 + * 2. 发送告警通知 + * + * @param taskId 任务ID + * @return 是否超时 + */ + boolean isTaskExecutionTimeout(Long taskId); + + /** + * 处理任务执行失败 + * 业务规则: + * 1. 记录失败原因 + * 2. 更新失败次数 + * 3. 根据重试策略决定是否重试 + * 4. 连续失败3次后暂停任务 + * 5. 发送失败告警 + * + * @param taskId 任务ID + * @param reason 失败原因 + */ + void handleTaskExecutionFailure(Long taskId, String reason); + + /** + * 重置任务执行统计 + * 清空任务的执行统计数据 + * + * @param taskId 任务ID + */ + void resetTaskStatistics(Long taskId); + + /** + * 查询正在执行的任务列表 + * + * @return 任务列表 + */ + List getActiveTasks(); + + /** + * 查询即将执行的任务列表 + * 业务规则: + * 1. 查询未来1小时内应该执行的任务 + * 2. 用于预加载和准备 + * + * @return 任务列表 + */ + List getUpcomingTasks(); +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/controller/TaskController.java b/src/main/java/com/tuoheng/airport/task/presentation/controller/TaskController.java new file mode 100644 index 0000000..20d918f --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/controller/TaskController.java @@ -0,0 +1,325 @@ +package com.tuoheng.airport.task.presentation.controller; + +import com.tuoheng.airport.task.application.dto.*; +import com.tuoheng.airport.task.application.service.TaskApplicationService; +import com.tuoheng.airport.task.presentation.converter.TaskVoConverter; +import com.tuoheng.airport.task.presentation.vo.*; +import com.tuoheng.airport.fms.application.dto.FlightRecordResponse; +import com.tuoheng.airport.fms.presentation.vo.FlightRecordVO; +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 java.util.List; + +/** + * 任务管理控制器(Presentation层) + * 提供多次飞行任务管理的 REST API 接口 + * 负责任务调度、计划管理 + * + * 职责: + * 1. 接收前端的 VO 对象 + * 2. 将 VO 转换为 DTO 传递给 Application 层 + * 3. 将 Application 层返回的 DTO 转换为 VO 返回给前端 + * 4. 不包含任何业务逻辑 + * + * @author tuoheng + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/tasks") +@RequiredArgsConstructor +@Validated +@Tag(name = "任务管理", description = "多次飞行任务管理相关接口") +public class TaskController { + + private final TaskApplicationService taskApplicationService; + + /** + * 创建飞行任务 + * POST /api/v1/tasks + */ + @PostMapping + @Operation(summary = "创建飞行任务", description = "创建新的飞行任务") + public Result createTask(@Valid @RequestBody TaskCreateVO vo) { + log.info("接收到创建任务请求: {}", vo); + TaskCreateRequest request = TaskVoConverter.toCreateRequest(vo); + TaskResponse response = taskApplicationService.createTask(request); + TaskVO result = TaskVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 更新任务信息 + * PUT /api/v1/tasks/{id} + */ + @PutMapping("/{id}") + @Operation(summary = "更新任务信息", description = "更新指定任务的基本信息") + public Result updateTask( + @Parameter(description = "任务ID") @PathVariable Long id, + @Valid @RequestBody TaskUpdateVO vo) { + log.info("接收到更新任务请求,任务ID: {}, 请求参数: {}", id, vo); + vo.setId(id); + TaskUpdateRequest request = TaskVoConverter.toUpdateRequest(vo); + TaskResponse response = taskApplicationService.updateTask(request); + TaskVO result = TaskVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询任务详情 + * GET /api/v1/tasks/{id} + */ + @GetMapping("/{id}") + @Operation(summary = "查询任务详情", description = "根据任务ID查询任务详细信息") + public Result getTaskById( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到查询任务请求,任务ID: {}", id); + TaskResponse response = taskApplicationService.getTaskById(id); + TaskVO result = TaskVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询任务列表 + * GET /api/v1/tasks + */ + @GetMapping + @Operation(summary = "查询任务列表", description = "根据条件查询任务列表") + public Result> queryTasks(TaskQueryVO vo) { + log.info("接收到查询任务列表请求,查询条件: {}", vo); + TaskQueryRequest request = TaskVoConverter.toQueryRequest(vo); + List responses = taskApplicationService.queryTasks(request); + List results = TaskVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 删除任务 + * DELETE /api/v1/tasks/{id} + */ + @DeleteMapping("/{id}") + @Operation(summary = "删除任务", description = "删除指定的任务(逻辑删除)") + public Result deleteTask( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到删除任务请求,任务ID: {}", id); + taskApplicationService.deleteTask(id); + return Result.success(); + } + + /** + * 启动任务调度 + * POST /api/v1/tasks/{id}/start + */ + @PostMapping("/{id}/start") + @Operation(summary = "启动任务调度", description = "启动任务的定时调度") + public Result startTask( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到启动任务请求,任务ID: {}", id); + TaskResponse response = taskApplicationService.startTask(id); + TaskVO result = TaskVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 暂停任务调度 + * POST /api/v1/tasks/{id}/pause + */ + @PostMapping("/{id}/pause") + @Operation(summary = "暂停任务调度", description = "暂停任务的定时调度") + public Result pauseTask( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到暂停任务请求,任务ID: {}", id); + TaskResponse response = taskApplicationService.pauseTask(id); + TaskVO result = TaskVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 恢复任务调度 + * POST /api/v1/tasks/{id}/resume + */ + @PostMapping("/{id}/resume") + @Operation(summary = "恢复任务调度", description = "恢复已暂停的任务调度") + public Result resumeTask( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到恢复任务请求,任务ID: {}", id); + TaskResponse response = taskApplicationService.resumeTask(id); + TaskVO result = TaskVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 停止任务调度 + * POST /api/v1/tasks/{id}/stop + */ + @PostMapping("/{id}/stop") + @Operation(summary = "停止任务调度", description = "停止任务的定时调度") + public Result stopTask( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到停止任务请求,任务ID: {}", id); + TaskResponse response = taskApplicationService.stopTask(id); + TaskVO result = TaskVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 立即执行任务 + * POST /api/v1/tasks/{id}/execute-now + */ + @PostMapping("/{id}/execute-now") + @Operation(summary = "立即执行任务", description = "立即执行任务,不等待调度时间") + public Result executeTaskNow( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到立即执行任务请求,任务ID: {}", id); + FlightRecordResponse response = taskApplicationService.executeTaskNow(id); + FlightRecordVO result = TaskVoConverter.toFlightRecordVO(response); + return Result.success(result); + } + + /** + * 查询任务的执行记录列表 + * GET /api/v1/tasks/{id}/records + */ + @GetMapping("/{id}/records") + @Operation(summary = "查询任务执行记录", description = "查询任务的所有执行记录") + public Result> getTaskRecords( + @Parameter(description = "任务ID") @PathVariable Long id, + @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum, + @Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize) { + log.info("接收到查询任务执行记录请求,任务ID: {}, 页码: {}, 每页数量: {}", id, pageNum, pageSize); + List responses = taskApplicationService.getTaskRecords(id, pageNum, pageSize); + List results = TaskVoConverter.toFlightRecordVOList(responses); + return Result.success(results); + } + + /** + * 查询任务的最新执行记录 + * GET /api/v1/tasks/{id}/latest-record + */ + @GetMapping("/{id}/latest-record") + @Operation(summary = "查询最新执行记录", description = "查询任务的最新一次执行记录") + public Result getLatestRecord( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到查询任务最新执行记录请求,任务ID: {}", id); + FlightRecordResponse response = taskApplicationService.getLatestRecord(id); + FlightRecordVO result = TaskVoConverter.toFlightRecordVO(response); + return Result.success(result); + } + + /** + * 查询任务统计信息 + * GET /api/v1/tasks/{id}/statistics + */ + @GetMapping("/{id}/statistics") + @Operation(summary = "查询任务统计", description = "查询任务的统计信息") + public Result getTaskStatistics( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到查询任务统计请求,任务ID: {}", id); + TaskStatisticsResponse response = taskApplicationService.getTaskStatistics(id); + TaskStatisticsVO result = TaskVoConverter.toStatisticsVO(response); + return Result.success(result); + } + + /** + * 批量启动任务 + * POST /api/v1/tasks/batch/start + */ + @PostMapping("/batch/start") + @Operation(summary = "批量启动任务", description = "批量启动多个任务的调度") + public Result batchStartTasks(@RequestBody List taskIds) { + log.info("接收到批量启动任务请求,任务数量: {}", taskIds.size()); + BatchOperationResultResponse response = taskApplicationService.batchStartTasks(taskIds); + BatchOperationResultVO result = TaskVoConverter.toBatchResultVO(response); + return Result.success(result); + } + + /** + * 批量暂停任务 + * POST /api/v1/tasks/batch/pause + */ + @PostMapping("/batch/pause") + @Operation(summary = "批量暂停任务", description = "批量暂停多个任务的调度") + public Result batchPauseTasks(@RequestBody List taskIds) { + log.info("接收到批量暂停任务请求,任务数量: {}", taskIds.size()); + BatchOperationResultResponse response = taskApplicationService.batchPauseTasks(taskIds); + BatchOperationResultVO result = TaskVoConverter.toBatchResultVO(response); + return Result.success(result); + } + + /** + * 批量删除任务 + * POST /api/v1/tasks/batch/delete + */ + @PostMapping("/batch/delete") + @Operation(summary = "批量删除任务", description = "批量删除多个任务") + public Result batchDeleteTasks(@RequestBody List taskIds) { + log.info("接收到批量删除任务请求,任务数量: {}", taskIds.size()); + BatchOperationResultResponse response = taskApplicationService.batchDeleteTasks(taskIds); + BatchOperationResultVO result = TaskVoConverter.toBatchResultVO(response); + return Result.success(result); + } + + /** + * 查询正在执行的任务列表 + * GET /api/v1/tasks/active + */ + @GetMapping("/active") + @Operation(summary = "查询正在执行的任务", description = "查询所有正在执行的任务") + public Result> getActiveTasks() { + log.info("接收到查询正在执行的任务请求"); + List responses = taskApplicationService.getActiveTasks(); + List results = TaskVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 查询任务的下次执行时间 + * GET /api/v1/tasks/{id}/next-execution-time + */ + @GetMapping("/{id}/next-execution-time") + @Operation(summary = "查询下次执行时间", description = "查询任务的下次执行时间") + public Result getNextExecutionTime( + @Parameter(description = "任务ID") @PathVariable Long id) { + log.info("接收到查询任务下次执行时间请求,任务ID: {}", id); + String nextExecutionTime = taskApplicationService.getNextExecutionTime(id); + return Result.success(nextExecutionTime); + } + + /** + * 复制任务 + * POST /api/v1/tasks/{id}/copy + */ + @PostMapping("/{id}/copy") + @Operation(summary = "复制任务", description = "复制现有任务创建新任务") + public Result copyTask( + @Parameter(description = "任务ID") @PathVariable Long id, + @RequestParam String newName) { + log.info("接收到复制任务请求,任务ID: {}, 新任务名称: {}", id, newName); + TaskResponse response = taskApplicationService.copyTask(id, newName); + TaskVO result = TaskVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询任务执行日历 + * GET /api/v1/tasks/{id}/calendar + */ + @GetMapping("/{id}/calendar") + @Operation(summary = "查询任务执行日历", description = "查询任务在指定月份的执行计划") + public Result> getTaskExecutionCalendar( + @Parameter(description = "任务ID") @PathVariable Long id, + @Parameter(description = "年份") @RequestParam Integer year, + @Parameter(description = "月份") @RequestParam Integer month) { + log.info("接收到查询任务执行日历请求,任务ID: {}, 年份: {}, 月份: {}", id, year, month); + List responses = taskApplicationService.getTaskExecutionCalendar(id, year, month); + List results = TaskVoConverter.toCalendarVOList(responses); + return Result.success(results); + } +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/converter/TaskVoConverter.java b/src/main/java/com/tuoheng/airport/task/presentation/converter/TaskVoConverter.java new file mode 100644 index 0000000..a20d111 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/converter/TaskVoConverter.java @@ -0,0 +1,63 @@ +package com.tuoheng.airport.task.presentation.converter; + +import com.tuoheng.airport.task.application.dto.*; +import com.tuoheng.airport.task.presentation.vo.*; +import com.tuoheng.airport.fms.application.dto.FlightRecordResponse; +import com.tuoheng.airport.fms.presentation.vo.FlightRecordVO; +import java.util.List; +import java.util.stream.Collectors; + +public class TaskVoConverter { + + public static TaskCreateRequest toCreateRequest(TaskCreateVO vo) { + // TODO: 实现转换逻辑 + return new TaskCreateRequest(); + } + + public static TaskUpdateRequest toUpdateRequest(TaskUpdateVO vo) { + // TODO: 实现转换逻辑 + return new TaskUpdateRequest(); + } + + public static TaskQueryRequest toQueryRequest(TaskQueryVO vo) { + // TODO: 实现转换逻辑 + return new TaskQueryRequest(); + } + + public static TaskVO toVO(TaskResponse response) { + // TODO: 实现转换逻辑 + return new TaskVO(); + } + + public static List toVOList(List responses) { + return responses.stream().map(TaskVoConverter::toVO).collect(Collectors.toList()); + } + + public static FlightRecordVO toFlightRecordVO(FlightRecordResponse response) { + // TODO: 实现转换逻辑 + return new FlightRecordVO(); + } + + public static List toFlightRecordVOList(List responses) { + return responses.stream().map(TaskVoConverter::toFlightRecordVO).collect(Collectors.toList()); + } + + public static TaskStatisticsVO toStatisticsVO(TaskStatisticsResponse response) { + // TODO: 实现转换逻辑 + return new TaskStatisticsVO(); + } + + public static BatchOperationResultVO toBatchResultVO(BatchOperationResultResponse response) { + // TODO: 实现转换逻辑 + return new BatchOperationResultVO(); + } + + public static TaskExecutionCalendarVO toCalendarVO(TaskExecutionCalendarResponse response) { + // TODO: 实现转换逻辑 + return new TaskExecutionCalendarVO(); + } + + public static List toCalendarVOList(List responses) { + return responses.stream().map(TaskVoConverter::toCalendarVO).collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/vo/BatchOperationResultVO.java b/src/main/java/com/tuoheng/airport/task/presentation/vo/BatchOperationResultVO.java new file mode 100644 index 0000000..417cde9 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/vo/BatchOperationResultVO.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.task.presentation.vo; + +import lombok.Data; + +@Data +public class BatchOperationResultVO { + private Integer successCount; + private Integer failureCount; +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskCreateVO.java b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskCreateVO.java new file mode 100644 index 0000000..3b369cc --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskCreateVO.java @@ -0,0 +1,56 @@ +package com.tuoheng.airport.task.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 任务创建 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskCreateVO { + + /** + * 任务编码 + */ + @NotBlank(message = "任务编码不能为空") + private String taskCode; + + /** + * 任务名称 + */ + @NotBlank(message = "任务名称不能为空") + private String taskName; + + /** + * 描述 + */ + private String description; + + /** + * 航线ID + */ + @NotNull(message = "航线ID不能为空") + private Long airlineId; + + /** + * 设备ID + */ + @NotNull(message = "设备ID不能为空") + private Long deviceId; + + /** + * 调度策略 + */ + @NotNull(message = "调度策略不能为空") + private TaskScheduleVO schedule; +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskExecutionCalendarVO.java b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskExecutionCalendarVO.java new file mode 100644 index 0000000..b7ab01e --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskExecutionCalendarVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.task.presentation.vo; + +import lombok.Data; + +@Data +public class TaskExecutionCalendarVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskQueryVO.java b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskQueryVO.java new file mode 100644 index 0000000..13efef0 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskQueryVO.java @@ -0,0 +1,43 @@ +package com.tuoheng.airport.task.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务查询 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskQueryVO { + + /** + * 任务名称(模糊查询) + */ + private String taskName; + + /** + * 任务状态 + */ + private String status; + + /** + * 航线ID + */ + private Long airlineId; + + /** + * 设备ID + */ + private Long deviceId; + + /** + * 创建人ID + */ + private Long creatorId; +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskScheduleVO.java b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskScheduleVO.java new file mode 100644 index 0000000..966b753 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskScheduleVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.task.presentation.vo; + +import lombok.Data; + +@Data +public class TaskScheduleVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskStatisticsVO.java b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskStatisticsVO.java new file mode 100644 index 0000000..83079a1 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskStatisticsVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.task.presentation.vo; + +import lombok.Data; + +@Data +public class TaskStatisticsVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskUpdateVO.java b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskUpdateVO.java new file mode 100644 index 0000000..0fdc68b --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskUpdateVO.java @@ -0,0 +1,51 @@ +package com.tuoheng.airport.task.presentation.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * 任务更新 VO + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskUpdateVO { + + /** + * 任务ID + */ + private Long id; + + /** + * 任务名称 + */ + @NotBlank(message = "任务名称不能为空") + private String taskName; + + /** + * 描述 + */ + private String description; + + /** + * 航线ID + */ + private Long airlineId; + + /** + * 设备ID + */ + private Long deviceId; + + /** + * 调度策略 + */ + private TaskScheduleVO schedule; +} diff --git a/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskVO.java b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskVO.java new file mode 100644 index 0000000..b1e0ade --- /dev/null +++ b/src/main/java/com/tuoheng/airport/task/presentation/vo/TaskVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.task.presentation.vo; + +import lombok.Data; + +@Data +public class TaskVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/ChangePasswordRequest.java b/src/main/java/com/tuoheng/airport/user/application/dto/ChangePasswordRequest.java new file mode 100644 index 0000000..4650198 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/ChangePasswordRequest.java @@ -0,0 +1,10 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class ChangePasswordRequest { + private Long userId; + private String oldPassword; + private String newPassword; +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/LoginRequest.java b/src/main/java/com/tuoheng/airport/user/application/dto/LoginRequest.java new file mode 100644 index 0000000..1b73aef --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/LoginRequest.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class LoginRequest { + private String username; + private String password; +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/LoginResponse.java b/src/main/java/com/tuoheng/airport/user/application/dto/LoginResponse.java new file mode 100644 index 0000000..02b881f --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/LoginResponse.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class LoginResponse { + private String token; + private UserResponse user; +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/PermissionResponse.java b/src/main/java/com/tuoheng/airport/user/application/dto/PermissionResponse.java new file mode 100644 index 0000000..18eba63 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/PermissionResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class PermissionResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/RoleCreateRequest.java b/src/main/java/com/tuoheng/airport/user/application/dto/RoleCreateRequest.java new file mode 100644 index 0000000..f8ebc15 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/RoleCreateRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class RoleCreateRequest { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/RoleQueryRequest.java b/src/main/java/com/tuoheng/airport/user/application/dto/RoleQueryRequest.java new file mode 100644 index 0000000..5d97594 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/RoleQueryRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class RoleQueryRequest { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/RoleResponse.java b/src/main/java/com/tuoheng/airport/user/application/dto/RoleResponse.java new file mode 100644 index 0000000..b4c531d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/RoleResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class RoleResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/RoleUpdateRequest.java b/src/main/java/com/tuoheng/airport/user/application/dto/RoleUpdateRequest.java new file mode 100644 index 0000000..2cc84e7 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/RoleUpdateRequest.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class RoleUpdateRequest { + private Long id; + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/UserCreateRequest.java b/src/main/java/com/tuoheng/airport/user/application/dto/UserCreateRequest.java new file mode 100644 index 0000000..287b5d0 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/UserCreateRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class UserCreateRequest { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/UserQueryRequest.java b/src/main/java/com/tuoheng/airport/user/application/dto/UserQueryRequest.java new file mode 100644 index 0000000..114fd0f --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/UserQueryRequest.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class UserQueryRequest { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/UserResponse.java b/src/main/java/com/tuoheng/airport/user/application/dto/UserResponse.java new file mode 100644 index 0000000..d64891c --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/UserResponse.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class UserResponse { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/dto/UserUpdateRequest.java b/src/main/java/com/tuoheng/airport/user/application/dto/UserUpdateRequest.java new file mode 100644 index 0000000..d8032dd --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/dto/UserUpdateRequest.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.user.application.dto; + +import lombok.Data; + +@Data +public class UserUpdateRequest { + private Long id; + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/application/service/RoleApplicationService.java b/src/main/java/com/tuoheng/airport/user/application/service/RoleApplicationService.java new file mode 100644 index 0000000..a560061 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/service/RoleApplicationService.java @@ -0,0 +1,98 @@ +package com.tuoheng.airport.user.application.service; + +import com.tuoheng.airport.user.application.dto.*; + +import java.util.List; + +/** + * 角色应用服务接口(Application层) + * 定义角色相关的用例(Use Cases) + * 协调领域模型完成角色管理操作 + * + * @author tuoheng + */ +public interface RoleApplicationService { + + /** + * 创建角色 + * 业务逻辑: + * 1. 验证角色编码唯一性 + * 2. 创建角色 + * 3. 分配默认权限(可选) + * + * @param request 创建请求 + * @return 角色响应 + */ + RoleResponse createRole(RoleCreateRequest request); + + /** + * 更新角色信息 + * 业务逻辑: + * 1. 验证角色存在 + * 2. 更新角色基本信息 + * 3. 不允许修改角色编码 + * + * @param request 更新请求 + * @return 角色响应 + */ + RoleResponse updateRole(RoleUpdateRequest request); + + /** + * 根据ID查询角色 + * + * @param id 角色ID + * @return 角色响应 + */ + RoleResponse getRoleById(Long id); + + /** + * 根据条件查询角色列表 + * + * @param request 查询请求 + * @return 角色列表 + */ + List queryRoles(RoleQueryRequest request); + + /** + * 删除角色 + * 业务逻辑: + * 1. 验证角色存在 + * 2. 检查角色是否可以删除(不能删除系统内置角色) + * 3. 检查是否有用户使用该角色 + * 4. 删除角色 + * 5. 清除角色权限关联 + * + * @param id 角色ID + */ + void deleteRole(Long id); + + /** + * 为角色分配权限 + * 业务逻辑: + * 1. 验证角色存在 + * 2. 验证权限存在 + * 3. 清除角色原有权限 + * 4. 分配新权限 + * 5. 清除相关用户的权限缓存 + * + * @param roleId 角色ID + * @param permissionIds 权限ID列表 + */ + void assignPermissions(Long roleId, List permissionIds); + + /** + * 查询角色的权限列表 + * + * @param roleId 角色ID + * @return 权限列表 + */ + List getRolePermissions(Long roleId); + + /** + * 查询角色的用户列表 + * + * @param roleId 角色ID + * @return 用户列表 + */ + List getRoleUsers(Long roleId); +} diff --git a/src/main/java/com/tuoheng/airport/user/application/service/UserApplicationService.java b/src/main/java/com/tuoheng/airport/user/application/service/UserApplicationService.java new file mode 100644 index 0000000..71ae570 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/application/service/UserApplicationService.java @@ -0,0 +1,192 @@ +package com.tuoheng.airport.user.application.service; + +import com.tuoheng.airport.user.application.dto.*; + +import java.util.List; + +/** + * 用户应用服务接口(Application层) + * 定义用户相关的用例(Use Cases) + * 协调领域模型完成用户管理操作 + * + * @author tuoheng + */ +public interface UserApplicationService { + + /** + * 创建用户 + * 业务逻辑: + * 1. 验证用户名唯一性 + * 2. 加密用户密码 + * 3. 创建用户账号 + * 4. 分配默认角色 + * + * @param request 创建请求 + * @return 用户响应 + */ + UserResponse createUser(UserCreateRequest request); + + /** + * 更新用户信息 + * 业务逻辑: + * 1. 验证用户存在 + * 2. 更新用户基本信息 + * 3. 不允许修改用户名 + * + * @param request 更新请求 + * @return 用户响应 + */ + UserResponse updateUser(UserUpdateRequest request); + + /** + * 根据ID查询用户 + * + * @param id 用户ID + * @return 用户响应 + */ + UserResponse getUserById(Long id); + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @return 用户响应 + */ + UserResponse getUserByUsername(String username); + + /** + * 根据条件查询用户列表 + * + * @param request 查询请求 + * @return 用户列表 + */ + List queryUsers(UserQueryRequest request); + + /** + * 删除用户 + * 业务逻辑: + * 1. 验证用户存在 + * 2. 检查用户是否可以删除(不能删除超级管理员) + * 3. 逻辑删除用户 + * 4. 清除用户的角色关联 + * + * @param id 用户ID + */ + void deleteUser(Long id); + + /** + * 启用用户 + * 业务逻辑: + * 1. 验证用户存在 + * 2. 更新用户状态为启用 + * + * @param id 用户ID + * @return 用户响应 + */ + UserResponse enableUser(Long id); + + /** + * 禁用用户 + * 业务逻辑: + * 1. 验证用户存在 + * 2. 不能禁用超级管理员 + * 3. 更新用户状态为禁用 + * 4. 清除用户的登录会话 + * + * @param id 用户ID + * @return 用户响应 + */ + UserResponse disableUser(Long id); + + /** + * 重置用户密码 + * 业务逻辑: + * 1. 验证用户存在 + * 2. 重置密码为默认密码 + * 3. 强制用户下次登录修改密码 + * + * @param id 用户ID + */ + void resetPassword(Long id); + + /** + * 修改用户密码 + * 业务逻辑: + * 1. 验证旧密码正确 + * 2. 验证新密码强度 + * 3. 加密并保存新密码 + * 4. 清除用户的登录会话 + * + * @param request 修改密码请求 + */ + void changePassword(ChangePasswordRequest request); + + /** + * 分配角色给用户 + * 业务逻辑: + * 1. 验证用户存在 + * 2. 验证角色存在 + * 3. 清除用户原有角色 + * 4. 分配新角色 + * 5. 清除用户权限缓存 + * + * @param userId 用户ID + * @param roleIds 角色ID列表 + */ + void assignRoles(Long userId, List roleIds); + + /** + * 查询用户的角色列表 + * + * @param userId 用户ID + * @return 角色列表 + */ + List getUserRoles(Long userId); + + /** + * 查询用户的权限列表 + * 业务逻辑: + * 1. 查询用户的所有角色 + * 2. 汇总所有角色的权限 + * 3. 去重返回 + * + * @param userId 用户ID + * @return 权限列表 + */ + List getUserPermissions(Long userId); + + /** + * 用户登录 + * 业务逻辑: + * 1. 验证用户名和密码 + * 2. 检查用户状态(是否禁用) + * 3. 生成登录令牌 + * 4. 记录登录日志 + * + * @param request 登录请求 + * @return 登录响应(包含令牌) + */ + LoginResponse login(LoginRequest request); + + /** + * 用户登出 + * 业务逻辑: + * 1. 清除用户登录会话 + * 2. 记录登出日志 + * + * @param userId 用户ID + */ + void logout(Long userId); + + /** + * 验证用户权限 + * 业务逻辑: + * 1. 查询用户的所有权限 + * 2. 检查是否包含指定权限 + * + * @param userId 用户ID + * @param permission 权限标识 + * @return 是否有权限 + */ + boolean hasPermission(Long userId, String permission); +} diff --git a/src/main/java/com/tuoheng/airport/user/domain/model/Permission.java b/src/main/java/com/tuoheng/airport/user/domain/model/Permission.java new file mode 100644 index 0000000..590cb76 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/domain/model/Permission.java @@ -0,0 +1,41 @@ +package com.tuoheng.airport.user.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 权限领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Permission { + + private Long id; + private String permissionCode; + private String permissionName; + private String resourceType; + private String resourcePath; + private String description; + private LocalDateTime createTime; + private LocalDateTime updateTime; + + public static Permission create(String permissionCode, String permissionName, String resourceType, String resourcePath) { + LocalDateTime now = LocalDateTime.now(); + return Permission.builder() + .permissionCode(permissionCode) + .permissionName(permissionName) + .resourceType(resourceType) + .resourcePath(resourcePath) + .createTime(now) + .updateTime(now) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/user/domain/model/Role.java b/src/main/java/com/tuoheng/airport/user/domain/model/Role.java new file mode 100644 index 0000000..29b7026 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/domain/model/Role.java @@ -0,0 +1,62 @@ +package com.tuoheng.airport.user.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 角色领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Role { + + private Long id; + private String roleCode; + private String roleName; + private String description; + private Boolean systemRole; + private Long tenantId; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Boolean deleted; + + // ==================== 业务方法 ==================== + + public boolean isSystemRole() { + return Boolean.TRUE.equals(this.systemRole); + } + + public void validateCanModify() { + if (this.isSystemRole()) { + throw new IllegalStateException("系统内置角色不能修改"); + } + } + + public void validateCanDelete() { + if (this.isSystemRole()) { + throw new IllegalStateException("系统内置角色不能删除"); + } + } + + public static Role create(String roleCode, String roleName, String description, Long tenantId) { + LocalDateTime now = LocalDateTime.now(); + return Role.builder() + .roleCode(roleCode) + .roleName(roleName) + .description(description) + .systemRole(false) + .tenantId(tenantId) + .createTime(now) + .updateTime(now) + .deleted(false) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/user/domain/model/User.java b/src/main/java/com/tuoheng/airport/user/domain/model/User.java new file mode 100644 index 0000000..1dccbe3 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/domain/model/User.java @@ -0,0 +1,109 @@ +package com.tuoheng.airport.user.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户领域模型 + * + * @author tuoheng + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class User { + + private Long id; + private String username; + private String password; + private String realName; + private String email; + private String phone; + private UserStatus status; + private Long deptId; + private Long tenantId; + private Boolean locked; + private Integer failedLoginAttempts; + private LocalDateTime lastLoginTime; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Boolean deleted; + + // ==================== 业务方法 ==================== + + public void enable() { + this.status = UserStatus.ENABLED; + this.updateTime = LocalDateTime.now(); + } + + public void disable() { + if (this.isSuperAdmin()) { + throw new IllegalStateException("不能禁用超级管理员"); + } + this.status = UserStatus.DISABLED; + this.updateTime = LocalDateTime.now(); + } + + public void lock() { + this.locked = true; + this.updateTime = LocalDateTime.now(); + } + + public void unlock() { + this.locked = false; + this.failedLoginAttempts = 0; + this.updateTime = LocalDateTime.now(); + } + + public void recordLoginSuccess() { + this.failedLoginAttempts = 0; + this.lastLoginTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); + } + + public void recordLoginFailure() { + this.failedLoginAttempts++; + if (this.failedLoginAttempts >= 5) { + this.lock(); + } + this.updateTime = LocalDateTime.now(); + } + + public boolean isEnabled() { + return this.status == UserStatus.ENABLED && !this.locked; + } + + public boolean isSuperAdmin() { + return "admin".equals(this.username); + } + + public void changePassword(String newPassword) { + if (newPassword == null || newPassword.length() < 8) { + throw new IllegalArgumentException("密码长度至少8位"); + } + this.password = newPassword; + this.updateTime = LocalDateTime.now(); + } + + public static User create(String username, String password, String realName, Long tenantId) { + LocalDateTime now = LocalDateTime.now(); + return User.builder() + .username(username) + .password(password) + .realName(realName) + .status(UserStatus.ENABLED) + .tenantId(tenantId) + .locked(false) + .failedLoginAttempts(0) + .createTime(now) + .updateTime(now) + .deleted(false) + .build(); + } +} diff --git a/src/main/java/com/tuoheng/airport/user/domain/model/UserStatus.java b/src/main/java/com/tuoheng/airport/user/domain/model/UserStatus.java new file mode 100644 index 0000000..f68ffdb --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/domain/model/UserStatus.java @@ -0,0 +1,29 @@ +package com.tuoheng.airport.user.domain.model; + +/** + * 用户状态枚举 + * + * @author tuoheng + */ +public enum UserStatus { + + /** + * 启用 + */ + ENABLED("启用"), + + /** + * 禁用 + */ + DISABLED("禁用"); + + private final String description; + + UserStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/tuoheng/airport/user/domain/repository/PermissionRepository.java b/src/main/java/com/tuoheng/airport/user/domain/repository/PermissionRepository.java new file mode 100644 index 0000000..c95c550 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/domain/repository/PermissionRepository.java @@ -0,0 +1,28 @@ +package com.tuoheng.airport.user.domain.repository; + +import com.tuoheng.airport.user.domain.model.Permission; + +import java.util.List; +import java.util.Optional; + +/** + * 权限仓储接口 + * + * @author tuoheng + */ +public interface PermissionRepository { + + Permission save(Permission permission); + + Optional findById(Long id); + + Optional findByPermissionCode(String permissionCode); + + List findByRoleId(Long roleId); + + List findByUserId(Long userId); + + void delete(Long id); + + boolean existsByPermissionCode(String permissionCode); +} diff --git a/src/main/java/com/tuoheng/airport/user/domain/repository/RoleRepository.java b/src/main/java/com/tuoheng/airport/user/domain/repository/RoleRepository.java new file mode 100644 index 0000000..a901bed --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/domain/repository/RoleRepository.java @@ -0,0 +1,30 @@ +package com.tuoheng.airport.user.domain.repository; + +import com.tuoheng.airport.user.domain.model.Role; + +import java.util.List; +import java.util.Optional; + +/** + * 角色仓储接口 + * + * @author tuoheng + */ +public interface RoleRepository { + + Role save(Role role); + + Optional findById(Long id); + + Optional findByRoleCode(String roleCode); + + List findByTenantId(Long tenantId); + + List findByUserId(Long userId); + + void delete(Long id); + + boolean existsByRoleCode(String roleCode); + + long countUsersByRoleId(Long roleId); +} diff --git a/src/main/java/com/tuoheng/airport/user/domain/repository/UserRepository.java b/src/main/java/com/tuoheng/airport/user/domain/repository/UserRepository.java new file mode 100644 index 0000000..fb3546d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/domain/repository/UserRepository.java @@ -0,0 +1,30 @@ +package com.tuoheng.airport.user.domain.repository; + +import com.tuoheng.airport.user.domain.model.User; + +import java.util.List; +import java.util.Optional; + +/** + * 用户仓储接口 + * + * @author tuoheng + */ +public interface UserRepository { + + User save(User user); + + Optional findById(Long id); + + Optional findByUsername(String username); + + List findByTenantId(Long tenantId); + + List findByDeptId(Long deptId); + + void delete(Long id); + + boolean existsByUsername(String username); + + long countByTenantId(Long tenantId); +} diff --git a/src/main/java/com/tuoheng/airport/user/domain/service/RoleDomainService.java b/src/main/java/com/tuoheng/airport/user/domain/service/RoleDomainService.java new file mode 100644 index 0000000..0f0cfc4 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/domain/service/RoleDomainService.java @@ -0,0 +1,139 @@ +package com.tuoheng.airport.user.domain.service; + +import com.tuoheng.airport.user.domain.model.Role; +import com.tuoheng.airport.user.domain.model.Permission; +import com.tuoheng.airport.user.domain.model.User; + +import java.util.List; + +/** + * 角色领域服务接口(Domain层) + * 封装角色相关的业务逻辑和业务规则 + * + * Domain Service 的职责: + * 1. 封装角色管理的核心业务规则 + * 2. 协调角色和权限的关系 + * 3. 保证角色数据的一致性 + * 4. 不包含事务管理(事务由 Application 层管理) + * + * @author tuoheng + */ +public interface RoleDomainService { + + /** + * 创建角色 + * 业务规则: + * 1. 角色编码必须唯一 + * 2. 角色名称不能为空 + * 3. 系统内置角色不能创建 + * + * @param role 角色领域模型 + * @return 创建后的角色 + */ + Role createRole(Role role); + + /** + * 更新角色信息 + * 业务规则: + * 1. 不能修改系统内置角色 + * 2. 不能修改角色编码 + * + * @param role 角色领域模型 + * @return 更新后的角色 + */ + Role updateRole(Role role); + + /** + * 删除角色 + * 业务规则: + * 1. 不能删除系统内置角色 + * 2. 检查是否有用户使用该角色 + * 3. 清除角色权限关联 + * + * @param roleId 角色ID + */ + void deleteRole(Long roleId); + + /** + * 为角色分配权限 + * 业务规则: + * 1. 验证权限存在 + * 2. 清除角色原有权限 + * 3. 分配新权限 + * 4. 清除相关用户的权限缓存 + * + * @param roleId 角色ID + * @param permissionIds 权限ID列表 + * @return 更新后的角色 + */ + Role assignPermissions(Long roleId, List permissionIds); + + /** + * 查询角色详情 + * + * @param roleId 角色ID + * @return 角色领域模型 + */ + Role getRoleById(Long roleId); + + /** + * 根据角色编码查询角色 + * + * @param roleCode 角色编码 + * @return 角色领域模型 + */ + Role getRoleByCode(String roleCode); + + /** + * 查询角色的权限列表 + * + * @param roleId 角色ID + * @return 权限列表 + */ + List getRolePermissions(Long roleId); + + /** + * 查询角色的用户列表 + * + * @param roleId 角色ID + * @return 用户列表 + */ + List getRoleUsers(Long roleId); + + /** + * 检查角色编码是否存在 + * + * @param roleCode 角色编码 + * @return 是否存在 + */ + boolean isRoleCodeExists(String roleCode); + + /** + * 检查角色是否为系统内置角色 + * 业务规则: + * 1. 系统内置角色不能修改 + * 2. 系统内置角色不能删除 + * + * @param roleId 角色ID + * @return 是否为系统内置角色 + */ + boolean isSystemRole(Long roleId); + + /** + * 检查角色是否被使用 + * 业务规则: + * 1. 被使用的角色不能删除 + * + * @param roleId 角色ID + * @return 是否被使用 + */ + boolean isRoleInUse(Long roleId); + + /** + * 统计角色的用户数量 + * + * @param roleId 角色ID + * @return 用户数量 + */ + long countRoleUsers(Long roleId); +} diff --git a/src/main/java/com/tuoheng/airport/user/domain/service/UserDomainService.java b/src/main/java/com/tuoheng/airport/user/domain/service/UserDomainService.java new file mode 100644 index 0000000..9269a7b --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/domain/service/UserDomainService.java @@ -0,0 +1,233 @@ +package com.tuoheng.airport.user.domain.service; + +import com.tuoheng.airport.user.domain.model.User; +import com.tuoheng.airport.user.domain.model.Role; +import com.tuoheng.airport.user.domain.model.Permission; + +import java.util.List; + +/** + * 用户领域服务接口(Domain层) + * 封装用户相关的业务逻辑和业务规则 + * + * Domain Service 的职责: + * 1. 封装用户管理的核心业务规则 + * 2. 协调用户、角色、权限的关系 + * 3. 保证用户数据的一致性 + * 4. 不包含事务管理(事务由 Application 层管理) + * + * @author tuoheng + */ +public interface UserDomainService { + + /** + * 注册新用户 + * 业务规则: + * 1. 用户名必须唯一 + * 2. 密码必须符合强度要求 + * 3. 新用户默认为启用状态 + * 4. 自动分配默认角色 + * + * @param user 用户领域模型 + * @return 注册后的用户 + */ + User registerUser(User user); + + /** + * 启用用户 + * 业务规则: + * 1. 只有禁用状态的用户才能启用 + * 2. 更新用户状态为启用 + * + * @param userId 用户ID + * @return 启用后的用户 + */ + User enableUser(Long userId); + + /** + * 禁用用户 + * 业务规则: + * 1. 只有启用状态的用户才能禁用 + * 2. 不能禁用超级管理员 + * 3. 禁用后清除用户的登录会话 + * + * @param userId 用户ID + * @return 禁用后的用户 + */ + User disableUser(Long userId); + + /** + * 重置用户密码 + * 业务规则: + * 1. 重置为默认密码 + * 2. 标记用户下次登录必须修改密码 + * + * @param userId 用户ID + * @return 重置后的用户 + */ + User resetPassword(Long userId); + + /** + * 修改用户密码 + * 业务规则: + * 1. 验证旧密码正确 + * 2. 新密码必须符合强度要求 + * 3. 新密码不能与最近3次使用的密码相同 + * 4. 加密保存新密码 + * + * @param userId 用户ID + * @param oldPassword 旧密码 + * @param newPassword 新密码 + * @return 修改后的用户 + */ + User changePassword(Long userId, String oldPassword, String newPassword); + + /** + * 验证用户密码 + * 业务规则: + * 1. 比对加密后的密码 + * 2. 记录失败次数 + * 3. 连续失败5次后锁定账号 + * + * @param userId 用户ID + * @param password 密码 + * @return 是否验证通过 + */ + boolean verifyPassword(Long userId, String password); + + /** + * 分配角色给用户 + * 业务规则: + * 1. 验证角色存在 + * 2. 清除用户原有角色 + * 3. 分配新角色 + * 4. 更新用户权限缓存 + * + * @param userId 用户ID + * @param roleIds 角色ID列表 + * @return 更新后的用户 + */ + User assignRoles(Long userId, List roleIds); + + /** + * 查询用户详情 + * + * @param userId 用户ID + * @return 用户领域模型 + */ + User getUserById(Long userId); + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @return 用户领域模型 + */ + User getUserByUsername(String username); + + /** + * 查询用户的角色列表 + * + * @param userId 用户ID + * @return 角色列表 + */ + List getUserRoles(Long userId); + + /** + * 查询用户的权限列表 + * 业务规则: + * 1. 查询用户的所有角色 + * 2. 汇总所有角色的权限 + * 3. 去重返回 + * + * @param userId 用户ID + * @return 权限列表 + */ + List getUserPermissions(Long userId); + + /** + * 删除用户 + * 业务规则: + * 1. 不能删除超级管理员 + * 2. 逻辑删除用户 + * 3. 清除用户的角色关联 + * 4. 清除用户的登录会话 + * + * @param userId 用户ID + */ + void deleteUser(Long userId); + + /** + * 检查用户是否有指定权限 + * 业务规则: + * 1. 查询用户的所有权限 + * 2. 检查是否包含指定权限 + * 3. 超级管理员拥有所有权限 + * + * @param userId 用户ID + * @param permissionCode 权限编码 + * @return 是否有权限 + */ + boolean hasPermission(Long userId, String permissionCode); + + /** + * 检查用户是否有指定角色 + * + * @param userId 用户ID + * @param roleCode 角色编码 + * @return 是否有角色 + */ + boolean hasRole(Long userId, String roleCode); + + /** + * 用户登录 + * 业务规则: + * 1. 验证用户名和密码 + * 2. 检查用户状态(是否禁用、是否锁定) + * 3. 更新最后登录时间 + * 4. 清除密码失败次数 + * + * @param username 用户名 + * @param password 密码 + * @return 登录的用户 + */ + User login(String username, String password); + + /** + * 锁定用户账号 + * 业务规则: + * 1. 密码连续失败5次后自动锁定 + * 2. 锁定后30分钟自动解锁 + * + * @param userId 用户ID + * @return 锁定后的用户 + */ + User lockUser(Long userId); + + /** + * 解锁用户账号 + * + * @param userId 用户ID + * @return 解锁后的用户 + */ + User unlockUser(Long userId); + + /** + * 检查用户名是否存在 + * + * @param username 用户名 + * @return 是否存在 + */ + boolean isUsernameExists(String username); + + /** + * 验证密码强度 + * 业务规则: + * 1. 长度至少8位 + * 2. 必须包含大小写字母、数字、特殊字符中的至少3种 + * + * @param password 密码 + * @return 是否符合强度要求 + */ + boolean isPasswordStrong(String password); +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/controller/RoleController.java b/src/main/java/com/tuoheng/airport/user/presentation/controller/RoleController.java new file mode 100644 index 0000000..d89bc36 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/controller/RoleController.java @@ -0,0 +1,148 @@ +package com.tuoheng.airport.user.presentation.controller; + +import com.tuoheng.airport.user.application.dto.*; +import com.tuoheng.airport.user.application.service.RoleApplicationService; +import com.tuoheng.airport.user.presentation.converter.RoleVoConverter; +import com.tuoheng.airport.user.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 java.util.List; + +/** + * 角色管理控制器(Presentation层) + * 提供角色管理的 REST API 接口 + * + * @author tuoheng + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +@Validated +@Tag(name = "角色管理", description = "角色管理相关接口") +public class RoleController { + + private final RoleApplicationService roleApplicationService; + + /** + * 创建角色 + * POST /api/v1/roles + */ + @PostMapping + @Operation(summary = "创建角色", description = "创建新的角色") + public Result createRole(@Valid @RequestBody RoleCreateVO vo) { + log.info("接收到创建角色请求: {}", vo); + RoleCreateRequest request = RoleVoConverter.toCreateRequest(vo); + RoleResponse response = roleApplicationService.createRole(request); + RoleVO result = RoleVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 更新角色信息 + * PUT /api/v1/roles/{id} + */ + @PutMapping("/{id}") + @Operation(summary = "更新角色信息", description = "更新指定角色的信息") + public Result updateRole( + @Parameter(description = "角色ID") @PathVariable Long id, + @Valid @RequestBody RoleUpdateVO vo) { + log.info("接收到更新角色请求,角色ID: {}, 请求参数: {}", id, vo); + vo.setId(id); + RoleUpdateRequest request = RoleVoConverter.toUpdateRequest(vo); + RoleResponse response = roleApplicationService.updateRole(request); + RoleVO result = RoleVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询角色详情 + * GET /api/v1/roles/{id} + */ + @GetMapping("/{id}") + @Operation(summary = "查询角色详情", description = "根据角色ID查询角色详细信息") + public Result getRoleById( + @Parameter(description = "角色ID") @PathVariable Long id) { + log.info("接收到查询角色请求,角色ID: {}", id); + RoleResponse response = roleApplicationService.getRoleById(id); + RoleVO result = RoleVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询角色列表 + * GET /api/v1/roles + */ + @GetMapping + @Operation(summary = "查询角色列表", description = "查询所有角色或根据条件筛选") + public Result> queryRoles(RoleQueryVO vo) { + log.info("接收到查询角色列表请求,查询条件: {}", vo); + RoleQueryRequest request = RoleVoConverter.toQueryRequest(vo); + List responses = roleApplicationService.queryRoles(request); + List results = RoleVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 删除角色 + * DELETE /api/v1/roles/{id} + */ + @DeleteMapping("/{id}") + @Operation(summary = "删除角色", description = "删除指定的角色") + public Result deleteRole( + @Parameter(description = "角色ID") @PathVariable Long id) { + log.info("接收到删除角色请求,角色ID: {}", id); + roleApplicationService.deleteRole(id); + return Result.success(); + } + + /** + * 为角色分配权限 + * POST /api/v1/roles/{id}/permissions + */ + @PostMapping("/{id}/permissions") + @Operation(summary = "分配权限", description = "为角色分配权限") + public Result assignPermissions( + @Parameter(description = "角色ID") @PathVariable Long id, + @RequestBody List permissionIds) { + log.info("接收到分配权限请求,角色ID: {}, 权限IDs: {}", id, permissionIds); + roleApplicationService.assignPermissions(id, permissionIds); + return Result.success(); + } + + /** + * 查询角色的权限列表 + * GET /api/v1/roles/{id}/permissions + */ + @GetMapping("/{id}/permissions") + @Operation(summary = "查询角色权限", description = "查询角色拥有的所有权限") + public Result> getRolePermissions( + @Parameter(description = "角色ID") @PathVariable Long id) { + log.info("接收到查询角色权限请求,角色ID: {}", id); + List responses = roleApplicationService.getRolePermissions(id); + List results = RoleVoConverter.toPermissionVOList(responses); + return Result.success(results); + } + + /** + * 查询角色的用户列表 + * GET /api/v1/roles/{id}/users + */ + @GetMapping("/{id}/users") + @Operation(summary = "查询角色用户", description = "查询拥有该角色的所有用户") + public Result> getRoleUsers( + @Parameter(description = "角色ID") @PathVariable Long id) { + log.info("接收到查询角色用户请求,角色ID: {}", id); + List responses = roleApplicationService.getRoleUsers(id); + List results = RoleVoConverter.toUserVOList(responses); + return Result.success(results); + } +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/controller/UserController.java b/src/main/java/com/tuoheng/airport/user/presentation/controller/UserController.java new file mode 100644 index 0000000..de14a64 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/controller/UserController.java @@ -0,0 +1,226 @@ +package com.tuoheng.airport.user.presentation.controller; + +import com.tuoheng.airport.user.application.dto.*; +import com.tuoheng.airport.user.application.service.UserApplicationService; +import com.tuoheng.airport.user.presentation.converter.UserVoConverter; +import com.tuoheng.airport.user.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 java.util.List; + +/** + * 用户管理控制器(Presentation层) + * 提供用户管理的 REST API 接口 + * 负责用户、角色、权限的管理 + * + * 职责: + * 1. 接收前端的 VO 对象 + * 2. 将 VO 转换为 DTO 传递给 Application 层 + * 3. 将 Application 层返回的 DTO 转换为 VO 返回给前端 + * 4. 不包含任何业务逻辑 + * + * @author tuoheng + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +@Validated +@Tag(name = "用户管理", description = "用户管理相关接口") +public class UserController { + + private final UserApplicationService userApplicationService; + + /** + * 创建用户 + * POST /api/v1/users + */ + @PostMapping + @Operation(summary = "创建用户", description = "创建新的用户账号") + public Result createUser(@Valid @RequestBody UserCreateVO vo) { + log.info("接收到创建用户请求: {}", vo); + UserCreateRequest request = UserVoConverter.toCreateRequest(vo); + UserResponse response = userApplicationService.createUser(request); + UserVO result = UserVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 更新用户信息 + * PUT /api/v1/users/{id} + */ + @PutMapping("/{id}") + @Operation(summary = "更新用户信息", description = "更新指定用户的基本信息") + public Result updateUser( + @Parameter(description = "用户ID") @PathVariable Long id, + @Valid @RequestBody UserUpdateVO vo) { + log.info("接收到更新用户请求,用户ID: {}, 请求参数: {}", id, vo); + vo.setId(id); + UserUpdateRequest request = UserVoConverter.toUpdateRequest(vo); + UserResponse response = userApplicationService.updateUser(request); + UserVO result = UserVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询用户详情 + * GET /api/v1/users/{id} + */ + @GetMapping("/{id}") + @Operation(summary = "查询用户详情", description = "根据用户ID查询用户详细信息") + public Result getUserById( + @Parameter(description = "用户ID") @PathVariable Long id) { + log.info("接收到查询用户请求,用户ID: {}", id); + UserResponse response = userApplicationService.getUserById(id); + UserVO result = UserVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 根据用户名查询用户 + * GET /api/v1/users/username/{username} + */ + @GetMapping("/username/{username}") + @Operation(summary = "根据用户名查询用户", description = "根据用户名查询用户详细信息") + public Result getUserByUsername( + @Parameter(description = "用户名") @PathVariable String username) { + log.info("接收到查询用户请求,用户名: {}", username); + UserResponse response = userApplicationService.getUserByUsername(username); + UserVO result = UserVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 查询用户列表 + * GET /api/v1/users + */ + @GetMapping + @Operation(summary = "查询用户列表", description = "根据条件查询用户列表") + public Result> queryUsers(UserQueryVO vo) { + log.info("接收到查询用户列表请求,查询条件: {}", vo); + UserQueryRequest request = UserVoConverter.toQueryRequest(vo); + List responses = userApplicationService.queryUsers(request); + List results = UserVoConverter.toVOList(responses); + return Result.success(results); + } + + /** + * 删除用户 + * DELETE /api/v1/users/{id} + */ + @DeleteMapping("/{id}") + @Operation(summary = "删除用户", description = "删除指定的用户(逻辑删除)") + public Result deleteUser( + @Parameter(description = "用户ID") @PathVariable Long id) { + log.info("接收到删除用户请求,用户ID: {}", id); + userApplicationService.deleteUser(id); + return Result.success(); + } + + /** + * 启用用户 + * POST /api/v1/users/{id}/enable + */ + @PostMapping("/{id}/enable") + @Operation(summary = "启用用户", description = "启用被禁用的用户账号") + public Result enableUser( + @Parameter(description = "用户ID") @PathVariable Long id) { + log.info("接收到启用用户请求,用户ID: {}", id); + UserResponse response = userApplicationService.enableUser(id); + UserVO result = UserVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 禁用用户 + * POST /api/v1/users/{id}/disable + */ + @PostMapping("/{id}/disable") + @Operation(summary = "禁用用户", description = "禁用用户账号") + public Result disableUser( + @Parameter(description = "用户ID") @PathVariable Long id) { + log.info("接收到禁用用户请求,用户ID: {}", id); + UserResponse response = userApplicationService.disableUser(id); + UserVO result = UserVoConverter.toVO(response); + return Result.success(result); + } + + /** + * 重置用户密码 + * POST /api/v1/users/{id}/reset-password + */ + @PostMapping("/{id}/reset-password") + @Operation(summary = "重置用户密码", description = "重置用户密码为默认密码") + public Result resetPassword( + @Parameter(description = "用户ID") @PathVariable Long id) { + log.info("接收到重置用户密码请求,用户ID: {}", id); + userApplicationService.resetPassword(id); + return Result.success(); + } + + /** + * 修改用户密码 + * POST /api/v1/users/{id}/change-password + */ + @PostMapping("/{id}/change-password") + @Operation(summary = "修改用户密码", description = "用户修改自己的密码") + public Result changePassword( + @Parameter(description = "用户ID") @PathVariable Long id, + @Valid @RequestBody ChangePasswordVO vo) { + log.info("接收到修改用户密码请求,用户ID: {}", id); + ChangePasswordRequest request = UserVoConverter.toChangePasswordRequest(vo); + request.setUserId(id); + userApplicationService.changePassword(request); + return Result.success(); + } + + /** + * 分配角色给用户 + * POST /api/v1/users/{id}/roles + */ + @PostMapping("/{id}/roles") + @Operation(summary = "分配角色", description = "为用户分配角色") + public Result assignRoles( + @Parameter(description = "用户ID") @PathVariable Long id, + @RequestBody List roleIds) { + log.info("接收到分配角色请求,用户ID: {}, 角色IDs: {}", id, roleIds); + userApplicationService.assignRoles(id, roleIds); + return Result.success(); + } + + /** + * 查询用户的角色列表 + * GET /api/v1/users/{id}/roles + */ + @GetMapping("/{id}/roles") + @Operation(summary = "查询用户角色", description = "查询用户拥有的所有角色") + public Result> getUserRoles( + @Parameter(description = "用户ID") @PathVariable Long id) { + log.info("接收到查询用户角色请求,用户ID: {}", id); + List responses = userApplicationService.getUserRoles(id); + List results = UserVoConverter.toRoleVOList(responses); + return Result.success(results); + } + + /** + * 查询用户的权限列表 + * GET /api/v1/users/{id}/permissions + */ + @GetMapping("/{id}/permissions") + @Operation(summary = "查询用户权限", description = "查询用户拥有的所有权限") + public Result> getUserPermissions( + @Parameter(description = "用户ID") @PathVariable Long id) { + log.info("接收到查询用户权限请求,用户ID: {}", id); + List responses = userApplicationService.getUserPermissions(id); + List results = UserVoConverter.toPermissionVOList(responses); + return Result.success(results); + } +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/converter/RoleVoConverter.java b/src/main/java/com/tuoheng/airport/user/presentation/converter/RoleVoConverter.java new file mode 100644 index 0000000..a9c8d4d --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/converter/RoleVoConverter.java @@ -0,0 +1,51 @@ +package com.tuoheng.airport.user.presentation.converter; + +import com.tuoheng.airport.user.application.dto.*; +import com.tuoheng.airport.user.presentation.vo.*; +import java.util.List; +import java.util.stream.Collectors; + +public class RoleVoConverter { + + public static RoleCreateRequest toCreateRequest(RoleCreateVO vo) { + // TODO: 实现转换逻辑 + return new RoleCreateRequest(); + } + + public static RoleUpdateRequest toUpdateRequest(RoleUpdateVO vo) { + // TODO: 实现转换逻辑 + return new RoleUpdateRequest(); + } + + public static RoleQueryRequest toQueryRequest(RoleQueryVO vo) { + // TODO: 实现转换逻辑 + return new RoleQueryRequest(); + } + + public static RoleVO toVO(RoleResponse response) { + // TODO: 实现转换逻辑 + return new RoleVO(); + } + + public static List toVOList(List responses) { + return responses.stream().map(RoleVoConverter::toVO).collect(Collectors.toList()); + } + + public static PermissionVO toPermissionVO(PermissionResponse response) { + // TODO: 实现转换逻辑 + return new PermissionVO(); + } + + public static List toPermissionVOList(List responses) { + return responses.stream().map(RoleVoConverter::toPermissionVO).collect(Collectors.toList()); + } + + public static UserVO toUserVO(UserResponse response) { + // TODO: 实现转换逻辑 + return new UserVO(); + } + + public static List toUserVOList(List responses) { + return responses.stream().map(RoleVoConverter::toUserVO).collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/converter/UserVoConverter.java b/src/main/java/com/tuoheng/airport/user/presentation/converter/UserVoConverter.java new file mode 100644 index 0000000..9ba1f5a --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/converter/UserVoConverter.java @@ -0,0 +1,56 @@ +package com.tuoheng.airport.user.presentation.converter; + +import com.tuoheng.airport.user.application.dto.*; +import com.tuoheng.airport.user.presentation.vo.*; +import java.util.List; +import java.util.stream.Collectors; + +public class UserVoConverter { + + public static UserCreateRequest toCreateRequest(UserCreateVO vo) { + // TODO: 实现转换逻辑 + return new UserCreateRequest(); + } + + public static UserUpdateRequest toUpdateRequest(UserUpdateVO vo) { + // TODO: 实现转换逻辑 + return new UserUpdateRequest(); + } + + public static UserQueryRequest toQueryRequest(UserQueryVO vo) { + // TODO: 实现转换逻辑 + return new UserQueryRequest(); + } + + public static UserVO toVO(UserResponse response) { + // TODO: 实现转换逻辑 + return new UserVO(); + } + + public static List toVOList(List responses) { + return responses.stream().map(UserVoConverter::toVO).collect(Collectors.toList()); + } + + public static ChangePasswordRequest toChangePasswordRequest(ChangePasswordVO vo) { + // TODO: 实现转换逻辑 + return new ChangePasswordRequest(); + } + + public static PermissionVO toPermissionVO(PermissionResponse response) { + // TODO: 实现转换逻辑 + return new PermissionVO(); + } + + public static List toPermissionVOList(List responses) { + return responses.stream().map(UserVoConverter::toPermissionVO).collect(Collectors.toList()); + } + + public static RoleVO toRoleVO(RoleResponse response) { + // TODO: 实现转换逻辑 + return new RoleVO(); + } + + public static List toRoleVOList(List responses) { + return responses.stream().map(UserVoConverter::toRoleVO).collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/ChangePasswordVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/ChangePasswordVO.java new file mode 100644 index 0000000..4319451 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/ChangePasswordVO.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class ChangePasswordVO { + private String oldPassword; + private String newPassword; +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/PermissionVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/PermissionVO.java new file mode 100644 index 0000000..f5711d5 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/PermissionVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class PermissionVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleCreateVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleCreateVO.java new file mode 100644 index 0000000..9093a00 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleCreateVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class RoleCreateVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleQueryVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleQueryVO.java new file mode 100644 index 0000000..4252faf --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleQueryVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class RoleQueryVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleUpdateVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleUpdateVO.java new file mode 100644 index 0000000..2427fa6 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleUpdateVO.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class RoleUpdateVO { + private Long id; + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleVO.java new file mode 100644 index 0000000..759d094 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/RoleVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class RoleVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/UserCreateVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/UserCreateVO.java new file mode 100644 index 0000000..b632dd9 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/UserCreateVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class UserCreateVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/UserQueryVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/UserQueryVO.java new file mode 100644 index 0000000..1fbe175 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/UserQueryVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class UserQueryVO { + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/UserUpdateVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/UserUpdateVO.java new file mode 100644 index 0000000..375072f --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/UserUpdateVO.java @@ -0,0 +1,9 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class UserUpdateVO { + private Long id; + // TODO: 添加字段 +} diff --git a/src/main/java/com/tuoheng/airport/user/presentation/vo/UserVO.java b/src/main/java/com/tuoheng/airport/user/presentation/vo/UserVO.java new file mode 100644 index 0000000..a82ecc1 --- /dev/null +++ b/src/main/java/com/tuoheng/airport/user/presentation/vo/UserVO.java @@ -0,0 +1,8 @@ +package com.tuoheng.airport.user.presentation.vo; + +import lombok.Data; + +@Data +public class UserVO { + // TODO: 添加字段 +}