文章增加字段+任务分析

This commit is contained in:
温文静WWW 2025-07-15 12:48:55 +08:00
parent a1445e792d
commit 16fc1183b9
24 changed files with 972 additions and 100 deletions

View File

@ -27,7 +27,6 @@ import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@RequestMapping("/aircraft/device") @RequestMapping("/aircraft/device")
public class AircraftDeviceController { public class AircraftDeviceController {

View File

@ -2,7 +2,10 @@ package com.aircraft.modules.article.controller;
import com.aircraft.modules.article.domain.CpArticle; 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.CpText;
import com.aircraft.modules.article.domain.dto.CpArticleDTO;
import com.aircraft.modules.article.domain.dto.CpArticleVo; import com.aircraft.modules.article.domain.dto.CpArticleVo;
import com.aircraft.modules.article.domain.dto.CpLabelVo; import com.aircraft.modules.article.domain.dto.CpLabelVo;
import com.aircraft.modules.article.mapper.CpArticleMapper; import com.aircraft.modules.article.mapper.CpArticleMapper;
@ -17,6 +20,7 @@ import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -27,6 +31,7 @@ import org.springframework.web.bind.annotation.*;
import javax.validation.Valid; import javax.validation.Valid;
import java.net.URI; import java.net.URI;
import java.sql.Timestamp;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
@ -109,7 +114,7 @@ public class CpArticleController {
@ApiOperation(value = "添加文章") @ApiOperation(value = "添加文章")
@RequestMapping(method = {RequestMethod.POST}) @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 { try {
// 处理参数校验错误 // 处理参数校验错误
if (result.hasErrors()) { if (result.hasErrors()) {
@ -130,45 +135,48 @@ public class CpArticleController {
} }
// 查找标签信息 // 查找标签信息
CpLabelVo cpLabelVo = cpLabelMapper.findByName(cpArticleVo.getLabelName()); CpLabel cpLabel = cpLabelMapper.findById(cpArticleDTO.getLabelId());
if (cpLabelVo == null) { if (cpLabel == null) {
// 标签不存在时返回明确错误信息 // 标签不存在时返回明确错误信息
Map<String, Object> error = new HashMap<>(); Map<String, Object> error = new HashMap<>();
error.put("code", 400); error.put("code", 400);
error.put("msg", "标签不存在:标签名=" + cpArticleVo.getLabelName()); error.put("msg", "标签不存在:标签名=" + cpArticleDTO.getLabelId());
return ResponseEntity.badRequest().body(error); return ResponseEntity.badRequest().body(error);
} }
// 关联标签与模块关键步骤
cpLabel.setModuleId(cpArticleDTO.getModuleId());
cpLabelMapper.updateById(cpLabel); // 更新标签的模块关联
CpArticle cpArticle = new CpArticle();
BeanUtils.copyProperties(cpArticleDTO, cpArticle);
// 设置文章基础信息 // 设置文章基础信息
cpArticleVo.setCreateTime(LocalDateTime.now()); cpArticle.setCreateTime(Timestamp.valueOf(LocalDateTime.now()));
cpArticleVo.setCheckState("w");// 默认未审核 cpArticle.setCheckState("t");// 默认已审核
cpArticleVo.setDelFlag(0); cpArticle.setDelFlag(0);
cpArticleVo.setCplabelId(cpLabelVo.getId());// 关联标签ID cpArticle.setCplabelId(cpLabel.getId());// 关联标签ID
Long authorId = getCurrentUserId(); Long authorId = getCurrentUserId();
// 设置作者ID // 设置作者ID
cpArticleVo.setAuthorId(authorId); cpArticle.setAuthorId(authorId);
entityService.save(cpArticleVo); entityService.save(cpArticle);
// 保存文章主信息
entityService.save(cpArticleVo);
// 保存文章内容 // 保存文章内容
CpText cpText = new CpText(); CpText cpText = new CpText();
cpText.setText(cpArticleVo.getText()); cpText.setText(cpArticleDTO.getText());
cpTextService.save(cpText); cpTextService.save(cpText);
// 回填文章内容ID并更新 // 回填文章内容ID并更新
cpArticleVo.setTextid(cpText.getId()); cpArticle.setTextid(cpText.getId());
entityService.updateById(cpArticleVo); entityService.updateById(cpArticle);
// 返回创建成功的响应 // 返回创建成功的响应
Map<String, Object> success = new HashMap<>(); Map<String, Object> success = new HashMap<>();
success.put("code", 201); success.put("code", 201);
success.put("msg", "文章创建成功"); success.put("msg", "文章创建成功");
success.put("data", cpArticleVo); success.put("data", cpArticle);
return ResponseEntity return ResponseEntity
.created(URI.create("/cpArticle/" + cpArticleVo.getId())) .created(URI.create("/cpArticle/" + cpArticle.getId()))
.body(success); .body(success);
} catch (Exception e) { } catch (Exception e) {
LOG.error("添加文章失败", e); LOG.error("添加文章失败", e);

View File

@ -21,7 +21,7 @@ import java.time.LocalDateTime;
*/ */
@Data @Data
@TableName("cp_article") @TableName("cp_article")
public class CpArticle { public class CpArticle extends BaseEntity {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -47,16 +47,13 @@ public class CpArticle {
@ApiModelProperty(value = "类型id关联标签表id") @ApiModelProperty(value = "类型id关联标签表id")
private Integer cplabelId; private Integer cplabelId;
@ApiModelProperty(value = "创建时间") @ApiModelProperty(value = "是否置顶1-置顶0-不置顶")
private LocalDateTime createTime; private Integer top;
@ApiModelProperty(value = "发布者id用户表或管理员表由发布者类型决定") @ApiModelProperty(value = "发布者id用户表或管理员表由发布者类型决定")
private Long authorId; private Long authorId;
@ApiModelProperty(value = "审核状态t-审核通过f-不通过w-未审核") @ApiModelProperty(value = "审核状态t-审核通过f-不通过w-未审核")
private String checkState; private String checkState = "t";
@ApiModelProperty(value = "删除标记“1”已删除“0”正常")
private Integer delFlag;
} }

View File

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

View File

@ -24,4 +24,7 @@ public interface CpLabelMapper extends BaseMapper<CpLabel> {
List<CpLabelVo> selectLabelTreeByModuleId(Integer moduleId); List<CpLabelVo> selectLabelTreeByModuleId(Integer moduleId);
void updateDelFlagById(Integer id, Integer delFlag); void updateDelFlagById(Integer id, Integer delFlag);
@Select("SELECT * FROM cp_label WHERE id = #{labelId}")
CpLabelVo findById(int labelId);
} }

View File

@ -2,7 +2,6 @@ package com.aircraft.modules.order.controller;
import com.aircraft.modules.order.domain.OrderMain; import com.aircraft.modules.order.domain.OrderMain;
import com.aircraft.modules.order.domain.dto.*; 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.IOrderMainService;
import com.aircraft.modules.order.service.OrderAnalysisService; import com.aircraft.modules.order.service.OrderAnalysisService;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; 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.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -69,7 +66,7 @@ public class OrderAnalysisController {
@ApiOperation("按日范围分析订单") @ApiOperation("按日范围分析订单")
@GetMapping("/day") @GetMapping("/day")
//额外返回传入时间范围内的每日订单总量 //额外返回传入时间范围内的每日订单总量
public ResponseEntity<OrderAnalysisResult<List<OrderDailyStat>>> analyzeByDayRange( public ResponseEntity<OrderAnalysisResult<List<DailyStat>>> analyzeByDayRange(
@ApiParam(value = "开始日期yyyy-MM-dd", required = true, example = "2025-01-01") @ApiParam(value = "开始日期yyyy-MM-dd", required = true, example = "2025-01-01")
@RequestParam("startDate") String startDate, @RequestParam("startDate") String startDate,
@ApiParam(value = "结束日期yyyy-MM-dd", required = true, example = "2025-01-31") @ApiParam(value = "结束日期yyyy-MM-dd", required = true, example = "2025-01-31")
@ -86,7 +83,7 @@ public class OrderAnalysisController {
} }
// 调用服务层 // 调用服务层
OrderAnalysisResult<List<OrderDailyStat>> result = orderAnalysisService.analyzeByDayRange(start, end); OrderAnalysisResult<List<DailyStat>> result = orderAnalysisService.analyzeByDayRange(start, end);
LOG.info("按日分析完成,时间范围:{}至{}", startDate, endDate); LOG.info("按日分析完成,时间范围:{}至{}", startDate, endDate);
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
} catch (Exception e) { } catch (Exception e) {
@ -98,7 +95,7 @@ public class OrderAnalysisController {
@ApiOperation("按月范围分析订单") @ApiOperation("按月范围分析订单")
@GetMapping("/month") @GetMapping("/month")
//传入的时间范围某年的某月到某年的某月额外返回时间范围内的每月总订单量 //传入的时间范围某年的某月到某年的某月额外返回时间范围内的每月总订单量
public ResponseEntity<OrderAnalysisResult<List<OrderMonthlyStat>>> analyzeByMonthRange( public ResponseEntity<OrderAnalysisResult<List<MonthlyStat>>> analyzeByMonthRange(
@ApiParam(value = "开始月份yyyy-MM", required = true, example = "2025-01") @ApiParam(value = "开始月份yyyy-MM", required = true, example = "2025-01")
@RequestParam("startDate") String startDate, @RequestParam("startDate") String startDate,
@ApiParam(value = "结束月份yyyy-MM", required = true, example = "2025-03") @ApiParam(value = "结束月份yyyy-MM", required = true, example = "2025-03")
@ -117,7 +114,7 @@ public class OrderAnalysisController {
} }
// 调用服务层 // 调用服务层
OrderAnalysisResult<List<OrderMonthlyStat>> result = orderAnalysisService.analyzeByMonthRange(start, end); OrderAnalysisResult<List<MonthlyStat>> result = orderAnalysisService.analyzeByMonthRange(start, end);
LOG.info("按月分析完成,时间范围:{}至{}", startDate, endDate); LOG.info("按月分析完成,时间范围:{}至{}", startDate, endDate);
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
} catch (Exception e) { } catch (Exception e) {
@ -129,7 +126,7 @@ public class OrderAnalysisController {
@ApiOperation("按季度范围分析订单") @ApiOperation("按季度范围分析订单")
@GetMapping("/quarter") @GetMapping("/quarter")
//传入的时间范围某年的某季到某年的某季yyyy-Qn (: 2025-Q1)额外返回时间范围内的每季总订单量 //传入的时间范围某年的某季到某年的某季yyyy-Qn (: 2025-Q1)额外返回时间范围内的每季总订单量
public ResponseEntity<OrderAnalysisResult<List<OrderQuarterlyStat>>> analyzeByQuarterRange( public ResponseEntity<OrderAnalysisResult<List<QuarterlyStat>>> analyzeByQuarterRange(
@ApiParam(value = "开始季度yyyy-Qn2025-Q1", required = true) @ApiParam(value = "开始季度yyyy-Qn2025-Q1", required = true)
@RequestParam("startDate") String startDate, @RequestParam("startDate") String startDate,
@ApiParam(value = "结束季度yyyy-Qn2025-Q4", required = true) @ApiParam(value = "结束季度yyyy-Qn2025-Q4", required = true)
@ -161,7 +158,7 @@ public class OrderAnalysisController {
} }
// 调用服务层 // 调用服务层
OrderAnalysisResult<List<OrderQuarterlyStat>> result = orderAnalysisService.analyzeByQuarterRange(start, end); OrderAnalysisResult<List<QuarterlyStat>> result = orderAnalysisService.analyzeByQuarterRange(start, end);
LOG.info("按季度分析完成,时间范围:{}至{}", startDate, endDate); LOG.info("按季度分析完成,时间范围:{}至{}", startDate, endDate);
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
} catch (Exception e) { } catch (Exception e) {
@ -174,7 +171,7 @@ public class OrderAnalysisController {
@ApiOperation("按年范围分析订单") @ApiOperation("按年范围分析订单")
@GetMapping("/year") @GetMapping("/year")
//传入的时间范围某年到某年额外返回时间范围内的每年总订单量 //传入的时间范围某年到某年额外返回时间范围内的每年总订单量
public ResponseEntity<OrderAnalysisResult<List<OrderYearlyStat>>> analyzeByYearRange( public ResponseEntity<OrderAnalysisResult<List<YearlyStat>>> analyzeByYearRange(
@ApiParam(value = "开始年份yyyy", required = true, example = "2023") @ApiParam(value = "开始年份yyyy", required = true, example = "2023")
@RequestParam("startDate") String startDate, @RequestParam("startDate") String startDate,
@ApiParam(value = "结束年份yyyy", required = true, example = "2025") @ApiParam(value = "结束年份yyyy", required = true, example = "2025")
@ -191,7 +188,7 @@ public class OrderAnalysisController {
} }
// 调用服务层 // 调用服务层
OrderAnalysisResult<List<OrderYearlyStat>> result = orderAnalysisService.analyzeByYearRange(start, end); OrderAnalysisResult<List<YearlyStat>> result = orderAnalysisService.analyzeByYearRange(start, end);
LOG.info("按年分析完成,时间范围:{}至{}", startDate, endDate); LOG.info("按年分析完成,时间范围:{}至{}", startDate, endDate);
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
} catch (Exception e) { } catch (Exception e) {

View File

@ -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<OrderDetailAnalysisResult<T>> analyzeAllOrderDetails() {
try {
// 1. 获取所有子单数据
List<OrderDetail> allDetails = orderDetailService.list();
// 2. 校验数据
if (CollectionUtils.isEmpty(allDetails)) {
LOG.info("全部子单分析:未找到子单数据");
return ResponseEntity.badRequest().body(null);
}
OrderDetailAnalysisResult<T> 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<OrderDetailAnalysisResult<List<DailyStat>>> 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<List<DailyStat>> 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<OrderDetailAnalysisResult<List<MonthlyStat>>> 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<List<MonthlyStat>> 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<OrderDetailAnalysisResult<List<QuarterlyStat>>> analyzeByQuarterRange(
@ApiParam(value = "开始季度yyyy-Qn2025-Q1", required = true)
@RequestParam("startDate") String startDate,
@ApiParam(value = "结束季度yyyy-Qn2025-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<List<QuarterlyStat>> 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<OrderDetailAnalysisResult<List<YearlyStat>>> 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<List<YearlyStat>> 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());
}
}

View File

@ -2,9 +2,9 @@ package com.aircraft.modules.order.domain.dto;
import lombok.Data; import lombok.Data;
//每日的订单总量统计实体类 //总量统计实体类
@Data @Data
public class OrderDailyStat { public class DailyStat {
private String date; private String date;
private Long orderCount; private Long dailyCount;//每日数量
} }

View File

@ -3,7 +3,7 @@ package com.aircraft.modules.order.domain.dto;
import lombok.Data; import lombok.Data;
@Data @Data
public class OrderMonthlyStat { public class MonthlyStat {
private String month; private String month;
private Long orderCount; private Long monthlyCount;//每月数量
} }

View File

@ -18,7 +18,8 @@ public class OrderAnalysisResult<T> {
private PageInfo pageInfo; // 分页信息 private PageInfo pageInfo; // 分页信息
private List<?> orderList; // 订单列表 private List<?> orderList; // 订单列表
private Map<String, Double> droneModelDistribution; // 无人机型号订单占比 private Map<String, Double> droneModelDistribution; // 无人机型号订单占比
private Map<String, Long> routeOrderDistribution; // 不同区域的订单总量 private List<RouteStat> routeDistribution;//景区-路线的订单分布
// private Map<String, Long> routeOrderDistribution; // 不同区域的订单总量
private T timeSeriesData; // 时间序列数据//年统计 private T timeSeriesData; // 时间序列数据//年统计
} }

View File

@ -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<T> {
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<RouteStat> routeDistribution; // 景区路线统计分布
private PageInfo pageInfo; // 分页信息
private List<?> orderDetailList; // 子单列表分页
private T timeSeriesData; // 时间序列数据//年统计
}

View File

@ -3,7 +3,7 @@ package com.aircraft.modules.order.domain.dto;
import lombok.Data; import lombok.Data;
@Data @Data
public class OrderQuarterlyStat { public class QuarterlyStat {
private String quarter; // 季度标识如2025-Q1 private String quarter; // 季度标识如2025-Q1
private Long orderCount; // 本季度订单总 private Long QuarterlyCount; // 每季度数
} }

View File

@ -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;//某路线的订单/任务数量
}

View File

@ -3,7 +3,7 @@ package com.aircraft.modules.order.domain.dto;
import lombok.Data; import lombok.Data;
@Data @Data
public class OrderYearlyStat { public class YearlyStat {
private String year; private String year;
private Long orderCount; private Long yearlyCount;
} }

View File

@ -3,7 +3,6 @@ package com.aircraft.modules.order.service;
import com.aircraft.modules.order.domain.OrderMain; import com.aircraft.modules.order.domain.OrderMain;
import com.aircraft.modules.order.domain.dto.*; import com.aircraft.modules.order.domain.dto.*;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDate; import java.time.LocalDate;
@ -11,13 +10,13 @@ import java.util.List;
@Service @Service
public interface OrderAnalysisService { public interface OrderAnalysisService {
OrderAnalysisResult<List<OrderDailyStat>> analyzeByDayRange(LocalDate start, LocalDate end); OrderAnalysisResult<List<DailyStat>> analyzeByDayRange(LocalDate start, LocalDate end);
OrderAnalysisResult<List<OrderMonthlyStat>> analyzeByMonthRange(LocalDate start, LocalDate end); OrderAnalysisResult<List<MonthlyStat>> analyzeByMonthRange(LocalDate start, LocalDate end);
OrderAnalysisResult<List<OrderQuarterlyStat>> analyzeByQuarterRange(LocalDate start, LocalDate end); OrderAnalysisResult<List<QuarterlyStat>> analyzeByQuarterRange(LocalDate start, LocalDate end);
OrderAnalysisResult<List<OrderYearlyStat>> analyzeByYearRange(LocalDate start, LocalDate end); OrderAnalysisResult<List<YearlyStat>> analyzeByYearRange(LocalDate start, LocalDate end);
<T> OrderAnalysisResult<T> analyzeAllOrders(List<OrderMain> allOrders); <T> OrderAnalysisResult<T> analyzeAllOrders(List<OrderMain> allOrders);
} }

View File

@ -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<T> analyzeAllOrderDetails(List<OrderDetail> allDetails);
OrderDetailAnalysisResult<List<DailyStat>> analyzeByDayRange(LocalDate start, LocalDate end);
OrderDetailAnalysisResult<List<MonthlyStat>> analyzeByMonthRange(LocalDate start, LocalDate end);
OrderDetailAnalysisResult<List<QuarterlyStat>> analyzeByQuarterRange(LocalDate start, LocalDate end);
OrderDetailAnalysisResult<List<YearlyStat>> analyzeByYearRange(LocalDate start, LocalDate end);
}

View File

@ -2,15 +2,20 @@ package com.aircraft.modules.order.service.impl;
import com.aircraft.modules.aircraft.domain.AircraftDevice; import com.aircraft.modules.aircraft.domain.AircraftDevice;
import com.aircraft.modules.aircraft.mapper.AircraftDeviceMapper; 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.OrderDetail;
import com.aircraft.modules.order.domain.OrderMain; import com.aircraft.modules.order.domain.OrderMain;
import com.aircraft.modules.order.domain.dto.*; import com.aircraft.modules.order.domain.dto.*;
import com.aircraft.modules.order.mapper.OrderDetailMapper; import com.aircraft.modules.order.mapper.OrderDetailMapper;
import com.aircraft.modules.order.mapper.OrderMainMapper; import com.aircraft.modules.order.mapper.OrderMainMapper;
import com.aircraft.modules.order.service.OrderAnalysisService; 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.route.mapper.CpRouteMapper;
import com.aircraft.modules.system.mapper.EmScenicMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -36,6 +41,9 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
private OrderDetailMapper orderDetailMapper; private OrderDetailMapper orderDetailMapper;
@Autowired @Autowired
private CpRouteMapper cpRouteMapper; 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"); private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@ -54,7 +62,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
* 按日范围分析订单 * 按日范围分析订单
*/ */
@Override @Override
public OrderAnalysisResult<List<OrderDailyStat>> analyzeByDayRange(LocalDate start, LocalDate end) { public OrderAnalysisResult<List<DailyStat>> analyzeByDayRange(LocalDate start, LocalDate end) {
// 1. 查询时间范围内的所有订单 // 1. 查询时间范围内的所有订单
List<OrderMain> orders = queryOrdersByDateRange(start, end); List<OrderMain> orders = queryOrdersByDateRange(start, end);
if (CollectionUtils.isEmpty(orders)) { if (CollectionUtils.isEmpty(orders)) {
@ -74,13 +82,13 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
System.out.println(dailyCountMap); System.out.println(dailyCountMap);
// 3. 填充每日统计数据确保日期连续性即使某天无订单也显示0 // 3. 填充每日统计数据确保日期连续性即使某天无订单也显示0
List<OrderDailyStat> dailyStats = new ArrayList<>(); List<DailyStat> dailyStats = new ArrayList<>();
LocalDate current = start; LocalDate current = start;
while (!current.isAfter(end)) { while (!current.isAfter(end)) {
String dateStr = current.format(DAY_FORMATTER); String dateStr = current.format(DAY_FORMATTER);
OrderDailyStat stat = new OrderDailyStat(); DailyStat stat = new DailyStat();
stat.setDate(dateStr); stat.setDate(dateStr);
stat.setOrderCount(dailyCountMap.getOrDefault(dateStr, 0L)); stat.setDailyCount(dailyCountMap.getOrDefault(dateStr, 0L));
dailyStats.add(stat); dailyStats.add(stat);
current = current.plusDays(1); // 移至下一天 current = current.plusDays(1); // 移至下一天
} }
@ -94,7 +102,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
* 按月范围分析订单 * 按月范围分析订单
*/ */
@Override @Override
public OrderAnalysisResult<List<OrderMonthlyStat>> analyzeByMonthRange(LocalDate start, LocalDate end) { public OrderAnalysisResult<List<MonthlyStat>> analyzeByMonthRange(LocalDate start, LocalDate end) {
// 1. 查询时间范围内的所有订单 // 1. 查询时间范围内的所有订单
List<OrderMain> orders = queryOrdersByDateRange(start, end); List<OrderMain> orders = queryOrdersByDateRange(start, end);
if (CollectionUtils.isEmpty(orders)) { if (CollectionUtils.isEmpty(orders)) {
@ -112,15 +120,15 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
)); ));
// 3. 填充每月统计数据确保月份连续性 // 3. 填充每月统计数据确保月份连续性
List<OrderMonthlyStat> monthlyStats = new ArrayList<>(); List<MonthlyStat> monthlyStats = new ArrayList<>();
LocalDate current = start.withDayOfMonth(1); // 从当月第一天开始 LocalDate current = start.withDayOfMonth(1); // 从当月第一天开始
LocalDate endMonth = end.withDayOfMonth(1); // 结束月份第一天 LocalDate endMonth = end.withDayOfMonth(1); // 结束月份第一天
while (!current.isAfter(endMonth)) { while (!current.isAfter(endMonth)) {
String monthStr = current.format(MONTH_FORMATTER); String monthStr = current.format(MONTH_FORMATTER);
OrderMonthlyStat stat = new OrderMonthlyStat(); MonthlyStat stat = new MonthlyStat();
stat.setMonth(monthStr); stat.setMonth(monthStr);
stat.setOrderCount(monthlyCountMap.getOrDefault(monthStr, 0L)); stat.setMonthlyCount(monthlyCountMap.getOrDefault(monthStr, 0L));
monthlyStats.add(stat); monthlyStats.add(stat);
current = current.plusMonths(1); // 移至下一月 current = current.plusMonths(1); // 移至下一月
} }
@ -134,7 +142,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
* 按季度范围分析订单 * 按季度范围分析订单
*/ */
@Override @Override
public OrderAnalysisResult<List<OrderQuarterlyStat>> analyzeByQuarterRange(LocalDate start, LocalDate end) { public OrderAnalysisResult<List<QuarterlyStat>> analyzeByQuarterRange(LocalDate start, LocalDate end) {
// 1. 查询时间范围内的所有订单 // 1. 查询时间范围内的所有订单
List<OrderMain> orders = queryOrdersByDateRange(start, end); List<OrderMain> orders = queryOrdersByDateRange(start, end);
if (CollectionUtils.isEmpty(orders)) { if (CollectionUtils.isEmpty(orders)) {
@ -154,7 +162,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
)); ));
// 3. 填充每季度统计数据确保季度连续性 // 3. 填充每季度统计数据确保季度连续性
List<OrderQuarterlyStat> quarterlyStats = new ArrayList<>(); List<QuarterlyStat> quarterlyStats = new ArrayList<>();
LocalDate currentQuarterStart = start; LocalDate currentQuarterStart = start;
// 调整至当前季度第一天如2025-05-10 2025-04-01 // 调整至当前季度第一天如2025-05-10 2025-04-01
currentQuarterStart = currentQuarterStart.minusMonths((currentQuarterStart.getMonthValue() - 1) % 3); currentQuarterStart = currentQuarterStart.minusMonths((currentQuarterStart.getMonthValue() - 1) % 3);
@ -165,9 +173,9 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
int quarter = (currentQuarterStart.getMonthValue() - 1) / 3 + 1; int quarter = (currentQuarterStart.getMonthValue() - 1) / 3 + 1;
String quarterStr = currentQuarterStart.format(YEAR_FORMATTER) + "-Q" + quarter; String quarterStr = currentQuarterStart.format(YEAR_FORMATTER) + "-Q" + quarter;
OrderQuarterlyStat stat = new OrderQuarterlyStat(); QuarterlyStat stat = new QuarterlyStat();
stat.setQuarter(quarterStr); stat.setQuarter(quarterStr);
stat.setOrderCount(quarterCountMap.getOrDefault(quarterStr, 0L)); stat.setQuarterlyCount(quarterCountMap.getOrDefault(quarterStr, 0L));
quarterlyStats.add(stat); quarterlyStats.add(stat);
// 移至下一季度第一天 // 移至下一季度第一天
@ -183,7 +191,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
* 按年范围分析订单 * 按年范围分析订单
*/ */
@Override @Override
public OrderAnalysisResult<List<OrderYearlyStat>> analyzeByYearRange(LocalDate start, LocalDate end) { public OrderAnalysisResult<List<YearlyStat>> analyzeByYearRange(LocalDate start, LocalDate end) {
// 1. 查询时间范围内的所有订单 // 1. 查询时间范围内的所有订单
List<OrderMain> orders = queryOrdersByDateRange(start, end); List<OrderMain> orders = queryOrdersByDateRange(start, end);
if (CollectionUtils.isEmpty(orders)) { if (CollectionUtils.isEmpty(orders)) {
@ -200,15 +208,15 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
Collectors.counting() Collectors.counting()
)); ));
// 3. 填充每年统计数据确保年份连续性 // 3. 填充每年统计数据确保年份连续性
List<OrderYearlyStat> yearlyStats = new ArrayList<>(); List<YearlyStat> yearlyStats = new ArrayList<>();
int startYear = start.getYear(); int startYear = start.getYear();
int endYear = end.getYear(); int endYear = end.getYear();
for (int year = startYear; year <= endYear; year++) { for (int year = startYear; year <= endYear; year++) {
String yearStr = String.valueOf(year); String yearStr = String.valueOf(year);
OrderYearlyStat stat = new OrderYearlyStat(); YearlyStat stat = new YearlyStat();
stat.setYear(yearStr); stat.setYear(yearStr);
stat.setOrderCount(yearCountMap.getOrDefault(yearStr, 0L)); stat.setYearlyCount(yearCountMap.getOrDefault(yearStr, 0L));
yearlyStats.add(stat); yearlyStats.add(stat);
} }
@ -279,7 +287,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
result.setDroneModelDistribution(calculateDroneModelDistribution(orders)); result.setDroneModelDistribution(calculateDroneModelDistribution(orders));
// 5. 景区路线订单分布 // 5. 景区路线订单分布
result.setRouteOrderDistribution(calculateRouteDistribution(orders)); result.setRouteDistribution(calculateRouteDistribution(orders));
return result; return result;
} }
@ -397,21 +405,78 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
/** /**
* 计算景区内各路线的订单分布 * 计算景区内各路线的订单分布
*
* @param orders 订单列表
* @return 路线订单分布映射键为路线名称值为对应订单数量
*/ */
private Map<String, Long> calculateRouteDistribution(List<OrderMain> orders) { public List<RouteStat> calculateRouteDistribution(List<OrderMain> orders) {
Map<String, Long> routeDistribution = new HashMap<>(); // 统计各路线的订单数量
Map<Long, Long> routeCountMap = new HashMap<>();
for (OrderMain order : Optional.ofNullable(orders).orElse(Collections.emptyList())) {
if (order == null || order.getRouteIds() == null || order.getRouteIds().isEmpty()) {
continue;
}
// 遍历所有订单统计各路线的订单数量1个订单会包含多个路线 String[] routeIdArray = order.getRouteIds().split(",");
for (OrderMain order : orders) { for (String routeIdStr : routeIdArray) {
Long routeId = 0L; // TODO 这里路线可能存在多个 String trimmedRouteId = routeIdStr.trim();
String routeName = cpRouteMapper.getNameById(routeId); if (!trimmedRouteId.isEmpty()) {
if (routeName != null) { try {
routeDistribution.put(routeName, routeDistribution.getOrDefault(routeName, 0L) + 1); Long routeId = Long.parseLong(trimmedRouteId);
routeCountMap.put(routeId, routeCountMap.getOrDefault(routeId, 0L) + 1);
} catch (NumberFormatException e) {
LOG.error("路线id格式错误: {}", trimmedRouteId, e);
}
}
} }
} }
// 转换统计结果为RouteStat列表
List<RouteStat> routeDistribution = new ArrayList<>();
if (!routeCountMap.isEmpty()) {
// 提取所有路线ID
Set<Long> routeIds = routeCountMap.keySet();
// 批量查询所有路线信息仅需查询路线名称和景区ID
List<CpRoute> routes = cpRouteMapper.listRoutesByIds(new ArrayList<>(routeIds));
Map<Long, CpRoute> routeMap = routes.stream()
.collect(Collectors.toMap(CpRoute::getId, route -> route));
// 提取所有景区ID
Set<Long> scenicIds = routes.stream()
.map(CpRoute::getScenicId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询所有景区名称
Map<Long, String> scenicNameMap = emScenicMapper.getScenicNameMap(new ArrayList<>(scenicIds));
// 组装结果
for (Map.Entry<Long, Long> 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; return routeDistribution;
} }
/** /**
@ -465,7 +530,7 @@ public class OrderAnalysisServiceImpl implements OrderAnalysisService {
result.setPageInfo(new PageInfo()); result.setPageInfo(new PageInfo());
result.setOrderList(Collections.emptyList()); result.setOrderList(Collections.emptyList());
result.setDroneModelDistribution(Collections.emptyMap()); result.setDroneModelDistribution(Collections.emptyMap());
result.setRouteOrderDistribution(Collections.emptyMap()); result.setRouteDistribution(Collections.emptyList());
result.setTimeSeriesData(null); result.setTimeSeriesData(null);
return result; return result;
} }

View File

@ -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<T> analyzeAllOrderDetails(List<OrderDetail> allDetails) {
return buildOrderDetailAnalysisResult(allDetails);
}
@Override
public OrderDetailAnalysisResult<List<DailyStat>> analyzeByDayRange(LocalDate start, LocalDate end) {
// 1. 查询时间范围内的所有订单
List<OrderDetail> orders = queryOrderDetailByDateRange(start, end);
if (CollectionUtils.isEmpty(orders)) {
return buildEmptyResult();
}
// 2. 按日分组统计每日订单量
Map<String, Long> 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<DailyStat> 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<List<MonthlyStat>> analyzeByMonthRange(LocalDate start, LocalDate end) {
// 1. 查询时间范围内的所有订单
List<OrderDetail> orders = queryOrderDetailByDateRange(start, end);
if (CollectionUtils.isEmpty(orders)) {
return buildEmptyResult();
}
// 2. 按月分组统计每月订单量
Map<String, Long> 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<MonthlyStat> 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<List<QuarterlyStat>> analyzeByQuarterRange(LocalDate start, LocalDate end) {
// 1. 查询时间范围内的所有订单
List<OrderDetail> orders = queryOrderDetailByDateRange(start, end);
if (CollectionUtils.isEmpty(orders)) {
return buildEmptyResult();
}
// 2. 按季度分组统计每季度订单量
Map<String, Long> 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<QuarterlyStat> 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<List<YearlyStat>> analyzeByYearRange(LocalDate start, LocalDate end) {
// 1. 查询时间范围内的所有订单
List<OrderDetail> orders = queryOrderDetailByDateRange(start, end);
if (CollectionUtils.isEmpty(orders)) {
return buildEmptyResult();
}
// 2. 按年分组统计每年订单量
Map<String, Long> 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<YearlyStat> 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<OrderDetail> queryOrderDetailByDateRange(LocalDate start, LocalDate end) {
// MyBatis Plus 条件构造器查询 create_time [start, end] 范围内的订单
LambdaQueryWrapper<OrderDetail> 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 <T> OrderDetailAnalysisResult<T> buildAnalysisResult(List<OrderDetail> orderDetails, T timeSeriesData) {
OrderDetailAnalysisResult<T> result = buildOrderDetailAnalysisResult(orderDetails);
// 时间序列数据///年统计
result.setTimeSeriesData(timeSeriesData);
return result;
}
/**
* 处理任务分析结果
*/
private <T> OrderDetailAnalysisResult<T> buildOrderDetailAnalysisResult(List<OrderDetail> orderDetails) {
OrderDetailAnalysisResult<T> 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<OrderDetail> 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<OrderDetail> tasks, int pageNum, int pageSize) {
PageInfo pageInfo = new PageInfo();
pageInfo.setPageNum(pageNum);
pageInfo.setPageSize(pageSize);
pageInfo.setTotal((long) tasks.size());
return pageInfo;
}
/**
* 获取分页后的任务列表
*/
private List<OrderDetail> getPagedTasks(List<OrderDetail> 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 <T> List<List<T>> partitionList(List<T> 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<RouteStat> calculateRouteDistribution(List<OrderDetail> orderDetails) {
// 1. 校验输入
if (CollectionUtils.isEmpty(orderDetails)) {
return Collections.emptyList();
}
// 2. 统计各路线的任务数量基于子单的routeId字段
Map<Long, Long> routeCountMap = orderDetails.stream()
.filter(detail -> detail.getRouteId() != null) // 过滤掉无路线ID的任务
.collect(Collectors.groupingBy(
OrderDetail::getRouteId, // 按路线ID分组
Collectors.counting() // 统计每组数量
));
// 3. 批量查询路线信息名称所属景区
List<RouteStat> result = new ArrayList<>();
if (!routeCountMap.isEmpty()) {
// 提取所有路线ID
ArrayList<Long> routeIds = new ArrayList<>(routeCountMap.keySet());
// 批量查询路线信息名称景区ID
List<CpRoute> routes = cpRouteMapper.listRoutesByIds(routeIds);
Map<Long, CpRoute> routeInfoMap = routes.stream()
.collect(Collectors.toMap(CpRoute::getId, route -> route));
// 提取所有景区ID用于批量查询景区名称
Set<Long> scenicIds = routes.stream()
.map(CpRoute::getScenicId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询景区名称
Map<Long, String> scenicNameMap = emScenicMapper.getScenicNameMap(new ArrayList<>(scenicIds));
// 4. 组装结果
for (Map.Entry<Long, Long> 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 <T> OrderDetailAnalysisResult<T> buildEmptyResult() {
OrderDetailAnalysisResult<T> 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;
}
}

View File

@ -5,6 +5,7 @@ import com.aircraft.modules.route.domain.dto.CpRouteVo;
import com.aircraft.modules.route.mapper.CpRouteMapper; import com.aircraft.modules.route.mapper.CpRouteMapper;
import com.aircraft.modules.system.mapper.UserMapper; import com.aircraft.modules.system.mapper.UserMapper;
import com.aircraft.modules.route.service.CpRouteService; 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.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
@ -92,7 +93,7 @@ public class CpRouteController {
@ApiOperation(value = "查询单个路线") @ApiOperation(value = "查询单个路线")
@RequestMapping(value = "{id}", method = {RequestMethod.GET}) @RequestMapping(value = "{id}", method = {RequestMethod.GET})
@ApiImplicitParam(name = "id", value = "路线ID", required = true, paramType = "path") // 调整参数描述 @ApiImplicitParam(name = "id", value = "路线ID", required = true, paramType = "path") // 调整参数描述
public ResponseEntity<CpRoute> one(@PathVariable final Integer id) { public ResponseEntity<CpRoute> one(@PathVariable final Long id) {
try { try {
CpRoute entity = cpRouteMapper.getRouteById(id); CpRoute entity = cpRouteMapper.getRouteById(id);
if (entity == null || entity.getDelFlag() == 1) { if (entity == null || entity.getDelFlag() == 1) {
@ -239,4 +240,40 @@ public class CpRouteController {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} }
} }
@ApiOperation(value = "根据景区名称模糊查询路线")
@RequestMapping(value = "searchRoutesByName", method = RequestMethod.GET)
public ResponseEntity<List<CpRoute>> searchRoutesByName(
@ApiParam(value = "景区名称(支持模糊匹配)", required = true)
@RequestParam("areaName") String areaName) {
try {
// 1. 参数校验
if (StringUtils.isBlank(areaName)) {
LOG.warn("景区名称模糊查询失败:景区名称为空");
return ResponseEntity.badRequest().build();
}
// 2. 构建查询条件仅针对景区名称进行模糊匹配
QueryWrapper<CpRoute> queryWrapper = new QueryWrapper<>();
queryWrapper.like("area_name", areaName)
.eq("del_flag", 0);
// 3. 查询匹配的路线
List<CpRoute> 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();
}
}
} }

View File

@ -4,6 +4,9 @@ import com.aircraft.modules.route.domain.CpRoute;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.ArrayList;
import java.util.List;
/** /**
* <p> * <p>
* 路线表 Mapper 接口 * 路线表 Mapper 接口
@ -17,7 +20,9 @@ public interface CpRouteMapper extends BaseMapper<CpRoute> {
void updateDelFlagById(Long id, Integer delFlag); void updateDelFlagById(Long id, Integer delFlag);
CpRoute getRouteById(Integer id); CpRoute getRouteById(Long id);
String getNameById(Long routeId); String getNameById(Long routeId);
List<CpRoute> listRoutesByIds(ArrayList<Long> longs);
} }

View File

@ -6,7 +6,9 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import io.lettuce.core.dynamic.annotation.Param; import io.lettuce.core.dynamic.annotation.Param;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* <p> * <p>
@ -32,4 +34,6 @@ public interface EmScenicMapper extends BaseMapper<EmScenic> {
* @return * @return
*/ */
List<AreaNumStatisVo> countByAreaId(); List<AreaNumStatisVo> countByAreaId();
Map<Long, String> getScenicNameMap(ArrayList<Long> longs);
} }

View File

@ -26,4 +26,14 @@
where valid = 't' where valid = 't'
group by area_id group by area_id
</select> </select>
<select id="getScenicNameMap" resultType="java.util.HashMap">
SELECT
id AS "key",
name AS "value"
FROM em_scenic
WHERE id IN
<foreach collection="scenicIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
</mapper> </mapper>

View File

@ -6,6 +6,8 @@
<id column="id" property="id" jdbcType="BIGINT"/> <id column="id" property="id" jdbcType="BIGINT"/>
<result column="order_id" property="orderId" jdbcType="BIGINT"/> <result column="order_id" property="orderId" jdbcType="BIGINT"/>
<result column="device_id" property="deviceId" jdbcType="BIGINT"/> <result column="device_id" property="deviceId" jdbcType="BIGINT"/>
<result column="routeId" property="routeId" jdbcType="BIGINT"/>
<result column="cargo_weight" property="cargoWeight" jdbcType="DECIMAL"/>
<result column="person_count" property="personCount" jdbcType="VARCHAR"/> <result column="person_count" property="personCount" jdbcType="VARCHAR"/>
<result column="operator_id" property="operatorId" jdbcType="BIGINT"/> <result column="operator_id" property="operatorId" jdbcType="BIGINT"/>
<result column="order_item_status" property="orderItemStatus" jdbcType="INTEGER"/> <result column="order_item_status" property="orderItemStatus" jdbcType="INTEGER"/>
@ -16,12 +18,13 @@
<result column="del_flag" property="delFlag" jdbcType="INTEGER"/> <result column="del_flag" property="delFlag" jdbcType="INTEGER"/>
</resultMap> </resultMap>
<!-- 通过订单ID列表查询订单详情列表 --> <!-- 基础查询列 -->
<select id="getOrderDetailsByOrderIds" resultMap="BaseResultMap"> <sql id="Base_Column_List">
SELECT
id, id,
order_id, order_id,
device_id, device_id,
routeId,
cargo_weight,
person_count, person_count,
operator_id, operator_id,
order_item_status, order_item_status,
@ -30,30 +33,28 @@
update_by, update_by,
update_time, update_time,
del_flag del_flag
</sql>
<!-- 通过订单ID列表查询订单详情列表 -->
<select id="getOrderDetailsByOrderIds" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM fms_od_order_detail FROM fms_od_order_detail
WHERE order_id IN WHERE order_id IN
<foreach collection="orderIds" item="id" open="(" separator="," close=")"> <foreach collection="orderIds" item="id" open="(" separator="," close=")">
#{id} #{id}
</foreach> </foreach>
AND del_flag = 0
ORDER BY order_id ASC, id ASC ORDER BY order_id ASC, id ASC
</select> </select>
<!-- 通过订单id获取单个订单详情--> <!-- 通过订单id获取单个订单详情-->
<select id="getOrderDetailsByOrderId" resultMap="BaseResultMap"> <select id="getOrderDetailsByOrderId" resultMap="BaseResultMap">
SELECT id, SELECT
order_id, <include refid="Base_Column_List"/>
device_id,
person_count,
operator_id,
order_item_status,
create_by,
create_time,
update_by,
update_time,
del_flag
FROM fms_od_order_detail FROM fms_od_order_detail
WHERE order_id = #{orderId} WHERE order_id = #{orderId}
ORDER BY fms_od_order_detail.create_time ASC AND del_flag = 0
ORDER BY create_time ASC
</select> </select>
</mapper>
</mapper>

View File

@ -19,4 +19,15 @@
FROM cp_route FROM cp_route
WHERE id = #{routeId} WHERE id = #{routeId}
</select> </select>
<select id="listRoutesByIds" resultType="com.aircraft.modules.route.domain.CpRoute">
SELECT
id,
name,
scenic_id
FROM cp_route
WHERE id IN
<foreach collection="routeIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
</mapper> </mapper>