From 16fc1183b94f5dc041730da4ee673c7758081ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A9=E6=96=87=E9=9D=99WWW?= <15144434+wen-wenjing-www@user.noreply.gitee.com> Date: Tue, 15 Jul 2025 12:48:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E7=AB=A0=E5=A2=9E=E5=8A=A0=E5=AD=97?= =?UTF-8?q?=E6=AE=B5+=E4=BB=BB=E5=8A=A1=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AircraftDeviceController.java | 1 - .../controller/CpArticleController.java | 44 +- .../modules/article/domain/CpArticle.java | 11 +- .../article/domain/dto/CpArticleDTO.java | 35 ++ .../modules/article/mapper/CpLabelMapper.java | 3 + .../controller/OrderAnalysisController.java | 19 +- .../OrderDetailAnalysisController.java | 207 +++++++++ .../{OrderDailyStat.java => DailyStat.java} | 6 +- ...OrderMonthlyStat.java => MonthlyStat.java} | 4 +- .../order/domain/dto/OrderAnalysisResult.java | 3 +- .../domain/dto/OrderDetailAnalysisResult.java | 25 + ...rQuarterlyStat.java => QuarterlyStat.java} | 4 +- .../modules/order/domain/dto/RouteStat.java | 11 + .../{OrderYearlyStat.java => YearlyStat.java} | 4 +- .../order/service/OrderAnalysisService.java | 9 +- .../service/OrderDetailAnalysisService.java | 22 + .../impl/OrderAnalysisServiceImpl.java | 123 +++-- .../impl/OrderDetailAnalysisServiceImpl.java | 435 ++++++++++++++++++ .../route/controller/CpRouteController.java | 39 +- .../modules/route/mapper/CpRouteMapper.java | 7 +- .../modules/system/mapper/EmScenicMapper.java | 4 + .../main/resources/mapper/EmScenicMapper.xml | 10 + .../mapper/order/OrderDetailMapper.xml | 35 +- .../resources/mapper/route/CpRouteMapper.xml | 11 + 24 files changed, 972 insertions(+), 100 deletions(-) create mode 100644 aircraft-system/src/main/java/com/aircraft/modules/article/domain/dto/CpArticleDTO.java create mode 100644 aircraft-system/src/main/java/com/aircraft/modules/order/controller/OrderDetailAnalysisController.java rename aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/{OrderDailyStat.java => DailyStat.java} (51%) rename aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/{OrderMonthlyStat.java => MonthlyStat.java} (58%) create mode 100644 aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderDetailAnalysisResult.java rename aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/{OrderQuarterlyStat.java => QuarterlyStat.java} (60%) create mode 100644 aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/RouteStat.java rename aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/{OrderYearlyStat.java => YearlyStat.java} (62%) create mode 100644 aircraft-system/src/main/java/com/aircraft/modules/order/service/OrderDetailAnalysisService.java create mode 100644 aircraft-system/src/main/java/com/aircraft/modules/order/service/impl/OrderDetailAnalysisServiceImpl.java diff --git a/aircraft-system/src/main/java/com/aircraft/modules/aircraft/controller/AircraftDeviceController.java b/aircraft-system/src/main/java/com/aircraft/modules/aircraft/controller/AircraftDeviceController.java index 132cd9f..2794e6a 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/aircraft/controller/AircraftDeviceController.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/aircraft/controller/AircraftDeviceController.java @@ -27,7 +27,6 @@ import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RestController @RequestMapping("/aircraft/device") - public class AircraftDeviceController { diff --git a/aircraft-system/src/main/java/com/aircraft/modules/article/controller/CpArticleController.java b/aircraft-system/src/main/java/com/aircraft/modules/article/controller/CpArticleController.java index 33c437f..1a66282 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/article/controller/CpArticleController.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/article/controller/CpArticleController.java @@ -2,7 +2,10 @@ package com.aircraft.modules.article.controller; import com.aircraft.modules.article.domain.CpArticle; +import com.aircraft.modules.article.domain.CpLabel; +import com.aircraft.modules.article.domain.CpModule; import com.aircraft.modules.article.domain.CpText; +import com.aircraft.modules.article.domain.dto.CpArticleDTO; import com.aircraft.modules.article.domain.dto.CpArticleVo; import com.aircraft.modules.article.domain.dto.CpLabelVo; import com.aircraft.modules.article.mapper.CpArticleMapper; @@ -17,6 +20,7 @@ import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; @@ -27,6 +31,7 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.net.URI; +import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.*; @@ -109,7 +114,7 @@ public class CpArticleController { @ApiOperation(value = "添加文章") @RequestMapping(method = {RequestMethod.POST}) - public ResponseEntity add(@Valid @RequestBody final CpArticleVo cpArticleVo, final BindingResult result) { + public ResponseEntity add(@Valid @RequestBody final CpArticleDTO cpArticleDTO, final BindingResult result) { try { // 处理参数校验错误 if (result.hasErrors()) { @@ -130,45 +135,48 @@ public class CpArticleController { } // 查找标签信息 - CpLabelVo cpLabelVo = cpLabelMapper.findByName(cpArticleVo.getLabelName()); - if (cpLabelVo == null) { + CpLabel cpLabel = cpLabelMapper.findById(cpArticleDTO.getLabelId()); + if (cpLabel == null) { // 标签不存在时返回明确错误信息 Map error = new HashMap<>(); error.put("code", 400); - error.put("msg", "标签不存在:标签名=" + cpArticleVo.getLabelName()); + error.put("msg", "标签不存在:标签名=" + cpArticleDTO.getLabelId()); return ResponseEntity.badRequest().body(error); } + // 关联标签与模块(关键步骤) + cpLabel.setModuleId(cpArticleDTO.getModuleId()); + cpLabelMapper.updateById(cpLabel); // 更新标签的模块关联 + + CpArticle cpArticle = new CpArticle(); + BeanUtils.copyProperties(cpArticleDTO, cpArticle); // 设置文章基础信息 - cpArticleVo.setCreateTime(LocalDateTime.now()); - cpArticleVo.setCheckState("w");// 默认未审核 - cpArticleVo.setDelFlag(0); - cpArticleVo.setCplabelId(cpLabelVo.getId());// 关联标签ID + cpArticle.setCreateTime(Timestamp.valueOf(LocalDateTime.now())); + cpArticle.setCheckState("t");// 默认已审核 + cpArticle.setDelFlag(0); + cpArticle.setCplabelId(cpLabel.getId());// 关联标签ID Long authorId = getCurrentUserId(); - // 设置作者ID - cpArticleVo.setAuthorId(authorId); - entityService.save(cpArticleVo); - // 保存文章主信息 - entityService.save(cpArticleVo); + cpArticle.setAuthorId(authorId); + entityService.save(cpArticle); // 保存文章内容 CpText cpText = new CpText(); - cpText.setText(cpArticleVo.getText()); + cpText.setText(cpArticleDTO.getText()); cpTextService.save(cpText); // 回填文章内容ID并更新 - cpArticleVo.setTextid(cpText.getId()); - entityService.updateById(cpArticleVo); + cpArticle.setTextid(cpText.getId()); + entityService.updateById(cpArticle); // 返回创建成功的响应 Map success = new HashMap<>(); success.put("code", 201); success.put("msg", "文章创建成功"); - success.put("data", cpArticleVo); + success.put("data", cpArticle); return ResponseEntity - .created(URI.create("/cpArticle/" + cpArticleVo.getId())) + .created(URI.create("/cpArticle/" + cpArticle.getId())) .body(success); } catch (Exception e) { LOG.error("添加文章失败", e); diff --git a/aircraft-system/src/main/java/com/aircraft/modules/article/domain/CpArticle.java b/aircraft-system/src/main/java/com/aircraft/modules/article/domain/CpArticle.java index d12206f..3c33338 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/article/domain/CpArticle.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/article/domain/CpArticle.java @@ -21,7 +21,7 @@ import java.time.LocalDateTime; */ @Data @TableName("cp_article") -public class CpArticle { +public class CpArticle extends BaseEntity { private static final long serialVersionUID = 1L; @@ -47,16 +47,13 @@ public class CpArticle { @ApiModelProperty(value = "类型id,关联标签表id") private Integer cplabelId; - @ApiModelProperty(value = "创建时间") - private LocalDateTime createTime; + @ApiModelProperty(value = "是否置顶:1-置顶,0-不置顶") + private Integer top; @ApiModelProperty(value = "发布者id(用户表或管理员表由发布者类型决定)") private Long authorId; @ApiModelProperty(value = "审核状态:t-审核通过,f-不通过,w-未审核") - private String checkState; - - @ApiModelProperty(value = "删除标记:“1”已删除,“0”正常") - private Integer delFlag; + private String checkState = "t"; } diff --git a/aircraft-system/src/main/java/com/aircraft/modules/article/domain/dto/CpArticleDTO.java b/aircraft-system/src/main/java/com/aircraft/modules/article/domain/dto/CpArticleDTO.java new file mode 100644 index 0000000..0e7e786 --- /dev/null +++ b/aircraft-system/src/main/java/com/aircraft/modules/article/domain/dto/CpArticleDTO.java @@ -0,0 +1,35 @@ +package com.aircraft.modules.article.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import javax.validation.constraints.NotBlank; + +@Data +public class CpArticleDTO { + @NotBlank(message = "标题不能为空") + @ApiModelProperty(value = "文章标题", required = true) + private String title; + + @ApiModelProperty(value = "封面图片URL") + private String photo; + + @ApiModelProperty(value = "文章类型:0-图文,1-外链", allowableValues = "0,1") + private Integer articleType; + + @ApiModelProperty(value = "文章外链") + private String url; + + @NotBlank(message = "标签id不能为空") + @ApiModelProperty(value = "标签ID", required = true) + private int labelId; + + @ApiModelProperty(value = "模块ID", required = true) + private int moduleId; + + @NotBlank(message = "文章内容不能为空") + @ApiModelProperty(value = "文章正文内容", required = true) + private String text; + + @ApiModelProperty(value = "是否置顶:1-置顶,0-不置顶") + private Integer top; +} \ No newline at end of file diff --git a/aircraft-system/src/main/java/com/aircraft/modules/article/mapper/CpLabelMapper.java b/aircraft-system/src/main/java/com/aircraft/modules/article/mapper/CpLabelMapper.java index 063740d..2f2633f 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/article/mapper/CpLabelMapper.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/article/mapper/CpLabelMapper.java @@ -24,4 +24,7 @@ public interface CpLabelMapper extends BaseMapper { List selectLabelTreeByModuleId(Integer moduleId); void updateDelFlagById(Integer id, Integer delFlag); + + @Select("SELECT * FROM cp_label WHERE id = #{labelId}") + CpLabelVo findById(int labelId); } diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/controller/OrderAnalysisController.java b/aircraft-system/src/main/java/com/aircraft/modules/order/controller/OrderAnalysisController.java index 61c81b0..d299437 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/order/controller/OrderAnalysisController.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/controller/OrderAnalysisController.java @@ -2,7 +2,6 @@ package com.aircraft.modules.order.controller; import com.aircraft.modules.order.domain.OrderMain; import com.aircraft.modules.order.domain.dto.*; -import com.aircraft.modules.order.mapper.OrderMainMapper; import com.aircraft.modules.order.service.IOrderMainService; import com.aircraft.modules.order.service.OrderAnalysisService; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; @@ -13,7 +12,6 @@ import org.apache.poi.ss.formula.functions.T; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -22,7 +20,6 @@ import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.Collections; import java.util.List; import java.util.regex.Pattern; @@ -69,7 +66,7 @@ public class OrderAnalysisController { @ApiOperation("按日范围分析订单") @GetMapping("/day") //额外返回传入时间范围内的每日订单总量 - public ResponseEntity>> analyzeByDayRange( + public ResponseEntity>> analyzeByDayRange( @ApiParam(value = "开始日期(yyyy-MM-dd)", required = true, example = "2025-01-01") @RequestParam("startDate") String startDate, @ApiParam(value = "结束日期(yyyy-MM-dd)", required = true, example = "2025-01-31") @@ -86,7 +83,7 @@ public class OrderAnalysisController { } // 调用服务层 - OrderAnalysisResult> result = orderAnalysisService.analyzeByDayRange(start, end); + OrderAnalysisResult> result = orderAnalysisService.analyzeByDayRange(start, end); LOG.info("按日分析完成,时间范围:{}至{}", startDate, endDate); return ResponseEntity.ok(result); } catch (Exception e) { @@ -98,7 +95,7 @@ public class OrderAnalysisController { @ApiOperation("按月范围分析订单") @GetMapping("/month") //传入的时间范围:某年的某月到某年的某月,额外返回时间范围内的每月总订单量 - public ResponseEntity>> analyzeByMonthRange( + public ResponseEntity>> analyzeByMonthRange( @ApiParam(value = "开始月份(yyyy-MM)", required = true, example = "2025-01") @RequestParam("startDate") String startDate, @ApiParam(value = "结束月份(yyyy-MM)", required = true, example = "2025-03") @@ -117,7 +114,7 @@ public class OrderAnalysisController { } // 调用服务层 - OrderAnalysisResult> result = orderAnalysisService.analyzeByMonthRange(start, end); + OrderAnalysisResult> result = orderAnalysisService.analyzeByMonthRange(start, end); LOG.info("按月分析完成,时间范围:{}至{}", startDate, endDate); return ResponseEntity.ok(result); } catch (Exception e) { @@ -129,7 +126,7 @@ public class OrderAnalysisController { @ApiOperation("按季度范围分析订单") @GetMapping("/quarter") //传入的时间范围:某年的某季到某年的某季,yyyy-Qn (例: 2025-Q1),额外返回时间范围内的每季总订单量 - public ResponseEntity>> analyzeByQuarterRange( + public ResponseEntity>> analyzeByQuarterRange( @ApiParam(value = "开始季度(yyyy-Qn,例:2025-Q1)", required = true) @RequestParam("startDate") String startDate, @ApiParam(value = "结束季度(yyyy-Qn,例:2025-Q4)", required = true) @@ -161,7 +158,7 @@ public class OrderAnalysisController { } // 调用服务层 - OrderAnalysisResult> result = orderAnalysisService.analyzeByQuarterRange(start, end); + OrderAnalysisResult> result = orderAnalysisService.analyzeByQuarterRange(start, end); LOG.info("按季度分析完成,时间范围:{}至{}", startDate, endDate); return ResponseEntity.ok(result); } catch (Exception e) { @@ -174,7 +171,7 @@ public class OrderAnalysisController { @ApiOperation("按年范围分析订单") @GetMapping("/year") //传入的时间范围;某年到某年,额外返回时间范围内的每年总订单量 - public ResponseEntity>> analyzeByYearRange( + public ResponseEntity>> analyzeByYearRange( @ApiParam(value = "开始年份(yyyy)", required = true, example = "2023") @RequestParam("startDate") String startDate, @ApiParam(value = "结束年份(yyyy)", required = true, example = "2025") @@ -191,7 +188,7 @@ public class OrderAnalysisController { } // 调用服务层 - OrderAnalysisResult> result = orderAnalysisService.analyzeByYearRange(start, end); + OrderAnalysisResult> result = orderAnalysisService.analyzeByYearRange(start, end); LOG.info("按年分析完成,时间范围:{}至{}", startDate, endDate); return ResponseEntity.ok(result); } catch (Exception e) { diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/controller/OrderDetailAnalysisController.java b/aircraft-system/src/main/java/com/aircraft/modules/order/controller/OrderDetailAnalysisController.java new file mode 100644 index 0000000..4e0dc09 --- /dev/null +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/controller/OrderDetailAnalysisController.java @@ -0,0 +1,207 @@ +package com.aircraft.modules.order.controller; + +import com.aircraft.modules.order.domain.OrderDetail; +import com.aircraft.modules.order.domain.dto.*; +import com.aircraft.modules.order.service.IOrderDetailService; +import com.aircraft.modules.order.service.OrderDetailAnalysisService; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.apache.poi.ss.formula.functions.T; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.regex.Pattern; + +@Api(tags = "系统:子单任务分析") +@RestController +@RequestMapping("/fmsOdOrderDetailAnalysis") +//统一返回时间范围内任务总量,任务状态统计以及任务状态分布,景区路线任务分布 +public class OrderDetailAnalysisController { + + private static final Logger LOG = LoggerFactory.getLogger(OrderDetailAnalysisController.class); + + @Autowired + private OrderDetailAnalysisService orderDetailAnalysisService; // 子单分析服务层 + @Autowired + private IOrderDetailService orderDetailService; + + // 日期格式化器 + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final Pattern QUARTER_PATTERN = Pattern.compile("^\\d{4}-Q[1-4]$"); + + + @ApiOperation("全部子单任务分析统计") + @GetMapping + public ResponseEntity> analyzeAllOrderDetails() { + try { + // 1. 获取所有子单数据 + List allDetails = orderDetailService.list(); + + // 2. 校验数据 + if (CollectionUtils.isEmpty(allDetails)) { + LOG.info("全部子单分析:未找到子单数据"); + return ResponseEntity.badRequest().body(null); + } + OrderDetailAnalysisResult result = orderDetailAnalysisService.analyzeAllOrderDetails(allDetails); + return ResponseEntity.ok(result); + } catch (Exception e) { + LOG.error("全部子单分析失败", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } + + + @ApiOperation("按日范围分析子单任务") + @GetMapping("/day") + // 额外返回:时间范围内任务列表 + public ResponseEntity>> analyzeByDayRange( + @ApiParam(value = "开始日期(yyyy-MM-dd)", required = true, example = "2025-01-01") + @RequestParam("startDate") String startDate, + @ApiParam(value = "结束日期(yyyy-MM-dd)", required = true, example = "2025-01-31") + @RequestParam("endDate") String endDate) { + try { + // 解析并校验日期 + LocalDate start = LocalDate.parse(startDate, DATE_FORMATTER); + LocalDate end = LocalDate.parse(endDate, DATE_FORMATTER); + if (start.isAfter(end)) { + LOG.warn("按日分析子单:开始日期[{}]晚于结束日期[{}]", startDate, endDate); + return ResponseEntity.badRequest().body(null); + } + OrderDetailAnalysisResult> result = + orderDetailAnalysisService.analyzeByDayRange(start, end); + LOG.info("子单按日分析完成,时间范围:{}至{}", startDate, endDate); + return ResponseEntity.ok(result); + } catch (Exception e) { + LOG.error("子单按日分析失败,参数:startDate={}, endDate={}", startDate, endDate, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } + + + @ApiOperation("按月范围分析子单任务") + @GetMapping("/month") + // 额外返回:时间范围内任务列表 + public ResponseEntity>> analyzeByMonthRange( + @ApiParam(value = "开始月份(yyyy-MM)", required = true, example = "2025-01") + @RequestParam("startDate") String startDate, + @ApiParam(value = "结束月份(yyyy-MM)", required = true, example = "2025-03") + @RequestParam("endDate") String endDate) { + try { + // 解析月份为当月第一天和最后一天 + LocalDate start = LocalDate.parse(startDate + "-01", DATE_FORMATTER); + LocalDate end = LocalDate.parse(endDate + "-01", DATE_FORMATTER) + .withDayOfMonth(LocalDate.parse(endDate + "-01", DATE_FORMATTER).lengthOfMonth()); + + if (start.isAfter(end)) { + LOG.warn("子单按月分析:开始月份[{}]晚于结束月份[{}]", startDate, endDate); + return ResponseEntity.badRequest().body(null); + } + + OrderDetailAnalysisResult> result = + orderDetailAnalysisService.analyzeByMonthRange(start, end); + LOG.info("子单按月分析完成,时间范围:{}至{}", startDate, endDate); + return ResponseEntity.ok(result); + } catch (Exception e) { + LOG.error("子单按月分析失败,参数:startDate={}, endDate={}", startDate, endDate, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } + + + @ApiOperation("按季度范围分析子单任务") + @GetMapping("/quarter") + // 额外返回:时间范围内任务列表 + public ResponseEntity>> analyzeByQuarterRange( + @ApiParam(value = "开始季度(yyyy-Qn,例:2025-Q1)", required = true) + @RequestParam("startDate") String startDate, + @ApiParam(value = "结束季度(yyyy-Qn,例:2025-Q4)", required = true) + @RequestParam("endDate") String endDate) { + try { + // 校验季度格式 + if (!QUARTER_PATTERN.matcher(startDate).matches() || !QUARTER_PATTERN.matcher(endDate).matches()) { + LOG.warn("子单按季度分析:格式错误,正确格式:yyyy-Q1~Q4"); + return ResponseEntity.badRequest().body(null); + } + + // 解析季度为日期范围 + String[] startParts = startDate.split("-Q"); + int startYear = Integer.parseInt(startParts[0]); + int startQuarter = Integer.parseInt(startParts[1]); + + String[] endParts = endDate.split("-Q"); + int endYear = Integer.parseInt(endParts[0]); + int endQuarter = Integer.parseInt(endParts[1]); + + LocalDate start = getQuarterFirstDay(startYear, startQuarter); + LocalDate end = getQuarterLastDay(endYear, endQuarter); + + if (start.isAfter(end)) { + LOG.warn("子单按季度分析:开始季度[{}]晚于结束季度[{}]", startDate, endDate); + return ResponseEntity.badRequest().body(null); + } + + // 调用服务层:按季度统计子单量、路线热度、操作员效率 + OrderDetailAnalysisResult> result = + orderDetailAnalysisService.analyzeByQuarterRange(start, end); + LOG.info("子单按季度分析完成,时间范围:{}至{}", startDate, endDate); + return ResponseEntity.ok(result); + } catch (Exception e) { + LOG.error("子单按季度分析失败,参数:startDate={}, endDate={}", startDate, endDate, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } + + + @ApiOperation("按年范围分析子单任务") + @GetMapping("/year") + // 额外返回:时间范围内任务列表 + public ResponseEntity>> analyzeByYearRange( + @ApiParam(value = "开始年份(yyyy)", required = true, example = "2023") + @RequestParam("startDate") String startDate, + @ApiParam(value = "结束年份(yyyy)", required = true, example = "2025") + @RequestParam("endDate") String endDate) { + try { + // 解析年份为当年第一天和最后一天 + LocalDate start = LocalDate.parse(startDate + "-01-01", DATE_FORMATTER); + LocalDate end = LocalDate.parse(endDate + "-12-31", DATE_FORMATTER); + + if (start.isAfter(end)) { + LOG.warn("子单按年分析:开始年份[{}]晚于结束年份[{}]", startDate, endDate); + return ResponseEntity.badRequest().body(null); + } + + // 调用服务层:按年统计子单总量、设备/路线年度表现、载人特征变化 + OrderDetailAnalysisResult> result = + orderDetailAnalysisService.analyzeByYearRange(start, end); + LOG.info("子单按年分析完成,时间范围:{}至{}", startDate, endDate); + return ResponseEntity.ok(result); + } catch (Exception e) { + LOG.error("子单按年分析失败,参数:startDate={}, endDate={}", startDate, endDate, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } + + + // 工具方法:复用原有季度日期转换逻辑 + private LocalDate getQuarterFirstDay(int year, int quarter) { + int month = (quarter - 1) * 3 + 1; + return LocalDate.of(year, month, 1); + } + + private LocalDate getQuarterLastDay(int year, int quarter) { + int month = quarter * 3; + return LocalDate.of(year, month, 1).withDayOfMonth(LocalDate.of(year, month, 1).lengthOfMonth()); + } +} \ No newline at end of file diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderDailyStat.java b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/DailyStat.java similarity index 51% rename from aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderDailyStat.java rename to aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/DailyStat.java index 26b030f..014005d 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderDailyStat.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/DailyStat.java @@ -2,9 +2,9 @@ package com.aircraft.modules.order.domain.dto; import lombok.Data; -//每日的订单总量统计实体类 +//总量统计实体类 @Data -public class OrderDailyStat { +public class DailyStat { private String date; - private Long orderCount; + private Long dailyCount;//每日数量 } diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderMonthlyStat.java b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/MonthlyStat.java similarity index 58% rename from aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderMonthlyStat.java rename to aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/MonthlyStat.java index 0676113..195b1df 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderMonthlyStat.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/MonthlyStat.java @@ -3,7 +3,7 @@ package com.aircraft.modules.order.domain.dto; import lombok.Data; @Data -public class OrderMonthlyStat { +public class MonthlyStat { private String month; - private Long orderCount; + private Long monthlyCount;//每月数量 } diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderAnalysisResult.java b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderAnalysisResult.java index 917eb9f..ab80812 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderAnalysisResult.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderAnalysisResult.java @@ -18,7 +18,8 @@ public class OrderAnalysisResult { private PageInfo pageInfo; // 分页信息 private List orderList; // 订单列表 private Map droneModelDistribution; // 无人机型号订单占比 - private Map routeOrderDistribution; // 不同区域的订单总量 + private List routeDistribution;//景区-路线的订单分布 +// private Map routeOrderDistribution; // 不同区域的订单总量 private T timeSeriesData; // 时间序列数据(日/月/年统计) } diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderDetailAnalysisResult.java b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderDetailAnalysisResult.java new file mode 100644 index 0000000..9207e60 --- /dev/null +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderDetailAnalysisResult.java @@ -0,0 +1,25 @@ +package com.aircraft.modules.order.domain.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@Data +public class OrderDetailAnalysisResult { + private Long totalTaskCount; // 总子单(任务)量 + private Long notStartedTaskCount; //未进行任务量 + private Long processingTaskCount; // 进行中任务量 + private Long completedTaskCount; // 已完成任务量 + private Long failedTaskCount; // 任务执行失败量 + private BigDecimal notStartedRate; // 任务未进行率 + private BigDecimal processingRate; // 任务进行率 + private BigDecimal completionRate; // 任务完成率 + private BigDecimal failedRate; // 任务失败率 + + private List routeDistribution; // 景区路线统计分布 + private PageInfo pageInfo; // 分页信息 + private List orderDetailList; // 子单列表(分页) + private T timeSeriesData; // 时间序列数据(日/月/年统计) +} \ No newline at end of file diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderQuarterlyStat.java b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/QuarterlyStat.java similarity index 60% rename from aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderQuarterlyStat.java rename to aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/QuarterlyStat.java index f10406c..e0a8254 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderQuarterlyStat.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/QuarterlyStat.java @@ -3,7 +3,7 @@ package com.aircraft.modules.order.domain.dto; import lombok.Data; @Data -public class OrderQuarterlyStat { +public class QuarterlyStat { private String quarter; // 季度标识(如2025-Q1) - private Long orderCount; // 本季度订单总量 + private Long QuarterlyCount; // 每季度数量 } diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/RouteStat.java b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/RouteStat.java new file mode 100644 index 0000000..ff932d6 --- /dev/null +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/RouteStat.java @@ -0,0 +1,11 @@ +package com.aircraft.modules.order.domain.dto; + +import lombok.Data; + +@Data +public class RouteStat { + private String scenicName;//景区名称,通过路线id获得景区id + private String routeName;//路线名称 + private Long routeId ;//路线id + private Long count;//某路线的订单/任务数量 +} diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderYearlyStat.java b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/YearlyStat.java similarity index 62% rename from aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderYearlyStat.java rename to aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/YearlyStat.java index fce0ee5..bdfa560 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/OrderYearlyStat.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/domain/dto/YearlyStat.java @@ -3,7 +3,7 @@ package com.aircraft.modules.order.domain.dto; import lombok.Data; @Data -public class OrderYearlyStat { +public class YearlyStat { private String year; - private Long orderCount; + private Long yearlyCount; } \ No newline at end of file diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/service/OrderAnalysisService.java b/aircraft-system/src/main/java/com/aircraft/modules/order/service/OrderAnalysisService.java index 3930eca..062ac11 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/order/service/OrderAnalysisService.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/service/OrderAnalysisService.java @@ -3,7 +3,6 @@ package com.aircraft.modules.order.service; import com.aircraft.modules.order.domain.OrderMain; import com.aircraft.modules.order.domain.dto.*; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import java.time.LocalDate; @@ -11,13 +10,13 @@ import java.util.List; @Service public interface OrderAnalysisService { - OrderAnalysisResult> analyzeByDayRange(LocalDate start, LocalDate end); + OrderAnalysisResult> analyzeByDayRange(LocalDate start, LocalDate end); - OrderAnalysisResult> analyzeByMonthRange(LocalDate start, LocalDate end); + OrderAnalysisResult> analyzeByMonthRange(LocalDate start, LocalDate end); - OrderAnalysisResult> analyzeByQuarterRange(LocalDate start, LocalDate end); + OrderAnalysisResult> analyzeByQuarterRange(LocalDate start, LocalDate end); - OrderAnalysisResult> analyzeByYearRange(LocalDate start, LocalDate end); + OrderAnalysisResult> analyzeByYearRange(LocalDate start, LocalDate end); OrderAnalysisResult analyzeAllOrders(List allOrders); } diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/service/OrderDetailAnalysisService.java b/aircraft-system/src/main/java/com/aircraft/modules/order/service/OrderDetailAnalysisService.java new file mode 100644 index 0000000..1802780 --- /dev/null +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/service/OrderDetailAnalysisService.java @@ -0,0 +1,22 @@ +package com.aircraft.modules.order.service; + +import com.aircraft.modules.order.domain.OrderDetail; +import com.aircraft.modules.order.domain.dto.*; +import org.apache.poi.ss.formula.functions.T; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +@Service +public interface OrderDetailAnalysisService { + OrderDetailAnalysisResult analyzeAllOrderDetails(List allDetails); + + OrderDetailAnalysisResult> analyzeByDayRange(LocalDate start, LocalDate end); + + OrderDetailAnalysisResult> analyzeByMonthRange(LocalDate start, LocalDate end); + + OrderDetailAnalysisResult> analyzeByQuarterRange(LocalDate start, LocalDate end); + + OrderDetailAnalysisResult> analyzeByYearRange(LocalDate start, LocalDate end); +} diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/service/impl/OrderAnalysisServiceImpl.java b/aircraft-system/src/main/java/com/aircraft/modules/order/service/impl/OrderAnalysisServiceImpl.java index ee635a9..4fa5dbe 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/order/service/impl/OrderAnalysisServiceImpl.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/service/impl/OrderAnalysisServiceImpl.java @@ -2,15 +2,20 @@ package com.aircraft.modules.order.service.impl; import com.aircraft.modules.aircraft.domain.AircraftDevice; import com.aircraft.modules.aircraft.mapper.AircraftDeviceMapper; +import com.aircraft.modules.order.controller.OrderAnalysisController; import com.aircraft.modules.order.domain.OrderDetail; import com.aircraft.modules.order.domain.OrderMain; import com.aircraft.modules.order.domain.dto.*; import com.aircraft.modules.order.mapper.OrderDetailMapper; import com.aircraft.modules.order.mapper.OrderMainMapper; import com.aircraft.modules.order.service.OrderAnalysisService; +import com.aircraft.modules.route.domain.CpRoute; import com.aircraft.modules.route.mapper.CpRouteMapper; +import com.aircraft.modules.system.mapper.EmScenicMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -36,6 +41,9 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { private OrderDetailMapper orderDetailMapper; @Autowired private CpRouteMapper cpRouteMapper; + @Autowired + private EmScenicMapper emScenicMapper; + private static final Logger LOG = LoggerFactory.getLogger(OrderAnalysisController.class); // 日期格式化器(用于按日/月/季度/年分组) private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); @@ -54,7 +62,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { * 按日范围分析订单 */ @Override - public OrderAnalysisResult> analyzeByDayRange(LocalDate start, LocalDate end) { + public OrderAnalysisResult> analyzeByDayRange(LocalDate start, LocalDate end) { // 1. 查询时间范围内的所有订单 List orders = queryOrdersByDateRange(start, end); if (CollectionUtils.isEmpty(orders)) { @@ -74,13 +82,13 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { System.out.println(dailyCountMap); // 3. 填充每日统计数据(确保日期连续性,即使某天无订单也显示0) - List dailyStats = new ArrayList<>(); + List dailyStats = new ArrayList<>(); LocalDate current = start; while (!current.isAfter(end)) { String dateStr = current.format(DAY_FORMATTER); - OrderDailyStat stat = new OrderDailyStat(); + DailyStat stat = new DailyStat(); stat.setDate(dateStr); - stat.setOrderCount(dailyCountMap.getOrDefault(dateStr, 0L)); + stat.setDailyCount(dailyCountMap.getOrDefault(dateStr, 0L)); dailyStats.add(stat); current = current.plusDays(1); // 移至下一天 } @@ -94,7 +102,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { * 按月范围分析订单 */ @Override - public OrderAnalysisResult> analyzeByMonthRange(LocalDate start, LocalDate end) { + public OrderAnalysisResult> analyzeByMonthRange(LocalDate start, LocalDate end) { // 1. 查询时间范围内的所有订单 List orders = queryOrdersByDateRange(start, end); if (CollectionUtils.isEmpty(orders)) { @@ -112,15 +120,15 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { )); // 3. 填充每月统计数据(确保月份连续性) - List monthlyStats = new ArrayList<>(); + List monthlyStats = new ArrayList<>(); LocalDate current = start.withDayOfMonth(1); // 从当月第一天开始 LocalDate endMonth = end.withDayOfMonth(1); // 结束月份第一天 while (!current.isAfter(endMonth)) { String monthStr = current.format(MONTH_FORMATTER); - OrderMonthlyStat stat = new OrderMonthlyStat(); + MonthlyStat stat = new MonthlyStat(); stat.setMonth(monthStr); - stat.setOrderCount(monthlyCountMap.getOrDefault(monthStr, 0L)); + stat.setMonthlyCount(monthlyCountMap.getOrDefault(monthStr, 0L)); monthlyStats.add(stat); current = current.plusMonths(1); // 移至下一月 } @@ -134,7 +142,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { * 按季度范围分析订单 */ @Override - public OrderAnalysisResult> analyzeByQuarterRange(LocalDate start, LocalDate end) { + public OrderAnalysisResult> analyzeByQuarterRange(LocalDate start, LocalDate end) { // 1. 查询时间范围内的所有订单 List orders = queryOrdersByDateRange(start, end); if (CollectionUtils.isEmpty(orders)) { @@ -154,7 +162,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { )); // 3. 填充每季度统计数据(确保季度连续性) - List quarterlyStats = new ArrayList<>(); + List quarterlyStats = new ArrayList<>(); LocalDate currentQuarterStart = start; // 调整至当前季度第一天(如2025-05-10 → 2025-04-01) currentQuarterStart = currentQuarterStart.minusMonths((currentQuarterStart.getMonthValue() - 1) % 3); @@ -165,9 +173,9 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { int quarter = (currentQuarterStart.getMonthValue() - 1) / 3 + 1; String quarterStr = currentQuarterStart.format(YEAR_FORMATTER) + "-Q" + quarter; - OrderQuarterlyStat stat = new OrderQuarterlyStat(); + QuarterlyStat stat = new QuarterlyStat(); stat.setQuarter(quarterStr); - stat.setOrderCount(quarterCountMap.getOrDefault(quarterStr, 0L)); + stat.setQuarterlyCount(quarterCountMap.getOrDefault(quarterStr, 0L)); quarterlyStats.add(stat); // 移至下一季度第一天 @@ -183,7 +191,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { * 按年范围分析订单 */ @Override - public OrderAnalysisResult> analyzeByYearRange(LocalDate start, LocalDate end) { + public OrderAnalysisResult> analyzeByYearRange(LocalDate start, LocalDate end) { // 1. 查询时间范围内的所有订单 List orders = queryOrdersByDateRange(start, end); if (CollectionUtils.isEmpty(orders)) { @@ -200,15 +208,15 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { Collectors.counting() )); // 3. 填充每年统计数据(确保年份连续性) - List yearlyStats = new ArrayList<>(); + List yearlyStats = new ArrayList<>(); int startYear = start.getYear(); int endYear = end.getYear(); for (int year = startYear; year <= endYear; year++) { String yearStr = String.valueOf(year); - OrderYearlyStat stat = new OrderYearlyStat(); + YearlyStat stat = new YearlyStat(); stat.setYear(yearStr); - stat.setOrderCount(yearCountMap.getOrDefault(yearStr, 0L)); + stat.setYearlyCount(yearCountMap.getOrDefault(yearStr, 0L)); yearlyStats.add(stat); } @@ -279,7 +287,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { result.setDroneModelDistribution(calculateDroneModelDistribution(orders)); // 5. 景区路线订单分布 - result.setRouteOrderDistribution(calculateRouteDistribution(orders)); + result.setRouteDistribution(calculateRouteDistribution(orders)); return result; } @@ -397,21 +405,78 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { /** * 计算景区内各路线的订单分布 - * - * @param orders 订单列表 - * @return 路线订单分布映射,键为路线名称,值为对应订单数量 */ - private Map calculateRouteDistribution(List orders) { - Map routeDistribution = new HashMap<>(); + public List calculateRouteDistribution(List orders) { + // 统计各路线的订单数量 + Map routeCountMap = new HashMap<>(); + for (OrderMain order : Optional.ofNullable(orders).orElse(Collections.emptyList())) { + if (order == null || order.getRouteIds() == null || order.getRouteIds().isEmpty()) { + continue; + } - // 遍历所有订单,统计各路线的订单数量,1个订单会包含多个路线 - for (OrderMain order : orders) { - Long routeId = 0L; // TODO 这里路线可能存在多个 - String routeName = cpRouteMapper.getNameById(routeId); - if (routeName != null) { - routeDistribution.put(routeName, routeDistribution.getOrDefault(routeName, 0L) + 1); + String[] routeIdArray = order.getRouteIds().split(","); + for (String routeIdStr : routeIdArray) { + String trimmedRouteId = routeIdStr.trim(); + if (!trimmedRouteId.isEmpty()) { + try { + Long routeId = Long.parseLong(trimmedRouteId); + routeCountMap.put(routeId, routeCountMap.getOrDefault(routeId, 0L) + 1); + } catch (NumberFormatException e) { + LOG.error("路线id格式错误: {}", trimmedRouteId, e); + } + } } } + + // 转换统计结果为RouteStat列表 + List routeDistribution = new ArrayList<>(); + + if (!routeCountMap.isEmpty()) { + // 提取所有路线ID + Set routeIds = routeCountMap.keySet(); + + // 批量查询所有路线信息(仅需查询路线名称和景区ID) + List routes = cpRouteMapper.listRoutesByIds(new ArrayList<>(routeIds)); + Map routeMap = routes.stream() + .collect(Collectors.toMap(CpRoute::getId, route -> route)); + + // 提取所有景区ID + Set scenicIds = routes.stream() + .map(CpRoute::getScenicId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + // 批量查询所有景区名称 + Map scenicNameMap = emScenicMapper.getScenicNameMap(new ArrayList<>(scenicIds)); + + // 组装结果 + for (Map.Entry entry : routeCountMap.entrySet()) { + Long routeId = entry.getKey(); + Long count = entry.getValue(); + + RouteStat stat = new RouteStat(); + stat.setRouteId(routeId); + stat.setCount(count); + + CpRoute route = routeMap.get(routeId); + if (route != null) { + stat.setRouteName(route.getName()); + + Long scenicId = route.getScenicId(); + if (scenicId != null) { + stat.setScenicName(scenicNameMap.getOrDefault(scenicId, "未知景区")); + } else { + stat.setScenicName("未知景区"); + } + } else { + stat.setRouteName("未知路线"); + stat.setScenicName("未知景区"); + } + + routeDistribution.add(stat); + } + } + return routeDistribution; } /** @@ -465,7 +530,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService { result.setPageInfo(new PageInfo()); result.setOrderList(Collections.emptyList()); result.setDroneModelDistribution(Collections.emptyMap()); - result.setRouteOrderDistribution(Collections.emptyMap()); + result.setRouteDistribution(Collections.emptyList()); result.setTimeSeriesData(null); return result; } diff --git a/aircraft-system/src/main/java/com/aircraft/modules/order/service/impl/OrderDetailAnalysisServiceImpl.java b/aircraft-system/src/main/java/com/aircraft/modules/order/service/impl/OrderDetailAnalysisServiceImpl.java new file mode 100644 index 0000000..468643c --- /dev/null +++ b/aircraft-system/src/main/java/com/aircraft/modules/order/service/impl/OrderDetailAnalysisServiceImpl.java @@ -0,0 +1,435 @@ +package com.aircraft.modules.order.service.impl; + +import com.aircraft.modules.aircraft.domain.AircraftDevice; +import com.aircraft.modules.order.controller.OrderAnalysisController; +import com.aircraft.modules.order.domain.OrderDetail; +import com.aircraft.modules.order.domain.OrderMain; +import com.aircraft.modules.order.domain.dto.*; +import com.aircraft.modules.order.mapper.OrderDetailMapper; +import com.aircraft.modules.order.service.OrderDetailAnalysisService; +import com.aircraft.modules.route.domain.CpRoute; +import com.aircraft.modules.route.mapper.CpRouteMapper; +import com.aircraft.modules.system.mapper.EmScenicMapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.poi.ss.formula.functions.T; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Service +public class OrderDetailAnalysisServiceImpl implements OrderDetailAnalysisService { + private static final Logger LOG = LoggerFactory.getLogger(OrderAnalysisController.class); + + // 日期格式化器(用于按日/月/季度/年分组) + private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); + private static final DateTimeFormatter QUARTER_FORMATTER = DateTimeFormatter.ofPattern("yyyy-Qq"); // 如2025-Q1 + private static final DateTimeFormatter YEAR_FORMATTER = DateTimeFormatter.ofPattern("yyyy"); + + // 订单状态 + private static final Integer STATUS_NOT_STARTED = 0;//未进行 + private static final Integer STATUS_PROCESSING = 1;//进行中 + private static final Integer STATUS_COMPLETED = 2;//已完成 + private static final Integer STATUS_FAILED = 3;//执行失败 + + @Autowired + private OrderDetailMapper orderDetailMapper; + @Autowired + private CpRouteMapper cpRouteMapper; + @Autowired + private EmScenicMapper emScenicMapper; + + @Override + public OrderDetailAnalysisResult analyzeAllOrderDetails(List allDetails) { + return buildOrderDetailAnalysisResult(allDetails); + } + + @Override + public OrderDetailAnalysisResult> analyzeByDayRange(LocalDate start, LocalDate end) { + // 1. 查询时间范围内的所有订单 + List orders = queryOrderDetailByDateRange(start, end); + if (CollectionUtils.isEmpty(orders)) { + return buildEmptyResult(); + } + + // 2. 按日分组统计每日订单量 + Map dailyCountMap = orders.stream() + .collect(Collectors.groupingBy( + order -> { + LocalDate localDate = order.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return localDate.format(DAY_FORMATTER); // 按日期字符串分组 + }, + Collectors.counting() // 统计每组数量 + )); + + System.out.println(dailyCountMap); + + // 3. 填充每日统计数据(确保日期连续性,即使某天无订单也显示0) + List dailyStats = new ArrayList<>(); + LocalDate current = start; + while (!current.isAfter(end)) { + String dateStr = current.format(DAY_FORMATTER); + DailyStat stat = new DailyStat(); + stat.setDate(dateStr); + stat.setDailyCount(dailyCountMap.getOrDefault(dateStr, 0L)); + dailyStats.add(stat); + current = current.plusDays(1); // 移至下一天 + } + + // 4. 构建并返回完整结果 + return buildAnalysisResult(orders, dailyStats); + } + + @Override + public OrderDetailAnalysisResult> analyzeByMonthRange(LocalDate start, LocalDate end) { + // 1. 查询时间范围内的所有订单 + List orders = queryOrderDetailByDateRange(start, end); + if (CollectionUtils.isEmpty(orders)) { + return buildEmptyResult(); + } + + // 2. 按月分组统计每月订单量 + Map monthlyCountMap = orders.stream() + .collect(Collectors.groupingBy( + order -> { + LocalDate localDate = order.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return localDate.format(MONTH_FORMATTER); + }, + Collectors.counting() + )); + + // 3. 填充每月统计数据(确保月份连续性) + List monthlyStats = new ArrayList<>(); + LocalDate current = start.withDayOfMonth(1); // 从当月第一天开始 + LocalDate endMonth = end.withDayOfMonth(1); // 结束月份第一天 + + while (!current.isAfter(endMonth)) { + String monthStr = current.format(MONTH_FORMATTER); + MonthlyStat stat = new MonthlyStat(); + stat.setMonth(monthStr); + stat.setMonthlyCount(monthlyCountMap.getOrDefault(monthStr, 0L)); + monthlyStats.add(stat); + current = current.plusMonths(1); // 移至下一月 + } + + // 4. 构建并返回完整结果 + return buildAnalysisResult(orders, monthlyStats); + } + + @Override + public OrderDetailAnalysisResult> analyzeByQuarterRange(LocalDate start, LocalDate end) { + // 1. 查询时间范围内的所有订单 + List orders = queryOrderDetailByDateRange(start, end); + if (CollectionUtils.isEmpty(orders)) { + return buildEmptyResult(); + } + + // 2. 按季度分组统计每季度订单量 + Map quarterCountMap = orders.stream() + .collect(Collectors.groupingBy( + order -> { + // 将 java.util.Date 转换为 java.time.YearMonth + YearMonth yearMonth = YearMonth.from(order.getCreateTime().toInstant().atZone(ZoneId.systemDefault())); + int quarter = (yearMonth.getMonthValue() - 1) / 3 + 1; + return yearMonth.format(YEAR_FORMATTER) + "-Q" + quarter; + }, + Collectors.counting() + )); + + // 3. 填充每季度统计数据(确保季度连续性) + List quarterlyStats = new ArrayList<>(); + LocalDate currentQuarterStart = start; + // 调整至当前季度第一天(如2025-05-10 → 2025-04-01) + currentQuarterStart = currentQuarterStart.minusMonths((currentQuarterStart.getMonthValue() - 1) % 3); + currentQuarterStart = currentQuarterStart.withDayOfMonth(1); + + while (!currentQuarterStart.isAfter(end)) { + // 计算当前季度标识(如2025-Q2) + int quarter = (currentQuarterStart.getMonthValue() - 1) / 3 + 1; + String quarterStr = currentQuarterStart.format(YEAR_FORMATTER) + "-Q" + quarter; + + QuarterlyStat stat = new QuarterlyStat(); + stat.setQuarter(quarterStr); + stat.setQuarterlyCount(quarterCountMap.getOrDefault(quarterStr, 0L)); + quarterlyStats.add(stat); + + // 移至下一季度第一天 + currentQuarterStart = currentQuarterStart.plusMonths(3); + } + + // 4. 构建并返回完整结果 + return buildAnalysisResult(orders, quarterlyStats); + } + + @Override + public OrderDetailAnalysisResult> analyzeByYearRange(LocalDate start, LocalDate end) { + // 1. 查询时间范围内的所有订单 + List orders = queryOrderDetailByDateRange(start, end); + if (CollectionUtils.isEmpty(orders)) { + return buildEmptyResult(); + } + + // 2. 按年分组统计每年订单量 + Map yearCountMap = orders.stream() + .collect(Collectors.groupingBy( + order -> { + LocalDate localDate = order.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return localDate.format(YEAR_FORMATTER); + }, + Collectors.counting() + )); + // 3. 填充每年统计数据(确保年份连续性) + List yearlyStats = new ArrayList<>(); + int startYear = start.getYear(); + int endYear = end.getYear(); + + for (int year = startYear; year <= endYear; year++) { + String yearStr = String.valueOf(year); + YearlyStat stat = new YearlyStat(); + stat.setYear(yearStr); + stat.setYearlyCount(yearCountMap.getOrDefault(yearStr, 0L)); + yearlyStats.add(stat); + } + + // 4. 构建并返回完整结果 + return buildAnalysisResult(orders, yearlyStats); + } + +// ====================== 通用工具方法 ====================== + + /** + * 根据日期范围查询订单 + */ + private List queryOrderDetailByDateRange(LocalDate start, LocalDate end) { + // MyBatis Plus 条件构造器:查询 create_time 在 [start, end] 范围内的订单 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.ge(OrderDetail::getCreateTime, start.atStartOfDay()) // 开始时间:当天00:00:00 + .le(OrderDetail::getCreateTime, end.atTime(23, 59, 59)); // 结束时间:当天23:59:59 + return orderDetailMapper.selectList(queryWrapper); + } + + /** + * 构建完整的订单分析结果 + */ + private OrderDetailAnalysisResult buildAnalysisResult(List orderDetails, T timeSeriesData) { + OrderDetailAnalysisResult result = buildOrderDetailAnalysisResult(orderDetails); + // 时间序列数据(日/月/季/年统计) + result.setTimeSeriesData(timeSeriesData); + return result; + } + + /** + * 处理任务分析结果 + */ + private OrderDetailAnalysisResult buildOrderDetailAnalysisResult(List orderDetails) { + OrderDetailAnalysisResult result = new OrderDetailAnalysisResult<>(); + + // 1. 基础统计指标 + long totalTaskCount = orderDetails.size(); + result.setTotalTaskCount(totalTaskCount); + + // 2. 按状态统计任务数量 + long notStartedCount = countTasksByStatus(orderDetails, STATUS_NOT_STARTED); + long processingCount = countTasksByStatus(orderDetails, STATUS_PROCESSING); + long completedCount = countTasksByStatus(orderDetails, STATUS_COMPLETED); + long failedCount = countTasksByStatus(orderDetails, STATUS_FAILED); + + // 3. 设置状态统计结果 + result.setNotStartedTaskCount(notStartedCount); + result.setProcessingTaskCount(processingCount); + result.setCompletedTaskCount(completedCount); + result.setFailedTaskCount(failedCount); + + // 4. 计算状态比率(保留两位小数) + BigDecimal total = BigDecimal.valueOf(totalTaskCount); + result.setNotStartedRate(calculateRate(notStartedCount, total)); + result.setProcessingRate(calculateRate(processingCount, total)); + result.setCompletionRate(calculateRate(completedCount, total)); + result.setFailedRate(calculateRate(failedCount, total)); + + // 5. 景区路线订单分布 + result.setRouteDistribution(calculateRouteDistribution(orderDetails)); + + // 6. 分页信息和订单列表(默认取第一页,每页10条) + PageInfo pageInfo = buildTaskPageInfo(orderDetails, 1, 10); + result.setPageInfo(pageInfo); + result.setOrderDetailList(getPagedTasks(orderDetails, pageInfo)); + + // 7. 时间序列数据(在调用此方法的上层设置) + result.setTimeSeriesData(null); // 由调用者设置具体时间序列数据 + + return result; + } + + /** + * 按状态统计任务数量 + */ + private long countTasksByStatus(List tasks, Integer status) { + return tasks.stream() + .filter(task -> status.equals(task.getOrderItemStatus())) + .count(); + } + + /** + * 计算比率(保留两位小数) + */ + private BigDecimal calculateRate(long count, BigDecimal total) { + if (total.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + return BigDecimal.valueOf(count) + .divide(total, 4, BigDecimal.ROUND_HALF_UP) // 中间计算保留4位小数 + .multiply(BigDecimal.valueOf(100)) // 转换为百分比 + .setScale(2, BigDecimal.ROUND_HALF_UP); // 最终结果保留两位小数 + } + + /** + * 构建任务分页信息 + */ + private PageInfo buildTaskPageInfo(List tasks, int pageNum, int pageSize) { + PageInfo pageInfo = new PageInfo(); + pageInfo.setPageNum(pageNum); + pageInfo.setPageSize(pageSize); + pageInfo.setTotal((long) tasks.size()); + return pageInfo; + } + + /** + * 获取分页后的任务列表 + */ + private List getPagedTasks(List tasks, PageInfo pageInfo) { + int start = (pageInfo.getPageNum() - 1) * pageInfo.getPageSize(); + int end = Math.min(start + pageInfo.getPageSize(), tasks.size()); + return tasks.subList(start, end); + } + + /** + * 将列表按指定大小分割为多个子列表 + * + * @param list 原始列表 + * @param batchSize 每个子列表的最大大小 + * @return 子列表集合 + */ + private List> partitionList(List list, int batchSize) { + if (list == null || list.isEmpty()) { + return Collections.emptyList(); + } + + return IntStream.range(0, (list.size() + batchSize - 1) / batchSize) + .mapToObj(i -> list.subList(i * batchSize, Math.min((i + 1) * batchSize, list.size()))) + .collect(Collectors.toList()); + } + + /** + * 计算景区内各路线的任务分布 + */ + public List calculateRouteDistribution(List orderDetails) { + // 1. 校验输入 + if (CollectionUtils.isEmpty(orderDetails)) { + return Collections.emptyList(); + } + + // 2. 统计各路线的任务数量(基于子单的routeId字段) + Map routeCountMap = orderDetails.stream() + .filter(detail -> detail.getRouteId() != null) // 过滤掉无路线ID的任务 + .collect(Collectors.groupingBy( + OrderDetail::getRouteId, // 按路线ID分组 + Collectors.counting() // 统计每组数量 + )); + + // 3. 批量查询路线信息(名称、所属景区) + List result = new ArrayList<>(); + if (!routeCountMap.isEmpty()) { + // 提取所有路线ID + ArrayList routeIds = new ArrayList<>(routeCountMap.keySet()); + + // 批量查询路线信息(名称、景区ID) + List routes = cpRouteMapper.listRoutesByIds(routeIds); + Map routeInfoMap = routes.stream() + .collect(Collectors.toMap(CpRoute::getId, route -> route)); + + // 提取所有景区ID(用于批量查询景区名称) + Set scenicIds = routes.stream() + .map(CpRoute::getScenicId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + // 批量查询景区名称 + Map scenicNameMap = emScenicMapper.getScenicNameMap(new ArrayList<>(scenicIds)); + + // 4. 组装结果 + for (Map.Entry entry : routeCountMap.entrySet()) { + Long routeId = entry.getKey(); + Long taskCount = entry.getValue(); + + RouteStat stat = new RouteStat(); + stat.setRouteId(routeId); + stat.setCount(taskCount); + + // 设置路线名称和景区名称 + CpRoute route = routeInfoMap.get(routeId); + if (route != null) { + stat.setRouteName(route.getName()); + Long scenicId = route.getScenicId(); + if (scenicId != null) { + stat.setScenicName(scenicNameMap.getOrDefault(scenicId, "未知景区")); + } else { + stat.setScenicName("未知景区"); + } + } else { + // 路线不存在的情况 + stat.setRouteName("未知路线"); + stat.setScenicName("未知景区"); + } + + result.add(stat); + } + } + + return result; + } + + /** + * 构建空结果(无任务时) + */ + private OrderDetailAnalysisResult buildEmptyResult() { + OrderDetailAnalysisResult result = new OrderDetailAnalysisResult<>(); + + // 任务统计指标 + result.setTotalTaskCount(0L); + + // 分项状态计数 + result.setNotStartedTaskCount(0L); + result.setProcessingTaskCount(0L); + result.setCompletedTaskCount(0L); + result.setFailedTaskCount(0L); + + // 状态比率 + result.setNotStartedRate(BigDecimal.ZERO); + result.setProcessingRate(BigDecimal.ZERO); + result.setCompletionRate(BigDecimal.ZERO); + result.setFailedRate(BigDecimal.ZERO); + + // 路线分布、分页和时间序列数据 + result.setRouteDistribution(Collections.emptyList()); + result.setPageInfo(new PageInfo()); + result.setOrderDetailList(Collections.emptyList()); + result.setTimeSeriesData(null); + + return result; + } + +} diff --git a/aircraft-system/src/main/java/com/aircraft/modules/route/controller/CpRouteController.java b/aircraft-system/src/main/java/com/aircraft/modules/route/controller/CpRouteController.java index 18d5de9..a978219 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/route/controller/CpRouteController.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/route/controller/CpRouteController.java @@ -5,6 +5,7 @@ import com.aircraft.modules.route.domain.dto.CpRouteVo; import com.aircraft.modules.route.mapper.CpRouteMapper; import com.aircraft.modules.system.mapper.UserMapper; import com.aircraft.modules.route.service.CpRouteService; +import com.aircraft.utils.StringUtils; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; @@ -92,7 +93,7 @@ public class CpRouteController { @ApiOperation(value = "查询单个路线") @RequestMapping(value = "{id}", method = {RequestMethod.GET}) @ApiImplicitParam(name = "id", value = "路线ID", required = true, paramType = "path") // 调整参数描述 - public ResponseEntity one(@PathVariable final Integer id) { + public ResponseEntity one(@PathVariable final Long id) { try { CpRoute entity = cpRouteMapper.getRouteById(id); if (entity == null || entity.getDelFlag() == 1) { @@ -239,4 +240,40 @@ public class CpRouteController { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + + @ApiOperation(value = "根据景区名称模糊查询路线") + @RequestMapping(value = "searchRoutesByName", method = RequestMethod.GET) + public ResponseEntity> searchRoutesByName( + @ApiParam(value = "景区名称(支持模糊匹配)", required = true) + @RequestParam("areaName") String areaName) { + + try { + // 1. 参数校验 + if (StringUtils.isBlank(areaName)) { + LOG.warn("景区名称模糊查询失败:景区名称为空"); + return ResponseEntity.badRequest().build(); + } + + // 2. 构建查询条件(仅针对景区名称进行模糊匹配) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.like("area_name", areaName) + .eq("del_flag", 0); + + // 3. 查询匹配的路线 + List routes = entityService.list(queryWrapper); + + // 4. 返回结果 + if (CollectionUtils.isEmpty(routes)) { + LOG.info("未找到景区名称包含'{}'的路线数据", areaName); + return ResponseEntity.ok(Collections.emptyList()); + } + + LOG.info("景区名称模糊查询成功,关键词='{}',共找到{}条路线", areaName, routes.size()); + return ResponseEntity.ok(routes); + + } catch (Exception e) { + LOG.error("景区名称模糊查询失败,关键词={}", areaName, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } } \ No newline at end of file diff --git a/aircraft-system/src/main/java/com/aircraft/modules/route/mapper/CpRouteMapper.java b/aircraft-system/src/main/java/com/aircraft/modules/route/mapper/CpRouteMapper.java index e251aca..c67b4b4 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/route/mapper/CpRouteMapper.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/route/mapper/CpRouteMapper.java @@ -4,6 +4,9 @@ import com.aircraft.modules.route.domain.CpRoute; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; +import java.util.ArrayList; +import java.util.List; + /** *

* 路线表 Mapper 接口 @@ -17,7 +20,9 @@ public interface CpRouteMapper extends BaseMapper { void updateDelFlagById(Long id, Integer delFlag); - CpRoute getRouteById(Integer id); + CpRoute getRouteById(Long id); String getNameById(Long routeId); + + List listRoutesByIds(ArrayList longs); } diff --git a/aircraft-system/src/main/java/com/aircraft/modules/system/mapper/EmScenicMapper.java b/aircraft-system/src/main/java/com/aircraft/modules/system/mapper/EmScenicMapper.java index a1ac638..d64d517 100644 --- a/aircraft-system/src/main/java/com/aircraft/modules/system/mapper/EmScenicMapper.java +++ b/aircraft-system/src/main/java/com/aircraft/modules/system/mapper/EmScenicMapper.java @@ -6,7 +6,9 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import io.lettuce.core.dynamic.annotation.Param; import org.apache.ibatis.annotations.Mapper; +import java.util.ArrayList; import java.util.List; +import java.util.Map; /** *

@@ -32,4 +34,6 @@ public interface EmScenicMapper extends BaseMapper { * @return */ List countByAreaId(); + + Map getScenicNameMap(ArrayList longs); } diff --git a/aircraft-system/src/main/resources/mapper/EmScenicMapper.xml b/aircraft-system/src/main/resources/mapper/EmScenicMapper.xml index 49610c0..f4ec653 100644 --- a/aircraft-system/src/main/resources/mapper/EmScenicMapper.xml +++ b/aircraft-system/src/main/resources/mapper/EmScenicMapper.xml @@ -26,4 +26,14 @@ where valid = 't' group by area_id + diff --git a/aircraft-system/src/main/resources/mapper/order/OrderDetailMapper.xml b/aircraft-system/src/main/resources/mapper/order/OrderDetailMapper.xml index be36b43..90c422e 100644 --- a/aircraft-system/src/main/resources/mapper/order/OrderDetailMapper.xml +++ b/aircraft-system/src/main/resources/mapper/order/OrderDetailMapper.xml @@ -6,6 +6,8 @@ + + @@ -16,12 +18,13 @@ - - + SELECT + FROM fms_od_order_detail WHERE order_id IN #{id} + AND del_flag = 0 ORDER BY order_id ASC, id ASC - - + \ No newline at end of file diff --git a/aircraft-system/src/main/resources/mapper/route/CpRouteMapper.xml b/aircraft-system/src/main/resources/mapper/route/CpRouteMapper.xml index 5e849cc..bab1726 100644 --- a/aircraft-system/src/main/resources/mapper/route/CpRouteMapper.xml +++ b/aircraft-system/src/main/resources/mapper/route/CpRouteMapper.xml @@ -19,4 +19,15 @@ FROM cp_route WHERE id = #{routeId} +